4. 设备树进阶属性:clocks、resets、dmas 与 power-domains
基础属性搞定了,咱们得聊聊真正让驱动跑起来的那些「硬核」属性。说白了,一个外设要工作,你得给它时钟、给它复位、给它 DMA 通道,还得管它的电源。这些在设备树里怎么描述?我踩过的坑可不少,今天一次性给你讲透。
核心观点:clocks、resets、dmas、power-domains 这四个属性,是驱动与硬件资源管理框架之间的「契约」。写对了,驱动自动获取资源;写错了,内核直接罢工。
4.1 clocks 与 clock-names:给外设「上发条」
没有时钟,芯片就是一块死硅。设备树里用 clocks 属性告诉驱动:你的时钟源是谁。而 clock-names 就是给这些时钟起个名字,方便驱动通过名字去查找。
我个人习惯,clock-names 一定要跟驱动代码里的 devm_clk_get() 参数严格对应。大小写、下划线,差一个字符都不行。
uart0: serial@10000000 {
compatible = "vendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clocks = <&clkc 12>, <&clkc 34>;
clock-names = "baud", "pclk";
};
这里 clocks 数组里有两个条目:第一个是波特率时钟,第二个是外设总线时钟。驱动里这么拿:
clk_baud = devm_clk_get(dev, "baud");
clk_pclk = devm_clk_get(dev, "pclk");
我的经验:如果某个时钟是可选的(比如有些芯片可以旁路),记得在驱动里用 devm_clk_get_optional(),否则设备树里没配这个时钟,驱动直接 probe 失败。
4.2 resets 与 reset-names:让外设「重启」
复位信号,说白了就是给外设一个「硬重启」。很多 SoC 的外设在上电后需要先释放复位才能工作。设备树里用 resets 属性来描述复位控制器和复位信号。
我曾经在一个项目里,因为漏写了 resets 属性,导致以太网控制器初始化时一直卡在「等待 PHY 复位完成」的死循环里。查了两天才发现,硬件复位信号根本没释放。
eth0: ethernet@20000000 {
compatible = "vendor,gmac";
reg = <0x20000000 0x2000>;
interrupts = <0 10 4>;
clocks = <&clkc 5>, <&clkc 6>;
clock-names = "stmmaceth", "pclk";
resets = <&rstc 8>;
reset-names = "stmmaceth";
};
驱动里通过 devm_reset_control_get() 拿到复位句柄,然后在初始化时调用 reset_control_deassert() 释放复位。
注意:复位信号是有顺序的。如果外设依赖多个复位源,resets 数组的顺序必须与硬件设计一致。我曾经见过一个 USB 控制器,需要先释放「核心复位」再释放「总线复位」,顺序反了直接挂死。
4.3 dmas 与 dma-names:给数据搬个家
DMA 就是数据搬运工。设备树里用 dmas 属性描述 DMA 通道,dma-names 给通道起名字。每个 DMA 条目通常包含:DMA 控制器 phandle、通道号、以及一些标志位。
你想想看,一个音频设备可能有「播放」和「录音」两个 DMA 通道,怎么区分?靠名字。
i2s0: i2s@30000000 {
compatible = "vendor,i2s";
reg = <0x30000000 0x1000>;
interrupts = <0 28 4>;
dmas = <&dma0 2>, <&dma0 3>;
dma-names = "tx", "rx";
};
驱动里用 dma_request_slave_channel() 或者 dma_request_chan() 来获取通道:
tx_chan = dma_request_slave_channel(dev, "tx");
rx_chan = dma_request_slave_channel(dev, "rx");
| 属性 | 作用 | 常见错误 |
|---|---|---|
dmas |
指定 DMA 控制器和通道 | 通道号与硬件不匹配 |
dma-names |
给通道起别名 | 名字与驱动不统一 |
| 标志位 | 控制 DMA 传输模式 | 漏写或写错导致传输失败 |
避坑指南:有些 SoC 的 DMA 控制器支持「流式传输」和「内存到内存」两种模式。标志位写错了,DMA 传输可能直接超时。我建议你在调试时先检查 /sys/kernel/debug/dmaengine/ 下的信息。
4.4 power-domains:管好外设的「电闸」
电源域,说白了就是控制外设的「电闸」。在低功耗设计中,一个电源域可以包含多个外设。设备树里用 power-domains 属性把外设挂到对应的电源域上。
嗯,这里要注意:电源域是有层级关系的。比如一个 GPU 可能属于「GPU 电源域」,而这个电源域又依赖于「核心电源域」。驱动在 probe 时,内核的 PM 框架会自动帮你把依赖的电源域都打开。
gpu: gpu@40000000 {
compatible = "vendor,gpu";
reg = <0x40000000 0x10000>;
interrupts = <0 45 4>;
power-domains = <&pd_gpu 0>;
};
驱动里不需要手动调用电源管理接口,内核会在 probe 前自动 power_on,在 remove 后自动 power_off。
我曾经踩过的坑:某个项目中,我把一个 USB 控制器挂到了「休眠时断电」的电源域上。结果系统挂起后,USB 控制器断电,醒来后重新上电,但寄存器状态没恢复,直接导致系统崩溃。解决方案是在电源域描述里加上 power-domains = <&pd_always_on 0>;。
4.5 四个属性的协同工作
在实际项目中,这四个属性经常一起出现。比如一个高速 SPI 控制器,既需要时钟、复位,又需要 DMA,还属于某个电源域。它们之间的时序关系很重要:
- 先确保电源域已经上电(内核自动处理)
- 释放复位信号
- 使能时钟
- 配置 DMA 通道
这个顺序在驱动代码里必须严格遵守。我见过有人先使能时钟再释放复位,结果复位信号把刚稳定的时钟又打乱了。
4.6 调试技巧:怎么看属性有没有生效
设备树写对了,驱动就能拿到资源。但怎么确认?我一般用这几招:
- 查看
/sys/firmware/devicetree/base/:这里会展开设备树,你可以直接看属性值对不对。 - 驱动 probe 日志:打开
dev_dbg(),看驱动是否成功获取了时钟、复位、DMA 通道。 - 电源域状态:
/sys/kernel/debug/pm_genpd/下可以看到每个电源域是否上电。
我的习惯:在驱动 probe 函数里加一段打印,把获取到的资源信息打出来。比如时钟频率、DMA 通道号。这样一旦出问题,一眼就能看出是设备树配错了还是驱动写错了。
好了,这四个进阶属性就聊到这儿。它们就像外设的「四根支柱」,缺一根设备都站不稳。写设备树时,多想想硬件实际是怎么工作的,别光顾着抄模板。