2. 中断源与中断向量:常见中断源分类与中断向量表
好,咱们接着聊中断。上一章我跟你说了中断是个什么东西,说白了就是CPU正在干活,突然被一个紧急事件叫停,处理完再回来继续干。那问题来了——到底是谁有资格叫停CPU?这些“叫停者”就是中断源。
我刚开始学单片机那会儿,总觉得中断这玩意儿挺玄乎。后来发现,其实中断源就那么几大类,摸清楚了,后面就好办了。
2.1 常见中断源分类
咱们以最常用的STM32和51单片机为例,中断源大致分三类:
2.1.1 外部中断
外部中断,就是由芯片外部引脚上的电平变化触发的。比如你按下一个按键,或者传感器输出一个脉冲,都能触发外部中断。
触发方式一般有这几种:
- 下降沿触发:信号从高电平跳变到低电平时触发。我项目中常用这种方式检测按键按下。
- 上升沿触发:信号从低电平跳变到高电平时触发。比如检测电机编码器的Z信号。
- 低电平触发:引脚为低电平时持续触发。注意,这种模式容易产生重复中断,需要小心处理。
- 高电平触发:引脚为高电平时持续触发,用得相对少一些。
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通过它找到对应的处理函数。
下一章,我会带你深入中断优先级的具体配置,以及如何避免中断嵌套中的死锁问题。到时候咱们再细聊。