ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux reset 框架深度解析:从驱动实现到功耗联动调试

Linux reset 框架深度解析:从驱动实现到功耗联动调试 1. 从功耗子系统说起reset 框架到底在解决什么问题搞嵌入式 Linux 的兄弟应该都有体会SoC 里面最让人头疼的不是那些功能模块本身而是模块之间的依赖关系和状态管理。功耗子系统管的是“什么时候省电、什么时候全速跑”而 reset 框架管的是“什么时候把外设恢复到初始状态、什么时候放开让它跑”。这两个东西看起来是两回事实际上在 SoC 里面它们是紧密咬合的。我最早接触 reset 框架是在一颗国产 SoC 上做显示子系统的 bring-up。当时屏幕死活点不亮查了三天寄存器最后发现是 Display Controller 的 reset 线没放开。那时候我才意识到reset 不是简单地写个寄存器就完事了它背后有一整套框架在管理复位信号的申请、释放、时序和依赖关系。Linux 内核的 reset 子系统官方叫法是Reset Controller Framework代码主要在drivers/reset/目录下核心头文件是include/linux/reset.h和include/linux/reset-controller.h。它要解决的问题其实很朴素SoC 里面有几十甚至上百个复位信号线每个外设可能依赖一个或多个复位线有的复位线还分共享和独占有的需要保持一段时间才能释放有的必须按特定顺序操作。如果每个驱动都直接去操作寄存器那代码会乱成一锅粥而且跨平台移植基本不可能。这个框架的核心抽象就两个东西reset controller复位控制器和reset control复位控制句柄。前者描述一个硬件复位控制器能提供哪些复位线后者是具体驱动申请到的某一条复位线的操作句柄。驱动通过reset_control_get()拿到句柄通过reset_control_assert()和reset_control_deassert()来操作复位线的拉低和释放。为什么我要在功耗子系统系列里面专门聊 reset因为在实际项目里功耗管理和复位管理经常是联动的。比如一个模块要进入低功耗状态你可能需要先 assert reset 把它彻底关掉退出低功耗的时候又要先 deassert reset 再恢复时钟和电源。如果 reset 的时序搞错了轻则模块工作不正常重则整个 SoC 挂死。我见过最离谱的案例是一个 MIPI DSI 控制器因为 reset 释放太早导致内部状态机没初始化完就开始收数据直接把总线拉死。所以这篇文章的目标很明确把 Linux reset 通用框架的架构、API、设备树绑定、驱动实现和实际调试经验一次性讲透。不管你是刚接触嵌入式 Linux 的新手还是已经在做 SoC bring-up 的老手应该都能从里面找到对自己有用的东西。特别是那些正在调试国产 SoC、全志平台或者自己流片的芯片的兄弟这篇内容应该能帮你少踩几个坑。2. reset 框架的整体架构与核心数据结构拆解2.1 框架分层consumer、provider 和核心层Linux reset 框架的分层很清晰跟 clock 框架、regulator 框架是一个套路。最上面是consumer也就是使用复位资源的外设驱动比如显示驱动、USB 驱动、以太网驱动。中间是reset core负责管理复位控制器的注册、查找和 API 分发。最下面是provider也就是具体 SoC 的复位控制器驱动它知道怎么操作硬件寄存器。consumer 侧看到的接口都在include/linux/reset.h里面常用的就那么几个struct reset_control *reset_control_get(struct device *dev, const char *id); struct reset_control *devm_reset_control_get(struct device *dev, const char *id); int reset_control_assert(struct reset_control *rstc); int reset_control_deassert(struct reset_control *rstc); int reset_control_reset(struct reset_control *rstc); int reset_control_status(struct reset_control *rstc);provider 侧需要实现的是struct reset_controller_dev和struct reset_control_opsstruct 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); }; struct reset_controller_dev { const struct reset_control_ops *ops; struct module *owner; struct list_head list; struct device_node *of_node; unsigned int nr_resets; ... };这个设计的好处是consumer 完全不需要知道底层寄存器长什么样只需要通过设备树声明自己需要哪条复位线然后调用标准 API 就行。provider 只需要实现几个回调函数把硬件操作封装起来。2.2 设备树里的 reset 绑定规则设备树是 consumer 和 provider 之间的桥梁。一个典型的外设节点会这样写usb0: usbfe800000 { compatible rockchip,rk3399-dwc3; reg 0x0 0xfe800000 0x0 0x100000; resets cru SRST_USB3OTG0; reset-names usb3-otg; ... };这里的resets属性指向复位控制器节点cru和具体的复位线编号SRST_USB3OTG0。reset-names是可选的但如果一个设备需要多条复位线强烈建议加上这样驱动里可以用名字来获取代码可读性会好很多。provider 侧的节点长这样cru: clock-controllerff760000 { compatible rockchip,rk3399-cru; reg 0x0 0xff760000 0x0 0x1000; #reset-cells 1; ... };#reset-cells 1表示这个复位控制器用 1 个 cell 来描述一条复位线。有些 SoC 的复位控制器可能需要 2 个 cell比如一个表示寄存器偏移一个表示位偏移。这个在写 provider 驱动的时候要跟设备树保持一致。注意resets属性里的 phandle 必须指向一个已经注册的 reset controller。如果 provider 驱动还没 probeconsumer 的reset_control_get()会返回-EPROBE_DEFER这时候驱动应该优雅地返回并等待重试。我见过不少新手驱动直接把这个错误当成致命错误处理结果就是 probe 顺序稍微一变就挂。2.3 reset_control 的引用计数与共享机制reset 框架里面有一个很容易被忽略但非常重要的机制引用计数。当你调用reset_control_get()拿到一个句柄的时候框架内部会维护一个引用计数。如果多个驱动申请同一条复位线只有第一个申请者会真正去操作硬件后面的申请者只是增加计数。这个设计是为了解决共享复位线的问题。比如一个 SoC 里面USB 和 PCIe 可能共用一条复位线如果 USB 驱动在 suspend 的时候 assert 了这条线PCIe 就会跟着挂掉。引用计数机制让框架可以协调多个 consumer 的行为避免互相踩踏。但是这里有个坑引用计数只对通过reset_control_get()获取的句柄生效。如果你直接用reset_control_assert()操作一个从设备树解析出来的句柄框架不会帮你做任何协调。所以正确的做法是每个驱动都通过devm_reset_control_get()或者reset_control_get()来获取自己的句柄让框架来管理生命周期。另外devm_前缀的 API 会在驱动 detach 的时候自动释放句柄省去了手动reset_control_put()的麻烦。在 probe 函数里我一般优先用devm_reset_control_get()除非有特殊需求需要手动控制释放时机。3. 从零实现一个 reset controller 驱动3.1 硬件寄存器模型分析假设我们有一颗虚构的 SoC叫 DemoSoC它的复位控制器有 32 条复位线分布在一个 32 位寄存器里面。写 1 到某一位表示 assert reset写 0 表示 deassert reset。寄存器基地址是0x10000000偏移0x10。这种模型在真实 SoC 里面非常常见比如全志的 RST_CLK 模块、瑞芯微的 CRU 模块本质上都是类似的位操作。有些 SoC 会把 assert 和 deassert 分成两个寄存器一个写 1 生效另一个写 1 生效这种叫“写 1 有效”模型。还有的 SoC 需要先写一个 key 寄存器解锁才能操作复位寄存器这种叫“保护寄存器”模型。我们这里用最简单的单寄存器模型来演示实际项目里根据 SoC 手册调整就行。3.2 驱动代码骨架与关键实现先看 provider 驱动的核心结构#include linux/reset-controller.h #include linux/io.h #include linux/of.h #include linux/platform_device.h struct demosoc_reset { struct reset_controller_dev rcdev; void __iomem *base; spinlock_t lock; }; static int demosoc_reset_assert(struct reset_controller_dev *rcdev, unsigned long id) { struct demosoc_reset *drv container_of(rcdev, struct demosoc_reset, rcdev); unsigned long flags; u32 val; spin_lock_irqsave(drv-lock, flags); val readl(drv-base 0x10); val | BIT(id); writel(val, drv-base 0x10); spin_unlock_irqrestore(drv-lock, flags); return 0; } static int demosoc_reset_deassert(struct reset_controller_dev *rcdev, unsigned long id) { struct demosoc_reset *drv container_of(rcdev, struct demosoc_reset, rcdev); unsigned long flags; u32 val; spin_lock_irqsave(drv-lock, flags); val readl(drv-base 0x10); val ~BIT(id); writel(val, drv-base 0x10); spin_unlock_irqrestore(drv-lock, flags); return 0; } static int demosoc_reset_status(struct reset_controller_dev *rcdev, unsigned long id) { struct demosoc_reset *drv container_of(rcdev, struct demosoc_reset, rcdev); u32 val readl(drv-base 0x10); return !!(val BIT(id)); } static const struct reset_control_ops demosoc_reset_ops { .assert demosoc_reset_assert, .deassert demosoc_reset_deassert, .status demosoc_reset_status, }; static int demosoc_reset_probe(struct platform_device *pdev) { struct demosoc_reset *drv; struct resource *res; drv devm_kzalloc(pdev-dev, sizeof(*drv), GFP_KERNEL); if (!drv) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); drv-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(drv-base)) return PTR_ERR(drv-base); spin_lock_init(drv-lock); drv-rcdev.ops demosoc_reset_ops; drv-rcdev.owner THIS_MODULE; drv-rcdev.nr_resets 32; drv-rcdev.of_node pdev-dev.of_node; return devm_reset_controller_register(pdev-dev, drv-rcdev); }这段代码有几个关键点值得展开说。第一锁的选择。我用的是spinlock因为复位操作可能在中断上下文里被调用。如果你确定只会在进程上下文操作用mutex也可以但spinlock更保险。注意readl和writel之间必须加锁否则并发操作会丢位。第二container_of的用法。这是内核里非常常见的技巧通过rcdev成员反推出整个demosoc_reset结构体的指针。前提是rcdev必须是结构体的成员而且不能是第一个成员其实第一个成员也行但那样直接用指针转换更简单。第三devm_reset_controller_register的自动清理。这个 API 会在设备 detach 的时候自动调用reset_controller_unregister省去了手动清理的代码。如果你的驱动需要更精细的控制可以用非 devm 版本。3.3 设备树节点与匹配表provider 驱动的设备树节点和匹配表reset: reset-controller10000000 { compatible demosoc,reset; reg 0x0 0x10000000 0x0 0x1000; #reset-cells 1; };static const struct of_device_id demosoc_reset_of_match[] { { .compatible demosoc,reset }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, demosoc_reset_of_match); static struct platform_driver demosoc_reset_driver { .probe demosoc_reset_probe, .driver { .name demosoc-reset, .of_match_table demosoc_reset_of_match, }, }; module_platform_driver(demosoc_reset_driver);consumer 侧的使用就很简单了struct reset_control *rstc; rstc devm_reset_control_get(pdev-dev, demo); if (IS_ERR(rstc)) return PTR_ERR(rstc); reset_control_assert(rstc); udelay(10); reset_control_deassert(rstc);实操心得reset_control_reset()这个 API 的行为取决于 provider 的实现。如果 provider 实现了.reset回调框架会调用它如果没有实现框架会先 assert 再 deassert中间不加延时。很多 SoC 的复位线需要保持至少几个时钟周期才能生效所以如果你的 provider 没有实现.reset最好在 consumer 侧手动 assert、udelay、deassert不要依赖框架的默认行为。4. 功耗子系统与 reset 框架的联动实战4.1 低功耗场景下的复位时序设计在功耗管理里面reset 的使用场景主要有三类。第一类是模块级电源门控。当一个模块要进入低功耗状态时先 assert reset 把模块内部状态清零然后关闭时钟最后切断电源。退出的时候顺序反过来先上电再开时钟最后 deassert reset。这个顺序不能乱否则模块可能因为时钟没稳定就开始工作而挂死。第二类是 suspend/resume 流程。系统进入 suspend 的时候很多外设需要复位以降低漏电。resume 的时候再重新初始化。这里的关键是reset 的 assert 和 deassert 必须跟时钟、电源的开关顺序严格配合。我一般会在驱动的suspend回调里先 assert reset然后在resume回调里 deassert中间穿插时钟和电源的操作。第三类是错误恢复。当模块检测到异常比如 DMA 传输错误、总线超时时可以通过 assert reset 再 deassert 来强制模块回到初始状态而不需要重启整个系统。这种用法在存储驱动和网络驱动里面很常见。4.2 一个真实的显示子系统 bring-up 案例我之前做过一个 MIPI DSI 控制器的 bring-upSoC 是国产的显示子系统有独立的复位线。当时遇到的问题是这样的屏幕能亮但显示内容有随机花屏而且花屏的位置每次都不一样。排查过程大概是这样的。先确认时钟频率没问题用示波器量了 MIPI 时钟频率和占空比都正常。然后查电源电压也稳。最后把怀疑点放到 reset 时序上。翻 SoC 手册发现DSI 控制器的复位释放后需要等待至少 1ms 才能开始配置寄存器。而我们的驱动里面reset_control_deassert()之后立刻就写寄存器了中间没有延时。加上usleep_range(1000, 2000)之后花屏问题消失。这个案例说明一个道理reset 框架只负责操作复位线不负责时序保证。时序是驱动开发者自己的责任。SoC 手册里面关于复位释放到寄存器可访问之间的延时要求一定要仔细看不能想当然。4.3 复位线与时钟、电源的依赖关系处理在设备树里面reset、clock、power-domain 这三个东西经常是一起出现的。一个典型的外设节点dsi: dsiff960000 { compatible demosoc,dw-mipi-dsi; reg 0x0 0xff960000 0x0 0x10000; clocks cru PCLK_DSI, cru DCLK_DSI; clock-names pclk, dclk; resets cru SRST_DSI_P; reset-names dsi-p; power-domains power PD_DSI; ... };驱动 probe 的时候框架会按照一定的顺序处理这些资源。一般来说devm_clk_get()和devm_reset_control_get()只是获取句柄不会真正操作硬件。真正的操作顺序由驱动自己控制。我的经验是先开时钟再 deassert reset最后开电源域。这个顺序的依据是时钟稳定是复位释放的前提复位释放是模块工作的前提电源域打开是模块耗电的前提。如果顺序反了比如先开电源域再开时钟模块可能会因为时钟缺失而进入不确定状态。当然具体顺序还要看 SoC 手册的要求。有些 SoC 的电源域控制是硬件自动完成的驱动只需要操作 reset 和 clock 就行。5. 常见问题排查与调试技巧实录5.1 reset_control_get 返回 -EPROBE_DEFER 怎么办这是最常见的问题没有之一。现象是驱动 probe 失败日志里面打印-EPROBE_DEFER。原因通常是 reset controller 驱动还没 probe或者设备树里面的 phandle 写错了。排查步骤确认 reset controller 节点在设备树里面存在并且compatible跟 provider 驱动的匹配表一致。确认#reset-cells的值跟 provider 驱动里面rcdev.nr_resets的解析方式一致。确认 consumer 节点的resets属性 phandle 指向正确。用cat /sys/kernel/debug/devices_deferred查看哪些设备在等待。如果 provider 是模块确认模块已经加载。避坑技巧-EPROBE_DEFER不是错误是正常的延迟探测机制。驱动里面应该把-EPROBE_DEFER原样返回不要转换成其他错误码否则框架不会重试。5.2 assert 之后模块仍然有输出这种情况通常说明复位线没有真正生效。可能的原因有几个。复位线编号错了。设备树里面的SRST_XXX宏定义可能跟实际硬件不一致。解决办法是查 SoC 手册的复位寄存器映射表确认位偏移。复位控制器驱动没有真正操作硬件。有些 provider 驱动的.assert回调是空的或者只打印日志不写寄存器。这种情况在早期的 BSP 代码里面很常见需要自己补全。复位线被其他驱动共享引用计数导致操作被忽略。用reset_control_status()查一下当前状态如果返回 0 说明复位线是 deassert 状态。如果多个驱动共享同一条复位线需要协调好各自的操作时机。5.3 系统 suspend 后无法 resume这个问题在功耗调试里面非常典型。现象是系统进入 suspend 之后按电源键没反应串口也没输出。排查思路确认 suspend 流程里面有没有 assert reset。如果 assert 了但没有在 resume 里面 deassert模块就永远处于复位状态。确认 resume 的顺序。有些 SoC 要求先 deassert reset 再开时钟有些要求反过来。查手册。用echo 1 /sys/power/pm_debug打开 PM 调试看 suspend/resume 的详细日志。如果串口没输出可能是串口控制器的复位线被 assert 了。检查串口驱动有没有在 suspend 里面操作 reset。我踩过的一个坑是在一个项目里面以太网驱动在 suspend 的时候 assert 了 PHY 的复位线但 resume 的时候忘记 deassert结果系统能起来但网络不通。后来在resume回调里面补上reset_control_deassert()就好了。5.4 常见问题速查表问题现象可能原因排查方法解决方案probe 返回 -EPROBE_DEFERreset controller 未就绪查 devices_deferred确认 provider 驱动加载顺序assert 后模块仍有输出复位线编号错误查 SoC 手册寄存器映射修正设备树中的复位线编号suspend 后无法 resumeresume 时未 deassert查 PM 日志在 resume 回调中补 deassert模块工作不稳定reset 释放后延时不足查 SoC 手册时序要求加 udelay/usleep_range多条复位线互相干扰共享复位线引用计数问题用 reset_control_status 查状态协调各驱动的操作时机6. 进阶话题reset 框架的扩展与定制6.1 实现自定义的 reset 回调标准的reset_control_ops只有 assert、deassert、status、reset 四个回调。但有些 SoC 的复位控制器有特殊行为比如需要先写解锁寄存器、需要按特定顺序操作多条复位线、或者复位线有自清零特性。这种情况下可以在 provider 驱动里面实现自定义逻辑。比如static int demosoc_reset_reset(struct reset_controller_dev *rcdev, unsigned long id) { struct demosoc_reset *drv container_of(rcdev, struct demosoc_reset, rcdev); /* 先写解锁 key */ writel(0x5A5A0000 | BIT(id), drv-base 0x00); udelay(1); /* 触发复位 */ writel(0x5A5A0000 | BIT(id), drv-base 0x04); udelay(10); /* 清除复位 */ writel(0x5A5A0000 | BIT(id), drv-base 0x08); return 0; }这种自定义逻辑封装在 provider 里面consumer 完全不需要知道底层细节直接调用reset_control_reset()就行。这就是框架的价值所在。6.2 复位线与电源域的联动在比较新的 SoC 里面复位控制器和电源域控制器经常是同一个硬件模块。比如一个电源域关闭的时候域内所有模块的复位线会自动被 assert。这种情况下驱动不需要手动操作 reset只需要控制电源域就行。但是有些 SoC 不是这样的电源域和复位是独立的。驱动需要先 assert reset再关电源域开电源域之后再 deassert reset。这个顺序如果搞反了可能会出现“电源域已开但模块还在复位”或者“模块已复位但电源域还开着”的中间状态导致漏电或者功能异常。我的建议是在设备树里面把 reset 和 power-domain 都声明清楚然后在驱动里面严格按照 SoC 手册的时序要求操作。如果手册没写清楚就用示波器量一下复位线和电源的时序关系自己总结出正确的顺序。6.3 调试 reset 问题的常用工具除了前面提到的devices_deferred和 PM 调试还有几个工具很有用。debugfs 接口。很多 reset controller 驱动会在 debugfs 里面暴露寄存器状态可以用cat /sys/kernel/debug/reset/xxx查看。如果没有可以自己在 provider 驱动里面加。ftrace。用echo function /sys/kernel/debug/tracing/current_tracer打开函数跟踪然后过滤reset_control相关的函数可以看到完整的调用流程。动态调试。在 provider 驱动的 assert/deassert 回调里面加dev_dbg()然后用echo -n file demosoc-reset.c p /sys/kernel/debug/dynamic_debug/control打开动态调试可以实时看到每次复位操作。硬件工具。如果软件层面查不出问题就用示波器或者逻辑分析仪量复位线的实际波形。我遇到过好几次软件日志显示 assert 成功但硬件上复位线根本没动的情况最后发现是寄存器地址映射错了。7. 写在最后的一些个人体会reset 框架看起来简单但真正用好并不容易。我做了这么多年嵌入式 Linux踩过的 reset 相关的坑至少有几十个。总结下来最重要的经验就三条。第一永远不要假设复位线已经处于你期望的状态。在 probe 的时候先 assert 再 deassert确保模块从一个确定的初始状态开始。不要依赖 bootloader 留下的状态也不要依赖其他驱动的操作。第二时序比操作本身更重要。assert 和 deassert 之间的延时、复位释放到寄存器可访问之间的延时、复位与时钟电源之间的顺序这些细节决定了系统能不能稳定工作。SoC 手册里面关于复位的章节一定要逐字逐句看。第三善用框架提供的调试手段。reset_control_status()、debugfs、ftrace、动态调试这些工具能帮你快速定位问题。不要一上来就改代码先搞清楚现状。这个系列后面还会继续聊功耗子系统里面的其他框架比如 clock、regulator、power-domain 跟 reset 的联动。如果你正在做 SoC bring-up 或者功耗优化欢迎一起交流。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进