2. 中断源与中断向量:常见中断源分类与中断向量表

好,咱们接着聊中断。上一章我跟你说了中断是个什么东西,说白了就是CPU正在干活,突然被一个紧急事件叫停,处理完再回来继续干。那问题来了——到底是谁有资格叫停CPU?这些“叫停者”就是中断源。

我刚开始学单片机那会儿,总觉得中断这玩意儿挺玄乎。后来发现,其实中断源就那么几大类,摸清楚了,后面就好办了。

2.1 常见中断源分类

咱们以最常用的STM32和51单片机为例,中断源大致分三类:

2.1.1 外部中断

外部中断,就是由芯片外部引脚上的电平变化触发的。比如你按下一个按键,或者传感器输出一个脉冲,都能触发外部中断。

触发方式一般有这几种:

  • 下降沿触发:信号从高电平跳变到低电平时触发。我项目中常用这种方式检测按键按下。
  • 上升沿触发:信号从低电平跳变到高电平时触发。比如检测电机编码器的Z信号。
  • 低电平触发:引脚为低电平时持续触发。注意,这种模式容易产生重复中断,需要小心处理。
  • 高电平触发:引脚为高电平时持续触发,用得相对少一些。
我的小经验: 外部中断的引脚通常有去抖需求。我曾经在一个按键项目中,没做去抖处理,结果按一次键触发了七八次中断。后来加了个10ms的软件延时去抖,问题就解决了。

2.1.2 定时器中断

定时器中断,是单片机内部定时器产生的。你设定一个时间,到了时间它就触发一次中断。这个太常用了。

常见应用场景:

  • 定时采样:比如每1ms采集一次ADC数据
  • PWM生成:通过定时器中断更新占空比
  • 系统心跳:做操作系统的时间片轮转
  • 延时任务:非阻塞式的延时处理

我记得有一次做电机控制,需要精确的20kHz PWM频率。用定时器中断配合硬件PWM模块,效果非常稳定。你想想看,如果没有定时器中断,靠软件死循环延时,CPU啥也别干了。

2.1.3 串口中断

串口中断,也叫UART中断。当串口接收到一个字节,或者发送完成一个字节时,就会触发中断。

串口中断主要分两种:

  • 接收中断(RXNE):收到数据时触发。我习惯在中断里把数据扔进环形缓冲区,主循环再慢慢处理。
  • 发送中断(TC/TXE):数据发送完成或发送寄存器为空时触发。适合大数据包发送。
注意: 串口中断服务函数里千万别做复杂运算!我曾经犯过这个错——在接收中断里做数据解析,结果导致后续数据丢失。中断里只做“搬运”,不做“加工”。

除了这三类,还有ADC中断、DMA中断、I2C中断、SPI中断等等。但万变不离其宗,理解了这三类,其他的触类旁通。

2.2 中断向量表的概念与结构

好,现在你知道有哪些中断源了。那CPU怎么知道每个中断源对应哪个处理函数呢?这就得靠中断向量表了。

中断向量表,说白了就是一张“地址映射表”。 每个中断源都有一个固定的编号,这个编号对应着中断向量表中的一个地址。当某个中断发生时,CPU自动从这个地址取出中断服务函数的入口地址,然后跳转过去执行。

我打个比方:你有一个电话本,每个联系人(中断源)对应一个电话号码(中断服务函数地址)。电话响了(中断发生),你查电话本(中断向量表),找到对应的人,然后打电话过去(执行中断服务函数)。

2.2.1 中断向量表的结构

以STM32为例,中断向量表通常放在Flash的起始地址(0x08000000)或者SRAM的起始地址(0x20000000)。

典型的中断向量表结构如下:

偏移地址 中断号 中断源 说明
0x0000 - 栈顶指针 初始SP值
0x0004 - Reset_Handler 复位中断,程序入口
0x0008 0 NMI_Handler 不可屏蔽中断
0x000C 1 HardFault_Handler 硬件错误中断
0x0010 2 MemManage_Handler 内存管理中断
... ... ... ...
0x0058 16 EXTI0_IRQHandler 外部中断0
0x005C 17 EXTI1_IRQHandler 外部中断1
0x0060 18 EXTI2_IRQHandler 外部中断2
0x0064 19 EXTI3_IRQHandler 外部中断3
0x0068 20 EXTI4_IRQHandler 外部中断4
0x00A0 28 TIM2_IRQHandler 定时器2中断
0x00A4 29 TIM3_IRQHandler 定时器3中断
0x00A8 30 TIM4_IRQHandler 定时器4中断
0x00B0 32 USART1_IRQHandler 串口1中断

你看,每个中断源在向量表中都有一个固定的位置。偏移地址就是中断号乘以4(因为每个地址占4字节)。

2.2.2 中断向量表的配置

在实际开发中,我们通常不需要手动编写整个向量表。编译器会帮我们生成。但理解它的原理很重要。

以STM32的HAL库为例,中断服务函数的命名是固定的:

// 外部中断0的服务函数
void EXTI0_IRQHandler(void)
{
    // 检查中断标志位
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
    {
        // 处理中断
        // ...
        
        // 清除中断标志位
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    }
}

// 定时器2中断服务函数
void TIM2_IRQHandler(void)
{
    if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
    {
        // 处理定时器更新事件
        // ...
        
        __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
    }
}

// 串口1中断服务函数
void USART1_IRQHandler(void)
{
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
    {
        // 读取接收到的数据
        uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);
        // 放入缓冲区
        // ...
    }
}
核心要点: 中断服务函数的名字必须和启动文件中定义的向量表名称完全一致。否则编译器链接时找不到对应函数,中断来了就会跑飞。

2.2.3 中断向量表的重定向

有时候我们需要把中断向量表从Flash搬到SRAM中。为什么?因为SRAM可以动态修改。比如做IAP(在应用编程)升级时,新的程序可能使用不同的中断向量。

STM32提供了VTOR寄存器(向量表偏移寄存器)来实现重定向:

// 将中断向量表重定向到SRAM的0x20000000地址
SCB->VTOR = 0x20000000;

// 或者重定向到Flash的某个偏移地址
SCB->VTOR = 0x08010000;  // 偏移64KB

嗯,这里要注意:重定向之前,必须确保目标地址已经正确初始化了向量表。否则CPU一进中断就找不到北了。

2.3 中断优先级与嵌套

说到中断向量表,就不得不提优先级。多个中断同时发生时,谁先处理?这就靠优先级来决定了。

STM32的NVIC(嵌套向量中断控制器)支持最多16级抢占优先级和16级子优先级。具体配置是这样的:

// 设置中断优先级分组
// NVIC_PriorityGroup_2: 2位抢占优先级,2位子优先级
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);

// 配置外部中断0的优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);  // 抢占优先级1,子优先级0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

// 配置定时器2的优先级
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);   // 抢占优先级2,子优先级0
HAL_NVIC_EnableIRQ(TIM2_IRQn);

// 配置串口1的优先级
HAL_NVIC_SetPriority(USART1_IRQn, 3, 0); // 抢占优先级3,子优先级0
HAL_NVIC_EnableIRQ(USART1_IRQn);

优先级数值越小,优先级越高。所以上面这个配置中,外部中断0优先级最高,串口1最低。

避坑指南: 我曾经在一个项目中,把定时器中断和外部中断设成了相同的抢占优先级。结果两个中断同时触发时,系统行为变得不可预测。后来统一规划了优先级,问题才解决。记住:同优先级的中断,谁先来谁先处理,不能依赖硬件自动排序。

2.4 小结

这一章咱们聊了中断源的三大分类——外部中断、定时器中断、串口中断。也讲了中断向量表的概念和结构。说白了,中断向量表就是一张“中断服务函数地址表”,CPU通过它找到对应的处理函数。

下一章,我会带你深入中断优先级的具体配置,以及如何避免中断嵌套中的死锁问题。到时候咱们再细聊。

课后小练习: 打开你的开发板例程,找到启动文件(startup_stm32xxx.s),看看中断向量表是怎么定义的。然后试着修改一个中断服务函数的名字,看看编译会不会报错。动手试试,比看十遍书都管用。