Chapter 04 · ARM 嵌入式 C 开发
中断系统
深入理解 Cortex-M NVIC 架构与中断优先级机制,正确编写 ISR,实现安全的临界区保护。
深入理解 Cortex-M NVIC 架构与中断优先级机制,正确编写 ISR,实现安全的临界区保护。
__disable_irq()),退出后开中断(__enable_irq())。另一种方法是使用原子操作或 FreeRTOS 的互斥量。volatile,防止编译器优化掉对它的读取。volatile,否则编译优化可能导致主循环永远读到旧值。/* NVIC 优先级配置示例 */ /* 第一步:设置优先级分组(整个程序只做一次,HAL_Init 已默认设置)*/ HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); /* NVIC_PRIORITYGROUP_4: 4位全给抢占, 0位子优先级 */ /* NVIC_PRIORITYGROUP_2: 2位抢占(0-3), 2位子(0-3) */ /* 第二步:配置各中断的优先级 */ HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0); /* 最高优先级 */ HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0); HAL_NVIC_SetPriority(USART1_IRQn, 3, 0); HAL_NVIC_SetPriority(DMA1_Stream0_IRQn, 2, 0); /* 第三步:使能中断 */ HAL_NVIC_EnableIRQ(EXTI0_IRQn); HAL_NVIC_EnableIRQ(TIM2_IRQn); HAL_NVIC_EnableIRQ(USART1_IRQn);
/** * @brief 配置 PA0 为外部中断(上升沿触发) * STM32F407-Discovery 用户按钮:按下 = PA0 高电平 */ void EXTI0_Config(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; /* 上升沿触发中断 */ /* GPIO_MODE_IT_FALLING : 下降沿 */ /* GPIO_MODE_IT_RISING_FALLING : 双边沿 */ GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); } /* ── 中断服务程序 ──────────────────────────────────────── */ /* 函数名必须与启动文件(startup_stm32f407xx.s)中的弱定义完全一致 */ volatile uint8_t btn_pressed = 0; /* 必须声明 volatile! */ void EXTI0_IRQHandler(void) { /* HAL 框架:先调 HAL 处理(清标志),再调用户回调 */ HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); } /* HAL 回调函数(弱函数,用户重写)*/ void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_0) { btn_pressed = 1; /* 仅设置标志,主循环处理 */ HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12); /* LED 切换 */ } } /* 主循环中检测并处理 */ void main_loop(void) { while (1) { if (btn_pressed) { btn_pressed = 0; /* 执行按键相关的耗时操作 */ } } }
/* ── 方法1:关/开全局中断(最简单,影响所有中断)── */ #define ENTER_CRITICAL() __disable_irq() #define EXIT_CRITICAL() __enable_irq() /* 示例:保护 64 位变量(读-改-写必须原子)*/ volatile uint64_t timestamp = 0; void Update_Timestamp(uint64_t new_val) { ENTER_CRITICAL(); timestamp = new_val; /* 64位赋值不是原子操作! */ EXIT_CRITICAL(); } uint64_t Read_Timestamp(void) { uint64_t val; ENTER_CRITICAL(); val = timestamp; EXIT_CRITICAL(); return val; } /* ── 方法2:保存/恢复中断状态(嵌套安全)── */ uint32_t Enter_Critical_Save(void) { uint32_t primask = __get_PRIMASK(); __disable_irq(); return primask; } void Exit_Critical_Restore(uint32_t primask) { if (!primask) __enable_irq(); /* 仅在之前是开中断状态才恢复 */ } /* ── 方法3:FreeRTOS 中使用 taskENTER_CRITICAL() ── */ /* 见第9章,FreeRTOS 提供任务级临界区,不影响中断优先级 >= configMAX_SYSCALL_INTERRUPT_PRIORITY */
① 忘记清中断标志:ISR 执行完,中断标志仍然置位,CPU 立刻再次进入 ISR,无限循环。
② 共享变量未 volatile:编译器优化,主循环永远读不到 ISR 写的新值。
③ ISR 中调用 HAL_Delay():SysTick 被屏蔽或优先级不够时,系统死锁。
④ 栈溢出:ISR 嵌套过深,或 ISR 中声明大型局部数组,超出 Cortex-M 的中断栈空间(默认 1KB)。
EXTI0 只能来自 PA0、PB0、PC0 ... 中的一个,通过 SYSCFG→EXTICR 选择端口。若需要 PA1 和 PB1 同时产生外部中断,是不可能的——它们都映射到 EXTI1,只能选一个。解决方案:把其中一个引脚换到不冲突的编号,或用轮询替代中断。