1. 从功耗子系统说起:reset 框架到底在解决什么问题
搞 Linux 功耗子系统的朋友大概率都碰到过这样的场景:某个外设时钟开着、电源域也供着电,但设备就是没反应,读寄存器全是默认值,或者干脆返回超时。排查半天发现是硬件模块还卡在复位状态没释放。这时候你需要的,就是 reset 通用框架。
Linux 的 reset 子系统(也就是 drivers/reset 目录下那套东西)干的事情说起来很朴素:给 SoC 上各种各样的复位控制器提供一个统一的抽象层,让驱动开发者不用去记每个芯片的复位寄存器偏移、位定义、解锁序列,而是通过标准 API 来申请、释放、触发复位。它和 clock 子系统、regulator 子系统、pinctrl 子系统是同一个设计哲学下的产物——把硬件差异收敛到 provider 驱动里,把使用方式统一到 consumer API 里。
为什么它会被归到功耗子系统的大框架下讲?因为复位和功耗管理是强耦合的。一个模块下电再上电,通常需要走一遍复位流程才能回到确定状态;反过来,复位释放的时机如果不对,模块可能在电源还没稳定时就开始工作,导致行为异常甚至损坏。所以做功耗管理的人,绕不开 reset 框架。
这篇文章适合谁看?如果你在写 SoC 平台驱动、在调外设 bring-up、在做低功耗场景下的模块上下电流程,或者你只是好奇 Linux 怎么把一堆五花八门的复位控制器管起来,那这篇梳理应该能帮到你。我会从框架设计思路讲到核心数据结构,再到实际写代码的完整流程,最后把我踩过的坑和排查经验一并倒出来。
2. reset 框架的整体设计与核心思路拆解
2.1 为什么需要通用框架而不是各写各的
在没有通用框架的年代,每个 SoC 厂商的驱动里都塞满了类似writel(0x1 << 5, base + 0x20)这样的代码。问题很明显:同一个外设驱动换一个平台就得改,复位序列的时序要求散落在各处,多个驱动共享一个复位控制器时还会互相踩。更麻烦的是,复位控制器本身可能需要在特定时钟开启、特定电源域上电后才能访问,这些依赖关系没有统一的地方去表达。
reset 框架的核心思路是引入 provider/consumer 模型。复位控制器驱动注册成 provider,向框架描述"我这里有 N 条复位线,每条线对应哪个设备";外设驱动作为 consumer,通过设备树里的 phandle 拿到自己那条复位线的句柄,然后调用reset_control_assert()和reset_control_deassert()来操作。中间的所有硬件细节被 provider 的struct reset_control_ops封装掉了。
这个设计带来的直接好处是:外设驱动完全平台无关,设备树负责把"哪个设备的复位线连到哪个控制器"这件事描述清楚,而控制器驱动只需要实现几个回调函数。职责边界非常干净。
2.2 三层数据结构:provider、controller、control
理解 reset 框架,关键是把三个层次的数据结构分清楚,很多人看源码看晕就是没理清这三者的关系。
最底层是struct reset_controller_dev,代表一个复位控制器硬件实例。它包含控制器自己的操作函数集ops、复位线的数量nr_resets、以及一个of_xlate回调用于解析设备树参数。一个 SoC 上可能有多个复位控制器,比如一个管外设复位、一个管电源域复位,那就注册多个reset_controller_dev。
中间层是struct reset_control,代表一条具体的复位线。它是 consumer 拿到的句柄,内部持有指向所属 controller 的指针、这条线的索引id、以及一些状态标志(比如是否已经 assert、是否 shared)。注意这个结构对 consumer 是不透明的,你只能通过 API 操作它。
最上层是 consumer 视角的struct reset_control句柄,通过reset_control_get()系列函数获取。这里有个容易混淆的点:reset_control既表示一条复位线,又是 consumer 操作的句柄,实际上框架内部会根据是否 shared、是否 optional 等情况做不同的包装。
2.3 复位线的共享与独占语义
实际硬件里,一条复位线可能被多个设备共用。比如一个总线上的多个外设共享同一个复位信号,或者一个电源域的复位同时影响好几个模块。reset 框架用reset_control_get_shared()和reset_control_get_exclusive()来区分这两种语义。
独占模式下,如果已经有别的 consumer 拿到了这条线,再申请就会失败,防止两个驱动互相干扰。共享模式下,多个 consumer 可以同时持有,但 assert 和 deassert 的语义变成了引用计数——只有所有持有者都 deassert 了,硬件上才真正释放复位。这个设计很巧妙,但也容易踩坑:如果你用共享模式拿了一条线,却以为自己的 deassert 会立即生效,那就错了,得等其他人都释放。
提示:设备树里如果没写
resets属性,用reset_control_get_optional()系列可以拿到一个空句柄,后续所有操作都变成 no-op,这样驱动可以同时兼容有复位线和没复位线的平台,不用写一堆 if 判断。
2.4 与 clock、regulator 的依赖顺序
复位操作往往不是孤立的。一个典型的上电序列是:先开 regulator 供电,再开 clock,然后 deassert reset,最后配置寄存器。顺序错了就可能访问不了寄存器或者模块工作异常。
reset 框架本身不负责编排这个顺序,它只提供操作原语。编排的责任在 consumer 驱动或者更上层的 power domain 框架。但框架提供了reset_control_reset()这个"自复位"操作,它会先 assert 再 deassert,中间带一个可配置的延时,适合那些需要脉冲复位信号的场景。这个延时通常由 provider 的reset_us字段指定,单位是微秒。
3. 核心细节解析与实操要点
3.1 provider 侧:如何实现一个复位控制器驱动
写一个复位控制器驱动,核心是实现struct reset_control_ops里的几个回调。最基础的是assert和deassert,分别对应拉低和释放复位信号。如果你的硬件支持自复位脉冲,还可以实现reset;如果支持读取当前复位状态,实现status。
static int my_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct my_reset_data *data = to_my_reset(rcdev); u32 val; /* 读取-修改-写,避免影响其他位 */ val = readl(data->base + MY_RESET_REG); val |= BIT(id); writel(val,>my_device: my-device@10000000 { compatible = "vendor,my-device"; reg = <0x10000000 0x1000>; clocks = <&clk_ctrl 10>; resets = <&rst_ctrl 5>; reset-names = "my_reset"; };reset-names是可选的,如果只有一条复位线,驱动里用reset_control_get(dev, NULL)就能拿到。如果有多条,就得用名字来区分,比如reset_control_get(dev, "my_reset")。
驱动里的典型用法:
struct reset_control *rst; rst = devm_reset_control_get_exclusive(dev, "my_reset"); if (IS_ERR(rst)) return PTR_ERR(rst); /* 上电流程:先确保复位是拉低的,再释放 */ reset_control_assert(rst); udelay(10); reset_control_deassert(rst);用devm_前缀的好处是资源会自动释放,不用在 remove 里手动 put,减少泄漏风险。但要注意,devm_reset_control_get拿到的句柄在驱动 unbind 时会自动释放,如果你的复位线是共享的,这个自动释放会减少引用计数,行为是正确的。
3.3 assert 与 deassert 的时序陷阱
很多人以为 assert 就是拉低、deassert 就是拉高,调用完立即生效。实际上这里有几个时序陷阱。
第一,写寄存器到硬件真正响应之间有延迟。有些复位控制器需要几个时钟周期才能让复位信号稳定,如果你的代码 assert 完立刻 deassert,硬件可能根本没识别到复位脉冲。解决办法是在 assert 和 deassert 之间加足够的延时,具体多少看芯片手册,通常几微秒到几十微秒。
第二,deassert 之后模块不一定立即就绪。有些模块释放复位后需要等待内部 PLL 锁定或者状态机初始化,这段时间访问寄存器可能返回错误。稳妥的做法是 deassert 后加一个延时,或者轮询某个状态位直到就绪。
第三,共享复位线的引用计数语义。如果你用 shared 模式,assert 会把引用计数减一,只有减到零才真正拉低。这意味着你的 assert 可能不会立即生效,得确认没有其他持有者。
注意:在原子上下文(比如中断处理里)调用 reset API 要小心,某些 provider 的实现可能带睡眠操作(比如 regmap 走 I2C/SPI 总线)。如果 provider 的 ops 里用了可能睡眠的函数,就不能在原子上下文调用。
3.4 复位与时钟、电源的配合顺序
一个经过验证的上电顺序模板是这样的:
- 使能 regulator,等待电压稳定
- 使能模块时钟,等待时钟稳定
- assert 复位(确保从确定状态开始)
- 延时若干微秒
- deassert 复位
- 延时等待模块就绪
- 配置模块寄存器
下电顺序反过来:先 assert 复位让模块停止工作,再关时钟,最后关电源。这个顺序能避免模块在时钟还在跳但电源已经掉的情况下产生不确定行为。
有些平台把复位控制集成在 power domain 里,power domain 上电时自动 deassert 复位,下电时自动 assert。这种情况下 consumer 驱动就不需要显式操作复位了,但你要清楚这个自动行为的存在,否则可能重复操作导致时序混乱。
4. 实操过程与核心环节实现
4.1 从零实现一个简单的复位控制器驱动
假设我们有一个虚拟的复位控制器,基地址 0x10000000,有 32 条复位线,寄存器布局是:偏移 0x00 是复位控制寄存器,写 1 拉低复位,写 0 释放;偏移 0x04 是复位状态寄存器,读回当前复位状态。
先定义私有数据结构:
struct my_rst_priv { struct reset_controller_dev rcdev; void __iomem *base; struct clk *clk; spinlock_t lock; };注意这里用了 spinlock,因为寄存器操作是原子的,多个 consumer 可能并发调用。如果你的控制器操作可能睡眠,就改用 mutex。
probe 函数里做几件事:映射寄存器、使能控制器自己的时钟(复位控制器本身也需要时钟才能工作)、初始化 spinlock、填充 rcdev、注册。
static int my_rst_probe(struct platform_device *pdev) { struct my_rst_priv *priv; struct resource *res; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); priv->clk = devm_clk_get(&pdev->dev, NULL); if (IS_ERR(priv->clk)) return PTR_ERR(priv->clk); clk_prepare_enable(priv->clk); spin_lock_init(&priv->lock); priv->rcdev.ops = &my_reset_ops; priv->rcdev.owner = THIS_MODULE; priv->rcdev.nr_resets = 32; priv->rcdev.of_node = pdev->dev.of_node; priv->rcdev.of_reset_n_cells = 1; return devm_reset_controller_register(&pdev->dev, &priv->rcdev); }of_reset_n_cells = 1表示设备树里resets属性每条线用一个 cell 描述索引。如果硬件需要额外参数(比如复位类型),可以设成 2 并自定义of_xlate。
4.2 assert/deassert 的加锁实现
并发场景下,两个 consumer 同时操作不同复位线,如果直接读改写会互相覆盖。所以要在读改写之间加锁:
static int my_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct my_rst_priv *priv = to_my_rst(rcdev); unsigned long flags; u32 val; spin_lock_irqsave(&priv->lock, flags); val = readl(priv->base + MY_RST_CTRL); val |= BIT(id); writel(val, priv->base + MY_RST_CTRL); spin_unlock_irqrestore(&priv->lock, flags); return 0; }用spin_lock_irqsave是因为复位操作可能从中断上下文发起,要防止中断里再拿同一把锁导致死锁。如果你的控制器确定只在进程上下文用,用普通 spin_lock 也行,但保险起见还是 irqsave。
4.3 设备树绑定与 of_xlate 解析
设备树绑定文档要写清楚compatible、reg、#reset-cells这些必需属性。#reset-cells = <1>表示每条复位线用一个 cell 描述。
如果硬件复位线索引不是简单的线性映射,比如需要区分"复位类型"和"复位索引",可以自定义 of_xlate:
static int my_reset_xlate(struct reset_controller_dev *rcdev, const struct of_phandle_args *spec) { /* spec->args[0] 是复位类型,spec->args[1] 是索引 */ if (spec->args[0] > MY_RST_TYPE_MAX) return -EINVAL; return spec->args[1]; }返回的值就是后续 assert/deassert 收到的id。这样 consumer 在设备树里写resets = <&rst_ctrl 1 5>就能表达"类型 1 的第 5 条线"。
4.4 用 debugfs 观察复位线状态
调试阶段,能实时看到每条复位线的状态非常有用。reset 框架提供了 debugfs 支持,挂载 debugfs 后可以在/sys/kernel/debug/reset/下看到每个控制器的信息,包括每条线的名字、当前是否 assert、被哪些设备持有。
如果框架自带的 debugfs 信息不够,可以在自己的 provider 里加自定义 debugfs 节点,把状态寄存器的原始值打出来。我调一个音频模块的复位问题时,就是靠对比状态寄存器和预期值,发现有一条复位线被 bootloader 拉低后没释放,导致内核里 deassert 也没用——因为硬件上有个 override 位没清。
5. 常见问题与排查技巧实录
5.1 复位不生效的几种典型原因
复位操作调用了但没效果,按下面这个顺序排查效率最高。
先确认 provider 的 assert/deassert 真的被调到了。在函数入口加pr_info或者用 ftrace 跟踪,如果没被调用,说明 consumer 拿到的句柄是空的(optional 模式且设备树没配),或者 of_xlate 返回了错误。
再确认寄存器写对了。用 devmem 直接读寄存器,对比 assert 前后的值。如果值没变,可能是时钟没开导致寄存器写不进去,或者基地址映射错了。
然后确认硬件层面没有 override。有些 SoC 有安全域或者 bootloader 配置的复位 override 位,内核写的值被硬件忽略。这种情况得先清 override 位,或者确认该复位线是否被硬件自动管理。
最后确认时序。assert 和 deassert 之间延时不够,硬件可能识别不到。把延时加大到毫秒级试试,如果好了就是时序问题。
5.2 共享复位线的引用计数踩坑
用reset_control_get_shared()拿到的句柄,assert 和 deassert 是引用计数语义。我遇到过一个案例:两个驱动共享一条复位线,驱动 A 先 probe,deassert 了;驱动 B 后 probe,也 deassert。然后驱动 A remove,调用 assert,结果硬件上复位被拉低了,驱动 B 还在工作,直接挂掉。
问题在于驱动 A 以为自己的 assert 只影响自己,实际上共享模式下 assert 会立即拉低硬件复位(因为引用计数从 2 减到 1,但框架的实现是只要有人 assert 就拉低)。正确的做法是:共享复位线的 assert/deassert 要成对出现,且驱动不应该在运行时随意 assert,只在初始化时 deassert 一次,remove 时 assert 一次。
提示:如果多个驱动需要独立控制同一条复位线,说明硬件设计上这条线不该共享,应该拆成多条。共享语义只适合"大家一起上电、一起下电"的场景。
5.3 复位控制器自身 probe 失败的排查
复位控制器驱动 probe 失败,常见原因是它依赖的时钟或电源还没就绪。用-EPROBE_DEFER返回让内核稍后重试是标准做法。但如果一直 defer,就要检查依赖链是不是有循环。
另一个坑是寄存器访问权限。有些复位控制器在安全域,非安全模式访问会触发 fault。这种情况要么在安全模式初始化,要么通过 SMC 调用转发。排查时看 dmesg 里有没有 "Unhandled fault" 或者 "SError" 之类的信息。
还有一种情况是设备树里#reset-cells写错了,导致 of_xlate 解析出的 id 越界。框架会在注册时检查nr_resets,但 of_xlate 返回越界值只会在实际调用时报错。建议在 of_xlate 里加边界检查。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| assert 无效果 | 句柄为空 | 检查设备树 resets 属性 | 补上属性或用 optional |
| assert 无效果 | 寄存器写不进 | devmem 读回对比 | 开时钟、查基地址 |
| assert 无效果 | 硬件 override | 读 override 寄存器 | 清 override 位 |
| deassert 后模块不工作 | 延时不够 | 加大延时测试 | 按手册加延时 |
| 共享线互相干扰 | 引用计数语义 | 看 debugfs 持有者 | 改独占或拆分 |
| probe 一直 defer | 依赖未就绪 | 看依赖链 | 检查时钟/电源驱动 |
| 访问寄存器 fault | 安全域限制 | 看 dmesg fault 信息 | 安全模式初始化 |
5.5 几个我踩过的坑和独家技巧
第一个坑:在reset_control_deassert()之后立刻访问模块寄存器,结果读到全 0。原因是模块内部状态机还没初始化完。后来在 deassert 后加了 100 微秒延时,问题消失。这个延时值因芯片而异,最好查手册的 "reset release to ready" 参数。
第二个坑:用devm_reset_control_get()在 probe 里拿句柄,但在 probe 失败路径上手动调用了reset_control_put(),导致 double free。devm 管理的资源不要手动释放,让内核统一处理。
第三个技巧:调试复位问题时,在 provider 的 assert/deassert 里加 tracepoint,用 trace-cmd 抓时间戳,能清楚看到每次操作的顺序和间隔。比加 printk 干净,也不会因为 printk 本身的延时干扰时序。
第四个技巧:如果怀疑是复位问题但不确定,可以临时在 consumer 驱动里把 deassert 去掉,看模块是否完全没反应。如果去掉 deassert 后模块反而正常了,说明复位线可能是低有效但被错误地当高有效处理,或者硬件上复位是自动释放的。
6. 复位框架与功耗管理的协同扩展
6.1 在系统 suspend/resume 中处理复位
系统进入 suspend 时,如果某个模块要下电,resume 时需要重新走一遍复位释放流程。标准做法是在驱动的suspend回调里 assert 复位(如果模块要下电),在resume回调里 deassert 并重新初始化。
但要注意,suspend 阶段可能处于原子上下文或者中断关闭状态,这时候调用可能睡眠的 reset API 会出问题。稳妥的做法是在suspend_late里操作,或者确认 provider 的 ops 不会睡眠。
另外,如果模块在 suspend 期间保持供电但时钟关闭,复位线通常不需要动。只有真正下电的模块才需要重新复位。判断依据是 power domain 的状态,不是简单的 suspend 深度。
6.2 runtime PM 场景下的复位时机
runtime PM 下,模块可能频繁上下电。每次上电都做完整复位会拖慢响应速度。优化思路是:只在第一次上电或者检测到异常时才复位,正常上下电只走时钟和电源开关。
实现上可以用一个标志位记录模块是否已经初始化过,runtime resume 时如果标志位已置就跳过复位。但要注意,如果模块在低功耗状态下丢失了配置,还是得重新复位。这个判断依赖具体硬件行为,没有通用答案。
6.3 复位框架的调试接口与性能考量
reset 框架的 debugfs 接口在CONFIG_RESET_CONTROLLER开启且 debugfs 挂载后可用。里面能看到每个控制器的复位线列表和状态。如果控制器很多,信息会比较杂,可以用grep过滤。
性能方面,reset API 的开销主要在 provider 的寄存器操作和加锁。如果 provider 走的是慢速总线(比如 I2C 扩展的复位控制器),每次操作可能几毫秒。这种场景下要避免在热路径里频繁调用 reset API,把复位操作集中到初始化阶段。
对于需要精确控制复位脉冲宽度的场景,reset_control_reset()配合 provider 的reset_us是最方便的。但如果 provider 没实现reset回调,框架会退化成 assert + udelay + deassert,延时用的是框架默认值,可能不满足硬件要求。所以 provider 最好显式实现reset并设置合适的reset_us。
6.4 从 reset 框架看 Linux 驱动模型的抽象哲学
reset 框架是 Linux 驱动模型"分离关注点"思想的一个典型样本。它把"复位控制器怎么操作"和"设备怎么用复位"彻底分开,中间用设备树做粘合。这个模式在 clock、regulator、pinctrl、GPIO、DMA 等子系统里反复出现,理解了其中一个,其他的都能触类旁通。
对驱动开发者来说,这意味着你写外设驱动时应该假设"复位、时钟、电源都是别人提供的资源",你只负责申请和使用。这种假设让驱动可以跨平台复用,也让平台适配的工作集中在 provider 驱动和设备树里。
对平台维护者来说,这意味着新增一个 SoC 支持时,主要工作是写 provider 驱动和填设备树,而不是改一堆外设驱动。这个分工是 Linux 能支持这么多平台的关键原因之一。
我在实际项目里最深的一点体会是:复位问题往往不是复位框架本身的问题,而是时序和依赖关系的问题。框架把操作抽象得很干净,但硬件上电顺序、延时要求、模块就绪判断这些"脏活"还是得驱动开发者自己处理。把 reset 框架用对,前提是你对硬件的上电时序有清晰的认识。如果手册里没写清楚,就老老实实拿示波器量,或者用最保守的延时先跑通,再逐步优化。