4. 设备树编译与反编译

基础属性搞定了,咱们得聊聊真正让驱动跑起来的那些「硬核」属性。说白了,一个外设要工作,你得给它时钟、给它复位、给它 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,还属于某个电源域。它们之间的时序关系很重要:

  1. 先确保电源域已经上电(内核自动处理)
  2. 释放复位信号
  3. 使能时钟
  4. 配置 DMA 通道

这个顺序在驱动代码里必须严格遵守。我见过有人先使能时钟再释放复位,结果复位信号把刚稳定的时钟又打乱了。

设备树进阶属性协同工作流程 外设节点 clocks resets dmas power-domains 驱动通过名字获取资源:clk_get / reset_control_get / dma_request_chan 顺序:power-on → deassert reset → enable clock → configure DMA

4.6 调试技巧:怎么看属性有没有生效

设备树写对了,驱动就能拿到资源。但怎么确认?我一般用这几招:

  • 查看 /sys/firmware/devicetree/base/:这里会展开设备树,你可以直接看属性值对不对。
  • 驱动 probe 日志:打开 dev_dbg(),看驱动是否成功获取了时钟、复位、DMA 通道。
  • 电源域状态:/sys/kernel/debug/pm_genpd/ 下可以看到每个电源域是否上电。

我的习惯:在驱动 probe 函数里加一段打印,把获取到的资源信息打出来。比如时钟频率、DMA 通道号。这样一旦出问题,一眼就能看出是设备树配错了还是驱动写错了。

好了,这四个进阶属性就聊到这儿。它们就像外设的「四根支柱」,缺一根设备都站不稳。写设备树时,多想想硬件实际是怎么工作的,别光顾着抄模板。