Linux驱动开发总结速记

字符设备、通用设备、平台设备及对应驱动写法

维度 字符设备 通用设备(总线设备) 平台设备
适用场景 字节流设备(串口、按键、LED、字符终端) 外部总线设备(USB、PCI、SCSI 等) 片上外设(GPIO、I2C 控制器、SPI 控制器、定时器)
核心结构 struct cdev + struct file_operations struct device + struct device_driver struct platform_device + struct platform_driver
注册关键函数 cdev_add()device_create() device_register()driver_register() platform_device_register()platform_driver_register()
注销关键函数 cdev_del()device_destroy() device_unregister()driver_unregister() platform_device_unregister()platform_driver_unregister()
匹配方式 设备号(主设备号 + 次设备号) 总线match函数(通常匹配name或 ID) 设备树compatible属性 > id_table > 设备 / 驱动name
硬件信息传递 硬编码或手动配置(需开发者手动关联硬件资源) 总线自动传递(如 PCI 的配置空间、USB 的描述符) 设备树(主流)或struct resource(内存地址、中断号等)
总线依赖 无专用总线(依赖 VFS 层关联设备与驱动) 依赖具体总线(如 USB 总线、PCI 总线) 依赖内核内置的platform_bus(虚拟总线)
用户空间访问方式 设备文件(/dev/xxx 设备文件(由总线驱动创建设备节点) 设备文件(由平台驱动创建设备节点)
驱动与硬件解耦程度 低(硬件信息常硬编码在驱动中) 中(依赖总线规范,与具体硬件型号解耦) 高(通过设备树完全分离硬件信息与驱动逻辑)
典型内核接口 register_chrdev_region()cdev_init() bus_register()bus_unregister() platform_get_resource()of_address_to_resource()
初始化顺序影响 无严格顺序(设备号唯一即可) 需先注册总线,再注册设备和驱动 驱动可先于设备加载(设备树解析后自动匹配)
嵌入式场景适用性 简单外设适用 外部扩展设备适用 片上集成外设首选(嵌入式系统主流方式)

设备写法

类型 核心结构体 注册方式 匹配机制 典型设备
字符设备 cdev、file_operations 注册设备号 + cdev_add 设备号匹配 串口、LED
通用设备 device、device_driver 总线框架注册 总线 name / 设备树匹配 USB、PCI 设备
平台设备 platform_device/driver platform 注册函数 设备树 compatible 匹配 SOC 外设、板载芯片

驱动写法

设备类型 驱动编写核心步骤 关键函数 / 代码片段示例
字符设备 1. 定义 file_operations 结构体 2. 分配 / 初始化 cdev 3. 申请设备号 4. 注册 cdev struct file_operations fops = { .read = my_read, .write = my_write }; cdev_init(&cdev, &fops); register_chrdev_region(devno, 1, "mydev"); cdev_add(&cdev, devno, 1);
通用设备 1. 定义 device_driver 结构体 2. 实现 probe/remove 函数 3. 注册驱动 struct device_driver drv = { .name = "mydev", .bus = &my_bus_type, .probe = my_probe, .remove = my_remove }; driver_register(&drv);
平台设备 1. 定义 platform_driver 结构体 2. 实现 probe/remove 3. 注册平台驱动 struct platform_driver pdrv = { .probe = my_probe, .remove = my_remove, .driver = { .name = "myplatdev" } }; platform_driver_register(&pdrv);

例程

提要

关于设备到底怎么注册,驱动怎么写。

字符设备cdev,不一定需要真实的硬件相绑定。

低耦合度的写法,

比如想点亮某个Led灯,直接 module_init 绑定模块入口函数,然后 入口函数中使用自定义的包含 cdev的结构体,实现cdev设备的设备号申请、初始化、注册就可以(控制灯需完善寄存器地址的控制)。

一个 led_cdev.c 实现上述内容,生成 led_cdev.ko就够了。


而同时,led是具有硬件实体的,同时LED灯、RTC时钟这种设备,无物理总线。

产生了中耦合度的写法,管理无物理总线的实体设备,即平台设备和平台设备驱动。

platform_device对 device结构体进行封装。其中 device结构体被称为通用设备,可用于所有设备类型。

随之诞生的是 platform_driver,平台驱动类型。

注意,平台总线,platform_bus,是用于管理无物理总线的总线类型。

写法是,

一个 led_pdev.c,定义 platform_device并填充设备信息,入口函数中 register 注册平台设备。

一个 ldev_pdrv.c,定义 platform_driver并填充 id_tables,入口函数中 实现 .probe,创建设备类class、注册平台驱动,资源通过 resource数组获取。

但其实,哪怕你用 cdev的写法注册了设备,只要 和 platform_driver的table_id匹配,平台驱动也是能检测到设备的。


除了通过 .ko文件装载,注册设备的方式。

也能直接通过修改设备树,加载设备树去注册设备。

  1. 向设备树添加 LED 节点;

  2. 写平台设备驱动框架(含入口、注销函数及设备结构体);

  3. 实现 probe 函数完成 LED 注册与初始化;

  4. 实现字符设备 write 操作函数;

  5. 编测试应用,通过输入不同值控制 LED 亮灭。

字符设备驱动

按字节流顺序访问(如串口、按键),不支持随机存取,大都不使用缓存器。是最基础的设备类型。

驱动结构

cpp 复制代码
// 字符设备驱动核心结构
struct cdev {
    struct kobject kobj;
    struct module *owner;          // 所属模块
    const struct file_operations *ops;  // 操作函数集(read/write等)
    struct list_head list;
    dev_t dev;                     // 设备号
    unsigned int count;
};

注册流程

1、分配设备号(静态或动态)

2、初始化 cdev 并绑定 file_operations

3、注册 cdev 到内核

cpp 复制代码
// 示例代码片段
dev_t dev_num;
struct cdev my_cdev;

// 1. 分配设备号
alloc_chrdev_region(
    &dev_num,  // 用于存储分配到的设备号的指针
    0,         // 起始次设备号
    1,         // 要分配的设备数量
    "my_char_dev"  // 设备名称,将显示在/proc/devices中
);

// 2. 初始化cdev
cdev_init(
    &my_cdev,  // 要初始化的cdev结构体的指针
    &fops      // 设备文件操作集结构体的指针
);
my_cdev.owner = THIS_MODULE;

// 3. 注册cdev
cdev_add(
    &my_cdev,  // 指向已初始化的cdev结构体的指针
    dev_num,   // 设备号(包含主设备号和次设备号)
    1          // 要添加的设备数量
);
// 4. 创建设备节点(需要先创建class)
class = class_create(THIS_MODULE, "my_class");
// 创建设备节点(通常在class下)
device_create(class, parent, dev_num, NULL, "mydev");

总结

简单注册字符设备(无硬件绑定),只需要有驱动代码就够了。

无硬件绑定则不需要 probe 函数。

而如果是与硬件绑定的字符设备,则需要使用平台设备驱动的写法。

字符设备驱动代码

1、用 alloc_chrdev_region 申请设备号

2、用 cdev_init 初始化 cdev,绑定 ops

3、注册 cdev,将设备号添加到 cdev_map 散列表。

这样 insmode指令装载驱动后,/proc/devices 目录和 /dev 目录下会出现该字符设备。

一个驱动适配多个字符设备

在 open的时候,通过 MINOR(inode->devno)读取不同的子设备号,通过 FILE 的私有数据获取不同子设备号的缓冲区,在 write/read 的时候 通过 copy_from_user/ copy_to_user 将 用户缓冲区和不同的内核设备缓冲区(其实就是我们自己驱动里定义的buf) 进行数据读取就可以。

应用场景

鼠标作为字符设备,在Linux内核中通过平台设备总线进行管理。

比如想自己写一个鼠标驱动,而鼠标属于输入设备(input device),内核已提供input子系统简化驱动开发,无需从零实现 file_operations。

鼠标插入 USB 接口时,

USB 控制器会检测到硬件连接,产生中断信号。

内核的 USB 核心驱动(usbcore)会响应中断,识别设备类型(通过 USB 设备描述符),确认这是一个输入设备(鼠标)。

USB 核心驱动会为鼠标创建一个设备对象(struct device),并填充设备信息(如厂商 ID、产品 ID、设备类型等)。

平台设备驱动

嵌入式系统中用于片上外设(如 GPIO、I2C 控制器),与平台总线(platform bus)绑定,依赖设备树(Device Tree)传递硬件信息。

驱动结构

cpp 复制代码
// 平台设备驱动结构
struct platform_driver {
    int (*probe)(struct platform_device *pdev);  // 匹配成功后执行
    int (*remove)(struct platform_device *pdev); // 设备移除时执行
    struct device_driver driver;  // 继承通用设备驱动
    const struct platform_device_id *id_table;  // 支持的设备ID列表
};

// 平台设备结构
struct platform_device {
    const char *name;  // 设备名称(需与驱动匹配)
    int id;            // 设备编号
    struct device dev; // 继承通用设备
    u32 num_resources; // 资源数量(如地址、中断)
    struct resource *resource;  // 硬件资源(内存地址、中断号等)
};

注册流程

驱动注册:platform_driver_register

设备注册:通常通过设备树描述,内核自动解析为 platform_device;也可手动注册。

cpp 复制代码
// 驱动注册示例
struct platform_driver my_platform_drv = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_dev",  // 需与设备树compatible或设备name匹配
        .of_match_table = my_of_match,  // 设备树匹配表
    },
};
platform_driver_register(&my_platform_drv);

// 手动注册设备(较少用,通常用设备树)
struct resource res[] = {
    { .start = 0x12340000, .end = 0x1234ffff, .flags = IORESOURCE_MEM },
    { .start = 10, .end = 10, .flags = IORESOURCE_IRQ },
};
struct platform_device my_platform_dev = {
    .name = "my_platform_dev",
    .id = -1,
    .resource = res,
    .num_resources = ARRAY_SIZE(res),
};
platform_device_register(&my_platform_dev);

通用设备驱动

基于struct device和struct device_driver的抽象,适用于非平台总线的设备(如 USB、PCI 等外部总线设备),也就是具有实体总线的设备。

驱动结构

cpp 复制代码
// 通用设备驱动结构
struct device_driver {
    const char *name;              // 驱动名称(需与设备匹配)
    struct bus_type *bus;          // 所属总线
    int (*probe)(struct device *dev);  // 设备匹配成功后执行
    int (*remove)(struct device *dev); // 设备移除时执行
    // ... 其他成员
};

// 通用设备结构
struct device {
    struct device *parent;
    struct device_driver *driver;  // 绑定的驱动
    struct bus_type *bus;          // 所属总线
    // ... 其他成员
};

注册流程

1、注册总线(如 USB 总线已内置,无需重复注册)

2、注册设备驱动(driver_register)

3、注册设备(device_register),总线会自动匹配设备与驱动

cpp 复制代码
// 示例代码片段
struct device_driver my_drv = {
    .name = "my_generic_dev",
    .bus = &my_bus_type,  // 关联到特定总线
    .probe = my_probe,
    .remove = my_remove,
};

// 注册驱动
driver_register(&my_drv);

// 注册设备
struct device my_dev = {
    .parent = NULL,
    .bus = &my_bus_type,
    .init_name = "my_generic_dev",  // 需与驱动name匹配
};
device_register(&my_dev);

总结

应用场景

自定义总线注册流程

自定义的总线适配所有符合匹配函数的设备和驱动

cpp 复制代码
#include <linux/module.h>
#include <linux/device.h>

// 1. 定义总线匹配函数
static int my_bus_match(struct device *dev, struct device_driver *drv) {
    return !strcmp(dev_name(dev), drv->name); // 按名称匹配
}

// 2. 定义总线结构
struct bus_type my_bus_type = {
    .name = "my_custom_bus",
    .match = my_bus_match,
};
EXPORT_SYMBOL(my_bus_type); // 导出总线,供设备和驱动使用

// 3. 注册总线
static int __init my_bus_init(void) {
    int ret = bus_register(&my_bus_type);
    if (ret < 0) {
        printk(KERN_ERR "bus_register failed: %d\n", ret);
        return ret;
    }
    printk(KERN_INFO "my_custom_bus initialized\n");
    return 0;
}

// 4. 注销总线
static void __exit my_bus_exit(void) {
    bus_unregister(&my_bus_type);
    printk(KERN_INFO "my_custom_bus exited\n");
}

// 模块入口/出口
module_init(my_bus_init);
module_exit(my_bus_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Custom Bus Registration Example");
相关推荐
一张假钞20 小时前
Ubuntu SSH 免密码登陆
linux·ubuntu·ssh
Wang's Blog21 小时前
Linux小课堂: 文件操作警惕高危删除命令与深入文件链接机制
linux·运维·服务器
水月wwww1 天前
操作系统——进程管理
linux·操作系统·vim·进程·进程调度
2501_915909061 天前
iOS 混淆实战,多工具组合完成 IPA 混淆与加固(源码 + 成品 + 运维一体化方案)
android·运维·ios·小程序·uni-app·iphone·webview
我科绝伦(Huanhuan Zhou)1 天前
分享一个可以一键制作在线yum源的脚本
linux·运维
casdfxx1 天前
捡到h3开发板,做了个视频小车。
驱动开发
爱宇阳1 天前
禅道社区版 Docker Compose 服务迁移教程
运维·docker·容器
Paper_Love1 天前
Linux-查看硬件接口软件占用
linux·运维·服务器
wydaicls1 天前
Linux 系统下 ZONE 区域的划分
linux·运维·服务器
带土11 天前
17. Linux wc命令
linux