开始之前呢先祝大家圣诞节快乐,同时参加明天的研究生考试的同学们一战成"硕",接下来我就直奔主题了。
今天我要通过库函数操作stm32f407上的按键实现控制LED小灯以及蜂鸣器,实现的功能如下:
- KEY0键控制LED0的亮灭
- KEY1键控制LED1的亮灭
- KEY2键同时控制控制LED0和LED1的亮灭转换
- WK_UP键控制蜂鸣器
这篇文章同时会涉及到LED和蜂鸣器,相当于是对前两次的学习进行一个复习,可能在前两篇文章中没有考虑到的细节今天都会尽量考虑进去,希望这篇文章可以更好的帮助到大家对GPIO的理解,废话不多说,下面正式开始。
LED的初始化配置
想要点亮LED,首先我们需要确定LED在stm32f407开发板上的硬件电路连接,如下图所示:
从图上可以看出,两个LED属于共阳极连接,也就是说,当GPIO口输出高电平时,LED熄灭,当GPIO口输出低电平时,LED点亮,那么只需要配置好GPIO的输出,即可实现LED的亮灭,所以,我们还得知道控制LED的GPIO口是哪一个,毕竟stm32f4有70个用于控制LED的GPIO口;
我们可以从从上图中看到,控制LED0的是GPIOF_9,控制LED1的是GPIOF_10,同时我们还可以看到GPIOF_8是用来控制蜂鸣器(BEEP)的。知道了LED的硬件连接后,接下来就可以开始通过库函数配置我们的GPIO了。
通过库函数来配置GPIO,我们需要用到的库函数是GPIO_Init,对于这个函数,有如下的说明:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
从GPIO_Init的函数原型可以看出,需要用到两个参数,这两个参数分别是:
- GPIO_TypeDef* GPIOx:用来指明配置的GPIO是7组GPIO中的哪一组,这里控制LED用到的是GPIOF,所以该参数只需填入宏定义GPIOF即可
- GPIO_InitTypeDef* GPIO_InitStruct:从参数名字就可以知道这个参数是一个结构体的地址,所以在这里为了更好的说明,我们自定义一个结构体:
GPIO_InitTypeDef led_gpio;
该结构体的成员呢有以下几个:
uint32_t GPIO_Pin; //该参数对应的是某组GPIO中的某一个,毕竟一组GPIO有16个GPIO引脚
//就如前面说到控制LED0的GPIO口是GPIOF_9;控制LED1的GPIO口是GPIOF_10;
GPIOMode_TypeDef GPIO_Mode;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOOType_TypeDef GPIO_OType;
GPIOPuPd_TypeDef GPIO_PuPd;
GPIO_InitStruct的后面4个参数就是用于配置GPIO模式寄存器的,我在第一篇文章里说到,配置GPIO的模式需要用到4个寄存器,如果你没有看过我的第一篇文章的话,没有关系,我再来详细的说明一下:这里说到的寄存器分别是:
- GPIOx_MODER:GPIO端口模式寄存器
- GPIOx_OSPEEDR:GPIO端口输出速度寄存器
- GPIOx_OTYPER:GPIO端口输出类型寄存器
- GPIOx_PUPDR:GPIO端口上拉/下拉寄存器
分别对应着GPIO_InitStruct的后四个成员
下面我就来具体说说要控制LED,这4个GPIOF分别需要怎么配置:
>GPIOF_MODER
GPIOF_MODER寄存器可选的配置分别有:输入模式(复位状态),通用输出模式,复用功能模式,模拟模式,这里很容易想到,我们要向控制LED0,需要通过GPIOF_9输出高电平或低电平,所以毫无疑问我们需要将GPIOF_MODER寄存器配置成通用输出模式,对于该寄存器的四种模式在库函数中有如下的说明:
GPIO_Mode_IN = 0x00,
GPIO_Mode_OUT = 0x01,
GPIO_Mode_AF = 0x02,
GPIO_Mode_AN = 0x03
在此我们只需选择GPIO_Mode_OUT即可,一句话代码如下:
led_gpio.GPIO_Mode = GPIO_Mode_OUT;
>GPIOF_OSPEEDR
GPIOF_OSPEEDR寄存器可选的配置分别有:2MHz,25MHz,50MHz,100MHz,在此我们选择100MHz的端口输出速度(对于速度的选择我也不太明白会有啥影响,或者说选择的标准是啥,希望明白的大佬可以在评论区里解答下,非常感谢),对于这4中选择在库函数中有如下定义:
#define GPIO_Speed_2MHz GPIO_Low_Speed
#define GPIO_Speed_25MHz GPIO_Medium_Speed
#define GPIO_Speed_50MHz GPIO_Fast_Speed
#define GPIO_Speed_100MHz GPIO_High_Speed
所以一句话代码如下:
led_gpio.GPIO_Speed = GPIO_Speed_100MHz;
>GPIOx_OTYPER
GPIOF_OSPEEDR寄存器可选的配置分别有:推挽输出,开漏输出,在此我们选择推挽输出模式(同样不明白这两种模式的区别以及选择标准,希望有大佬可以在评论区解答下,感谢),对于这两种输出模式在库函数中有如下的定义:
typedef enum
{
GPIO_OType_PP = 0x00, //推挽输出
GPIO_OType_OD = 0x01 //开漏输出
}GPIOOType_TypeDef;
所以一句话代码如下:
led_gpio.GPIO_OType = GPIO_OType_PP;
>GPIOx_PUPDR
GPIOF_OSPEEDR寄存器可选的配置分别有:浮空,上拉,下拉,GPIOF_OSPEEDR的配置就比较自由,在此我选择上拉,以确保开发板上电时是熄灭的,对于这3种模式在库函数中有如下定义:
GPIO_PuPd_NOPULL = 0x00,
GPIO_PuPd_UP = 0x01,
GPIO_PuPd_DOWN = 0x02
所以一句话代码如下
led_gpio.GPIO_PuPd = GPIO_PuPd_UP;
到此,LED的GPIO配置就完成了,注意LED0和LED1配置的GPIO_pin是有区别的,同时不要忘了在调用GPIO_Init之前,一定要先使能GPIOF的外设时钟,这里需要用到的函数为:
void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState)
对于该函数的第1个参数用来指明初始化的是哪一个外设时钟,有如下定义(只截取了一段):
#define RCC_AHB1Periph_GPIOA ((uint32_t)0x00000001)
#define RCC_AHB1Periph_GPIOB ((uint32_t)0x00000002)
#define RCC_AHB1Periph_GPIOC ((uint32_t)0x00000004)
#define RCC_AHB1Periph_GPIOD ((uint32_t)0x00000008)
#define RCC_AHB1Periph_GPIOE ((uint32_t)0x00000010)
#define RCC_AHB1Periph_GPIOF ((uint32_t)0x00000020)
#define RCC_AHB1Periph_GPIOG ((uint32_t)0x00000040)
在此我们选择RCC_AHB1Periph_GPIOF即可,对于第2个参数是用于说明是否要初始化的,具体的定义如下:
#define IS_FUNCTIONAL_STATE(STATE) (((STATE) == DISABLE) || ((STATE) == ENABLE))
很明显我们要选择ENABLE
那么到这里第一个阶段的工作就完成了,我们可以将以上的代码封装成一个LED初始化的.c文件,只需要在main函数中调用即可,led_init.c代码如下:
void led_init(void){
GPIO_InitTypeDef led_gpio; //定义的GPIO模式初始化结构体
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
led_gpio.GPIO_Pin = GPIO_Pin_9||GPIO_Pin_10;//采用或运算可同时对LED1和LED0进行初始化
led_gpio.GPIO_Mode = GPIO_Mode_OUT;
led_gpio.GPIO_Speed = GPIO_Speed_100MHz;
led_gpio.GPIO_OType = GPIO_OType_PP;
led_gpio.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOF,&led_gpio);
}
BEEP(蜂鸣器)的初始化配置
在了解了GPIO的配置过程后,后续的共组就容易的多了。同样的,想要配置好BEEP,首先我们需要先知道BEEP的硬件是怎样连接的,从而知道应该对哪一个GPIO口进行配置,从前面的LED的硬件连接图上可以找的BEEP连接的是GPIOF_8,那么下面我直接上代码,详细的内容见代码的注释,毕竟今天的重点是按键:
void beep_init(){
GPIO_InitTypeDef beep_gpio; //定义的GPIO模式初始化结构体
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
beep_gpio.GPIO_Mode = GPIO_Mode_OUT; //配置GPIO模式为通用输出模式
beep_gpio.GPIO_OType = GPIO_OType_PP; //配置GPIO输出模式为推挽输出
beep_gpio.GPIO_Pin = GPIO_Pin_8; //选择配置8号引脚
beep_gpio.GPIO_PuPd = GPIO_PuPd_UP; //配置GPIO端口为上拉模式
beep_gpio.GPIO_Speed = GPIO_Speed_100MHz; //配置GPIO输出速度为100MHz
GPIO_Init(GPIOF,&beep_gpio); //调用GPIO初始化函数
}
KEY的初始化配置与扫描
KEY的初始化配置
首先逃不掉的还是硬件连接图:
从图中可以看出,对于按键KEY0,KEY1,KEY2,当它们连接到的GPIO端口为上拉模式时,如按键按下,此时GPIO口读到低电平,若按键没有按下,则GPIO口读到高电平,二对于KEY_UP来说就正好相反,当它对应的GPIO口为下拉模式时,没有按键按下,GPIO口读到第电平,按键按下则读到高点配,也就意味着我们对KEY_num和KEY_UP的配置是不一样的,同时,我们现在需要的不再是GPIO的通用输出模式,而是输入模式了,而且此处我们不需要再配置GPIO的输出模式(即推挽输出或者开漏输出),有了这样的认识后,我么们再来确定需要配置哪写GPIO口吧:
由上图可以知道,对于KEY0,KEY1,KEY2需要配置的GPIO分别是GPIOE_4,GPIOE_3,GPIOE_2,而KEY_UP配置的GPIO是GPIOA_0。在此,模仿LED与BEEP的配置过程可以有一下代码:
void key_init(void){
GPIO_InitTypeDef key_gpio; //定义GPIO初始化的结构体
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE|RCC_AHB1Periph_GPIOA,ENABLE);
key_gpio.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_3|GPIO_Pin_2;//采用或运算可同时对KEY0,KEY1和KEY2进行初始化
key_gpio.GPIO_Mode = GPIO_Mode_IN; //配置GPIO模式为输入模式
key_gpio.GPIO_Speed = GPIO_Speed_100MHz;//配置GPIO输出速度为100MHz
key_gpio.GPIO_PuPd = GPIO_PuPd_UP; //配置GPIO端口为上拉模式
GPIO_Init(GPIOE,&key_gpio); //调用GPIO初始化函数
key_gpio.GPIO_Pin = GPIO_Pin_0; //对KEY_UP进行初始化
key_gpio.GPIO_PuPd = GPIO_PuPd_DOWN; //配置GPIO端口为下拉模式
GPIO_Init(GPIOA,&key_gpio); //调用GPIO初始化函数
}
KEY的扫描
想要按下不同的按键实现不同的功能,就需要知道按下的键是哪一个,然后通过按下的按键控制相应的设备,就需要循环扫描按键以确定键值,下面使用伪代码说明一下按键扫描的过程:
//key_scan.c
void key_scan{
if(有按键按下){
延时10ms消抖;
if(确实有按键按下)
return 键值
else return 无效值;
}
}
//main.c
int main(){
while(1){
int key = key_scan();
switch(key){
case 1 : 点亮LED;break;
case 2 : 熄灭LED;break;
...
}
延时10ms
}
}
这样便实现了通过按键控制不同设备了,但是存在一个问题,当我按下某一个按键不放时,key_scan函数永远检测到有按键按下并且返回该键键值,再main函数中就会一直点亮或熄灭LED,如何避免呢,在此可以很巧妙的运用static定义一个静态的变量作为按键按下与否的标志位(简单来说就是保存一下上一次按键的结果),直接上伪代码解释(只需再原来的按键扫描的基础上修改即可)
//key_scan.c
void key_scan{
static KEY_STATE = 1;
if(KEY_STATE = 1 && 有按键按下){
延时10ms消抖;
KEY_STATE = 0 //按键有效从而更新状态
if(确实有按键按下)
return 键值
else if(没有按键按下) KEY_STATE= 1;//重新更新状态
}
return 0;
}
可能不好理解,可以试着多循环几次应该就会明朗许多,那么具体的按键扫描函数如下所示:
u8 key_scan(u8 mode){
static u8 key_state = 1;
if(mode) key_state = 1;
if(key_state && (KEY0 == 0 || KEY1 == 0 || KEY2 == 0 || WKUP == 1)){
delay_ms(10);
key_state = 0;
if(KEY0 == 0) return 1;
else if(KEY1 == 0) return 2;
else if(KEY2 == 0) return 3;
else if(WKUP == 1) return 4;
}else if(KEY0 == 1 && KEY1 == 1 && KEY2 == 1 && WKUP == 0)
key_state = 1;
return 0;
}
在此需要说明的是:判断是否有按键按下,我们需要调用函数实现
GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)
在以上的按键扫描函数中KEY0 == 0这样的表示即为GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4),是在头文件中进行了宏定义而已,具体如下:
//key.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 WKUP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)
分别对应判读KEY0,KEY1,KEY2以及KEY_UP是否按下。
至此所有的流程都已搞定,接下啦就是main函数的类容了:
int main(void){
u8 key;
delay_init(168);
led_init();
beep_init();
key_init();
LED0 = 0;
LED1 = 0;
while(1){
key = key_scan(0);
if(key){
switch(key){
case LED0_PRES: LED0 = !LED0; break;
case LED1_PRES: LED1 = !LED1; break;
case BEEP_PRES: BEEP = !BEEP; break;
case CHAG_PRES: {
LED0 = !LED0;
LED1 = !LED1;
}
break;
}
}else delay_ms(10);
}
}
同样这里需要注意的是对于LED和蜂鸣器的操作我采用了位操作的方式,位操作的相关内容可以参考我上一篇的文章,这里也是采用的宏定义方式,具体如下:
//led.h
#define LED0 PFout(9)
#define LED1 PFout(10)
//beep.h
#define BEEP PFout(8)
总结一下:经常会在CSDN上查找大佬的文章来学习,但没想过写一篇文章这么恼火,花了我两个半小时才搞定,太难了,不过一定要坚持呀!!!