# EXTI 外部中断
NVIC 的名字叫做,嵌套中断向量控制器
NVIC 的出现,就是为了当 CPU 的秘书,如果我们把所有的中断都接在 CPU 上,CPU 对应位置就得进行适配,设计上就很麻烦。并且如果很多中断进行申请,造成了拥堵,CPU 也会很难搞,因为 CPU 还是主要负责进行运算的,中断分配的任务就放到别的地方吧。
NVIC 有很多个输入口,你有多少个中断都可以接过来
NVIC 只有一个输出口,直接接到 CPU 上
stm32 有 68 个可屏蔽中断通道,包含 EXTI、TIM、ADC、USART、SPI、I2C、RTC 等多个外设使用 NVIC 统一管理中断,每个中断通道都拥有 16 个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
NVIC 的中断优先级由优先级寄存器的 4 位(0~15)决定,这 4 位可以进行切分,分为高 n 位的抢占优先级和低 4-n 位的响应优先级,记住,值越小的优先级越高,比如 0 就是最高的优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
举个例子,就好像是有个医生在给一个人进行看病,抢占优先级高的可以直接把看病的那个人挤开,让自己去看。然后响应优先级就好像是有个人排队,他就应该排在第一位,大概就是这个意思。
综上所述,现在我们可以这么理解,NVIC 其实可以理解成为一个叫号系统,而 CPU 就是那个医生,下面我们来介绍第一位病人,EXTI 外部中断
EXTI(Extern Interrupt)外部中断
EXTI 可以监测指定 GPIO 口的电平信号,当其指定的 GPIO 口产生电平变化时,EXTI 将立即向 NVIC 发出中断申请,经过 NVIC 裁决后即可中断 CPU 主程序,使 CPU 执行 EXTI 对应的中断程序
支持的触发方式:上升沿 / 下降沿 / 双边沿 / 软件触发
解释一下:这里的上升沿是指由低电平上升到高电平,下降沿是由高电平降低到低电平,而双边沿就是指上下都计数
支持的 GPIO 口:所有 GPIO 口,但相同的 Pin 不能同时触发中断
通道数:16 个 GPIO_Pin,外加 PVD 输出、RTC 闹钟、USB 唤醒、以太网唤醒
触发响应方式:中断响应 / 事件响应
# 首先介绍一下 EXTI 的基本结构
这里的每个 GPIO 都有 16 个引脚,显然对于只有 16 个输入口的 EXT 是远远超出的,所以我们中间加一个 AFIO 中断引脚选择器,你可以理解为 GPIOA,B,C 共用一个 Pin 引脚口,那么就可以解释了,为什么相同的引脚不能同时触发中断,因为这样,你根本不知道是哪个 GPIO 口发生了中断。
经过 EXTI 后,分为了两种输出,一种直接接到 NVIC,一种接入其他外设。
根据常理,本来 20 种的中断输入应该有 20 种的中断输出,但是可能是 STM 公司觉得有点占用 NVIC 的通道,于是,将 EXTI5~9, EXTI10~15 分别合并为了一种通道, 也就是说 EXTI5~9 会触发同一种中断函数,同理 10 到 15 也是
对于合并通道的中断函数,还需根据标志位进一步确定这个是由哪个中断函数进来的
# AFIO 复用 IO 口
AFIO 主要用于引脚复用功能的选择和重定义,在 STM32 中,AFIO 主要完成两个任务:复用功能引脚重映射、中断引脚选择。
这就是 AFIO 的作用
# EXTI 外部中断的框图
记住其中带弧度的是或门,只要有一个输入端是 1,那最终输出端的数值就是 1
平的是与门,记住电路的符号就行
这也就能解释为什么软件的外部中断,也可以进行中断
由于自身设计的微妙,我们可以做到 A,B 两个端口输出的波的相位差 90 度,可以通过判断 A,B 端口的高低电平,从而确定此时是正转还是反转。
分析 A 端,当左端的端口并未进行接通时,由于 VCC 连接 R1,相当于进行电位的上拉,从此 A 点的电位为高电位。而当左端的端口接上时 A 端相当于直接接 GND。这里的 R3 起到了限流电阻的作用,防止电流过大。
这里的 C1 起到了滤波电容的作用,防止输出的电位过于抖动
滤波电容这块,感觉还是不太会
右端的电路同理
程序部分
首先在 Hardware 文件夹中新建 CountSensor.h 和 CountSensor.c 文件
CountSensor.h文件 | |
#ifndef _COUNT_SENSOR_H | |
#define _COUNT_SENSOR_H | |
void CountSensor_Init(void); | |
uint16_t get(void); | |
#endif |
# 那么如何配置外部的中断
只需要把从 GPIO 到 NVIC 这一路的信号电路都打开即可
第一步,配置 RCC,把我们这里涉及到的外设时钟都打开,不打开时钟,外设是无法工作的
第二步,配置 GPIO 口,选择我们的端口为输入模式
第三步,配置 AFIO,选择我们用的这一路 GPIO,连接到后面的 EXTI
第四步,配置 EXTI,选择边沿触发模式,不如上升沿,下降沿,或者双边沿
第五步,配置 NVIC,给我们这个中断一个合适的优先级别
最后通过 NVIC,外部中断的信号就能进入 CPU 了,这样,CPU 才能收到中断信号来跳转到中断函数里面,执行中断程序
那,这五步就是外部中断的配置流程
1. 首先第一步,配置时钟 RCC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); |
注意是 RCC_APB2 因为 GPIOB 是 APB2 的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); |
当你不确定这个外设是接在哪个总线上的时候,我们可以跳转到这个函数的定义
比如这个函数 RCC_APB2 这个函数,可以通过 arg 看到所有的这些外设类型
/** | |
* @brief Enables or disables the High Speed APB (APB2) peripheral clock. | |
* @param RCC_APB2Periph: specifies the APB2 peripheral to gates its clock. | |
* This parameter can be any combination of the following values: | |
* @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB, | |
* RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE, | |
* RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1, | |
* RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1, | |
* RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3, | |
* RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17, | |
* RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11 | |
* @param NewState: new state of the specified peripheral clock. | |
* This parameter can be: ENABLE or DISABLE. | |
* @retval None | |
*/ |
接着还需要打开 EXTI 和 NVIC 的外设,但是这两个外设的时钟是一直打开的,所以不需要再次打开,
NVIC 不需要开启时钟的原因是因为,NVIC 属于内核的外设,内核的外设都是不需要开启时钟的
而 RCC 管的都是内核外的外设,所以管不到 NVIC
第二步,配置 GPIO
GPIO_InitTypeDef GPIO_InitStructure;//注释,这里的GPIO_InitStructure只是一个名称,也可以替换成为u,等等其他的名字
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//这个输入模式,具体需要什么,需要到stm32手册中进行查询
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
接下来进行第三步,配置 AFIO,AFIO 并没有自己独立的库函数,它的对应函数在 GPIO 的文件中
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
然后在其中进行跳转,可以查到对应的
/**
* @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[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}
通过最后两行,我们可以发现,其实,这个函数就是配置 AFIO 的函数
如果你想配置 PB14 号口为中断口,那么我们就可以这样写
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
# 第四步,配置 EXTI 文件
首先先来学习一下 EXTI 的基本库函数
void EXTI_DeInit(void);//调用它,就可以把EXTI的配置都清除,恢复成上电默认的状态
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);//可以根据这个结构体里的函数配置EXTI外设,初始化EXTI主要用的就是这个函数,使用方法与GPIO_Init等同对比就行
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
void EXTI_ClearFlag(uint32_t EXTI_Line);
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
代码书写
我们首先肯定得初始化外设
/**
* @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)
然后根据提示,进行相关参数的配置
在文档中查找相关词 EXTI_InitTypeDef,可以得到如下的代码
EXTI_InitTypeDef a;
a.EXTI_Line=EXTI_Line14;//这四项直接在定义完a后会直接出来
a.EXTI_LineCmd=ENABLE;
a.EXTI_Mode=EXTI_Mode_Interrupt;
a.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_Init(&a);//初始化a去
在全局寻找 EXTI_Line,第一个参数
#define EXTI_Line0 ((uint32_t)0x00001) /*!< External interrupt line 0 */ | |
#define EXTI_Line1 ((uint32_t)0x00002) /*!< External interrupt line 1 */ | |
#define EXTI_Line2 ((uint32_t)0x00004) /*!< External interrupt line 2 */ | |
#define EXTI_Line3 ((uint32_t)0x00008) /*!< External interrupt line 3 */ | |
#define EXTI_Line4 ((uint32_t)0x00010) /*!< External interrupt line 4 */ | |
#define EXTI_Line5 ((uint32_t)0x00020) /*!< External interrupt line 5 */ | |
#define EXTI_Line6 ((uint32_t)0x00040) /*!< External interrupt line 6 */ | |
#define EXTI_Line7 ((uint32_t)0x00080) /*!< External interrupt line 7 */ | |
#define EXTI_Line8 ((uint32_t)0x00100) /*!< External interrupt line 8 */ | |
#define EXTI_Line9 ((uint32_t)0x00200) /*!< External interrupt line 9 */ | |
#define EXTI_Line10 ((uint32_t)0x00400) /*!< External interrupt line 10 */ | |
#define EXTI_Line11 ((uint32_t)0x00800) /*!< External interrupt line 11 */ | |
#define EXTI_Line12 ((uint32_t)0x01000) /*!< External interrupt line 12 */ | |
#define EXTI_Line13 ((uint32_t)0x02000) /*!< External interrupt line 13 */ | |
#define EXTI_Line14 ((uint32_t)0x04000) /*!< External interrupt line 14 */ | |
#define EXTI_Line15 ((uint32_t)0x08000) /*!< External interrupt line 15 */ | |
#define EXTI_Line16 ((uint32_t)0x10000) /*!< External interrupt line 16 Connected to the PVD Output */ | |
#define EXTI_Line17 ((uint32_t)0x20000) /*!< External interrupt line 17 Connected to the RTC Alarm event */ | |
#define EXTI_Line18 ((uint32_t)0x40000) /*!< External interrupt line 18 Connected to the USB Device/USB OTG FS | |
Wakeup from suspend event */ | |
#define EXTI_Line19 ((uint32_t)0x80000) /*!< External interrupt line 19 Connected to the Ethernet Wakeup event */ |
第二个参数
FunctionalState EXTI_LineCmd; /*!< Specifies the new state of the selected EXTI lines. | |
This parameter can be set either to ENABLE or DISABLE */ | |
开始中断还是不开启,ENABLE还是DISABLE |
第三个参数
typedef enum | |
{ | |
EXTI_Mode_Interrupt = 0x00, | |
EXTI_Mode_Event = 0x04 | |
}EXTIMode_TypeDef; |
是外部中断还是事件中断,这里我们采用外部中断
第四个参数
/** | |
* @brief EXTI Trigger enumeration | |
*/ | |
typedef enum | |
{ | |
EXTI_Trigger_Rising = 0x08, | |
EXTI_Trigger_Falling = 0x0C, | |
EXTI_Trigger_Rising_Falling = 0x10 | |
}EXTITrigger_TypeDef; |
这里代表的是选择上升沿触发,下降沿触发,还是上升下降沿都触发
到此为止,外部中断配置完成,第四步结束
EXTI_InitTypeDef a; | |
a.EXTI_Line=EXTI_Line14; | |
a.EXTI_LineCmd=ENABLE; | |
a.EXTI_Mode=EXTI_Mode_Interrupt; | |
a.EXTI_Trigger=EXTI_Trigger_Falling; | |
EXTI_Init(&a); |
# 第五步,配置 NVIC 外设
因为 NVIC 属于 stm32 内核,所以其库函数,被分配到杂项 misc 中
先学习一下 NVIC 的库函数
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); |
这个函数用来配置中断分组的,参数是中断分组的方式
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); |
根据 NVIC 结构体中指定的参数初始化 NVIC
跳转
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; | |
} | |
//pre-emption priority 代表的是抢占优先级,subpriority 代表的是相应优先级 |
这里我们选择第二组,两位响应,两位抢占,稍微平均一点
即配置成
void NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
然后就是经典操作了
对 NVIC 的初始化
NVIC_InitTypeDef b; | |
b.NVIC_IRQChannel= ; | |
b.NVIC_IRQChannelCmd= ; | |
b.NVIC_IRQChannelPreemptionPriority= ; | |
b.NVIC_IRQChannelSubPriority= ; | |
NVIC_Init(&b); |
接下来就是配置参数
跳转定义,全局工程文件搜索 IRQn_Type
根据我们选择芯片的种类,选择对应的中断函数
#ifdef STM32F10X_MD | |
ADC1_2_IRQn = 18, /*!< ADC1 and ADC2 global Interrupt */ | |
USB_HP_CAN1_TX_IRQn = 19, /*!< USB Device High Priority or CAN1 TX Interrupts */ | |
USB_LP_CAN1_RX0_IRQn = 20, /*!< USB Device Low Priority or CAN1 RX0 Interrupts */ | |
CAN1_RX1_IRQn = 21, /*!< CAN1 RX1 Interrupt */ | |
CAN1_SCE_IRQn = 22, /*!< CAN1 SCE Interrupt */ | |
EXTI9_5_IRQn = 23, /*!< External Line[9:5] Interrupts */ | |
TIM1_BRK_IRQn = 24, /*!< TIM1 Break Interrupt */ | |
TIM1_UP_IRQn = 25, /*!< TIM1 Update Interrupt */ | |
TIM1_TRG_COM_IRQn = 26, /*!< TIM1 Trigger and Commutation Interrupt */ | |
TIM1_CC_IRQn = 27, /*!< TIM1 Capture Compare Interrupt */ | |
TIM2_IRQn = 28, /*!< TIM2 global Interrupt */ | |
TIM3_IRQn = 29, /*!< TIM3 global Interrupt */ | |
TIM4_IRQn = 30, /*!< TIM4 global Interrupt */ | |
I2C1_EV_IRQn = 31, /*!< I2C1 Event Interrupt */ | |
I2C1_ER_IRQn = 32, /*!< I2C1 Error Interrupt */ | |
I2C2_EV_IRQn = 33, /*!< I2C2 Event Interrupt */ | |
I2C2_ER_IRQn = 34, /*!< I2C2 Error Interrupt */ | |
SPI1_IRQn = 35, /*!< SPI1 global Interrupt */ | |
SPI2_IRQn = 36, /*!< SPI2 global Interrupt */ | |
USART1_IRQn = 37, /*!< USART1 global Interrupt */ | |
USART2_IRQn = 38, /*!< USART2 global Interrupt */ | |
USART3_IRQn = 39, /*!< USART3 global Interrupt */ | |
EXTI15_10_IRQn = 40, /*!< External Line[15:10] Interrupts */ | |
RTCAlarm_IRQn = 41, /*!< RTC Alarm through EXTI Line Interrupt */ | |
USBWakeUp_IRQn = 42 /*!< USB Device WakeUp from suspend through EXTI Line Interrupt */ | |
#endif /* STM32F10X_MD */ |
配置第一个参数
b.NVIC_IRQChannel=EXTI15_10_IRQn ; |
配置第二个参数
FunctionalState NVIC_IRQChannelCmd; /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel | |
will be enabled or disabled. | |
This parameter can be set either to ENABLE or DISABLE */ |
负责确定中断通道是使能还是失能,这里我们选择 ENABLE
配置第三个参数
NVIC_IRQChannelPreemptionPriority | |
这个是负责的抢占优先级 | |
NVIC_IRQChannelSubPriority | |
这个是负责相应优先级 |
对于当前我们的这种情况,因为只有一种中断,所以我们采用参数可以 随便一点,只有当有很多个中断的时候,才会出现拥挤
b.NVIC_IRQChannelPreemptionPriority=1 ; | |
b.NVIC_IRQChannelSubPriority= 1 ; |
高电平 (GPIO_PIN_SET)、低电平 (GPIO_PIN_RESET)。
那么中断程序应该放在哪里呢,这就需要我们写一个中断函数
在 STM32 中,中断函数的名称都是固定的,每个中断通道都对应一个中断函数
中断函数的名字,我们可以参照一下启动文件,在其中找到中断函数
EXTI15_10_IRQHandler |
注意,这个中断函数一定要书写正确,因为如果中断函数写错了,那么程序就无法进入中断了
void EXTI15_10_IRQHandler(void) | |
{ | |
if(EXTI_GetITStatus(EXTI_Line14)==1)// 获取一下是哪个中断置为 1 了 | |
{ | |
EXTI_ClearITPendingBit(EXTI_Line14);// 这里需要清除一下中断,因为如果中断函数不清除的话,会一直在中断函数里面循环。如果你不清楚中断标志位,那么它就会一直申请中断。这样程序就会不断相应中断,执行中断程序,那么程序就会卡死在中断程序里面了,所以我们每次中断程序结束后,都应该清除一下标志位 | |
num++; | |
} | |
} |