1、中断基础概念:中断的定义、中断向量表、中断优先级、中断嵌套
各位同学,咱们今天聊聊中断。说实话,中断这玩意儿是嵌入式系统的灵魂。没有中断,CPU就得像个傻子一样轮询——你想想看,那得多浪费算力?我刚开始做驱动时,对中断的理解也就停留在“按个键触发个事情”的层面。后来踩了不少坑,才真正搞明白这里面的门道。
1.1 中断到底是什么?
中断,说白了就是CPU正在干一件事,突然被一个紧急事件打断,它得停下来去处理这个紧急事件,处理完再回来接着干原来的活。
举个生活中的例子:你正在看书(主程序),突然电话响了(中断事件),你放下书去接电话(中断服务程序),接完电话回来继续看书(恢复执行)。嗯,就是这么个道理。
在嵌入式系统里,中断的来源五花八门:
- 外部中断:GPIO引脚电平变化、按键按下
- 定时器中断:定时器溢出、PWM周期到达
- DMA中断:数据传输完成
- 通信外设中断:UART收到数据、SPI传输完成、I2C仲裁丢失
- 软件中断:程序主动触发的中断(比如系统调用)
核心要点:中断机制让CPU从“被动轮询”变成“主动响应”,效率提升不止一个数量级。我在一个物联网项目里,把轮询改成了中断驱动,CPU占用率直接从85%降到了12%。
1.2 中断向量表——中断的“通讯录”
中断向量表,你可以把它理解成一本通讯录。每个中断源都有一个唯一的编号(中断号),CPU收到中断信号后,就去查这个通讯录,找到对应的处理函数地址,然后跳过去执行。
在ARM Cortex-M系列处理器里,中断向量表通常放在Flash的起始地址(0x00000000)。前16个是系统异常,从第16个开始才是外部中断。我习惯把向量表叫做“中断的入口地图”——地图画错了,你永远到不了目的地。
| 向量位置 | 中断号 | 类型 | 描述 |
|---|---|---|---|
| 0x00000000 | - | 栈顶指针 | 初始SP值 |
| 0x00000004 | - | Reset_Handler | 复位中断 |
| 0x00000008 | - | NMI_Handler | 不可屏蔽中断 |
| 0x0000000C | - | HardFault_Handler | 硬错误 |
| ... | ... | ... | ... |
| 0x00000040 | 0 | IRQ0_Handler | 第一个外部中断 |
| 0x00000044 | 1 | IRQ1_Handler | 第二个外部中断 |
避坑指南:我曾经在一个项目里,因为中断向量表地址配置错了,导致中断触发后程序直接跑飞。查了两天才发现是链接脚本里向量表偏移设错了。记住:中断向量表地址必须与硬件设计一致,尤其是做Bootloader时,向量表重定位(VTOR)一定要配对。
1.3 中断优先级——谁更紧急谁先上
多个中断同时来了怎么办?总得有个先后顺序吧。中断优先级就是干这个的。
ARM Cortex-M处理器支持可编程的中断优先级,一般用4位(0-15)或8位(0-255)来表示优先级数值。注意:数值越小,优先级越高。这个跟直觉相反,我刚开始也老搞混。
优先级分组也是个有意思的设计。Cortex-M把优先级分为抢占优先级和子优先级:
- 抢占优先级:决定能否打断正在执行的中断
- 子优先级:当抢占优先级相同时,决定谁先执行
举个例子:中断A的抢占优先级是1,中断B的抢占优先级是2。A正在执行,B来了——B得等着,因为A的优先级更高。但如果A和B的抢占优先级都是1,那就看子优先级,子优先级小的先执行。
实际经验:我在做电机控制驱动时,把电流环中断设为最高优先级(0),速度环设为次高(1),位置环设为最低(2)。这样电流环永远不会被其他中断打断,保证了控制的实时性。你想想看,如果电流环被耽误了,电机可能直接就烧了。
1.4 中断嵌套——中断里还能有中断?
中断嵌套,就是高优先级的中断可以打断低优先级的中断服务程序。这听起来有点递归的味道,但硬件帮你处理好了。
当CPU正在执行中断A的服务程序时,来了一个优先级更高的中断B:
- CPU暂停中断A的执行
- 保存中断A的上下文(寄存器、返回地址等)
- 跳转到中断B的服务程序
- 中断B执行完毕,恢复中断A的上下文
- 继续执行中断A
这个过程,硬件自动完成了大部分工作。Cortex-M处理器有硬件压栈机制,发生中断嵌套时,硬件会自动把当前中断的寄存器压入栈中,然后切换到新中断的栈空间。
注意:中断嵌套虽然好用,但别滥用。嵌套层数太多会导致栈溢出,而且中断响应延迟会变大。我见过一个项目,嵌套了5层中断,结果栈空间不够,程序跑着跑着就挂了。我的建议是:中断嵌套控制在2-3层以内,能用轮询解决的别用中断嵌套。
1.5 中断处理流程全景图
下面这张图,把中断从触发到处理完成的整个流程串起来了。我画图时习惯把关键节点标清楚,这样调试时一眼就能看出问题出在哪。
1.6 中断服务程序(ISR)的编写要点
写ISR跟写普通函数不一样。我见过太多新手在ISR里干各种“大事”,结果系统卡得跟幻灯片似的。记住几个原则:
- 快进快出:ISR里别做耗时操作,比如打印、malloc、延时。我一般只在ISR里设置标志位或拷贝数据,真正的处理放到主循环或任务里。
- 不可重入:除非你明确知道自己在做什么,否则别在ISR里调用不可重入函数。比如printf,很多实现都不是线程安全的。
- 关中断时间要短:如果需要在ISR里关中断保护临界区,关中断的时间越短越好。我曾经在一个项目里关了50微秒的中断,结果UART丢数据了。
个人习惯:我写ISR时,第一件事就是清除中断标志位。为什么?因为如果不先清标志,同一个中断可能会反复触发,导致ISR被重入,逻辑就乱了。这个坑我踩过,现在每次写ISR都条件反射地先清标志。
1.7 中断与Linux内核的关联
咱们这门课讲的是嵌入式Linux中断处理,所以得提一句Linux里的中断机制。Linux把中断分成了上半部(Top Half)和下半部(Bottom Half):
- 上半部:在中断上下文执行,要求快,只做最紧急的事(比如清标志、读硬件寄存器)
- 下半部:在软中断或工作队列中执行,可以做耗时操作(比如数据处理、协议解析)
这个设计思路,说白了就是把中断处理拆成“紧急”和“不紧急”两部分。紧急的立刻处理,不紧急的等会儿再说。我在做网络驱动时,上半部只把收到的数据包拷贝到缓冲区,下半部才做协议栈处理——这样网卡就不会因为处理协议而错过下一个数据包。
总结一下:中断是嵌入式系统的“紧急通道”,向量表是“通讯录”,优先级是“排队规则”,嵌套是“紧急打断紧急”。把这些基础概念吃透了,后面写中断驱动才能游刃有余。