STM32 中断不触发 UART4、TIM5、SPI3 集体罢工问题排查指南
STM32 中断不触发?UART4、TIM5、SPI3 集体罢工——你漏了中断向量表这项最隐蔽的检查
芯片 STM32F103RC(高容量 / HD)+ 启动文件 startup_stm32f10x_md.s(中容量 / MD),宏定义写对了
STM32F10X_HD,编译零 Error 零 Warning,函数逻辑滴水不漏,但 UART4 中断就是进不去——这个问题卡了两天,最后发现不只是 UART4,11 个高容量外设的中断全部在裸奔。
一、问题背景
STM32F103RCT6 大棚网关,通过 UART4(PC10/PC11)接 CC2530 ZigBee 协调器收传感器数据:
CC2530(采集) ──ZigBee──→ E18(协调器) ──UART 9600──→ STM32 UART4 → OLED显示
UART4 初始化、NVIC 优先级、中断服务函数、环形缓冲区——全写了,烧录之后 "CC2530 WAIT"纹丝不动,RX COUNT 永远为 0。
二、排查过程(5 轮排查,越查越离谱)
| 轮次 | 怀疑方向 | 做了什么 | 结果 |
|---|---|---|---|
| 1 | 波特率不匹配 | 对比参考工程 115200→9600 | ❌ 没用 |
| 2 | CC2530 UART 发送方式不对 | while(U0CSR&0x02) → UTX0IF 标准方式 |
❌ 没用 |
| 3 | STM32 UART4 初始化漏配置 | 补 USART_ClearFlag(RXNE|ORE) |
❌ 没用 |
| 4 | RF 端数据根本没发出来 | CC2530 加 500ms 心跳帧(LED 确认在发) | ❌ 没用 |
| 5 | 打开 Map 文件 | 搜索 UART4 |
✅ 找到! |
Map 文件里的关键输出:
Removing stm32f10x_it.o(i.UART4_IRQHandler), (64 bytes).
Removing cc2530.o(i.CC2530_RingBuf_Put), (56 bytes).
链接器把 UART4_IRQHandler 当死代码删了。为什么?因为向量表里没有人引用它。
三、根因分析——不只 UART4,11 个外设全部中招
3.1 启动文件是什么、为什么它管中断
STM32 上电后,CPU 从 Flash 地址 0x00000000 取第一条指令。这个地址存的不是代码,是中断向量表——一张"中断号 → 函数地址"的跳转表。
启动文件 startup_stm32f10x_xx.s 就是用汇编写的这张表。它跟 C 代码里的 NVIC_Init()、USART_ITConfig() 没有直接关系——C 代码管"使能中断",.s 文件管"中断来了跳哪里"。C 代码可以配得完美无缺,但 .s 里缺了对应的 DCD 声明,中断就会跳到未定义的 Flash 区域,等于石沉大海。
3.2 中容量 vs 高容量——中断向量表差异
ST 把 STM32F103 按 Flash 大小分了几个等级,每级的中断数量不同:
| 型号后缀 | Flash | 启动文件 | 中断数量 | 独有外设 |
|---|---|---|---|---|
| C8 / CB | 64-128KB | startup_stm32f10x_md.s |
43 个 | — |
| RC / RD / RE / RF / RG | 256-1024KB | startup_stm32f10x_**hd**.s |
54 个 | UART4/5, TIM5/8, SPI3, ADC3, FSMC, SDIO |
| XL | 768-1024KB | startup_stm32f10x_xl.s |
60 个 | HD 全部 + TIM6/7 等 |
问题来了:工程宏定义是 STM32F10X_HD(正确),芯片是 STM32F103RC(高容量),但启动文件用的是 startup_stm32f10x_md.s(中容量)。
中容量向量表到此为止(位置 42,地址 0x000000A8):
DCD USBWakeUp_IRQHandler ; ← 中容量最后一个向量
__Vectors_End
高容量比中容量多出的 11 个向量全部缺失:
| IRQ# | 中断名 | 外设 | 典型用途 | 场景概率 |
|---|---|---|---|---|
| 43-46 | TIM8_BRK/UP/TRG_COM/CC | TIM8 高级定时器 | 电机 PWM、舵机、编码器 | ⚠️ 中 |
| 47 | ADC3 | ADC3 | 第三路独立 ADC | ⚠️ 中 |
| 48 | FSMC | FSMC | 并口 LCD / 外部 SRAM | 低 |
| 49 | SDIO | SDIO | SD 卡读写 | 低 |
| 50 | TIM5 | TIM5 通用定时器 | 编码器模式、输入捕获 | ⚠️ 高 |
| 51 | SPI3 | SPI3 | SPI Flash / SPI 屏幕 / RF 模块 | ⚠️ 高 |
| 52 | UART4 | UART4 | 扩展串口、GPS/蓝牙/ZigBee | ⚠️ 高 |
| 53 | UART5 | UART5 | 扩展串口 | ⚠️ 中 |
概率最高的三个:TIM5(通用定时器编码器模式非常常用)、SPI3(第三个 SPI,传感器/屏幕常用)、UART4(第四串口,GPS/蓝牙/ ZigBee 模块常用)。这三个在大部分 IoT 网关项目里都会用上至少一个。
3.3 硬件层面:NVIC 查表失败,CPU 跳飞
以 UART4 为例,中断号为 52:
1. UART4 产生中断 → 通知 NVIC
2. NVIC 查中断号 52,计算向量偏移 = 0x00000000 + 4×52 = 0x000000D0
3. NVIC 读 0x000000D0 处的 4 字节 → 当作函数地址跳转
4. 但中容量向量表在 0x000000A8 就结束了!
5. 0x000000D0 处是擦除态 Flash (0xFFFFFFFF) 或 Default_Handler 死循环 (B .)
结果:CPU 跳到 0xFFFFFFFF → 取指异常 → HardFault → 进 HardFault_Handler 死循环。表面现象就是系统"卡死了",而且没有任何报错,因为 HardFault 之后你根本看不到。
3.4 两种故障模式
模式 A:“中断不触发”(你主动开了这个外设的中断)
就像 UART4——你调用 USART_ITConfig(UART4, USART_IT_RXNE, ENABLE) 使能了中断,数据来的时候中断源确实触发了,但向量表里没有入口 → CPU 跳飞 → HardFault → 死机或程序异常。如果侥幸没死机,表象就是数据永远收不到。
模式 B:“沉默炸弹”(你没用这个外设,但它意外触发了中断)
这才是最危险的场景。比如你根本没配 SPI3,但 GPIO 上的毛刺或默认配置恰好让 SPI3 的某个中断标志置位了。如果 NVIC 全局使能打开(__enable_irq()),此时 SPI3(IRQ#51)也会向 NVIC 请求中断。
中容量向量表中 IRQ#51 的位置是什么?超出 __Vectors_End 之后的 Flash 内容不可预测——可能是 0xFFFFFFFF、可能是随机数据、可能是其他代码段。CPU 跳到随机地址后行为完全不确定:可能 HardFault,可能执行垃圾指令把内存踩坏,可能死机不报任何错(连 LED 都不闪一下就永远卡住)。
这种 Bug 最难排查——没有日志、没有错误提示、LED 状态莫名卡住、偶尔又能跑很久才死。你反复检查 C 代码找不到任何问题,因为问题根源在 .s 文件里。
四、如何判断你是否也掉进了这个坑
症状 Checklist
- UART4 / UART5 / TIM5 / SPI3 / TIM8 / ADC3 中任何一个中断不触发
- USART1/2/3、TIM2/3/4、SPI1/2 等基础外设中断正常
- 编译 0 Error 0 Warning
- NVIC 和中断使能配置反复确认过
- 轮询模式能收到数据,开中断就不行(这个是最关键的信号)
- 系统偶尔无故卡死,没有 HardFault 日志输出
- 芯片型号后缀带 C/D/E/F/G(高容量/大容量/超大容量)
三种验证方法
方法一:Map 文件——最可靠的证据
打开 Keil 输出目录的 .map 文件,搜你的 ISR 函数名:
grep -i "UART4_IRQHandler" Output/*.map
看到 Removing → 中奖。看到正常的地址 → 向量表没问题。
方法二:看启动文件名
Keil 工程树里的 .s 文件名后缀:
_md.s→ 中容量 → 缺 HD 向量_hd.s→ 高容量 → OK_xl.s→ 超大容量 → OK_cl.s→ 互联型 → OK
方法三:直接搜 .s 文件里的 DCD
用文本编辑器打开 .s 文件,搜索 UART4:
- 搜不到 → 缺
- 搜得到
DCD UART4_IRQHandler→ OK
五、解决方案
方案 A:替换正确启动文件(一劳永逸,推荐)
从 Keil 安装目录取 startup_stm32f10x_hd.s:
<Keil>\ARM\Startup\ST\STM32F10x\startup_stm32f10x_hd.s
或从 ST 标准外设库:Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/arm/
替换后 Clean → Rebuild all。
方案 B:手动补全向量表(三个位置,应急用)
如果暂时不想替换整个文件,在现有 _md.s 中修改三个位置。补全的 11 个向量:
TIM8_BRK, TIM8_UP, TIM8_TRG_COM, TIM8_CC, ADC3, FSMC, SDIO, TIM5, SPI3, UART4, UART5
位置 1 — 向量表,USBWakeUp_IRQHandler 和 __Vectors_End 之间:
DCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
DCD TIM8_BRK_IRQHandler ; TIM8 Break (HD only)
DCD TIM8_UP_IRQHandler ; TIM8 Update (HD only)
DCD TIM8_TRG_COM_IRQHandler ; TIM8 Trigger and Commutation (HD only)
DCD TIM8_CC_IRQHandler ; TIM8 Capture Compare (HD only)
DCD ADC3_IRQHandler ; ADC3 (HD only)
DCD FSMC_IRQHandler ; FSMC (HD only)
DCD SDIO_IRQHandler ; SDIO (HD only)
DCD TIM5_IRQHandler ; TIM5 (HD only)
DCD SPI3_IRQHandler ; SPI3 (HD only)
DCD UART4_IRQHandler ; UART4 (HD only)
DCD UART5_IRQHandler ; UART5 (HD only)
__Vectors_End
位置 2 — EXPORT 声明,在 Default_Handler PROC 内的 EXPORT 列表末尾:
EXPORT USBWakeUp_IRQHandler [WEAK]
EXPORT TIM8_BRK_IRQHandler [WEAK]
EXPORT TIM8_UP_IRQHandler [WEAK]
EXPORT TIM8_TRG_COM_IRQHandler [WEAK]
EXPORT TIM8_CC_IRQHandler [WEAK]
EXPORT ADC3_IRQHandler [WEAK]
EXPORT FSMC_IRQHandler [WEAK]
EXPORT SDIO_IRQHandler [WEAK]
EXPORT TIM5_IRQHandler [WEAK]
EXPORT SPI3_IRQHandler [WEAK]
EXPORT UART4_IRQHandler [WEAK]
EXPORT UART5_IRQHandler [WEAK]
位置 3 — Handler 跳转标签,USBWakeUp_IRQHandler 和 B . 之间:
USBWakeUp_IRQHandler
TIM8_BRK_IRQHandler
TIM8_UP_IRQHandler
TIM8_TRG_COM_IRQHandler
TIM8_CC_IRQHandler
ADC3_IRQHandler
FSMC_IRQHandler
SDIO_IRQHandler
TIM5_IRQHandler
SPI3_IRQHandler
UART4_IRQHandler
UART5_IRQHandler
B .
⚠️ 三处必须全部补齐!向量表 DCD 管"从哪里跳" → EXPORT 管"符号可见" → 标签管"跳到哪里"。缺任何一步都会导致链接器优化掉或运行时跳飞。
修改后 Clean Targets → Rebuild all 并确认构建日志中 startup_stm32f10x_md.s 被重新汇编。
六、修复启动文件后还有一件事——补上 ISR 兜底函数
向量表补齐后,11 个新向量都指向 Default_Handler 里的 B .(死循环)。对于你已经用的外设(如 UART4),你在 stm32f10x_it.c 里写了自己的 ISR,链接器会用你的。但对于你还没写的那些(TIM5/SPI3/ADC3/…),如果将来任何原因导致它们意外触发中断,CPU 就会跳到 Default_Handler → 死循环 → 系统静默卡死。
所以修完 .s 后,建议在 stm32f10x_it.c 里把 11 个 HD 中断的兜底函数全部加上——至少做到"清标志位 + 安全返回",不卡死不踩内存:
/* ============ HD 中断兜底(防止误触发导致系统卡死) ============ */
void TIM8_BRK_IRQHandler(void) { }
void TIM8_UP_IRQHandler(void) { TIM_ClearITPendingBit(TIM8, TIM_IT_Update); }
void TIM8_TRG_COM_IRQHandler(void) { }
void TIM8_CC_IRQHandler(void) { }
void ADC3_IRQHandler(void)
{
if (ADC_GetITStatus(ADC3, ADC_IT_EOC))
ADC_ClearITPendingBit(ADC3, ADC_IT_EOC);
}
void FSMC_IRQHandler(void) { }
void SDIO_IRQHandler(void) { }
void TIM5_IRQHandler(void)
{
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
TIM_ClearITPendingBit(TIM5, TIM_IT_Update);
}
void SPI3_IRQHandler(void)
{
if (SPI_I2S_GetITStatus(SPI3, SPI_I2S_IT_RXNE) != RESET)
SPI_I2S_ReceiveData(SPI3); /* 读 DR 清标志 */
}
void UART5_IRQHandler(void)
{
if (USART_GetFlagStatus(UART5, USART_FLAG_ORE) != RESET)
USART_ReceiveData(UART5);
if (USART_GetITStatus(UART5, USART_IT_RXNE) != RESET)
USART_ReceiveData(UART5); /* 读 DR 清标志并丢弃 */
}
加上这些兜底函数后,就算某个没用到的 HD 外设意外触发中断,也不会把系统卡死——这才是完整的安全防线。
七、中断正常工作的完整条件链(记住这 6 条)
| # | 条件 | 检查方式 |
|---|---|---|
| 1 | 外设时钟已开 | RCC_APB1PeriphClockCmd(UART4, ENABLE) |
| 2 | NVIC 通道已配置 | NVIC_Init(&n) |
| 3 | 外设中断源已使能 | USART_ITConfig(UART4, RXNE, ENABLE) |
| 4 | 全局中断未屏蔽 | 默认开启,除非调了 __disable_irq() |
| 5 | 启动文件有该向量 ← 本文重点 | 搜 Map 文件或 .s 文件 |
| 6 | C 文件定义了 ISR 函数 | void UART4_IRQHandler(void) |
第 1-4 条出问题会有明显的编译警告或运行时日志。第 5 条不会有任何提示——向量表缺了就是缺了,Keil/IAR 都不检查芯片型号和启动文件的匹配关系。用 CubeMX 生成工程不会犯这个错误(CubeMX 自动选正确启动文件),但手动维护的老工程、网上下的模板、拷贝过去的项目,必须自己查。
八、总结
| 教训 | 细节 |
|---|---|
STM32F10X_HD ≠ 向量表正确 |
宏定义管 C 代码,.s 是独立汇编 |
| 不只是 UART4 | 11 个高容量外设全部受影响 |
| 不只中断不触发 | 严重时系统静默卡死无日志 |
| Map 文件是照妖镜 | Removing = 向量表缺了 |
改 .s 后必须 Clean + Rebuild |
普通 Build 不重汇编 |
| 补向量表后记得加 ISR 兜底 | 防止未用外设误触发卡死系统 |
最隐蔽的一类嵌入式 Bug:代码逻辑全对、寄存器配置全对、外设初始化的每一步都对——但中断就是进不去,甚至系统莫名其妙卡死。问题不在你的 C 代码里,在那个你从来不会打开看的 .s 文件中。
遇到外设中断不触发、系统无故卡死——先搜 Map 文件,再查 .s 向量表。这两个检查用不了 2 分钟,但能省下两天的无效排查。
2026-07-15 记于大棚物联网网关项目
如果这篇文章帮你省了两天排查时间,欢迎点赞收藏。评论区交流你踩过的类似坑。
更多推荐
所有评论(0)