1.中断的理解
什么是中断:
当 CPU 正在执行主程序时,如果遇到紧急事件(中断请求),CPU 会暂停当前任务,保存当前状态(如将关键寄存器的值保存到栈中),然后跳转执行对应的中断服务函数(ISR)。紧急事件处理完成后,CPU 恢复之前的状态,继续执行主程序,这个过程称为中断。
为了更直观地理解,我们可以用一个生活场景来类比:
想象你正在厨房里煮饭(主程序)。这时:
- 电话铃响了(中断请求)——电话响是一个突发事件,你需要暂停煮饭去接电话。
- 记住锅里还有饭在煮(保存上下文)——你心里记着饭还在锅里,并把火调小,避免饭烧焦,就像 CPU 保存当前状态,以便之后能恢复任务。
- 接电话聊天(执行中断服务函数)——你转身去接电话,与朋友快速交谈。
- 挂断电话后回到厨房(恢复上下文)——通话结束后,你回到厨房,回想起自己正在煮饭这件事。
- 继续煮饭(返回主程序)——你打开锅盖,继续煮饭并完成接下来的步骤。
中断的主要分类:
1.内核中断
- 系统异常中断:由处理器自身的异常状态触发,如硬件故障、非法指令等。
- 故障中断:如总线错误、存储器访问错误等。
2.外部中断
- 片上外设中断:来自 MCU 内部的外设(如 USART、TIM、ADC 等)触发的中断。
- 外部中断线(EXTI)中断:来自芯片引脚(如外部按钮、传感器信号等)通过 EXTI 控制器触发的中断。
3.软件中断
- 由软件指令触发(如
SVC
指令),常用于切换到特权级模式或者系统调用。
什么是中断优先级:
中断优先级决定了当多个中断同时发生时,CPU处理它们的顺序。STM32 的中断优先级分为三个层级:
-
抢占优先级(主优先级):决定是否可以打断正在执行的中断。高抢占优先级的中断可以打断低抢占优先级的中断。
-
响应优先级(子优先级):当多个中断的抢占优先级相同时,响应优先级决定它们的执行顺序,优先级越高的中断先执行。
-
自然优先级:由硬件决定,与中断向量表中断请求的位置有关,仅在抢占优先级和响应优先级都相同时才会生效,自然优先级不可能相同,因此是兜底规则,很少触发比较。
中断嵌套规则:
同时发生的中断
-
首先比较抢占优先级,谁的抢占优先级高就先执行谁。
-
如果抢占优先级相同,则比较响应优先级,谁的响应优先级高就先执行谁。
-
如果抢占优先级和响应优先级都相同,则比较自然优先级,自然优先级高的中断先执行,自然优先级不会相同。
-
执行完高优先级中断后,CPU 会继续执行低优先级中断。
为了更直观地理解,我们可以用一个生活场景来类比:
想象你办公室写报告(主程序),这时:
电话铃响了(中断1触发),同时你的老板走进办公室要你立即汇报工作(中断2触发)。
因为老板的事情更紧急(优先级更高),你放下电话去汇报工作。
汇报结束后,你回去接电话(执行中断1)。
接完电话后,你继续写报告(返回主程序)。
嵌套中断
-
如果事件1正在执行,此时事件2发生,比较两者的抢占优先级。
-
如果事件2的抢占优先级更高,则中断事件1,先执行事件2,执行完后再返回继续执行事件1。
-
如果事件2的抢占优先级低于或等于事件1,则继续执行事件1,待其执行完后再处理事件2。
注意:在处理中断嵌套时,不比较自然优先级和响应优先级,只考虑抢占优先级。
生活场景类比:
想象你正在厨房里煮饭(主程序)。这时:
- 电话铃响了(中断1触发)——你暂停煮饭去接电话。
- 正当你接电话时,锅里的水突然溢出来了!(中断2触发,且抢占优先级比中断1更高)——你立刻对朋友说:“等一下!”,冲去厨房处理溢水。
- 处理完溢水后——你回去继续接电话(返回中断1)。
- 电话打完了——你最后回到厨 房,继续煮饭(返回主程序)。
但还有另一种场景:
还是在厨房里煮饭(主程序)。这时:
- 锅里的水突然溢出来了!(中断2触发,抢占优先级较高),你立刻暂停煮饭,冲去关火、擦干水渍(执行中断2的服务函数)。
- 正当你擦水时,电话铃响了(中断1触发,抢占优先级较低),你听到了电话铃声,但因为溢水更紧急,你无视电话铃声,继续处理溢水问题。只有等你解决完溢水问题后,才会去接电话。
- 溢水处理完毕后(中断2结束),你放下抹布,去接电话(执行中断1的服务函数)。
- 最后,你才回到厨房,继续煮饭(返回主程序)。
优先级等级总结:抢占优先级>响应优先级>自然优先级,抢占优先级决定是否可以嵌套,响应优先级和自然优先级决定中断同时发生时,先响应谁(如果抢占相同,才依据响应,如果抢占和响应都相同,自然优先级才会起作用)
中断服务函数与普通函数的执行机制:
-
相同点:
- 程序执行跳转:无论是普通函数还是中断服务函数,程序执行时都会跳转到另一段代码,待执行完成后,再跳转回原来的位置,继续执行主程序。实际上,两者都使用了程序控制流跳转,只是跳转时机不同。
-
不同点:
- 普通函数:普通函数的执行时机是由主程序的控制流程决定的,通常是在主函数中明确调用某个函数时才执行。当程序运行到函数调用时,它跳转到目标函数执行,完成后返回。
- 中断服务函数(ISR):中断服务函数的执行时机是不固定的,它依赖于硬件或外部事件(如外部信号、定时器溢出等)。当中断发生时,当前执行的代码会被中断,程序会跳转到对应的ISR来处理该中断,执行完ISR后再返回到中断发生前的程序继续执行。
注意:在中断服务程序中,不能出现死循环。实际在使用的时候,服务程序要求执行的程序尽量短,因为中断要求实时响应,不能出现大延时。
什么时候用中断:
- 主动过程:如果事件是可控且可预测的,比如在特定时刻执行的任务,使用普通函数足够了,不需要中断。
- 被动过程:如果事件无法预测,且需要立即响应,就使用中断。
有2个生活中的场景可以帮助理解一下:
- 在看书的过程中,8点钟需要给某人打一个电话,这种情况就没有必要一直在电话旁等着,我们可以一直看书,只要8点钟的时候去打电话就好,这是主动过程,不用中断。
- 在看书的过程中,只知道会有电话打进来,但是时间不确定,这种情况铃声就是中断信号,我们听到铃声才去接电话,这是被动过程,铃声是一个中断信号。
2.CM3和和基于其的STM32中断控制系统
嵌套向量中断控制器NVIC:
在 Cortex-M3 内核中,有一个专门用于管理中断的硬件模块------NVIC(Nested Vectored Interrupt Controller )。它直接集成在内核中,用于处理所有外部和内部的中断请求,并支持优先级和嵌套机制。
NVIC 的核心功能:
中断嵌套机制:
NVIC 允许高优 先级的中断抢占正在执行的低优先级中断,实现中断嵌套。
可编程优先级:
- 抢占优先级(Preemption Priority):决定一个中断是否可以打断其他中断。
- 响应优先级(Subpriority):在抢占优先级相同时,决定谁先执行。
- 通过 AIRCR(应用程序中断和复位控制寄存器)配置优先级分组。
向量表机制:
每个中断都有一个固定的入口地址,保存在向量表中。NVIC 通过向量表快速找到对应的中断服务函数(ISR)。
中断使能/屏蔽:
提供寄存器来控制中断的开启和关闭,比如:
ISER
(中断设置使能寄存器)ICER
(中断清除使能寄存器)中断挂起/清除:
ISPR
(中断设置挂起寄存器)ICPR
(中断清除挂起寄存器)系统异常管理:
还负责管理一些系统级的异常,比如:
- 硬故障(HardFault)
- 总线错误(BusFault)
- 用法错误(UsageFault)
优先级的设置:
在Cortex-M3(CM3)架构中,NVIC负责管理中断的优先级,每个中断源都有一个8位的优先级寄存器。但在SMT32中只用到其中的高4位:
- 高4位 用于配置中断优先级,包括 抢占优先级 和 响应优先级。
- 低4位 固定为 0,没有实际作用。
具体抢占优先级和响应优先级各用多少位需要根据设置的优先级分组决定。
说明:优先级的级别值越小,优先级越高。
优先级的分组:
在Cortex-M3(CM3)架构中,优先级分组是通过设置应用程序中断及复位控制寄存器(AIRCR)的PRIGROUP
字段(位10~8)来实现的。这个设置决定了抢占优先级和响应优先级在优先级寄存器高4位中的分配方式。
优先级分组规则:
优先级分组的写入值与抢占优先级和响应优先级的位数之间的关系如下表所示:
写入值(10~8位) | 抢占优先级位数 | 响应优先级位数 | 抢占优先级范围 | 响应优先级范围 |
---|---|---|---|---|
0x07 (7) | 0 | 4 | 0 | 0~15 |
0x06 (6) | 1 | 3 | 0~1 | 0~7 |
0x05 (5) | 2 | 2 | 0~3 | 0~3 |
0x04 (4) | 3 | 1 | 0~7 | 0~1 |
0x03 (3) | 4 | 0 | 0~15 | 0 |
规律总结:
-
写入值 = 7 - 抢占优先级的位数
例如,如果需要设置抢占优先级占用2位,则写入值为7 - 2 = 5
(即0x05
)。 -
抢占优先级位数 + 响应优先级位数 = 4
优先级寄存器的高4位被分配给抢占优先级和响应优先级,两者位数之和为4。
设置优先级分组的步骤:
-
确定抢占优先级和响应优先级的级别值
根据应用需求,确定抢占优先级和响应优先级的范围。 -
确定抢占优先级占用的位数
根据抢占优先级的范围,确定需要的位数。 -
计算写入值
使用公式写入值 = 7 - 抢占优先级位数
,计算出需要写入PRIGROUP
字段的值。 -
配置AIRCR寄存器
将计算出的写入值设置到AIRCR寄存器的PRIGROUP
字段(位10~8)。
3.STM32中断的编程步骤
3.1NVIC控制器的4个主要函数:
在 Cortex-M 系列芯片中,NVIC 是内核级的中断控制器,其寄存器和配置方式由 ARM 统一设计,因此不同芯片厂商(如 ST、NXP、TI 等)的 NVIC 配置方法是完全兼容的。ARM 通过 CMSIS(Cortex Microcontroller Software Interface Standard)提供了一套标准化的 NVIC 配置函数,开发者无需直接操作寄存器,只需调用这些函数即可完成中断配置。
这些函数定义在 CMSIS 核心文件 core_cm3.h
/core_cm4.h
中(具体文件名取决于内核版本)。
中断源优先级分组设置:
在 Cortex-M 系列芯片中,NVIC_SetPriorityGrouping 函数用于设置中断优先级的分组规则,决定 抢占优先级 和 抢占优先级 的位数分配。这个函数是 CMSIS 标准库的一部分,开发者可以通过它统一配置所有中断源的优先级分组。
函数:void NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
函数名:NVIC_SetPriorityGrouping
函数功能:设置中断的优先级分组,分配抢占优先级和响应优先级的位数。
函数返回值:无
函数参数:uint32_t PriorityGroup //写入值 = 7 - 要设置的抢占优先级的位数
注意:优先级分组是全局配置,所有中断源共用同一分组规则,只需设置一次。
位置:通常放在主函数中其他模块初始化函数的上方。
计算优先级编码:
在 Cortex-M 系列芯片中,NVIC_EncodePriority 用于将 抢占优先级 和 响应优先级 编码为一个完整的优先级值。这个函数是 CMSIS 标准库的一部分,开发者无需手动计算优先级编码,直接调用该函数即可。
函数:uint32_t NVIC_EncodePriority (uint32_t PriorityGroup, uint32_t PreemptPriority, uint32_t SubPriority)
函数名:NVIC_EncodePriority
函数功能:根据优先级分组、抢占优先级值和子优先级值,计算出一个完整的优先级编码值用于后续设置中断优先级。
函数返回值:uint32_t
函数参数:uint32_t PriorityGroup //优先级分组
uint32_t PreemptPriority //抢占优先级值
uint32_t SubPriority //响应优先级值
注意:确保 NVIC_EncodePriority
的 PriorityGroup
参数与全局优先级分组一致。抢占优先级和子优先级值必须在分组允许的范围内,否则会导致未定义行为。
位置:放在各自中断源的配置函数里。
具体某个中断源的优先级设置:
在 Cortex-M 系列芯片中,NVIC_SetPriority 函数用于设置具体某个中断源的优先级。每个中断源都有一个唯一的中断编号(IRQn_Type
),通过该函数可以将计算出的优先级编码写入对应的中断优先级寄存器中。
函数:void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
函数名:NVIC_SetPriority
函数功能:设置具体某个中断源的优先级
函数返回值:无
函数参数:IRQn_Type IRQ //具体中断的编号(直接写名字)
uint32_t priority //计算出来的中断优先级编码
关于IRQn_Type IRQ这个参数,具体中断的编号在stm32f10x.h中
在stm32f10x.h中,可以找到typedef enum IRQn,这个 IRQn 枚举类型定义了一系列中断号。
这些中断号可以分为三部分:
- · 内核级别中断源 — Cortex-M3 固有,负数中断号。
- · STM32 通用中断源 — 所有 STM32F10x 共有,正数中断号,从 0 开始。
- · 芯片型号专属中断源 — 各型号芯片额外的外设中断号,通过宏定义筛选。
前两部分是所有芯片都有的,至于第三部分各芯片型号各自的中断源,我们要如何选择正确的型号专属中断源呢?还记得我们之前在创建工程时添加过的一个全局宏定义吗?
路径:Options for Target -> C/C++ -> Define
这个宏定义在这里就派上用场了,我们找我们添加的宏定义对应的部分就好。
中断响应通道使能:
在 Cortex-M 系列芯片中,NVIC_EnableIRQ 函数用于使能某个中断源的响应通道。通过调用该函数,NVIC 模块会开始监听指定的中断源,并在中断发生时跳转到对应的中断服务函数(ISR)进行处理。
函数:void NVIC_EnableIRQ(IRQn_Type IRQn)
函数名:NVIC_EnableIRQ
函数功能:NVIC模块响应片上外设中断源
函数返回值:无
函数参数:IRQn_Type IRQ //具体中断的编号
说明:
中断控制器层级的中断使能和外设层级的中断使能
这里以串口接收中断使能为例子说明,实际一个外设的外设层的中断使能有很多。
例: (这里我也只是截取了一小部分)
层级关系:
- 串口接收中断使能(外设层):告诉外设“当数据接收完成时,要生成中断信号”。
- 中断响应通道使能(中断控制器层):告诉CPU“允许响应来自该外设的中断信号”。
缺一不可:
- 如果只使能外设中断但未开启中断通道,外设会发送中断请求,但CPU会忽略。
- 如果只开启中断通道但未使能外设中断,外设根本不会产生中断请求。
总结:
①设置优先级分组,例:
NVIC_SetPriorityGrouping(5); //两位抢占,两位响应
②计算优先级编码值,例:
uint32_t pri = NVIC_EncodePriority(5, 1, 2); //抢占优先级1,响应优先级2
③设置具体中断源,例:
NVIC_SetPriority(USART1_IRQn, pri); //设置USART1的具体优先级
④NVIC响应通道使能,例:
NVIC_EnableIRQ(USART1_IRQn); //使能USART1的中断通道
3.2中断服务函数
在 Cortex-M 系列芯片中,中断服务函数(ISR,Interrupt Service Routine) 是当某个中断触发时,由 CPU 自动调用的函数。中断服务函数的编写需要遵循特定的规则和格式,以确保其正确性和高效性。
中断服务函数的特点:
-
函数名固定
中断服务函数的名称必须与芯片厂商提供的向量表名称一致。向量表通常定义在启动文件(如startup_stm32f410x.hd.s
)中。 -
无参数、无返回值
中断服务函数的原型必须为:void ISR_Name(void);
-
无需手动调用
中断服务函数由硬件自动调用,开发者无需在代码中显式调用。 -
执行时间尽量短
中断服务函数应尽量简短,避免使用大量延时或死循环,以免影响其他中断的响应。 -
清除中断标志
在中断服务函数中,必须清除触发中断的标志位,否则会重复进入中断。
中断服务函数的编写步骤:
-
确定中断服务函数名
在芯片厂商提供的启动文件(如 startup_stm32f410x.hd.s)中查找中断向量表,确定所需中断服务函数的名称。如图为STM32F103芯片的中断服务函数名:
-
编写中断服务函数
我们通常将中断服务函数编写在单独的源文件中,例如
isr.c
,如果该文件不存在,可以手动创建一个。 -
判断具体哪个中断信号触发
同一中断源可能有多个中断信号。当中断发生时,需判断究竟是哪个信号触发了中断(需要打开对应的外设层的中断使能),以便根据不同情况执行相应操作。通常可以通过读取相关的标志寄存器或者状态寄存器来确认是哪一个中断信号。
-
清除中断标志
在中断服务函数中,必须清除触发中断的标志位,以避免重复进入中断服务函数。部分中断标志由硬件自动清除,但大多数情况下,中断标志需要通过软件手动清除。可以通过写入特定寄存器或清除中断标志位来实现。这一步很关键,因为如果不清除中断标志,处理器会不断地进入中断服务函数,导致程序“卡住”在中断服务程序中。
-
处理中断逻辑
编写具体的紧急事件处理程序,根据实际需求执行必要的操作。这部分逻辑通常需要尽量简洁,以免长时间占用处理器资源。