嵌入式从零开始(第五篇):嵌入式大脑 —— 中断与事件驱动

张开发
2026/4/12 12:53:45 15 分钟阅读

分享文章

嵌入式从零开始(第五篇):嵌入式大脑 —— 中断与事件驱动
还在用while轮询按键、让CPU“傻等”吗前言CPU 的“瞎等”困境假设我们写了一个程序让单片机检测按键是否按下然后点亮 LED。最直觉的写法是while(1){if(GPIO_ReadInputPin(BUTTON)PRESSED){LED_ON();}}这叫轮询Polling。CPU 像个勤恳的保安不停地在门口张望“按键没按键没按键没”——99.9% 的时间答案都是“没”但 CPU 依然忙得团团转无法去做其他事。这不仅浪费算力而且响应延迟不可控假如循环里还做了别的事检测按键的周期可能长达几毫秒按键按下到响应的时间就变成“随缘”了。有没有办法让 CPU平时安心做自己的事一旦按键按下就立刻放下手头工作去处理这就是中断干的事。一、中断是什么中断Interrupt是硬件或软件向 CPU 发出的一个“紧急请求”让 CPU 暂停当前正在执行的任务转而去执行一段特定的代码中断服务程序处理完后再回到原来的任务继续执行。我们可以把 CPU 想象成一个正在写作业的学生轮询学生每隔 1 秒抬头看一眼门口有没有人敲门。效率极低。中断学生安心写作业。有人敲门时门铃中断信号响起学生记下作业进度去开门处理完再回来继续写。中断的三大好处实时响应事件发生时立刻处理延迟只有几个时钟周期。高效利用 CPU没有事件时 CPU 可以全力做主要任务。低功耗没事做时可以进入睡眠模式中断将其唤醒。二、中断的来源与分类1. 外部中断引脚中断由芯片引脚上的电平变化触发上升沿、下降沿、高电平、低电平。比如按键按下、传感器输出跳变。2. 内部外设中断由片内外设产生定时器溢出、串口接收完成、ADC 转换结束、DMA 传输完成等。3. 系统异常内核中断由内核自身产生复位、硬件错误HardFault、系统调用SVC等。在 STM32 中中断控制器叫NVICNested Vectored Interrupt Controller负责管理所有中断的使能、优先级、嵌套。三、中断向量表ISR 的“通讯录”当中断发生时CPU 怎么知道该跳转到哪个地址执行代码答案是一张固定的表——中断向量表。它存放在 Flash 的起始地址通常是0x08000000里面按顺序存放着每个中断对应的中断服务函数ISR的入口地址。比如地址0x08000004复位中断程序入口地址0x08000008NMI不可屏蔽中断地址0x0800000CHardFault……地址0x080000XXEXTI0 中断外部引脚 0地址0x080000YYUSART1 中断我们写的void USART1_IRQHandler(void)这个函数名就是通过链接脚本“注册”到向量表中对应位置的。在 STM32 HAL 库中很多中断已经帮你写好了弱定义__weak的空函数你只需要重写override即可。四、中断优先级与嵌套如果多个中断同时发生或者一个中断正在执行时另一个更紧急的中断到来怎么办NVIC给每个中断分配了一个优先级。优先级分为抢占优先级Preemption Priority决定能否打断正在执行的中断。数值越小优先级越高。高抢占优先级可以打断低抢占优先级。子优先级Sub Priority当抢占优先级相同时决定谁先执行但不能互相打断。STM32 通常用 4 位表示优先级可以配置分组比如 3 位抢占 1 位子优先级或 22 等。例子中断 A抢占优先级 0正在执行中断 B抢占优先级 1到来 → B 必须等 A 完成。中断 A抢占优先级 0正在执行中断 C抢占优先级 0子优先级 1到来 → C 等待 A 完成但若 A 和 C 同时发生子优先级高的数字小先执行。注意中断服务程序应尽量短小精悍避免在 ISR 中做耗时操作。如果需要处理大量数据通常的做法是在 ISR 中置一个标志位然后由主循环或任务去处理。五、中断服务程序ISR的“三大纪律”写 ISR 时必须遵守以下原则否则系统可能崩溃1. 快进快出不要在 ISR 里做延时、复杂计算、打印长字符串。理想情况下ISR 只做清除中断标志重要否则会反复进入读取/保存少量数据设置一个标志位通过信号量/消息队列唤醒一个任务在 RTOS 中2. 保护共享资源如果 ISR 和主循环或其他中断访问同一个全局变量必须使用volatile关键字并考虑原子操作或禁用中断。volatileuint8_tflag0;voidEXTI0_IRQHandler(void){flag1;__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);}3. 避免调用不可重入函数printf、malloc等函数不可重入即不能在中断中安全调用。如需调试输出可以用轮询方式或 RTOS 的专用接口。六、从轮询到中断一个按键点灯的完整例子下面用 STM32 HAL 库展示如何配置外部中断。1. GPIO 配置使能中断GPIO_InitTypeDef GPIO_InitStruct{0};__HAL_RCC_GPIOA_CLK_ENABLE();GPIO_InitStruct.PinGPIO_PIN_0;// 假设按键接 PA0GPIO_InitStruct.ModeGPIO_MODE_IT_FALLING;// 下降沿触发按键按下时电平从高到低GPIO_InitStruct.PullGPIO_PULLUP;// 内部上拉HAL_GPIO_Init(GPIOA,GPIO_InitStruct);// 使能 EXTI0 中断HAL_NVIC_SetPriority(EXTI0_IRQn,2,0);HAL_NVIC_EnableIRQ(EXTI0_IRQn);2. 编写中断服务函数在 stm32f1xx_it.c 中或自己定义voidEXTI0_IRQHandler(void){// 检查是否是 PA0 引起的中断if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)!RESET){// 翻转 LED假设 LED 接 PC13HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);// 清除中断标志必须__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);}}3. 主循环可以“悠闲”做其他事intmain(void){HAL_Init();SystemClock_Config();// ... 初始化 LED 和按键中断 ...while(1){// 这里可以跑其他任务比如刷新屏幕、处理串口数据// 按键按下时会自动触发中断无需轮询}}七、事件驱动思想中断之上的编程模型有了中断我们可以构建事件驱动的程序架构裸机事件驱动主循环中检查由 ISR 设置的标志位执行相应处理。RTOS 事件驱动ISR 中释放信号量、发送消息队列唤醒等待的任务。这种模式让系统变得更加模块化、低耦合。例如串口接收中断每收到一个字节放入环形缓冲区并置位“数据可用”标志。主循环检测到标志从缓冲区取数据进行协议解析。相比轮询事件驱动能极大提高 CPU 利用率和响应实时性。八、中断的“副作用”与注意事项1. 中断延迟从硬件事件发生到 ISR 第一条指令执行需要一段时间保存现场、查向量表等。STM32 通常为 12 个时钟周期左右可以接受。但如果关中断时间过长会显著增加延迟。2. 临界区某些时候你需要禁止中断来保护一段代码例如操作一个不能被中断打断的共享变量。使用__disable_irq()和__enable_irq()但临界区应尽可能短。__disable_irq();// 操作敏感数据__enable_irq();3. 优先级反转虽然不如 RTOS 中常见但在裸机中如果低优先级中断长时间占用 CPU高优先级中断也可能被延迟。解决方法是保持 ISR 短小或合理配置优先级。4. 中断丢失如果同一个中断在 ISR 执行期间再次发生是否会丢失对于电平触发的中断只要引脚电平保持有效状态退出 ISR 后会再次触发。对于边沿触发的中断在 ISR 执行期间发生的边沿可能被记录挂起退出后立即再次进入。但若 ISR 执行时间过长连续多个边沿可能只响应一次。一般设计应保证 ISR 足够快。九、总结中断硬件向 CPU 发出的紧急请求实现实时响应和高效利用 CPU。NVICSTM32 的中断管理器支持优先级嵌套。ISR 准则快进快出、保护共享资源、不可重入函数要当心。事件驱动基于中断的编程思想让系统更高效、模块化。注意事项中断延迟、临界区、优先级反转、中断丢失。掌握了中断我们就从“顺序执行”的思维迈入了“异步事件驱动”的思维。这是嵌入式系统进阶的关键一步。系列导航第一篇嵌入式到底是什么含 ARM/C51/STM32 关系第二篇串口江湖 —— UART、RS-232、RS-485番外篇波特率解析第三篇两线走天下 —— I2C 总线精讲第四篇极速先锋 —— SPI 总线精讲第五篇嵌入式大脑 —— 中断与事件驱动本文第六篇时间管理大师 —— 定时器与系统滴答预告如果这篇文章让你彻底告别了while(!flag);的笨办法点个赞让更多人看到吧

更多文章