Linux 设备驱动概述
Linux 设备驱动是内核与硬件交互的核心桥梁,负责屏蔽硬件细节、提供统一操作接口。其以内核模块为主要存在形式,支持动态加载 / 卸载,核心功能涵盖硬件初始化、中断处理、电源管理及数据传输,是嵌入式 Linux 系统实现硬件控制的关键组件。
一.Device Tree
2.1 设备树的核心价值
设备树是种硬件描述语言,早年用于 PowerPC,后来因 ARM 设备的问题普及。
早期 ARM 设备多且硬件配置乱(不同厂商、型号的外设地址、中断号都不同)。传统 Linux 得给每款设备写专属 BSP,把硬件信息硬编码进内核,导致内核臃肿、编译慢、维护难。
设备树则把硬件描述抽成 DTS/dtsi 文件,编译成 DTB 后由 bootloader 加载。内核读 DTB 就知硬件配置,不用改源码就能适配不同设备。
现在它成了 ARM、RISC-V 的标准,让同一内核兼容多硬件,移植维护更简单。
2.2 设备树四大核心要素(含 dtsi)
设备树的完整工作流依赖四大关键要素,其中dtsi是实现硬件描述复用的核心,具体如下:
|-------|-----------------------------|----------|-------------------------------------------|--------------|
| 要素   | 全称                         | 格式 / 类型 | 核心作用                                     | 文件扩展名       |
| DTS  | Device Tree Source         | 文本文件    | 描述特定单板的硬件细节(如板级外设、引脚配置)                  | .dts        |
| DTSI | Device Tree Source Include | 文本文件    | 存放多个 DTS 共享的通用硬件描述(如 SOC 内核、通用外设),实现代码复用 | .dtsi       |
| DTC  | Device Tree Compiler       | 编译工具    | 将 DTS(含引用的 dtsi)编译为内核可解析的二进制文件           | 无(工具命令为dtc) |
| DTB  | Device Tree Blob           | 二进制文件   | 内核启动时解析的硬件描述文件,由 DTS 经 DTC 编译生成          | .dtb        |
关键关系链路
通用硬件描述(dtsi) → 被特定单板描述(DTS)引用 → 经DTC编译 → 生成内核可解析文件(DTB) → 内核启动时加载解析,获取硬件信息。
2.3 设备树基本结构
一个典型的 DTS 文件结构如下:
/dts-v1/;
#include "skeleton.dtsi"  // 包含公共定义
/ {
    model = "My Development Board";
    compatible = "myboard,rev1";
    
    #address-cells = <1>;
    #size-cells = <1>;
    
    memory {
        reg = <0x80000000 0x20000000>;  // 起始地址和大小
    };
    
    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0";
    };
    
    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        compatible = "simple-bus";
        ranges;
        
        uart0: serial@12340000 {
            compatible = "ns16550a";
            reg = <0x12340000 0x100>;
            interrupts = <5 0>;
            clock-frequency = <1843200>;
        };
        
        i2c0: i2c@12350000 {
            compatible = "nxp,pca9548";
            reg = <0x12350000 0x100>;
            #address-cells = <1>;
            #size-cells = <0>;
            /* I2C设备 */
        };
    };
};
        主要元素说明:
/:根节点compatible:兼容性字符串,用于内核匹配相应的设备驱动reg:寄存器地址信息,格式为<地址 长度>interrupts:中断信息#address-cells和#size-cells:指定子节点 reg 属性的地址和长度字段数量
2.4设备树在驱动中的应用
在设备驱动中,可以通过以下函数获取设备树中的信息:
// 获取属性值
ssize_t of_property_read_string(struct device_node *np,
                              const char *propname,
                              const char **out_string);
// 读取整数属性
int of_property_read_u32(struct device_node *np,
                        const char *propname,
                        u32 *out_value);
// 获取reg属性
int of_address_to_resource(struct device_node *dev,
                          int index,
                          struct resource *r);
// 匹配compatible字符串
const struct of_device_id *of_match_device(const struct of_device_id *matches,
                                          struct device *dev);
        platform平台驱动中使用设备树匹配的示例:
static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "myvendor,mydevice" },
    { /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_driver_of_match);
static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-driver",
        .of_match_table = my_driver_of_match,
    },
};
module_platform_driver(my_driver);
        2.5 api函数
二.字符设备驱动框架
2.1 字符设备注册顺序
1.驱动模块加载与删除 (module_init 和 module_exit) - > 2.编写模块加载的函数->3.给字符设备注册主设备号与次设备号->4.初始化一个字符设备cdev并且将其添加进linux系统->5.创建一个抽象类 ->创建设备节点(使得设备能够自动在/dev目录下生成)
2.2 api函数
1.驱动模块加载:module_init(function)
2.驱动模块删除:module_exit(function)
3.字符设备号
(1)手动:
MKDEV(ma,mi) + register_chrdev_region(dev_t from, unsigned count, const char *name)
注:其中ma为主设备号 mi为次设备号
(2)自动,但得确定次设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
(3)注销:unregister_chrdev_region**(devid,unsigned num)****;**
4.初始化字符设备
初始化:void cdev_init(struct cdev *cdev, const struct file_operations *fops)
删除:void cdev_del(struct cdev *p)
5.字符设备
添加:int cdev_add(struct cdev *p, dev_t dev, unsigned count)
6.类
创建:struct class *class_create (struct module *owner, const char *name)
删除:void class_destroy(struct class *cls);
7.设备
创建:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
销毁: void device_destroy(struct class *class, dev_t devt)
2.3 示例
            
            
              cpp
              
              
            
          
          #define GPIOLED_CNT         1
#define GPIOLED_NAME        "gpioled"
#define LEDON               1
#define LEDOFF              0
 
struct gpioled_dev{
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node  *nd;
    int led_gpio;
};
static int __init led_init(void)
{
    int ret;
    if(gpioled.major){
        gpioled.devid = MKDEV(gpioled.major,0);
        register_chrdev_region(gpioled.devid,GPIOLED_CNT,GPIOLED_NAME);
    }else{
        alloc_chrdev_region(&gpioled.devid,0,GPIOLED_CNT,GPIOLED_NAME);
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }
    printk("major = %d,minor = %d",gpioled.major,gpioled.minor);
    
    gpioled.cdev.owner = THIS_MODULE;
    cdev_init(&gpioled.cdev,&gpioled_fops);
    cdev_add(&gpioled.cdev,gpioled.devid,GPIOLED_CNT);
    
    gpioled.class = class_create(THIS_MODULE,GPIOLED_NAME);
    if(IS_ERR(gpioled.class)){
        return PTR_ERR(gpioled.class);
    }
    gpioled.device = device_create(gpioled.class,NULL,gpioled.devid,NULL,GPIOLED_NAME);
    if(IS_ERR(gpioled.device)){
        return PTR_ERR(gpioled.device);
    }
    return 0;
}
static void __exit led_exit(void)
{
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid,GPIOLED_CNT);
    device_destroy(gpioled.class,gpioled.devid);
    class_destroy(gpioled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("GARY");
        三.操作gpio
3.1
3.2
3.3
3.4
四.input子系统
五linux中断
5.1 设备树编写
先观察dtsi里面的intc中断控制节点
            
            
              cpp
              
              
            
          
          intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
        发现它有三个cell :
第一个cell对应的是中断类型,0 :SPI 中断,1 :PPI 中断。
第二个cell对应的是中断号 ,对于spi(0~987) 对于ppi(0~15)
第三个cell对应的是中断类型,1:上升沿触发 2:下降沿触发 4:高电平触发 5:低电平触发
接着,举一个例子
            
            
              cpp
              
              
            
          
          fxls8471@1e {
 compatible = "fsl,fxls8471";
 reg = <0x1e>;
 position = <0>;
 interrupt-parent = <&gpio5>;
 interrupts = <0 8>;
};
        可以知道它是对应gpio5,低电平触发的中断
5.2 中断的顺序
1.获取中断号(可从设备树或者使用的gpio_to_irq获取)->2.注册中断->3,编写中断复位函数
5.3