Linux平台设备
笔记学习整理基于野火鲁班猫教程并且添加自己学习后理解的内容然后还有ai的一些总结。如果有说的不好或者不对的地方希望大家指正!!!
首先设备模型包括了总线-设备-驱动三个大部分。这个模型告诉我们设备的信息与设备的操作应该是分开的。我们可以知道常见的总线类型有i2c,spi等。但是如果对于控制一个led来说,应该是什么总线?但是驱动开发的标准应该是要符合设备模型的。所以平台设备就是用来级解决这样的问题。平台设备模型有平台总线-平台设备-平台驱动。控制Led的方法可以作为平台驱动,led就可以作为平台设备。
先简单介绍一下结构体,他们和设备模型的结构一样或者是继承。
平台总线:
内核中使用bus_type来抽象描述系统中的总线,平台总线结构体原型如下所示:

内核用platform_bus_type来描述平台总线,该总线在linux内核启动的时候自动进行注册。

上图是linux内核自动注册平台总线的简略代码。
对于bus_type结构体之前设备模型有讲。里面的成员,name是总线名字,match是匹配方法,用于匹配总线上的设备和驱动,代码下图。

![]()
下面一些东西可能还没见到不要紧后面我会讲。我们可以看到这里调用了to_platform_device()和to_platform_driver()宏。实际上里面就是containerof函数,找到我需要的platform_device和platform_driver结构体。后面会讲,因为其实平台设备和平台驱动结构体的成员有最基础的设备和驱动对应的结构体,可以说是一种继承关系。
然后第一个if是驱动覆盖匹配,这是最高优先级的匹配方式,用于 “强制让设备绑定到指定驱动”。如果platform_device的driver_override字段被设置(比如用户手动指定了驱动名),则直接比较driver_override和驱动的.driver.name是否一致。
第二个if是设备树匹配,这是现代嵌入式系统最常用的匹配方式(优先级仅次于driver_override),基于设备树的compatible属性匹配。调用of_driver_match_device函数,比较platform_device对应的设备树节点的compatible字符串,和驱动的of_match_table中的compatible是否一致。
第三个if是ACPI 匹配。基于 ACPI(高级配置与电源接口)的匹配方式,主要用于 x86 / 服务器平台。我不会哈哈我觉得不太重要。
第四个if是id_table匹配,基于驱动的id_table(平台设备 ID 表)匹配。如果驱动定义了id_table(如led_pdev_ids),则调用platform_match_id,比较platform_device的.name和id_table中的.name是否有一致的条目。这个是以前没有设备树的情况下使用的,比较传统。
最后的return是兜底操作,直接比较设备的.name和驱动的.driver.name是否一致,一致则匹配成功。
匹配操作优先级:驱动覆盖(driver_override) > 设备树(OF) > ACPI > ID表(id_table) > 驱动名兜底。
平台驱动:
内核中使用platform_driver结构体来描述平台驱动,结构体原型如下所示:

probe: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化。
remove: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是probe函数实现操作的逆过程。
driver: Linux设备模型中用于抽象驱动的device_driver结构体,platform_driver继承该结构体,也就获取了设备模型驱动对象的特性;
id_table: 表示该驱动能够兼容的设备类型。这个可以用于上述说到的设备与驱动匹配中的id表匹配。
platform_device_id结构体原型如下所示:

Name字符数组就相当于字符串,存的是驱动“支持”的设备名,到时候设备实现热插拔测试的时候可以通过idtable来进行设备与驱动的匹配。但是前提是这个设备的名称与idtable中的name字段有匹配符合的。然后driverdata主要是存储设备的一些配置或者啥的,到时候需要用就方便可以减少冗余代码。
例如这样的代码:
// 1. 定义配置结构体(复杂参数)
struct led_config {
int gpio;
int freq;
u32 reg_base;
};
// 2. 预定义每个设备的配置(扩展时只加这里,不用改probe)
static struct led_config led_cfg_red = {.gpio=1, .freq=1, .reg_base=0x12340000};
static struct led_config led_cfg_green = {.gpio=2, .freq=2, .reg_base=0x12350000};
static struct led_config led_cfg_blue = {.gpio=3, .freq=3, .reg_base=0x12360000};
// 3. ID表中用driver_data绑定配置指针
static struct platform_device_id led_pdev_ids[] = {
{"led_red", (kernel_ulong_t)&led_cfg_red},
{"led_green", (kernel_ulong_t)&led_cfg_green},
{"led_blue", (kernel_ulong_t)&led_cfg_blue},
{},
};
// 4. probe中直接取配置(逻辑极简,无需if-else)
static int led_probe(struct platform_device *pdev) {
const struct platform_device_id *id = platform_get_device_id(pdev);
struct led_config *cfg = (struct led_config *)id->driver_data;
// 直接使用配置参数(不管多少设备,这几行代码就够了)
printk("GPIO:%d,频率:%d,基地址:0x%x\n", cfg->gpio, cfg->freq, cfg->reg_base);
return 0;
}
可以看到platform_get_device_id(pdev);就是获取平台驱动的成员idtable的。然后有driverdata就可以直接方便拿取设备数据。你会发现,使用idtable来对驱动与设备进行匹配的话,驱动开发者要很清楚这个驱动可以匹配什么设备,毕竟idtalble的name字段要写,所以需要遵循相应开发手册之类的。
当我们初始化了platform_driver之后,通过platform_driver_register()函数来注册我们的平台驱动,该函数原型如下:

函数参数和返回值如下:
参数: drv: platform_driver类型结构体指针
返回值:
成功: 0
失败: 负数
由于platform_driver继承了driver结构体,结合Linux设备模型的知识, 当成功注册了一个平台驱动后,就会在/sys/bus/platform/driver目录下生成一个新的目录项。
当卸载的驱动模块时,需要注销掉已注册的平台驱动,platform_driver_unregister()函数用于注销已注册的平台驱动,该函数原型如下:

参数: drv: platform_driver类型结构体指针
返回值: 无
上面所讲的内容是最基本的平台驱动框架,只需要实现probe函数、remove函数,初始化platform_driver结构体,并调用platform_driver_register进行注册即可。
平台设备:
内核使用platform_device结构体来描述平台设备,结构体原型如下:

name: 设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致;
id: 指定设备的编号,Linux支持同名的设备,而同名设备之间则是通过该编号进行区分;
dev: Linux设备模型中的device结构体,linux内核大量使用了面向对象思想,platform_device通过继承该结构体可复用它的相关代码,方便内核管理平台设备;
num_resources: 记录资源的个数,当结构体成员resource存放的是数组时,需要记录resource数组的个数,内核提供了宏定义ARRAY_SIZE用于计算数组的个数;
resource: 平台设备提供给驱动的资源,如irq,dma,内存等等。该结构体会在接下来的内容进行讲解;
id_entry: 这个是和平台驱动结构体的idtable一样的结构体类型,但是这个平台设备的identry一般不主动定义。一般是平台设备的name与平台驱动的idtable匹配成之后,让平台设备的identry指向它匹配成功的那一个idtable中的某一条条目。是这样的,毕竟一个驱动可能支持多个设备,但是我们之前的platform device id结构体是只能填写一个支持设备,所以通过驱动侧会使用一个platform device id结构体的数组把这些支持的设备条目都存起来,赋值给平台驱动的idtable。所以匹配成功后identry就会指向某一个对应条目了。
代码例如:
// 驱动侧:实际定义platform_device_id的结构体数据
static struct platform_device_id led_pdev_ids[] = {
{"led_red", LED_TYPE_RED}, // 结构体1
{"led_green", LED_TYPE_GREEN}, // 结构体2
{}, // 结束符
};
// 驱动侧:将数组指针赋值给id_table
static struct platform_driver led_pdrv = {
.id_table = led_pdev_ids, // 指向上面的数组
.probe = led_probe,
/* 其他成员... */
};
// 设备侧:只定义了name,没有任何platform_device_id的结构体
static struct platform_device rled_pdev = {
.name = "led_red", // 和驱动侧id_table中的name一致
.id = 0,
/* 其他硬件描述成员... */
};
加载设备和驱动后,发现ledred是匹配的,然后内核把设备侧的id_entry指针,指向驱动侧led_pdev_ids中的{"led_red", LED_TYPE_RED}条目。之前我们说platform device id结构体的driverdata可以存一些设备配置。我们就可以直接使用,例如:
static int led_probe(struct platform_device *pdev) {
// 设备侧的id_entry指向驱动侧的led_pdev_ids[0]
const struct platform_device_id *id = pdev->id_entry;
if (!id) return -ENODEV;
// 拿到驱动侧为该设备准备的配置
kernel_ulong_t cfg = id->driver_data;
...
}
设备信息:
平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。
硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源、IO口等等
软件信息:以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等
我们可以看到平台设备的结构体中有一个结构体struct resource,对于硬件信息,使用结构体struct resource来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下:

name: 指定资源的名字,可以设置为NULL;
start、end: 指定资源的起始地址以及结束地址
flags: 用于指定该资源的类型,在Linux中,资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型,最常见的有以下几种:

设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口,主要有IO端口映射和IO內存映射两种方式。 对应于IO端口映射方式,只能通过专门的接口函数(如inb、outb)才能访问,不要问为啥,这是一种规矩; 采用IO内存映射的方式,可以像访问内存一样,去读写寄存器。在嵌入式中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM。
在资源结构体的start和end中,对于IORESOURCE_IO或者是IORESOURCE_MEM,他们表示要使用的内存的起始位置以及结束位置; 若是只用一个中断引脚或者是一个通道,则它们的start和end成员值必须是相等的。这个资源结构体还可以配合设备树使用,这个以后再讲。
在内核源码/include/linux/ioport.h中,提供了宏定义DEFINE_RES_MEM、DEFINE_RES_IO、DEFINE_RES_IRQ和DEFINE_RES_DMA 。例如DEFINE_RES_MEM根据传入的起始地址_start和长度_size,自动计算结束地址,并将资源类型标记为内存资源。
(这里我觉得说的不好)而对于软件信息,这种特殊信息需要我们以私有数据的形式进行封装保存,我们注意到platform_device结构体中, 有个device结构体类型的成员dev。在前面章节,我们提到过Linux设备模型使用device结构体来抽象物理设备, 该结构体的成员platform_data可用于保存设备的私有数据。platform_data是void *类型的万能指针, 无论你想要提供的是什么内容,只需要把数据的地址赋值给platform_data即可, 还是以GPIO引脚号为例,示例代码如下:

将保存了GPIO引脚号的变量pin地址赋值给platform_data指针,在驱动程序中probe函数可以调用dev_get_platdata函数,可以获取到我们需要的引脚号。
这多说一点,我们看device结构体的时候会发现有两个void*的指针叫platformdata和driverdata,那platformdata就是我们设备提供的一些设备信息可以存里面,driverdata可以通过dev_set_drvdata函数来存一些驱动初始化设备后动态创建的数据。所以platformdata是设备给驱动,driverdata是驱动给设备。
当我们定义并初始化好platform_device结构体后,需要把它注册、挂载到平台设备总线上。注册平台设备需要使用platform_device_register()函数,该函数原型如下:

函数参数和返回值如下:
参数: pdev: platform_device类型结构体指针
返回值:
成功: 0
失败: 负数
同样,当需要注销、移除某个平台设备时,我们需要使用platform_device_unregister函数,来通知平台设备总线去移除该设备。

函数参数和返回值如下:
参数: pdev: platform_device类型结构体指针
返回值: 无
Idtable匹配设备和驱动:
讲完了平台设备与驱动,我们看一下使用idtable是怎么匹配设备与驱动的。Bustype结构体的成员match存储了匹配设备和驱动的方法,然后用idtable匹配的话,又会调用platform_match_id 函数,如下图:

实际上就是那平台设备pdev的name成员与驱动的idtable(此处叫id)的成员的name字段匹配。假设下图是平台驱动idtable赋值的对象:

请注意,platform_match_id 函数的id形参传的已经是平台驱动的idtable了。则platform_match_id 函数中的id->name[0]表示 “当前platform_device_id条目name字符串的第一个字符,对于有效条目(如"led_red"),name[0]是'l'(非 0),循环继续。

整体匹配流程大致如上图。
假如总线匹配设备与驱动的时候之前的一切都不起作用,那就会兜底,直接比较设备的.name和驱动的.driver.name是否一致。

然后我像讲一下带devm_前缀的函数,例如devm_kzalloc。
当使用devm_kzalloc分配内存,且驱动与设备匹配成功后移除设备,内核会自动释放这块 devm 管理的内存,无需手动调用kfree—— 这是devm_*系列函数(设备资源管理函数)的核心特性。
一、设备移除时的具体流程(devm 内存的生命周期)
1、设备移除触发资源释放:
当设备被移除(如执行platform_device_unregister、物理拔插设备、卸载驱动),内核会触发struct device的析构流程,调用设备的release回调函数。
2、遍历 devm 资源链表释放内存:
devm_kzalloc分配的内存会被内核记录在pdev->dev(设备的struct device)的资源链表中。当设备析构时,内核会遍历这个链表,自动调用kfree释放所有通过devm_*分配的内存(包括devm_kzalloc、devm_ioremap、devm_request_irq等)。
3、无内存泄漏风险:
即使驱动中没有手动释放内存的代码,devm 机制也能保证内存被正确回收,避免传统kzalloc+kfree可能出现的内存泄漏问题。
二、两种常见的 “设备移除” 场景及表现
场景 1:卸载驱动(rmmod)
驱动卸载时,内核会先解绑设备与驱动(调用驱动的remove函数),再触发设备的资源释放流程,自动释放devm_kzalloc的内存。
可通过dmesg或内存检测工具(如slabtop)验证:内存使用量会恢复到驱动加载前的状态。
场景 2:注销平台设备(platform_device_unregister)
设备注销时,内核会先调用驱动的remove函数(若已绑定),再遍历设备的 devm 资源链表,释放所有 devm 分配的内存。
此时pdev->dev的资源链表会被清空,对应的内存块被内核回收。
devm_kzalloc分配的内存与pdev->dev强绑定,只有当pdev->dev的生命周期结束时(设备被移除 / 注销),内存才会被释放。如果只是驱动的probe函数执行失败(返回非 0),内核也会自动释放该次probe中通过devm_*分配的内存,避免资源残留。
6884

被折叠的 条评论
为什么被折叠?



