在前面的文章控制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,PG0
- EXIT1可以映射到PA1,PB1,PC1,PD1,PE1,PF1,PG1
- EXIT2可以映射到PA2,PB2,PC2,PD2,PE2,PF2,PG2
- EXIT3可以映射到PA3,PB0,PC3,PD3,PE3,PF3,PG3
…- EXIT14可以映射到PA14,PB14,PC14,PD14,PE14,PF14,PG14
- EXIT15可以映射到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个。
ST官方同样提供了相应的库函数来配置外部中断线与GPIO的映射关系
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
使用库函数我们就不用去挨个配置每个寄存器了,该函数的具体使用本喵会文章后面的代码过程中演示。
既然是用外部中断实现按键,那么中断服务函数是必不可少的,又因为用到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相连。
- 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 4
void 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_TX
GPIO_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_RX
GPIO_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;//抢占优先级是3
NVIC_InitStructe.NVIC_IRQChannelSubPriority=3;//响应优先级是3
NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;//通道使能
NVIC_Init(&NVIC_InitStructe);
//接收中断使能
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
//使能USART1
USART_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
{
//如果接收到的是0x0a
if(res!=0x0a)
USART_RX_STA=0;//如果不是,说明接收错误,整个标志位清0
else
USART_RX_STA|=0x8000;//接收完成了
}
else//还没有接收到0x0d
{
//如果接收到的是0x0d
if(res==0x0d)
USART_RX_STA|=0x0400;//接收0x0d标志位置1
else//接收到的不是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
接下来就是最重要的一步了,外部中断的初始化
这里我们用到与按键相连的PE4,PE3,PE2,PA0四个GPIO口
将使用到的GPIO设置成输入模式
使能AFIO寄存器时钟
该寄存器控制着中断线的映射,所以必须使能
- EXIT4映射到PE4
- EXIT3映射到PE3
- EXIT2映射到PE2
- EXIT0映射到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_H
void 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);//exit4
EXTI_InitStructe.EXTI_Line=EXTI_Line4;//中断线4
EXTI_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);//exit3
EXTI_InitStructe.EXTI_Line=EXTI_Line3;//中断线3
EXTI_Init(&EXTI_InitStructe);
//外部中断线2初始化
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);//exit2
EXTI_InitStructe.EXTI_Line=EXTI_Line2;//中断线2
EXTI_Init(&EXTI_InitStructe);
//外部中断线0初始化
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);//exit0
EXTI_InitStructe.EXTI_Line=EXTI_Line0;//中断线0
EXTI_InitStructe.EXTI_Trigger=EXTI_Trigger_Rising;//上升沿触发方式
EXTI_Init(&EXTI_InitStructe);
//中断初始化
NVIC_InitStructe.NVIC_IRQChannel=EXTI0_IRQn;//通道外部通道0
NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2
NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x00;//响应优先级全部设成2
NVIC_Init(&NVIC_InitStructe);
NVIC_InitStructe.NVIC_IRQChannel=EXTI4_IRQn;//通道外部通道4
NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2
NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x01;//响应优先级全部设成2
NVIC_Init(&NVIC_InitStructe);
NVIC_InitStructe.NVIC_IRQChannel=EXTI3_IRQn;//通道外部通道3
NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2
NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x02;//响应优先级全部设成2
NVIC_Init(&NVIC_InitStructe);
NVIC_InitStructe.NVIC_IRQChannel=EXTI2_IRQn;//通道外部通道2
NVIC_InitStructe.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructe.NVIC_IRQChannelPreemptionPriority=0x02;//抢占优先级全部设成2
NVIC_InitStructe.NVIC_IRQChannelSubPriority=0x03;//响应优先级全部设成2
NVIC_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灯和蜂鸣器实验的效果一样,实验中的串口部分效果和串口实验——简单数据收发的效果一样。
想看实验结果的小伙伴可以移步到上面提到的俩篇文章。