【全栈第15课】platform driver + Device Tree
Android 全栈工程师进阶教程 第15课。platform driver + Device Tree。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 15 课。完整 26 课见 GitHub 仓库。
【全栈第15课】platform driver + Device Tree
0. 这节课你将做出什么
写一个 platform driver + 配套 Device Tree(DTS)节点,理解 ARM SoC 驱动的标准模型:DTS 描述硬件,驱动靠 compatible 匹配后自动 probe。 学完你能回答:真实手机驱动为什么不靠手动 insmod 建设备(像 L14 那样),而是写一段 DTS?compatible 是什么?probe 什么时候被调?驱动怎么从 DTS 读硬件配置(寄存器地址/中断/GPIO)?
1. 背景:L14 的局限
L14 的字符驱动靠 module_init 手动建 misc 设备——这适合"软件设备"(没有真实硬件)。但真实硬件(触控 IC、传感器)的驱动不这么写,因为:
- 一块板子上有几十上百个硬件,每个的寄存器地址、中断、供电都不同
- 这些信息不该硬编码在驱动 C 代码里(换个板子就得改代码重编)
解法:Device Tree(设备树)。把"这块板上有什么硬件、各自的地址/中断/供电"写在 .dts 文件里(数据),驱动代码(逻辑)通过 compatible 字符串匹配到对应硬件。数据和逻辑分离——换板子只改 DTS,不动驱动代码。这是 ARM Linux 的标准模型。
2. 全局图:设备树驱动模型
.dts 节点(描述硬件,数据) platform_driver(逻辑,代码)
hello@12340000 { static struct platform_driver {
compatible = "xiaomi,hello-device"; ───匹配─── .of_match_table = {{.compatible="xiaomi,hello-device"}}
reg = <0x12340000 0x1000>; }
interrupts = <0 42 4>;
};
│ 内核解析 DTS,发现 compatible 匹配
▼ 自动调用 driver 的 probe(pdev)
probe 里:of_property_read_* 读 DTS 配置 → ioremap 寄存器 → 申请中断 ...
3. 某真实机型 真实印证
某真实机型 是 GKI prebuilt 无 kernel 源码,DTS 在独立 kernel/vendor 仓。但模型通用:
- 真实触控驱动如
drivers/input/touchscreen/focaltech_*.c,probe 里of_property_read_u32读中断脚、复位脚 GPIO。 - DTS 节点在
arch/arm64/boot/dts/<vendor>/<board>.dts。
4. 前置准备
- 依赖 L13(内核/.ko)、L14(驱动基础)。
- 真编译:本机 Linux 6.17(已验证)。
- 文件:
hello_platform.c、hello-device.dtsi、Makefile。
5. 动手:写 platform driver + DTS
5.1 DTS 节点 hello-device.dtsi
&soc { // 挂在 soc 节点下
hello_dev: hello@12340000 { // @后是 reg 基址(命名约定)
compatible = "xiaomi,hello-device"; // ★ 匹配关键:和驱动 of_match_table 一致
reg = <0x12340000 0x1000>; // 寄存器基址 0x12340000,大小 0x1000(probe 里 ioremap)
interrupts = <0 42 4>; // 中断号 42(L17 用)
xiaomi,sample-rate = <1000>; // 自定义属性(probe 里读)
xiaomi,label = "hello-sensor"; // 自定义字符串属性
status = "okay"; // okay=启用,disabled=禁用(不 probe)
};
};
> reg 在 platform 节点里是内存/寄存器地址(对比 L16 I2C 节点里 reg 是从设备地址,易混)。status="disabled" 的节点内核不会匹配 probe——量产里关闭某硬件就改这个。
5.2 platform driver hello_platform.c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h> // Device Tree of_*
#include <linux/mod_devicetable.h>
// probe:DTS 有节点 compatible 匹配本驱动时,内核自动调用
static int hello_probe(struct platform_device *pdev) {
struct device *dev = &pdev->dev;
u32 rate = 0;
const char *label = NULL;
dev_info(dev, "probe, node=%s\n", dev->of_node ? dev->of_node->name : "none");
// 从 DTS 读配置 —— BSP 读硬件参数的标准方式
if (of_property_read_u32(dev->of_node, "xiaomi,sample-rate", &rate) == 0)
dev_info(dev, "DTS sample-rate=%u\n", rate);
if (of_property_read_string(dev->of_node, "xiaomi,label", &label) == 0)
dev_info(dev, "DTS label=%s\n", label);
// 真实驱动这里:ioremap(reg) 映射寄存器 / 申请中断(L17) / 注册子系统
return 0;
}
// ★ 注意:6.11+ remove 返回 void(不是 int!老内核才是 int,新内核编不过)
static void hello_remove(struct platform_device *pdev) {
dev_info(&pdev->dev, "remove\n");
}
// of_device_id:声明本驱动匹配哪些 compatible
static const struct of_device_id hello_of_match[] = {
{ .compatible = "xiaomi,hello-device" }, // 必须和 DTS 节点 compatible 一致
{ } // sentinel(结束标记,不能少)
};
MODULE_DEVICE_TABLE(of, hello_of_match);
static struct platform_driver hello_driver = {
.probe = hello_probe,
.remove = hello_remove, // 6.11+ 是 void 返回
.driver = {
.name = "hello-platform",
.of_match_table = hello_of_match, // 挂上 compatible 表
},
};
module_platform_driver(hello_driver); // 宏自动生成 init/exit(注册/注销 driver)
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Lesson15 platform driver + device tree");
逐块讲为什么:
module_platform_driver(hello_driver)一个宏 = 自动生成 module_init(注册 driver)+ module_exit(注销)。比 L14 手写 init/exit 省事。of_match_table是匹配的核心:内核启动解析 DTS,每遇到一个节点,拿它的 compatible 去所有 driver 的 of_match_table 里找,匹配上就调那个 driver 的 probe。of_property_read_u32/string是从 DTS 读硬件参数的标准 API——这是 BSP 工程师每天用的。
6. 看底层:probe 是怎么被触发的
内核启动 → 解析 DTS(.dtb)→ 为每个 status="okay" 的节点创建 platform_device
→ 拿节点 compatible 去 platform bus 上所有 driver 的 of_match_table 匹配
→ 匹配上 → 调该 driver 的 probe(platform_device*)
→ probe 里 pdev->dev.of_node 就是这个 DTS 节点,of_property_read_* 从它读配置
所以你不用手动建设备(L14 那样),DTS 一描述,匹配上就自动 probe。
7. 编译 & 运行(✅ 真编译过)
make # 真编译出 hello_platform.ko
# DTS 要编进设备的 .dtb(在 kernel/vendor 仓改 dts 重编 dtb)
# .ko 加载后,若 DTS 有匹配节点 → probe 触发:
adb shell dmesg | grep -i hello # 看 "probe, node=hello" + DTS 读到的值
真编译结果(本机 6.17,修正 remove 签名后):
CC [M] hello_platform.o
LD [M] hello_platform.ko ← 修正 remove→void 后编译通过
> 验证状态(诚实):本机 6.17 真编译。首次编译失败——remove 返回 int 在 6.11+ 报 incompatible pointer type,改 void 后通过(这是真编译才能发现的 bug,见踩坑)。
8. 踩坑提醒(真编译踩到的关键)
- ⚠️ remove 返回值 6.11+ 从 int 改 void:
static int hello_remove(...)在新内核(含 某真实机型 6.12)编不过,报void(*) vs int(*) incompatible。改static void hello_remove(...)。这是本课真编译抓到的真 bug。(社区 remove→remove_new→remove 演变) - of_match_table 少 sentinel:数组结尾要有空
{ },否则内核越界。 - compatible 不一致:DTS 和驱动两边 compatible 差一个字 → 不 probe,驱动像没装。
- reg 含义混淆:platform 节点里是内存地址,I2C 节点里是从设备地址(L16)。
- status="disabled":节点禁用了不会 probe,调试时检查这个。
9. 常见问题分析与定位(platform driver 实战)
9.1 常见问题清单
- probe 没被调用(驱动装了但不工作)
- probe 里 of_property_read 读不到值
- 编译报 remove 指针类型不兼容
- 多个驱动抢同一个 compatible
- ioremap 失败
9.2 分析与定位
① probe 没被调用
- 现象:.ko 加载了,但 dmesg 没有 probe 日志。
- 原因(高→低):compatible 不一致 / DTS 节点 status≠okay / DTS 没编进 dtb / .ko 没加载。
- 定位:
adb shell dmesg | grep -i hello # 有没有 probe
adb shell cat /proc/device-tree/soc/hello@12340000/compatible # DTS 里的 compatible 实际值
adb shell cat /proc/device-tree/soc/hello@12340000/status # status 是 okay 吗
adb shell ls /sys/bus/platform/drivers/hello-platform/ # 驱动注册了吗
adb shell ls /sys/bus/platform/devices/ | grep hello # 设备(DTS节点)生成了吗
/sys/bus/platform/devices/有节点但/sys/bus/platform/drivers/.../下没绑定 → compatible 不匹配。
② of_property_read 读不到
- 原因:DTS 里属性名拼错,或类型不对(read_u32 读了个字符串)。
- 定位:
cat /proc/device-tree/.../xiaomi,sample-rate | xxd看 DTS 里实际有没有、值是多少。
③ 编译 remove 不兼容 — 见踩坑1,改 void。
④ compatible 冲突 — 两个驱动都声明同一个 compatible,只有一个能 probe。检查是不是重复定义。
⑤ ioremap 失败 — reg 地址/大小写错,或该地址已被别的驱动 request。dmesg 看 ioremap 报错。
9.3 定位决策树
platform driver 出问题
├─ probe 没调 → dmesg 无 probe
│ ├─ /sys/bus/platform/devices/ 有节点吗 → 无=DTS 没编进/status≠okay
│ ├─ compatible 比对(/proc/device-tree vs 驱动 of_match)→ 不一致改一致
│ └─ /sys/bus/platform/drivers/ 有驱动吗 → 无=.ko 没加载
├─ 读不到属性 → cat /proc/device-tree 看实际值 → 属性名/类型
├─ 编译 remove 不兼容 → 改 void(6.11+)
├─ compatible 冲突 → 查重复定义
└─ ioremap 失败 → dmesg → reg 地址/大小/已被占用
10. 小结
- 设备树模型 = 数据(DTS)与逻辑(驱动)分离:换板子改 DTS 不动驱动代码。
- compatible 是匹配核心:DTS 节点和驱动 of_match_table 一致才 probe。
- probe 自动触发:内核解析 DTS,匹配上就调,不用手动建设备(对比 L14)。
- of_property_read_ 从 DTS 读硬件配置*,BSP 每天用。
- ⚠️ remove 6.11+ 返回 void(真编译抓到的 bug)。
- 排查口诀:probe 没调先看 /sys/bus/platform/devices+compatible 一致性;读不到查 /proc/device-tree。
下节预告
L16:I2C 总线驱动 —— 触控/传感器/指纹真实挂的总线。写一个 i2c_driver,读芯片 ID 验证通信。