4. 传统板级代码方式:在 board-xxx.c 中静态注册 platform_device,理解其局限性
在设备树(Device Tree)普及之前,ARM Linux 内核通过板级文件(board-xxx.c)硬编码来描述硬件资源。每个开发板对应一个 C 源文件,开发者在其中静态定义 platform_device 结构体,并调用 platform_device_register() 将其注册到内核中。这种方式虽然直观,但随着 SoC 和板卡种类的爆炸式增长,其维护成本和代码冗余问题日益突出。
4.1 传统板级代码的工作流程
板级文件的核心任务是将硬件资源(内存地址、中断号、DMA 通道等)以 C 结构体的形式固化在源码中,并在系统初始化时注册为 platform_device。其典型流程如下:
- 定义资源数组:使用
struct resource描述设备的 I/O 地址范围、中断号等。 - 定义 platform_device 结构体:填充设备名称、ID、资源指针及数量。
- 注册设备:在板级初始化函数中调用
platform_device_register()或platform_add_devices()。 - 驱动匹配:驱动通过
platform_driver_register()注册,内核根据name字段进行匹配。
关键点: 传统方式下,设备与驱动的匹配完全依赖
name 字段的一致性,硬件资源描述与内核代码深度耦合。
4.2 代码示例:在 board-xxx.c 中静态注册 platform_device
以下是一个典型的板级文件片段,演示如何为 SPI 控制器静态注册 platform_device:
/* arch/arm/mach-example/board-demo.c */
#include <linux/platform_device.h>
#include <linux/spi/spi.h>
/* 1. 定义 SPI 控制器的硬件资源 */
static struct resource spi_resources[] = {
[0] = {
.start = 0x1000A000,
.end = 0x1000AFFF,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_SPI0,
.end = IRQ_SPI0,
.flags = IORESOURCE_IRQ,
},
};
/* 2. 定义 platform_device 结构体 */
static struct platform_device spi_device = {
.name = "example-spi",
.id = 0,
.num_resources = ARRAY_SIZE(spi_resources),
.resource = spi_resources,
};
/* 3. 定义平台数据(可选) */
static struct spi_board_info demo_spi_devices[] = {
{
.modalias = "m25p80",
.max_speed_hz = 10000000,
.bus_num = 0,
.chip_select = 0,
.mode = SPI_MODE_0,
},
};
/* 4. 板级初始化函数 */
static int __init demo_board_init(void)
{
platform_device_register(&spi_device);
spi_register_board_info(demo_spi_devices,
ARRAY_SIZE(demo_spi_devices));
return 0;
}
arch_initcall(demo_board_init);
4.3 传统板级代码的局限性分析
随着内核支持板卡数量的增加,板级文件方式暴露出以下严重问题:
| 局限性 | 具体表现 | 后果 |
|---|---|---|
| 代码冗余 | 同一 SoC 的不同板卡需要重复定义相同的资源结构体 | 内核源码体积膨胀,维护困难 |
| 硬编码耦合 | 硬件地址、中断号直接写在 C 代码中,修改需重新编译内核 | 无法支持同一内核镜像在不同板卡上运行 |
| 命名冲突 | 不同板卡可能使用相同的 name 字段,导致驱动匹配混乱 |
调试困难,易引入难以发现的 Bug |
| 缺乏可扩展性 | 新增板卡需要新建 board-xxx.c 并修改 Kconfig/Makefile | 开发效率低,社区维护成本高 |
| 二进制兼容性差 | 硬件资源变更必须重新编译内核,无法提供通用内核镜像 | 不利于产品化与版本管理 |
避坑提示: 在传统板级代码中,如果
platform_device.name 与驱动中的 id_table 或 driver.name 不匹配,设备将永远不会被驱动绑定。这种错误在编译时无法发现,只能在运行时通过调试内核日志定位。
4.4 传统方式与设备树方式的对比
为了更直观地理解传统方式的不足,下面将其与设备树方式进行对比:
| 对比维度 | 传统板级代码 | 设备树方式 |
|---|---|---|
| 硬件描述载体 | C 语言结构体(.c 文件) | 文本文件(.dts / .dtsi) |
| 资源修改方式 | 修改 C 代码 → 重新编译内核 | 修改 DTS → 编译 DTB → 替换 DTB 文件 |
| 多板卡支持 | 每个板卡一个 board-xxx.c | 同一内核镜像 + 不同 DTB 文件 |
| 驱动匹配依据 | name 字段字符串匹配 |
compatible 属性 + 设备树节点 |
| 社区主流支持 | ARM 32 位旧平台(逐渐淘汰) | ARM64、RISC-V、x86 等主流架构 |
4.5 传统板级代码的匹配流程示意图
下图展示了传统方式下,板级文件、platform_device、platform_driver 三者之间的静态注册与匹配关系:
4.6 从传统方式到设备树的演进动因
传统板级代码的局限性直接推动了设备树(Device Tree)在 Linux 内核中的全面采用。以下是几个关键的演进动因:
- 减少内核源码冗余:设备树将硬件描述从内核源码中剥离,同一 SoC 的多个板卡只需维护不同的 DTS 文件,而非多个 board-xxx.c。
- 支持通用内核镜像:通过 DTB 文件传递硬件信息,同一内核镜像可以在不同硬件平台上运行,只需更换 DTB 即可。
- 简化驱动开发:驱动开发者无需关心板级文件中的资源定义,只需解析设备树节点中的
reg、interrupts等属性。 - 社区标准化:设备树成为 ARM64、RISC-V 等架构的标准硬件描述方式,新平台不再接受板级文件方式。
学习建议: 虽然传统板级代码在主流内核中已被设备树取代,但理解其工作原理对于掌握 platform 设备与驱动的匹配机制仍然非常重要。许多遗留的嵌入式项目仍在使用旧版本内核,熟悉 board-xxx.c 有助于维护和移植工作。