news 2026/4/3 4:53:40

Linux平台设备驱动

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux平台设备驱动

Linux内核使用总线来处理设备,总线连接了CPU与这些设备。有些总线足够智能,并内嵌了可发现性逻辑以枚举连接到总线上的设备。在引导阶段的初期,Linux内核会请求这些总线提供它们所枚举的设备以及这些设备正常工作所需的资源(如中断线和内存区域)​。PCI、USB和SATA总线都属于这种可发现性总线的范畴。但实际情况往往并不能如我们所愿。有许多设备,CPU仍然无法检测到。其中大部分不可发现的设备位于芯片内部,尽管有些设备位于慢速或低级总线上,这些总线并不支持设备发现。因此,内核必须提供机制来接收有关硬件的信息,用户必须告诉内核可以从哪里找到这些设备。在Linux内核中,这些不可发现的设备称为平台设备(platform device)。由于它们并没有位于已知的I2C、SPI总线或任何不可发现的总线上,Linux内核实现了平台总线(也称为伪平台总线)的概念,以维护“设备始终通过总线连接到CPU”的范式。

Linux内核平台核心抽象struct platform_device

name

在上述数据结构中,name表示平台设备的名称。必须谨慎选择分配给平台设备的名称。平台设备与平台驱动程序的匹配是在伪平台总线匹配函数platform_match()中进行的,在这个函数中,在某些情况下(没有设备树或ACPI支持且没有id表匹配)​,匹配会回退到名称匹配,即比较驱动程序的名称和平台设备的名称。

id

platform_device的id字段是一个整数,核心使命是:
当系统中存在多个同类型的平台设备(比如 2 个片上 UART、3 个 GPIO 控制器)时,通过id给每个设备分配唯一的 “编号”,让内核能区分它们;若只有一个该类型设备,则用特殊值(-1)表示 “无需编号”。

  1. 取值 - 1(PLATFORM_DEVID_NONE):单设备无编号
    适用场景:系统中只有一个该类型的平台设备(比如单板上只有 1 个 I2C 控制器、1 个 SPI 控制器);
    内核操作:直接将底层设备(struct device)的名称设置为platform_device.name,不加任何后缀;
    例子:
structplatform_deviceuart_dev={.name="uart",.id=PLATFORM_DEVID_NONE,// 等价于-1// 其他属性...};

内核生成的设备名称为uart,无后缀。
2. 取值 - 2(PLATFORM_DEVID_AUTO):多设备自动分配 id
适用场景:系统中有多个同类型的平台设备(比如单板上有 3 个 UART、2 个 PWM 控制器),且不想手动指定编号;
内核操作:
遍历已注册的同名称平台设备,找到最小的未使用整数 id(从 0 开始);
将该 id 分配给新设备,设备名称拼接为name.id;
例子:

// 注册第一个UART设备structplatform_deviceuart_dev1={.name="uart",.id=PLATFORM_DEVID_AUTO};// 内核分配id=0 → 设备名uart.0// 注册第二个UART设备structplatform_deviceuart_dev2={.name="uart",.id=PLATFORM_DEVID_AUTO};// 内核分配id=1 → 设备名uart.1
  1. 其他值(手动指定 id):自定义编号
    适用场景:需要明确指定编号区分同类型设备(比如按硬件位置指定:UART0 对应串口 1、UART1 对应串口 2);
    内核操作:直接用指定的 id 拼接设备名称,无需自动分配;
    注意:手动指定的 id 不能重复(否则内核会报错,设备注册失败);
    例子:
// 手动指定id=2 → 设备名uart.2structplatform_deviceuart_dev={.name="uart",.id=2};

resource和num_resources

resource是分配给平台设备的资源数组,num_resources是该数组中元素的数量。

id_entry

对于通过id表进行平台设备和驱动程序匹配的情况,pdev->id_entry将指向structplatform_device_id类型的匹配id表条目,该条目将使平台驱动程序与此平台设备匹配。无论平台设备如何注册,它们都需要由适当的驱动程序(即平台驱动程序)驱动。此类 驱动程序必须实现一组回调函数,当设备在平台总线上出现/消失时,平台核心会使用这些回调函数。

platform_driver

平台驱动程序在Linux内核中表示为platform_driver结构体实例,该结构体定义如下:

probe

· probe:这是匹配发生后设备声明驱动程序时调用的探测函数。


内核负责为platform_device提供参数。当设备驱动程序在内核中注册时,总线驱动程序将调用探测函数。

remove

· remove:当设备不再需要驱动程序时,将调用此移除函数来删除驱动程序。移除函数声明如下:

driver

设备模型的基础驱动程序结构,必须提供名称(名称必须谨慎选择)​、所有者以及其他一些字段(如设备树匹配表)​。对于平台驱动程序,在对驱动程序和设备进行匹配之前,platform_device.name和platform_driver.driver.name字段必须相同。【兜底条件】

id_table

平台驱动程序向总线代码提供的一种将实际设备绑定到驱动程序的方式。还有一种方式是通过设备树进行绑定

处理平台设备

分配和注册平台设备

由于平台设备无法自行向系统注册,因此必须手动填充并注册平台设备到系统中,同时也需要注册它们的资源和私有数据。在早期的平台核心阶段,平台设备在板级文件(如i.MX6的arch/arm/mach-imx/mach-imx6q.c)中声明,并使用platform_device_add()或platform_device_register()注册到内核中,具体使用哪一个取决于平台设备的分配方式。

静态方法

你需要在代码中枚举平台设备并在每个平台上调用platform_device_register()函数来进行注册。这个函数定义如下:

static struct platform_device my_pdev = { .name = "my_drv_name", .id = 0, .resource = jz4740_udc_resources, .num_resources = ARRAY_SIZE(jz4740_udc_resources), }; int foo() { [...] return platform_device_register(&my_pdev); }

通过静态方法,平台设备被静态初始化并传递给platform_device_register()函数。静态方法还允许批量添加平台设备,相应的函数可以使用platform_add_devices(),它接收一个指向平台设备的指针数组和该数组中元素的数量作为参数。该函数定义如下:

动态方法

调用platform_device_alloc()函数以动态分配和初始化平台设备

struct platform_device* platform_device_alloc(const char *name, int id);

其中,name是所要添加的设备的基本名称,id是平台设备的实例id。如果执行成功,此函数将返回一个有效的平台设备对象,执行失败则返回NULL。
使用动态方法分配的平台设备专门使用platform_device_add()函数向系统注册

int platform_device_add(struct platform_device *pdev);

这个函数唯一的参数是使用platform_device_alloc()函数分配的平台设备。如果执行成功,该函数返回0;如果执行失败,则返回错误码。


add 是更底层的操作,仅负责 “把设备加入内核设备模型”,不触发总线匹配,对字段的校验更基础(但核心字段仍不能为空);
register 是 add 的 “超集”(内部会调用 add),但多了 “注册到 platform 总线、触发驱动匹配” 的逻辑,校验更严格;
实际使用中,你在调用 add 前一定会先填充字段(否则 add 也会失败),而直接调用 register 时跳过了填充步骤,才会觉得 “add 可以、register 不行”。

如果使用platform_device_add()注册平台设备失败,则应该使用platform_device_put()函数释放platform_device结构体占用的内存。该函数定义如下:

status = platform_device_add(evm_led_dev); if (status < 0) { platform_device_put(evm_led_dev); [...] }

也就是说,无论平台设备以何种方式分配和注册,都必须使用platform_device_unregister()函数取消注册,该函数定义如下:


需要注意的是,platform_device_unregister()会在内部调用platform_device_put()。

使用设备树

可以把设备树里的simple-bus节点比作嵌入式系统里的 “简易硬件货架”:
这个 “货架”(simple-bus 节点)本身没有任何 “智能功能”—— 不需要专门的驱动来控制它,唯一作用就是 “摆放子设备(子节点)”;
货架上的 “商品”(子设备)没法被 “自动扫码识别”(无动态探测),只能靠预先贴好的 “位置标签”(设备树里的地址信息,如reg属性、@地址)找到;
系统启动时,内核会自动把这个货架上的所有 “第一层商品”(一级子节点),都转换成可被 platform 驱动管理的 “平台设备”(platform_device)。

用法 1:在 SoC 节点 / 片上内存映射总线下声明片上设备

SoC(芯片)内部的核心外设(如 UART、SPI 控制器、GPIO、定时器、DMA)没有独立的物理总线(比如 I2C 的硬件线路),而是通过内存映射方式访问 —— 即这些外设的控制寄存器直接对应到 CPU 的物理内存地址(写某个内存地址 = 操作外设寄存器)。这类外设无法归到 I2C/SPI 等物理总线,因此放在兼容simple-bus的 SoC 节点下统一管理。

在上面的例子中,只有bar和foz节点被注册为平台设备,baz节点不会被注册(其直接父节点在compatible字符串中没有simple-bus)​。因此,只要有任何平台驱动程序的compatible匹配表中包含company、product和/或labcsmart、something,这些平台设备就将被探测。

用法 2:声明调节器设备

常见的一种用法是在SoC节点或片上内存映射总线下声明芯片上的设备。另一种用法是声明调节器设备,如下所示:

使用平台资源

可热插拔设备可枚举并广告所需的资源,与之相反,内核并不知道系统中的平台设备是什么、它们具备什么功能或者它们需要什么资源才能正常工作。由于缺乏自动协商过程,为内核提供给定平台设备所需资源的任何信息都是受欢迎的。这些资源可以是IRQ线、DMA通道、内存区域、I/O端口等。资源在代码中表示为resource结构体实例,该结构体定义在include/linux/ioport.h文件中,如下所示:

在上述数据结构中,start/end指向资源的开始/结束。它们指示I/O或内存区域的开始和终止位置。因为IRQ线、总线和DMA通道没有范围,所以通常为start和end赋予相同的值。flags是描述资源类型的掩码,如IORESOURCE_BUS。flags可能的值如下。
· IORESOURCE_IO表示PCI/ISA I/O端口。
· IORESOURCE_MEM表示内存区域。
· IORESOURCE_REG表示寄存器偏移量。
· IORESOURCE_IRQ表示IRQ线。
· IORESOURCE_DMA表示DMA通道。
· IORESOURCE_BUS表示总线。
最后,用name字段标识或描述资源,因此我们可以通过name字段来提取资源。将这些资源分配给平台设备的方式有两种,第一种是在同一编译单元中声明和注册平台设备时进行分配,第二种是从设备树中进行分配。

旧方法

旧方法主要在不支持设备树的内核中使用,主要用于多功能设备,其中的主芯片(封装芯片)与子设备共享资源。资源的提供方式与平台设备相同。

资源分配
static struct resource foo_resources[] = { [0] = { .start = 0x10000000, .end = 0x10001000, .flags = IORESOURCE_MEM, .name = "mem1", }, [1] = { .start = JZ4740_UDC_BASE_ADDR2, .end = JZ4740_UDC_BASE_ADDR2 + 0x10000 - 1, .flags = IORESOURCE_MEM, .name = "mem2", }, [2] = { .start = 90, .end = 90, .flags = IORESOURCE_IRQ, .name = "mc-irq", }, };

上面的代码展示了3个资源(两个内存类型的资源和一个IRQ类型的资源)​,它们的类型可以用IORESOURCE_IRQ和IORESOURCE_MEM进行标识。第1个是4KB的内存区域;第2个也是一个内存区域,其范围由一个宏定义;最后一个是IRQ 90。

对于静态分配的平台设备,资源应该按以下方式分配:

static struct platform_device foo_pdev = { .name = "foo-device", .resource = foo_resources, .num_resources = ARRAY_SIZE(foo_resources), [...] };

对于动态分配的平台设备,资源的分配是在一个函数中完成的,分配方式如下:

struct platform_device *foo_pdev; [...] foo_pdev = platform_device_alloc("foo-device", ...); if (!foo_pdev) return -ENOMEM; foo_pdev->resource = foo_resources; foo_pdev->num_resources = ARRAY_SIZE(foo_resources);
资源获取


参数n表示需要哪种类型的资源,若为0,则表示第一个MMIO区域。例如,驱动程序可以通过以下代码找到它的第二个MMIO区域:

平台数据platform_data

如果struct resource不足以编码信息,作为扩展可以使用platform_device.device.platform_data。
数据可以包含在更大的结构体中,并赋值给这个额外字段platform_data。
以下代码描述了额外的平台数据,这些数据对应一组指向平台设备类型私有函数的指针:

对于静态分配的平台设备,执行以下命令:

如果发现平台设备是动态分配的,则使用platform_device_add_data()函数分配数据,该函数定义如下:

上述函数如果执行成功,则返回0。参数data是用作平台数据的数据,size是数据的大小。

在平台驱动程序中,平台设备的pdev->dev.platform_data字段将指向平台数据。虽然我们可以引用该字段,但建议使用内核提供的dev_get_platdata()函数,该函数定义如下:

为了获取包含函数结构的集合,驱动程序可以执行以下操作:

新方法

随着设备树的出现,事情变得更加简单了。为了保持兼容性,设备树中指定的内存区域、中断和DMA资源被转换为resource结构体实例,以便平台核心可以通过platform_get_resource()、platform_get_resource_by_name()或platform_get_irq()返回合适的资源,无论这个资源是以传统方式获取的还是从设备树中获取的,都没有影响。

设备树的局限:驱动往往需要「定制化的平台数据」—— 这是驱动开发者自己定义的结构体(比如struct led_platform_data),设备树是纯静态的硬件描述文件,不知道这个结构体的字段、大小、含义,因此无法直接按这个结构体格式提供数据;
举例:设备树能告诉驱动 “LED 的 GPIO 引脚是 10”,但没法直接告诉驱动 “LED 默认闪烁频率是 500ms、最大亮度是 100”(这些是驱动专属的逻辑参数,对应自定义结构体)。
为了传递这样的额外数据,驱动可以使用 of_device_id 中的.data 字段,以使平台设备和驱动程序匹配。然后,.data 字段可以指向平台数据”
核心解决方案:of_device_id是设备树和驱动的「匹配规则表」,其中的.data字段是一个无类型指针(void*),可以指向驱动自定义的平台数据结构体;
工作逻辑:
驱动定义of_device_id数组,每个条目包含compatible(匹配设备树节点的标识)和.data(指向该设备对应的定制化平台数据);
内核通过compatible匹配设备树节点(平台设备)和驱动;
匹配成功后,驱动可以拿到.data字段指向的平台数据,补充设备树无法传递的定制化参数。

如果驱动程序期望以传统方式接收平台数据,则应该检查platform_device->dev.platform_data指针。如果该指针有非空值,则意味着设备是以传统方式实例化的,同时与平台数据一起使用,而没有使用设备树。在这种情况下,平台数据应像往常一样使用。然而,如果设备是从设备树代码实例化的,则platform_data指针将为NULL,表示必须直接从设备树获取信息。在这种情况下,驱动程序将在平台设备的dev.of_node字段中找到一个device_node指针,然后便可以使用各种设备树访问例程[特别是of_get_property()]从设备树中提取所需的数据。

平台驱动程序抽象和架构

探测和释放设备

平台驱动程序的入口点是探测函数,该函数在与平台设备匹配后会被调用。探测函数原型如下:

无论平台设备以传统方式实例化还是从设备树中创建,都可以使用传统的平台核心API[如platform_get_resource()、platform_get_resource_by_name()、platform_get_irq()等类似的API]来提取其资源。

在探测函数中,必须请求驱动程序所需的任何资源(如GPIO、时钟、IIO通道等)和数据。如果需要进行映射,则可以在探测函数中执行此操作。

移除设备

当从系统中移除设备或注销平台驱动程序时,必须撤销探测函数已完成的所有操作。移除函数就是用来实现这一目的的,移除函数原型如下:

移除函数只有在所有资源被释放和清理完毕后才应返回0,否则应返回适当的错误码以通知用户。移除函数的参数包括之前传递给探测函数的同一平台设备。

配置platform_device_id(平台设备id匹配方式)

驱动程序本身是无用的,要使其对设备有用,就必须告诉内核驱动程序可以管理哪些设备。为此,必须提供一个id表,并将它分配给platform_driver.id_table字段。这将允许平台设备匹配。但是,为了将此功能扩展到模块自动加载,就必须将同样的id表提供给MODULE_DEVICE_TABLE,以便生成模块别名。

在上述数据结构中,name是设备的名称;driver_data是驱动程序的状态值,可以设置为指向每个设备数据结构的指针。下面是一个来自drivers/mmc/host/mxs-mmc.c的例子:

当把mxs_ssp_ids赋值给platform_driver.id_table字段时,平台设备将能够根据它们的名称与任意platform_device_id.name表项进行匹配,platform_device. id_entry将指向此表中触发匹配的表项。

#include <linux/platform_device.h> #include <linux/module.h> // 1. 定义驱动侧的匹配清单(mxs_ssp_ids) static const struct platform_device_id mxs_ssp_ids[] = { // 表项1:匹配name="mxs-ssp.0"的设备,附加数据100 { .name = "mxs-ssp.0", .driver_data = 100 }, // 表项2:匹配name="mxs-ssp.1"的设备,附加数据200 { .name = "mxs-ssp.1", .driver_data = 200 }, { } // 哨兵项,结束数组 }; // 告诉内核这是平台设备ID表(必须) MODULE_DEVICE_TABLE(platform, mxs_ssp_ids); // 2. 定义probe函数,使用id_entry获取匹配项 static int mxs_ssp_probe(struct platform_device *pdev) { // 匹配成功后,pdev->id_entry指向mxs_ssp_ids中匹配的表项 if (!pdev->id_entry) { pr_err("无匹配的id_entry!\n"); return -ENODEV; } // 打印匹配结果:设备名称、匹配的表项名称、附加数据 pr_info("设备name:%s\n", pdev->name); pr_info("匹配的id_table表项name:%s\n", pdev->id_entry->name); pr_info("匹配表项的附加数据:%lu\n", pdev->id_entry->driver_data); // 后续硬件初始化逻辑... return 0; } // 3. 定义platform驱动结构体,赋值id_table static struct platform_driver mxs_ssp_driver = { .probe = mxs_ssp_probe, .id_table = mxs_ssp_ids, // 核心:把匹配清单赋值给id_table .driver = { .name = "mxs-ssp-driver", }, }; // 4. 注册驱动 module_platform_driver(mxs_ssp_driver); MODULE_LICENSE("GPL");

配置of_device_id(设备树匹配方式)

为了允许通过设备树中声明的兼容字符串匹配平台设备,平台驱动程序必须使用structof_device_id的元素列表设置platform_driver.driver.of_match_table。然后,为了允许从设备树匹配自动加载模块,必须将设备树匹配表提供给MODULE_DEVICE_ TABLE。以下是一个示例:

如果在驱动程序中设置了of_device_id,则匹配是根据任意of_device_id.compatible元素与设备节点中compatible属性的值是否匹配来判断的。要获得导致匹配的of_device_id表项,驱动程序应调用of_match_device(),将设备树匹配表和底层设备结构platform_ device.dev作为参数传递。

为什么需要of_match_device()?对比之前的platform_device_id匹配:内核会自动把platform_device.id_entry指向匹配的platform_device_id表项;但of_device_id匹配没有这个 “自动赋值” 的字段,驱动必须主动调用of_match_device()才能拿到 “到底是哪一个of_device_id表项匹配了当前设备”。

#include <linux/platform_device.h> #include <linux/of.h> #include <linux/module.h> // 1. 定义驱动专属的平台数据(通过of_device_id.data传递) struct ssp_platform_data { int clock_freq; // SSP时钟频率 bool dma_enable; // 是否启用DMA }; // 2. 定义不同版本SSP设备的平台数据 static struct ssp_platform_data ssp_v1_data = { .clock_freq = 10000000, // 10MHz .dma_enable = false, }; static struct ssp_platform_data ssp_v2_data = { .clock_freq = 20000000, // 20MHz .dma_enable = true, }; // 3. 定义of_device_id匹配表(关联compatible和.data) static const struct of_device_id mxs_ssp_of_match[] = { { .compatible = "fsl,mxs-ssp-v1", .data = &ssp_v1_data, // 绑定v1版本的平台数据 }, { .compatible = "fsl,mxs-ssp-v2", .data = &ssp_v2_data, // 绑定v2版本的平台数据 }, { } // 哨兵项 }; MODULE_DEVICE_TABLE(of, mxs_ssp_of_match); // 4. probe函数中调用of_match_device()获取匹配表项 static int mxs_ssp_probe(struct platform_device *pdev) { // 核心:调用of_match_device()获取匹配的of_device_id表项 const struct of_device_id *match = of_match_device(mxs_ssp_of_match, &pdev->dev); if (!match) { pr_err("未找到匹配的of_device_id表项!\n"); return -ENODEV; } // 从匹配表项中获取附加数据(.data字段) struct ssp_platform_data *pdata = (struct ssp_platform_data *)match->data; if (!pdata) { pr_err("无平台数据!\n"); return -EINVAL; } // 打印匹配结果和附加数据 pr_info("匹配成功!兼容标识:%s\n", match->compatible); pr_info("SSP时钟频率:%d Hz,DMA启用:%d\n", pdata->clock_freq, pdata->dma_enable); // 后续硬件初始化逻辑(根据平台数据配置SSP)... return 0; } // 5. 定义platform驱动结构体 static struct platform_driver mxs_ssp_driver = { .probe = mxs_ssp_probe, .driver = { .name = "mxs-ssp-driver", .of_match_table = mxs_ssp_of_match, // 关联设备树匹配表 }, }; // 6. 注册驱动 module_platform_driver(mxs_ssp_driver); MODULE_LICENSE("GPL");

在定义了这些匹配表之后,可以将它们赋值给平台驱动程序数据结构,代码如下:

驱动程序初始化和注册

向内核注册平台驱动程序很简单,只需要在模块的初始化函数中调用platform_driver_register()或platform_driver_probe()函数即可。
必须调用platform_driver_unregister()函数来注销平台驱动程序。

platform_driver_register和platform_driver_probe函数的区别

· platform_driver_register()函数用于将驱动程序注册并放入内核维护的驱动程序列表中,这意味着可以按需调用其探测函数,前提是与平台设备的匹配发生了新的变化。使用platform_driver_register()函数注册的任何平台驱动程序都必须使用platform_driver_unregister()函数来注销。

· 若使用platform_driver_probe()函数,内核将立即运行匹配循环以检查是否有平台设备可以与该平台驱动程序匹配,并为每个匹配的设备调用其探测函数。如果此时没有找到设备,则简单地忽略该平台驱动程序。这种方法可以防止延迟探测,因为不会在系统中注册驱动程序。探测函数被放置在__init部分,当内核启动完成时,这部分将被释放(前提是驱动程序被编译为静态模块)​,从而避免了延迟探测,减少了驱动程序占用的内存。如果完全确定设备在系统中,请使用这种方法。

核心区别在驱动生命周期:register让驱动常驻内核(支持后续新设备),probe让驱动 “一次性工作”(不常驻、省内存);


如果已知设备不支持热插拔,则可以将探测函数放置在__init部分。
在模块加载完成后立即注册平台驱动程序是正确的选择。注销平台驱动程序亦如此,必须在模块的卸载路径中完成。注销平台驱动程序的适当位置是module_exit()函数。

#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/platform_device.h> static int my_pdrv_probe(struct platform_device *pdev) { pr_info("Device probed\n"); return 0; } static void my_pdrv_remove(struct platform_device *pdev) { pr_info("Device remove\n"); } static struct platform_driver mypdrv = { .probe = my_pdrv_probe, .remove = my_pdrv_remove, [...] }; static int __init foo_init(void) { [...] return platform_driver_register(&mypdrv); } module_init(foo_init); static void __exit foo_cleanup(void) { [...] platform_driver_unregister(&mypdrv); } module_exit(foo_cleanup);

module_init()和module_exit()可以使用module_platform_driver()宏代替

从零编写平台驱动

现在,让我们想象一个平台设备,它是内存映射设备,其映射的内存范围控制在从地址0x02008000开始,大小为0x4000。然后,假设该平台设备可以在任务完成时向CPU发送中断,并且中断线号为31。

在设备树中实例化平台设备

要将一个节点注册为平台设备,该节点的直接父节点就必须在其兼容性字符串列表中包含“simple-bus”​,这也是下面所要实现的内容:

demo { compatible = "simple-bus"; demo_pdev: demo_pdev@0 { compatible = "labcsmart,demo-pdev"; reg = <0x02008000 0x4000>; interrupts = <0 31 IRQ_TYPE_LEVEL_HIGH>; }; };

设备树节点命名中@xxx的部分叫单元地址(Unit Address),它的核心作用是「区分同一父节点下同名的子节点」,而非直接对应reg里的物理地址 —— 这是新手最容易混淆的点。

  1. 单元地址的核心规则
    单元地址的值由父节点的#address-cells(地址单元格数)和#size-cells(大小单元格数)决定,是父节点视角下的 “逻辑地址”,而非硬件的物理地址;
    你的示例中父节点demo只标注了compatible = “simple-bus”,但未显式定义#address-cells和#size-cells—— 内核对simple-bus的默认处理是#address-cells = <1>、#size-cells = <1>,但单元地址的取值仅用于区分节点,不要求和reg的物理地址一致;
    @0只是一个 “逻辑标识”:表示这是父节点demo下第一个(逻辑地址 0)demo_pdev类型的节点。如果父节点下有第二个demo_pdev节点,就可以写demo_pdev@1,和reg的物理地址无关。

传统方式实例化平台设备

#include <linux/module.h> #include <linux/init.h> #include <linux/platform_device.h> #define DEV_BASE 0x02008000 #define PDEV_IRQ 31 static struct resource pdev_resource[] = { [0] = { .start = DEV_BASE, .end = DEV_BASE + 0x4000, .flags = IORESOURCE_MEM, }, [1] = { .start = PDEV_IRQ, .end = PDEV_IRQ, .flags = IORESOURCE_IRQ, }, }; struct platform_device demo_pdev = { .name = "demo_pdev", .id = 0, .num_resources = ARRAY_SIZE(pdev_resource), .resource = pdev_resource, /* .dev = { .platform_data = (void *)&big_struct_1, } /* };

注册平台设备

static int demo_pdev_init() { return platform_device_register(&demo_pdev); } module_init(demo_pdev_init); static void demo_pdev_exit() { platform_device_unregister(&demo_pdev); } module_exit(demo_pdev_exit);

编写设备树匹配表和id表

#include <linux/module.h> #include <linux/init.h> #include <linux/interrupt.h> #include <linux/of_platform.h> #include <linux/platform_device.h> static const struct of_device_id labcsmart_dt_ids[] = { { .compatible = "labcsmart,demo-pdev", /*.data = (void *)&big_struct_1, */ }, { .compatible = "labcsmart,other-pdev", /*.data = (void *)&big_struct_2, */ }, {/*sentinel*/} }; MODULE_DEVICE_TABLE(of, labcsmart_dt_ids);

在上述代码中,设备树匹配表枚举了支持的设备,并根据它们的compatible属性进行了区分。这里我们再次注释掉了.data赋值语句,这只是为了展示我们如何根据与平台设备匹配的条目,传递特定于平台的数据结构。这个特定于平台的数据结构可以是big_struct_1或big_struct_2。当需要传递平台数据但仍想使用设备树时,建议使用这种方式。

static const struct platform_device_id labcsmart_ids[] = { { .name = "demo-pdev", /*.driver_data = &big_struct_1,*/ }, { .name = "other-pdev", /*.driver_data = &big_struct_2,*/ }, {/*sentinel*/} }; MODULE_DEVICE_TABLE(platform, labcsmart_ids);

实现探测函数

static u32 *reg_base; static struct resource *res_irq; static int demo_pdev_probe(struct platform_device *pdev) { struct resource *regs; regs = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!regs) { dev_err(&pdev->dev, "coiuld not get IO memory\n"); return -ENXIO; } reg_base = devm_ioremap(&pdev->dev, regs->start, resource_size(regs)); if (!reg_base) { dev_err(&pdev->dev, "could not remap memory\n"); return 0; } res_irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0); devm_request_irq(&pdev->dev, res_irq->start, top_half, IRQF_TRIGGER_FALLING, "demo-pdev", NULL); [...] return 0; }

在上面的探测函数中,可以添加许多内容或以不同的方式执行操作。其中一种情况是,如果驱动程序只与设备树兼容,并且相关的平台设备也从设备树进行实例化,则执行以下操作:

struct device_node *np = pdev->dev.of_node; const struct of_device_id *of_id = of_match_device(labcsmart_dt_ids, &pdev->dev); struct big_struct *pdata_struct = (big_struct*)of_id->data;

在上述代码中,我们获取了与平台设备相关联的设备树节点的引用,然后可以使用任何与设备树相关的API[如of_get_property()]从中提取数据。接下来,在设备树匹配表中为支持的设备提供特定于平台的数据结构,使用of_match_device()指向对应的条目,并提取特定平台的数据。如果匹配是通过id表发生的,pdev->id_entry将指向导致匹配发生的条目,pdev->id_entry->driver_data将指向适当的大型数据结构。


需要特别提醒你的是,探测函数专门使用devm_前缀函数处理资源。这些函数会在适当的时机释放资源。这意味着不需要移除函数。
如果不用devm,移除函数一般这么写

在上述代码中,IRQ资源被释放,内存映射被销毁。

初始化和注册平台驱动

static struct platform_driver demo_driver = { .probe = demo_pdrv_probe, /* .remove = demo_pdrv_remove, */ .driver = { .owner = THIS_MODULE, .name = "demo_pdev", } }; module_platform_driver(demo_driver);

还有一个重要的问题需要考虑:平台设备、平台驱动程序和设备树中设备节点的名称。它们都相同,名为demo_pdev。这是一种提供匹配机会的方式,即通过平台设备和平台驱动程序名称进行匹配,因为当设备树、id表和ACPI匹配全部失败时,名称匹配将被用作后备选项。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/1 1:03:59

35、Linux信号处理:基础与应用

Linux信号处理:基础与应用 1. 特定信号介绍 在Linux系统中,有一些特定信号具有独特的触发条件和用途: - SIGVTALRM :当使用 ITIMER_VIRTUAL 标志创建的定时器到期时, setitimer() 函数会发送此信号。 - SIGWINCH :当终端窗口大小改变时,内核会为前台进程组中…

作者头像 李华
网站建设 2026/4/1 23:13:17

41、系统编程技术全解析:从GCC扩展到多领域知识融合

系统编程技术全解析:从GCC扩展到多领域知识融合 1. GCC扩展特性 在编程实践中,GCC编译器提供了一些独特的扩展特性,极大地增强了代码的灵活性和效率。 1.1 范围 case 语句 GCC允许在 switch 语句中使用范围 case ,这一特性在处理连续值范围时非常实用。例如: …

作者头像 李华
网站建设 2026/4/1 2:00:12

10、构建 Linux 防火墙与无线接入点的综合指南

构建 Linux 防火墙与无线接入点的综合指南 1. 单主机防火墙设置 在网络安全中,单主机防火墙的设置至关重要。以下几个内核参数的调整能增强主机安全性: - net.ipv4.conf.all.send_redirects = 0 :只有路由器才需要此功能,其他设备可将其关闭。 - net.ipv4.conf.all.…

作者头像 李华
网站建设 2026/4/1 20:11:13

26、跨平台远程图形桌面使用指南

跨平台远程图形桌面使用指南 1. 使用 VNC 进行远程 Linux 到 Linux 管理 在 Linux 系统中,可以使用 VNC 来控制其他 Linux 计算机。下面以 TightVNC 为例,介绍具体的操作步骤: 1. 安装 VNC 服务器和客户端 :在两台 Linux 计算机上安装 TightVNC 服务器和客户端。 2. …

作者头像 李华
网站建设 2026/3/20 9:08:10

大模型赋能网安(七):安全数据基座——当大模型遇见杀伤链与ATTCK

文章目录 概要 整体架构流程 技术名词解释 技术细节 小结 概要 提示:这里可以添加技术概要 在网络安全运营中心(SOC)的日常工作中,最大的痛点并非缺乏数据,而是缺乏对数据的“认知”。传统的安全设备每天生成数以万计的离散告警(Alerts),如防火墙拒绝、异常进程…

作者头像 李华