前面两个实验,我们介绍了 STM32F4 的 IO 口作为输出的使用,这一次,我们将向大家介绍如何使用 STM32F4 的 IO 口作为输入用。我们将利用板载的 4 个按键,来控制板载的两个 LED 的亮灭和蜂鸣器。通过本次的学习,你将了解到 STM32F4 的 IO 口作为输入口 的使用方法。
目录
一、实现的功能
二、 按键与输入数据寄存器介绍
2.1独立按键简介
2.2 GPIO 端口输入数据寄存器(IDR)
三、硬件设计
四、 程序设计
4.1创建工程模板
4.2工程中创建对应的文件
4.3添加文件路径
4.4添加相应的外设固件库和所需文件
4.5程序流程图
4.5.1 编写LED0和LED1初始化函数驱动代码
4.5.2 编写Beep蜂鸣器初始化函数
4.5.3 编写按键初始化函数
4.5.4 编写按键扫描函数
4.5.5 编写延时函数
4.5.6 程序主函数
五、下载验证
一、实现的功能
通过探索者 STM32F4 开发板上载有的 4 个按钮(KEY_UP、 KEY0、KEY1 和 KEY2),来控制板上的 2 个 LED(DS0 和 DS1)和蜂鸣器,其中 KEY_UP 控 制蜂鸣器,按一次叫,再按一次停;KEY2 控制 DS0,按一次亮,再按一次灭;KEY1 控制 DS1, 效果同 KEY2;KEY0 则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。
二、 按键与输入数据寄存器介绍
2.1独立按键简介
按键是一种电子开关,使用时轻轻按开关按钮就可使开关接通,当松开手时, 开关断开。我们开发板上使用的按键及内部简易图如下图所示:
按键管脚两端距离长的表示默认是导通状态,距离短的默认是断开状态,如 果按键按下,初始导通状态变为断开,初始断开状态变为导通。几乎每个开发板都会板载有独立按键,因为按键用处很多。常态下,独立按键是断开的, 按下的时候才闭合。每个独立按键会单独占用一个 IO 口,通过 IO 口的高低电平判断按键的状态。但是按键在闭合和断开的时候,都存在抖动现象,即按键在闭合时不会马上就稳定的连接, 断开时也不会马上断开。这是机械触点,无法避免。通常的按键所用开关为机械弹性开关, 当机械触点断开 、闭合时,电压信号 如下图所示:
由于机械点的弹性作用,按键开关在闭合时不会马上稳定的接通,在断开时也不会一下子断开,因而在闭合和断开的瞬间均伴随着一连串的抖动。抖动时间的长短由按键的机械特性决定的,一般为 5ms 到 10ms。按键稳定闭合时间的长短则由操作人员的按键动作决定的,一般为零点几秒至数秒。按键抖动会引起按键被误读多次。为了确保 CPU 对按键的一次闭合仅作一次处理(即采样稳定闭合阶段),必须进行消抖。 消抖方法分为硬件消抖和软件消抖,我们常用软件的方法消抖。
- 软件消抖:方法很多,最简单的是延时消抖。检测到按键按下后,一般进行 10ms 延时,用于跳过抖动的时间段,如果消抖效果不好可以调整这个 10ms 延时,因为不同类 型的按键抖动时间可能有偏差。待延时过后再检测读取按键状态,如果没有按下,那我们就判断这是抖动或者干扰造成的;如果按键还是按下状态,那么我们就认为这是按键真的按下了。对按键释放的判断同理。
- 硬件消抖:利用 RC 电路的电容充放电特性来对抖动产生的电压毛刺进行平滑出来,从而实现消抖,但是成本会更高一点,本着能省则省的原则,我们推荐使用软件消抖即可。
2.2 GPIO 端口输入数据寄存器(IDR)
本实验我们将会用到 GPIO 端口输入数据寄存器,下面来介绍一下。 该寄存器用于存储 GPIOx 的输入状态,它连接到施密特触发器上,IO 口外部的电平信号经过触发器后,模拟信号就被转化成 0 和 1 这样的数字信号,并存储到该寄存器中。寄存器描述如图所示。
该寄存器低 16 位有效,分别对应每一组 GPIO 的 16 个引脚。当 CPU 访问该寄存器,如果对应的某位为 0(IDRy=0),则说明该 IO 口输入的是低电平,如果是 1(IDRy=1),则表示输入的是高电平,y=0~15。对于我们使用固件库编程,STM32F4 的 IO 口做输入使用的时候,是通过调用函数 GPIO_ReadInputDataBit()来读取 IO 口的状态的,也就是读取该寄存器的值。
三、硬件设计
本实验用到的硬件资源有:
LED0和LED1 以及蜂鸣器和 STM32F4 的连接在前面都已经分别介绍了,独立按键硬件部分的原理图,如图所示,在探索者 STM32F4 开发板上的按键KEY_UP 连接在 PA0 上, KEY0 连接在 PE4 上、KEY1 连接在 PE3 上、KEY2 连接在 PE2 上。如图 8.2.1 所示:
这里需要注意的是:
KEY0、KEY1 和 KEY2 是低电平有效的,而 KEY_UP 是高电平有效的,并且外部都没有上下拉电阻,所以,需要在 STM32F4 内部设置上下拉。 这里的有效指的是:按键按下时,它是处于高电平还是低电平,很明显,从按键的原理图可知,对于KEY0、KEY1 和 KEY2 ,当按键按下时,GPIO 引脚的输入状态为低电平(按键所在的电路不通,引脚接地)。因此,他们是低电平有效,并且,我们在设置这三个端口模式时,应该上拉,默认是高电平,当按键按下,是低电平,这样便可以检测按键是否按下;对于KEY_UP,当按键被按下的时候,引脚的输入状态为高电平 ,(按键所在的电路导通,引脚接到电源)。因此,他们是高电平有效,并且,我们在设置这个端口模式时,应该下拉,默认是低电平,当按键按下,是高电平,这样便可以检测按键是否按下;总结:只要我们检测引脚的输入电平,即可判断按键是否被按下。
四、 程序设计
4.1创建工程模板
直接复制创建好的库函数模板, 在此模板上进行程序开发。将复制过来的模板文件夹重新命名为“实验三:按键输入实验 ”。打开此文件夹,在Project目录下新建一个文件夹,命名为:MyKey,用于存放按键的驱动程序,因为本次实验涉及到LED、Beep因此,同样需要建立和前两个实验一样的文件夹,存放对应的驱动程序。创建好后,目录如下所示:
4.2工程中创建对应的文件
在工程下创建对应的文件夹,同时新建对应的源文件和头文件,创建好后如下图所示:
4.3添加文件路径
要想工程在编译阶段能够找到创建的文件,必须要加入文件路径,详细步骤之前已经介绍过,路径添加完毕后,如下图所示:
4.4添加相应的外设固件库和所需文件
本次实验并没有用到额外的外设,我们就是通过STM32F407上的GPIOF口作为输入口和输出口,输出高电平,蜂鸣器发声,输出低电平,蜂鸣器不发声(IO口驱动蜂鸣器);输出低电平,LED灯点亮,输出低电平,LED灯熄灭;输入高低电平可判断按键是否被按下。
4.5程序流程图
程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。本实验的程序流程图如下:
4.5.1 编写LED0和LED1初始化函数驱动代码
由于后面我们需要利用按键来控制实现不同的功能,KEY2 控制 DS0,按一次亮,再按一次灭;KEY1 控制 DS1, 效果同 KEY2;KEY0 则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。所以我们必须要首先对它们进行初始化,设置PF9和PF10的GPIO口的端口模式。这与我们之前跑马灯实验初始化函数相同。
在 myled.h文件夹下编写如下代码:
#ifndef __MYLED_H
#define __MYLED_Hvoid LED_Init(void); //LED初始化函数#endif
在 myled.c文件夹下编写如下代码:
#include "stm32f4xx.h" // Device header
#include "myled.h"void LED_Init(void)
{//第一步:使能GPIOF两个口的时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能 GPIOF 时钟//第二步:GPIOF9,F10 初始化设置GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;//LED0 和 LED1 对应 IO 口GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化 GPIO//第三步:设置灯的初始状态GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10 设置高电平,灯灭}
4.5.2 编写Beep蜂鸣器初始化函数
由于后面我们需要利用按键来控制实现不同的功能,其中 KEY_UP 控 制蜂鸣器,按一次叫,再按一次停;所以我们必须要首先对它们进行初始化,设置PF8的GPIO口的端口模式.
在 mybeep.h文件夹下编写如下代码:
#ifndef __MYBEEP_H__
#define __MYBEEP_H__void BEEP_Init(void); //蜂鸣器初始化函数#endif
在 mybeep.c文件夹下编写如下代码:
#include "stm32f4xx.h" // Device header
#include "mybeep.h"void BEEP_Init(void)
{//第一步:开启时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能 GPIOF 时钟//第二步:初始化蜂鸣器对应引脚 GPIOF8GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHzGPIO_Init(GPIOF, &GPIO_InitStructure);//初始化 GPIOGPIO_ResetBits(GPIOF,GPIO_Pin_8); //蜂鸣器对应引脚 GPIOF8 拉低,此时不会发声(初始状态)}
4.5.3 编写按键初始化函数
我们后面需要检测按键是否被按下,从而执行不同的操作,实现不同的功能,通过上面查看原理图我们知道,有四个按键,在探索者 STM32F4 开发板上的按键KEY_UP 连接在 PA0 上, KEY0 连接在 PE4 上、KEY1 连接在 PE3 上、KEY2 连接在 PE2 上。此时GPIO口需要用作输入口,通过检测对应GPIO口的高低电平(通过标准库函数读取即可),从而判断按键是否被按下,因此,我们首先需要对按键进行初始化;
在 mykey.h文件夹下编写如下代码:
#ifndef __MYKEY_H__
#define __MYKEY_H__
#include <stdint.h>//对按键操作函数做了下面的定义
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4) //PE4
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3) //PE3
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2) //PE2
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) //PA0//按键进行宏定义增加程序可读性
#define KEY0_PRES 1
#define KEY1_PRES 2
#define KEY2_PRES 3
#define WKUP_PRES 4void KEY_Init(void);//按键初始化函数
uint8_t KEY_Scan(uint8_t mode); //按键扫描函数#endif
需要注意的是:为提高代码的可读性,为了后续对按键进行便捷的操作,我们为按键操作函数做了下面的定义,此外我对按键的键值同样使用了宏定义。
//对按键操作函数做了下面的定义
#define KEY0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4) //PE4
#define KEY1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3) //PE3
#define KEY2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2) //PE2
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0) //PA0通过这种方式,我们后面直接使用KEY0、KEY1、KEY2、WK_UP相当于调用相应端口读取电平函数,KEY0、KEY1、KEY2和WK_UP分别是读取对应按键状态的宏定义。用GPIO_ReadInputDataBit() 函数实现,该函数的返回值就是 IO 口的状态,返回值是枚举类型,取值 0 或者 1。看起来更加直观。
//按键进行宏定义增加程序可读性
#define KEY0_PRES 1
#define KEY1_PRES 2
#define KEY2_PRES 3
#define WKUP_PRES 4KEY0_PRES、KEY1_PRES、KEY2_PRES 和 WKUP_PRES 则是按键对应的四个键值宏定 义标识符。同样使得代码更加直观,而不是数字1 2 3 4 ,直接代表按键。
在 mykey.c文件夹下编写如下代码:
KEY_Init()函数用来初始化按键的端口及时钟。要知道按键是否按下,就需要读取按键所对应的 IO 口电平状态,因此我们需要把 GPIO 配置为输入模式,并且需要把 KEY_UP 按键对应的GPIOA_Pin_0设置为下拉模式,其他几个GPIO口,PE4、 PE3、 PE2需要设置为上拉模式,前面我们分析过。
#include "stm32f4xx.h" // Device header
#include "mykey.h"
#include "mydelay.h"//按键初始化函数:四个按键对应4个GPIO口
void KEY_Init(void)
{//第一步:开启时钟,使能 GPIOA,GPIOE 时钟RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,ENABLE);//第二步:设置端口模式 //step1.对于PE2、PE3、PE4应设置为输入上拉模式GPIO_InitTypeDef Struct;Struct.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;//KEY0 KEY1 KEY2 对应引脚Struct.GPIO_Mode = GPIO_Mode_IN;//普通输入模式Struct.GPIO_Speed = GPIO_Speed_100MHz;//100MStruct.GPIO_PuPd = GPIO_PuPd_UP;//上拉,默认高电平GPIO_Init(GPIOE, &Struct);//初始化 GPIOE2,3,4//step2.对于PA0应设置为输入下拉模式GPIO_InitTypeDef Struct1;//按键WK_UP 对应引脚 PA0Struct1.GPIO_Pin = GPIO_Pin_0;Struct1.GPIO_Mode = GPIO_Mode_IN;//普通输入模式Struct1.GPIO_Speed = GPIO_Speed_100MHz;//100MStruct1.GPIO_PuPd = GPIO_PuPd_DOWN ;//下拉,默认低电平GPIO_Init(GPIOA, &Struct1);//初始化 GPIOA0}
4.5.4 编写按键扫描函数
要知道哪个按键被按下,就需要编写按键扫描函数
同样在 mykey.c文件夹下编写如下代码:
mode: 具体含义如下:
- 0, 不支持连续按(当按键按下不放时, 只有第一次调用会返回键值, 必须松开以后, 再次按下才会返回其他键值)
- 1, 支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
uint8_t KEY_Scan(uint8_t mode)
{static uint8_t key=1;//按键按松开标志if(mode){ key=1; } if(key&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1)) //任何一个按键按下{My_Delay_ms(10);//去抖动key=0; if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)==0) //按键0被按下return 1;else if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==0)//按键1被按下return 2;else if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)==0)//按键2被按下return 3;else if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)//按键WK_UP被按下return 4;}else if(GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)==1&&GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)==1&&GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)==1&&GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==0){key=1;} return 0;// 无按键按下
}
key_scan 函数用于扫描这 4 个 IO 口是否有按键按下。key_scan 函数,支持两种扫描方式, 通过 mode 参数来设置。
- 当 mode 为 0 的时候,key_scan 函数将不支持连续按,扫描某个按键,该按键按下之后必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。
- 当 mode 为 1 的时候,函数是支持 连续扫描的,即使按键未松开,在函数内部有 if(mode==1)这条判断语句,因此 key 始终是等于 1 的,所以可以连续扫描按键,当按下某个按键,会一直返回这个按键的键值,这样做的好处是可以很方便实现连按操作。
有了 mode 这个参数,大家就可以根据自己的需要,选择不同的方式。函数内的 My_Delay_ms(10)即为软件消抖处理,通常延时 10ms 即可。 KEY_Scan 函数还带有一个返回值,如果未有按键按下,返回值即为 0,否则返回值即为对应按键的键值,如 KEY_UP_PRESS、KEY0_PRESS、KEY1_PRESS、KEY2_PRESS,这都是头文件内定义好的宏,方便大家记忆和使用。函数内定义了 一个 static 变量,所以该函数不是一个可重入函数(静态局部变量在函数进行下一次调用时不会重新初始化,直接使用上一次调用函数后的值)。还有一点要注意的就是该函数按键的扫描是有优先级的,因为函数内用了 if...else if...else 格式,所 以最先扫描处理的按键是 KEY_UP,其次是 KEY0,然后是 KEY1,最后是 KEY2。如果需要将其优先级设置一样,那么可以全部用 if 语句。
4.5.5 编写延时函数
利用系统节拍定时器可以实现精准延时(后期博客详细总结),因此我们实现了三个延时任意时间的函数,后续项目使用直接引入头文件,然后直接调用相应的函数即可。
在 mydelay.h文件夹下编写如下代码:
#ifndef __MYDELAY_H__
#define __MYDELAY_H__
#include <stdint.h>void My_Delay_us(uint32_t num); //延时任意微秒
void My_Delay_ms(uint32_t num); //延时任意毫秒
void My_Delay_s(uint32_t num); //延时任意秒#endif
在 mydelay.c文件夹下编写如下代码:
#include "stm32f4xx.h" // Device header
#include "mydelay.h"//延时num微秒
void My_Delay_us(uint32_t num)
{while(num--){SysTick ->CTRL = (1 << 0);SysTick ->CTRL &= ~(1<<2);SysTick ->CTRL &= ~(1<<1);SysTick ->VAL = 0x0;SysTick ->LOAD = 21; //1秒 21000000HZ,1毫秒 21000HZ 1微秒 21 HZwhile(!(SysTick ->CTRL & (1<<16)));SysTick ->CTRL = ~(1<<0);}
}//延时num毫秒,1毫秒等于1000微秒
void My_Delay_ms(uint32_t num)
{while(num--){My_Delay_us(1000);}
}//延时num秒,1秒等于1000毫秒
void My_Delay_s(uint32_t num)
{while(num--){My_Delay_ms(1000);}
}
4.5.6 程序主函数
首先是打开外设时钟。接下来调用 led_init 来初始化 LED 灯,调用 beep_init 函数初始化蜂鸣器,调用 key_init 函数初始化按键。最后在无限循环里面扫描获取键值,扫描函数传入的参数值为 0,即 mode=0, 所以这里只对它单次按键操作,将扫描函数返回后的键值保存在变量 key 内,接着用键值判断哪个按键按下,如果有按键按下则翻转相应的灯或翻转蜂鸣器,如果没有按键按下则延时 10ms。编写代码如下:
这里用到了一个电平翻转函数 GPIO_ToggleBits ();,一个按键被连续按下两次,那么电平便会翻转,从而控制灯的翻转和蜂鸣器的打开和关闭。
#include "stm32f4xx.h" // Device header
#include "mykey.h"
#include "mydelay.h"
#include "myled.h"
#include "mybeep.h"int main(void)
{uint8_t key; //保存键值 LED_Init(); //初始化 LED 端口PF9和PF10BEEP_Init(); //初始化蜂鸣器端口PF8KEY_Init(); //初始化与按键连接的硬件接口while(1){key=KEY_Scan(0); //得到键值if(key){ switch(key){ case WKUP_PRES: //控制蜂鸣器开和关GPIO_ToggleBits (GPIOF,GPIO_Pin_8);break;case KEY0_PRES: //控制 LED0 翻转GPIO_ToggleBits (GPIOF,GPIO_Pin_9);break;case KEY1_PRES: //控制 LED1 翻转GPIO_ToggleBits (GPIOF,GPIO_Pin_10);break;case KEY2_PRES: //同时控制 LED0,LED1 翻转 GPIO_ToggleBits (GPIOF,GPIO_Pin_9);GPIO_ToggleBits (GPIOF,GPIO_Pin_10);break;}}else {My_Delay_ms(10); //延时10ms}} }
五、下载验证
我们先来看看编译结果,如图所示:
可以看到 0 错误,0 警告,编译通过接下来,大家就可以下载验证了。这里我们使用 DAP 仿真器下载。下载完之后,运行结果如图所示,可以看到我们可以按 KEY0、KEY1、KEY2 来看看 LED 灯的变化或者按 KEY_UP 看看蜂鸣器的变化,和我们预期的结果一致,符合预期设计。
按键输入
至此,我们的本次的学习就结束了。学习了 STM32F407 的 IO 作为输入的使用方法, 在前面的 GPIO 输出的基础上又学习了一种 GPIO 使用模式,大家可以回顾前面跑马灯实验介绍的 GPIO 的八种模式类型巩固 GPIO 的知识。