Day 12–13 · Platform Driver 模型¶
预计时长:2 小时 / 天,共 4 小时
类型:理论 + 代码阅读
做什么¶
理解嵌入式 Linux 驱动的核心组织模型:platform_device + platform_driver 的匹配机制。重点是 probe 函数的调用时机、资源获取方式、以及 devm_ 系列的 managed resource 思想。
要了解什么¶
1. 为什么需要 Platform Driver?¶
字符设备驱动有个问题:驱动和硬件是硬编码绑定的(写死寄存器地址、中断号)。如果换一块板子,就得改驱动代码。
Platform Driver 把这两件事分开: - 平台设备(platform_device):描述硬件资源(寄存器基地址、中断号、时钟等),由 Device Tree 或板级代码提供 - 平台驱动(platform_driver):只写操控硬件的逻辑,不包含具体地址
内核在两者之间做匹配(match),匹配成功后调用驱动的 probe 函数。
2. 匹配机制¶
static const struct of_device_id my_of_match[] = {
{ .compatible = "myvendor,my-peripheral" }, // 匹配 DTS 中的 compatible
{ .compatible = "myvendor,my-peripheral-v2", .data = &v2_config },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match); // 让 modprobe 知道这个模块匹配哪些设备
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-peripheral",
.of_match_table = my_of_match,
},
};
匹配顺序(优先级递减):
1. DTS 的 compatible 属性 vs of_match_table
2. platform_device.name vs platform_driver.driver.name
3. probe 函数的职责¶
probe 是驱动最核心的函数,在设备和驱动成功匹配后被调用。它需要完成:
static int my_probe(struct platform_device *pdev)
{
struct my_priv *priv;
struct resource *res;
int irq, ret;
/* 1. 分配私有数据(用 devm_,让内核管理生命周期) */
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv) return -ENOMEM;
/* 2. 获取并映射寄存器(iomem) */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 第 0 个 MEM 资源
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base)) return PTR_ERR(priv->base);
/* 3. 获取中断号 */
irq = platform_get_irq(pdev, 0);
if (irq < 0) return irq;
/* 4. 请求中断 */
ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
IRQF_SHARED, dev_name(&pdev->dev), priv);
if (ret) return ret;
/* 5. 获取时钟、复位等其他资源 */
priv->clk = devm_clk_get(&pdev->dev, "apb_pclk");
if (IS_ERR(priv->clk)) return PTR_ERR(priv->clk);
/* 6. 使能时钟,初始化硬件 */
ret = clk_prepare_enable(priv->clk);
if (ret) return ret;
/* 7. 保存私有数据到设备 */
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "probe success\n");
return 0;
}
4. devm_ 系列:Managed Resources 思想¶
devm_(device-managed)函数分配的资源由内核跟踪,在 remove 调用或 probe 失败时自动释放,不需要手动 kfree/iounmap/free_irq。
// 传统方式(需要手动释放,容易内存泄漏)
priv = kzalloc(sizeof(*priv), GFP_KERNEL);
priv->base = ioremap(res->start, resource_size(res));
request_irq(irq, handler, 0, name, priv);
// remove 里必须手动:kfree + iounmap + free_irq
// devm 方式(自动释放,强烈推荐)
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
priv->base = devm_ioremap_resource(&pdev->dev, res);
devm_request_irq(&pdev->dev, irq, handler, 0, name, priv);
// remove 里什么都不用写(或者只需要做业务层面的清理)
规则:在 probe 里全部用 devm_,remove 里用于回滚 probe 中的业务逻辑,不用操心资源释放。
练习¶
练习 1:阅读真实驱动 leds-gpio.c¶
带着以下问题阅读(约 250 行,半小时可读完):
- [ ] 找到
of_match_table中的compatible字符串,它对应 DTS 里的哪个属性? - [ ] 找到
probe函数,它用了哪些devm_函数? - [ ] 找到
struct gpio_leds_priv,理解它是如何通过platform_get_drvdata在 probe 和其他函数间传递的 - [ ] 找到如何从 DTS 读取 GPIO 属性(
devm_gpiod_get相关)
练习 2:写一个最小 platform driver 框架¶
platform_demo.c:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
MODULE_LICENSE("GPL");
struct demo_priv {
void __iomem *base;
int irq;
};
static int demo_probe(struct platform_device *pdev)
{
struct demo_priv *priv;
struct resource *res;
dev_info(&pdev->dev, "probe called!\n");
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv) return -ENOMEM;
/* 尝试获取第一个 MEM 资源(QEMU 里没有真实硬件,这会返回 NULL) */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
dev_info(&pdev->dev, "MEM resource: 0x%llx - 0x%llx\n",
(u64)res->start, (u64)res->end);
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
} else {
dev_info(&pdev->dev, "No MEM resource (running in QEMU?)\n");
}
priv->irq = platform_get_irq(pdev, 0);
dev_info(&pdev->dev, "IRQ: %d\n", priv->irq);
platform_set_drvdata(pdev, priv);
return 0;
}
static int demo_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "remove called\n");
// devm_ 资源自动释放,这里不需要做什么
return 0;
}
static const struct of_device_id demo_of_match[] = {
{ .compatible = "my,platform-demo" },
{ }
};
MODULE_DEVICE_TABLE(of, demo_of_match);
static struct platform_driver demo_driver = {
.probe = demo_probe,
.remove = demo_remove,
.driver = {
.name = "platform-demo",
.of_match_table = demo_of_match,
},
};
module_platform_driver(demo_driver);
// 等价于手写 module_init/module_exit,调用 platform_driver_register/unregister
- [ ] 编译并在 QEMU 中加载,确认
probe被调用(dmesg 里看到 "probe called!") - [ ] 卸载后确认 "remove called" 出现
练习 3:在 DTS 中创建 platform_device 节点¶
在 QEMU 的 DTS(或直接修改 /sys/firmware/devicetree/base/)中添加节点,使其与你的驱动匹配:
/* 在 QEMU virt 的 DTS 中添加(找到 qemu-virt.dts 或直接在 QEMU -append 里指定 dtb) */
my_demo_device: demo@12340000 {
compatible = "my,platform-demo";
reg = <0x12340000 0x1000>;
interrupts = <0 32 4>;
};
- [ ] 重新编译 QEMU 使用的 DTB,观察 probe 是否被自动调用(不需要手动 insmod 额外操作)
延伸阅读¶
| 资料 | 具体位置 | 说明 |
|---|---|---|
| Linux Device Drivers LDD3 | Ch.14 "The Linux Device Model" | 设备模型基础 |
| 《Linux 设备驱动开发详解》宋宝华 | 第 11 章 | platform 驱动详解 |
| Linux Device Driver Development Madieu | Ch.6 "Introduction to Devices, Drivers..." | 现代 platform 驱动 |
| 内核文档 | Documentation/driver-api/driver-model/platform.rst |
官方 platform driver 文档 |
| LWN.net | https://lwn.net/Articles/448499/ | "The platform device API" |
Day 14 · sysfs 与 debugfs 接口¶
预计时长:1.5 小时
类型:实验
做什么¶
给字符设备驱动加上 sysfs 属性(通过 /sys/ 读写驱动状态),以及 debugfs 接口(暴露内部调试信息)。这两个接口在 BSP 开发中天天用到。
要了解什么¶
sysfs:用户态 ↔ 内核驱动的标准接口¶
sysfs 挂载在 /sys/,将内核对象(devices、buses、drivers)以文件系统形式暴露。每个文件对应一个属性,读写操作映射到 show / store 回调。
// 使用 DEVICE_ATTR_RW 宏(最简方式)
static ssize_t my_value_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct my_priv *priv = dev_get_drvdata(dev);
return sysfs_emit(buf, "%d\n", priv->value); // 用 sysfs_emit 代替 sprintf
}
static ssize_t my_value_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct my_priv *priv = dev_get_drvdata(dev);
int val;
if (kstrtoint(buf, 10, &val)) // 安全的字符串转整数
return -EINVAL;
priv->value = val;
return count;
}
static DEVICE_ATTR_RW(my_value); // 生成 dev_attr_my_value,权限 0644
static DEVICE_ATTR_RO(my_status); // 只读,权限 0444
static DEVICE_ATTR_WO(my_command); // 只写,权限 0200
// 在 probe 中注册
device_create_file(&pdev->dev, &dev_attr_my_value);
// 或用属性组(推荐,可一次注册多个)
static struct attribute *my_attrs[] = {
&dev_attr_my_value.attr,
&dev_attr_my_status.attr,
NULL,
};
static const struct attribute_group my_attr_group = { .attrs = my_attrs };
sysfs_create_group(&pdev->dev.kobj, &my_attr_group);
访问方式:
debugfs:内部调试信息暴露¶
debugfs 挂载在 /sys/kernel/debug/,用于暴露任意调试信息,无格式限制,不需要严格遵守 sysfs 的"每个文件一个值"原则。
#include <linux/debugfs.h>
struct dentry *debugfs_dir;
u32 debug_value = 0;
// 在 probe 或 init 中创建
debugfs_dir = debugfs_create_dir("my_driver", NULL); // /sys/kernel/debug/my_driver/
debugfs_create_u32("value", 0644, debugfs_dir, &debug_value); // 直接暴露变量
debugfs_create_file("dump", 0444, debugfs_dir, priv, &my_dump_fops); // 自定义文件
// 在 remove 中清理
debugfs_remove_recursive(debugfs_dir);
练习¶
- [ ] 给 Day 10-11 的字符设备驱动加一个 sysfs 属性
buf_len(只读),暴露当前缓冲区内容长度 - [ ] 加一个 sysfs 属性
clear(只写),写入任意内容后清空驱动内部缓冲区 - [ ] 用 debugfs 暴露一个整数计数器
read_count,记录read被调用的次数 - [ ] 验证:加载驱动后,
cat /sys/...、echo > /sys/...、cat /sys/kernel/debug/my_driver/read_count都正确工作
延伸阅读¶
| 资料 | 具体位置 | 说明 |
|---|---|---|
| 内核文档 | Documentation/filesystems/sysfs.rst |
sysfs 官方规范 |
| 内核文档 | Documentation/filesystems/debugfs.rst |
debugfs 使用指南 |
| Linux Device Driver Development Madieu | Ch.14 "Linux Kernel Debugging" | debugfs 实战 |
| LWN.net | https://lwn.net/Articles/31185/ | "The sysfs filesystem" |