从单按键状态机思维扫描引申到4*4矩阵按键全键无冲扫描,一步一步教,超好理解,超好复现(STM32程序例子HAL库)

目前大部分代码存在的问题

​ 单次只能对单个按键产生反应;多个按键按下就难以修改;并且代码耦合度较高,逻辑难以修改,对于添加长按,短按,双击的需求修改困难。

解决

16个按键按下无冲,并且代码简单,使用状态机思想。修改及其简单。

就算需要修改为8*8的键盘也修改的代码不会超过5行

示范开发板:STM32F103C8T6

单按键扫描思路讲解;

我们先讲解我的单按键的扫描思路;这个理解后,上手矩阵就会非常简单;

按键接线:

按键接入PA0引脚,按下时电平为低

在这里插入图片描述

首先会设置两个三个变量,分别是按键电平状态,按键状态,和按键按下标志位

对应代码:

    _Bool key_level;            //按键当前电平unsigned char key_state;    //按键状态_Bool once_downflag;        //按键按下标志位

按键电平:负责表示当前按键实时电平,(1或0)

按键状态:负责表示当前按键的处于状态,(待按下,消抖判断,待松开)

按下标志位,如果确定按键按下,此标志位就会至1;

扫描思路:首先我们会建立一个函数,函数负责单按键的扫描,假如接的按键是PA0,按下时电平为0.

注:默认这个按键扫描为20ms调用一次,下同

//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{}

电平读取

首先,读取电平,将PA0引脚电平赋值到key_level;变量

//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平}

然后会进入状态机,状态机负责判断这个读取的电平进行按键按下和状态转换

状态机判断

那么对应状态机的第一个状态:按键按下判断,

这里使用switch会加快运行速度,使用if也可以。

按键状态变量key_state默认是0,那么我们就以0状态为待按下状态

首先是判断按键是否按下,如果按下(电平为0)就改变状态为消抖判断

下面是代码

//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平switch(key_state)//状态机判断按键状态{case 0://待按下状态if(key_level == 0)//如果检测到按键按下{key_state = 1;//改变状态到消抖判断}break;case 1:break;case 2:break;}
}

20ms后再次进入本函数,但是状态为1(消抖判断状态)

这个状态负责判断本次按下是否为真实按下,

如果是,就将按键按下标志位置为1供外部读取,并且改变状态,为待松开

如果不是真实按下,则恢复状态为状态0;

(注意,进入到状态1前已经进行了20ms的延迟了,即按键消抖。所以状态机1就只需判断按键是否真实按下)

代码如下:

//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平switch(key_state)//状态机判断按键状态{case 0://待按下状态if(key_level == 0)//如果检测到按键按下{key_state = 1;//改变状态到消抖判断}break;case 1://消抖判断if(key_level == 1)//如果电平为1,即松开,恢复状态机为0{key_state = 0;}else if(key_level == 0)//如果电平为0 ,代码按键确定按下,则按下标志位置1,状态机改为待松开状态{once_downflag = 1;key_state = 2;}break;case 2:break;}
}

20ms后再次进入本函数,但是状态为2(假如上次状态确认为按下)

那么本状态就仅仅需要负责,当按键松开后恢复状态机即可

代码如下:

//此为单按键扫描,扫描接到PA0上面的按键
void Key_Scan(void)
{key_level = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//读取按键电平switch(key_state)//状态机判断按键状态{case 0://待按下状态if(key_level == 0)//如果检测到按键按下{key_state = 1;//改变状态到消抖判断}break;case 1://消抖判断if(key_level == 1)//如果电平为1,即松开,恢复状态机为0{key_state = 0;}else if(key_level == 0)//如果电平为0 ,代码按键确定按下,则按下标志位置1,状态机改为待松开状态{once_downflag = 1;key_state = 2;}break;case 2://待松开if(key_level == 1)//按键松开了,则恢复状态机{key_state = 0;}break;}
}

之后在主函数内部读取按下标志位皆可进行对应的按键操作了。

如:

if(once_downflag == 1)//按键标志位为1{key_struct[0].once_downflag =0 ;//清除标志位//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);//取反灯}

这就是单按键扫描的状态机思路

矩阵按键扫描思路讲解

如果你理解了单按键,你就会发现,一个按键有自己对应的电平变量,状态变量,和标志位变量

那么16个按键呢?

实物以及接线:

首先是我使用的矩阵键盘以及接线

R1——PA0
R2——PA1
R3——PA2
R4——PA3

C1——PA4
C2——PA5
C3——PA6
C4——PA7

img

img点击并拖拽以移动

那就是16个按键都有自己对应的电平变量,状态变量,和标志位变量;

由于16个按键一一设置对应的变量太麻烦了,我加入结构体的使用

结构体定义如下

typedef struct Key_Struct    //按键结构体
{_Bool key_level;    //按键电平unsigned char key_state;    //按键状态_Bool once_downflag;    //按键按下标志位
} Key_Struct; 

然后我们定义好16个按键的变量

Key_Struct key_struct[16]; //16个按键结构体

好接下来到代码层次:

我们如何获取每个按键的电平呢?

一个一个扫描怎么样,先扫描KEY1,然后KEY2,然后KEY3,KEY4,KEY5.。。。。等

从左到右,从上到下。(既行列扫描)

再看图

img

思路:

第一行扫描:我们把PA0置低,PA1,2,3都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量,

第二行扫描:我们把PA1置低,PA0,2,3都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量。

第三行扫描:我们把PA2置低,PA0,1,3都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量。

第四行扫描:我们把PA3置低,PA0,1,2都置高。再分别读取PA4、5、6、7的输入,将读取的值分别赋到其对应的电平变量。

如上做一个循环,就是完整的16个按键的电平扫描;

同样:先创建一个按键扫描函数:

void Key_Scan(void)
{}

那么这个函数多久调用一次呢。

我的思路是1ms调用一次,因为每次进入到这个函数就只会进行一个按键扫描。那么扫描16个按键需要16次。所以如果1ms调用一次函数,那么同一个按键的两次扫描间隔就是16ms。也是符合按下消抖的条件的。

下面的为16个按键选取的扫描位置下标 i变量:

void Key_Scan(void)
{static unsigned char i;//静态变量i,表示当前扫描第几个按键if(++i > 15) i=0;//本次扫描结束后切换到下一个按键,或者全部扫描完后从头开始
}

电平读取

好,读取16个按键电平(代码行数很少,但是需要一定的C语言和单片机基础)

void Key_Scan(void)
{static unsigned char i;//静态变量i,表示当前扫描第几个按键//行选HAL_GPIO_WritePin(GPIOA,0x0F,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);//列选key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));if(++i >= 16) i=0;//本次扫描结束后切换到下一个按键,或者全部扫描完后从头开始
}

思路:

行选:
HAL_GPIO_WritePin(GPIOA,0x0F,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);

首先第一个句子先将PA0到PA3引脚全部置高,传入的0x0F对应的寄存器低四位

第二个句子就是根据当前的 i 按键下标变量判断扫描第几行。

假如i=0,即第1个按键,第1行,对应PA0

假如i=4,即第5个按键,第2行,对应PA1

假如i=15,即第16个按键,第4行,对应PA3

发现一个计算公式

PA(x) :x = (i/4)

x取值向下取整;

对应代码:

HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);

将0x01左移需要的行数就是对应的引脚了;

列选:

key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));

一样,要先根据i的值判断当前是第几列

假如i=0,即第1个按键,第1列,对应PA4

假如i=4,即第5个按键,第1列,对应PA4

假如i=10,即第11个按键,第3列,对应PA6

假如i=15,即第16个按键,第4列,对应PA7

发现一个计算公式

PA(x) :x = (i%4)+4;

+4是为了让偏移从PA4开始,因为从PA4开始才对应列选

对应代码:

key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));

这样,16个按键的电平读取就完成了,1ms调用一次函数,每16次为一次完整的周期。

也是本矩阵扫描最难的部分。

状态机判断:

跟单按键一样,三个状态,(待按下,消抖判断,待松开)

不分步讲解了,直接上代码;

//矩阵按键扫描函数,1ms调用一次
void Key_Scan(void)
{static unsigned char i;//静态变量i,表示当前扫描第几个按键//行选HAL_GPIO_WritePin(GPIOA,0x0F,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOA,0x01<<(unsigned char)(i/4),GPIO_PIN_RESET);//列选key_struct[i].key_level = HAL_GPIO_ReadPin(GPIOA,(0x01<<((i%4)+4)));//电平赋值//状态机判断switch(key_struct[i].key_state){case 0:	//待按下if(key_struct[i].key_level == 0){key_struct[i].key_state = 1;}break;case 1:	//消抖判断if(key_struct[i].key_level == 1){key_struct[i].key_state = 0;}else if(key_struct[i].key_level == 0){key_struct[i].once_downflag = 1;key_struct[i].key_state = 2;}break;case 2://待松开if(key_struct[i].key_level == 1){key_struct[i].key_state = 0;}break;}if(++i >= 16) i=0;//本次扫描结束后切换到下一个按键,或者全部扫描完后从头开始
}

之后在主函数内部读取按下标志位皆可进行对应的按键操作了。

如:

void KEY_Process(void)
{if(key_struct[0].once_downflag == 1){key_struct[0].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[1].once_downflag == 1){key_struct[1].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[2].once_downflag == 1){key_struct[2].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[3].once_downflag == 1){key_struct[3].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[4].once_downflag == 1){key_struct[4].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[5].once_downflag == 1){key_struct[5].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[6].once_downflag == 1){key_struct[6].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[7].once_downflag == 1){key_struct[7].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[8].once_downflag == 1){key_struct[8].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[9].once_downflag == 1){key_struct[9].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13); }if(key_struct[10].once_downflag == 1){key_struct[10].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[11].once_downflag == 1){key_struct[11].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[12].once_downflag == 1){key_struct[12].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[13].once_downflag == 1){key_struct[13].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[14].once_downflag == 1){key_struct[14].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}if(key_struct[15].once_downflag == 1){key_struct[15].once_downflag =0 ;//HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);}}

这个就是任意一个按键按键就取反灯

以上就是按键扫描的全部了

如果你需要按键的单双击,长按判断,可以参考下面这个文章,扫描的思路是一样的。

http://t.csdnimg.cn/AUQOA
如果你觉得写的不错,希望点赞加收藏,这是给我最大的称赞

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

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

相关文章

如何在CentOS本地搭建DataEase数据分析服务并实现远程查看数据分析

文章目录 前言1. 安装DataEase2. 本地访问测试3. 安装 cpolar内网穿透软件4. 配置DataEase公网访问地址5. 公网远程访问Data Ease6. 固定Data Ease公网地址 前言 DataEase 是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务…

【项目分享】用 Python 写一个桌面倒计日程序!

事情是这样的&#xff0c;我们班主任想委托我做一个程序&#xff0c;能显示还有几天考试。我立即理解了这个意思&#xff0c;接下了这个项目。 话不多说&#xff0c;来看看这个项目吧—— 项目简介 仓库地址&#xff1a;https://gitee.com/yaoqx/desktop-countdown-day 这是 …

幻兽帕鲁中文怎么设置 游戏中文修改方法 《幻兽帕鲁》宠物指定配种显示英文解决方法 幻兽帕鲁Steam游戏解说合集 Mac玩Windows游戏

在广阔的世界中收集神奇的生物“帕鲁”&#xff0c;派他们进行战斗、建造、做农活&#xff0c;工业生产等&#xff0c;这是一款支持多人游戏模式的全新开放世界生存制作游戏。幻兽帕鲁支持多人在线捕捉“帕鲁”&#xff0c;展开丰富的冒险玩法&#xff1b;不同的关卡具有不同的…

Bellman Ford算法:解决负权边图的最短路径问题

Bellman Ford算法的介绍 在计算机科学的世界中&#xff0c;Bellman Ford算法是一种解决单源最短路径问题的算法&#xff0c;它可以处理有负权边的图。这个算法的名字来源于两位科学家Richard Bellman和Lester Randolph Ford&#xff0c;他们是这个算法的发明者。 这个算法的主…

AI图书推荐:2024年ChatGPT副业搞钱指南

本书《2024年ChatGPT副业搞钱指南》&#xff08;ChatGPT Side Hustles 2024&#xff09;由Alec Rowe撰写&#xff0c;旨在指导读者如何利用ChatGPT技术来提升被动收入、创造新的现金流&#xff0c;并在数字化时代保持领先。 本书是深入了解被动收入未来的综合指南。本书揭示了超…

【算法基础实验】图论-基于DFS的连通性检测

基于DFS的连通性检测 理论基础 在图论中&#xff0c;连通分量是无向图的一个重要概念&#xff0c;特别是在处理图的结构和解析图的组成时。连通分组件表示图中的一个子图&#xff0c;在这个子图中任意两个顶点都是连通的&#xff0c;即存在一条路径可以从一个顶点到达另一个顶…

Flutter应用下拉菜单设计DropdownButtonFormField控件介绍

文章目录 DropdownButtonFormField介绍使用方法重点代码说明属性解释 注意事项 DropdownButtonFormField介绍 Flutter 中的 DropdownButtonFormField 是一个用于在表单中选择下拉菜单的控件。它是 DropdownButton 和 TextFormField 的组合&#xff0c;允许用户从一组选项中选择…

井字棋游戏

1. 游戏创建 1.1导包 from tkinter import * import numpy as np import math import tkinter.messagebox 1.2 窗口内容 1.2.1创建一个窗口 root Tk() # 窗口名称 root.title("井字棋 from Sun") 1.2.2 创建一个框架&#xff0c;将其放置在窗口中 Frame1 F…

汽车底盘域的学习笔记

前言&#xff1a;底盘域分为传统车型底盘域和新能源车型底盘域&#xff08;新能源系统又可以分为纯电和混动车型&#xff0c;有时间可以再研究一下&#xff09; 1&#xff1a;传统车型底盘域 细分的话可以分为四个子系统 传动系统 行驶系统 转向系统 制动系统 1.1传动系…

什么样的内外网文档摆渡,可以实现安全高效传输?

内外网文档摆渡通常指的是在内网&#xff08;公司或组织的内部网络&#xff09;和外网&#xff08;如互联网&#xff09;之间安全地传输文件的过程。这个过程需要特别注意安全性&#xff0c;因为内网往往包含敏感数据&#xff0c;直接连接内网和外网可能会带来安全风险。因此会…

设计模式——终止模式之两阶段终止模式

文章目录 1. 错误思路2. 两阶段终止模式2.1 利用 isInterrupted2.2 利用停止标记interrupt-打断park Two Phase Termination 在一个线程 T1 中如何“优雅”终止线程 T2&#xff1f;这里的【优雅】指的是给 T2 一个料理后事的机会。 1. 错误思路 使用线程对象的 stop() 方法停…

GEM TSU Interface Details and IEEE 1588 Support

摘要&#xff1a;Xilinx ZNYQ ULTRASCALE MPSOC的GEM和1588的使用 对于FPGA来说&#xff0c;只需要勾选一些znyq的配置就行了&#xff0c;其余的都是软件的工作&#xff1b; 所有配置都勾选之后&#xff0c;最终会露出来的接口如下&#xff1a; GEM需要勾选的配置如下&#xf…

15.Blender Eevee和Cycles渲染引擎对比

初步介绍 Eevee是实时渲染的引擎&#xff0c;会省略一些解算方式&#xff0c;尤其对光线和阴影 Cycles会考虑这些因素&#xff0c;所以会对光线和阴影的表达更加真实&#xff0c;有一个实时光线追踪的功能 Cycles渲染完之后&#xff0c;每移动一次画面&#xff0c;都会重新渲染…

政安晨:【Keras机器学习示例演绎】(十九)—— 可视化网络学习内容

目录 简介 设置 建立特征提取模型 设置梯度上升过程 设置端到端滤波器可视化回路 可视化目标层中的前 64 个滤波器 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras机器学习实战 希望政安晨的博客能够对您有所裨益&…

基于Rust的多线程 Web 服务器

构建多线程 Web 服务器 在 socket 上监听 TCP 连接解析少量的 HTTP 请求创建一个合适的 HTTP 响应使用线程池改进服务器的吞吐量优雅的停机和清理注意&#xff1a;并不是最佳实践 创建项目 ~/rust ➜ cargo new helloCreated binary (application) hello package~/rust ➜ma…

kaggle之皮肤癌数据的深度学习测试

kaggle之皮肤癌数据的深度学习测试 近期一直在肝深度学习 很久之前&#xff0c;曾经上手搞过一段时间的深度学习&#xff0c;似乎是做轮胎花纹的识别&#xff0c;当初用的是TensorFlow&#xff0c;CPU版本的&#xff0c;但已经很长时间都没弄过了 现在因为各种原因&#xff…

全面解析平台工程与 DevOps 的区别与联系

平台工程的概念非常流行&#xff0c;但很多开发人员仍然不清楚它是如何实际运作的&#xff0c;这是非常正常的。 平台工程是与 DevOps 并行吗&#xff1f;还是可以相互替代&#xff1f;或者 DevOps 和平台工程是两个完全不同的概念&#xff1f; 一种比较容易将两者区分开来的方…

打包的意义 作用等前端概念集合 webpack基础配置等

基础网页是什么&#xff1f; 在学校最基础的三剑客 原生JS CSS H5就可以开发静态网页了 对于浏览器而言也能识别这些基础的文件和语法&#xff0c;真正的所见即所得&#xff0c;非常直接。 为什么要使用框架库&#xff1f; 对于常用的前端框架而言&#xff0c;无论是Vue Rea…

普通屏幕已过时?裸眼3D屏幕显示效果更胜一筹!

随着多媒体技术的迅猛进步&#xff0c;我们日常生活中的内容展现方式&#xff0c;已经经历了前所未有的变革。在这其中&#xff0c;裸眼3D屏幕的应用&#xff0c;无疑是最为引人注目的亮点&#xff0c;它相较于传统屏幕&#xff0c;在显示效果上展现出了鲜明的优势&#xff0c;…

RGB灯珠的控制-单片机通用模板

RGB灯珠的控制-单片机通用模板 一、RGB控制的原理二、RGB.c的实现三、RGB.h的实现四、color色彩空间变换以及控制渐变一、RGB控制的原理 ①通过IO发送脉冲识别0/1编码,组合24Bit的RGB数据,从而控制RGB;②每个RGB灯珠通过DIN、DOU进行级联起来;③通过HSV色彩转换成RGB从而控…