3. 复位源检测与上报:硬件原理与寄存器解析

系统异常复位了,第一件事是什么?

不是急着去初始化外设,也不是去打印Logo。你得先搞清楚——是谁把系统踹了一脚。

复位源检测,说白了就是找到那个“肇事者”。是上电复位?看门狗超时?还是外部按键强制复位?不同的复位源,恢复策略完全不同。我见过不少团队,复位后一股脑全盘初始化,结果该保留的现场丢了,该快速恢复的路径走了全流程,白白浪费了几百毫秒。

3.1 复位源的本质:谁触发了CPU的“硬重启”

每个SoC内部都有一个复位控制器模块(Reset Controller),它像个黑匣子,记录着最近一次复位的来源。这个模块通常挂在APB或低速总线上,上电后第一个可访问的寄存器往往就是复位状态寄存器。

常见的复位源,我按触发方式分成了三类:

复位源类型 典型触发条件 恢复策略
上电复位(POR) 系统首次上电,VDD从0上升到稳定 全流程初始化,无需保留现场
外部复位(EXT_RST) 复位按键、调试器、外部看门狗 视场景而定,通常全初始化
内部复位 看门狗超时、软件触发、电源监测 快速恢复,尽量保留上下文

这里有个关键点:内部复位往往意味着系统之前是在运行的。你想想看,看门狗为什么会超时?大概率是软件跑飞了或者某个中断卡死了。这时候如果直接全盘初始化,之前保存的关键数据就全丢了。

核心原则:内部复位走快速恢复路径,外部复位和上电复位走完整初始化路径。这个判断必须在U-Boot启动的最早期完成。

3.2 寄存器解析:以i.MX6ULL为例

光讲理论太虚了,我们拿一块实际芯片来看看。我手头最常用的是NXP i.MX6ULL,它的复位控制器叫SRC(System Reset Controller)。

SRC里有个关键寄存器——SRC_SRSR(System Reset Status Register)。地址是0x020D_8000,偏移0x00。上电后第一件事,U-Boot的start.S里就会读它。

/* i.MX6ULL SRC_SRSR 寄存器位定义 */
#define SRC_SRSR_IPG_RST        (1 << 0)  /* IPG总线复位 */
#define SRC_SRSR_WDG_RST        (1 << 1)  /* 看门狗复位 */
#define SRC_SRSR_IPP_USER_RST   (1 << 2)  /* 外部复位 */
#define SRC_SRSR_CSU_RST        (1 << 3)  /* CSU安全复位 */
#define SRC_SRSR_TEMPSENSE_RST  (1 << 4)  /* 温度传感器复位 */
#define SRC_SRSR_JTAG_RST       (1 << 5)  /* JTAG复位 */
#define SRC_SRSR_WARM_BOOT      (1 << 6)  /* 软复位标志 */

读这个寄存器有个坑——它是粘性的。什么意思?复位发生后,这些位会保持住,直到你软件主动清零。如果你不清零,下次复位时读到的可能是两次复位的叠加状态。

我曾经踩过的坑:某次调试看门狗复位问题,发现复位源寄存器里同时显示了POR和WDG两个标志。排查了半天,最后发现是U-Boot里没有在读取后清零寄存器,导致上电时的POR标志一直残留。所以记住:读完后立即写1清零。

清零操作很简单:

/* 读取复位源 */
uint32_t rst_src = readl(SRC_BASE_ADDR + 0x00);

/* 立即清零,避免干扰下次判断 */
writel(0xFFFFFFFF, SRC_BASE_ADDR + 0x00);

3.3 复位源上报:从汇编到C的传递

寄存器读到了,怎么把这个信息传给后面的C代码?

我个人习惯的做法是:在汇编阶段(start.S)就把复位源保存到一个通用寄存器里,然后作为参数传递给C语言的入口函数。这样C代码一上来就能做策略判断。

以i.MX6ULL的U-Boot为例,流程大概是这样的:

/* start.S 片段 */
reset:
    /* 读取复位源 */
    ldr r0, =SRC_BASE_ADDR
    ldr r1, [r0, #0x00]
    
    /* 保存到r4,后面传给C函数 */
    mov r4, r1
    
    /* 清零复位状态寄存器 */
    mov r2, #0xFFFFFFFF
    str r2, [r0, #0x00]
    
    /* ... 其他初始化 ... */
    
    /* 跳转到C入口,r0传递复位源 */
    mov r0, r4
    bl board_init_f

C代码这边,接收参数后立即做策略分支:

void board_init_f(uint32_t reset_source)
{
    /* 判断复位源,决定初始化策略 */
    if (reset_source & SRC_SRSR_WDG_RST) {
        /* 看门狗复位:走快速恢复路径 */
        quick_recovery_init();
    } else if (reset_source & SRC_SRSR_IPP_USER_RST) {
        /* 外部复位:走完整初始化 */
        full_init();
    } else {
        /* 上电或其他复位:全流程 */
        full_init();
    }
}

小技巧:如果SoC支持多个复位源同时置位(比如POR+WDG同时发生),建议按优先级处理。我个人习惯的优先级是:POR > 外部复位 > 看门狗复位 > 其他内部复位。POR优先级最高,因为它意味着系统刚上电,没有任何现场需要保留。

3.4 不同SoC的复位源差异

不同厂商的复位控制器设计思路差别很大。我列几个常见的:

SoC系列 复位控制器 关键寄存器 特殊点
NXP i.MX6 SRC SRC_SRSR 粘性位,需软件清零
TI AM335x PRM PRM_RSTST 读后自动清零
Rockchip RK33xx CRU CRU_GLB_RST_ST 支持多个复位域
Allwinner H3 RTC RTC_BOOT_CTRL 复位源与启动源共用

嗯,这里要注意TI AM335x的PRM模块。它的复位状态寄存器是读后自动清零的。如果你在汇编阶段读了一次,到C函数里再读就是0了。所以必须像前面说的那样,在汇编阶段就把值保存下来。

3.5 复位源检测的完整流程

把上面的知识点串起来,一张图就能看明白:

复位源检测与上报完整流程 复位事件发生 硬件锁存复位源到寄存器 start.S 读取并保存 写1清零复位状态寄存器 作为参数传给C入口 C代码根据复位源选择恢复策略 ⚠ 注意 • 粘性位必须软件清零 • 部分SoC读后自动清零 • 汇编阶段就要保存值

这个流程里,最容易被忽略的就是清零操作。我见过不止一个项目,因为没清零导致第二次复位时读到错误的状态,然后走了错误的初始化路径。你说冤不冤?

3.6 实战中的几个坑

最后分享几个我在实际项目中踩过的坑:

  • 坑一:复位源寄存器被BootROM提前读了。有些SoC的BootROM会在启动早期读取复位源,然后自己清零了。等你U-Boot起来,寄存器里已经是0了。解决办法是查芯片手册,看BootROM的行为,必要时从其他保留寄存器里找回信息。
  • 坑二:看门狗复位和上电复位同时发生。系统刚上电,看门狗还没配好就超时了。这时候复位源寄存器里POR和WDG位都是1。我的处理方式是:只要POR置位,就按上电复位处理。
  • 坑三:调试器干扰复位源。用JTAG/SWD调试时,调试器可能会注入复位信号。这时候读到的复位源可能不是真实的。建议在产品发布前,做一次不带调试器的复位源测试。

我的建议:在U-Boot的早期初始化中,把复位源信息打印出来。哪怕产品最终要关掉打印,开发阶段也一定要开着。它能帮你快速定位很多诡异的启动问题。

复位源检测,说白了就是一句话:先问“为什么重启”,再决定“怎么重启”。这个判断做对了,后面的快速恢复路径才能走得通。做错了,轻则浪费几百毫秒,重则丢失关键数据。