stm32之中断模式
一、中断简介
当CPU收到中断或者异常时,它会暂停执行当前的程序或任务,通过一定的机制跳转到负责处理这个信号的相关处理程序中,在完成对这个信号的处理后再跳转回刚才被打断的程序或任务中继续执行,其具体的处理过程如下。
二、外部中断配置
(一)外部中断简介
编号 1 是输入线, EXTI 控制器有 19 个中断/事件输入线,这些输入线可以通过寄存器设置为任意一个 GPIO,也可以是一些外设的事件,这部分内容我们将在后面专门讲解。输入线一般是存在电平变化的信号。
编号 2 是一个边沿检测电路,它会根据上升沿触发选择寄存器 (EXTI_RTSR) 和下降沿触发选择寄存器 (EXTI_FTSR)对应位的设置来控制信号触发。边沿检测电路以输入线作为信号输入端,如果检测到有边沿跳变就输出有效信号 1 给编号 3 电路,否则输出无效信号0。而 EXTI_RTSR 和EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。
编号 3 电路实际就是一个或门电路,它一个输入来自编号 2 电路,另外一个输入来自软件中断事件寄存器 (EXTI_SWIER)。
EXTI_SWIER 允许我们通过程序控制就可以启动中断/事件线,这在某些地方非常有用。我们知道或门的作用就是有 1 就为 1,所以这两个输入随便一个有有效信号 1就可以输出 1 给编号 4 电路。
编号 4 电是一个与门电路,它一个输入是编号 3 电路,另外一个输入来自中断屏蔽寄存器(EXTI_IMR)。与门电路要求输入都为 1 才输出1,导致的结果是如果 EXTI_IMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 4 电路输出的信号都为
0;如果 EXTI_IMR设置为 1 时,最终编号 4 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。编号 4 电路输出的信号会被保存到挂起寄存器(EXTI_PR) 内,如果确定编号 4 电路输出为 1 就会把 EXTI_PR 对应位置 1。
编号 5 是将 EXTI_PR 寄存器内容输出到 NVIC 内,从而实现系统中断事件控制。
EXTI(External interrupt/event controller)—外部中断/事件控制器,管理了控制器的 20 个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。
中断优先级分组
中断允许嵌套,不同的中断有不同的优先级,高优先级的中断可以打断低优先级的中断。
中断分组分为抢占优先级和子优先级,在中断发生时,同抢断优先级的互不打断。同抢占优先级且同时发生的中断,才看子优先级。
(二)配置外部中断
我们本例是使用外部中断控制LED灯的变化,首先我们要开启外部时钟,初始化所要用到的LED端口,我在上一篇博客已经介绍过了,在这里就不赘述了。
具体可参考
配置好所要用到的LED灯后,我们就开始开启外部中断了,因为STMF103c8t6最小系统板并没有按键,所以我们这里用杜邦线代替。使用杜邦线从高电平到低电平模拟开关按下,低电平到高电平模拟开关弹起。
1.配置外部中断
因为这里有三个LED灯,所以我初始化三个GPIO端口模拟按键。
- 选择PA3,选择GPIO_EXTI3模式
继续选择PA6,PB13为GPIO_EXTI(这里按个人喜好选择端口)
- 在Pinout&Configuration–System core–GPIO中选择PA3,配置为如下模式
然后经PA6,PB13也配置为下降沿触发,上拉输入
2.配置中断优先级
- 在Pinout&Configuration–System core–NVIC中使能外部中断,并配置优先级
3.生成并查看代码
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LED_Y_GPIO_Port, LED_Y_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LED_G_GPIO_Port, LED_G_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LED_R_GPIO_Port, LED_R_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = LED_Y_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(LED_Y_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pins : PAPin PAPin */
GPIO_InitStruct.Pin = KEY1_Pin|KEY2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = LED_G_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(LED_G_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = KEY3_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEY3_GPIO_Port, &GPIO_InitStruct);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = LED_R_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(LED_R_GPIO_Port, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI3_IRQn);
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
HAL_NVIC_SetPriority(EXTI15_10_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
(三)编写中断函数
我们在stm32f1xx_it.c
文件中,可以找到配置好的中断处理函数
当中断来临时,进入相应的中断处理函数
可以看出,这几个外部中断都使用了同一个处理函数,HAL_GPIO_EXTI_IRQHandler,通过传入不同的参数,来区分是哪一条中断线触发的中断。不同的外部中断都调用了同一个HAL库的处理函数:HAL_GPIO_EXTI_IRQHandler。
我们打开HAL_GPIO_EXTI_IRQHandler
函数看看
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_IRQHandler
函数里面又调用了回调函数HAL_GPIO_EXTI_Callback(GPIO_Pin)
我们再打开HAL_GPIO_EXTI_Callback(GPIO_Pin)
看看
/**
* @brief EXTI line detection callbacks.
* @param GPIO_Pin: Specifies the pins connected EXTI line
* @retval None
*/
__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
*/
}
关于这几个函数的逻辑关系如下
而在HAL_GPIO_EXTI_IRQHandler的处理函数中,又调用了一个名为HAL_GPIO_EXTI_Callback的回调函数。此回调函数是用户编写业务逻辑的函数。
在计算机程序设计中,回调函数,或简称回调(Callback 即call then back
被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。
我们注意到,在用户重定义的处理函数名称之前,有__Weak标记,这是为什么?在HAL库中采用了一种弱函数机制,在HAL_GPIO_EXTI_Callback函数前有个__weak标记,表明这是个弱函数。弱函数可以被用户定义的同名函数覆盖,也就是说,如果用户定义了一个函数名为HAL_GPIO_EXTI_Callback,系统就不再编译有weak标记的函数,所以,被weak标记的,都是备胎。
__weak在回调函数的时候经常用到。这样的好处是,系统默认定义了一个空的回调函数,保证编译器不会报错。同时,如果用户自己要定义用户回调函数,那么只需要重新定义即可,不需要考虑函数重复定义的问题
图片及参考来源(更多内容请访问)
关于这部分不理解也没有关系,我们只需要知道我们要将中断处理函数放到一个名为void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
(用户重新定义一个)的函数中即可,这是我们唯一需要自己编写的服务函数
为了方便,我就在main文件中加入我写的中断服务函数
代码如下:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case GPIO_PIN_3:
{
HAL_GPIO_TogglePin(LED_R_GPIO_Port,LED_R_Pin);
}
break;
case GPIO_PIN_6:
{
HAL_GPIO_TogglePin(LED_G_GPIO_Port,LED_G_Pin);
}
break;
case GPIO_PIN_13:
{
HAL_GPIO_TogglePin(LED_Y_GPIO_Port,LED_Y_Pin);
}
break;
default: break;
}
}
此代码实现的是每一次按键被按下,LED电平翻转一次。
(四) 实例演示
本例由于使用c8t6系统板上没有多余的按键,所以我们使用杜邦线代替。我这里使用杜邦线将PA6引出,PA6每从高电平到低电平,LED灯翻转一下。
由于系统板可能有点接触不良,所以在实验过程中可能会出现灯莫名其妙的闪烁,这是正常情况。
由于其他“按键”情况都是一样的,所以我这里只展示了PA6,经测试,其他端口可以分别控制相应的LED灯。
三、使用串口中断发送“Hello windows!”
上例我们是在while循环里连续发送Hello windows!,但是这样程序执行效率低,本例我们使用串口的方式来发送数据,并且在while循环里做LED闪烁实验。
- 串口中断请求
(一)配置工程
关于时钟和相关GPIO的配置我这里就跳过了,这里主要是配置USART1的中断。
- 在Pinout&Configuration–Connectivity–Usart1下,选择模式为异步模式
- 在该界面下选择NVIC Settings,勾选中断
在HAL库中,串口的接收中断、发送中断都需要开启这个中断。
然后经过基本的工程配置就可以生成代码了
具体可参考STM32CubeMX环境搭建及使用
(二)编写用户函数
我们使用一个数组来保存我们需要发送的数据
uint8_t temp[]="Hello windowns\n";
然后在main函数中调用HAL_UART_Transmit_IT(&huart1, temp, sizeof(temp))
将数组中的内容发送出去,发送完成后,自动调用发送完成回调函数HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
我们在回调函数中发送再次发送相同的数据,在合适的条件下结束发送。
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(i<=100)
{
HAL_UART_Transmit_IT(&huart1, temp, sizeof(temp));
i++; //每调用一次,i++,当调用超过100次时,不再发送数据
}
}
使能发送中断函数HAL_UART_Transmit_IT(&huart1, temp, sizeof(temp));
(三)实验效果
可以看到,上位机接收了1632个字节就不再接收了,同时LED也在闪烁。
此时LED灯闪烁
四、使用串口接收数据并回显
使用串口中断连续发送数据,程序一直频繁进入中断,效率不高,接下来我们使用串口中断接收来自上位机发送的数据,并将接收到的数据发送到上位机显示出来。
我们可以直接利用上面生成的工程,只需要修改一下代码即可。
- 定义接收缓存区
uint8_t receive[255];
- 重新定义回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
HAL_UART_Transmit(&huart1,receive,1,0xFFFF);
HAL_UART_Receive_IT(&huart1,receive,1);
}
}
- 在主函数中开启接收中断
HAL_UART_Receive_IT(&huart1,receive,1); //每接收一个字节就进入中断
- 效果如下
此时LED灯仍在闪烁,如上例一样,这里就不演示了。
总结
如果使用cpu查询模式,程序会不断的执行查询,效率低;如果开启中断后,cpu就不用时时刻刻重复执行操作,而是可以腾出手去做其他的事;当中断来临(满足条件后),程序从当前执行的指令跳转到中断处,执行完毕后再继续做跳转前的工作,这极大的提升了效率。
关于在HAL库中大量使用回调函数(以函数名字作参数),一般是以-weak弱定义的,我们可根据实际需要重定义。使用回调函数可以让代码更加灵活、提高运行效率。
以上若有不当之处,敬请指教!!!
参考
《STM32库开发指南–基于野火指南者开发板》