如果你在深夜两点还盯着调试器窗口里的 "cannot access target" 发呆,或者在串口助手里看到显示器上滚过整屏乱码想直接砸键盘,那么这篇总结大概率对你有点用。STM32 大概是很多嵌入式开发者从入门到放弃之间的必经路段,开发环境门槛低、中文资料多、官方例程丰富,但恰恰因为"看起来太容易",那些真正消耗时间的坑,几乎全藏在被忽略的细节里。这篇东西不是教程,更像是我这几年调板子攒下的一本笔记本,把花过真时间、掉过真坑的地方记下来,顺带把每一次的排查思路讲清楚,希望能帮你少走几次弯路。
1. 环境与工程配置:先能编译,再谈其他
1.1 装了 Keil5 却打不开 C51 工程,多半是 Pack 安装顺序的问题
见过不少刚入坑的朋友,电脑上装了 Keil5,拿着老师发的 51 工程双击打开,弹出来一句 "device not found",人直接懵了。这不是工程坏了,而是 Keil5 和 Keil4 的一个根本差异你没踩准:Keil5 把设备支持包(Device Pack)和编译器拆开装了,C51 工程需要 C51 的 Pack,ARM 工程需要对应型号的 STM32 Pack,两边互不通用。
我第一次装的时候图省事,先装了一个带 STM32 支持的破解版,后来又装了 C51 支持,结果 UV4 目录下的器件库直接乱掉,打开 51 工程报错,打开 STM32 工程也报错。后来干脆卸载,先装 C51 版本,确认 51 工程能打开,再装 ARM 版本,两台"平行世界"才消停。这里给一个建议:装 Keil5 的时候,路径里不要带中文和空格,默认路径最省心。C51 和 ARM 可以共存,但最好是同一个大版本,比如都用 5.39,混着装版本容易出怪问题。
芯片包(Pack)也是一样的逻辑。很多人创建 STM32F103C8 工程时发现器件列表里搜不到,不是找不到,是你在 Pack Installer 里没装对应厂商的 DFP。打开 Pack Installer,在 Packs 标签页里找到 STMicroelectronics 那一系列,展开能看到 STM32F1xx 的支持包,在线下载慢无所谓,可以去官网下离线 .pack 文件,双击就会自动导入 Keil。如果导入后器件列表还是空的,检查一下 Keil 的 Pack 存储路径是否和安装目录一致,很多绿色版、精简版在这一点上会各种翻车。
1.2 工程路径和编译器的隐性门槛
说到路径,这里藏着一个特别容易被忽视的坑:STM32 工程路径里一旦出现中文或空格,编译可能一切正常,到了下载调试阶段却奇奇怪怪。我遇到过一整个团队都调不好的现象,代码在 Release 工程师那边编译下载都能用,到了现场设备就是连接不上,最后发现是把工程文件拷到了一个带括号和中文名为位数的路径下,ADC 校准值读取全偏。虽然理论上新版本 Keil 对中文路径兼容性有所改善,但我个人的习惯是:新建工程一律放到纯英文路径下,目录名不要带特殊字符。
编译器版本不匹配也是新人常常原地转圈的点。用 AC5(Arm Compiler 5)的旧工程,拿到装了默认 AC6(Arm Compiler 6)的新 Keil 里,一编译满屏 error,最常见的是内联汇编语法、结构体指针类型转换这类写法标准变严了。调试的时候还有另一个问题:AC6 的优化等级默认是 -O0?不是,有些工程默认 -O2,你在 Watch 窗口里看不到变量实时变化,哪怕点暂停,变量也显示 "optimized out",这不是代码错了,是优化器把变量优化掉了。调试阶段把优化等级调到 -O0,发布时再改回来,这一条能省掉你大量怀疑人生的时间。
1.3 CubeMX 和 HAL 库版本不一致的连锁反应
很多项目现在都是先 CubeMX 生成框架,再往里面填逻辑。这里最容易踩的坑是:你装了一个版本的 STM32CubeF1 固件包,然后队友用另一个版本生成代码传给你,两边 HAL 库文件结构、API 参数有细微差异,合并代码的瞬间各种 "undeclared identifier" 和 "implicit declaration" 直接把你淹没。我现在的流程是固定一套版本组合:CubeMX 版本、HAL 库版本、Keil 版本、芯片包版本,装好之后尽量闭口不谈升级,很多时候升级带来的小改动比代码本身还折腾。
还有一点要提醒:CubeMX 生成的代码在 Keil 里编不过,很多时候是因为没选择正确的 "Device Pack"。CubeMX 默认按芯片型号给你生成,但 Keil 侧如果 Pack 版本太老或者缺失,编译时 startup 文件、系统时钟源文件都可能报错。检查办法很直接——打开工程里的设备列表,看一下 Debug 类下的确认框里,芯片型号后面有没有跟着正确的 DFP 版本标识,没有就重装对应 Pack。
2. 时钟树与定时器:越基础的地方越容易翻车
2.1 系统时钟算错,串口和定时器全跟着乱
如果要给 STM32 调试里的"隐蔽锅王"评个奖,我会把票投给时钟树。串口乱码、定时器定时时间不对、PWM 频率差一倍、DAC 输出毛刺,这些问题的根因排查到最后,有相当高比例都指向同一个事实——系统时钟频率和代码里定义的不一致。
一个很典型的现场:我用外部 8MHz 晶振,在 CubeMX 里 HCLK 想跑到 72MHz,但 PLL 配置参数没改对,实际 SYSCLK 可能跑到了 64MHz 或者别的值。这个时候最迷惑人的是编译不报错,程序也跑,但串口发出来的数据就是乱码。为什么?因为 HAL 库计算 USART 波特率时用的是 SystemCoreClock 这个全局变量,如果你没让系统和这个变量保持同步,波特率寄存器配置值就是基于错误时钟算出来的,每一个位的时间宽度都是错的。
排查这类问题,我的思路是:先看是不是所有外设时间都偏。定时器 1 秒定时实际上 0.9 秒触发、串口发送空字符的波形时长偏长或者偏短,基本可以断定是时钟源问题。用逻辑分析仪抓 TX 引脚的波形,看一个波特率周期实际多少微秒,反推系统时钟真实频率,比在代码里翻时钟树配置还快。如果你手头没有逻辑分析仪,也可以先量一下 MCO 引脚(如果把它配置成输出系统时钟),用万用表频率档直接看数值,一眼就露馅。
2.2 定时器中断里塞了太多东西,整个系统"假死"
定时器中断是 STM32 项目里的另一个重灾区。很多新人的第一反应是:定时器到了时间我就去处理一堆事情,于是把按键扫描、显示刷新、PID 运算、甚至串口打印全放进中断回调里。表面看逻辑很通顺,但实际跑起来你会发现中断执行时间超过了定时器中断周期,系统一直在进中断出中断,主循环根本抢不到 CPU,现象就是"程序卡死了"。
我调试过一个"每秒闪烁的 LED 偶尔卡顿"的项目,最终用调试器暂停发现程序停在 USART 发送函数里,因为中断里调了 HAL_UART_Transmit 且超时设置成无限等待,而串口又没人接收,缓冲区塞满直接死锁。所以中断里面应该只做三件事:置标志位、拷贝关键数据、清中断标志。真正的业务逻辑放主循环或者高优先级任务里。因为定时器中断里做浮点运算还会引入震动级的定时误差,比如做输入捕获测频率,中断里加一个浮点数乘法,捕获值就开始跳。
2.3 编码器模式的方向判定与信号滤波
用 STM32 定时器做正交编码器计数,这个功能看起来简单,配置项也不多,但坑在信号连接和滤波上。最经典的一道题:A 相和 B 相接反了,程序读出来的计数值只会往一个方向走,你预期正转+100,实际得到 -100(或者反过来)。一开始我以为是代码里计数方向判断条件写反了,反复检查逻辑没问题,最后拿示波器量编码器两根输出才发现是接反了。这个坑的教训是:怀疑软件之前先拿示波器看一眼波形,不要对着代码空想。
比接反更隐蔽的是编码器信号毛刺问题。电机运行时,编码器输出线上容易叠加噪声,如果你在 CubeMX 里没有使能定时器的数字滤波(Input Filter),计数值会在电机启动瞬间莫名其妙跳几个数。STM32 定时器滤波的本质是连续采样 N 次信号保持稳定才认为电平有效,对抖动有非常好的抑制作用。配置位置在定时器的输入捕获通道参数里,Filter 值可以按自己系统的噪声频率试,我通常从 0x0A 这个档开始调。注意:如果用了编码器接口模式,滤波参数影响的是捕获输入,但极性设置 PANIC,方向反了不是砍代码,先检查配置和接线。
3. 串口与调试输出:乱码背后站着的不一定是波特率
3.1 经典的"串口乱码"排查链路,往往不是波特率的锅
一说串口乱码,九成的人第一反应是我波特率没配对。不可否认新手最常见的就是 PC 端和单片机端波特率不一致,但如果你确认两边设置一模一样还在乱码,接下来的排查方向就要完整走一遍链路了。
我固定的排查顺序是这样的:
- 先看是不是偶发乱码。如果第一个字符正常、后面全乱或者一帧数据偶尔插几个错值,优先怀疑共地问题。两个设备之间没有接 GND,信号就没有统一的参考电平,偶尔通信成功只是运气好,这种我在现场遇到过不止一次,USB 转 TTL 和单片机之间必须共地。
- 如果从第一个字符就乱,而且乱得很有规律(比如全是 0xFF 或 0x00),检查串口引脚配置和默认复用功能有没有被占用。
- 如果每个字符都能接收到,但打印出来是文不对题的符号,比如读 0x41 显示 'a',检查双方数据格式(8 数据位、无校验、1 停止位)是否一致。这里有个隐形坑:某些 USB 转串口工具默认是 7 位数据位甚至带奇偶校验,在设备管理器里把它改回 8N1 再试。
- 整个排查走完还是规律性乱码,去查时钟树。这个我在上一节讲过了,HAL 库的波特率寄存器计算依赖 SystemCoreClock,一旦这里和真实频率对不上,你怎么调波特率选项都白搭。
说到硬件层,强烈建议你在桌面备一个便宜的逻辑分析仪(十几块钱那种 8 通道 24M 采样就够用),抓串口 TX 引脚看波形。逻辑分析仪能把每一位的电平宽度拉出来,配合计算器一看,实际波特率是 9600 还是 8333 一目了然,这种直观性比看串口助手的十六进制显示强太多。
3.2 DMA 接收与空闲中断:缓冲区错位的常见病根
我早期做串口通信,用的是最简单的阻塞查询方式,一个字节一个字节地接收,数据规模小的时候没事,一旦数据帧长了或者连续发包,就发现丢字节、帧错位。后来换成了 DMA + 空闲中断(IDLE line detection)方案,但这里也有一个经典坑。
很多人配好 DMA 开始接收,HAL_UARTEx_ReceiveToIdle_DMA接收到一帧数据后进回调,你以为每次回调拿到的都是完整帧,于是直接处理 buffer 里的数据。可如果对方发的是两帧连在一起的连续数据,或者一帧被拆成两次到达,你的缓冲区就会错位——上一次的旧数据和这次的新数据混在一起,解析出来的内容面目全非。
解决思路是不要依赖"单次回调等于单帧",而是自己维护一个环形缓冲区,把每一轮 DMA 到达的数据按顺序写入环形队列,解析器在队列里通过帧头、帧尾、长度字段去切包。伪代码大概这样:
#define BUFFER_SIZE 256 uint8_t dma_buffer[BUFFER_SIZE]; uint8_t ring_buffer[BUFFER_SIZE * 2]; uint16_t ring_head = 0, ring_tail = 0; void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART1) { for (uint16_t i = 0; i < Size; i++) { ring_buffer[ring_head] = dma_buffer[i]; ring_head = (ring_head + 1) % sizeof(ring_buffer); } // 重新启动DMA接收 HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_buffer, BUFFER_SIZE); } }另外一个几乎人人都会踩的隐藏细节:串口助手发送区的"加回车换行"勾选项。有人写好了帧协议,上位机模拟发送,协议里没有 \r\n,但助手默认勾着"发送新行",单片机收到末尾多出两个字节,解析永远不通过。排查这个问题最简单的方式是在接收中断里打印收到的十六进制值,看一下末尾到底是 0xAA 还是 0xAA 0x0D 0x0A。
3.3 USB 虚拟串口:枚举成功但收不到数据的现象
STM32 自带的 USB 虚拟串口(CDC 类)是很多项目用来替代 UART 的便捷方案,一根 Type-C 直接连电脑,方便得很。但它收到的反馈往往也很"玄"。我遇到过的情况是:设备管理器中端口已经出现了,但打开串口助手发送数据,单片机一点反应都没有。
这种时候先别怀疑 CDC 驱动,优先检查两点。第一,用官方 USB 分析工具看一眼枚举描述符,如果你用 CubeMX 生成的描述符没改过,但修改了 VID/PID,某些上位机软件会拒绝识别。第二,CDC 传输和 UART 不一样,它本质是 USB 端点轮询,不是有数据就立刻传到。你发送端调HAL_CDC_Transmit,如果返回USBD_BUSY是因为上一次数据还没有被主机取走,不能简单地忽略返回值,需要加入发送完成回调或重试机制。
CDC 发送还有一个隐性坑是阻塞等待。CDC_Transmit_FS里用了while循环,如果你的主循环频率很高,一直调用它反复发短数据,USB 协议层的端点缓冲区还没 ready,你的主循环就被这个 while 给卡住了,表现出来就是"整个程序变慢"。处理方法是维护一个发送队列,只有在上一次发送完成之后才放新的数据进去,或者干脆把发送频率限制在几十赫兹以内,对调式消息这种低频需求足够了。
4. 下载器与连接器:救不活的板子往往从这里开始
4.1 复用 JTAG/SWD 引脚之后,下载器瞬间失灵
ST-Link 连不上,这是 STM32 开发者最崩溃的瞬间,尤其是代码烧进去一次、第二次就下载不了的情况。有些项目的硬件设计为了省引脚,会把 PA15、PB3、PB4 拿来当普通 GPIO 用,但前提是你在初始化代码里显式禁止 JTAG 功能、保留 SWD。如果你把它当普通 IO 用了,同时 SWDIO/SWCLK 引脚又被复用,Keil 里的下载工具在连接时连不上目标,报 "RDDI-DAP Error" 或者 "Cannot access Target",这种大概率是自身代码把调试口干掉了。
如果你已经把一个复用调试引脚的固件烧进去了,第二次下载失败,这时怎么办?我试过的可靠办法是:把板子断电,按住复位键,点击下载按钮,在 Keil 开始连接的一瞬间松开复位。原理是让内核在复位瞬间短暂停下来,连接器趁这段窗口期劫持住 CPU,把后续的烧录流程走完。如果每次都坚持这么做,说明这个习惯要养成了:任何复用 SWD 引脚的程序,在初始化代码的头几行给你自己留一个"快捷键"——比如长按某个按键 3 秒直接进入 sleep 同时释放 SWD 引脚,否则你就永远绑死在按住复位这一招上了。
比这个更惨的是把 JTAG 引脚全部复用后,连 SWD 也挂了。我见过有人为了省一个 LED 的 IO 口,把 SWDIO 复用成输出口,然后整板没法下载,最后是拉高 BOOT0 进入系统存储器引导,用串口 ISP 把程序擦掉才救回来。这里建议是很明确的:STM32F1 把 SWD 引脚复用成普通 IO 可以,但一定在你的工程配置里把调试口保留打开,至少保留 SWD 调试功能,并尽量不要占用 SWDIO/SWCLK 两个引脚。
4.2 ST-Link 连接不上的几类真实原因
除了引脚复用问题,ST-Link 本身也有不少"亲戚"级别的坑。最常见的是接线太长,杜邦线超过 20cm,SWCLK 和 SWDIO 信号衰减,会时不时连接失败。解决办法是降低 SW 速度,Keil 的调试设置里把 Max Clock 调到 1MHz,很多时候就能救回来。其次,ST-Link 和目标板没共地是另一个高频问题,调试器参考电压不对,连接时断时续。连接线里的 GND 必须接上,SWD 的四根线(VCC、GND、SWDIO、SWCLK)全部接好再接电源,这条顺序也别搞反。
还有 NRST 引脚被外部电容拉得太低的情况。有些国产芯片或者自制板在 NRST 上接了大电容,启动时复位信号上升沿太慢,调试器连接时识别不到编号。这种情况我们程序员是改不了硬件的,但可以在 Keil 的 Flash Download 设置里把 Reset and Run 选项勾上,或者接线时用 ST-Link 的 NRST 引脚去强拉一下,让调试器控制硬复位。
ST-Link 固件版本过老也是隐藏原因。如果你手里的是一个年初生产的老 ST-Link,插入电脑后在设备管理器里能看到,但 Keil 总提示找不到 ST-Link,去 ST 官网下载 ST-Link 固件升级工具,给它升个级,很多疑难杂症直接消失。
4.3 printf 重定向与半主机模式
串口打印是开发调试的基本操作,但 printf 在 Keil MDK 环境下的重定向问题,初学者几乎必踩一次。默认情况下,Keil 的 ARM 编译器如果要使用 printf,它需要实现底层fputc函数,而不是像 PC 那样直接能用。如果你只写了printf没有实现fputc重定向,程序会跑进半主机模式(Semihosting),板子上没有接调试器时,程序直接停在BKPT指令上死掉,没有任何报错。
我最终落地的写法是重定向到串口:
#include "stdio.h" int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }同时,在 Keil 的 Options for Target 里勾选Use MicroLIB。不勾的话,C 库的标准输入输出走的是完整版实现,内部会去检查半主机通道,在裸机上会死等。勾了 MicroLIB 之后,静态链接库变小,而且不会触发半主机请求。注意 HAL_UART_Transmit 是阻塞函数,如果在定时器中断里直接调用 printf,中断执行时间不可控,系统会卡死在高优先级中断里,这个前面也讲过。正确做法是只在主循环或者低优先级任务里做打印。
5. 调试手段的升级:别只会点 Run 按钮
5.1 仿真窗口看得见的信息,比你用串口打印更丰富
很多刚接触 STM32 的人遇到 bug,第一反应就是给代码里塞 printf,不仅慢,而且像串口打印浮点数值这种场景,容易破坏实时性。调试器本身提供的变量观察和寄存器查看,才是排查复杂问题的第一利器。
Keil 的调试界面里,Peripherals 下拉菜单能看到所有外设寄存器的实时状态。比如定时器没输出 PWM,你可以点开 TIMx 的寄存器页面,看 CR1 的 CEN 是否被置位,CCER 的 CCxE 是否使能,CCMR 的输出模式到底有没有配置正确。这种寄存器级别的观察力比你在代码里搜 HAL 库函数调用要直观得多。另外一个实用技巧是 Watch 窗口添加表达式,有些变量是数组或者结构体,你可以直接查看内部每个成员;配合断点条件功能,可以让程序在特定值时才停下来,不需要慢慢单步跑。
唯一的无奈是优化等级。如果你把工程编译默认优化等级放到了 -O2,那很多局部变量在 Watch 窗口里会消失。切换优化等级到 -O0,或者在想观察的变量前加volatile修饰符,让编译器别把它优化掉。我个人的调试习惯是:调代码期间全部 -O0,发布之前再统一测一遍高优化模式下的行为。
5.2 把调试输出做成一个正规的"小系统"
前两年调试一个直流电机 PID 控制的项目,起初我把 PID 的状态量每 5ms 就通过串口发一次,上位机收到的数据太密,解析不过来,同时主循环被串口发送阻塞,控制频率直接从期望的 200Hz 掉到 50Hz 以下,电机抖动得像在跳舞。后来下了个狠心,给调试输出做了三级通道:正常打印只放关键事件,周期性状态数据降到每秒 20 帧,只有手动触发调试开关时才进入到完整高频打印模式。
如果你需要看 PID 响应曲线,不要自己拿着秒表盯着串口助手。有开源免费的串口绘图工具,比如 Vofa+ 就是一款很常用的简易上位机,你只需要按照它的协议格式发数据,就能在电脑上画出实时曲线,调 PID 参数的时候看着曲线来回调,效率比对着数字猜高好几个数量级。这个习惯帮我节省了至少一整天的调试时间,每次调 Kp、Ki 时曲线直观变化,很快就能找到趋势。
5.3 逻辑分析仪是嵌入式开发者的第三只眼
如果让我选一个"买房后第一件家具"级别的调试设备,逻辑分析仪排第一。不是示波器,是逻辑分析仪,因为便宜、简单、秒上手。很多 STM32 项目里的疑难杂症,结论都能从波形里直接读出来。
比如你调 I2C 从机,代码里查了一圈中断配置都没有问题,而用逻辑分析仪抓 SCL/SDA 两条线,发现主机发了 START 之后从机根本没拉低 ACK,于是问题立刻从"我的代码哪里错了"变成了"从机有没有上电、地址到底对不对",排查效率完全不是一个级别。再比如串口波特率不准、PWM 占空比错误、外部中断毛刺频繁触发,这类问题,示波器太贵不方便时刻摆着,逻辑分析仪插上电脑就是八个通道,足够覆盖日常 90% 的数字信号调试需求。
6. 两个完整现场复盘:从现象到根因的排查链路
6.1 案例一:跑 PID 算法时一开串口打印就重启
现象很怪,系统从串口打印 PID 参数表和当前误差值时,只要打印频率提高,板子直接死机或重启。第一反应是电源不够,外接电源带着电机时压降大,串口发送瞬间电流波动导致复位。换了一路稳压芯片供电,现象依旧。
然后我开始怀疑堆栈溢出,因为 printf 格式化浮点数会使用大量栈空间,STM32 默认启动文件里的堆栈只有 1KB 左右,一旦递归或者中断嵌套深,溢出触发 HardFault。我检查了启动文件,把 Stack_Size 从 0x400 改到 0x1000,问题依旧。
真正把问题定位到代码,是在仿真器里暂停后看到的现场:程序停在HAL_UART_Transmit内部的一个 while 等待发送完成标志位,永远等不回来。原因是调试用的串口助手没有打开 RTS/DTR 流控,硬件流控引脚悬空,USART 的发送就卡住了。这个问题的典型教训是:串口调试时,硬件流控引脚(如 RTS/CTS)如果没有用到,别在 CubeMX 里勾成自动使能,很多国产 USB 转串口模块默认把 RTS/DTR 拉出来,进入 Keil 调试状态后它们的状态会被改变,串口收发就卡了。我把硬件流控关闭,再在 CubeMX 里把 USART 的 RTS/CTS 引脚改为 Disable,完美解决。这让我之后每开一个新工程都会先去检查串口配置里的 Flow Control。
6.2 案例二:STM32 与 K210 双向通信,数据总是不对
这个项目是用 STM32F103 做主控,K210 做视觉识别模块,两者通过 UART 通信。接好了线,烧好程序,K210 那边识别到了目标,发给 STM32 的数据帧把我精心设计的帧头、帧尾、校验字段全打乱了。最常收到 0xFF,偶尔收到全 0x00。
一开始我以为是波特率不匹配,两边都设置了 115200。用逻辑分析仪抓 K210 的 UART 引脚波形,逐位展开后发现:K210 发出来的波形幅度是 3.3V,但 STM32 那边的 UART 引脚被配置成了 5V 容忍模式并挂了一个上拉电阻,更关键的是——两块板子的 GND 根本没有接在一起。信号参考地不一致,电平逻辑直接就乱了。
把 GND 接上之后,通信立刻稳定了。紧接着又出现第二个问题,K210 的串口在发送数据时,TX 默认是推挽输出,而 STM32 侧的 RX 上有一个大的滤波电容,信号边沿被削平,导致采样错误率极高。处理方法是把波特率降到 9600,并去掉 STM32 侧的上拉电阻和滤波电容。两次排查下来,回看现象,第一次是"地没共",第二次是"边沿太缓",都不是什么高深的知识,但如果不拿逻辑分析仪去看波形,纯靠代码推测,估计还得再修一个通宵。
7. 写几个我后来养成的习惯
第一,新板子到手,第一件事不是跑厂商例程,而是先搭一个最小工程,验证四件事:外部时钟源频率是否正确、串口能否输出一个递增计数器、Flash 读写是否正常、GPIO 点灯是否可控。这四项如果全部通过,之后在这个板子上遇到任何问题,你就能放心地往"应用逻辑"上去找,而不是回头怀疑底层环境。这个习惯帮我排掉了至少六成项目初期的莫名 bug。
第二,只要是复用 SWD/JTAG 引脚的设计,代码里一定留一个"调试逃生口——比如长按某个按键 3 秒进入低功耗模式并释放 SWD 引脚,或者上电时检测调试串口有没有收到特定字符,收到就不初始化复用外设。别以为这种事情只见于小作坊产品,我见过不少商用板子就是被"第二次下载失败"坑到返厂的。
第三,调试信息不要全部怼到一个串口上。如果可以分出第二路串口,一路只做业务通信,一路做调试打印,两路互不干扰。没有第二路串口,也不要慌,用 USB 虚拟串口或者蓝牙透传模块顶上。调试日志的数据结构提前设计好,带时间戳、带模块标签、带级别过滤,这在排查问题时会省掉大量的比对工作。
坑永远踩不完,但多数坑的根因其实就那么几类:时钟不对、电平不对、优先级不对、中断里干了不该干的事。把这些共通教训沉淀下来,下次遇到新问题时,你会发现自己排查的手段,慢慢从"盲目的代码扫读"变成了"有章法的分层定位"。