1. Linux Platform 虚拟总线驱动模型概述
在嵌入式Linux系统开发中,Platform总线是最基础也是最常用的虚拟总线之一。它解决了SoC内部外设的管理问题,为那些没有物理总线连接的设备提供了统一的设备驱动模型。
1.1 传统驱动开发的痛点
早期的Linux驱动开发存在几个明显问题:
- 硬件信息硬编码:寄存器地址、中断号等直接写在驱动代码中
- 缺乏统一管理:每个驱动各自为政,资源分配混乱
- 移植性差:硬件变更需要修改驱动源码并重新编译
我曾在一个项目中遇到过这样的问题:当客户更换了硬件平台后,我们需要为每个外设驱动修改几十处寄存器定义,工作量巨大且容易出错。
1.2 Platform总线的解决方案
Platform总线通过以下方式解决了上述问题:
- 设备与驱动分离:硬件描述归设备,控制逻辑归驱动
- 统一资源管理:总线负责协调设备和驱动
- 支持动态配置:通过设备树实现硬件描述的灵活性
2. Platform总线架构解析
2.1 核心数据结构
Platform总线模型建立在三个关键数据结构上:
2.1.1 platform_bus_type
c复制struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match, // 关键匹配函数
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
这个结构体定义了Platform总线的行为,其中最重要的是match函数,它决定了设备和驱动如何配对。
2.1.2 platform_device
c复制struct platform_device {
const char *name; // 设备名称
int id; // 设备ID
struct device dev; // 内嵌的通用设备
u32 num_resources; // 资源数量
struct resource *resource; // 资源数组
};
platform_device描述了一个具体的硬件设备,包括其名称、ID和硬件资源。
2.1.3 platform_driver
c复制struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
};
platform_driver实现了设备的控制逻辑,最重要的两个回调是probe和remove。
2.2 总线初始化流程
Platform总线的初始化发生在内核启动早期:
bash复制start_kernel()
└── rest_init()
└── kernel_init()
└── do_basic_setup()
└── driver_init()
└── platform_bus_init()
在platform_bus_init()中主要完成两项工作:
- 注册platform_bus设备
- 注册platform_bus_type总线类型
这会在sysfs中创建以下目录结构:
code复制/sys/devices/platform
/sys/bus/platform
├── devices
└── drivers
3. 平台设备(platform_device)详解
3.1 设备资源描述
平台设备通过struct resource描述硬件资源:
c复制struct resource {
resource_size_t start; // 起始地址
resource_size_t end; // 结束地址
unsigned long flags; // 资源类型标志
};
资源类型标志包括:
- IORESOURCE_MEM:内存区域
- IORESOURCE_IRQ:中断号
- IORESOURCE_IO:I/O端口
3.2 设备注册流程
设备注册的核心函数调用链:
c复制platform_device_register()
└── platform_device_add()
└── device_add()
关键操作包括:
- 设置父设备为platform_bus
- 设置总线类型为platform_bus_type
- 将资源插入全局资源树
- 添加到内核设备模型
3.3 资源获取API
驱动中常用的资源获取函数:
c复制// 获取内存资源
struct resource *platform_get_resource(
struct platform_device *dev,
unsigned int type,
unsigned int num);
// 获取中断号
int platform_get_irq(struct platform_device *dev, unsigned int num);
4. 平台驱动(platform_driver)实现
4.1 驱动基本结构
一个完整的平台驱动需要实现以下内容:
c复制static int my_probe(struct platform_device *pdev)
{
// 初始化硬件、注册设备等
return 0;
}
static int my_remove(struct platform_device *pdev)
{
// 释放资源
return 0;
}
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
4.2 设备树支持
现代Linux驱动开发推荐使用设备树描述硬件:
dts复制my_device {
compatible = "vendor,my-device";
reg = <0x12340000 0x1000>;
interrupts = <0 45 4>;
};
驱动中通过of_match_table进行匹配:
c复制static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-device" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
4.3 驱动注册流程
驱动注册的核心函数调用链:
c复制platform_driver_register()
└── __platform_driver_register()
└── driver_register()
└── bus_add_driver()
└── driver_attach()
5. 设备与驱动匹配机制
5.1 匹配触发时机
匹配可以在两种情况下触发:
- 设备注册时
- 驱动注册时
内核会确保已存在的设备和驱动能够正确配对。
5.2 五级匹配优先级
Platform总线采用五级匹配策略:
- driver_override:最高优先级,强制匹配
- 设备树匹配:通过compatible属性
- ACPI匹配:x86平台常用
- ID表匹配:通过platform_device_id
- 名称匹配:比较设备名和驱动名
匹配函数platform_match()按此顺序依次尝试。
5.3 设备树匹配细节
设备树匹配的核心是比较compatible字符串:
c复制static inline int of_driver_match_device(
struct device *dev,
const struct device_driver *drv)
{
return of_match_device(drv->of_match_table, dev) != NULL;
}
6. 实战:LED驱动开发
6.1 硬件描述
假设我们要控制一个连接在GPIO0_C7的LED:
- 寄存器基地址:0xFDD60000
- 数据寄存器偏移:0x0004
- 方向寄存器偏移:0x000C
6.2 设备树方式实现
设备树节点:
dts复制led {
compatible = "vendor,led";
reg = <0xFDD60004 0x4>, <0xFDD6000C 0x4>;
led-pin = <7>;
};
驱动实现:
c复制static int led_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *reg;
int pin;
// 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
reg = devm_ioremap_resource(&pdev->dev, res);
// 获取设备树属性
of_property_read_u32(pdev->dev.of_node, "led-pin", &pin);
// 初始化GPIO方向
// ...
return 0;
}
6.3 传统方式实现
设备定义:
c复制static struct resource led_res[] = {
DEFINE_RES_MEM(0xFDD60004, 4),
DEFINE_RES_MEM(0xFDD6000C, 4),
};
static struct platform_device led_pdev = {
.name = "led",
.id = -1,
.num_resources = ARRAY_SIZE(led_res),
.resource = led_res,
};
驱动匹配:
c复制static struct platform_driver led_drv = {
.driver = {
.name = "led",
},
.probe = led_probe,
};
7. 调试技巧与常见问题
7.1 sysfs调试
Platform设备在sysfs中的位置:
code复制/sys/devices/platform/
/sys/bus/platform/devices/
可以查看设备属性和资源信息。
7.2 常见问题排查
-
probe函数未调用:
- 检查设备树compatible是否匹配
- 确认设备已成功注册
- 查看内核日志中的匹配信息
-
资源获取失败:
- 检查设备树reg属性
- 确认资源索引正确
- 使用devm_系列函数管理资源
-
设备重复注册:
- 检查设备ID是否冲突
- 确认模块卸载时注销设备
7.3 性能优化建议
- 使用devm_系列函数自动管理资源
- 合理设计probe函数的初始化顺序
- 对于频繁操作,考虑ioremap_cached替代ioremap
8. 进阶话题
8.1 多设备支持
一个驱动可以支持多个设备:
c复制static const struct platform_device_id led_ids[] = {
{ "led-v1", 0 },
{ "led-v2", 1 },
{ }
};
在probe中通过id_entry区分设备。
8.2 延迟探测
当依赖的资源未就绪时,可以返回-EPROBE_DEFER让内核稍后重试。
8.3 电源管理
实现suspend/resume回调支持系统休眠:
c复制static int led_suspend(struct device *dev)
{
// 保存状态并关闭设备
return 0;
}
static int led_resume(struct device *dev)
{
// 恢复状态
return 0;
}
static const struct dev_pm_ops led_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(led_suspend, led_resume)
};
9. 实际项目经验分享
在最近的一个工业控制器项目中,我们使用Platform总线管理了超过20个外设。以下是一些实战经验:
- 设备树组织:将相关外设分组到子节点中,提高可读性
- 资源冲突检测:使用内核的resource管理避免地址重叠
- 模块化设计:将通用功能抽象为平台驱动,特殊功能通过platform_data扩展
一个典型的错误案例是忘记实现设备的release回调,导致内核警告:
code复制Device 'my-device' does not have a release() function
解决方法很简单:
c复制static void dummy_release(struct device *dev)
{
/* Nothing to do */
}
static struct platform_device my_device = {
.dev = {
.release = dummy_release,
},
};
10. 最佳实践总结
根据多年开发经验,我总结了Platform驱动开发的几个最佳实践:
- 优先使用设备树:提高代码的可移植性
- 资源管理自动化:多用devm_系列函数减少错误
- 完善的错误处理:检查每个可能失败的操作
- 详细的日志输出:方便调试和问题追踪
- 文档同步更新:维护好设备树绑定文档
对于复杂的驱动,建议采用分层设计:
- 底层:平台驱动处理硬件差异
- 中间层:实现核心逻辑
- 上层:提供统一用户接口
