字符设备、通用设备、平台设备及对应驱动写法
维度 | 字符设备 | 通用设备(总线设备) | 平台设备 |
---|---|---|---|
适用场景 | 字节流设备(串口、按键、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文件装载,注册设备的方式。
也能直接通过修改设备树,加载设备树去注册设备。
-
向设备树添加 LED 节点;
-
写平台设备驱动框架(含入口、注销函数及设备结构体);
-
实现 probe 函数完成 LED 注册与初始化;
-
实现字符设备 write 操作函数;
-
编测试应用,通过输入不同值控制 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");