EXTI外部中断
一. 中断介绍
中断就是让cpu中断当前的正常指令而转去执行另一处特点的代码的一种机制。
- 例如:当你在观看电视时,突然烧的水烧开了,你不得不去将烧开的水拿走。这时你去拿水的动作就是中断。
二. 外部中断(EXTI)
外部中断/事件控制器由20个产生事件/中断请求的边沿检测器组成(即表示有EXTI0~EXTI9),每个输入线可以独立地配置输入类型(脉冲或挂起)和对应的触发事件(上升沿或下降沿或者双边沿都触发)。每个输入线都可以独立地被屏蔽。挂起寄存器保持着状态线的中断请求。
- 简单来说就是引脚电平变换引起中断产生
产生中断的配置过程
硬件中断选择
-
配置20个中断线的屏蔽位(EXTI_IMR)
-
配置所选中断线的触发选择器(EXTI_RTSR和EXTI_FTSR);
-
配置对应到外部中断控制器(EXTI)的NVIC中断通道的使能和屏蔽位,使得20个中断线中的请求可以被正确地响应。
软件中断选择
-
配置20个中断/事件线屏蔽位(EXTI_IMR, EXTI_EMR)
-
设置软件中断寄存器的请求位(EXTI_SWIER)
一般我们使用硬件中断选择所以这里主要分析一下硬件中断选择
- 要产生中断,必须先配置好并使能中断线。根据需要的边沿检测设置2个触发寄存器。(外部中断框图中,则通过右侧输入线输入电平信号通过边沿检测电路判断是上升沿触发还是下降沿触发。这时经过或门,将产生中断的端口存入请求挂起寄存器中)
- 同时在中断屏蔽寄存器的相应位写’1’允许中断请求。
- 当外部中断线上发生了期待的边沿时,将产生一个中断请求,对应的挂起位也随之被置’1’。在挂起寄存器的对应位写’1’,将清除该中断请求。(中断屏蔽寄存器相当于开关的作用)
- 随之产生中断至NVIC中断控制器
补充:
- 外部唤醒线是边沿触发的,这些线上不能出现毛刺信号。
- 在写EXTI_FTSR寄存器时,在外部中断线上的下降沿信号不能被识别,挂起位不会被置位。在写EXTI_RTSR寄存器时,在外部中断线上的上升沿信号不能被识别,挂起位也不会被置位。在同一中断线上,可以同时设置上升沿和下降沿触发。即任一边沿都可触发中断。
- 相同的pin口(如PA0和PB0)不能同时触发中断
三. 代码及底层逻辑分析
主函数
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "InfraredSensor.h"
uint16_t count;
int main(void)
{
OLED_Init();
Init_InfraredSensor();
OLED_ShowString(1, 1, "num:");
while(1)
{
count=Get_InfraredSensor();
OLED_ShowNum(1,6,count,4);
}
}
初始化OLED和对射式红外计数器,然后通过对射式红外计数器每次高低电平的变换来定义产生中断的条件,然后输出在OLED显示屏上。
对射式红外计数器初始化函数
uint16_t num=0;
void Init_InfraredSensor(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
//使能RCC时钟让GPIOB和AFIO挂载在APB2总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//GPIO初始化,端口采用输入模式(具体采用那种模式与信号源电路有关)
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//GPIOB_Pin_14与中断线EXTILine14映射
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
//初始化EXTI
EXTI_InitStruct.EXTI_Line=EXTI_Line14;
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
//可选择模式,通过中断触发还是事件触发
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
//选择触发方式(上升沿,下降沿,上升沿和下降沿同时触发)
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStruct);
//设置中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//初始化中断优先级,并初始化,使能
NVIC_InitStruct.NVIC_IRQChannel=EXTI15_10_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStruct);
}
函数GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14)
该寄存器四位共同作用于一个中断线;第三位和第四位(斜体)共同决定使用哪个寄存器,0~3对应AFIO_EXTICR(1-4);第一位和第二位(加粗)共同决定使用那个输入源。
/**
* @brief Selects the GPIO pin used as EXTI Line.
* @param GPIO_PortSource: selects the GPIO port to be used as source for EXTI lines.
* This parameter can be GPIO_PortSourceGPIOx where x can be (A..G).
* @param GPIO_PinSource: specifies the EXTI line to be configured.
* This parameter can be GPIO_PinSourcex where x can be (0..15).
* @retval None
*/
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmp = 0x00;
/* Check the parameters */
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,在该例程中AFIO_EXTICR4.(GPIO_PinSource >> 0x02=0x0000 0011,0011=3)
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
GPIO_PinSource14=0x0000 1110;
GPIO_PinSource14 & (uint8_t)0x03=0x0000 0010;
0x04 * (GPIO_PinSource & (uint8_t)0x03)=0x04 * 0x02=8;
((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03))=0x0F00;
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
第一行
选择对应AFIO_EXTICR,在该例程中AFIO_EXTICR4.(GPIO_PinSource >> 0x02=0x0000 0011,0011=3)
(AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp)=AFIO->EXTICR[3] &= 0xF0FF 总结就是将EXTI14置0;
第二行
选择对应的输入端(PAx,PBx…)
(((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)))= 0x01 << 0x08 = 0x0100 = 0x0000 0001 0000 0000;
#define GPIO_PortSourceGPIOA ((uint8_t)0x00)
#define GPIO_PortSourceGPIOB ((uint8_t)0x01)
#define GPIO_PortSourceGPIOC ((uint8_t)0x02)
#define GPIO_PortSourceGPIOD ((uint8_t)0x03)
#define GPIO_PortSourceGPIOE ((uint8_t)0x04)
#define GPIO_PortSourceGPIOF ((uint8_t)0x05)
#define GPIO_PortSourceGPIOG ((uint8_t)0x06)
#define GPIO_PinSource0 ((uint8_t)0x00)
#define GPIO_PinSource1 ((uint8_t)0x01)
#define GPIO_PinSource2 ((uint8_t)0x02)
#define GPIO_PinSource3 ((uint8_t)0x03)
#define GPIO_PinSource4 ((uint8_t)0x04)
#define GPIO_PinSource5 ((uint8_t)0x05)
#define GPIO_PinSource6 ((uint8_t)0x06)
#define GPIO_PinSource7 ((uint8_t)0x07)
#define GPIO_PinSource8 ((uint8_t)0x08)
#define GPIO_PinSource9 ((uint8_t)0x09)
#define GPIO_PinSource10 ((uint8_t)0x0A)
#define GPIO_PinSource11 ((uint8_t)0x0B)
#define GPIO_PinSource12 ((uint8_t)0x0C)
#define GPIO_PinSource13 ((uint8_t)0x0D)
#define GPIO_PinSource14 ((uint8_t)0x0E)
#define GPIO_PinSource15 ((uint8_t)0x0F)
函数EXTI_Init(&EXTI_InitStruct)
该函数主要用于初始化EXTI
/**
* @brief Initializes the EXTI peripheral according to the specified
* parameters in the EXTI_InitStruct.
* @param EXTI_InitStruct: pointer to a EXTI_InitTypeDef structure
* that contains the configuration information for the EXTI peripheral.
* @retval None
*/
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)
{
uint32_t tmp = 0;
/* Check the parameters */
assert_param(IS_EXTI_MODE(EXTI_InitStruct->EXTI_Mode));
assert_param(IS_EXTI_TRIGGER(EXTI_InitStruct->EXTI_Trigger));
assert_param(IS_EXTI_LINE(EXTI_InitStruct->EXTI_Line));
assert_param(IS_FUNCTIONAL_STATE(EXTI_InitStruct->EXTI_LineCmd));
tmp = (uint32_t)EXTI_BASE;
if (EXTI_InitStruct->EXTI_LineCmd != DISABLE)
{
/* Clear EXTI line configuration */
//在中断屏蔽寄存器和时间屏蔽寄存器中将对应端口置零
EXTI->IMR &= ~EXTI_InitStruct->EXTI_Line;
EXTI->EMR &= ~EXTI_InitStruct->EXTI_Line;
//相当于配置EXTI模式
tmp += EXTI_InitStruct->EXTI_Mode;
//在这时在外部寄存器中已经配置了对应模式(是中断还是事件)
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
/* Clear Rising Falling edge configuration */
//将上升沿和下降沿寄存器中对应的端口置零
EXTI->RTSR &= ~EXTI_InitStruct->EXTI_Line;
EXTI->FTSR &= ~EXTI_InitStruct->EXTI_Line;
/* Select the trigger for the selected external interrupts */
if (EXTI_InitStruct->EXTI_Trigger == EXTI_Trigger_Rising_Falling)
{
/* Rising Falling edge */
//将上升沿和下降沿寄存器中对应的端口置1
EXTI->RTSR |= EXTI_InitStruct->EXTI_Line;
EXTI->FTSR |= EXTI_InitStruct->EXTI_Line;
}
else
{
//在之前已经配置过了所以在这里可以将tmp重新赋值EXTI_BASE;并将触发模式写到相应地址
tmp = (uint32_t)EXTI_BASE;
tmp += EXTI_InitStruct->EXTI_Trigger;
//使能对应EXTI中断
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
}
}
else
{
tmp += EXTI_InitStruct->EXTI_Mode;
/* Disable the selected external lines */
*(__IO uint32_t *) tmp &= ~EXTI_InitStruct->EXTI_Line;
}
}
补充:
*(__IO uint32_t *) tmp |= EXTI_InitStruct->EXTI_Line;
其中*(__IO uint32_t *),表示的是指向相应的地址,__IO表示volatile(即定义是可读/可写/可读写)。这句代码的意思就是指向了EXTI_BASE所在地址并将EXTI_InitStruct->EXTI_Line的值写进该地址
#ifdef __cplusplus
#define __I volatile /*!< defines 'read only' permissions */
#else
#define __I volatile const /*!< defines 'read only' permissions */
#endif
#define __O volatile /*!< defines 'write only' permissions */
#define __IO volatile /*!< defines 'read / write' permissions */
关于EXTI_BASE的解释:
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define EXTI_BASE (APB2PERIPH_BASE + 0x0400)
这是在头文件中已经定义好的。表示的就是一步步的寻找到对应中断寄存器。
EXTI中断挂载在APB2上,而APB2挂载在AHB系统总线上(照我的理解:EXTI_BASE相当于操纵中断的最终地址)
函数NVIC_PriorityGroupConfig()
/**
* @brief Configures the priority grouping: pre-emption priority and subpriority.
* @param NVIC_PriorityGroup: specifies the priority grouping bits length.
* This parameter can be one of the following values:
* @arg NVIC_PriorityGroup_0: 0 bits for pre-emption priority
* 4 bits for subpriority
* @arg NVIC_PriorityGroup_1: 1 bits for pre-emption priority
* 3 bits for subpriority
* @arg NVIC_PriorityGroup_2: 2 bits for pre-emption priority
* 2 bits for subpriority
* @arg NVIC_PriorityGroup_3: 3 bits for pre-emption priority
* 1 bits for subpriority
* @arg NVIC_PriorityGroup_4: 4 bits for pre-emption priority
* 0 bits for subpriority
* @retval None
*/
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
/* Check the parameters */
assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));
/* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */
SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}
首先需要说明的是寻找有关NVIC手册说明的需要在Cortex—M3编程手册上找。
SCB->AIRCR表示的是指向应用程序中断和复位控制寄存器,该寄存器的功能是AIRCR提供了异常模型的优先级分组控制、数据访问的端序状态控制和系统的重置控制。
#define AIRCR_VECTKEY_MASK ((uint32_t)0x05FA0000)
为什么AIRCR_VECTKEY_MASK值是0x05FA0000?
因为在手册的应用程序中断和复位控制寄存器说明中有这样一段话
To write to this register, you must write 0x5FA to the VECTKEY field, otherwise the processor ignores the write.
如果要写这个寄存器的话,VECTKEY必须赋值为0x5FA,否则过程中就要忽视掉这部分的写过程。
31-16位写入0x5FA。设置优先级全都在10-8位。
为什么要分组?
为有效管理系统所有中断,STM32单片机采用优先级分组的方式进行管理,分为抢占优先级组和非抢占优先级组,4个位根据划分方式不同,存在五组分组方式。
函数
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
{
uint32_t tmppriority = 0x00, tmppre = 0x00, tmpsub = 0x0F;
/* Check the parameters */
assert_param(IS_FUNCTIONAL_STATE(NVIC_InitStruct->NVIC_IRQChannelCmd));
assert_param(IS_NVIC_PREEMPTION_PRIORITY(NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority));
assert_param(IS_NVIC_SUB_PRIORITY(NVIC_InitStruct->NVIC_IRQChannelSubPriority));
if (NVIC_InitStruct->NVIC_IRQChannelCmd != DISABLE)
{
/* Compute the Corresponding IRQ Priority ---------------*/
tmppriority = (0x700 - ((SCB->AIRCR) & (uint32_t)0x700))>> 0x08;
tmppre = (0x4 - tmppriority);
tmpsub = tmpsub >> tmppriority;
tmppriority = (uint32_t)NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority << tmppre;
tmppriority |= NVIC_InitStruct->NVIC_IRQChannelSubPriority & tmpsub;
tmppriority = tmppriority << 0x04;
NVIC->IP[NVIC_InitStruct->NVIC_IRQChannel] = tmppriority;
/* Enable the Selected IRQ Channels ----------------------*/
//使能中断的产生
NVIC->ISER[NVIC_InitStruct->NVIC_IRQChannel >> 0x05] =
(uint32_t)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (uint8_t)0x1F);
}
else
{
/* Disable the Selected IRQ Channels ---------------------*/
NVIC->ICER[NVIC_InitStruct->NVIC_IRQChannel >> 0x05] =
(uint32_t)0x01 << (NVIC_InitStruct->NVIC_IRQChannel & (uint8_t)0x1F);
}
}
tmppriority = (0x700 - ((SCB->AIRCR) & (uint32_t)0x700))>> 0x08;
tmppre = (0x4 - tmppriority);
tmpsub = tmpsub >> tmppriority;
第一行
表示计算出分组具体是第几组。以第一组[NVIC_PriorityGroup_1 ((uint32_t)0x600)]为例,(SCB->AIRCR) & (uint32_t)0x700)保留决定组号的第8-10位,0-7位置0;(0x70 - ((SCB->AIRCR) & (uint32_t)0x700))=(0x700 - 0x600)=0x100;右移八位=0x001。
第二行/第三行
决定pre-emption priority(抢占优先级)和subpriority(响应优先级)是如何分组的(即在四位二进制码中各占几位)
tmppriority = (uint32_t)NVIC_InitStruct->NVIC_IRQChannelPreemptionPriority << tmppre;
tmppriority |= NVIC_InitStruct->NVIC_IRQChannelSubPriority & tmpsub;
tmppriority = tmppriority << 0x04;
那么1行,就是像临时变量中写入抢断优先级的值;
2行,就是向临时变量中写入亚优先级的值;注意抢断优先级和亚优先级一个在先一个在后,一旦分组一定,那么它们是会乖乖呆在自己位置上,不会去占用别人的位置的。
__IO uint8_t IP[240]; /*!< Offset: 0x300 Interrupt Priority Register (8Bit wide) */
NVIC->IP[NVIC_InitStruct->NVIC_IRQChannel] = tmppriority;
将已经配置好的优先级放入中断优先级寄存器中
中断函数
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line14)==SET)
{
num++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
EXTI15_10_IRQHandler是固定的中断向量,每一个中断通道号都有对应的中断向量。
DCD EXTI15_10_IRQHandler ; EXTI Line 15..10
函数EXTI_GetITStatus()
/**
* @brief Checks whether the specified EXTI line is asserted or not.
* @param EXTI_Line: specifies the EXTI line to check.
* This parameter can be:
* @arg EXTI_Linex: External interrupt line x where x(0..19)
* @retval The new state of EXTI_Line (SET or RESET).
*/
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)
{
ITStatus bitstatus = RESET;
uint32_t enablestatus = 0;
/* Check the parameters */
assert_param(IS_GET_EXTI_LINE(EXTI_Line));
//将中断屏蔽寄存器中的值与上中断通道号的值,目的是可以通过这个变量判断是否中断使能
enablestatus = EXTI->IMR & EXTI_Line;
//在这里是判断挂起寄存器中是否又触发请求并且中断屏蔽寄存器中是否有开放来自线x上的中断请求。
if (((EXTI->PR & EXTI_Line) != (uint32_t)RESET) && (enablestatus != (uint32_t)RESET))
{
bitstatus = SET;
}
else
{
bitstatus = RESET;
}
return bitstatus;
}
判断中断是否在使能。
函数EXTI_ClearITPendingBit(EXTI_Line14)
/**
* @brief Clears the EXTI's line pending bits.
* @param EXTI_Line: specifies the EXTI lines to clear.
* This parameter can be any combination of EXTI_Linex where x can be (0..19).
* @retval None
*/
void EXTI_ClearITPendingBit(uint32_t EXTI_Line)
{
/* Check the parameters */
assert_param(IS_EXTI_LINE(EXTI_Line));
EXTI->PR = EXTI_Line;
}
将对应的挂起寄存器中的通道号清零
为什么置1救能清零?
当在外部中断线上发生了选择的边沿事件,该位被置’1’。在该位中写入’1’可以清除它,也可以
通过改变边沿检测的极性清除