需求:
- 我们要实现通过按下按键来触发外部中断,然后点亮LED灯。
中断简单来说,CPU在处理一件事的过程中,突然来了其他事件来需要CPU处理,然后CPU放下眼前的事,去处理突然来的事。这些事件就是中断。
CPU还有一个辅助人员:NVIC,可以帮助CPU进行筛选,根据优先级进行排序,选出一事件,然后让CPU处理。
一、以寄存器的方式实现
(1)时钟配置
- 在我使用的开发板上,PF8和PF10控制Key1和Key3,且这两个按键控制逻辑刚好相反。分别按下时,输出的电平信号相反,在后续的对应步骤中,在详细说明。
- 也就是意味着,我们现在需要打开PF------GPIOF的时钟。
除了GPIOF的时钟,还有AFIO,什么是AFIO?为什么需要打开它的时钟?- 我们需要从中断来源的分类开始说起:
- 内部其他控件
- 片上外设
- 外部中断
- 我们需要从中断来源的分类开始说起:
我们使用按键触发的是外部中断,不同于另外两种中断,另外两种中断,是可以直接与NVIC建立联系,而外部中断不可以。
这就好比CPU是皇帝,NVIC是宰相,内部其他控件是内廷近臣,片上外设是朝廷六部,它们有什么事,可以直接向宰相奏折。
而外部中断好比地方/外国使臣,它们肯定不能直接与宰相奏折呗!肯定需要进行一个机构进行统一管理(AFIO) ,最终在由另外一个机构与NVIC建立联系(EXIT)。
外部中断都是复用GPIO口,我使用的stm32包含GPIOA~GPIOG,一共有112根引脚,倘若这些引脚尝试的外部中断都与NVIC建立联系,那硬件电路设计不得非常复杂,而且占用空间吗?
所以通过AFIO将所有GPIO端口,相同的序号的引脚映射成一根,总数112根的引脚线被压缩成16根:EXIT0~EXIT15
- 在实际使用的使用,只需要关注使用的哪个GPIO,哪个引脚就可以了。
c
RCC->APB2ENR |=RCC_APB2ENR_IOPFEN;
RCC->APB2ENR |=RCC_APB2ENR_AFIOEN;
(2)配置GPIO工作模式
- 由于此时stm32需要获取来自外部中断的信息,所以需要配置为输入模式,以及上/下拉的具体模式
c
//输入模式--00
GPIOF->CRH&=~GPIO_CRH_MODE10;
GPIOF->CRH&=~GPIO_CRH_MODE8;
//上下拉输入模式--10
GPIOF->CRH|=GPIO_CRH_CNF10_1;
GPIOF->CRH&=~GPIO_CRH_CNF10_0;
GPIOF->CRH|=GPIO_CRH_CNF8_1;
GPIOF->CRH&=~GPIO_CRH_CNF8_0;
- 当我们完成了工作模式的配置以后,还需要给这个引脚设置一个默认的输入值,这个输入值如何设置,得参考按键的原理图:
- PF8连接着的K1,当我们按下的时候,这个线路就会导通,接地,因此就会输入低电平给引脚。所以我们需要设置PF8默认输出高电平,这样当按下的时候,就会由高变低 ,出现一个下降沿,可以作为促发中断的条件。
- 同理PF10连接着的K3,当我们按下的时候,这个线路就会导通,接地,因此就会输入高电平给引脚。所以我们需要设置PF10默认输出低电平,这样当按下的时候,就会由低变高 ,出现一个上升沿,可以作为促发中断的条件。
中断触发的条件有三种:
- 上升沿触发
- 下降沿触发
- 上升沿和下降沿都触发
c
//以上是包含,上下拉两种模式,我们要选择其中一种
GPIOF->IDR|=GPIO_IDR_IDR8;//默认上拉
GPIOF->IDR&=~GPIO_IDR_IDR10;//即默认是下拉
(3)配置AFIO 配置引脚复用选择
我们已经知道,AFIO将GPIOA~GPIOF的112根引脚映射成了最终的16根引脚,从EXIT0 ~ EXIT15。而我们使用的PF10和PF8,对应的就是EXIT8和EXIT10。
实际的寄存器中是将这16根引脚的配置,划分到了四个寄存器中:

我们要使用的10和8号引脚就属于EXTICR这个寄存器中。

在完成了查找引脚的工作以后,我们还需要说明这个引脚是属于哪一个GPIO端口的,我们可以看到每一个EXTIx中都有四位来表示是哪一个GPIO端口的,自然是PF:0101;
c
//可以先清零对应位
AFIO->EXTICR[2]&=~AFIO_EXTICR3_EXTI10;
AFIO->EXTICR[2]&=~AFIO_EXTICR3_EXTI8;
//配置对应位
AFIO->EXTICR[2]|=AFIO_EXTICR3_EXTI10_PF;
AFIO->EXTICR[2]|=AFIO_EXTICR3_EXTI8_PF;
(4)EXIT
现在我们可以来配置这个与NVIC建立联系的寄存器了。
如何建立联系?
自然很好理解,换一个问法就是如何触发中断?
有三种触发中断的方式,
- 对于PF10我们采用的是上升沿的触发方式
- 对于PF8我们采用的是下降沿的触发方式

除了这个以外,还有需要对事件屏蔽寄存器进行配置,我们需要让NVIC看到这个中断事件。因此,

将对应位给它置1.
c
//4.配置EXTI
//首先上升沿触发有效
EXTI->RTSR|=EXTI_RTSR_TR10;
EXTI->RTSR|=EXTI_FTSR_TR8;
//还配置屏蔽寄存器---是不是应该打开 0是屏蔽 1是打开
EXTI->IMR|=EXTI_IMR_MR10;
EXTI->IMR|=EXTI_IMR_MR8;
(5)NVIC的配置
最后就需要我们的大宰相NVIC,来进行最终的配置,前面我们已经提到了,它是帮助CPU筛选要处理的中断程序的:
- 主要是设置优先级类型
- 设置优先级
- 最后打开中断使能,等待最终中断的产生
c
//5.配置NVIC
//5.1配置优先级的类型,有五种
NVIC_SetPriorityGrouping(3); //全部是抢占优先级
//配置 哪个中断的优先级
NVIC_SetPriority(EXTI15_10_IRQn,3);
NVIC_SetPriority(EXTI9_5_IRQn,3);
//中断使能
NVIC_EnableIRQ(EXTI15_10_IRQn);
NVIC_EnableIRQ(EXTI9_5_IRQn);
(6)完善中断处理程序
-
通常当产生多个中断事件,这些事件都会交付给CPU处理,没被CPU处理的事件,就会处于挂起状态。
-
当我们进入中断程序要处理该事件时,第一件事就是取消该事件的挂起状态。
-
然后在判断是否真正的产生对应信号,比如我们设置的是上升沿触发,那我们就还需要读取对应引脚是否处于高电平。
-
最后就可以实现一些功能了
注意:
- 中断程序,尽量要保证简短,不能太过冗余
- 尽量不要写延时函数,循环语句之类的。
- 我们通常会定义一个全局的标志位,在中断中置1或0。然后在主函数中,根据这个标志位值,执行一些相应的操作
中断函数名都是固定的,我们可以去中断向量表 中去寻找:

c
//中断处理程序 中断向量表中寻找
void EXTI15_10_IRQHandler(void)
{
//进入中断程序以后,需要将挂起位清零
//写1,清零
EXTI->PR|=EXTI_PR_PR10;
if((GPIOF->IDR&GPIO_IDR_IDR10)!=0)
{
//翻转LED
Int_LED_Toggle(LED1);
}
}
void EXTI9_5_IRQHandler(void)
{
//进入中断程序以后,需要将挂起位清零
//写1,清零
EXTI->PR|=EXTI_PR_PR8;
if((GPIOF->IDR&GPIO_IDR_IDR8)!=1)
{
Int_LED_Toggle(LED2);
}
}
二、以HAL的方式实现
1.前置知识说明
这部分内容,我将带着大家来梳理一下通过HAL库的方式实现按键触发外部中断控制LED灯的亮灭。
首先,我们来看原理图,看一下我们需要的外设信息:

- 我们要使用KEY1和KEY3,来控制两个LED灯的亮灭,这是为什么呢?
- 因为这两个按键的控制逻辑恰好相反,我们可以来感受不同逻辑下,实现相同的功能。
- 首先是KEY1,可以看到它外接GND,这就意味着,当我们按下按键的时候,整个线路导通,PF8就会输入一个低电平给单片机。
- 而KEY2的逻辑,就相反,它外接VCC,所以导通的时候,就会输入给PF10高电平。
- 那如何通过按键来触发外部中断呢?
- 在此之前,我们已经介绍了触发外部中断的三种情况:
- 上升沿触发
- 下降沿触发
- 上升沿和下降沿都会触发
因此,我们可以给PF8和PF10设置一个初始值;
- KEY1按下之后,就是一个低电平,所以我们让PF8默认输入高电平,按下之后,电平由高变低,就形成了一个下降沿。
- 同理PF10就默认输出低电平,按下按键之后就变成高电平,电平由低变高,就形成了一个上升沿。
2.Cubemx的配置
现在我们可以打开CubeMX来进行一个图像化的配置:
- 首先选择我们要使用的引脚,进行模式的配置

- 外部中断是复用引脚,所以在给引脚进行配置的时候,有配置的选项:EXITx
2.引脚具体模式的配置以及打开NVIC的使能

- 如图所呈现的就是PF8配置情况,下降沿触发,默认输入高电平。

- 打开这个外部中断使能
3.优先级类型选择和优先级的配置

- 我们只对抢占优先级进行了一个配置。设置为3
常规配置:
- 晶振类型选择
- dubug选择单总线one-wire模式
- 时钟树,高速外部时钟8Mhz,经过9倍频为72Mhz,然后还需要给APB1进行分频。
- 工程管理的配置:
- 命名
- 选择IDE
- 保留必要文件和生成外设的.c和.h文件
(完整的图示说明可以见我写的第一篇)
3.在VScode中实现


- 这里面包含了各种中断,找到我们要使用的两个中断
- 我们可以看到这两个中断调用的都是同一个函数,只是传递的参数不一样。
我们鼠标移动到指定位置,按住Ctrl 点击,然后就可以跳转

- 在这里,代码已经帮助我们完成了产生中断进入中断程序是,挂起状态清除的工作。
- 然后可以看到调用了一个回调函数。

- 这是一个弱实现的一个函数,因此我们就可以对它进行一个重写,写我们目标的中断程序。
中断程序要保证简短,尽量不要写循环,延时之类的,我们可以给我们的按键定义两个静态的状态全局变量,当按下按键的时候,触发外部中断,在中断程序中,将其置1.然后再主函数的死循环中,检测这个状态变量的变化,执行相应的操作。
c
static uint8_t KEY1_State=0;
static uint8_t KEY2_State=0;
c
while (1)
{
/* USER CODE END WHILE */
if(HAL_GPIO_ReadPin(GPIOF,GPIO_PIN_10)==1&&KEY2_State==1)
{
HAL_Delay(20);
HAL_GPIO_TogglePin(GPIOA,LED2_Pin);
KEY2_State=0;
}
if(HAL_GPIO_ReadPin(GPIOF,GPIO_PIN_8)==0&&KEY1_State==1)
{
HAL_Delay(20);
HAL_GPIO_TogglePin(GPIOA,LED1_Pin);
KEY1_State=0;
}
c
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin==KEY1_Pin)
{
KEY1_State=1;
}
if(GPIO_Pin==KEY2_Pin)
{
KEY2_State=1;
}
}

