【STM32F103笔记】4、中断之外部中断——喂~烧水啦
这一篇来说一下单片机或者说所有处理器提高运行效率的方法——中断处理,为什么这么说呢,记得我以前看到过一个十分形象的例子,这里我“修饰”一下和大家分享:
小明看着电影突然想喝水,但是水壶里没水了,要烧开一壶水(他不喝冷水的),于是把壶装满水放在炉子上(当然也可以用电热水壶嘛),然后突然精神分裂了:
- 一个小明每隔10秒就去揭开壶盖看看水有没有开,终于他在第100次揭盖的时候发现,水终于开了,并且也错过了电影的精彩片段,而且他还发现在这段时间他啥也没干;
- 另一个小明把壶放炉子上就继续看电影去了,然后发现电影不好看,又换了好几部电影,终于找到了想看的;正看着电影,炉子那边传来哨声(现在的话就是热水壶咔哒一下断电的声音),——于是小明暂停电影,赶快去关炉子。
这里的小明相当于处理器,需要不停地处理很多操作,而烧水可以类比一个事件,当事件来临时需要处理器进行处理,但又不需要处理器一直关注这个事件,因为不知道这个事件什么时候会来;
而第一个小明类比于处理器使用不断查询的方法,一旦发现事件来临就采取对应的处理方法,这样处理器大半时间都用在查询事件是否来临上,极大地降低了处理器的效率(干了很多无用功——揭盖子);
但第二个小明相当于处理器使用了中断进行处理,也就是处理器先设置好相关的中断(把炉子放水上——啊呸,把水壶放炉子上),然后就可以不再操心这个事件,放开了手去处理别的事件;等到事件来领,触发中断,这样就告诉处理器,事件来临需要处理了(水开了去关炉子);这样一来,在等到事件来临的间隙,就能处理很多事情,大大提高了处理器的效率。
有人要说了,烧水不是大概10分钟嘛,到时候去看一眼就好了;好嘛,今天小明家把原来烧煤球的炉子换成了天然气的炉子,火力猛得很,5分钟不到就烧开了,等到小明10分钟去看的时候,水壶都快烧干了。
也就是说,当处理器不知道事件具体会什么时候来,那么使用定时去查询的话,就可能错过事件,或者事件还没来,需要继续查询。
因此,如果是处理器外部事件的发生,比如引脚电平的变化,或者一个触发信号等等,墙裂推荐使用中断处理。
按键触发外部中断
STM32的每个GPIO引脚都可以配置为外部中断引脚,因此,本篇用笔者的黑色最小系统板来进行说明,因为它自带一个按键,通过这个按键触发引脚的外部中断,并改变LED的亮灭状态。
电路
和上一篇中按键的电路一模一样,PA0为按键输入,按下接地为低电平;同样利用PC13控制的LED来展示按键触发外部中断的效果。
中断配置
STM32F103系列使用的Cortex-M3内核,有一个强大的异常(Exception)处理系统,在Cortex-M3的编程手册中(ST官网可以下载,搜索文档编号PM0056,或者名称STM32F10xxx/20xxx/21xxx/L1xxxx Cortex®-M3 programming manual)可以了解其异常处理系统,这里的异常包括复位(Reset)、不可屏蔽中断(NMI: NonMaskable Interrupt)、硬件错误(Hard fault)、…,以及用户可以定义使用的中断(Interrupt (IRQ))。
中断向量
从Cortex-M3内核的异常向量(Exception Vector)表中可以看出:
从0x0000地址开始,首先是Initial SP,初始化栈指针的值,然后0x0004地址为Reset,也就是第二篇说明启动方式时对应启动文件中的标号Reset,有兴趣的话可以对照启动文件中的标号去看看,了解下其他异常向量是怎么处理的;
从第16个异常向量IRQ0开始,是通常用户可以定义使用的中断请求向量,也就是说,当某个中断被触发后,程序运行指针会被导向相应的中断向量地址,比如IRQ0触发,PC指针就将指向0x0040这个地址,而这个地址一般会用来存储IRQ0的中断服务程序,这样,PC指针就能进入IRQ0的中服程序,处理中断;
什么,中断服务程序为什么不就放在这个地址?
留给每个中断请求向量的地址空间只有4个字节,写不下一个中服程序的哈哈哈。
中断优先级
那么,既然有这么多中断请求向量,而每个中断向量又将对应各种中断情况,那肯定需要有一个先后顺序了;
Cortex-M3内核的中断优先级又分为两种:
- preemption priority:抢占优先级,设置值越小级别越高,即当两个不同的中断同时发生时,抢占优先级高的中断优先进行处理;并且,当有中断T1正在处理时,若发生了中断T2,且T2的抢占优先级高于T1的,那么T1的中断处理将被打断,转而执行T2的中断处理程序,等T2的中断处理程序执行完后,再继续执行T1的中断处理程序;
- subpriority:响应优先级,设置值越小级别越高,当两个抢占优先级相同的中断同时发生时,响应优先级高的中断先进行处理。
Cortex-M3内核通过一个叫做内嵌中断控制器(NVIC: Nested Vectored Interrupt Controller)的东西来控制中断进行控制,在库中提供了响应的操作函数与数据类型,包括NVIC初始化、优先级设置等:
上述的异常向量、中断优先级、NVIC等都是Cortex-M3内核的东西,不要与STM32混淆,STM32是因为使用了Cortex-M3内核才具有这些。
外部中断External Interrupt(EXTI:不是exit!)
STM32的每个GPIO都可以配置为外部中断,不同引脚通过不同的中断路径进入中断处理器:
可以看到这里使用的PA0,位于EXTI0路径上;需要注意的是,只有EXTI0-EXTI4是单独的中断路径,而EXTI9_5和EXTI15_10都是共用一个路径。同样,在库函数中也提供了外部中断EXTI的相关操作函数。
程序
同样,和用于控制LED的引脚一样,外部中断EXTI0和NVIC控制器也需要进行初始化。
NVIC初始化
库函数中,XX初始化都伴随着XX_InitTypeDef,即提供一个初始化结构体用于设置,然后调用XX_Init()函数将初始化结构体中的设置写入寄存器,完成功能的设置。
NVIC初始化程序:
/**
* @brief Configure EXTI0 and set priority
* @param None
* @retval None
*/
void NVICConfig(void)
{
NVIC_InitTypeDef NVICInitStruct;
// 设置优先级配置方法
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
// 中断通道选择EXTI0,EXTI0_IRQn定义在stm32f10x.h文件中
NVICInitStruct.NVIC_IRQChannel = EXTI0_IRQn;
// 定义抢占优先级为最高
NVICInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
// 定义响应优先级为最高
NVICInitStruct.NVIC_IRQChannelSubPriority = 0;
// 使能
NVICInitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVICInitStruct);
}
外部中断初始化
外部中断初始化包括GPIOA0引脚的初始化以及EXTI0的初始化:
/**
* @brief Configure EXTI0 and set priority
* @param None
* @retval None
*/
void PA0ExtConfig(void)
{
GPIO_InitTypeDef GPIOInitStruct;
EXTI_InitTypeDef EXTIInitStruct;
// 同时开启GPIOA和AFIO的外设时钟
// GPIO用作外部中断或者重映射时需开启AFIO时钟,复用功能时则不用
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
GPIOInitStruct.GPIO_Pin = GPIO_Pin_0;
// 不需要设置复用功能,直接设置为上拉输入即可,按键接低电平所以设置为上拉
// 如果需要使用GPIO的复用功能,则相应的设置GPIO_Mode_AF_xx
GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIOInitStruct);
// 设置外部中断源为GPIOA的Pin_0引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// PA0的外部中断为EXTI0
EXTIInitStruct.EXTI_Line = EXTI_Line0;
// 模式为外部中断
EXTIInitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
// 设置中断触发方式为下降沿触发
EXTIInitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
// 使能
EXTIInitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTIInitStruct);
}
中断服务程序(中断处理程序)
在USER文件夹下有stm32f10x_it.c文件,结尾的it即为interrupt的缩写,这个文件存放中断服务程序,打开文件可以看到里面已经有了一些默认的空函数,当需要用到这些中断功能时,就可以在相应的函数中写入对应的中断处理程序。
在文件结尾,有:
/**
* @brief This function handles PPP interrupt request.
* @param None
* @retval None
*/
/*void PPP_IRQHandler(void)
{
}*/
这个被注释的函数PPP_IRQHandler(void)是提供给用户编写外设中断函数用的,比如我们接下来要写的外部中断函数,其中PPP并不能随意填写,这里的名称是和startup_stm32f10x_hd.s文件中设置好的异常向量标号一致的,了解汇编和单片机地址的朋友应该明白为什么,这里可以简要说明一下:
- 单片机地址一般指的是其内部RAM的地址分配,就像上面的vector table图里一样,地址从0开始递增,在启动文件startup_stm32f10x_hd.s中,按照vector table中的顺序,将内部RAM前面一小部分的地址从0开始分别命名,这样标号__initial_sp就对应了RAM中的0x0000地址,Reset_Handler标号对应0x0004地址,依次类推;
- 在汇编程序中,可以认为标号代表的就是其所在的地址;
- 单片机或是处理器内核上电或者复位时,其程序运行指针PC会跳转指向一个固定的地址,一般为0x0000或者RAM开头的某个地址,然后从这个地址开始运行;
- 而当进入中断(或者异常)时,PC又会跳转到固定的地址,也就是vector table中对应中断的地址,比如STM32某个中断通过它的中断路径,最后触发NVIC控制器的Hard Fault中断,这是PC指针会跳转到Hard Fault对应的地址,也就是图中的0x000C开始运行;
- 而在启动文件startup_stm32f10x_hd.s中,将这个地址命名为了HardFault_Handler,这样,PC指针就会startup_stm32f10x_hd.s文件中标号为HardFault_Handler的后续程序:
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
- 从而跳转到stm32f10x_it.c文件中的HardFault_Handler()函数,进入死循环(进入死循环是因为函数中默认的处理就是while(1)):
/**
* @brief This function handles Hard Fault exception.
* @param None
* @retval None
*/
void HardFault_Handler(void)
{
/* Go to infinite loop when Hard Fault exception occurs */
while (1)
{
}
}
因此,在startup_stm32f10x_hd.s文件中找到EXTI0对应的标号EXTI0_IRQHandler,在stm32f10x_it.c文件中写入中断处理函数,函数名即为标号,和上一篇一样,将LED取反就可以:
/**
* @brief This function handles EXTI0(PA0) interrupt request.
* @param None
* @retval None
*/
void EXTI0_IRQHandler(void)
{
BitAction status;
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
status = (BitAction)(1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
GPIO_WriteBit(GPIOC, GPIO_Pin_13, status);
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
完整程序
main.c:
/* Includes ------------------------------------------------------------------*/
#include "stm32f10x.h"
/* Private functions ---------------------------------------------------------*/
void NVICConfig(void);
void PA0ExtConfig(void);
void PC13LEDConfig(void);
/**
* @brief Main body program
* @param None
* @retval None
*/
int main(void)
{
PC13LEDConfig();
PA0ExtConfig();
NVICConfig();
while(1);
}
/**
* @brief Configure EXTI0 and set priority
* @param None
* @retval None
*/
void NVICConfig(void)
{
NVIC_InitTypeDef NVICInitStruct;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
NVICInitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVICInitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVICInitStruct.NVIC_IRQChannelSubPriority = 0;
NVICInitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVICInitStruct);
}
/**
* @brief Configure EXTI0 and set priority
* @param None
* @retval None
*/
void PA0ExtConfig(void)
{
GPIO_InitTypeDef GPIOInitStruct;
EXTI_InitTypeDef EXTIInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
GPIOInitStruct.GPIO_Pin = GPIO_Pin_0;
// no need AF mode and dont have AF mode
GPIOInitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIOInitStruct);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
EXTIInitStruct.EXTI_Line = EXTI_Line0;
EXTIInitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTIInitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTIInitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTIInitStruct);
}
void PC13LEDConfig(void)
{
GPIO_InitTypeDef GPIOInitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIOInitStruct.GPIO_Pin = GPIO_Pin_13;
GPIOInitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIOInitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIOInitStruct);
}
stm32f10x_it.c中的中断服务程序,添加在PPP_IRQHandler后面就行:
/**
* @brief This function handles PPP interrupt request.
* @param None
* @retval None
*/
/*void PPP_IRQHandler(void)
{
}*/
/**
* @brief This function handles EXTI0(PA0) interrupt request.
* @param None
* @retval None
*/
void EXTI0_IRQHandler(void)
{
BitAction status;
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
status = (BitAction)(1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
GPIO_WriteBit(GPIOC, GPIO_Pin_13, status);
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
运行结果
和上一篇中的按键一模一样,只不过处理器通过中断来处理按键,其它时间什么也不用做,而上一篇中需要不停的循环查询按键对应的引脚: