4、设备树实战:为Sensor添加设备树节点
设备树,说白了就是Linux内核用来描述硬件的一本「硬件说明书」。我刚开始接触嵌入式Linux时,总觉得设备树很玄乎,后来发现它其实就是一套树形结构的数据,告诉内核:你的板子上有哪些外设、它们挂在哪里、怎么配置。
今天我们就拿一个真实的Sensor来练手。假设我们有一颗I2C接口的温度传感器——TMP117,挂在I2C总线0上,地址是0x48,使用中断引脚来通知数据就绪。
4.1 设备树节点的基本结构
一个完整的设备树节点,通常包含这几样东西:
- compatible:驱动匹配的关键字,说白了就是「我叫什么名字」
- reg:设备的地址或寄存器基址
- interrupts:中断号、触发方式
- 其他属性:比如时钟频率、GPIO引脚等
来看一个实际的例子。我在项目中给TMP117添加的节点长这样:
&i2c0 {
status = "okay";
tmp117: temperature-sensor@48 {
compatible = "ti,tmp117";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
#thermal-sensor-cells = <1>;
};
};
嗯,这里要注意:@48是设备在总线上的地址,必须和reg字段一致。我曾经见过有人写@49但reg写0x48,结果内核死活匹配不上,排查了半天。
4.2 解析compatible属性
compatible是驱动和设备树之间的「暗号」。内核通过它来找到对应的驱动程序。
格式一般是:"厂商,设备型号"。比如"ti,tmp117"表示TI公司的TMP117传感器。
驱动里怎么匹配?看这段代码:
static const struct of_device_id tmp117_of_match[] = {
{ .compatible = "ti,tmp117" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, tmp117_of_match);
static struct i2c_driver tmp117_driver = {
.driver = {
.name = "tmp117",
.of_match_table = tmp117_of_match,
},
.probe = tmp117_probe,
.id_table = tmp117_id,
};
为什么要有of_match_table?我个人的习惯是:即使你的设备是I2C或SPI总线,也一定要加上设备树匹配表。这样内核在枚举设备时,会优先通过compatible来匹配,而不是靠传统的ID表。兼容性更好,也更容易调试。
4.3 解析reg属性
reg属性描述的是设备的地址信息。对于I2C设备,它就是从设备地址;对于SPI设备,它是片选号;对于内存映射设备,它是基地址和长度。
在驱动里,我们用of_get_property或者of_i2c_get_board_info来读取。不过更常见的做法是:I2C核心层已经帮我们解析好了,在probe函数里直接拿client->addr就行。
但如果你想手动解析,可以这样:
struct device_node *np = client->dev.of_node;
u32 addr;
if (of_property_read_u32(np, "reg", &addr)) {
dev_err(&client->dev, "failed to get reg property\n");
return -EINVAL;
}
dev_info(&client->dev, "sensor address: 0x%02x\n", addr);
你想想看,如果设备树里reg写错了,这里读出来的地址就是错的,后续I2C通信全废。所以我在项目中每次都会加这么一段打印,确认地址对不对。
4.4 解析interrupts属性
中断属性稍微复杂一点。它通常包含两个值:中断号和触发方式。
触发方式常用的有:
| 宏定义 | 值 | 说明 |
|---|---|---|
| IRQ_TYPE_EDGE_RISING | 1 | 上升沿触发 |
| IRQ_TYPE_EDGE_FALLING | 2 | 下降沿触发 |
| IRQ_TYPE_LEVEL_HIGH | 4 | 高电平触发 |
| IRQ_TYPE_LEVEL_LOW | 8 | 低电平触发 |
驱动里解析中断的典型写法:
int irq_num;
irq_num = of_irq_get(np, 0);
if (irq_num < 0) {
dev_err(&client->dev, "failed to get irq\n");
return irq_num;
}
dev_info(&client->dev, "irq number: %d\n", irq_num);
/* 注册中断 */
ret = devm_request_threaded_irq(&client->dev, irq_num, NULL,
tmp117_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"tmp117", client);
我曾经踩过一个坑:设备树里写的是IRQ_TYPE_EDGE_FALLING,但驱动里注册时用了IRQF_TRIGGER_LOW。结果中断触发方式不匹配,传感器数据就绪了,中断却一直不来。排查了整整一下午才发现是触发方式写错了。
4.5 使用of_函数族读取设备树信息
Linux内核提供了一套of_开头的函数,专门用来读取设备树属性。我把常用的几个列出来:
| 函数 | 用途 |
|---|---|
| of_property_read_u32 | 读取32位整数属性 |
| of_property_read_string | 读取字符串属性 |
| of_property_read_u32_array | 读取整数数组 |
| of_get_property | 获取属性原始数据 |
| of_irq_get | 获取中断号 |
| of_iomap | 将地址映射到虚拟空间 |
| of_find_node_by_path | 通过路径查找节点 |
| of_find_compatible_node | 通过compatible查找节点 |
来看一个综合的例子,把前面讲的都串起来:
static int tmp117_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct device_node *np = client->dev.of_node;
struct tmp117_data *data;
u32 temp;
int ret;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 读取自定义属性:采样频率 */
ret = of_property_read_u32(np, "ti,sample-rate", &temp);
if (ret) {
/* 没读到就用默认值 */
temp = 4; /* 默认4Hz */
dev_warn(&client->dev, "use default sample rate: %d\n", temp);
}
data->sample_rate = temp;
/* 读取中断 */
data->irq = of_irq_get(np, 0);
if (data->irq < 0) {
dev_info(&client->dev, "no irq configured, use polling mode\n");
}
/* 读取reg地址(其实client->addr已经有了,这里演示用) */
of_property_read_u32(np, "reg", &temp);
dev_info(&client->dev, "device addr: 0x%02x, irq: %d\n",
temp, data->irq);
/* 注册中断 */
if (data->irq > 0) {
ret = devm_request_threaded_irq(&client->dev, data->irq, NULL,
tmp117_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"tmp117", data);
if (ret) {
dev_err(&client->dev, "failed to request irq\n");
return ret;
}
}
i2c_set_clientdata(client, data);
return 0;
}
devm_系列函数(设备资源管理),比如devm_kzalloc、devm_request_threaded_irq。这样在设备卸载或驱动出错时,内核会自动帮你释放资源,省去很多麻烦。
4.6 调试技巧
设备树写对了没有?驱动读到了没有?我一般用这几个方法验证:
- 查看设备树是否被正确解析:在/sys/firmware/devicetree/base/目录下,可以看到内核解析后的设备树结构
- 查看设备是否被注册:
ls /sys/bus/i2c/devices/,看看有没有你的设备 - 查看驱动是否匹配:
cat /sys/bus/i2c/devices/0-0048/driver,看驱动名是否正确 - 使用devicetree.org的语法检查工具:编译设备树时加上
DTC_FLAGS=-@可以生成符号表,方便调试
我记得有一次,设备树编译通过了,驱动也加载了,但probe就是不调用。最后发现是status = "okay"写成了status = "ok"。内核只认"okay"和"disabled",写"ok"会被当成无效状态,直接跳过。
4.7 小结
设备树实战这块,说白了就是三件事:
- 在设备树里把硬件描述清楚(compatible、reg、interrupts)
- 在驱动里用of_函数把属性读出来
- 确保设备树和驱动的匹配条件一致
你只要把这三步走通,Sensor驱动移植就完成了80%。剩下的就是具体的寄存器读写和数据处理逻辑,那些反而是相对固定的套路。
下一章我们会深入Sensor的电源管理和休眠唤醒机制,到时候再聊。