🐱作者:一只大喵咪1201
🐱专栏:《STM32学习》
🔥格言:你只管努力,剩下的交给时间!
外部中断实现按键实验
😻描述😻外部中断概述😼外部中断配置寄存器😻实验代码😼LED初始化😼蜂鸣器初始化😼按键初始化😼串口初始化😼外部中断初始化😻效果展示😻描述
在前面的文章控制LED和蜂鸣器的按键实验中详细的讲解了怎样通过GPIO的输入模式来控制LED灯和蜂鸣器的状态。这篇文章同样是实现上诉的功能,但是方式是采用外部中断的方式实现的,也就是4个按键分别对应一个外部中断,中断程序中控制一种状态。
😻外部中断概述
本喵使用的STM32F103ZET6芯片有7组GPIO,每一组GPIO又有16个IO口,而STM32的强大之处就在于,它的每一个IO口都支持外部中断的响应,这样来看它共有112个IO口可以实现中断响应,这么多IO口同样也有管理的方式。
该芯片有19根外部中断线,分别是:线 0~15:对应外部 IO 口的输入中断。线 16:连接到 PVD 输出。线 17:连接到 RTC 闹钟事件。线 18:连接到 USB 唤醒事件。
本次实验只使用到线0到15,也就是外部中断的中断线,其他3根中断线在使用到的时候本喵会详细的讲解。
可以看到,外部中断线有16根,而IO口有112个,所以它们之间存在一个映射关系:
可以看到,外部中断线与GPIO之间的映射关系为:
EXIT0可以映射到PA0,PB0,PC0,PD0,PE0,PF0,PG0EXIT1可以映射到PA1,PB1,PC1,PD1,PE1,PF1,PG1EXIT2可以映射到PA2,PB2,PC2,PD2,PE2,PF2,PG2EXIT3可以映射到PA3,PB0,PC3,PD3,PE3,PF3,PG3…EXIT14可以映射到PA14,PB14,PC14,PD14,PE14,PF14,PG14EXIT15可以映射到PA15,PB15,PC15,PD15,PE15,PF15,PG15
也就是IO口对应下标的数字是几就可以作为对应外部中断线的映射口。
注意:
一根外部中断线只能映射到一个IO口上,比如EXIT0映射到了PA0口上,此时PB0便不能再作为EXIT0的映射口。IO口作为映射口以后,并不影响读取该IO口的数据。
我们知道,中断是有中断服务函数的,那么当中断发生的时候,是CPU是怎么执行对应的中断函数的呢?
上图是一个中断向量表,不同的外部中断线产生中断请求并且被响应后就会竟如对应的中断程序,该程序的入口在对应的向量地址处。
EXIT0到EXIT4这5个外部中断对应的5个中断服务函数。中断发生并被响应后就会去执行对应的中断服务函数。EXIT5到EXIT9是共用一个中断服务函数的。这5个外部中断发生并被响应后都会进入同一个外部中断服务函数,所以在函数中需要写响应的判断代码,确定是哪个中断。EXIT10到EXIT15同样也是共用一个中断服务函数的。这5个中断发生并被响应后同样会进入一个中断服务函数。
😼外部中断配置寄存器
外部中断线与GPIO的映射关系是通过配置外部中断配置寄存器实现的,在AFIO(辅助功能寄存器)中配置外部中断的寄存器有3个。
AFIO_EXTICR1
上图是它的每一位的分布情况以及具体的控制情况。
AFIO_EXTICR2
上图是它的每一位的分布情况以及具体的控制情况。
AFIO_EXTICR3
上图是它的每一位的分布情况以及具体的控制情况。
ST官方同样提供了相应的库函数来配置外部中断线与GPIO的映射关系
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
使用库函数我们就不用去挨个配置每个寄存器了,该函数的具体使用本喵会文章后面的代码过程中演示。
😻实验代码
既然是用外部中断实现按键,那么中断服务函数是必不可少的,又因为用到LED灯,蜂鸣器,按键,以及串口,所以还需要这几个硬件以及串口外设的初始化函数。
😼LED初始化
通过原理图我们可以看到LED0是与PB5相连的,LED1是与PE5相连的。
LED0和LED1是采用的共阳极接法,也就是当PB5口和PE5口是低电平的时候LED0和LED1亮。
代码如下
led.h中的代码:
#ifndef __LED_H#define __LED_H#include "sys.h"//位操作头文件引用#define LED0 PBout(5)#define LED1 PEout(5)//为使用方便,直接用硬件名来控制灯的状态void LED_Init(void);//函数声明#endif
led.c中的代码:
#include "stm32f10x.h"//引用顶级头文件#include "led.h"//引用led初始化头文件void LED_Init(){GPIO_InitTypeDef GPIO_InitStructe;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE,ENABLE);//将GPIOB和GPIOE的时钟使能//PB5初始化GPIO_InitStructe.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出模式GPIO_InitStructe.GPIO_Pin=GPIO_Pin_5;GPIO_InitStructe.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructe);//PE5初始化GPIO_InitStructe.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出模式GPIO_InitStructe.GPIO_Pin=GPIO_Pin_5;GPIO_InitStructe.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOE,&GPIO_InitStructe);//初始状态为灯全灭LED0=1;LED1=1;}
😼蜂鸣器初始化
可以看到,蜂鸣器是与PB8相连的。
当PB8是高电平的时候,蜂鸣器发出响声。
beep.h中的代码:
#ifndef __BEEP_H#define __BEEP_H#include "sys.h"#define beep PBout(8)void BEEP_Init(void);#endif
beep.c中的代码:
#include "stm32f10x.h"#include "beep.h"void BEEP_Init(){GPIO_InitTypeDef GPIO_InitStructe;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//将GPIOB和GPIOE的时钟使能//PB5初始化GPIO_InitStructe.GPIO_Mode=GPIO_Mode_Out_PP;//推挽输出模式GPIO_InitStructe.GPIO_Pin=GPIO_Pin_8;GPIO_InitStructe.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructe);//蜂鸣器初始化为不响beep=0;}
😼按键初始化
可以看到,KEY0与PE4相连,KEY1与PE3相连,KEY2与PE2相连。
WK_UP按键与PA0相连。
KEY0到KEY2的另一端都是与地相连,当按键按下的时候是低电平,所以这3个按键要设置成上拉输入模式。WK_UP与高电平相连,当按键按下的时候是高电平,所以这个按键要设置成下拉输入模式。
key.h中的代码:
#ifndef __KEY_H#define __KEY_H#include "stm32f10x.h"#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)#define KEY0_PRES 1#define KEY1_PRES 2#define KEY2_PRES 3#define WK_UP_PRES 4void KEY_Init(void);#endif
key.c中的代码:
#include "stm32f10x.h"#include "key.h"#include "delay.h"void KEY_Init(void){GPIO_InitTypeDef GPIO_InitStructe;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//将GPIOA和GPIOE时钟使能//KEY0到KEY2设置成上拉输入模式GPIO_InitStructe.GPIO_Mode=GPIO_Mode_IPU;GPIO_InitStructe.GPIO_Pin=GPIO_Pin_4|GPIO_Pin_3|GPIO_Pin_2;GPIO_Init(GPIOE,&GPIO_InitStructe);//WK_UP设置成下拉输入模式GPIO_InitStructe.GPIO_Mode=GPIO_Mode_IPD;GPIO_InitStructe.GPIO_Pin=GPIO_Pin_0;GPIO_Init(GPIOA,&GPIO_InitStructe);}
😼串口初始化
我们可以看到,USART1_TX端口复用引脚是PA9,USART1_RX端口复用引脚是PA10。
至于PB6和PB7是端口重映射引脚,一般情况下是使用不到的,有兴趣的小伙伴可以看本喵的文章端口复用和重映像。
知道了串口是哪个引脚后我们需要对串口进行配置,让CPU知道该引脚此时是当作串口的。
如上图,我们使用的是全双工模式,需要对将PA9端口配置成推挽复用输出。对PA10配置成浮空或者上拉输入。进行GPIOA和USART1的时钟使能串口初始化配置中断优先级配置使能接收中断打开串口
将串口配置好后就需要写串口中断服务函数了。
串口通信的的约定可以看本喵的文章串口实验——简单的数据收发。
本喵在这里就不详细讲解如何写的了,直接上代码:
usart.h中的代码:
#include "sys.h"#include "usart.h" #if SYSTEM_SUPPORT_OS#include "includes.h"//ucos 使用 #endif#ifndef __USART_H#define __USART_H#include "stdio.h"#include "sys.h"#define USART_REC_LEN 200 //定义最大接收字节数 200#define EN_USART1_RX 1//使能(1)/禁止(0)串口1接收extern u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 extern u16 USART_RX_STA; //接收状态标记void uart_init(u32 bound);#endif
usart.c中的代码:
#if 1#pragma import(__use_no_semihosting) //标准库需要的支持函数 struct __FILE {int handle; }; FILE __stdout; //定义_sys_exit()以避免使用半主机模式 void _sys_exit(int x) {x = x; } //重定义fputc函数 int fputc(int ch, FILE *f){while((USART1->SR&0X40)==0);//循环发送,直到发送完毕 USART1->DR = (u8) ch;return ch;}#endif /*使用microLib的方法*//* int fputc(int ch, FILE *f){USART_SendData(USART1, (uint8_t) ch);while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}return ch;}int GetKey (void) { while (!(USART1->SR & USART_FLAG_RXNE));return ((int)(USART1->DR & 0x1FF));}*/#if EN_USART1_RX //如果使能了接收//串口1中断服务程序//注意,读取USARTx->SR能避免莫名其妙的错误 u8 USART_RX_BUF[USART_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.//接收状态//bit15,接收完成标志//bit14,接收到0x0d//bit13~0,接收到的有效字节数目u16 USART_RX_STA=0; //接收状态标记 void uart_init(u32 bound){GPIO_InitTypeDef GPIO_InitStructe;USART_InitTypeDef USART_InitStructe;NVIC_InitTypeDef NVIC_InitStructe;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1,ENABLE);//PA9初始化为复用推挽输出,复用为USART1_TXGPIO_InitStructe.GPIO_Pin=GPIO_Pin_9;GPIO_InitStructe.GPIO_Speed=GPIO_Speed_50MHz;GPIO_InitStructe.GPIO_Mode=GPIO_Mode_AF_PP;GPIO_Init(GPIOA,&GPIO_InitStructe);//PA10初始化为浮空输入,复用为USART1_RXGPIO_InitStructe.GPIO_Pin=GPIO_Pin_10;GPIO_InitStructe.GPIO_Mode=GPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA,&GPIO_InitStructe);//USART1初始化USART_InitStructe.USART_BaudRate=bound;//波特率USART_InitStructe.USART_WordLength=USART_WordLength_8b;//字长为8个字节USART_InitStructe.USART_StopBits=USART_StopBits_1;//停止位1位USART_InitStructe.USART_Parity=USART_Parity_No;//没有奇偶校验位USART_InitStructe.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;//接收和发送USART_InitStructe.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//五硬件流控制USART_Init(USART1,&USART_InitStructe);//中断优先级初始化NVIC_InitStructe.NVIC_IRQChannel=USART1_IRQn;//USART1通道中断NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级是3NVIC_InitStructe.NVIC_IRQChannelSubPriority=3;//响应优先级是3NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;//通道使能NVIC_Init(&NVIC_InitStructe);//接收中断使能USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//使能USART1USART_Cmd(USART1,ENABLE);}void USART1_IRQHandler(void){u8 res=0;#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.OSIntEnter(); #endif//判断是否发生接收中断if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET){res=USART_ReceiveData(USART1);//接收读取到的数据if((USART_RX_STA&0x8000)==0)//接收是否未完成{if(USART_RX_STA&0x4000)//是否接收到了0x0d{//如果接收到的是0x0aif(res!=0x0a)USART_RX_STA=0;//如果不是,说明接收错误,整个标志位清0elseUSART_RX_STA|=0x8000;//接收完成了}else//还没有接收到0x0d{//如果接收到的是0x0dif(res==0x0d)USART_RX_STA|=0x0400;//接收0x0d标志位置1else//接收到的不是0x0d{//将接收到的数据存到数组中USART_RX_BUF[USART_RX_STA&0x3FFF]=res;USART_RX_STA++;//如果数据个数大于最大接收长度if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收错误,标志位清0}}}}#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.OSIntExit(); #endif}#endif
😼外部中断初始化
接下来就是最重要的一步了,外部中断的初始化
使能用到的GPIO口
这里我们用到与按键相连的PE4,PE3,PE2,PA0四个GPIO口
将使用到的GPIO设置成输入模式
使能AFIO寄存器时钟
设置外部中断线的映射关系该寄存器控制着中断线的映射,所以必须使能
EXIT4映射到PE4EXIT3映射到PE3EXIT2映射到PE2EXIT0映射到PA0外部中断初始化
PE4,PE3,PE2这3个IO口采用的是上拉输入模式,当有按键按下时,该IO口是低电平所以3个IO口要设置成下降沿触发中断。PA0采用的是下拉输入模式,当有键按下时,该IO口是高电平所以PA0要设置成上升沿触发中断。中断优先级初始化
中断通道是EXIT0,EXIT4,EXIT3,EXIT2,一共4个通道抢占优先级全部设成2响应优先级全部设成2使能中断通道编写中断服务函数
外部中断线0对应的是WK_UP键的中断,当键按下后蜂鸣器的状态发生反转。外部中断线4对应的是KEY0键的中断,当键按下后俩个LED的状态同时发生反转。外部中断线3对应的是KEY1键的中断,当键按下后LED0的状态发生反转。外部中断线2对应的是KEY2键的中断,当键按下后LED1的状态发生反转。清除中断标志位
外部中断的中断标志位在发生中断后会由硬件置1,但是在执行完中断服务函数后硬件不会自动清零,所以需要我们手动在中断服务函数的最后将标志位清0。
exit.h中的代码:
#ifndef __EXIT_H#define __EXIT_Hvoid EXIT_Init(void);#endif
exit.c中的代码:
#include "exit.h"#include "stm32f10x.h"#include "led.h"#include "beep.h"#include "delay.h"#include "key.h"void EXIT_Init(void){EXTI_InitTypeDef EXTI_InitStructe;NVIC_InitTypeDef NVIC_InitStructe;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE|RCC_APB2Periph_GPIOA,ENABLE);//使能GPIOA和GPIOE组的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能AFIO时钟KEY_Init();//外部中断线4初始化GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);//exit4EXTI_InitStructe.EXTI_Line=EXTI_Line4;//中断线4EXTI_InitStructe.EXTI_LineCmd=ENABLE;EXTI_InitStructe.EXTI_Mode=EXTI_Mode_Interrupt;//模式是中断EXTI_InitStructe.EXTI_Trigger=EXTI_Trigger_Falling;//下降沿触发方式EXTI_Init(&EXTI_InitStructe);//外部中断线3初始化GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);//exit3EXTI_InitStructe.EXTI_Line=EXTI_Line3;//中断线3EXTI_Init(&EXTI_InitStructe);//外部中断线2初始化GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);//exit2EXTI_InitStructe.EXTI_Line=EXTI_Line2;//中断线2EXTI_Init(&EXTI_InitStructe);//外部中断线0初始化GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);//exit0EXTI_InitStructe.EXTI_Line=EXTI_Line0;//中断线0EXTI_InitStructe.EXTI_Trigger=EXTI_Trigger_Rising;//上升沿触发方式EXTI_Init(&EXTI_InitStructe);//中断初始化NVIC_InitStructe.NVIC_IRQChannel=EXTI0_IRQn;//通道外部通道0NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x00;//响应优先级全部设成2NVIC_Init(&NVIC_InitStructe);NVIC_InitStructe.NVIC_IRQChannel=EXTI4_IRQn;//通道外部通道4NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x01;//响应优先级全部设成2NVIC_Init(&NVIC_InitStructe);NVIC_InitStructe.NVIC_IRQChannel=EXTI3_IRQn;//通道外部通道3NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x02;//响应优先级全部设成2NVIC_Init(&NVIC_InitStructe);NVIC_InitStructe.NVIC_IRQChannel=EXTI2_IRQn;//通道外部通道2NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x03;//响应优先级全部设成2NVIC_Init(&NVIC_InitStructe);}void EXTI0_IRQHandler(void){delay_ms(10);//延时消抖//确定是否是WK_UP键按下if(WK_UP==1){beep=!beep;//蜂鸣器状态发生反转}EXTI_ClearITPendingBit(EXTI_Line0);}void EXTI4_IRQHandler(void){delay_ms(10);//延时消抖//确定是否是KEY0键按下if(KEY0==0){LED0=!LED0;LED1=!LED1;//俩个LED灯状态同时反转}EXTI_ClearITPendingBit(EXTI_Line4);}void EXTI3_IRQHandler(void){delay_ms(10);//延时消抖//确定是否是KEY1键按下if(KEY1==0){LED0=!LED0;//LED0状态发生反转}EXTI_ClearITPendingBit(EXTI_Line3);}void EXTI2_IRQHandler(void){delay_ms(10);//延时消抖//确定是否是KEY2键按下if(KEY2==0){LED1=!LED1;//LED1状态发生反转}EXTI_ClearITPendingBit(EXTI_Line2);}
😻效果展示
由于上传视频需要比较长的时间,该实验的效果和控制LED灯和蜂鸣器实验的效果一样,实验中的串口部分效果和串口实验——简单数据收发的效果一样。
想看实验结果的小伙伴可以移步到上面提到的俩篇文章。