1. 从硬件编码到设备树的演进之路
深夜两点,示波器的荧光映在脸上,我盯着串口调试终端里不断刷新的错误信息,第37次按下复位键。硬件手册上白纸黑字写着中断号42,但驱动里读出来的却是0。这种场景对于嵌入式Linux开发者来说再熟悉不过——硬件描述被硬编码在内核源码中,任何细微的变动都需要重新编译整个内核。
这就是传统平台设备驱动最令人头疼的问题。在早期的Linux内核中,硬件资源(如寄存器地址、中断号、DMA通道等)都是直接以C结构体的形式写在板级支持包(BSP)中。以我最近调试的这个串口设备为例,老式的写法是这样的:
c复制static struct resource serial_res[] = {
{
.start = 0xFE00A000,
.end = 0xFE00AFFF,
.flags = IORESOURCE_MEM,
},
{
.start = 42, // 中断号
.end = 42,
.flags = IORESOURCE_IRQ,
}
};
这种方式的弊端显而易见:当硬件设计变更时(在嵌入式开发中这几乎是必然的),开发者必须修改内核源码,重新编译并部署整个内核。更糟糕的是,同一款SoC的不同板级设计需要在内核中维护多套几乎相同的代码,导致内核源码树越来越臃肿。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 平台设备驱动:传统方式的深入解析
虽然设备树已成为现代Linux内核的标准配置,但理解传统的平台设备驱动机制仍然很有必要。这不仅因为大量遗留代码仍在使用这种方式,更因为设备树在底层仍然构建在平台设备的概念之上。
平台设备驱动的核心是两个数据结构:platform_device和platform_driver。前者描述硬件资源,后者实现驱动逻辑。它们的匹配是通过名称字符串完成的:
c复制static struct platform_device my_device = {
.name = "my_platform_device",
.id = -1,
.resource = mydev_res,
.num_resources = ARRAY_SIZE(mydev_res),
};
static struct platform_driver my_driver = {
.driver = {
.name = "my_platform_device",
},
.probe = mydev_probe,
.remove = mydev_remove,
};
在驱动初始化时,内核会遍历所有已注册的platform_device,与platform_driver进行名称匹配。匹配成功后,调用驱动的probe函数,并将对应的platform_device作为参数传入。
提示:即使在设备树体系中,内核在启动时也会将设备树节点转换为
platform_device,因此理解这一机制对调试设备树相关问题很有帮助。
