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。其典型流程如下:

  1. 定义资源数组:使用 struct resource 描述设备的 I/O 地址范围、中断号等。
  2. 定义 platform_device 结构体:填充设备名称、ID、资源指针及数量。
  3. 注册设备:在板级初始化函数中调用 platform_device_register() 或 platform_add_devices()。
  4. 驱动匹配:驱动通过 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 三者之间的静态注册与匹配关系:

传统板级代码:platform_device 静态注册与匹配流程 board-xxx.c 静态定义 platform_device platform_device name = "example-spi" platform_driver driver.name = "example-spi" 注册 name 匹配 struct resource .start = 0x1000A000 .end = 0x1000AFFF .flags = IORESOURCE_MEM 资源指针 匹配成功 → probe() ⚠ 局限性说明 • 硬件资源硬编码在 C 文件中,修改需重新编译整个内核。 • 每个板卡需要独立的 board-xxx.c,代码重复且难以维护。

4.6 从传统方式到设备树的演进动因

传统板级代码的局限性直接推动了设备树(Device Tree)在 Linux 内核中的全面采用。以下是几个关键的演进动因:

  • 减少内核源码冗余:设备树将硬件描述从内核源码中剥离,同一 SoC 的多个板卡只需维护不同的 DTS 文件,而非多个 board-xxx.c。
  • 支持通用内核镜像:通过 DTB 文件传递硬件信息,同一内核镜像可以在不同硬件平台上运行,只需更换 DTB 即可。
  • 简化驱动开发:驱动开发者无需关心板级文件中的资源定义,只需解析设备树节点中的 reg、interrupts 等属性。
  • 社区标准化:设备树成为 ARM64、RISC-V 等架构的标准硬件描述方式,新平台不再接受板级文件方式。
学习建议: 虽然传统板级代码在主流内核中已被设备树取代,但理解其工作原理对于掌握 platform 设备与驱动的匹配机制仍然非常重要。许多遗留的嵌入式项目仍在使用旧版本内核,熟悉 board-xxx.c 有助于维护和移植工作。

4.7 本章小结