【正点原子STM32连载】 第十五章 按键输入实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

news2024/11/19 9:23:42

1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第十五章 按键输入实验

上一章,我们介绍了STM32F1的IO口作为输出的使用。本章,我们将向大家介绍如何使用STM32F1的IO口作为输入。我们将利用板载的3个按键,来控制板载的两个LED灯亮灭。通过本章的学习,我们将了解到STM32F1的IO口作为输入的使用方法。
本章分为如下几个小节
15.1 按键与输入数据寄存器
15.2 硬件设计
15.3 程序设计
15.4 下载验证

15.1 按键与输入数据寄存器简介

15.1.1 独立按键简介
几乎每个开发板都会板载有独立按键,因为按键用处很多。常态下,独立按键是断开的,按下的时候才闭合。每个独立按键会单独占用一个IO口,通过IO口的高低电平判断按键的状态。但是按键在闭合和断开的时候,都存在抖动现象,即按键在闭合时不会马上就稳定的连接,断开时也不会马上断开。这是机械触点,无法避免。独立按键抖动波形图如下:
在这里插入图片描述

图15.1.1.1 独立按键抖动波形图
图中的按下抖动和释放抖动的时间一般为5~10ms,如果在抖动阶段采样,其不稳定状态可能出现一次按键动作被认为是多次按下的情况。为了避免抖动可能带来的误操作,我们要做的措施就是给按键消抖(即采样稳定闭合阶段)。消抖方法分为硬件消抖和软件消抖,我们常用软件的方法消抖。
软件消抖:方法很多,我们例程中使用最简单的延时消抖。检测到按键按下后,一般进行10ms延时,用于跳过抖动的时间段,如果消抖效果不好可以调整这个10ms延时,因为不同类型的按键抖动时间可能有偏差。待延时过后再检测按键状态,如果没有按下,那我们就判断这是抖动或者干扰造成的;如果还是按下,那么我们就认为这是按键真的按下了。对按键释放的判断同理。
硬件消抖:利用RC电路的电容充放电特性来对抖动产生的电压毛刺进行平滑出来,从而实现消抖,但是成本会更高一点,本着能省则省的原则,我们推荐使用软件消抖即可。
15.1.2 GPIO端口输入数据寄存器(IDR)
本实验我们将会用到GPIO端口输入数据寄存器,下面来介绍一下。
该寄存器用于存储GPIOx的输入状态,它连接到施密特触发器上,IO口外部的电平信号经过触发器后,模拟信号就被转化成0和1这样的数字信号,并存储到该寄存器中。寄存器描述如图15.1.2.1所示。
在这里插入图片描述

图15.1.2.1 GPIOx IDR寄存器描述
该寄存器低16位有效,分别对应每一组GPIO的16个引脚。当CPU访问该寄存器,如果对应的某位为0(IDRy=0),则说明该IO口输入的是低电平,如果是1(IDRy=1),则表示输入的是高电平,y=0~15。
15.2 硬件设计

  1. 例程功能
    通过开发板上的三个独立按键控制LED灯: KEY_UP控制蜂鸣器翻转,KEY1控制LED1翻转,KEY2控制LED0翻转,KEY0控制LED0/LED1同时翻转。
  2. 硬件资源
    1)LED灯
    LED0 – PB5
    LED1 – PE5
    2)独立按键
    KEY0 – PE4
    KEY1 – PE3
    KEY2 – PE2
    KEY_UP – PA0
  3. 原理图
    独立按键硬件部分的原理图,如图15.2.1所示:
    在这里插入图片描述

图15.2.1 独立按键与STM32F1连接原理图
这里需要注意的是:KEY0、KEY1和KEY2是低电平有效的,而KEY_UP则是高电平有效的,并且外部都没有上下拉电阻,所以需要在STM32F103内部设置上下拉,来确定设置空闲电平状态。
15.3 程序设计
15.3.1 HAL_GPIO_ReadPin函数
HAL_GPIO_ReadPin函数是GPIO口的读引脚函数。其声明如下:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
函数描述:
用于读取GPIO引脚状态,通过IDR寄存器读取。
函数形参:
形参1是端口号,可以选择范围:GPIOA~GPIOG。
形参2是引脚号,可以选择范围:GPIO_PIN_0到 GPIO_PIN_15。
函数返回值:
引脚状态值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.2.1 按键输入实验程序流程图
15.3.3 程序解析

  1. 按键驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。按键(KEY)驱动源码包括两个文件:key.c和key.h。
    下面我们先解析key.h的程序,我们把它分两部分功能进行讲解。
     按键引脚定义
    由硬件设计小节,我们知道KEY0、KEY1、KEY2和KEY_UP分别来连接到PE4、PE3、PE2和PA0上,我们做了下面的引脚定义。
/* 引脚 定义 */
#define KEY0_GPIO_PORT           	GPIOE
#define KEY0_GPIO_PIN             	GPIO_PIN_4
/* PE口时钟使能 */
#define KEY0_GPIO_CLK_ENABLE()   	do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   

#define KEY1_GPIO_PORT              	GPIOE
#define KEY1_GPIO_PIN               	GPIO_PIN_3
/* PE口时钟使能 */
#define KEY1_GPIO_CLK_ENABLE()    	do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)

#define KEY2_GPIO_PORT              	GPIOE
#define KEY2_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)   
  按键操作函数定义
为了后续对按键进行便捷的操作,我们为按键操作函数做了下面的定义。
#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引脚 */

#define KEY0_PRES    1              /* KEY0按下 */
#define KEY1_PRES    2              /* KEY1按下 */
#define KEY2_PRES    3              /* KEY2按下 */
#define WKUP_PRES    4              /* KEY_UP按下(即WK_UP) */
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是低电平有效的(即一端接地),所以我们要设置为内部上拉,而KEY_UP是高电平有效的(即一端接电源),所以我们要设置为内部下拉。
另一个函数是按键扫描函数,其定义如下:

/**
 * @brief       按键扫描函数
 * @note        该函数有响应优先级(同时按下多个按键): WK_UP > 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,第二优先的是KEY0,最后是按键KEY2。该函数有返回值,如果有按键按下,则返回非0值,如果没有或者按键不正确,则返回0。
2. main.c代码
在main.c里面编写如下代码:

int main(void)
{
    uint8_t key; 

    HAL_Init();                             	/* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9);	/* 设置时钟, 72Mhz */
    delay_init(72);                       	/* 延时初始化 */
    led_init();                            	/* 初始化LED */
    beep_init();                            /* 初始化蜂鸣器 */
    key_init();                           	/* 初始化按键 */
    LED0(0);                              	/* 先点亮LED0 */
    
    while(1)
    {
        key = key_scan(0);               	/* 得到键值 */
        if (key)
        {
            switch (key)
            {
                case WKUP_PRES:          	/* 控制蜂鸣器 */
                    BEEP_TOGGLE();       	/* BEEP状态取反 */
                    break;
                case KEY2_PRES:           	/* 控制LED0(RED)翻转 */
                    LED0_TOGGLE();       	/* LED0状态取反 */
                    break;
                case KEY1_PRES:          	/* 控制LED1(GREEN)翻转 */
                    LED1_TOGGLE();       	/* LED1状态取反 */
                    break;
                case KEY0_PRES:          	/* 同时控制LED0, LED1翻转 */
                    LED0_TOGGLE();          /* LED0状态取反 */
                    LED1_TOGGLE();          /* LED1状态取反 */
                    break;
            } 
        }
        else
        {
            delay_ms(10);
        }
    }
}

首先是调用系统级别的初始化:初始化 HAL库、系统时钟和延时函数。接下来,调用led_init来初始化LED灯,调用key_init函数初始化按键。最后在无限循环里面扫描获取键值,接着用键值判断哪个按键按下,如果有按键按下则翻转相应的灯,如果没有按键按下则延时10ms。
15.4 下载验证
在下载好程序后,我们可以按KEY0、KEY1、KEY2和KEY_UP来看看LED灯的变化,是否和我们预期的结果一致?
至此,我们的本章的学习就结束了。本章学习了STM32F103的IO作为输入的使用方法,在前面的GPIO输出的基础上又学习了一种GPIO使用模式,大家可以回顾前面跑马灯实验介绍的GPIO的八种模式类型巩固GPIO的知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/562884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue之MVVM模型

文章目录 前言一、简说MVVM模型二、走进MVVM总结 前言 Vue的创建者在创建Vue时没有完全遵守MVVM(一种软件架构模式),但是Vue的设计受到了他它的启发。这也是为什么经常用vm(ViewModel的缩写)这个变量名表示Vue实例。 …

服了呀,被现在的00后卷麻了....

现在的小年轻真的卷得过分了。前段时间我们公司来了个00年的,工作没两年,跳槽到我们公司起薪18K,都快接近我了。后来才知道人家是个卷王,从早干到晚就差搬张床到工位睡觉了。 最近和他聊了一次天,原来这位小老弟家里条…

docker创建emqx容器,emqx版本4.4.9

题记:网上找了很多文章,都没能怎么说明白如何对mqtt连接通信做认证,也就是tcp方式的时候,携带user、password,很苦恼,最后找了一大圈,说emqx的4.4.9版本的Dashboard有插件,果然顺藤摸…

vue动态class的写法

本文会详细介绍 vue动态 class的写法,并且提供一些我个人的理解,希望对你有所帮助。 如果你是一个新手,或者想了解 vue的源码,那么首先应该学习 vue的基础知识,比如:什么是静态语言,有什么作用等…

155. 最小栈

题目描述&#xff1a; 主要思路&#xff1a; 利用辅助栈来实现&#xff0c;一个最小栈用类似于单调栈的方法进行维护&#xff0c;具体见代码。 class MinStack { private:stack<int> x;stack<int> minx; public:MinStack() {minx.push(INT_MAX);}void push(int v…

IMX6ULL的I2C驱动详细分析

IMX6ULL的I2C驱动详细分析 文章目录 IMX6ULL的I2C驱动详细分析i2c_imx_driver 的平台驱动注册i2c_imx_probe注册函数i2c_imx_algoI2C算法结构体i2c_imx_start开始I2Ci2c_imx_stop停止I2Ci2c_imx_isr中断服务函数i2c_imx_dma_writeDMA 进行写操作的 I2C 传输2c_imx_dma_readi2c_…

动图怎么拆分成静图?简单快速分解gif的方法

怎么把gif动图变成静态图片&#xff1f;常见的gif动图画面生动丰富&#xff0c;是由一帧一帧静态的图片合成的&#xff0c;当我们想要把gif动图拆分成多张图片的时候要怎么操作呢&#xff1f;有没有什么简单方便的工具吗&#xff1f; 一、有没有简单方便的gif工具呢&#xff1…

Linux内存简介

Linux内存简介 概述 为何MemTotal小于RAM容量 [rootiZbp1dphe2bpv39op1g123Z ~]# dmesg | grep Memory [ 1.391064] Memory: 131604168K/134217136K available (14346K kernel code, 9546K rwdata, 9084K rodata, 2660K init, 7556K bss, 2612708K reserved, 0K cma-reserved) …

云原生改造- istio

istio 官网有bookinfo的案例&#xff0c; 但是这个案例过于繁琐&#xff0c;直接就可以运行&#xff0c;但是有些原理不是很清楚。 本教程是k8s改造成istio一个案例。spring-petclinic-msa是网上找到的一个k8s的一个java案例&#xff0c;通过spring-petclinic-msa改造成istio&a…

盘点团队在线帮助文档怎么做?

团队在线帮助文档是一个非常重要的工具&#xff0c;它可以帮助团队成员更好地协作和沟通&#xff0c;提高工作效率&#xff0c;并减少沟通成本。在本文中&#xff0c;我们将会盘点团队在线帮助文档的各个方面&#xff0c;以帮助您更好地了解如何创建一个高效的在线帮助文档。 …

6. python的for循环

文章目录 一、for循环1.1、for循环分析1.2、注意事项 二、遍历数值列表2.1、range()函数的使用2.2、 创建数值列表2.3、对数值列表进行简单统计 一、for循环 有时&#xff0c;我们需要对列表内的所有元素逐一进行相同的操作&#xff0c;为避免出现大量重复的代码&#xff0c;p…

js一行代码就能完成的事情,为什么要写两行

三元运算符 用三元运算符代替简单的if else if (age < 18) { me 小姐姐; } else { me 老阿姨; }改用三元运算符,一行就能搞定 me age < 18 ? 小姐姐 : 老阿姨;复杂的判断三元运算符就有点不简单易懂了 const you "董员外" const your "菜…

一周吃透Java面试八股文(2023最新整理

Java就业大环境仍然根基稳定&#xff0c;市场上有很多机会&#xff0c;技术好的人前景就好&#xff0c;就看你有多大本事了。小编得到了一份很不错的资源&#xff0c;建议大家可以认真地来看看以下的资料&#xff0c;来提升一下自己的核心竞争力&#xff0c;在面试中轻松应对面…

如何在WordPress页面上显示或隐藏小部件?

您想在 WordPress 网站的特定页面上显示或隐藏小部件吗&#xff1f; 默认情况下&#xff0c;当您将小部件添加到您的网站时&#xff0c;它们将显示在所有页面上。但是&#xff0c;有时您可能只想在某些页面上显示选定的小部件可以帮助您更好地自定义您的网站并提供个性化的用户…

如何在 Fedora 37 上安装 FileZilla?

FileZilla 是一款流行的开源 FTP&#xff08;文件传输协议&#xff09;客户端&#xff0c;它提供了一个直观的界面来管理和传输文件。本文将详细介绍如何在 Fedora 37 上安装 FileZilla。以下是安装过程的详细步骤&#xff1a; 步骤一&#xff1a;更新系统 在安装任何软件之前…

【大数据之Hive】一、Hive概念及框架原理

1 Hive概念 Hive是基于Hadoop的一个数据仓库工具&#xff0c;可以将结构化的数据文件映射为一张表&#xff0c;并提供类SQL查询功能&#xff0c;主要完成海量数据的分析和计算。 优点&#xff1a;简化数据开发流程及提高了效率。 2 Hive本质 Hive是一个Hadoop客户端&#xff…

【golang】2、http client、爬图

文章目录 一、http client爬取并存储 jpg 用 golang 可以很方便的爬图&#xff08;http 下载图片&#xff0c;存储为 jpg 格式&#xff09;。 一、http client http client 有如下最佳实践&#xff1a; 尽量用 default http client&#xff1a;默认的 http client 设置了很多…

排序算法——堆排序

把上面的序列变成一个完全二叉树&#xff0c;要想实现大顶堆&#xff08;大顶堆&#xff1a;叶子节点不考虑&#xff0c;每个节点都要比他的两个孩子节点要大&#xff09;&#xff0c;就要进行如下操作&#xff0c;你会发现len/2就是最后一个非叶子节点 第一步是从下往上调 9和…

Apache Pulsar部署搭建

1.部署规划 部署 Pulsar 集群包括以下步骤(按顺序)&#xff1a; 1.部署一个 ZooKeeper 集群&#xff0c;初始化 Pulsar 集群元数据。2.部署一个 Bookeeper 集群。3.部署一个或多个 Pulsar brokers。4.部署 Pulsar manager&#xff08;可选&#xff09;。 2.节点规划 主机名…

【C++】函数重载 - 给代码增添多彩的魔法

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、缘起 2、函数重载概述 3、函数重载注意事项 4、总结 1、缘起 函数重载&#xff0c;是编程世界中的一抹迷人色彩&#xff0c…