
1. 功耗子系统里的 reset 框架到底在管什么搞嵌入式 Linux 的人尤其是做 SoC 底层驱动的迟早会撞上 reset 子系统。你打开一颗主流 SoC 的 datasheet会发现上面密密麻麻标着几十甚至上百个复位源USB PHY 复位、以太网 MAC 复位、显示控制器复位、音频子系统复位、各种外设总线复位。这些复位信号如果每个驱动都自己写寄存器去操作代码会乱成一锅粥而且不同 SoC 之间完全没法复用。Linux 内核的 reset 通用框架就是为了解决这个问题而生的它把复位控制器抽象成一个 provider把需要复位的设备抽象成 consumer中间通过设备树或者 ACPI 来建立映射关系。这个框架在功耗子系统里扮演的角色特别关键。因为很多低功耗场景本质上就是关掉某个模块的时钟和电源然后给它一个复位脉冲让它回到确定的初始状态。比如系统进入 suspend 的时候某些外设需要先 assert reset 再 deassert确保唤醒后状态干净。又比如运行时电源管理Runtime PM里设备挂起时如果只是关时钟寄存器状态可能还残留着下次恢复时行为不可预测这时候 reset 就派上用场了。我见过不少项目驱动工程师图省事直接在代码里写writel(0x1, base 0x10)去操作复位寄存器。短期看没问题但一旦 SoC 换型号、复位寄存器偏移变了或者多个驱动共享同一个复位控制器需要互斥保护这种硬编码就会变成灾难。reset 框架的价值就在于它提供了统一的 API、引用计数、互斥锁、以及设备树绑定规范让复位操作变得可描述、可复用、可调试。这篇文章我会从框架设计思路、核心数据结构、设备树绑定、API 使用、常见坑几个维度把 reset 通用框架彻底梳理一遍。适合正在写 SoC 驱动、调试低功耗流程、或者准备给新芯片移植 reset 驱动的朋友。读完之后你应该能自己写一个简单的 reset controller 驱动也能看懂内核里那些devm_reset_control_get到底在干什么。2. reset 框架的整体设计与核心抽象2.1 为什么需要 provider 和 consumer 两层模型Linux 内核里很多子系统都采用 provider/consumer 模型比如 clock、regulator、pinctrl、gpio。reset 框架也遵循这个套路原因很直接复位控制器是硬件资源通常一个 SoC 里有一个或多个复位控制器模块每个模块管理一组复位线。而使用这些复位线的设备驱动遍布各个子系统它们不应该关心复位控制器内部寄存器怎么排布只需要说我要复位我自己。provider 层对应的是struct reset_controller_dev它描述一个复位控制器实例里面包含操作函数集struct reset_control_ops、复位线数量、of_node 指针、以及一个struct list_head用于挂到全局链表。consumer 层对应的是struct reset_control它代表某个设备对某条复位线的引用里面记录了对应的控制器指针、复位线 ID、是否共享、是否独占等信息。这种分层带来的好处是驱动代码里只需要reset_control_assert(rst)和reset_control_deassert(rst)完全不碰寄存器。控制器驱动负责把这两个抽象操作翻译成具体的寄存器读写。SoC 换了只要换控制器驱动上层设备驱动一行不用改。2.2 核心数据结构拆解先看struct reset_controller_dev定义在include/linux/reset-controller.hstruct reset_controller_dev { const struct reset_control_ops *ops; struct module *owner; struct list_head list; struct list_head reset_control_head; struct device *dev; struct device_node *of_node; int of_reset_n_cells; int of_xlate(struct reset_controller_dev *rcdev, const struct of_phandle_args *reset_spec); unsigned int nr_resets; };几个关键字段值得展开说。ops是操作函数集至少要实现assert和deassert可选实现resetassert 后延时再 deassert、status查询当前复位状态。nr_resets是这个控制器管理的复位线总数通常等于寄存器位宽乘以寄存器个数。of_reset_n_cells和of_xlate决定了设备树里怎么描述一条复位线后面讲设备树绑定时会细说。再看struct reset_control定义在include/linux/reset.hstruct reset_control { struct reset_controller_dev *rcdev; struct list_head list; unsigned int id; struct kref refcnt; bool shared; bool array; atomic_t deassert_count; atomic_t triggered_count; };id就是这条复位线在控制器里的编号。shared表示这条复位线是否被多个 consumer 共享共享模式下引用计数逻辑不一样。deassert_count和triggered_count是两个原子计数器用来处理嵌套调用和共享场景下的状态一致性。refcnt是 kref管理这个 reset_control 结构本身的生命周期。2.3 引用计数与共享复位线的处理逻辑复位线共享是个容易出 bug 的地方。假设一条复位线同时被 USB 控制器和 PHY 驱动引用如果 USB 驱动 deassert 之后 PHY 驱动又 assert那 USB 就挂了。reset 框架用deassert_count来解决每次 deassert 计数加一assert 计数减一只有计数从 0 变 1 时才真正操作硬件从 1 变 0 时才真正 assert。但这里有个前提共享的复位线必须在获取时显式声明shared标志也就是用reset_control_get_shared或者设备树里加reset-names配合devm_reset_control_get_shared。如果两个驱动都用独占方式获取同一条复位线第二个会直接失败返回-EBUSY。这个设计是为了防止误用因为独占语义下框架假设只有你一个人在操作这条线。triggered_count则是给reset_control_reset用的。这个 API 表示执行一次完整的 assert-deassert 脉冲有些硬件要求这种脉冲操作不能被嵌套框架用这个计数器来检测非法嵌套并给出警告。3. 设备树绑定与 of_xlate 机制详解3.1 reset 控制器的设备树节点写法一个典型的 reset 控制器节点长这样rst: reset-controller10000000 { compatible vendor,foo-reset; reg 0x10000000 0x1000; #reset-cells 1; };#reset-cells 1表示引用这条控制器下的某条复位线时需要提供一个额外的 cell 来指定线号。有些复杂控制器可能需要两个 cell比如第一个 cell 指定寄存器组第二个指定位偏移那#reset-cells 2。consumer 侧的写法usb0: usb20000000 { compatible vendor,foo-usb; reg 0x20000000 0x10000; resets rst 5; reset-names usb-reset; };resets属性里rst 5表示引用 rst 控制器下编号为 5 的复位线。reset-names给这条复位线起了个名字驱动里可以用devm_reset_control_get(dev, usb-reset)按名字获取比按索引获取更可读、更不容易出错。3.2 of_xlate 的默认实现与自定义场景当 consumer 引用一条复位线时框架需要把设备树里的 phandle 参数翻译成控制器内部的线号。默认的翻译函数是of_reset_simple_xlate逻辑很简单检查#reset-cells是否为 1然后直接取第一个 cell 作为 id再检查 id 是否小于nr_resets。int of_reset_simple_xlate(struct reset_controller_dev *rcdev, const struct of_phandle_args *reset_spec) { if (WARN_ON(reset_spec-args_count ! rcdev-of_reset_n_cells)) return -EINVAL; if (reset_spec-args[0] rcdev-nr_resets) return -EINVAL; return reset_spec-args[0]; }大多数控制器用这个默认实现就够了。但有些场景需要自定义比如复位线号不是线性排列的或者需要根据多个 cell 组合计算。这时候在控制器驱动里把rcdev-of_xlate指向自己的函数即可。我遇到过一颗国产 SoC它的复位寄存器不是连续排布的每 32 条线之间有个保留区线号需要做映射转换就是用自定义 of_xlate 解决的。3.3 设备树绑定的常见错误与排查设备树写错是 reset 框架报错的高频原因。最常见的三种第一种#reset-cells和引用时的参数个数不匹配。比如控制器写了#reset-cells 1consumer 写resets rst 5 0多给了一个参数解析时直接失败。内核启动日志里会打印of_parse_phandle_with_args相关的错误。第二种phandle 指向了错误的节点。比如把 clock 控制器的 phandle 写到了 resets 属性里框架会尝试调用 clock 控制器的 of_xlate结果类型不匹配崩溃。这种错误在编译 dtb 时不会报只有运行时才暴露。第三种reset-names和resets数量不一致。如果驱动按名字获取框架会遍历reset-names找到对应索引再去resets里取对应项。数量对不上会导致获取到错误的复位线或者直接返回-ENOENT。排查这类问题我通常先看/sys/kernel/debug/reset或者/sys/kernel/debug/clk类似路径下有没有 reset 相关的调试信息然后确认of_node是否正确解析。内核命令行加initcall_debug和of_debug也能帮助定位。4. reset 框架 API 全解析与实操要点4.1 获取与释放 reset control 的几种方式内核提供了多个获取 API选哪个取决于你的使用场景API用途是否自动释放是否共享reset_control_get按索引获取否否reset_control_get_shared按索引获取共享线否是devm_reset_control_get按索引获取设备生命周期管理是否devm_reset_control_get_shared按索引获取共享线自动释放是是devm_reset_control_get_exclusive显式独占获取是否devm_reset_control_get_optional可选获取不存在不报错是否新手最容易犯的错是用reset_control_get然后忘记reset_control_put导致内存泄漏和引用计数错乱。我的建议是只要在 probe 函数里获取一律用devm_前缀的版本让设备模型帮你管理生命周期。只有在极少数需要在非 probe 上下文动态获取释放的场景才用非 devm 版本。optional版本也很有用。有些设备的复位线在部分硬件配置下不存在比如某个外设的复位线在低配版芯片里被裁掉了。用 optional 获取返回NULL时不报错驱动里判断一下if (rst)再操作即可避免为了兼容性写一堆 ifdef。4.2 assert、deassert、reset 三个核心操作获取到 reset control 之后三个核心操作int reset_control_assert(struct reset_control *rst); int reset_control_deassert(struct reset_control *rst); int reset_control_reset(struct reset_control *rst);assert表示拉低复位信号让设备处于复位状态。deassert表示释放复位让设备开始工作。reset表示执行一次完整的复位脉冲通常是 assert 之后延时一段时间再 deassert。这里有个硬件层面的细节很多 SoC 的复位信号是低电平有效也就是说 assert 实际上是写 0 到寄存器位deassert 是写 1。但框架层面不关心极性控制器驱动在ops-assert里处理极性转换。所以你在设备驱动里看到reset_control_assert就理解为让设备进入复位不用管寄存器实际写的是 0 还是 1。reset_control_reset的使用要谨慎。它内部会调用ops-reset如果控制器驱动没实现这个回调框架会退化成 assert udelay deassert。但延时时间是多少框架默认用RESET_CONTROL_DELAY_US通常是 10 微秒。有些硬件需要更长的复位脉冲比如 1 毫秒这时候要么控制器驱动自己实现ops-reset并加足够延时要么设备驱动自己 assert、usleep_range、deassert 三步走。4.3 批量操作与数组式 reset control有些设备有多条复位线比如一个显示子系统可能有核心复位总线复位像素时钟域复位三条线。逐条获取逐条操作很啰嗦框架提供了数组式 APIstruct reset_control *devm_reset_control_array_get(struct device *dev, bool shared, bool optional); int reset_control_assert_array(struct reset_control *rst); int reset_control_deassert_array(struct reset_control *rst);数组式获取会把设备树里resets属性列出的所有复位线打包成一个 reset_control操作时一次性全部 assert 或 deassert。这在需要保证多条复位线操作顺序一致的场景下特别有用避免逐条操作时中间被其他驱动插入操作。不过要注意数组式操作不保证原子性它只是循环调用单条操作。如果硬件要求多条线严格同时变化那得靠控制器驱动提供批量操作回调目前框架层面没有这个抽象。5. 自己动手写一个 reset 控制器驱动5.1 驱动骨架与 ops 实现假设我们有一颗虚拟 SoC复位控制器基地址 0x10000000有 32 条复位线每条线对应寄存器的一个 bit写 1 表示 deassert写 0 表示 assert。驱动骨架struct foo_reset { struct reset_controller_dev rcdev; void __iomem *base; struct spinlock lock; }; static int foo_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct foo_reset *priv container_of(rcdev, struct foo_reset, rcdev); unsigned long flags; u32 val; spin_lock_irqsave(priv-lock, flags); val readl(priv-base); val ~BIT(id); writel(val, priv-base); spin_unlock_irqrestore(priv-lock, flags); return 0; } static int foo_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id) { struct foo_reset *priv container_of(rcdev, struct foo_reset, rcdev); unsigned long flags; u32 val; spin_lock_irqsave(priv-lock, flags); val readl(priv-base); val | BIT(id); writel(val, priv-base); spin_unlock_irqrestore(priv-lock, flags); return 0; } static const struct reset_control_ops foo_reset_ops { .assert foo_reset_assert, .deassert foo_reset_deassert, };这里用 spinlock 而不是 mutex因为 reset 操作可能在中断上下文被调用比如某个设备在中断处理里需要复位自己。用 readl/writel 而不是直接指针解引用保证在不同架构上的内存序正确。5.2 probe 函数与注册流程static int foo_reset_probe(struct platform_device *pdev) { struct foo_reset *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); spin_lock_init(priv-lock); priv-rcdev.ops foo_reset_ops; priv-rcdev.owner THIS_MODULE; priv-rcdev.dev pdev-dev; priv-rcdev.of_node pdev-dev.of_node; priv-rcdev.nr_resets 32; priv-rcdev.of_reset_n_cells 1; return devm_reset_controller_register(pdev-dev, priv-rcdev); }devm_reset_controller_register会自动处理注销省去 remove 函数里的清理逻辑。of_reset_n_cells设为 1 表示设备树里每条复位线用一个 cell 描述。如果控制器支持多个寄存器组nr_resets要设成总线条数assert/deassert里根据 id 计算寄存器偏移和位偏移。5.3 注册后的调试与验证驱动注册成功后可以在/sys/kernel/debug/reset下看到控制器信息需要内核开启CONFIG_RESET_CONTROLLER和 debugfs。更直接的验证方式是写一个简单的 consumer 驱动获取一条复位线assert 之后读硬件寄存器确认 bit 被清零deassert 之后确认 bit 被置一。如果控制器注册失败常见原因有nr_resets为 0、ops为空、of_node为空但用了设备树匹配。内核日志里会打印reset_controller_register相关的错误码对照排查即可。6. 常见问题排查与避坑经验实录6.1 复位线获取失败的五种典型原因错误码原因排查方法-ENOENT设备树里没有 resets 属性或名字不匹配检查 dts 节点和 reset-names-EBUSY复位线已被独占获取确认是否需要 shared 获取-EINVALof_xlate 参数不合法id 越界检查 #reset-cells 和线号范围-EPROBE_DEFER控制器驱动还没加载确认控制器 probe 顺序必要时调整 initcall 等级-ENOMEM内存分配失败检查系统内存压力-EPROBE_DEFER是最常见的之一。如果 consumer 驱动比 controller 驱动先 probe获取复位线时会返回这个错误框架会自动把 consumer 驱动加入 deferred probe 列表等 controller 就绪后重试。但如果 controller 驱动本身 probe 失败consumer 会一直 defer表现为设备无法工作。这时候要先去排查 controller 驱动为什么没起来。6.2 复位后设备不工作的排查思路有时候 assert 再 deassert 之后设备就是不起来。我踩过的坑里排前三的是第一复位脉冲太短。有些外设要求复位信号保持至少 1 毫秒而框架默认的reset_control_reset只延时 10 微秒。解决办法是在控制器驱动的ops-reset里加足够的usleep_range或者在设备驱动里手动 assert、延时、deassert。第二复位释放和时钟使能的顺序错了。很多硬件要求先给时钟再释放复位反过来会导致设备处于不确定状态。这个顺序在驱动里要严格按 datasheet 来不能想当然。第三多条复位线之间的释放顺序有要求。比如总线复位必须先释放核心复位后释放否则总线访问会挂死。这种场景用数组式 API 一次性操作可能反而不对需要逐条按顺序操作。6.3 共享复位线的引用计数陷阱共享复位线用deassert_count做引用计数但有个陷阱如果 A 驱动 deassert 了两次比如 probe 里一次resume 里又一次计数变成 2然后 B 驱动 assert 一次计数变成 1硬件层面复位线仍然是 deassert 状态。A 驱动如果 suspend 时只 assert 一次计数变成 0硬件才真正 assert但 B 驱动可能还在使用这条线结果 B 就挂了。避免这个问题的原则是每次 deassert 必须对应一次 assert不能多也不能少。驱动里最好把 reset control 的操作封装成foo_enable/foo_disable这样的配对函数内部维护状态防止调用次数不匹配。6.4 低功耗场景下 reset 的使用建议在 suspend/resume 流程里用 reset 要特别小心。suspend 时 assert 复位线resume 时 deassert看起来合理但如果这条复位线是共享的其他设备可能不希望被复位。我的建议是suspend 流程里尽量只关时钟和电源不要动复位线除非 datasheet 明确要求。复位操作留给 probe 和 error recovery 场景。Runtime PM 场景下如果设备挂起时需要复位来清除状态那要在runtime_suspend回调里做并且确保runtime_resume里重新初始化设备。这里有个细节runtime PM 的回调可能在中断上下文被调用所以 reset 操作必须是原子安全的控制器驱动用 spinlock 而不是 mutex 就很重要。7. 从功耗子系统视角看 reset 框架的扩展方向reset 框架目前的设计偏向静态描述、运行时操作但在功耗管理越来越精细的趋势下有些扩展方向值得关注。比如把 reset 线和 power domain、clock domain 关联起来实现关电域时自动 assert 复位的硬件联动。这需要框架层面提供更丰富的描述能力目前主要靠控制器驱动自己实现。另一个方向是 reset 状态的查询和调试。现在ops-status是可选的很多控制器没实现导致调试时看不到某条复位线当前是 assert 还是 deassert。如果所有控制器都实现 status配合 debugfs 就能实时查看复位状态排查低功耗问题时效率会高很多。我在实际项目里的体会是reset 框架用好了能省大量重复代码但前提是控制器驱动要写扎实设备树要写准确API 调用要配对。这三样任何一样出问题都会表现为设备莫名其妙不工作而且往往没有明显报错排查起来很费时间。所以每次移植新 SoC我都会先写一个最小化的 reset 测试驱动把每条复位线都 assert/deassert 一遍确认硬件行为符合预期再往上叠业务驱动。这个习惯帮我省过至少两次通宵调试。