
1. 从功耗子系统里翻出 reset 框架为什么这两者会扯上关系第一次看到功耗子系统和reset 通用框架被放在同一个话题里很多人会愣一下——这俩不是八竿子打不着吗一个是管电的一个是管复位的。但如果你真正在 SoC 驱动层摸爬滚打过就会明白它们其实是同一根绳上的蚂蚱。先说清楚这个背景。Linux 内核里的reset 子系统drivers/reset/是一套通用的复位控制器框架它的核心目标是把某个硬件模块需要被复位一下这件事抽象成统一的 API让各个 SoC 厂商的复位控制器都能挂进来。而功耗子系统power domain、runtime PM、genpd 等管的是模块的上电、下电、时钟开关。问题来了一个模块从掉电状态恢复时往往需要先复位再上电或者上电后立即复位否则寄存器状态是脏的硬件行为不可预期。这就是为什么梳理 reset 通用框架会被归到功耗子系统这个系列里。它们共享同一套生命周期管理逻辑什么时候断电、什么时候复位、复位和上电的先后顺序怎么保证。我在做某国产 SoC 的显示子系统驱动时就吃过这个亏——display controller 从 power domain 下电再上电后如果不做一次 resetDMA 描述符指针会停在上一次的位置屏幕直接花屏。当时排查了两天才定位到是 reset 调用时机不对。这篇文章我打算把 reset 通用框架从里到外拆一遍框架的设计动机、核心数据结构、API 使用方式、和设备树Device Tree的绑定关系、和 power domain 的时序配合以及我在实际项目里踩过的几个坑。适合正在写 SoC 驱动、做 BSP 移植、或者想深入理解 Linux 设备驱动模型的读者。不管你是刚接触嵌入式 Linux 的新手还是已经写过几个驱动的老手应该都能从里面找到有用的东西。提示本文讨论的 reset 框架基于 Linux 主线内核的通用实现不同厂商的复位控制器驱动如 reset-simple、reset-sunxi、reset-imx 等在细节上会有差异但框架层的逻辑是一致的。2. reset 框架到底解决了什么问题从裸寄存器操作说起2.1 没有框架之前驱动作者是怎么写复位的在 reset 子系统成型之前驱动里做复位基本就是直接操作寄存器。比如某个 SoC 的显示模块复位寄存器在0x1c00000偏移0x20处bit 3 是复位位那代码大概长这样void __iomem *base ioremap(0x1c00000, 0x1000); u32 val readl(base 0x20); val | BIT(3); writel(val, base 0x20); udelay(10); val ~BIT(3); writel(val, base 0x20);这段代码能跑但问题一大堆。首先寄存器地址是硬编码的换个 SoC 就得改代码。其次复位位的极性不统一有的 SoC 是写 1 复位、写 0 释放有的是反过来驱动作者得去翻手册。第三多个驱动可能操作同一个复位控制器没有互斥保护并发场景下会互相踩。第四复位控制器本身可能是一个独立的设备它自己也需要被电源管理硬编码 ioremap 完全绕过了设备模型。我见过最离谱的一个 BSP三个驱动各自 ioremap 了同一块复位寄存器区域结果其中一个驱动在 suspend 时把整个区域 unmap 了另外两个驱动 resume 时直接 oops。这种问题在没有统一框架的年代非常常见。2.2 reset 框架的抽象层次reset 框架做的事情本质上是把复位这个动作抽象成一个消费者-提供者consumer-provider模型这和 clk、regulator、gpio 子系统的设计思路是一脉相承的。提供者provider复位控制器驱动负责实现具体的寄存器操作注册一个struct reset_controller_dev。消费者consumer需要使用复位的设备驱动通过reset_control_get()拿到一个struct reset_control句柄然后调用reset_control_assert()/reset_control_deassert()来操作。中间层reset 核心drivers/reset/core.c负责匹配、引用计数、锁保护、设备树解析。这样带来的好处是驱动作者不再关心寄存器在哪、极性如何只需要在设备树里声明我用到了哪个复位代码里调用标准 API 即可。换 SoC 时改设备树就行驱动代码一行不用动。2.3 复位和时钟、电源的关系这里要特别强调一个概念复位不等于断电。复位是把模块的内部状态机拉回初始态但模块可能仍然有电、有时钟。而断电是彻底切断供电。两者在时序上是有讲究的。典型的模块启动序列是这样的打开电源power domain on使能时钟clk enable释放复位reset deassert配置寄存器、启动工作而关闭序列反过来停止工作断言复位reset assert关闭时钟关闭电源注意第 2 步和第 3 步的顺序必须先有时钟复位释放才有意义。如果时钟没开就 deassert reset模块可能进入一个不确定状态。这个坑我在一个音频子系统上踩过当时是时钟和复位的顺序写反了导致 I2S 偶尔出不了声概率大概十分之一非常难查。3. 核心数据结构拆解reset_controller_dev 和 reset_control3.1 reset_controller_dev提供者视角复位控制器驱动注册的核心结构是struct reset_controller_dev定义在include/linux/reset-controller.h。关键字段如下struct reset_controller_dev { const struct reset_control_ops *ops; struct module *owner; struct list_head list; struct device_node *of_node; struct device *dev; unsigned int nr_resets; ... };其中ops是最重要的它定义了具体的复位操作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 delay deassert 的便捷操作assert和deassert是分开的原子操作。status用来查询当前复位状态不是所有控制器都实现。nr_resets表示这个控制器管多少个复位线of_node用于设备树匹配。注册函数是reset_controller_register()注销是reset_controller_unregister()。3.2 reset_control消费者视角消费者拿到的是struct reset_control这个结构对驱动作者来说基本是黑盒只需要知道它是个句柄。获取句柄的 API 有几个变体API用途是否必须reset_control_get(dev, id)按名字获取必须存在是devm_reset_control_get(dev, id)带设备管理的版本推荐reset_control_get_optional(dev, id)可选不存在返回 NULL否devm_reset_control_get_optional(dev, id)可选 devm推荐reset_control_get_exclusive(dev, id)独占获取视场景reset_control_get_shared(dev, id)共享获取视场景强烈建议用 devm 版本这样驱动卸载时框架会自动释放引用不用手写 release 逻辑。我见过太多驱动在 remove 路径里忘了reset_control_put()导致引用计数泄漏模块重新加载时拿不到复位线。exclusive和shared的区别在于独占模式下同一根复位线只能被一个消费者持有共享模式下可以多个消费者持有引用计数归零时才真正释放。对于大多数场景用默认的reset_control_get就够了它内部会根据设备树里的reset-names和共享属性来决定。3.3 引用计数与锁保护reset 核心内部对每个reset_control维护了引用计数对每个reset_controller_dev维护了自旋锁。这意味着多个驱动共享同一根复位线时只有最后一个释放的才会真正 deassert。assert/deassert 操作是原子的不会因为并发调用而错乱。但要注意引用计数只保护句柄层面不保护硬件时序。如果你在驱动里 assert 之后立刻 deassert中间没有足够的延时硬件可能还没完成复位。这个延时通常由控制器驱动的reset回调内部处理但如果你用的是分开的 assert/deassert就得自己加udelay。4. 设备树绑定复位线是怎么被描述的4.1 提供者节点的写法复位控制器在设备树里是一个独立节点通常带#reset-cells属性。以 reset-simple 为例rst: reset-controller10000000 { compatible soc,reset-simple; reg 0x10000000 0x1000; #reset-cells 1; };#reset-cells 1表示引用这个控制器时需要一个参数复位线编号。有些控制器需要两个参数比如编号 标志位那就是2。4.2 消费者节点的写法设备节点里通过resets属性引用复位线display12000000 { compatible soc,display; reg 0x12000000 0x10000; resets rst 5, rst 6; reset-names axi, core; clocks clk 10; power-domains pd 2; };resets里的每个条目对应一根复位线reset-names给它们起名字。驱动里就可以用devm_reset_control_get(dev, axi)按名字拿。这里有个容易忽略的点resets和reset-names的顺序必须一一对应。如果顺序写反了驱动拿到的句柄就指向错误的复位线后果可能是复位了不该复位的模块或者该复位的没复位。我在一个项目里见过reset-names少写了一个结果驱动按名字拿的时候拿到 NULL直接 probe 失败。4.3 复位线和时钟、电源域的绑定关系一个模块往往同时有clocks、resets、power-domains三个属性。它们之间的时序由驱动自己控制框架不会自动帮你排序。但有一个约定俗成的规则power-domains由 genpd 框架在 runtime PM 时自动管理。clocks由 clk 框架管理驱动在 probe 时通常用clk_prepare_enable。resets由驱动显式调用。所以驱动 probe 的典型顺序是先pm_runtime_enable和pm_runtime_get_sync触发 power domain 上电再clk_prepare_enable最后reset_control_deassert。这个顺序不能乱。5. 实操写一个最小可用的复位控制器驱动5.1 基于 reset-simple 的快速实现如果你的 SoC 复位控制器就是简单的寄存器某位写 1 复位、写 0 释放那可以直接用内核自带的reset-simple驱动只需要在设备树里描述清楚寄存器布局即可。它的 compatible 是reset-simple支持reg、#reset-cells、以及可选的reset-simple-assert-offset等属性。但很多 SoC 的复位控制器没那么规整比如复位位分散在多个寄存器、极性不统一、需要先解锁某个保护寄存器。这时候就得自己写驱动。5.2 手写一个复位控制器驱动下面是一个简化版的驱动骨架假设复位寄存器是一个 32 位寄存器每个 bit 对应一根复位线写 1 复位、写 0 释放#include linux/module.h #include linux/platform_device.h #include linux/reset-controller.h #include linux/io.h #include linux/of.h struct my_reset { struct reset_controller_dev rcdev; void __iomem *base; spinlock_t lock; }; static int my_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct my_reset *priv container_of(rcdev, struct my_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 my_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id) { struct my_reset *priv container_of(rcdev, struct my_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 my_reset_status(struct reset_controller_dev *rcdev, unsigned long id) { struct my_reset *priv container_of(rcdev, struct my_reset, rcdev); return !!(readl(priv-base) BIT(id)); } static const struct reset_control_ops my_reset_ops { .assert my_reset_assert, .deassert my_reset_deassert, .status my_reset_status, }; static int my_reset_probe(struct platform_device *pdev) { struct my_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 my_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; return devm_reset_controller_register(pdev-dev, priv-rcdev); } static const struct of_device_id my_reset_of_match[] { { .compatible my,soc-reset }, { } }; MODULE_DEVICE_TABLE(of, my_reset_of_match); static struct platform_driver my_reset_driver { .probe my_reset_probe, .driver { .name my-soc-reset, .of_match_table my_reset_of_match, }, }; module_platform_driver(my_reset_driver);几个关键点用devm_reset_controller_register省去手动注销。加自旋锁因为 assert/deassert 可能在中断上下文被调用。nr_resets要设对设小了消费者拿不到高位复位线设大了可能越界访问。status回调可选但实现了以后调试很方便。5.3 消费者驱动怎么用消费者侧代码更简单struct reset_control *rst; rst devm_reset_control_get_optional(dev, core); if (IS_ERR(rst)) return PTR_ERR(rst); if (rst) { reset_control_assert(rst); udelay(10); reset_control_deassert(rst); }注意udelay(10)这个延时。很多 SoC 手册会规定复位脉冲的最小宽度比如 1us 或 10us。如果控制器驱动的reset回调里没做延时消费者就得自己加。我一般习惯在 assert 和 deassert 之间加 10us这个值对绝大多数模块都够用。6. 和功耗子系统的时序配合几个真实的坑6.1 坑一power domain 下电后复位状态丢失有些 SoC 的复位寄存器本身也在某个 power domain 里。当这个 domain 下电时复位寄存器的值会丢失重新上电后复位状态变成默认值可能是全复位。这时候如果驱动以为复位已经释放了直接去访问模块寄存器就会失败。解决办法是在 power domain 的power_on回调里重新配置复位状态或者在驱动 resume 时重新 deassert。我倾向于后者因为驱动自己最清楚需要什么状态。6.2 坑二reset 和 clk 的顺序反了前面提过deassert reset 之前必须有时钟。但有些模块比较特殊assert reset 之前反而要先关时钟否则复位过程中时钟还在翻转可能导致模块进入异常状态。这个要看具体 SoC 手册。我的经验是上电顺序 power → clk → deassert reset下电顺序 assert reset → clk off → power off。这个顺序对 90% 的模块都适用。剩下 10% 需要看手册特殊处理。6.3 坑三共享复位线的引用计数如果两个驱动共享同一根复位线都用reset_control_get拿句柄那么驱动 A assert硬件复位。驱动 B assert引用计数加一硬件仍然复位。驱动 A deassert引用计数减一但硬件不复位因为 B 还持有。驱动 B deassert引用计数归零硬件释放复位。这个行为在大多数场景下是对的但如果驱动 A 期望自己 deassert 后硬件立即释放就会出问题。这时候应该用reset_control_get_exclusive让第二个驱动拿不到句柄从而暴露设计冲突。6.4 坑四reset 控制器自身的 probe 顺序reset 控制器驱动本身也是一个 platform driver它也需要被 probe。如果消费者驱动比它先 probedevm_reset_control_get会返回-EPROBE_DEFER。这是正常的驱动框架会自动重试。但如果 reset 控制器的 probe 一直失败消费者就会一直 defer最终超时。排查这类问题的关键是看dmesg里有没有probe defer相关的日志以及 reset 控制器节点的status是不是okay。我遇到过一次是 reset 控制器的时钟没在设备树里配导致它自己 probe 失败连累了下面一堆设备。7. 调试手段怎么确认复位真的生效了7.1 用 debugfs 查看复位状态如果 reset 控制器驱动实现了status回调内核会在/sys/kernel/debug/reset/下暴露每个控制器的状态。可以直接cat查看cat /sys/kernel/debug/reset/my-soc-reset输出会列出每根复位线的编号和当前状态。这是最直接的验证手段。7.2 用 devmem 直接读寄存器如果 debugfs 没暴露可以用devmem工具直接读复位寄存器devmem 0x10000000 32对比 assert 和 deassert 前后的值就能确认操作是否生效。注意devmem需要 root 权限而且地址必须是物理地址。7.3 示波器抓复位引脚对于硬件工程师来说最可靠的办法是用示波器抓复位引脚的波形。软件层面看到的寄存器值对不代表硬件引脚真的翻转了——中间可能隔着 level shifter、GPIO 扩展芯片等。我在一个项目里就遇到过软件读回来是对的但复位引脚因为外部上拉电阻太大实际没拉低的情况。7.4 常见错误码对照错误码含义排查方向-EPROBE_DEFER控制器还没 probe检查控制器节点 status、依赖-ENOENT设备树里没找到对应复位线检查 resets/reset-names-ENOMEM内存分配失败一般不会除非内存耗尽-EINVAL参数错误检查 id 是否越界-EBUSY复位线被独占检查是否有其他驱动持有8. 写在最后一些个人体会reset 框架看起来简单但真正用好它需要对整个设备生命周期有清晰的认识。我个人的习惯是在驱动 probe 的最开始就拿复位句柄在硬件初始化之前做一次完整的 assert-deassert 循环确保模块从一个干净的状态开始。这个习惯帮我避免了很多偶发问题。另外不要迷信框架的自动化。reset 框架能帮你管理引用计数和并发但硬件时序永远是你自己的责任。延时该加就加顺序该查手册就查手册别偷懒。我在音频子系统上踩的那个时钟顺序的坑就是因为想当然地以为先复位再开时钟也行结果被现实教育了两天。最后一个建议如果你在做一个新 SoC 的 BSP先把 reset 控制器驱动跑通再去做其他外设。因为几乎所有外设都依赖复位reset 不通后面全是连锁反应。这个顺序能帮你省下大量排查时间。