
早上到公司同事一脸苦笑地跟我说新焊好的板子上电之后串口打印全是乱码按一下复位键再启动就完全正常。这种“上电不正常、按复位正常”的毛病十有八九出在 STM32 的上电复位环节。STM32 上电复位听起来是个很基础的概念实际上牵扯到电源爬坡速率、NRST 引脚外围电路、芯片内部的 POR/PDR/BOR 阈值甚至启动文件里的执行顺序。任何一个环节没处理好都会给你整出“偶发跑飞”“仿真器连不上”“USB 无法识别”这种玄学问题。这篇文章我把自己从入门画最小系统到做批量产品这些年跟“上电复位”死磕出来的笔记完整整理一遍。先讲清楚复位到底复了什么内容再讲硬件电路怎么做才稳接着讲软件上如何区分复位来源最后用三个真实故障案例把完整排查链路走一遍。不管你是刚学 STM32 准备自己画板子还是已经在做产品被偶发复位搞得焦头烂额这篇都值得收藏慢慢看。1. 上电复位到底在复位什么从 RCC_CSR 寄存器看 STM32 的复位源头1.1 六类复位源上电复位只是其中之一很多人对复位的理解就是“把某个引脚拉低一下”。实际上 STM32 内部有六类复位源上电复位只是其中一种。搞清楚这些复位源你才能理解为什么“复位”有时候能解决问题有时候反而掩盖了问题。复位源触发条件标志位常见触发场景上电复位 POR/PDRVDD 从 0 上升到阈值以上或掉电低于阈值PORRSTF每次断电重新上电欠压复位 BORVDD 低于可配置的电压阈值BORRSTF电源跌落、大电流负载拉低电压外部复位NRST 引脚被外部拉低PINRSTF按下复位按键、外部复位芯片动作独立看门狗复位IWDG 递减计数溢出IWDGRSTF程序跑飞、喂狗不及时窗口看门狗复位WWDG 未在窗口期内刷新WWDGRSTF中断长时间阻塞导致喂狗失败软件复位执行 NVIC_SystemReset()SFTRSTFOTA 升级、参数配置切换低功耗管理复位特定低功耗模式唤醒等事件LPWRRSTFStandby 模式被唤醒上电复位发生在芯片内部。VDD 从 0 开始往上爬内部比较器探测到电压越过阈值之后CPU 内核、外设寄存器、Flash 控制器全部回到初始状态程序从 0x00000000 重新取指执行。这个过程完全不需要外部电路干预只要把电源接对就能发生。但注意这里有个容易被忽略的点上电复位触发的是“全套复位”。不仅 CPU 寄存器清零外设寄存器也全清零SRAM 内容虽然还在但是不可靠上电瞬间 SRAM 内容本来就是随机值。所以代码里所有需要初始化的东西包括全局变量初始值都要依靠启动文件和 C 运行时环境去重新建立。1.2 POR、PDR、BOR 的区别电压还不够就开跑是万恶之源先记住一句话芯片不是“一有电”就能跑的。内部电源要越过 POR 阈值复位状态要保持足够时间等待电源和时钟稳定然后才能释放复位、开始取指。PORPower-On ResetVDD 从 0 上升过程中电压低于某个阈值时强制保持复位状态。这个阈值通常在 1.x 伏量级。PDRPower-Down ResetVDD 从正常电压往下掉掉到低于某个阈值时触发复位。它和 POR 是同一个比较器电路的两面所以有些文档里直接写成 POR/PDR。BORBrown-Out Reset欠压复位阈值可以通过选项字节配置。BOR 的阈值比 POR 高作用是更早感知电源跌落。比如 3.3V 供电时BOR 阈值可以配成 2.9VVDD 一跌破 2.9V 马上复位避免芯片带病工作到 Flash 读取出错。从工程角度看真正危险的不是“电压瞬间断开”而是“电压刚好处于边界区域”。举个例子一个电机启动瞬间电源从 3.3V 跌到 2.8V如果 BOR 阈值配置在 2.9V芯片会在电机启动瞬间复位如果 BOR 没配置VDD 继续跌到 POR 阈值以下才会复位。两种表现差异很大前者至少是可控的复位后者可能进入 Flash 读取异常、程序指针乱飞的未知状态。理解这点之后你再看很多“玄学 bug”就通透了上电瞬间电压爬坡过慢、中间有跌落、电源纹波过大都可能让芯片反复处于 POR/PDR 边界表现出来就是上电第一次跑不起来按复位键反而正常。2. 硬件上最容易翻车的三个点电源爬坡、NRST 引脚与复位电路元件选型2.1 VDD 上升速率太快和太慢都不行STM32 数据手册里通常会给一个 VDD 上升速率的约束网络资料里常见的是 tRAWVDD 这类参数具体数值以对应型号的数据手册为准。硬件设计的原则是VDD 的爬坡时间不能太短也不能太长。爬坡太快有什么问题内部 POR 比较器还没来得及稳定判断VDD 已经冲到正常工作区复位释放时机可能不可控。爬坡太慢呢MCU 长时间工作在欠压区GPIO 电平不确定外部设备可能比 MCU 先完成上电引脚竞争、误动作全来了。实际操作中更常见的是“爬坡太慢”。很多工程师喜欢在 LDO 输出端堆大电容觉得电容越大越稳定。结果就是 VDD 上升时间被拖得非常长芯片在低压区停留过久偶尔能正常启动偶尔死在半路。我自己的板子常用做法是每个 VDD 引脚旁放一个 100nF 陶瓷电容LDO 输出端放一个 10μF 左右的钽电容或电解电容必要时再并联一个 100nF 高频去耦。整个板子的电源上升时间落在 1ms 到几十 ms 之间这个区间比较安全。2.2 NRST 引脚外部电路内部上拉电阻和 RC 时间的配合STM32 的 NRST 引脚不是裸的芯片内部已经有一个上拉电阻F1 系列典型值在 40kΩ 左右。这意味着哪怕外部什么都不接NRST 在正常工作时也是高电平。但实际板子不能什么都不接。官方推荐的最小电路通常是NRST 引脚到 VDD 接一个 10kΩ 上拉电阻NRST 到 GND 接一个 100nF 电容复位按键并联在这个电容两端。按下按键时NRST 被拉低释放后电容充电NRST 重新回到高电平。这个电路的时间常数可以算一下。外部 10kΩ 和内部约 40kΩ 并联等效上拉电阻大约 8kΩ再乘 100nF 电容时间常数约 0.8ms。NRST 要下降到内部复位有效电平大约 0.3 倍 VDD需要大约 1.2 个时间常数也就是不到 1ms。这个低电平持续时间远大于芯片内部要求的复位脉冲宽度所以非常可靠。这里有个容易犯的错复位按键应该接在 NRST 和 GND 之间而不是 NRST 和 VDD 之间。按键按下的动作是把 NRST 拉低如果接反了按下时变成把 NRST 拉高反而会干扰复位逻辑。电容为什么选 100nF 而不是更大我见过有人放 4.7μF按下复位键后 NRST 下降沿变得很缓内部的施密特触发电路识别不出干净的触发沿结果就是“按了复位键没反应”。所以 RC 值不是越大越好够用就行。2.3 什么时候必须上外部复位监控芯片RC 复位电路的优点是便宜、简单缺点是它只是给 NRST 提供一个延迟并不真正“监控”电源。如果系统存在很大的瞬态负载比如电机启动、射频发射、电磁阀动作电源电压会瞬间跌落RC 电路根本感知不到这种毫秒级的跌落芯片就可能跑飞。这时候需要外部复位监控芯片常见的有 MAX809、ADM809、STM809 这些。它们实时监测 VDD一旦 VDD 低于阈值立即把 NRST 拉低VDD 恢复并且稳定后再释放复位。原理和内部 POR/BOR 类似但外部芯片的响应速度、阈值精度通常更好而且可以选更高的监测电压。方案成本复位可靠性是否监控电源跌落适合场景纯 RC 复位最低中否学习板、无大负载系统RC 按键低中否通用 MCU 板外部复位芯片几毛到一块多高是批量产品、电机/射频等强干扰场景我做批量产品时只要板子上有电机驱动、电磁阀、无线模块这些大电流瞬态负载就会加一个复位监控芯片。成本增加不多但能省下大量售后排查时间。3. 程序里确认“这次复位是怎么来的”复位标志的读取与清零3.1 RCC_CSR 寄存器复位标志藏在时钟控制里STM32 的复位标志不在 RCC 的普通寄存器里而在 RCC_CSR时钟控制/状态寄存器。芯片复位后这些标志位会被硬件置 1并且一直保留到你手动清除。正是因为“复位标志能保留”我们才能在程序里判断上一次复位到底是谁触发的。标准外设库的写法是这样#include stm32f10x_rcc.h void ResetSource_Check(void) { if (RCC_GetFlagStatus(RCC_FLAG_PORRST) ! RESET) printf(复位源: 上电复位 POR/PDR\n); if (RCC_GetFlagStatus(RCC_FLAG_PINRST) ! RESET) printf(复位源: 外部复位 NRST\n); if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) ! RESET) printf(复位源: 独立看门狗复位\n); if (RCC_GetFlagStatus(RCC_FLAG_WWDGRST) ! RESET) printf(复位源: 窗口看门狗复位\n); if (RCC_GetFlagStatus(RCC_FLAG_SFTRST) ! RESET) printf(复位源: 软件复位\n); if (RCC_GetFlagStatus(RCC_FLAG_LPWRRST) ! RESET) printf(复位源: 低功耗管理复位\n); /* 清除所有复位标志否则下次无法区分是新的复位还是历史残留 */ RCC_ClearFlag(); }HAL 库的思路一样只是 API 换了名字#include stm32f1xx_hal_rcc.h void ResetSource_Check_HAL(void) { if (__HAL_RCC_GET_FLAG(RCC_FLAG_PORRST)) printf(复位源: 上电复位 POR/PDR\n); if (__HAL_RCC_GET_FLAG(RCC_FLAG_PINRST)) printf(复位源: 外部复位 NRST\n); if (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST)) printf(复位源: 独立看门狗复位\n); /* 清除全部复位标志 */ __HAL_RCC_CLEAR_RESET_FLAGS(); }这段代码应该放在 main 函数最开始、任何外设初始化之前。因为一旦外设初始化开始执行串口、LED、中断都打开了你再想准确采集复位标志逻辑上就乱了。3.2 为什么必须在 main 最开头就处理复位标志RCC_CSR 里的复位标志位是“写 1 清除”如果你不在程序早期清掉下次复位时看到的标志可能是上次的历史残留。举个例子系统上电后正常启动上电复位标志 PORRSTF 置 1如果程序里没清过一会看门狗超时复位你读到的 IWDGRSTF 和 PORRSTF 都是 1这时你根本不知道是看门狗复位还是上电复位。更隐蔽的问题是 OTA 升级场景。固件升级完成后通常要软复位SFTRSTF 置位。如果程序把“软复位”和“上电复位”的后续处理混在一起可能导致升级完参数恢复出厂设置用户数据丢失。正确做法是软复位代表“保持用户配置重新加载新固件”上电复位代表“全新启动配置初始化为默认值”。3.3 用 RAM 魔数区分上电复位和软件复位有时候光看复位标志还不够。比如看门狗复位的标志是 IWDGRSTF软件复位是 SFTRSTF这些都好区分。但如果你想判断“这次启动到底是不是真正的掉电后上电”复位标志也能做到PORRSTF 就是干这个的。不过实际项目中很多人喜欢用 RAM 魔数来做这个判断因为更直观、也可以自定义更多状态。原理很简单Cortex-M3 内核系统复位软件复位、看门狗复位不会清 SRAM而掉电后重新上电SRAM 内容是随机的。我们可以约定一块固定 RAM 地址如果启动时这块地址里存着一个约定魔数说明 RAM 没有被断电清掉不是上电复位如果没有魔数说明是真正的上电复位。#define MAGIC_ADDR 0x20000000u #define MAGIC_VALUE 0xA5A5A5A5u static volatile uint32_t *magic (uint32_t *)MAGIC_ADDR; uint8_t Is_PowerOn_Reset(void) { if (*magic ! MAGIC_VALUE) { *magic MAGIC_VALUE; /* 第一次上电写入魔数 */ return 1; } else { *magic 0; /* 非上电复位清除魔数模拟掉电效果 */ return 0; } }注意两个坑一是编译器可能优化掉对固定地址的访问建议加 volatile二是这个技巧要求 RAM 地址确实能被程序访问不要选到被其他外设占用的区域。一般在链接脚本里预留一个专门的段更干净我不想把代码写得太长所以直接用了固定地址的方式。3.4 复位标志检查和 SystemInit 的执行顺序ST 的启动文件里Reset_Handler 会先调用 SystemInit 配置 Flash 等待周期和时钟然后才进入 __main 跳 C 环境最后调到 main。SystemInit 本身不会动 RCC_CSR 里的复位标志位所以你在 main 第一行读取复位标志是安全的。但有一点要想清楚如果你依赖复位标志来决定要不要重新初始化外部晶振比如“软复位不重新初始化时钟”这个思路是危险的。因为 SystemInit 已经先于 main 执行了时钟配置main 里看到的时钟状态和复位标志没有直接关系。更稳妥的做法是把复位标志读出来存到一个全局变量里后续逻辑用这个全局变量做判断而不是在 main 里再回头去操作时钟相关寄存器。4. 上电复位不彻底的典型症状与完整排查链路4.1 症状一上电后串口打印乱码或 0x00按复位键后正常这个症状我见过太多次了。多数人第一反应是波特率算错了但“复位后正常”这个信息基本排除了波特率问题——因为波特率错的话按多少次复位都是乱码。完整的排查链路应该是这样示波器探头挂到 3.3V 电源输出上观察 VDD 爬坡过程。重点看有没有中间跌落、爬坡时间是不是太长。如果 VDD 有多次跌落说明 LDO 在启动时进入过保护或振荡芯片经历了反复 POR。示波器第二通道挂 NRST观察上电瞬间 NRST 有没有被内部复位电路拉低一段时间再释放。如果 NRST 一直恒高说明芯片内部复位没有正常动作问题很可能出在 VDD 本身。万用表量 BOOT0 和 BOOT1 的电平。BOOT0 悬空时上电瞬间可能是高也可能是低一旦高点平芯片从系统存储器启动用户程序根本没跑串口自然打印异常。最小系统上 BOOT0 必须接一个 10kΩ 下拉电阻。如果以上都没问题检查外部晶振是否起振。程序里如果配置成 HSE而上电瞬间 HSE 起振失败系统会卡在时钟初始化附近。表现可能不是乱码而是完全无输出。这套流程走完绝大多数“上电异常、复位正常”的问题都能定位到电源爬坡或 BOOT 引脚电平上。4.2 症状二Keil 下载报错 no target / 上电后 USB 设备无法识别很多初学者第一次焊完板子兴奋地点了 Keil 下载结果提示 “No target connected”然后就开始怀疑复位电路。这里要理解 STM32 的调试端口生命周期芯片上电复位后SWD/JTAG 引脚默认是调试功能。但如果用户程序在启动后把 SWD 引脚复用成普通 GPIO或者执行了GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)禁用了整个调试口那么调试器就永远连不上了。上电复位越正常用户代码执行得越早调试口反而被关得越快——这个现象容易让人误判成“复位电路有问题”。解决办法是让芯片停在用户程序之前把 BOOT0 拉高重新上电芯片进入系统存储器 Bootloader用户 Flash 里的代码不执行。此时连调试器可以正常识别芯片。用调试器自带工具擦除整个 Flash或者把 BOOT0 拉回低电平再连。如果你用的 J-Link可以试试它的 “Connect under Reset” 模式让调试器在复位释放的同时强行抓取内核。这个模式对于“程序一启动就关调试口”的场景特别有效。USB 无法识别的情况类似。STM32 的 USB 模块通常依赖外部设备端上拉电阻信号如果固件没正常跑起来D 引脚不会被主动拉高电脑就识别不到 USB 设备。遇到这种问题先确认 CPU 到底有没有执行到 USB 初始化代码而不是一上来就怀疑 USB 电路。4.3 症状三程序运行一段时间后自动重启像被反复复位这种问题要分两类排查。第一类是看门狗复位。程序里开了 IWDG但喂狗路径上有一个耗时很长的阻塞操作比如 Flash 擦写期间关中断时间过长喂狗超时IWDG 计数器溢出芯片复位。此时读取复位标志IWDGRSTF 一定是置位的。解决办法不是去掉看门狗而是把喂狗放到高优先级中断里或者优化耗时操作的分片执行。第二类是欠压复位。系统正常工作时某个外设突然启动瞬间电流把 VDD 拉低触发了 BOR 或 PDR。这种情况复位标志可能是 BORRSTF 或 PORRSTF。排查方向是示波器抓 VDD 波形重点观察重启瞬间有没有明显的电压凹陷。如果确认是电源跌落要么加大电源余量要么把 BOR 阈值调低一些让芯片在更低电压下继续工作但要注意 Flash 工作的电压下限不是越低越好。4.4 排查复位问题需要哪些工具工具用途示波器100MHz 以上抓 VDD 爬坡、跌落、NRST 波形串口助手打印复位标志和调试信息J-Link / ST-Link程序烧录必要时使用 Connect under Reset万用表测量 BOOT0、BOOT1、电源对地阻值示波器是排查复位问题的核心工具别省。很多问题光靠逻辑分析仪很难看出来因为你观察的是电压模拟量不是数字电平。5. 三个真实案例从“玄学 bug”到“板上钉钉”的定位过程5.1 AMS1117 输出电容从钽电容换成陶瓷电容上电偶发失败有次改版为了省成本、降低高度我把 AMS1117-3.3 输出端的 22μF 钽电容换成同容值 22μF 陶瓷电容。结果第一批样机就出现偶发现象十块板子里两三块上电后完全没反应按一下复位键又好了。一开始怀疑焊接问题补焊无果。示波器抓 VDD发现一个极其隐蔽的现象上电瞬间 VDD 波形在 2.8V 附近出现了一个明显的“台阶”停留几十毫秒才继续爬升到 3.3V。AMS1117 在这种状态下出现了类似保护的行为输出反复尝试爬升。查资料之后才明白AMS1117 这类传统 LDO 对输出电容的 ESR 是有要求的。钽电容 ESR 相对高反而帮助 LDO 稳定陶瓷电容 ESR 极低低到让 LDO 的反馈环路相位裕度不足在某些负载条件下产生自激振荡。虽然陶瓷电容标的容值和钽电容一样但偏压下实际容量也差很多ESR 特性完全不同直接替换就是会出问题。后来把输出电容换回 10μF 电解电容并在 STM32 每个 VDD 引脚旁保留 100nF 陶瓷电容问题消失。这个案例给我的教训是电源电路里的电容不能只看容值和耐压ESR、自谐振频率、直流偏压特性都可能导致完全不同的系统行为。看起来和上电复位没关系其实每一步都在影响 VDD 的爬坡和稳定性最终体现就是 POR/PDR 触发异常。5.2 调试口被禁用上电复位让程序“秒杀”了调试器有次量产固件里加了这么一句GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE);目的是把 SWD 的两个引脚释放出来做普通 GPIO 用。结果程序烧进去之后第二天同事想再调试发现 J-Link 怎么都连不上。他以为是板子坏了换了几块都一样。我让他把 BOOT0 拉高重新上电J-Link 果然又能连上了。原因不用多说SWJ_Disable 之后SWD 引脚不再响应调试请求这个配置在代码里而上电复位一旦完成程序第一件事就是把调试口关掉。J-Link 在复位瞬间只有极短的时间窗口能抓住内核错过了就再也连不上。这个案例的教训有两层第一量产固件里不要轻易永久禁用调试口除非你做好了一次性烧录不再调试的心理准备或者设计一个延迟窗口比如按住按键启动时才保留调试口第二理解了上电复位和启动文件的顺序你就知道恢复手段是 BOOT0 拉高而不是反复插拔调试器。5.3 “按下复位键没反应”一个过大电容引发的疑案还有一次是客户反馈板上复位按键按下去没反应程序不重启。我去现场看万用表量 NRST 引脚按下按键时确实变成了低电平但芯片就是不复位。示波器一看就明白了NRST 的下降沿非常缓慢从高到低拖了好几毫秒才跌到低电平中间还伴随一些回弹。这是因为这个板子的复位电容被改成了 4.7μF外部上拉电阻和这个电容组成的时间常数太大NRST 的边沿变得太“软”。内部复位电路有一个施密特触发输入对这种缓慢边沿的响应并不理想最终就没有形成一次干净利落的复位事件。把 4.7μF 换回 100nF问题立刻解决。后来我在自己的设计规范里加了一条NRST 引脚外部 RC 的时间常数控制在 0.5ms 到 2ms 之间电容超过 1μF 必须实测边沿速度。6. 聊点实在的上电复位这件事我后来养成的习惯有一段时间我被“偶发复位”折腾怕了后来慢慢形成一套固定流程新板子第一次上电必做不急着烧程序先空板子上电示波器抓 VDD 爬坡确认没有台阶、没有跌落。再抓 NRST 上电瞬间的释放波形确认复位保持时间足够。烧一个只做“读取复位标志并打印”的测试程序跑几次冷启动、按复位、喂狗超时复位确认每个复位源的标志都能正确读到。确认没问题之后才开始正式功能开发。这套流程看起来多花十分钟实际上能省掉后面不知道多少个“玄学”问题。上电复位这颗“第一颗扣子”一旦系错后面全乱。再分享一个小技巧如果你怀疑某个偶发复位是电压跌落引起的又不想一直盯着示波器可以在代码里把 BOR 阈值配成比实际电源正常值略低一点然后用一个 GPIO 翻转来标记“芯片曾经掉到过欠压状态”。再配合复位标志基本能把故障窗口缩到很小。最后一句总结性的经验不是套话是我真心的建议遇到 STM32 上电复位类问题先看硬件波形再读软件标志按顺序排查比瞎猜快十倍。