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_IRQHandlerB . 之间:

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 记于大棚物联网网关项目

如果这篇文章帮你省了两天排查时间,欢迎点赞收藏。评论区交流你踩过的类似坑。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐