一、中断是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
主程序:
while(1) {
do_task_A(); // ← 执行到这里
do_task_B();
}

│ 按键突然按下

中断发生: ├─ 硬件自动保存现场
├─ 跳到中断服务函数 ISR
├─ ISR 执行:GPIO_ReadPin() + 标志位
├─ 硬件自动恢复现场
├─ 返回主程序断点

do_task_A(); // ← 接着刚才的位置继续

核心价值:CPU 不需要轮询 “按键按了吗?按键按了吗?”,中断来了自动处理。

二、NVIC 嵌套向量中断控制器

2.1 优先级体系

1
2
3
4
5
6
7
8
9
10
11
12
STM32F103 Cortex-M3: 16 个可编程优先级(4 位)

优先级 = 抢占优先级 + 子优先级

NVIC_PriorityGroup_0: 0 bit 抢占 + 4 bit 子 → 16 级子优先级,无抢占嵌套
NVIC_PriorityGroup_1: 1 bit 抢占 + 3 bit 子 → 2 级抢占 + 8 级子
NVIC_PriorityGroup_2: 2 bit 抢占 + 2 bit 子 → 4 级抢占 + 4 级子
NVIC_PriorityGroup_3: 3 bit 抢占 + 1 bit 子 → 8 级抢占 + 2 级子
NVIC_PriorityGroup_4: 4 bit 抢占 + 0 bit 子 → 16 级抢占,无子优先级

抢占优先级:数值越小越优先。高抢占可打断低抢占的中断(嵌套)。
子优先级:数值越小越优先。但不会嵌套,只决定同时挂起时谁先执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
示例(Group_2):

ISR_A: 抢占=0, 子=0 ← 最高,可打断 B 和 C
ISR_B: 抢占=1, 子=0 ← 与 C 同抢占,子=0 优先
ISR_C: 抢占=1, 子=1

时间线:
正在执行 ISR_C(1,1)

ISR_A(0,0) 挂起 → 抢占,打断 C → 执行 A → 返回 C

继续执行 ISR_C

ISR_C 和 ISR_B 同时挂起 → B(子=0) 先执行

2.2 HAL 优先级配置

1
2
3
4
5
6
7
8
9
10
11
/* 初始化时设置一次 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); // 2+2 分组

/* 每个外设单独设置 */
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); // 最高优先
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 1); // 次高,同抢占
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); // 低抢占

HAL_NVIC_EnableIRQ(USART1_IRQn);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

三、外部中断 EXTI

3.1 硬件连接

1
2
3
4
5
6
7
GPIO 输入 → 边沿检测电路 → EXTI 控制器 → NVIC → CPU

支持上升沿/下降沿/双沿触发。

PA0~PG0 都映射到 EXTI0
PA1~PG1 都映射到 EXTI1
...(同一编号的引脚共享 EXTI 线)

3.2 按键中断示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 按键接 PC13,按下 → 低电平,上升沿 = 松开触发 */
void EXTI_Init(void) {
GPIO_InitTypeDef gpio = {0};

__HAL_RCC_GPIOC_CLK_ENABLE();

gpio.Pin = GPIO_PIN_13;
gpio.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发(按下)
gpio.Pull = GPIO_PULLUP; // 上拉,默认高电平
HAL_GPIO_Init(GPIOC, &gpio);

HAL_NVIC_SetPriority(EXTI15_10_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}

/* 中断回调(所有 EXTI10~15 共享这个中断向量) */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_13) {
// 按键 PC13 被按下
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转 LED
}
}

3.3 EXTI 中断向量映射

EXTI 线 中断向量 映射引脚
EXTI0 EXTI0_IRQn PA0, PB0, PC0…
EXTI1 EXTI1_IRQn PA1, PB1, PC1…
EXTI2 EXTI2_IRQn PA2, PB2, PC2…
EXTI3 EXTI3_IRQn PA3, PB3, PC3…
EXTI4 EXTI4_IRQn PA4, PB4, PC4…
EXTI5~9 EXTI9_5_IRQn 共用一个向量
EXTI10~15 EXTI15_10_IRQn 共用一个向量

四、定时器中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
TIM_HandleTypeDef htim2;

void TIM2_Init(void) {
__HAL_RCC_TIM2_CLK_ENABLE();

htim2.Instance = TIM2;
htim2.Init.Prescaler = 7199; // 72MHz / 7200 = 10KHz
htim2.Init.Period = 9999; // 10KHz / 10000 = 1Hz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim2);

HAL_NVIC_SetPriority(TIM2_IRQn, 0, 1);
HAL_NVIC_EnableIRQ(TIM2_IRQn);

HAL_TIM_Base_Start_IT(&htim2); // 启动定时 + 中断
}

/* 定时器溢出回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// 每秒执行一次
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}

五、串口中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
uint8_t rx_byte;  // 单字节接收缓冲

void UART_IT_Init(void) {
// ... MX_USART1_UART_Init() ...

HAL_NVIC_SetPriority(USART1_IRQn, 0, 2);
HAL_NVIC_EnableIRQ(USART1_IRQn);

HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 启动中断接收
}

/* 每收到 1 字节触发 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 处理 rx_byte
// 必须再次启动接收,否则中断只触发一次
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}

空闲中断(接收不定长数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 利用 IDLE 中断检测一帧结束 */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能空闲中断

void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清标志

// 计算收到的字节数
uint16_t len = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
uint16_t rx_len = RX_BUF_SIZE - len;

// 处理 rx_buf[0..rx_len-1]
}
HAL_UART_IRQHandler(&huart1); // 处理普通中断
}

六、中断优先级实战分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
项目示例:MPU6050 姿态解算

ISR 抢占 子 原因
─────────────────────── ── ── ──────────────────
SysTick_Handler 0 0 系统节拍,FreeRTOS 心跳
TIM2 采样中断 0 1 125Hz 读 MPU6050,不可被延迟
USART1 接收 0 2 指令接收,实时性高
EXTI 按键 1 0 不紧急,可被打断
TIM3 PWM 更新 1 1 灯效,延迟无感
USART2 日志输出 2 0 最低,断掉也无所谓

规则:
1. 硬实时(采样/控制)→ 抢占 0
2. 通信(串口/CAN) → 抢占 0~1
3. UI(按键/显示) → 抢占 1
4. 日志/调试 → 抢占 2

七、中断编写注意事项

7.1 ISR 要短

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* ❌ 错误:ISR 里做耗时操作 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
HAL_Delay(100); // 死等 → 阻塞其他中断
printf("tick\r\n"); // 串口阻塞发送 → 更慢
MPU6050_Calibrate(); // 耗时几百 ms → 灾难
}

/* ✅ 正确:ISR 只设标志,主循环处理 */
volatile uint8_t flag = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
flag = 1; // 仅 1 条赋值语句
}

int main(void) {
while (1) {
if (flag) {
flag = 0;
do_heavy_work(); // 耗时操作放主循环
}
}
}

7.2 共享变量加 volatile

1
2
3
volatile uint8_t rx_complete = 0;  // ISR 和主循环共享 → 必须 volatile

// 不加 volatile:编译器可能优化成寄存器缓存,主循环永远读到旧值

7.3 中断抢占导致的竞态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 两个 ISR 都可能修改同一个全局变量 */
volatile int counter = 0;

// ISR_A: counter++;
// ISR_B: counter++;

// 如果 A 的抢占优先级 > B:
// A 可能在 B 执行 counter++ 的中间打断
// → 读-改-写序列被打断 → 计数丢失

// 解决:关中断保护
__disable_irq();
counter++;
__enable_irq();

7.4 清除中断标志

1
2
3
4
5
6
7
/* HAL 库的回调会自动清除标准标志,但自定义中断要注意 */
void EXTI15_10_IRQHandler(void) {
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_13)) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_13); // 必须手动清除
// ... 处理
}
}

八、中断调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. 中断是否进入?
→ ISR 开头加 GPIO 翻转,示波器测引脚
→ 或 ISR 里设置断点(注意:断点不阻塞其他中断)

2. 中断优先级是否对?
→ NVIC 配置打印出来
→ 高抢占的 ISR 能否打断低抢占的 ISR

3. 中断频率是否如预期?
→ 在 ISR 里计数,每秒打印一次计数值
→ TIM2 1Hz 中断,计数器应该是 1

4. 是否漏中断?
→ 示波器测中断引脚波形
→ 检查 ISR 执行时间是否过长(> 一个中断周期)