本帖最后由 正点原子运营 于 2023-7-5 14:35 编辑
第十五章 按键输入实验 1)实验平台:正点原子探索者STM32F407开发板
2) 章节摘自【正点原子】STM32F407开发指南 V1.1
3)购买链接:https://detail.tmall.com/item.htm?id=609294673401
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)STM32技术交流QQ群:151941872
上一章,我们介绍了STM32F407的IO口作为输出的使用。本章,我们将向大家介绍如何使用STM32F407的IO口作为输入。我们将利用板载的4个按键,来控制板载的两个LED和蜂鸣器。通过本章的学习,我们将了解到STM32F407的IO口作为输入的使用方法。 本章分为如下几个小节: 15.1 按键与输入数据寄存器 15.2 硬件设计 15.3 程序设计 15.4 下载验证
15.1 按键与输入数据寄存器简介
15.1.1 独立按键简介几乎每个开发板都会板载有独立按键,因为按键用处很多。常态下,独立按键是断开的,按下的时候才闭合。每个独立按键会单独占用一个IO口,通过IO口的高低电平判断按键的状态。但是按键在闭合和断开的时候,都存在抖动现象,即按键在闭合时不会马上就稳定的连接,断开时也不会马上断开。这是机械触点,无法避免。独立按键抖动波形图如下: 图中的按下抖动和释放抖动的时间一般为5~10ms,如果在抖动阶段采样,其不稳定状态可能出现一次按键动作被认为是多次按下的情况。为了避免抖动可能带来的误操作,我们要做的措施就是给按键消抖(即采样稳定闭合阶段)。消抖方法分为硬件消抖和软件消抖,我们常用软件的方法消抖。
软件消抖:方法很多,我们例程中使用最简单的延时消抖。检测到按键按下后,一般进行10ms延时,用于跳过抖动的时间段,如果消抖效果不好可以调整这个10ms延时,因为不同类型的按键抖动时间可能有偏差。待延时过后再检测按键状态,如果没有按下,那我们就判断这是抖动或者干扰造成的;如果还是按下,那么我们就认为这是按键真的按下了。对按键释放的判断同理。
硬件消抖:利用RC电路的电容充放电特性来对抖动产生的电压毛刺进行平滑出来,从而实现消抖,但是成本会更高一点,本着能省则省的原则,我们推荐使用软件消抖即可。
15.1.2 GPIO端口输入数据寄存器(IDR)本实验我们将会用到GPIO端口输入数据寄存器,下面来介绍一下。
该寄存器用于存储GPIOx的输入状态,它连接到施密特触发器上,IO口外部的电平信号经过触发器后,模拟信号就被转化成0和1这样的数字信号,并存储到该寄存器中。寄存器描述如图15.1.2.1所示。 该寄存器低16位有效,分别对应每一组GPIO的16个引脚。当CPU访问该寄存器,如果对应的某位为0(IDRy=0),则说明该IO口输入的是低电平,如果是1(IDRy=1),则表示输入的是高电平,y=0~15。
15.2 硬件设计
1. 例程功能通过开发板上的四个独立按键控制LED灯:KEY0控制LED0翻转,KEY1控制LED1翻转,KEY2控制LED0、LED1同时翻转,KEY_UP控制蜂鸣器翻转。
2. 硬件资源1)LED灯 LED0 – PF9 LED1 – PF10 2)独立按键 KEY0 – PE4 KEY1 – PE3 KEY2 – PE2 KEY_UP – PA0(程序中的宏名:WK_UP) 3)蜂鸣器 BEEP – PF8
3. 原理图独立按键硬件部分的原理图,如图15.2.1所示: 图15.2.1 独立按键与STM32F407连接原理图 这里需要注意的是:KEY0、KEY1和KEY2设计为采样到按键另一端的低电平为有效电平,而KEY_UP则需要采样到高电平才为按键有效,并且按键外部没有上下拉电阻,所以需要在STM32F407内部设置上下拉。
15.3 程序设计
15.3.1 HAL_GPIO_ReadPin函数HAL_GPIO_ReadPin函数是GPIO口的读引脚函数。其声明如下: - GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
复制代码l 函数描述: 用于读取GPIO引脚状态,通过IDR寄存器读取。
l 函数形参: 形参1是端口号,可以选择范围:GPIOA~GPIOG。 形参2是引脚号,可以选择范围:GPIO_PIN_0到GPIO_PIN_15。
l 函数返回值: 引脚状态值0或者1
GPIO输入配置步骤 1)使能对应GPIO时钟 本实验按键用到PA0和PE2/3/4四个IO口,因此需要先使能GPIOA和GPIOE的时钟,代码如下: - __HAL_RCC_GPIOA_CLK_ENABLE();
- __HAL_RCC_GPIOE_CLK_ENABLE();
复制代码2)设置对应GPIO工作模式(上拉/下拉输入) 本实验GPIO使用输入模式(带上拉/下拉),从而可以读取IO口的状态,实现按键检测,GPIO模式通过函数HAL_GPIO_Init设置实现。
3)读取GPIO引脚高低电平 在配置好GPIO工作模式后,我们就可以通过HAL_GPIO_ReadPin函数读取GPIO引脚的高低电平,从而实现按键检测了。
15.3.2 程序流程图我们为各个按键定义键值,下面看看本实验的程序流程图: 15.3.3 程序解析
1. 按键驱动代码这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。按键(KEY)驱动源码包括两个文件:key.c和key.h。
下面我们先解析key.h的程序,我们把它分两部分功能进行讲解。
l 按键引脚定义 由硬件设计小节,我们知道KEY0、KEY1、KEY2和KEY_UP分别来连接到PE4、PE3、PE2和PA0上,我们做了下面的引脚定义。 - /* 引脚 定义 */
- #defineKEY0_GPIO_PORT GPIOE
- #defineKEY0_GPIO_PIN GPIO_PIN_4
- /* PE口时钟使能 */
- #define KEY0_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
- #defineKEY1_GPIO_PORT GPIOE
- #defineKEY1_GPIO_PIN GPIO_PIN_3
- /* PE口时钟使能 */
- #define KEY1_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
- #defineKEY2_GPIO_PORT GPIOE
- #defineKEY2_GPIO_PIN GPIO_PIN_2
- /* PE口时钟使能 */
- #define KEY2_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
- #define WKUP_GPIO_PORT GPIOA
- #define WKUP_GPIO_PIN GPIO_PIN_0
- /* PA口时钟使能 */
- #define WKUP_GPIO_CLK_ENABLE() do{__HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
复制代码 l 按键操作函数定义 为了后续对按键进行便捷的操作,我们为按键操作函数做了下面的定义。 - #define KEY0 HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN) /* 读取KEY0引脚 */
- #define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT,KEY1_GPIO_PIN) /* 读取KEY1引脚 */
- #define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_PORT,KEY2_GPIO_PIN) /* 读取KEY2引脚 */
- #define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT,WKUP_GPIO_PIN) /* 读取WKUP引脚 */
- #defineKEY0_PRES 1 /* KEY0按下 */
- #defineKEY1_PRES 2 /* KEY1按下 */
- #defineKEY2_PRES 3 /* KEY2按下 */
复制代码 KEY0、KEY1、KEY2和WK_UP分别是读取对应按键状态的宏定义。用HAL_GPIO_ReadPin函数实现,该函数的返回值就是IO口的状态,返回值是枚举类型,取值0或者1。
KEY0_PRES、KEY1_PRES、KEY2_PRES和WKUP_PRES则是按键对应的四个键值宏定义标识符。
下面我们再解析key.c的程序,这里有两个函数,先看按键初始化函数,其定义如下: - /**
- * @brief 按键初始化函数
- * @param 无
- * @retval 无
- */
- void key_init(void)
- {
- GPIO_InitTypeDef gpio_init_struct; /* GPIO配置参数存储变量 */
- KEY0_GPIO_CLK_ENABLE(); /* KEY0时钟使能 */
- KEY1_GPIO_CLK_ENABLE(); /* KEY1时钟使能 */
- KEY2_GPIO_CLK_ENABLE(); /* KEY2时钟使能 */
- WKUP_GPIO_CLK_ENABLE(); /* WKUP时钟使能 */
- gpio_init_struct.Pin = KEY0_GPIO_PIN; /* KEY0引脚 */
- gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
- gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
- gpio_init_struct.Speed =GPIO_SPEED_FREQ_HIGH; /* 高速 */
- HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_init_struct); /* KEY0引脚模式设置 */
- gpio_init_struct.Pin = KEY1_GPIO_PIN; /* KEY1引脚 */
- gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
- gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
- gpio_init_struct.Speed =GPIO_SPEED_FREQ_HIGH; /* 高速 */
- HAL_GPIO_Init(KEY1_GPIO_PORT, &gpio_init_struct); /* KEY1引脚模式设置 */
- gpio_init_struct.Pin = KEY2_GPIO_PIN; /* KEY2引脚 */
- gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
- gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
- gpio_init_struct.Speed =GPIO_SPEED_FREQ_HIGH; /* 高速 */
- HAL_GPIO_Init(KEY2_GPIO_PORT, &gpio_init_struct); /* KEY2引脚模式设置 */
- gpio_init_struct.Pin = WKUP_GPIO_PIN; /* WKUP引脚 */
- gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
- gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
- gpio_init_struct.Speed =GPIO_SPEED_FREQ_HIGH; /* 高速 */
- HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct); /* WKUP引脚模式设置*/
- }
复制代码这里需要注意的是:KEY0、KEY1和KEY2是低电平有效的(即一端接地),所以我们要设置为内部上拉,而KEY_UP是高电平有效的(即一端接电源),所以我们要设置为内部下拉。
另一个函数是按键扫描函数,其定义如下: - /**
- * @brief 按键扫描函数
- * @note 该函数有响应优先级(同时按下多个按键): WKUP > KEY2 > KEY1 > KEY0!!
- * @param mode:0 / 1, 具体含义如下:
- * @arg 0, 不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,
- * 必须松开以后, 再次按下才会返回其他键值)
- * @arg 1, 支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
- * @retval 键值, 定义如下:
- * KEY0_PRES, 1, KEY0按下
- * KEY1_PRES, 2, KEY1按下
- * KEY2_PRES, 3, KEY2按下
- * WKUP_PRES, 4, WKUP按下
- */
- uint8_t key_scan(uint8_t mode)
- {
- static uint8_t key_up = 1; /* 按键按松开标志 */
- uint8_t keyval = 0;
- if (mode) key_up = 1; /* 支持连按 */
- if (key_up && (KEY0 == 0 || KEY1 == 0 || KEY2 == 0 || WK_UP == 1))
- { /* 按键松开标志为1, 且有任意一个按键按下了 */
- delay_ms(10); /* 去抖动 */
- key_up = 0;
- if (KEY0 == 0) keyval = KEY0_PRES;
- if (KEY1 == 0) keyval = KEY1_PRES;
- if (KEY2 == 0) keyval = KEY2_PRES;
- if (WK_UP == 1) keyval = WKUP_PRES;
- }
- else if (KEY0 == 1 && KEY1 == 1 && KEY2 == 1 && WK_UP == 0)
- { /* 没有任何按键按下, 标记按键松开 */
- key_up = 1;
- }
- return keyval; /* 返回键值 */
- }
复制代码key_scan函数用于扫描这4个IO口是否有按键按下。key_scan函数,支持两种扫描方式,通过mode参数来设置。
当mode为0的时候,key_scan函数将不支持连续按,扫描某个按键,该按键按下之后必须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次触发,而坏处就是在需要长按的时候比较不合适。
当mode为1的时候,key_scan函数将支持连续按,如果某个按键一直按下,则会一直返回这个按键的键值,这样可以方便的实现长按检测。
有了mode这个参数,大家就可以根据自己的需要,选择不同的方式。这里要提醒大家,因为该函数里面有static变量,所以该函数不是一个可重入函数,在有OS的情况下,这个大家要留意下。可以看到该函数的消抖延时是10ms。同时还有一点要注意的是,该函数的按键扫描是有优先级的,最优先的是KEY_UP,第二优先的是KEY2,第三优先的是KEY1,最后是按键KEY0。该函数有返回值,如果有按键按下,则返回非0值,如果没有或者按键不正确,则返回0。
2. main.c代码在main.c里面编写如下代码: - int main(void)
- {
- uint8_t key;
-
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- led_init(); /* 初始化LED */
- beep_init(); /* 初始化蜂鸣器 */
- key_init(); /* 初始化按键 */
- LED0(0); /* 先点亮红灯 */
- while(1)
- {
- key = key_scan(0); /* 得到键值 */
- if (key)
- {
- switch (key)
- {
- case WKUP_PRES: /* 控制蜂鸣器 */
- BEEP_TOGGLE(); /* BEEP状态取反 */
- break;
- case KEY0_PRES: /* 控制LED0(RED)翻转 */
- LED0_TOGGLE(); /* LED0状态取反 */
- break;
- case KEY1_PRES: /* 控制LED1(GREEN)翻转 */
- LED1_TOGGLE(); /* LED1状态取反 */
- break;
- case KEY2_PRES: /* 同时控制LED0, LED1翻转 */
- LED0_TOGGLE(); /* LED0状态取反 */
- LED1_TOGGLE(); /* LED1状态取反 */
- break;
- default : break;
- }
- }
- else
- {
- delay_ms(10);
- }
- }
- }
复制代码首先是调用系统级别的初始化:初始化 HAL库、系统时钟和延时函数。接下来,调用led_init来初始化LED灯,调用beep_init函数初始化蜂鸣器,调用key_init函数初始化按键。最后在无限循环里面扫描获取键值,接着用键值判断哪个按键按下,如果有按键按下则翻转相应的灯或翻转蜂鸣器,如果没有按键按下则延时10ms。
15.4 下载验证在下载好程序后,我们可以按KEY0、KEY1、KEY2来看看LED灯的变化或者按KEY_UP看看蜂鸣器的变化,是否和我们预期的结果一致?
至此,我们的本章的学习就结束了。本章学习了STM32F407的IO作为输入的使用方法,在前面的GPIO输出的基础上又学习了一种GPIO使用模式,大家可以回顾前面跑马灯实验介绍的GPIO的八种模式类型巩固GPIO的知识。 |