一、回顾
在前面的章节中,我们学习了如何编写一个字符设备驱动,比如 05_char_device/mychardev.c,大致流程是这样的:
static int __init mychardev_init(void)
{
// 1. 申请设备号
alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
// 2. 初始化并注册 cdev
cdev_init(&cdev_test, &cdev_fops_test);
cdev_add(&cdev_test, dev_num, 1);
// 3. 创建设备类和设备节点
class_test = class_create(THIS_MODULE, CLASS_NAME);
device_create(class_test, NULL, dev_num, NULL, DEV_NAME);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
这段代码能跑,但有一个致命问题:
硬件信息和驱动逻辑混在一起了!
比如,如果我们要操作 GPIO、I2C、SPI 等硬件,我们需要在驱动里写死寄存器地址、中断号等信息:
#define GPIO_BASE_ADDR 0x12340000 // 硬件地址写死
#define IRQ_NUM 56 // 中断号写死2
问题来了:
- 如果换一个芯片,GPIO 地址变了,需要去修改驱动代码。
- 如果想在不重新编译驱动的情况下修改硬件参数,无法做到。
这显然不符合 Linux 内核"驱动与硬件信息分离"的设计哲学。
二、总线-设备-驱动模型
Linux 内核为了解决上述问题,设计了一套总线模型(Bus-Device-Driver Model):
设备(Device):描述硬件资源
- "我是一个 GPIO 控制器"
- "我的寄存器基地址是 0x12340000"
- "我的中断号是 56"
驱动(Driver):实现硬件操作逻辑
- "我能控制 GPIO 设备"
- "只要告诉我地址和中断号,我就能工作"
总线(Bus):负责匹配设备和驱动
- "compatible 字符串一样,匹配成功!"
- "驱动,这是你要的设备,去工作吧!"
三、什么是Platform总线
Linux 内核中有很多种总线类型:
| 总线类型 | 对应硬件 |
|---|---|
i2c_bus_type | I2C 总线设备 |
spi_bus_type | SPI 总线设备 |
usb_bus_type | USB 总线设备 |
platform_bus_type | 伪总线(Platform 总线) |
Platform 总线是一种"伪总线",它不是真实的物理总线(不像 I2C、SPI 那样有具体的硬件总线协议),而是 Linux 内核虚拟出来的一种总线,专门用于管理那些无法归类到其他总线的设备。
哪些设备会用到 Platform 总线?
几乎所有直接挂在 SoC 芯片上的片内外设都使用 Platform 总线:
GPIO控制器UART串口控制器RTC实时时钟Watchdog看门狗ADC/DAC模数转换器PWM脉宽调制器DMA控制器
为什么它们用 Platform 总线?
因为它们没有真实的总线连接,直接通过 内存映射寄存器(Memory-Mapped I/O) 访问,不需要复杂的总线协议。
四、Platform总线的工作流程
1、设备树定义节点
需要在设备树文件中编写描述硬件的信息:
/ {
mychardev@0 {
compatible = "lckfb,mychardev"; // 厂商名,设备名
reg = <0x12340000 0x1000>; // 寄存器地址范围
interrupts = <56>; // 中断号
dev-id = <0xFFA8>; // 设备ID
status = "okay"; // 设备状态
};
};2
3
4
5
6
7
8
9
2、解析设备树
这一步是内核自动完成的,我们不需要写任何代码!
内核启动时,会自动将设备树中的节点转换为 struct platform_device 结构体,并注册到 Platform 总线上:
- 读取设备树文件(
.dtb) - 解析设备树中的每一个节点
- 自动为每个节点创建对应的
struct platform_device结构体 - 自动调用
platform_device_register()注册到 Platform 总线
// 内核会根据设备树自动创建这个结构体
struct platform_device my_pdev = {
.name = "mychardev",
.id = 0,
.resource = {
// 内核把设备树中的 reg = <0x12340000 0x1000> 转换成这个
{
.start = 0x12340000, // 寄存器起始地址
.end = 0x12340FFF, // 寄存器结束地址(0x12340000 + 0x1000 - 1)
.flags = IORESOURCE_MEM, // 资源类型:内存
},
// 内核把设备树中的 interrupts = <56> 转换成这个
{
.start = 56, // 中断号
.flags = IORESOURCE_IRQ, // 资源类型:中断
},
},
// 设备树中的 dev-id = <0xFFA8> 也会被保存,可以通过 of_property_read_u32() 读取
};
// 内核会自动调用这个函数把设备注册到 Platform 总线上
platform_device_register(&my_pdev);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
内核帮我们做了:
- 把
reg = <0x12340000 0x1000>转换成内存资源(IORESOURCE_MEM) - 把
interrupts = <56>转换成中断资源(IORESOURCE_IRQ) - 把
dev-id = <0xFFA8>保存为设备树属性,驱动可以读取
3、驱动注册时与设备匹配
我们编写的 Platform 驱动 会向内核注册:
下面的代码是我们在驱动里需要编写的!
static struct platform_driver my_driver = {
.probe = my_probe, // 设备匹配成功后调用的函数
.remove = my_remove, // 设备移除时调用的函数
.driver = {
.name = "mychardev", // 驱动名称
.of_match_table = of_match_ptr(my_of_match), // 设备树匹配表
},
};
// 定义匹配表
static const struct of_device_id my_of_match[] = {
{ .compatible = "lckfb,mychardev" }, // 匹配设备树中 compatible = "lckfb,mychardev" 的节点
{ /* 结束符,必须有 */ }
};
// 注册驱动到内核
module_platform_driver(my_driver);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4、总线匹配并调用probe
当内核发现:
- 设备的
compatible = "lckfb,mychardev"(设备树来的参数)** - 驱动的
of_match_table中也有"lckfb,mychardev"(驱动程序来的参数)**
Platform 总线就会:
- 调用驱动的
probe()函数 - 把匹配到的
platform_device指针传递给probe() - 驱动在
probe()中获取硬件资源并初始化设备
五、我们需要编写的框架
我们需要做以下的事情
- 定义匹配表(匹配设备树中的
compatible字段,如果匹配成功则调用该驱动)
static const struct of_device_id my_of_match[] = {
{ .compatible = "lckfb,mychardev" }, // 必须和设备树中的 compatible 完全一致!
{ /* 结束符 */ }
};
MODULE_DEVICE_TABLE(of, my_of_match); // 将匹配表导出给内核2
3
4
5
- 实现
probe函数(设备匹配成功后会自动调用)
static int my_probe(struct platform_device *pdev)
{
struct resource *res;
u32 dev_id;
int irq;
// 从 platform_device 中获取寄存器地址资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
return -ENODEV;
}
// 从 platform_device 中获取中断号
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
return irq;
}
// 从设备树中读取自定义属性 dev-id
if (of_property_read_u32(pdev->dev.of_node, "dev-id", &dev_id)) {
return -EINVAL;
}
// 申请字符设备号.......
// 初始化 cdev.......
// 创建设备节点.......
// ioremap 映射寄存器地址.......
// 注册中断处理函数.......
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
关键函数说明:
- 使用
platform_get_resource()获取寄存器地址 - 使用
platform_get_irq()获取中断号 - 使用
of_property_read_u32()读取自定义属性
- 实现
remove函数(设备移除或驱动卸载时调用)
static int my_remove(struct platform_device *pdev)
{
// 释放资源:
// 1. 注销字符设备
// 2. 释放中断
// 3. iounmap 取消地址映射
return 0;
}2
3
4
5
6
7
8
9
- 定义
platform_driver结构体
static struct platform_driver my_driver = {
.probe = my_probe, // 匹配成功后调用
.remove = my_remove, // 移除时调用
.driver = {
.name = "mychardev", // 驱动名称
.of_match_table = my_of_match, // 指定匹配表
},
};2
3
4
5
6
7
8
- 注册驱动(自动实现
init和exit)
module_platform_driver(my_driver);六、为什么要用Platform驱动
相比传统的字符设备驱动,Platform 驱动:
| 传统字符设备驱动 | Platform 驱动 | |
|---|---|---|
| 硬件信息 | 写死在驱动代码里 | 在设备树中描述 |
| 代码复用性 | 差(地址变了要改代码) | 强(只需改设备树) |
| 多设备支持 | 需要复制粘贴代码 | 一个驱动自动支持多设备 |
| 初始化函数 | module_init() | probe()(自动调用) |
| 设备匹配 | 无 | Platform 总线自动匹配 |
| 获取硬件资源 | 手动定义宏 | platform_get_resource() |
| 标准性 | 不规范 | Linux 内核标准做法 |