1.中断的概念
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
按照事件发生的顺序,中断过程包括 :
①中断源发出中断请求;
②判断当前处理机是否允许中断和该中断源是否被屏蔽;
③优先权排队;
④处理机执行完当前指令或当前指令无法执行完,则立即停止当前程序,保护断点地址和处理机当前状态,转入相应的中断服务程序;
⑤执行中断服务程序;
⑥恢复被保护的状态,执行“中断返回”指令回到被中断的程序或转入其他程序。
上述过程中前四项操作是由硬件完成的,后两项是由软件完成的。
2.NVIC与EXTI
2.1 NVIC简介
NVIC 即嵌套向量中断控制器,全称 Nested vectored interrupt controller。它是内核的器件。
M3 内核都 是支持 256 个中断,其中包含了 16 个系统中断和 240 个外部中断,并且具有 256 级的可编程中断设置。然而芯片厂商一般不会把内核的这些资源全部用完,如 STM32F103RCT6 的系统中 断有 10 个,外部中断有 60 个。下图是中断向量表-系统中断部分。
2.1.1 NVIC寄存器
根据程序中对于NVIC结构体的定义可以分析出NVIC相关的寄存器
typedef struct
{
__IOM uint32_t ISER[8U]; /* 中断使能寄存器 */
uint32_t RESERVED0[24U];
__IOM uint32_t ICER[8U]; /* 中断清除使能寄存器 */
uint32_t RSERVED1[24U];
__IOM uint32_t ISPR[8U]; /* 中断使能挂起寄存器 */
uint32_t RESERVED2[24U];
__IOM uint32_t ICPR[8U]; /* 中断解挂寄存器 */
uint32_t RESERVED3[24U];
__IOM uint32_t IABR[8U]; /* 中断有效位寄存器 */
uint32_t RESERVED4[56U];
__IOM uint8_t IP[240U]; /* 中断优先级寄存器(8Bit 位宽) */
uint32_t RESERVED5[644U];
__OM uint32_t STIR; /* 软件触发中断寄存器 */
} NVIC_Type;
STM32F103 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用 STM32F103 的中断。下面重点介绍这几个寄存器:
ISER[8]:ISER 全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。上面说了 CM3 内核支持 256 个中断,这里用 8 个 32 位寄存器来控制,每个位控制一个中断。但是STM32F103 的可屏蔽中断最多只有 60 个,所以对我们来说,有用的就是两个(ISER[0]和 ISER[1]),总共可以表示 64 个中断。而 STM32F103 只用了其中的 60 个。ISER[0]的 bit0~31 分 别对应中断 0~31;ISER[1]的 bit0~27 对应中断 32~59,这样总共 60 个中断就可以分别对应上 了。你要使能某个中断,必须设置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映射等设置才算是一个完整的中断设置)。
ICER[8]:全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER一样。这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄存器都是写 1 有效的,写 0 是无效的。
ISPR[8]:全称是:Interrupt Set Pending Registers,是一个中断使能挂起控制寄存器组。每个位对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别的中断。写 0 是无效的。
ICPR[8]:全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断解挂。写 0 无效。
IABR[8]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。
IP[240]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄存器组相当重要!STM32F103 的中断分组与这个寄存器组密切相关。IP 寄存器组由 240 个8bit 的寄存器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 240 个可屏蔽中断。而 STM32F103 只用到了其中的 60 个。IP[59]~IP[0]分别对应中断 59~0。而每个可屏蔽中断占用的 8bit 并没有全部使用,而是只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。
2.1.2 中断优先级
STM32 中的中断优先级可以分为:抢占式优先级和响应优先级,响应优先级也称子优先级,每个中断源都需要被指定这两种优先级。抢占式优先级和响应优先级的区别:
抢占优先级:抢占优先级高的中断可以打断正在执行的抢占优先级低的中断。
响应优先级:抢占优先级相同,响应优先级高的中断不能打断响应优先级低的中断。
当两个或者多个中断的抢占优先级和响应优先级相同时,遵循自然优先级,看中断向量表中的中断排序,数值越小,优先级越高。
在 NVIC 中由寄存器 NVIC_IPR0-NVIC_IPR59 共 60 个寄存器控制中断优先级,每个寄存器的 8 位,所以就有了 240 个宽度为 8bit 的中断优先级控制寄存器,原则上每个外部中断可配置的优先级为 0~255,数值越小,优先级越高。但是实际上 M3 芯片为了精简设计,只使用了高四位[7:4],低四位取零,这样以至于最多只有 16 级中断嵌套,即 2^4=16。
对于 NVCI 的中断优先级分组:STM32F103 将中断分为 5 个组,组 0~4。该分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。
根据此表可以清楚判断组对应的配置关系,如优先级分组为1,那么此时所有的60个中断,每个中断的中断优先寄存器高四位的最高1位为抢占优先级,低三位为响应优先级。可以设置抢占优先级为0或1,响应优先级为0-7。数值越小优先级越高。
结合实例说明一下:假定设置中断优先级分组为 2,然后设置中断 3(RTC_WKUP 中断)的抢占优先级为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。中断 7(外部中断 1)的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为:
中断 7>中断 3>中断 6。
上面例子中的中断 3 和中断 7 都可以打断中断 6 的中断。而中断 7 和中断 3 却不可以相互打断!
2.1.3 HAL库中NVIC相关函数
1. HAL_NVIC_SetPriorityGrouping 函数
HAL_NVIC_SetPriorityGrouping 是设置中断优先级分组函数。其声明如下:
void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);
⚫ 函数描述:
用于设置中断优先级分组。
⚫ 函数形参:
形参 1 是中断优先级分组号,可以选择范围:NVIC_PRIORITYGROUP_0 到
NVIC_PRIORITYGROUP_4(共 5 组)。
⚫ 函数返回值:
无
⚫ 注意事项:
这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调
用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面
设置好的抢占优先级和响应优先级不匹配。
2. HAL_NVIC_SetPriority 函数
HAL_NVIC_SetPriority 是设置中断优先级函数。其声明如下:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
⚫ 函数描述:
用于设置中断的抢占优先级和响应优先级(子优先级)。
⚫ 函数形参:
形参 1 是中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f103xe.h。
形参 2 是抢占优先级,可以选择范围:0 到 15。
形参 3 是响应优先级,可以选择范围:0 到 15。
⚫ 函数返回值:
无
3. HAL_NVIC_EnableIRQ 函数
HAL_NVIC_EnableIRQ 是中断使能函数。其声明如下:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
⚫ 函数描述:
用于使能中断。
⚫ 函数形参:
形 参 IRQn 是 中 断 号 , 可 以 选 择 范 围 : IRQn_Type 定 义 的 枚 举 类 型 , 定 义 在
stm32f103xe.h。
⚫ 函数返回值:
无
4. HAL_NVIC_DisableIRQ 函数
HAL_NVIC_DisableIRQ 是中断除能函数。其声明如下:
void HAL_NVIC_disableIRQ(IRQn_Type IRQn);
⚫ 函数描述:
用于中断除能。
⚫ 函数形参:
形参 IRQn 是中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f103xe.h。
⚫ 函数返回值:
无
5. HAL_NVIC_SystemReset 函数
HAL_NVIC_SystemReset 是系统复位函数。其声明如下:
void HAL_NVIC_SystemReset(void);
⚫ 函数描述:
用于软件复位系统。
⚫ 函数形参:
无形参
⚫ 函数返回值:
无
其他的 NVIC 函数用得较少,我们就不一一列出来了。NVIC 的介绍就到这,下面介绍外
部中断。
2.2 EXTI简介
EXTI 即是外部中断和事件控制器,它是由 20 个产生事件/中断请求的边沿检测器组成。每一条输入线都可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。EXTI 的功能框图是最直接把有关 EXTI 的知识点连接起来的图,掌握了该图的来龙去脉,就会对 EXTI 有了一个整体熟悉,编程时候可以得心应手。
由图可知,有两条主线,一是由输入线到NVIC中断控制器,一是由输入线到脉冲发生器,分别是产生中断与产生事件,也就是EXTI的两大部分功能。
标号①为一个边沿检测电路,包括边沿检测电路与上升下降沿触发选择器。以输入线作为信号输入端,如果检测到由边沿跳变就输出有效信号“1”到标号②部分电路,否则输入无效信号“0”。边沿跳变的标准取决于对于上升下降沿触发选择寄存器的对应位的设置。
标号②为一个或门电路,其输入端分别为软件中断事件寄存器和边沿检测电路的输入信号,也就是说只要软件中断事件寄存器与边沿检测电路的输入信号只需要由一方输出‘1’便会使得标号②电路向标号③和标号④ 电路输出‘1’。通过对软件中断事件寄存器的读写操作就可以启动中断/事件线,即相当于输出有效信号‘1’到或门电路输入端。
标号3为一个与门电路,输入端分别是中断屏蔽寄存器与请求挂起寄存器。即当中断屏蔽寄存器与请求挂起寄存器同时输出‘1’时才会输出‘1’。这样子的情况下,如果中断屏蔽 寄存器(EXTI_IMR)设置为 0 时,不管从标号②电路输出的信号特性如何,最终标号③电路输 出的信号都是 0;假如中断屏蔽寄存器(EXTI_IMR)设置为 1 时,最终标号③电路输出的信号才由标号②电路输出信号决定,这样子就可以简单控制 EXTI_IMR 来实现中断的目的。标号④ 电路输出‘1’就会把请求挂起寄存器(EXTI_PR)对应位置 1。
最后,请求挂起寄存器(EXTI_PR)的内容就输出到 NVIC 内,实现系统中断事件的控制。
接下来我们看看 EXTI 功能框图的产生事件的线路。 产生事件线路是从标号②之后与中断线路有所不用,之前的线路都是共用的。标号④是一 个与门,输入端来自标号②电路以及来自于事件屏蔽寄存器(EXTI_EMR)。如果 EXTI_EMR 寄存器设置为 0,那不管标号②电路输出的信号是‘0’还是‘1’,最终标号④输出的是‘0’; 如果 EXTI_EMR 寄存器设置为 1,最终标号④电路输出信号就由标号③电路输出的信号决定,这样子就可以简单的控制 EXTI_EMR 来实现是否产生事件的目的。
标号④电路输出有效信号 1 就会使脉冲发生器电路产生一个脉冲,而无效信号就不会使其产生脉冲信号。脉冲信号产生可以给其他外设电路使用,例如定时器,模拟数字转换器等,这样的脉冲信号一般用来触发 TIM 或者 ADC 开始转换。产生中断线路目的使把输入信号输入到 NVIC,进一步运行中断服务函数,实现功能。而产生事件线路目的是传输一个脉冲信号给其他外设使用,属于硬件级功能。
EXTI 支持 19 个外部中断/事件请求,这些都是信息输入端,也就是上面提及到了输入线,
具体如下:
EXTI 线 0~15:对应外部 IO 口的输入中断
EXTI 线 16:连接到 PVD 输出
EXTI 线 17:连接到 RTC 闹钟事件
EXTI 线 18:连接到 USB 唤醒事件
EXTI 线 19:连接到以太网唤醒事件
从上面可以看出,STM32F1 供给 IO 口使用的中断线只有 16 个,但是 STM32F1 的 IO 口却远远不止 16 个,所以 STM32 把 GPIO 管脚 GPIOx.0~GPIOx.15(x=A,B,C,D,E,F,G)分别对应中断线 0~15。这样子每个中断线对应了最多 9 个 IO 口,以线 0 为例:它对应了 GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0 和 GPIOG.0。而中断线每次只能连接到 1个 IO 口上,这样就需要通过配置决定对应的中断线配置到哪个 GPIO 上了。GPIO 和外部中断线映射关系是在寄存器 AFIO_EXTICR1 ~ AFIO_EXTICR4 中配置的。下图是寄存器 AFIO_EXTICR1的配置图。
AFIO_EXTICR1 寄存器配置 EXTI0 到 EXTI3 线,对应的外部中断引脚有 PAx 到 PGx,x=0 到 3。AFIO_EXTICR2 寄存器配置 EXTI4 到 EXTI7 线,对应的外部中断引脚有PAx 到 PGx,x=4 到 7。AFIO_EXTICR3 寄存器配置 EXTI8 到 EXTI11 线,对应的外部中断引脚有PAx 到 PGx,x=8 到 11。AFIO_EXTICR4 寄存器配置 EXTI12 到 EXTI15 线,对应的外部中断引脚有 PAx 到 PGx,x=12 到 15。
另外要注意的是,我们配置 AFIO 相关寄存器前,还需要打开 AFIO 时钟。
3.硬件设计
本次实验选用正点原子STM32RCT6开发版,需要实现的功能为:通过外部中断的方式让开发板上的三个独立按键控制LED灯,KEY_UP 控制 LED0 翻转,KEY1 控制 LED1 翻转,KEY0 控制 LED0 和 LED1 翻转。
硬件资源有
1)LED 灯 :LED0 – PA8 LED1 – PD2
2)独立按键:KEY0 - PC5 KEY1 - PA15 WK_UP - PA0
原理图如下:
4.程序设计
1) 使能对应 GPIO 口时钟。
2) 设置 GPIO 工作模式,触发条件,开启 AFIO 时钟,设置 IO 口与中断线的映射关系。
这些步骤 HAL 库全部封装在 HAL_GPIO_Init 函数里面,我们只需要设置好对应的参数,再调用 HAL_GPIO_Init 函数即可完成配置。
3) 配置中断优先级(NVIC),并使能中断。
配置好 GPIO 模式以后,我们需要设置中断优先级和使能中断,中断优先级我们使用 HAL_NVIC_SetPriority 函数设置,中断使能我们使用 HAL_NVIC_EnableIRQ 函数设置。
4) 编写中断服务函数。
每开启一个中断,就必须编写其对应的中断服务函数,否则将会导致死机(CPU 将找不到中断服务函数)。
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00U)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); /* 清中断标志位 */
HAL_GPIO_EXTI_Callback(GPIO_Pin); /* 外部中断回调函数 */
}
}
该函数实现的作用非常简单,通过入口参数 GPIO_Pin 判断中断来自哪个 IO 口,然后清除相应的中断标志位,最后调用回调函数 HAL_GPIO_EXTI_Callback()实现控制逻辑。在所有的外部中断服务函数中直接调用外部中断共用处理函数 HAL_GPIO_EXTI_IRQHandler,然后在回调函数 HAL_GPIO_EXTI_Callback 中通过判断中断是来自哪个 IO 口编写相应的中断服务控制逻辑。因此我们可以在 HAL_GPIO_EXTI_Callback 里面实现控制逻辑编写。
外部中断引脚定义 :由硬件设计小节,我们知道 KEY0、KEY1 和 KEY_UP 分别来连接到 PC5、PA15 和 PA0 上,我们做了下面的引脚定义。
/* 引脚 和 中断编号 & 中断服务函数 定义 */
#define KEY0_INT_GPIO_PORT GPIOC
#define KEY0_INT_GPIO_PIN GPIO_PIN_5
/* PC 口时钟使能 */
#define KEY0_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define KEY0_INT_IRQn EXTI9_5_IRQn
#define KEY0_INT_IRQHandler EXTI9_5_IRQHandler
#define KEY1_INT_GPIO_PORT GPIOA
#define KEY1_INT_GPIO_PIN GPIO_PIN_15
/* PA 口时钟使能 */
#define KEY1_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define KEY1_INT_IRQn EXTI15_10_IRQn
#define KEY1_INT_IRQHandler EXTI15_10_IRQHandler
#define WKUP_INT_GPIO_PORT GPIOA
#define WKUP_INT_GPIO_PIN GPIO_PIN_0
/* PA 口时钟使能 */
#define WKUP_INT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define WKUP_INT_IRQn EXTI0_IRQn
#define WKUP_INT_IRQHandler EXTI0_IRQHandler
KEY0、KEY1 和 WK_UP 分别连接 PC5、PA15 和 PA0,即对应了 EXTI5、EXTI15 和EXTI0 这三条外部中断线。这里需要注意的是 EXTI0 到 EXTI4 都是有单独的中断向量,EXTI5 到 EXTI9 是公用 EXTI9_5_IRQn,EXTI10 到 EXTI15 是公用 EXTI15_10_IRQn。
void extix_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
KEY0_INT_GPIO_CLK_ENABLE(); /* KEY0 时钟使能 */
KEY1_INT_GPIO_CLK_ENABLE(); /* KEY1 时钟使能 */
WKUP_INT_GPIO_CLK_ENABLE(); /* WKUP 时钟使能 */
gpio_init_struct.Pin = KEY0_INT_GPIO_PIN; /* KEY0 引脚 */
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct); /* KEY0 引脚初始化 */
gpio_init_struct.Pin= KEY1_INT_GPIO_PIN; /* KEY1 引脚 */
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING; /* 下升沿触发 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct); /* KEY1 引脚初始化 */
gpio_init_struct.Pin = WKUP_INT_GPIO_PIN; /* WKUP 引脚 */
gpio_init_struct.Mode = GPIO_MODE_IT_RISING; /* 上升沿触发 */
gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
HAL_GPIO_Init(WKUP_INT_GPIO_PORT, &gpio_init_struct); /* WKUP 引脚初始化 */
HAL_NVIC_SetPriority(WKUP_INT_IRQn, 2, 0); /* 抢占优先级为 2,子优先级为 0 */
HAL_NVIC_EnableIRQ(WKUP_INT_IRQn); /* 使能中断线 0 */
HAL_NVIC_SetPriority(KEY0_INT_IRQn, 2, 1); /* 抢占优先级为 2,子优先级为 1 */
HAL_NVIC_EnableIRQ(KEY0_INT_IRQn); /* 使能中断线 1 */
HAL_NVIC_SetPriority(KEY1_INT_IRQn, 2, 2); /* 抢占优先级为 2,子优先级为 2 */
HAL_NVIC_EnableIRQ(KEY1_INT_IRQn); /* 使能中断线 15 */
}
void WKUP_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN); /* 调用中断处理公用函数 */
}
void KEY0_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN); /* 调用中断处理公用函数 */
}
void KEY1_INT_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN); /* 调用中断处理公用函数 */
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* 消抖,此处为了方便使用了延时函数,实际代码中禁止在中断服务函数中调用
任何 delay 之类的延时函数!!! */
delay_ms(20);
switch (GPIO_Pin)
{
case KEY0_INT_GPIO_PIN:
if (KEY0 == 0) /* KEY0U 中断 */
{
LED0_TOGGLE(); /* LED0 状态取反 */
LED1_TOGGLE(); /* LED1 状态取反 */
}
break;
case KEY1_INT_GPIO_PIN:
if (KEY1 == 0) /* KEY1 中断 */
{
LED1_TOGGLE(); /* LED1 状态取反 */
}
break;
case WKUP_INT_GPIO_PIN:
if (WK_UP == 1) /* WK_UP 中断 */
{
LED0_TOGGLE(); /* LED0 状态取反 */
}
break;
}
}
最后的main.c代码如下
int main(void)
{
HAL_Init(); /* 初始化 HAL 库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为 115200 */
led_init(); /* 初始化 LED */
extix_init(); /* 初始化外部中断输入 */
LED0(0); /* 先点亮红灯 */
while (1)
{
printf("OK\r\n");
delay_ms(1000);
}
}