在 Linux 内核的功耗管理体系中,SoC 上的硬件资源需要一层层抽象才能被上层驱动安全使用。时钟有 CCF,电源域有 genpd,复位也需要自己的通用框架——reset framework。
我们从一个典型的 SoC 功耗控制路径看 reset 的位置:

整个功耗控制链路上,reset 位于最靠近硬件的底层:当需要关闭一个不用的硬件模块,顺序通常是停止请求 → 关闭时钟 → 置位复位 → 关闭电源;打开时反过来:上电 → 等待电压稳定 → 开时钟 → 释放复位 → 开始工作。
CCF 管"给不给时钟信号",reset 管"进不进工作状态",两者配合才能让一个模块彻底关断,实现最低功耗。如果没有 reset 框架,每个驱动都要自己操作复位寄存器,代码重复,而且共享复位线会出问题。
本文我们就从实际硬件出发,按实际情况 → 抽象对象 → 框架模型 → 数据流 → 运行行为 → 实际案例这个路径,把 reset 框架的设计说清楚。
在 CCF 系列文章中,我们分析过 Linux 内核如何把 SoC 上成百上千个时钟资源用通用框架建模,使驱动能够通过统一接口获取和操作。SoC 上还有一类资源与时钟高度相似,就是复位信号。
每个硬件模块——外设、控制器、总线接口——上电后都需要复位信号完成初始化状态机。一个典型的移动 SoC 集成的复位源数量在 100 到 300 个之间。这些复位信号分散在多个控制器中,有的独立存在,有的与时钟控制器集成在一起,还有的通过 PMU 间接控制。
早期内核没有统一的复位管理机制。每个驱动自己实现复位操作,直接读写硬件寄存器。同一条复位线被多个硬件模块共享时,驱动之间没有同步机制,容易出现一个驱动刚释放复位,另一个驱动又马上置位的冲突。复位操作的时序参数也是硬编码在各个驱动中,芯片改版后需要同步修改十几个驱动文件。
这些问题在多平台内核维护中被放大。同一个 IP 核的驱动可能要适配五六个不同的 SoC,每个 SoC 的复位寄存器布局都不一样。内核社区在 3.10 版本前后开始引入 reset framework,解决复位资源的标准化管理问题。
复位本身的作用是把硬件模块拉到已知初始状态,保证上电后从确定状态开始工作。在功耗管理里,复位也常用来做彻底关闭模块:不需要的模块保持复位状态,可以彻底关掉时钟甚至电源,功耗更低。所以复位框架和 CCF 时钟框架总是一起出现在 SoC 上:每个硬件模块往往需要先复位,再开时钟才能正常工作,关闭时先关时钟,再保持复位才能最低功耗。

图中可以看到清晰的边界:CCF 负责时钟建模,reset 框架负责复位建模,最终都是给外设/IP 模块提供控制能力,两者都是底层资源底座,上层驱动只需要通过统一接口操作,不需要关心寄存器细节。
reset 框架的核心对象模型比 CCF 更简单,只有四个核心对象:
struct reset_control {
struct reset_controller_dev *rcdev;
unsigned int id;
bool shared;
atomic_t deassert_count;
};
每个 reset_control 关联到一个复位控制器设备 struct reset_controller_dev,以及该控制器内的复位线编号 id。对于共享的复位线,内核通过 deassert_count 引用计数管理多个 consumer 的并发操作。
复位控制器驱动提供一组操作函数指针:
struct reset_control_ops {
int (*reset)(struct reset_controller_dev *rcdev, unsigned long id);
int (*assert)(struct reset_controller_dev *rcdev, unsigned long id);
int (*deassert)(struct reset_controller_dev *rcdev, unsigned long id);
int (*status)(struct reset_controller_dev *rcdev, unsigned long id);
};
这四个操作为框架提供了统一的硬件抽象。reset 执行完整的复位脉冲,assert 置位复位信号,deassert 释放复位信号,status 查询当前复位状态。
复位信号有两种工作模式。自清除模式下,assert 操作自动触发一个硬件脉冲后释放。电平模式下,assert 和 deassert 是两个独立的操作,复位信号保持在置位状态直到显式释放。框架内部处理这两种模式的差异,consumer 驱动不需要区分。
常用接口包括:
of_reset_control_get:通过 DT 获取复位句柄reset_control_assert:断言复位,让模块保持复位状态reset_control_deassert:解除复位,让模块开始工作
和时钟框架对比:时钟框架是树形结构,有 parent 级联关系,clock 可以复用 parent 分频;复位框架没有复杂级联,结构更扁平,每个复位线通常对应控制器中的一个 bit 或一组 bit;接口更简单,主要实现 assert/deassert,不需要 rate 计算、parent 级联和频率传播。
复位框架采用 provider-consumer 分层模型,与时钟框架的设计高度一致。
provider 侧由 SoC 顶层复位控制器驱动实现。驱动初始化时探测硬件寄存器,填充 reset_control_ops 函数指针,然后调用 reset_controller_register 把控制器注册到框架中。注册时指定控制器支持的复位线数量和设备树节点匹配方式。
consumer 侧的外设驱动通过 devm_reset_control_get 系列函数从设备树获取复位句柄。拿到句柄后调用 reset_control_assert、reset_control_deassert 等通用接口操作复位信号,不需要知道具体寄存器地址和位域定义。
共享复位线是框架处理的一个核心场景。多个设备可能挂在同一条复位信号下,比如一个总线下面的所有外设共享同一个总线复位。框架通过引用计数确保只有最后一个 consumer 调用 deassert 时才真正释放复位,避免中间状态导致的硬件异常。
独占复位线则直接操作硬件,没有引用计数开销。框架在 consumer 获取句柄时检查复位线的共享属性,自动选择对应的操作路径。
复位控制器可以级联。一个主控制器输出的复位信号作为另一个子控制器的输入,子控制器再拆分出更多的复位线。框架支持这种级联拓扑,consumer 看到的仍然是统一的接口。
复位控制器驱动一般对应 SoC 顶层复位模块。它的 probe 流程非常清晰:

核心步骤:
整个流程非常直接,因为没有树形结构需要建立,本质是创建控制器 → 暴露一组复位线 → 交给 reset core 做统一分发。
consumer 驱动,也就是具体外设/IP 驱动,使用复位的流程也比较固定:

整个使用流程符合驱动开发的通用模式:
resets = <&cru 0>; 指定控制器和索引devm_reset_control_get() 获取复位句柄reset_control_assert() 让模块保持复位,进入确定的初始状态reset_control_deassert() 解除复位,模块开始工作释放流程通常不需要显式编写。使用 devm 接口时,设备移除阶段由 devm 框架自动释放资源。
这里最容易出问题的不是接口,而是顺序。寄存器访问、clock、regulator、power domain 状态必须满足硬件时序;如果 clock 没开就访问寄存器,或者电压未稳定就释放复位,设备可能直接启动异常。
复位框架的运行时行为围绕引用计数和并发保护展开。
多个驱动同时操作同一条共享复位线时,框架通过 atomic_t 类型的 deassert_count 保证原子性。递增和递减操作都是原子指令,不需要额外的锁。这在中断上下文也能安全执行。
驱动获取复位句柄时可以指定获取模式。RESET_EXCLUSIVE 强制要求独占访问,如果复位线已经被标记为共享则返回错误。RESET_SHARED 允许共享访问,多个 consumer 可以同时持有同一条复位线的句柄。
复位状态查询通过 reset_control_status 接口实现。这个接口返回 0 表示复位已释放,正数表示复位已置位,负数表示不支持查询。不是所有硬件都能回读复位状态,provider 驱动根据硬件能力实现。
拿实际芯片看,RK3588 把大量顶层控制能力放在 CRU / PMUCRU 这类控制器里。它们既提供 clock,也提供 reset。设备树中通过 #clock-cells 和 #reset-cells 说明 consumer 引用时需要携带 ID。

RK3588 顶层控制器提供多个独立复位线,例如:
每个复位线对应一个具体硬件模块,各个模块驱动各自获取需要的复位句柄,操作统一走 reset framework 接口。
以 Rockchip RK3588 SoC 的 CRU(Clock and Reset Unit)控制器为例,驱动在 drivers/reset/reset-rockchip.c 中实现,注册时指定每个复位线对应的寄存器偏移和位域:
static conststruct rockchip_reset_info rk3588_reset_info[] = {
RK3588_RESET(0, 0x400, 0),
RK3588_RESET(1, 0x400, 1),
...
};
RK3588_RESET 宏把复位线编号映射到 CRU 寄存器地址和位偏移。provider 驱动的 assert 函数根据这个信息计算出要写入的寄存器值,执行写操作后插入 10 微秒延迟,确保复位时序满足硬件要求。
consumer 侧以 SDHCI 驱动为例,设备树节点中声明复位线引用:
sdmmc: mmc@fe2b0000 {
compatible = "rockchip,rk3588-dw-apb-uart";
resets = <&cru SRST_MMC0>;
reset-names = "reset";
};
驱动 probe 时通过 devm_reset_control_get_exclusive 获取独占复位句柄,然后在硬件初始化序列中执行:
reset_control_assert(host->rst);
udelay(10);
reset_control_deassert(host->rst);
这个序列产生一个 10 微秒的复位脉冲,让 SDHCI 控制器进入已知的初始状态。整个过程中,SDHCI 驱动不需要知道 CRU 寄存器的地址,也不需要处理 RK3588 与其他 Rockchip 芯片的复位控制器差异。
如果多个设备共享同一条复位线,比如 SPI0 和 I2C0 都挂在同一个总线复位下,驱动分别获取同一个复位线的句柄。第一个驱动 deassert 时真正释放复位,第二个驱动调用 deassert 时引用计数递增但不操作硬件。两个驱动都 assert 后引用计数归零,复位信号才真正置位。
RK3588 还通过 PMIC 提供部分电源域的复位控制,这部分作为独立的复位控制器驱动注册到框架中。consumer 驱动引用 PMIC 复位线的方式与引用 CRU 复位线完全相同,接口没有差异。
reset 框架是 Linux 内核 SoC 资源底座中比较简单的一层:
放到功耗管理的整体视角看,reset 是模块开关路径中的关键资源:
所以 reset framework 本身不直接制定功耗策略,但它是功耗管理能落到硬件动作上的重要底座。