本人编著的《基于STM32的嵌入式系统原理及应用》(ISBN:9787030697974)刚刚在科学出版社出版。这本书花费了半年以上的时间,凝聚了笔者作为高校教师和嵌入式工程师的一些经验,希望对大学生、嵌入式初学者和有一定经验的工程师都有参考和借鉴作用。在写作中尤其注意了不做芯片手册的“搬运工”和“翻译者”,试图从开发者和工程师的角度理清知识点之间的逻辑关系,给读者一条清晰的学习路径。写作中尽量做到用生活中的实例来阐述抽象的概念;用工程实例来帮助大家提高嵌入式开发的实践能力。另外为帮助读者理解某些难点,我们在Bilibili上陆续发布免费的讲解的视频,欢迎大家关注我的B站账号:“何乐生0”(本文讲解点这里)。
下面将该书中关于STM32的Cortex-M3中断控制器NVIC的一小节发出来供大家拍砖,欢迎大家批评指正。以下内容欢迎转载,但请注明出处: https://www.cnblogs.com/helesheng
一、中断机制概述
中断是计算机系统的一种处理异步事件的重要方法。它的作用是在计算机的CPU运行软件的同时,监测系统内外有没有发生需要CPU处理的“紧急事件”:当需要处理的事件发生时,中断控制器会打断CPU正在处理的常规事务,转而插入一段处理该紧急事件的代码;而该事务处理完成之后,CPU又能正确地返回刚才被打断的地方,以继续运行原来的代码。中断可以分为“中断响应”、“中断处理”和“中断返回”三个阶段。
中断处理事件的异步性是指,紧急事件在什么时候发生与CPU正在运行的程序完全没有关系,是无法预测的。既然无法预测,只能随时查看这些“紧急事件”是否发生,而中断机制最重要的作用,是将CPU从不断监测紧急事件是否发生这类繁重工作中解放出来,将这项“相对简单”的繁重工作交给“中断控制器”这个硬件来完成。中断机制的第二个重要作用是判断哪个或哪些中断请求更紧急,应该优先被响应和处理,并且寻找不同中断请求所对应的中断处理代码所在的位置。中断机制的第三个作用是帮助CPU在运行完处理紧急事务的代码后,正确地返回之前运行被打断的地方。根据上述中断处理的过程及其作用,读者会发现中断机制既提高了CPU正常运行常规程序的效率,又提高了响应中断的速度,是几乎所有现代计算机都配备的一种重要机制。
嵌入式系统是嵌入宿主对象中,帮助宿主对象完成特定任务的计算机系统,其主要工作就是和真实世界打交道。能够快速、高效地处理来自真实世界的异步事件成为嵌入式系统的重要标志,因此中断对于嵌入式系统而言显得尤其重要,是学习嵌入式系统的难点和重点。
1.1 中断的基本概念
这里通过一个生活实例来解释中断机制中涉及的各种概念:某人在家中读书学习,假设可能有两件打断他学习的事情分别是:有人敲门来访和接听电话。安静地读书学习相当于嵌入式处理器的常规程序,而“有人敲门来访”和“接听电话”两件事就是可能引发中断的“中断源”,中断之前他读书所达到的页码称为“断点”。如图1所示,中断源提出要求响应中断称为“中断请求”;而CPU打断原来的程序运行,转而处理开门或接电话等事情称为“中断响应”;为响应事件而运行的程序称为“中断服务程序”;处理完异步事件,返回的过程称为“中断返回”。中断响应过程又可以分为自动保存当前寄存器值的“现场保护”和定位并跳转到中断服务程序地址两个小步骤。中断返回也相应地分为恢复寄存器值的“恢复现场”和恢复原来主程序运行的位置的“返回断点”两个小步骤。当然,也可以选择不理睬这些打断读书的事情,例如,将手机设置为静音状态,此时就称为“中断屏蔽”。
图1 中断示意图
如图2所示,当电话铃声和敲门声同时响起时,只能根据事情的重要性,先响应一件事,而决定孰先孰后的分级机制称为“中断优先级”。显然图2中开门迎客的优先级高于接听电话。
图2 中断优先级示意图
优先级高可以有两种体现,一种如图2所示,两个中断同时到来时先响应优先级较高的那个中断请求,再响应优先级低的那个。优先级高还可以体现为,即使优先级低的中断服务程序在运行中,优先级高的中断也可以打断优先级低的中断服务程序,而优先级低的中断服务程序只有等到优先级高的中断服务程序运行完成后才能继续运行。如图3所示,开门任务能够打断接听电话任务,形成类似“菊花”的嵌套结构,就称为“中断嵌套”。STM32中将能够被打断,实现中断嵌套的优先级称为“抢占优先级”,而不能够被打断,只能优先响应的中断优先级称为“子优先级”。
图3 中断嵌套示意图
中断机制中还有一个重要概念称为“中断向量表”,中断向量表是一张由中断服务程序的入口地址构成的表格,一般占据代码空间的起始地址。处理器设计者会将所有可能响应的中断源所对应的中断服务程序入口地址按照固定的顺序排列在该表格中。当某个中断源被响应时,处理器会自动跳转到该中断源的中断服务程序入口所在的表格地址,并由该表格位置进一步跳转到中断服务程序。
1.2 ARM Cortex-M3的中断控制器——NVIC
ARM V7架构将处理器分为应用处理器A型、实时处理器R型和微控制器M型。M型中最重要的ARM Cortex-M3充分考虑了嵌入式应用中对异步事件处理的实时性要求。因而与之前的ARM架构不同,直接在Cortex-M3内核中集成了功能强劲的中断控制器,并命名为NVIC。在内核中集成中断控制器不但防止了不同芯片生产厂商间的不兼容,还大大提高了中断控制器和Cortex-M3内核的结合紧密度,降低了中断响应时间,提高了实时性。
NVIC直译为嵌套向量中断控制器,顾名思义,指ARM Cortex-M3的中断控制器支持中断嵌套和中断向量表的自动跳转功能。典型的NVIC可支持256个中断,其中包括16个由内核产生的异常中断和240个外设中断。其中,内核异常中断指由Cortex-M3内核产生的复位、硬件错误、SysTick定时器中断等中断,而外设中断则是由管脚电平变化、UART或DMA等外设变化引起的中断。NVIC还能实现Cortex-M3内核响应中断请求后的自动现场保护(自动保存处理器状态寄存器)和中断返回时的自动现场恢复(自动恢复处理器状态寄存器)。另外还有一种称为“中断尾链(末尾连锁)”的技术,能够在从高优先级的中断服务程序返回时避免多余的自动现场恢复,即自动进入低优先级的中断服务程序,从而避免了一轮多余的自动现场恢复和自动现场保护操作。
二、 STM32的NVIC
STM32的NVIC也只是标准NVIC的一部分,但主要功能都已经包含在其中:STM32的NVIC可支持16个内核异常中断和68个外设中断(其中STM32F103系列60个,STM32F107系列68个)。同时,每个中断源可配置4位优先级控制字PRI_n(ARM Cortex-M3内核定义了8位,STM32微控制器只使用了其中的4位),具有16级可编程中断优先级。
表1是STM32F103系列的中断向量表,供读者查询。其中灰色部分是内核异常中断。
表1 STM32F103系列的中断向量表
位置 |
优先级 |
优先级类型 |
名称 |
说明 |
地址 |
— |
— |
— |
保留 |
0x0000_0000 |
|
3 |
固定 |
Reset |
复位 |
0x0000_0004 |
|
2 |
固定 |
NMI |
不可屏蔽 RCC时钟安全系统(CSS)连接到NMI向量 |
0x0000_0008 |
|
1 |
固定 |
硬件失效(HardFault) |
所有类型的失效 |
0x0000_000C |
|
0 |
可设置 |
存储管理(MemManage) |
存储器管理 |
0x0000_0010 |
|
1 |
可设置 |
总线错误(BusFault) |
预取指失败,存储器访问失败 |
0x0000_0014 |
|
2 |
可设置 |
错误应用(UsageFault) |
未定义的指令或非法状态 |
0x0000_0018 |
|
— |
— |
— |
保留 |
0x0000_001C ~0x0000_002B |
|
3 |
可设置 |
SVCall |
通过SWI指令的系统服务调用 |
0x0000_002C |
|
4 |
可设置 |
调试监控(DebugMonitor) |
调试监控器 |
0x0000_0030 |
|
— |
— |
— |
保留 |
0x0000_0034 |
|
5 |
可设置 |
PendSV |
可挂起的系统服务 |
0x0000_0038 |
|
6 |
可设置 |
SysTick |
系统定时器 |
0x0000_003C |
|
0 |
7 |
可设置 |
WWDG |
窗口定时器中断 |
0x0000_0040 |
1 |
8 |
可设置 |
PVD |
连接到EXTI的电源检测(PVD)中断 |
0x0000_0044 |
2 |
9 |
可设置 |
TAMPER |
侵入检测中断 |
0x0000_0048 |
3 |
10 |
可设置 |
RTC |
实时时钟(RTC)全局中断 |
0x0000_004C |
4 |
11 |
可设置 |
FLASH |
闪存全局中断 |
0x0000_0050 |
5 |
12 |
可设置 |
RCC |
复位和时钟控制(RCC)中断 |
0x0000_0054 |
6 |
13 |
可设置 |
EXTI0 |
EXTI线0中断 |
0x0000_0058 |
7 |
14 |
可设置 |
EXTI1 |
EXTI线1中断 |
0x0000_005C |
8 |
15 |
可设置 |
EXTI2 |
EXTI线2中断 |
0x0000_0060 |
9 |
16 |
可设置 |
EXTI3 |
EXTI线3中断 |
0x0000_0064 |
10 |
17 |
可设置 |
EXTI4 |
EXTI线4中断 |
0x0000_0068 |
位置 |
优先级 |
优先级类型 |
名称 |
说明 |
地址 |
11 |
18 |
可设置 |
DMA1通道1 |
DMA1通道1全局中断 |
0x0000_006C |
12 |
19 |
可设置 |
DMA1通道2 |
DMA1通道2全局中断 |
0x0000_0070 |
13 |
20 |
可设置 |
DMA1通道3 |
DMA1通道3全局中断 |
0x0000_0074 |
14 |
21 |
可设置 |
DMA1通道4 |
DMA1通道4全局中断 |
0x0000_0078 |
15 |
22 |
可设置 |
DMA1通道5 |
DMA1通道5全局中断 |
0x0000_007C |
16 |
23 |
可设置 |
DMA1通道6 |
DMA1通道6全局中断 |
0x0000_0080 |
17 |
24 |
可设置 |
DMA1通道7 |
DMA1通道7全局中断 |
0x0000_0084 |
18 |
25 |
可设置 |
ADC1_2 |
ADC1和ADC2的全局中断 |
0x0000_0088 |
19 |
26 |
可设置 |
USB_HP_CAN_TX |
USB高优先级或CAN发送中断 |
0x0000_008C |
20 |
27 |
可设置 |
USB_LP_CAN_RX0 |
USB低优先级或CAN接收0中断 |
0x0000_0090 |
21 |
28 |
可设置 |
CAN_RX1 |
CAN接收1中断 |
0x0000_0094 |
22 |
29 |
可设置 |
CAN_SCE |
CAN SCE中断 |
0x0000_0098 |
23 |
30 |
可设置 |
EXTI9_5 |
EXTI线[9:5]中断 |
0x0000_009C |
24 |
31 |
可设置 |
TIM1_BRK |
TIM1刹车中断 |
0x0000_00A0 |
25 |
32 |
可设置 |
TIM1_UP |
TIM1更新中断 |
0x0000_00A4 |
26 |
33 |
可设置 |
TIM1_TRG_COM |
TIM1触发和通信中断 |
0x0000_00A8 |
27 |
34 |
可设置 |
TIM1_CC |
TIM1捕获比较中断 |
0x0000_00AC |
28 |
35 |
可设置 |
TIM2 |
TIM2全局中断 |
0x0000_00B0 |
29 |
36 |
可设置 |
TIM3 |
TIM3全局中断 |
0x0000_00B4 |
30 |
37 |
可设置 |
TIM4 |
TIM4全局中断 |
0x0000_00B8 |
31 |
38 |
可设置 |
I2C1_EV |
I2C1事件中断 |
0x0000_00BC |
32 |
39 |
可设置 |
I2C1_ER |
I2C1错误中断 |
0x0000_00C0 |
33 |
40 |
可设置 |
I2C1_EV |
I2C2事件中断 |
0x0000_00C4 |
34 |
41 |
可设置 |
I2C1_ER |
I2C2错误中断 |
0x0000_00C8 |
35 |
42 |
可设置 |
SPI1 |
SPI1全局中断 |
0x0000_00CC |
36 |
43 |
可设置 |
SPI2 |
SPI2全局中断 |
0x0000_00D0 |
37 |
44 |
可设置 |
USART1 |
USART1全局中断 |
0x0000_00D4 |
38 |
45 |
可设置 |
USART2 |
USART2全局中断 |
0x0000_00D8 |
39 |
46 |
可设置 |
USART3 |
USART3全局中断 |
0x0000_00DC |
40 |
47 |
可设置 |
EXTI15_10 |
EXTI线[15:10]中断 |
0x0000_00E0 |
41 |
48 |
可设置 |
RTCAlarm |
连接到EXTI的RTC闹钟中断 |
0x0000_00E4 |
42 |
49 |
可设置 |
USB唤醒 |
连接到EXTI的从USB待机唤醒中断 |
0x0000_00E8 |
43 |
50 |
可设置 |
TIM8_BRK |
TIM8刹车中断 |
0x0000_00EC |
位置 |
优先级 |
优先级类型 |
名称 |
说明 |
地址 |
44 |
51 |
可设置 |
TIM8_UP |
TIM8更新中断 |
0x0000_00F0 |
45 |
52 |
可设置 |
TIM8_TRG_COM |
TIM8触发和通信中断 |
0x0000_00F4 |
46 |
53 |
可设置 |
TIM8_CC |
TIM8捕获比较中断 |
0x0000_00F8 |
47 |
54 |
可设置 |
ADC3 |
ADC3全局中断 |
0x0000_00FC |
48 |
55 |
可设置 |
FSMC |
FSMC全局中断 |
0x0000_0100 |
49 |
56 |
可设置 |
SDIO |
SDIO全局中断 |
0x0000_0104 |
50 |
57 |
可设置 |
TIM5 |
TIM5全局中断 |
0x0000_0108 |
51 |
58 |
可设置 |
SPI3 |
SPI3全局中断 |
0x0000_010C |
52 |
59 |
可设置 |
UART4 |
UART4全局中断 |
0x0000_0110 |
53 |
60 |
可设置 |
UART5 |
UART5全局中断 |
0x0000_0114 |
54 |
61 |
可设置 |
TIM6 |
TIM6全局中断 |
0x0000_0118 |
55 |
62 |
可设置 |
TIM7 |
TIM7全局中断 |
0x0000_011C |
56 |
63 |
可设置 |
DMA2通道1 |
DMA2通道1全局中断 |
0x0000_0120 |
57 |
64 |
可设置 |
DMA2通道2 |
DMA2通道2全局中断 |
0x0000_0124 |
58 |
65 |
可设置 |
DMA2通道3 |
DMA2通道3全局中断 |
0x0000_0128 |
59 |
66 |
可设置 |
DMA2通道4_5 |
DMA2通道4和DMA2通道5全局中断 |
0x0000_012C |
NVIC通过中断优先级控制字PRI_n既支持嵌套中断又支持非嵌套中断,方法如表2所示,将每个中断的优先级控制字PRI_n(4位)分为两截:前半截用于定义本中断的抢占优先级,后半截用于定义子优先级。而分割的具体办法由优先级组别寄存器定义:若优先级组别定义为4,则前半截的抢占优先级占据全部4位(共可定义24=16中抢占优先级),而后半截的子优先级占据0位(无法定义子优先级);若优先级组别定义为3,则前半截的抢占优先级占据全部3位(共可定义23=8种抢占优先级),而后半截的子优先级占据1位(共可定义21=2种子优先级);其他优先级组别定义以此类推,表2所示是NVIC的中断优先级配置。
表2 NVIC的中断优先级配置
优先级组别 |
抢占优先级 |
子优先级 |
4 |
4位/16级 |
0位/0级 |
3 |
3位/8级 |
1位/2级 |
2 |
2位/4级 |
2位/4级 |
1 |
1位/2级 |
3位/8级 |
0 |
0位/0级 |
4位/16级 |
值得注意的是,每个中断源都拥有自己的优先级控制字PRI_n(n为中断源编号),但优先级组别寄存器只有一个。即一旦对NVIC定义了优先级控制字的分割方式,则对所有中断源的所有PRI_n,分割方式都是相同的,并且意法半导体官方不建议在程序中频繁修改优先级组别寄存器的内容。
与本节关于通用中断嵌套规则的描述相同,STM32的NVIC的优先级嵌套规则如下:
(1)抢占优先级高的中断可以打断抢占优先级低的中断服务,构成中断嵌套。
(2)当两个或多个同级别抢占优先级的中断出现时,它们不能构成中断嵌套,但STM32先响应子优先级高的中断请求。
(3)当两个或者多个同级别抢占优先级和同级别子优先级的中断同时出现时,STM32先响应在中断向量表中靠前的那个中断。
通过实际生活的例子类比上述NVIC响应顺序原则:在火车站购票时,先比较抢占优先级,抢占优先级高(军人)的中断优先响应;当抢占优先级相同时,比较子优先级,子优先级高(军衔)的中断优先响应;当上述两者都相同时,比较它们在中断向量表中的位置(年龄),位置低(年龄大)的中断优先响应。
再通过一个中断配置实例,说明STM32的NVIC配置和响应顺序:假定设置优先级组为2,然后进行以下设置。
(1)中断3(RTC中断)的抢占优先级为2,子优先级为1。
(2)中断6(外部中断0)的抢占优先级为3,子优先级为0。
(3)中断7(外部中断1)的抢占优先级为2,子优先级为0。
则中断优先级顺序为:中断7 >中断3 >中断6。其中,中断3和中断7的抢占优先级相同,所以中断3不能被中断7打断,但中断6可以被中断3或中断7打断。
三、NVIC的配置和使用
STM32的嵌套向量中断控制器需要和中断源配合使用,本书将在后续章节中采用标准外设库提供的库函数,并结合具体中断源外设来详细讲解。这里为了帮助读者理解NVIC的工作方式,仅给出基于标准外设库的NVIC通用配置流程。
(1)配置中断优先级分组,例如:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//将中断优先级组别配置为2
上面代码调用的函数“void NVIC_PriorityGroupConfig(uint32_t NVIC_Priority Group);”是意法半导体官方提供的标准外设库函数,其功能是对中断优先级组别进行配置。而其参数NVIC_PriorityGroup_2是由标准外设库事先定义的宏,代表将组别设为2,也就是2位抢占优先级,2位子优先级。
(2)针对具体需要的中断源,设置对应的抢占优先级和子优先级,初始化NVIC,例如:
NVIC_Init(&NVIC_InitStructure); //用结构体NVIC_InitStructure中定义的参数初始化NVIC //寄存器
函数“void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);”也是标准外设库中提供的函数。&NVIC_InitStructure是指向初始化结构体NVIC_InitStructure的指针。结构体NVIC_InitStructure的定义如下,其成员包含了NVIC的主要参数。
typedef struct
{
uint8_t NVIC_IRQChannel; //设置中断源是哪一个
uint8_t NVIC_IRQChannelPreemptionPriority; //抢占优先级
uint8_t NVIC_IRQChannelSubPriority; //子优先级
FunctionalState NVIC_IRQChannelCmd; //使能/禁能本中断源
} NVIC_InitTypeDef;
(3)编写对应的中断服务程序。官方提供的标准外设库已经在文件stm32f10 x_it.c中为STM32所包含的每一个外设编写了中断服务程序的框架。其命名规则为“void PPP_IRQHandler(void);”,其中PPP代表了具体中断源的缩写,如:
void WWDG_IRQHandler(void); //窗口看门狗中断服务
void RTC_IRQHandler (void); //实时时钟中断服务
void EXTI0_IRQHandler (void); //外部中断0中断服务
void EXTI1_IRQHandler (void); //外部中断1中断服务
void USART1_IRQHandler(void); //串口1中断服务
void SPI1_IRQHandle(void); //SPI1中断服务