4、复位控制器节点:reset-cells属性、复位提供者与消费者、复位信号索引、复位控制器的设备树表示

好,咱们接着聊复位控制器。说实话,时钟和复位在嵌入式系统里就像一对双胞胎,谁也离不开谁。时钟给系统提供心跳,复位则保证系统每次都能从干净的状态启动。我在调试一块新板子时,就遇到过因为复位信号没配好,导致外设死活不干活的情况。折腾了两天才发现,原来是复位控制器的设备树描述出了问题。

今天这一节,咱们就专门把复位控制器的设备树表示讲透。你想想看,一个SoC里那么多模块,每个模块都需要复位信号。谁提供复位?谁使用复位?信号怎么索引?这些都得在设备树里说清楚。

4.1 复位提供者与消费者模型

跟时钟一样,复位也分提供者和消费者。提供者就是能产生复位信号的硬件模块,比如系统级的复位控制器。消费者就是需要被复位的那些外设IP。

我个人习惯把复位控制器想象成一个「复位信号分发中心」。它内部有多个复位输出,每个输出对应一个索引号。外设想要复位自己,就通过设备树告诉内核:「我要用复位控制器输出的第几路信号」。

举个例子,一个典型的复位控制器可能管理着USB、Ethernet、I2C等模块的复位。每个模块对应一个复位输出索引。设备树里就这么描述:

// 复位提供者:复位控制器
rstc: reset-controller@1000e000 {
    compatible = "vendor,syscon-reset";
    reg = <0x1000e000 0x1000>;
    #reset-cells = <1>;  // 一个cell表示复位索引
};

// 复位消费者:USB控制器
usb: usb@12000000 {
    compatible = "vendor,usb-controller";
    reg = <0x12000000 0x10000>;
    interrupts = <0 42 4>;
    resets = <&rstc 3>;  // 使用复位控制器的第3路复位
};

这里的关键就是 resets 属性。它告诉内核:这个USB外设的复位信号,来自 rstc 这个复位控制器的第3路输出。

核心要点:

  • #reset-cells:复位提供者声明,表示需要几个cell来描述一个复位信号
  • resets:复位消费者使用,引用复位控制器并指定索引
  • 提供者与消费者通过 phandle + 索引的方式建立连接

4.2 reset-cells 属性的含义

#reset-cells 这个属性,说白了就是告诉别人:「你要用我的复位信号,得给我几个参数?」

最常见的值是1,表示只需要一个复位索引号。但有些复杂的复位控制器,可能还需要额外的参数,比如复位类型(硬复位还是软复位)、复位持续时间等。这时候 #reset-cells 就可能等于2或3。

我见过一个比较特殊的案例,某个FPGA厂商的复位控制器用了 #reset-cells = <2>。第一个cell是复位索引,第二个cell是复位极性(高有效还是低有效)。当时我接手这个项目时,看到这个配置还愣了一下,后来查了手册才明白。

来看一个 #reset-cells = <2> 的例子:

rstc: reset-controller@20000000 {
    compatible = "vendor,complex-reset";
    reg = <0x20000000 0x1000>;
    #reset-cells = <2>;  // 需要两个参数
};

// 消费者使用
uart: serial@21000000 {
    compatible = "vendor,uart";
    reg = <0x21000000 0x1000>;
    resets = <&rstc 5 0>;  // 索引5,低有效复位
};

我的建议: 除非硬件手册明确要求,否则尽量用 #reset-cells = <1>。参数越少,设备树越简洁,出错的概率也越低。我在项目中一般只用一个索引,其他复位参数通过驱动内部的配置寄存器来处理。

4.3 复位信号索引的设计

复位信号索引怎么分配?这其实是个设计问题。我见过两种做法:

  1. 按模块功能分配:比如0-7给高速外设(USB、PCIe),8-15给低速外设(I2C、SPI),16-23给内部模块(DMA、Timer)。
  2. 按硬件寄存器位分配:直接对应复位控制寄存器中的bit位置。比如bit0控制模块A,bit1控制模块B,索引就直接用bit号。

我个人更倾向于第二种。为什么?因为硬件设计就是这么来的。复位控制器的寄存器,每个bit控制一个模块的复位。索引号直接对应bit位置,驱动实现起来也简单,读寄存器、写寄存器,一目了然。

来看一个实际的设备树片段:

// 假设复位控制寄存器布局:
// bit0: CPU core reset
// bit1: GPU reset  
// bit2: DSP reset
// bit3: USB reset
// bit4: Ethernet reset

rstc: reset-controller@30000000 {
    compatible = "vendor,bit-reset";
    reg = <0x30000000 0x1000>;
    #reset-cells = <1>;
};

gpu: gpu@40000000 {
    compatible = "vendor,gpu";
    reg = <0x40000000 0x10000>;
    resets = <&rstc 1>;  // bit1,对应GPU复位
};

usb: usb@50000000 {
    compatible = "vendor,usb";
    reg = <0x50000000 0x10000>;
    resets = <&rstc 3>;  // bit3,对应USB复位
};

注意: 索引号一定要和硬件手册保持一致。我曾经遇到过一个问题:硬件工程师改了复位控制器的寄存器映射,但没更新设备树。结果内核复位了错误的模块,系统直接崩溃。所以每次硬件改版,设备树一定要同步更新。

4.4 复位控制器的完整设备树表示

好了,咱们把前面讲的内容串起来,看一个完整的复位控制器设备树节点应该怎么写。

一个标准的复位控制器节点,通常包含以下属性:

属性 说明 是否必需
compatible 兼容字符串,用于匹配驱动
reg 寄存器地址和大小
#reset-cells 复位描述需要的cell数量
reset-controller 空属性,标记这是一个复位控制器 推荐

来看一个实际项目中用过的例子:

// 系统复位控制器
sys_rst: reset-controller@a0000000 {
    compatible = "rockchip,rk3399-cru-reset", "syscon-reset";
    reg = <0xa0000000 0x1000>;
    #reset-cells = <1>;
    reset-controller;  // 明确声明这是复位控制器
};

// 使用复位的外设
i2c0: i2c@ff3c0000 {
    compatible = "rockchip,rk3399-i2c";
    reg = <0xff3c0000 0x1000>;
    interrupts = <0 57 4>;
    clocks = <&cru SCLK_I2C0>;
    resets = <&sys_rst 12>;  // I2C0的复位索引是12
    status = "disabled";
};

这里有个细节:reset-controller 这个空属性。它其实是个标记,告诉内核「我是复位提供者」。虽然有些驱动不检查这个属性,但我建议加上。为什么?因为设备树是给人看的,加上这个标记,阅读代码的人一眼就能认出这是个复位控制器节点。

避坑指南: 我曾经在一个项目里,把 #reset-cells 写成了 #clock-cells。结果内核在解析设备树时,把复位信号当成了时钟信号去处理,导致外设初始化时序完全错乱。这种低级错误,排查起来特别费时间。所以写设备树时,一定要仔细核对属性名。

4.5 多复位控制器的场景

一个复杂的SoC里,可能不止一个复位控制器。比如:

  • 系统级复位控制器:管理所有主要模块的复位
  • 电源域复位控制器:管理某个电源域内的模块复位
  • 外设专用复位控制器:某些高速外设自带复位逻辑

这种情况下,每个复位控制器都有自己的 #reset-cellsreset-controller 声明。消费者根据自己的需求,选择合适的复位控制器。

// 系统复位
sys_rst: reset-controller@a0000000 {
    #reset-cells = <1>;
    reset-controller;
};

// 电源域复位
pd_rst: reset-controller@b0000000 {
    #reset-cells = <1>;
    reset-controller;
};

// 消费者可以引用不同的复位控制器
usb: usb@c0000000 {
    resets = <&sys_rst 3>;  // 使用系统复位
};

gpu: gpu@d0000000 {
    resets = <&pd_rst 0>;   // 使用电源域复位
};

这种设计的好处是灵活。不同的复位域可以独立管理,互不影响。我在做多核处理器项目时,就用了三个复位控制器:一个给主核域,一个给从核域,一个给外设域。这样任何一个域复位,都不会影响其他域的正常工作。

嗯,复位控制器的设备树表示,核心就是这些。记住三个关键点:#reset-cells 声明参数个数,resets 引用复位信号,索引号对应硬件设计。把这三点搞清楚了,复位相关的设备树配置就不会出大问题。