EXTI(External interrupt/event controller)—外部中断/事件控制器,和在STM32NVIC中断优先级管理(中断向量表)中讲述的CM3内核的外部中断不同。特指的是,在中断向量表中的EXTI的外部中断。STM32的每个IO都可以作为外部中断输入。EXTI管理了控制器的 20个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。EXTI 可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。
STM32的中断控制器支持19个外部中断、事件请求(也就是19条外部中断线),每个中断设有状态位,每个中断/ 事件都有独立的触发和屏蔽设置。STM32的19个外部中断对应着19路中断线,分别是EXTI_Line0-EXTI_Line18::
线0~15:对应外部IO口的输入中断;
线16:连接到PVD输出;
线17:连接到RTC闹钟事件;
线18:连接到USB唤醒事件。
外部中断的功能框图包含了外部中断最核心内容,掌握了功能框图,对外部中断就有一个整体的把握,在编程时思路就非常清晰。外部中断功能框图见图。
在图可以看到很多在信号线上打一个斜杠并标注“20”字样,这个表示在控制器内部类似的信号线路有 20 个,这与外部中断总共有 20 个中断/事件线是吻合的。所以我们只要明白其中一个的原理,那其他 19 个线路原理也就知道了。
外部中断可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件上就有所不同。
首先我们来看图中红色虚线指示的电路流程。它是一个产生中断的线路,最终信号流入到 NVIC 控制器内。
编号 1 是输入线,外部中断控制器有 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和编号 6电路。
编号 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 内,从而实现系统中断事件控制。
接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一个脉冲信号。
产生事件线路是在编号3电路之后与中断线路有所不同,之前电路都是共用的。
编号 6 电路是一个与门电路,它一个输入来自编号 3 电路,另外一个输入来自事件屏蔽寄存器(EXTI_EMR)。如果 EXTI_EMR设置为 0时,那不管编号 3电路的输出信号是 1还是 0,最终编号 6 电路输出的信号都为 0;如果 EXTI_EMR 设置为 1 时,最终编号 6 电路输出的信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_EMR 来实现是否产生
事件的目的。
编号 7 是一个脉冲发生器电路,当它的输入端,即编号 6 电路的输出端,是一个有效信号 1 时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。
编号 8 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他外设电路使用,比如定时器 TIM、模拟数字转换器 ADC等等,这样的脉冲信号一般用来触发 TIM 或者 ADC开始转换。
产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且是电路级别的信号传输,属于硬件级的。
另外,EXTI是在 APB2总线上的,在编程时候需要注意到这点。
每条外部中断线可以独立的配置触发方式(上升沿、下降沿或者双边沿触发)、使能/失能中断、专用的状态位。
外部中断有 20个中断/事件线,每个 GPIO都可以被设置为输入线,占用 EXTI0至 EXTI15,还有另外四根用于特定的外设事件。可以看出,STM32供IO使用的中断线只有16条,但是STM32F10x系列的IO口多达上百个,其中STM32F103ZET6有112个引脚(7组GPIO,每组16个)。那么中断线怎么跟IO口对应呢?
从上图可以轻易地看出来:每个GPIOx的同一编号的引脚(共7个)与一条中断线对应。比如PA0、PB0、PC0、PD0、PE0、PF0、PG0一条中断线。EXTI0至 EXTI15用于 GPIO,通过编程控制可以实现任意一个 GPIO作为外部中断的输入源。由表可知,EXTI0 可以通过 AFIO 的外部中断配置寄存器 1(AFIO_EXTICR1)的EXTI0[3:0]位选择配置为 PA0、PB0、PC0、PD0、PE0、PF0、PG0,其他 EXTI线(EXTI中断/事件线)使用配置都是类似的。
注意一下:同一个时间,只能有一个IO口映射到同一个中断线。也就是说,PA0和PB0不能同一时间映射到同一个中断线,而PA0、PA1则可以同时映射,因为它们不是在同一个中断线上。
那么是不是16个中断线就可以分配16个中断服务函数呢?
答案也不是。之前在STM32NVIC中断优先级管理文章中,我们介绍了中断向量表。在中断向量表中,IO口外部中断在中断向量表中只分配了7个中断向量,也就是只能使用7个中断服务函数。
从上图中可以看出,外部中断线5~9分配一个中断向量,共用一个服务函数;外部中断线10~15分配一个中断向量,共用一个中断服务函数;而中断向量线0-4则单独使用一个中断服务函数。也就是说,IO口的外部中断最多只能使用7个。
中断服务函数的映射关系:
GPIO | IRQn | IRQHandler |
GPIO_Pin0 | EXTI0_IRQn | EXTI0_IRQHandler |
GPIO_Pin1 | EXTI1_IRQn | EXTI1_IRQHandler |
GPIO_Pin2 | EXTI2_IRQn | EXTI2_IRQHandler |
GPIO_Pin3 | EXTI3_IRQn | EXTI3_IRQHandler |
GPIO_Pin4 | EXTI4_IRQn | EXTI4_IRQHandler |
GPIO_Pin5 — GPIO_Pin9 | EXTI9_5_IRQn | EXTI9_5_IRQHandler |
GPIO_Pin10 — GPIO_Pin15 | EXTI15_10_IRQn | EXTI15_10_IRQHandler |
下面列举了中断服务函数的名称(在启动文件startup_stm32f10x_hd.s上):
void EXTI0_IRQHandler()
void EXTI1_IRQHandler()
void EXTI2_IRQHandler()
void EXTI3_IRQHandler()
void EXTI4_IRQHandler()
void EXTI9_5_IRQHandler()
void EXTI15_10_IRQHandler()
作用:每一位对应着一条中断线。清零表示屏蔽该线路上的中断请求;置1表示打开该线路上的中断请求。这是一个 32 寄存器。但是只有前 19 位有效。当位 x 设置为1 时,则开启这个线上的中断,否则关闭该线上的中断。
作用:每一位对应着一条中断线。清零表示屏蔽该线路上的事件请求;置1表示打开该线路上的事件请求。同IMR ,只是该寄存器是针对事件的屏蔽和开启。
作用:每一位对应着一条中断线。清零表示关闭上升沿触发中断或事件;置1表示打开上升沿触发中断或事件。该寄存器同IMR ,也是一个32为的寄存器,只有前 19位有效。位 x 对应线x 上的上升沿触发,如果设置为 1 ,则是允许上升沿触发中断/ 事件。否则,不允许。
作用:每一位对应着一条中断线。清零表示关闭下降沿触发中断或事件;置1表示打开下降沿触发中断或事件。同 PTSR,不过这个寄存器是设置下降沿的。下降沿和上升沿可以被同时设置,这样就变成了任意电平触发了。
作用:每一位对应着一条中断线。当EXTIx中断有效且SWIER=0时,向SWIER写1将触发中断请求。通过向该寄存器的位x 写入 1 ,在未设置 IMR 和EMR的时候,将设置PR中相应位挂起。如果设置了IMR 和EMR时将产生一次中断。被设置的SWIER位,将会在PR中的对应位清除后清除。
作用:每一位对应着一条中断线。如果第i个中断触发了,则PR位对应位自动置1;向PR位写入1清零该位,同时清除SWIER位。0 ,表示对应线上没有发生触发请求。1,表示外部中断线上发生了选择的边沿事件。通过向该寄存器的对应位写入 1 可以清除该位。在中断服务函数里面经常会要向该寄存器的对应位写1 来清除中断请求。
EXTICR寄存器组,总共有4 个,因为编译器的寄存器组都是从0 开始编号的,所以EXTICR[0]~ EXTICR[3],对应《STM32参考手册》里的 EXTICR1~ EXTICR 4(查了好久才搞明白这个数组的含义!!)。每个 EXTICR只用了其低16 位。
EXTICR[0] ~EXTICR[3]的分配如下:
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
作用:设置IO口与中断线之间的映射关系。
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
作用:初始化中断线,设置触发方式、中断还是事件等等。
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
作用:前者判断中断线中断状态,是否发生;后者清除中断线上的中断标志位。
实际上固件库除了这两个标志位函数之外,还提供了两个函数来判断外部中断状态和清除爱步状态标志位的函数,分别为:
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
void EXTI_ClearFlag(uint32_t EXTI_Line);
它们的作用和前面两个函数的作用类似,只是在EXTI_GetITStatus()函数中,会先判断这种中断是否使能,使能了才去判断中断标志位,而EXTI_GetFlagStatus则直接判断状态标志位。
1)开启IO口时钟,初始化IO口为输入。调用函数:GPIO_Init();
2)开启IO口复用时钟。调用函数:RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
3)设置IO口与中断线的映射关系。调用函数:GPIO_EXTILineConfig();
4)初始化线上中断,设置触发条件等。调用函数:EXTI_Init();
5)配置中断分组(NVIC),并使能中断。调用函数:NVIC_Init();
6)编写中断服务函数。调用函数:EXTIx_IRQHandler();
7)清除中断标志位。调用函数:EXTI_ClearITPendingBit()。
下面按照这个一般步骤来进行一个简单的外部中断程序:
void KEY_Init(void) //IO初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
//使能PORTA,PORTE时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);
//初始化GPIO模式和对应管脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0-KEY2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4
//初始化 WK_UP-->GPIOA.0 下拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0设置成输入,默认下拉
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}
void EXTIX_Init(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//按键端口初始化
KEY_Init();
//使能复用功能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//GPIOE.2 中断线以及中断初始化配置 下降沿触发
//选择EXTI信号源
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);
//确定中断线、中断模式、触发方式并使能:
EXTI_InitStructure.EXTI_Line=EXTI_Line2; //KEY2
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOE.3 中断线以及中断初始化配置 下降沿触发 //KEY1
//选择EXTI信号源
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
//确定中断线、中断模式、触发方式并使能:
EXTI_InitStructure.EXTI_Line=EXTI_Line3;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOE.4 中断线以及中断初始化配置 下降沿触发 //KEY0
//选择EXTI信号源
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);
//确定中断线、中断模式、触发方式并使能:
EXTI_InitStructure.EXTI_Line=EXTI_Line4;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOA.0 中断线以及中断初始化配置 上升沿触发 PA0 WK_UP
//选择EXTI信号源
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
//确定中断线、中断模式、触发方式并使能:
EXTI_InitStructure.EXTI_Line=EXTI_Line0;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//确定中断源、优先级(抢占优先级和子优先级),使能:
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键WK_UP所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
//确定中断源、优先级(抢占优先级和子优先级),使能:
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键KEY2所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
//确定中断源、优先级(抢占优先级和子优先级),使能:
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; //使能按键KEY1所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
//确定中断源、优先级(抢占优先级和子优先级),使能:
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn; //使能按键KEY0所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
//外部中断0服务程序
void EXTI0_IRQHandler(void)
{
delay_ms(10);//消抖
if(WK_UP==1) //WK_UP按键
{
BEEP=!BEEP;
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位
}
//外部中断2服务程序
void EXTI2_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY2==0) //按键KEY2
{
LED0=!LED0;
}
EXTI_ClearITPendingBit(EXTI_Line2); //清除LINE2上的中断标志位
}
//外部中断3服务程序
void EXTI3_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY1==0) //按键KEY1
{
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line3); //清除LINE3上的中断标志位
}
void EXTI4_IRQHandler(void)
{
delay_ms(10);//消抖
if(KEY0==0) //按键KEY0
{
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line4); //清除LINE4上的中断标志位
}
EXTIX_Init函数
1)RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE),使能复用功能时钟,这个步骤不能省略,否则肯定会出问题。STM32端口复用和重映射(AFIO辅助功能时钟)
2)在编写中断处理函数的过程中,在最后一步,一定要记得清除中断标志位。调用函数:EXTI_ClearITPendingBit()。否则下次中断就不会发生。
这里我们配置两个的中断软件优先级一样,如果出现了两个按键同时按下的情况,那怎么办,到底该执行哪一个中断?当两个中断的软件优先级一样的时候,中断来临时,具体先执行哪个中断服务函数由硬件的中断编号决定,编号越小,优先级越高。有关外设的硬件编号可查询《STM32F10X-中文参考手册》的中断和事件章节中的向量表,表中的位置编号即是每个外设的硬件中断优先级。当然,我们也可以把抢占优先级设置成一样,子优先级设置成不一样,这样就可以区别两个按键同时按下的情况,而不用硬件去对比硬件编号。
GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);用于选择 GPIO 管脚用作外部中断线路。库里面是这样定义的:
/**
* @摘要 选择用于EXTI线的GPIO管脚。
* @参数 GPIO_PortSource: 选择GPIO端口作为EXTI线路的源。
* 该参数可以是GPIO_PortSourceGPIOx, x可以是(A..G)。
* @参数 GPIO_PinSource: 指定要配置的EXTI线路。
* 该参数可以是GPIO_PinSourcex,其中x可以是(0..15)。
* @返回值 无
*/
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmp = 0x00;
/* 检查参数 */
assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource));
assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}