第3章 中断系统:从向量表到嵌套实战
中断,说白了就是CPU正在干活,突然被一个紧急事件打断,跑去处理完再回来继续干活。这个机制在嵌入式系统里太重要了。我刚开始学RISC-V中断时,被向量表和优先级搞得晕头转向,后来踩了几个坑才真正搞明白。
今天咱们就把CH32的中断系统彻底讲透。你跟着我走一遍,保证能上手写中断服务函数。
3.1 中断向量表——CPU的紧急电话本
中断向量表是什么?你可以把它想象成一个电话本。每个中断源都有一个对应的电话号码(入口地址)。当CPU收到中断信号时,就去查这个电话本,找到对应的处理函数跳过去执行。
CH32系列基于RISC-V架构,它的向量表起始地址默认在0x00000000。但有个坑——RISC-V的向量表有两种模式:直接模式和向量模式。
| 模式 | 说明 | CH32默认 |
|---|---|---|
| 直接模式(Direct) | 所有中断跳转到同一个入口(mtvec.BASE),由软件判断中断源 | 否 |
| 向量模式(Vectored) | 每个中断有独立入口,硬件自动跳转 | 是(推荐) |
关键寄存器:mtvec
mtvec寄存器控制向量表基地址和模式。位[1:0]:00=直接模式,01=向量模式。CH32上电默认是向量模式,基地址0x00000000。
我个人习惯在项目启动时显式配置一下,防止某些Bootloader改过这个寄存器:
// 设置向量表基地址为0x00000000,向量模式
__asm volatile("csrw mtvec, 0x00000001");
嗯,这里要注意:向量表必须4字节对齐。如果你把程序放在0x08000000(Flash偏移),mtvec就要写成0x08000001(最后一位1表示向量模式)。
3.2 中断优先级——谁更紧急?
多个中断同时来了怎么办?CPU得有个规矩,先处理优先级高的。CH32的中断优先级分为抢占优先级和子优先级,说白了就是:
- 抢占优先级:决定能不能打断正在执行的中断
- 子优先级:抢占优先级相同时,谁先执行
CH32的NVIC支持最多4位优先级配置(0~15),但具体多少位取决于芯片型号。我查过CH32V203的数据手册,它支持4位抢占优先级,没有子优先级。
| 优先级值 | 抢占优先级 | 说明 |
|---|---|---|
| 0 | 最高 | 系统异常,不可屏蔽 |
| 1~3 | 高 | 关键外设中断(如TIM1) |
| 4~7 | 中 | 普通外设中断(如UART) |
| 8~15 | 低 | 非关键中断(如按键) |
我曾经踩过的坑:把两个中断设成相同优先级,结果它们互相抢占,导致系统卡死。后来才明白——相同优先级的中断不会互相打断,必须等当前中断处理完才能响应下一个。
3.3 外部中断配置——GPIO也能触发中断
外部中断就是把GPIO引脚的变化变成中断信号。CH32的每个GPIO口都可以配置为外部中断源,但要注意:同一组GPIO(如PA0~PA15)共享一个中断线。
配置步骤其实就四步:
- 使能GPIO时钟和AFIO时钟
- 配置GPIO模式为输入(上拉/下拉)
- 配置EXTI线:触发方式(上升沿/下降沿/双边沿)
- 配置NVIC:使能中断、设置优先级
来看个实际例子,配置PA0为下降沿触发的外部中断:
void EXTI0_Init(void)
{
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置PA0为上拉输入
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置EXTI线0,下降沿触发
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 4. 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; // 抢占优先级2
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
小技巧:如果多个引脚共用一条EXTI线(比如PA0和PB0都用EXTI0),只能同时使能一个。我一般用跳线帽或者PCB上做选择,避免冲突。
3.4 中断服务函数编写——名字不能写错
中断服务函数(ISR)的名字是固定的,必须和启动文件里的向量表一致。CH32的启动文件里已经声明了弱符号(weak),你只需要定义同名的函数就行。
常见的ISR命名规则:
- EXTI0_IRQHandler — EXTI线0中断
- USART1_IRQHandler — USART1中断
- TIM1_UP_IRQHandler — TIM1更新中断
写ISR时有个铁律:快进快出。中断里别做复杂运算,别调printf,别等延时。我见过有人在中断里做浮点运算,结果系统响应慢得像蜗牛。
// 正确的ISR写法
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 清除中断标志位——这一步必须做!
EXTI_ClearITPendingBit(EXTI_Line0);
// 处理中断:比如设置一个标志位
g_button_pressed = 1;
// 不要在中断里做耗时操作
// printf("Button pressed!\n"); // ❌ 错误示范
// delay_ms(100); // ❌ 错误示范
}
}
重要提醒:RISC-V的中断服务函数末尾不需要加interrupt属性,编译器会自动处理返回指令。但如果你用汇编写ISR,记得用mret指令返回,不是ret。
3.5 中断嵌套——高优先级打断低优先级
中断嵌套说白了就是:正在处理一个中断,又来一个更高优先级的中断,CPU先处理新的,处理完再回来继续旧的。
CH32默认支持中断嵌套,但有个前提:新中断的抢占优先级必须高于当前中断。相同优先级不会嵌套,会排队等。
来看个实际场景:
- CPU正在执行主循环
- UART中断来了(优先级3),CPU跳去处理
- UART处理到一半,TIM1中断来了(优先级1,更高)
- CPU暂停UART处理,去处理TIM1
- TIM1处理完,返回继续处理UART
- UART处理完,返回主循环
这个流程我用SVG画出来,你一看就明白:
写嵌套中断时,我有个习惯:低优先级中断里尽量少做事。因为如果你在低优先级中断里磨蹭太久,高优先级中断来了就得等着,这会影响实时性。
中断嵌套的黄金法则:
- 高优先级中断:只做最紧急的事(比如读取数据寄存器)
- 低优先级中断:可以做一些非紧急处理(比如协议解析)
- 所有中断:绝对不要调用
delay()或printf()
3.6 实战:按键中断控制LED
光说不练假把式。咱们写个完整的例子:按键触发外部中断,翻转LED状态。
// main.c
#include "ch32v20x.h"
volatile uint8_t g_button_pressed = 0;
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
EXTI_ClearITPendingBit(EXTI_Line0);
g_button_pressed = 1; // 设置标志位,主循环处理
}
}
int main(void)
{
// 初始化LED(PC13)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_SetBits(GPIOC, GPIO_Pin_13); // 初始关闭
// 初始化外部中断(PA0)
EXTI0_Init();
while(1)
{
if(g_button_pressed)
{
g_button_pressed = 0;
// 翻转LED——在主循环里做,不在中断里做
if(GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13))
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
else
GPIO_SetBits(GPIOC, GPIO_Pin_13);
}
}
}
经验之谈:中断里只设标志位,主循环里处理具体操作。这样既保证了中断响应速度,又避免了中断里做复杂操作带来的风险。我所有项目都这么干,从来没出过问题。
好了,中断系统这块就讲完了。你把这几个概念理清楚——向量表、优先级、外部中断配置、ISR写法、嵌套规则——基本上就能应对大部分项目需求了。下次遇到中断相关的问题,先检查向量表配置对不对,再看优先级有没有冲突,最后看ISR里有没有做耗时操作。这三板斧下去,90%的问题都能解决。