16 KiB
[TOC]
1. 中断概述
常见的数据传输方式:p176
- 无条件方式:不关心外设的状态,直接发送或者读取。例如按键读取或则LED的控制。还有上个例子中串口的发送(问题:上个例子串口的程序有什么特点和缺陷)。
- 查询方式:先查询外设的状态(一般是通过寄存器读);条件允许的情况下再进行读写。
- 中断方式:不查询外设的状态,当外设有状态转变的时候产生一个中断,调用一小段代码(ISR中断服务),用来通知主程序;主程序根据通知的内容进行读写操作。
- 直接存储器访问方式(DMA):与中断类似,更适合大批量数据的读写;外设与内存之间建立一个联系,当一批数据收到或则是发送完成后产生中断,通知主程序进行处理。
1.1. 中断相关的概念(p177)
1.1.1. 中断的定义
1.1.2. 中断的作用
最初是解决处理器速度高,外设速度低的问题,提高处理器的利用率。
中断方式还有以下优点:
分时操作想到了什么?
1.1.3. 中断优先级和中断嵌套(p178)
中断是有优先级的,当某个中断的ISR还没有执行完成,此时新中断产生;如果新中断的优先级高于正在执行的中断,就会先执行优先级高的中断ISR;否则当前中断的ISR不受影响,当前中断ISR执行完成后,再执行新的中断ISR。可以想象成多线程,主程序的优先级最低;ISR是按照优先级进行执行的,高优先级的ISR会打断低优先级的ISR执行。
1.1.4. 中断服务程序和中断向量表
我们将中断发生时所执行的特定程序称为中断服务程序,英文缩写为ISR。中断服务程序一般由用户编写,主要内容是该中断发生时需要执行的具体任务。以定时中断实现指示灯的闪烁为例,在发生定时中断时,需要切换指示灯的状态。那么定时中断所对应的中断服务程序的主要内容就是切换指示灯的状态。
在计算机系统中,当某一个中断源提出中断申请后,处理器要如何准确地找到这个中断源所对应的中断服务程序呢?首先,为了区分各个中断源,计算机系统为每一个中断源分配了一个编号,这个编号称为中断类型号。接着,对于系统需要响应的每一个中断源,都预先编写好对应的中断服务程序。最后,按照中断类型号,从小到大将所有中断服务程序的入口地址(函数名)依次排列,组合为一张表格的形式,这个表格就称为中断向量表(中断对应的ISR,是有优先级的),而中断服务程序(ISR)的入口地址称为中断向量。
中断向量表通常位于存储器的零地址处。当某一个中断源发出中断申请时,处理器根据识别到的中断类型号查找中断向量表,,到对应的表项。表项的内容就是该中断所对应的中断服务程序的入口地址,然后跳转到该地址执行具体的中断处理任务。从理解的角度上看,我们可以把中断向量表看作一个数组:每一个数组元素的内容就是中断服务程序的入口地址。数组元素的下标相当于每一个中断的中断类型号。
1.1.5. 中断相应过程
了解了中断的相关概念后,我们可以总结出中断响应的完整步骤:
- 中断源发出中断请求。
- 判断处理器是否允许中断,以及该中断源是否被屏蔽。
- 多个中断同时申请时,需要进行优先级排队。
- 处理器暂停当前程序,保护断点地址和处理器的当前状态,根据中断类型号,查找中断向量表,转到对应的中断服务程序。 执行具体的中断处理任务。
- 恢复被保护的处理器状态,执行中断返回指令,回到被暂停程序的断点地址处。
1.2. STM32中断系统
中断可以由硬件或者软件触发。在ARM处理器中,把能够打断当前代码执行流程的事件分为异常(exception)和中断(interrupt)两类。二者的区别在于:中断是由内核外部产生的,一般由硬件触发,比如定时器中断和外部中断等。而异常通常是内核自身产生的,大多由软件触发,比如除法出错异常,预取指失败等。
1.2.1. 嵌套向量中断控制器
在基于Cortex-M内核设计的ARM处理器中,提供了一个专用的硬件模块:嵌套向量中断控制器( nested vectored interrupt controller,NVIC),用来管理全部的异常和中断。通过对 NVIC的编程,可以实现中断的使能、中断优先级的设置以及中断触发方式的选择等功能。
从表 7-1 可以看到,除了复位中断、不可屏蔽中断和类型错误中断的中断优先级是固定的外,其余中断源的中断优先级均可设置。对于每一个中断源而言,按照它在中断向量表中的位置,给出了一个默认的中断优先级,编号越小的优先级越高。
注意:由于 STM32 微控制器片内集成的外设较多,而中断通道的数量有限,因此会出现多个外设共享同一个中断通道的情况。对于单个外设而言,它通常具备若于个可以引起中断的中断源。比如定时器就具备更新中断、捕获中断和匹配中断等多个中断源。外设的所有中断源只能通过该外设所指定的中断通道向内核发出中断申请,在中断服务程序内部再判断是该外设的哪一种中断。
1.2.2. STM32中断优先级设置
中断源通过中断通道向内核发出中断申请,设置中断源的优先级实际上是设置中断通道的优先级。中断通道的优先级通过NVIC中的中断优先级寄存器NVIC_IP进行设置,该寄存器是8位,理论上可以配置256个中断优先级。STM32微控制器只使用了其中的高4位,并分成了两个优先级:抢占优先级(preempition priority)和子优先级(subpriority )。
具体的分组情况如下:
-
第0组:所有4位用于指定子优先级。
-
第1组:最高1位用于指定抢占优先级,后面3位用于指定子优先级。
-
第2组:最高2位用于指定抢占优先级,后面2位用于指定子优先级。
-
第3组:最高3位用于指定抢占优先级,后面1位用于指定子优先级。
-
第4组:所有4位用于指定抢占优先级。
不同分组情况下,抢占优先级和子优先级的等级划分如表7-2所示。
在判断一个中断的优先级时,需要综合考虑这两个优先级。当多个中断同时提出中断申请时,抢占优先级高的中断会优先得到执行。如果抢占优先级相同,则比较子优先级:子优先级高的中断会优先得到执行。如果抢占优先级和子优先级都相同的话,就根据各中断在中断向量表中的位置来确定,中断向量表中靠前的中断优先得到执行。在HAL库的初始化过程中,HAL库初始化函数HAL_Init()将优先级分组设置为第4组,即只有0~15共16级抢占式优先级,没有子优先级。编号越小的优先级越高:0号为最高,15号为最低。
1.2.3. 外部中断控制器
什么是上升沿中断,什么是下降沿中断?这个就叫做边缘触发。
1.2.4. 外部中断
GPIO很多,中断资源有限这么办?复用,多个物理的GPIO公用一个中断向量。
STM32中文参考手册的131页的中断向量也可以看到。
2. HAL库的中断处理(p184)
2.1. 使用按键来转换LED的状态
使用Key1控制绿色LED(PA7),按一下开,再按一下关。
2.1.1. Stm32CubeMX配置
PA7 设置GPIO_Output,并设置label为LED;PB12设置GPIO_EXIT12,并设置label为BTN。
在 GPIO 设置中,配置PB12:
- External Interrupt Mode with Falling edge trigger detection:外部中断,下降沿触发;
- Pull up 可以不设置;
在NVIC中,使能 EXTI 10-15 的外部中断:
关闭并生成代码。
在main.c的大约55行加入代码:
/* USER CODE BEGIN 0 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
/* USER CODE END 0 */
下载运行后,可以通过 Key1 控制绿色LED的亮灭。
为什么不用设置芯片内的上拉?
为什么使用上拉而不是下拉?
2.2. HAL库的中断封装
- 设置中断触发条件。如外部中断的边沿触发,定时器更新中断的时间间隔。
- 设置中断优先等级。系统中存在多个中断时,需要根据任务的紧迫程度,设置不同的中断优先级。
- 使能外设的某个中断。微控制器片内集成的外设一般都有多个中断源,这些中断源的使能由外设的寄存器控制。例如,串口通信的发送数据寄存器空中断TXE和发送完成中断 TC 的使能,就是由串口控制寄存器 CR1中的 TXEIE 位和 TCIE 位控制(这个放到串口中讲解)。
- 判断中断源。对于有多个中断源的外设,进入中断服务程序后,需要根据中断标志来判断具体发生了哪一种中断。
- 清除中断标志。发生中断后,相应的中断标志位置位,以便 MCU 查询。这些中断标志要注意及时清除,避免重复进入中断。
- 编写程序。用户根据实际项目的需求来编写中断发生时应该执行的操作。
在上述的六个中断编程步骤中,借助CubeMX软件可以通过图形化的设置方式完成前三个步骤,而后三个步骤可以借助 HAL,库提供的接口函数进一步简化。
CubeMX关于中断的文件有两个(P188)startup_stm32fXXXX.s和stm32f1xx_it.c 。
startup_stm32fXXXX.s 在我们的实验当中是 startup_stm32f103c8tx.s 在下图的位置:
该文件是汇编文件,建了中断向量表,并预先为每一个中断编写了中断服务程序。这些中断服务程序的内部都是死循环,不执行任何具体操作,其目的只是为了初始化中断向量表。中断服务程序的属性设置为“weak”,用户可以在用户文件中重新定义一个同名函数作为实际执行的中断服务程序。
.weak EXTI0_IRQHandler
.thumb_set EXTI0_IRQHandler,Default_Handler
.weak EXTI1_IRQHandler
.thumb_set EXTI1_IRQHandler,Default_Handler
.weak EXTI2_IRQHandler
.thumb_set EXTI2_IRQHandler,Default_Handler
.weak USART1_IRQHandler
.thumb_set USART1_IRQHandler,Default_Handler
.weak USART2_IRQHandler
.thumb_set USART2_IRQHandler,Default_Handler
注意,上面 weak 修饰的就是缺省的中断服务(一个函数),所有的中断服务都指向了一个缺省的实现:Default_Handler,其定义在该文件大约111行左右:
.section .text.Default_Handler,"ax",%progbits
Default_Handler:
Infinite_Loop:
b Infinite_Loop
.size Default_Handler, .-Default_Handler
这个函数的实现是一个死循环(汇编我忘了),不执行任何操作,目的是初始化中断向量(让中断向量指向一个函数)。
我们看看按钮如何工作的。
在 startup_stm32f103c8tx.s 的大约355行定义了一个函数:
.weak EXTI15_10_IRQHandler
.thumb_set EXTI15_10_IRQHandler,Default_Handler
这个函数是使用 weak 进行修饰的,接下来在 stm32f1xx_it.c 的 204 行:
/**
* @brief This function handles EXTI line[15:10] interrupts.
*/
void EXTI15_10_IRQHandler(void)
{
/* USER CODE BEGIN EXTI15_10_IRQn 0 */
/* USER CODE END EXTI15_10_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(BTN_Pin);
/* USER CODE BEGIN EXTI15_10_IRQn 1 */
/* USER CODE END EXTI15_10_IRQn 1 */
}
注意:两个文件中都有 EXTI15_10_IRQHandler 这个函数,其实这在C语言中是不允许的(C语言是全局标识符,没有命名空间);因为第一个文件中的 EXTI15_10_IRQHandler 函数被标记成 weak,二个函数并没有标记成 weak,编译器在编译的时候会忽略第一个函数(有点像Java继承中的函数覆盖)。
继续看 HAL_GPIO_EXTI_IRQHandler 这个函数:
/**
* @brief This function handles EXTI interrupt request.
* @param GPIO_Pin: Specifies the pins connected EXTI line
* @retval None
*/
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
/* EXTI line interrupt detected */
if (__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
会发现其调用了 HAL_GPIO_EXTI_Callback 函数,就是我们在主程序中写的让LED反转的函数。在该函数的下面,有一个函数:
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(GPIO_Pin);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
}
看到了吗?这个函数是自动生成的,也是 weak 修饰的。因此我们在main.c 中写的同名函数其实是覆盖了这个函数。
仔细分析 HAL_GPIO_EXTI_IRQHandler 函数中还做了什么事情?
2.3. 外部中断处理流程(p184)
- startup_stm32f103c8tx.s 中有缺省的中断处理函数ISR,且全部为 weak 修饰;
- 如果开启了某个中断的ISR,会在 stm32f1xx_it.c 中生成对应的 ISR 去覆盖 startup_stm32f103c8tx.s 的缺省ISR,例如 EXTI15_10_IRQHandler
- 一系列的函数调用后,最后会调用一个 weak 修饰的函数,如:HAL_GPIO_EXTI_Callback,这就是用户代码逻辑所在的位置,可以覆盖这个函数。
3. 外部中断的HAL库定义
3.1. 外部中断的数据类型
GPIO_InitTypeDef:
typedef struct
{
uint32_t Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of @ref GPIO_pins_define */
uint32_t Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIO_mode_define */
uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
This parameter can be a value of @ref GPIO_pull_define */
uint32_t Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIO_speed_define */
} GPIO_InitTypeDef;
在main.c 的大约160 行,是外部中断的初始化过程,书上没有说,可以看看:
/*Configure GPIO pin : BTN_Pin */
GPIO_InitStruct.Pin = BTN_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(BTN_GPIO_Port, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
通过注释可以看出来,第一部分是初始化一个结构体;第二部分是设置中断的优先级(第二个参数是抢占优先级;第三个参数是子优先级),以及使能外部中断。