从单按键状态机思维扫描引申到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
如果你觉得写的不错,希望点赞加收藏,这是给我最大的称赞

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AIGC技术带来的安全与隐私问题探讨

如何看待AIGC技术&#xff1f; 简介&#xff1a;探讨AIGC技术的发展现状和未来趋势。提醒&#xff1a;在发布作品前&#xff0c;请把不需要的内容删掉。 方向一&#xff1a;技术应用 机遇和挑战 AIGC国内场景应用图谱 方向二&#xff1a;伦理与风险 垄断与隐私风险 AI民主化诉…

Linux--MyMiniTry--Vim

首先下载好vim,我们可以按以下的方式进行光标的移动&#xff08;也可以回车进行换行&#xff09; &#xff08;--> 进入教程&#xff09; &#xff08;初始的时候没有文本&#xff0c;你怎么按都没有用&#xff09; &#xff08;我们要先按 i &#xff0c;进行插入文本才…

前端单元测试的艺术:专业化策略与Vue项目高效实践指南

单元测试是软件开发中的基石&#xff0c;尤其对于前端领域&#xff0c;它为保证代码质量、提升开发效率、强化项目稳定性提供了不可或缺的支持。本文将深入剖析单元测试的核心理念&#xff0c;揭示其在前端开发中的独特价值&#xff0c;并提炼出一套专业且高效的实践策略&#…

全志ARM-官方库SDK安装和验证

进入界面&#xff0c;输入以下指令 git clone https://github.com/orangepi-xunlong/wiringOP //下载源码 cd wiringOP //进入文件夹 sudo ./build clean //清除编译信息 sudo ./build …

电容的理论基础

目录 1.电容的本质&#xff1a; 2.电容量的大小 2.1电容的单位 2.2电容的决定式 ​编辑3.电容的特点 5.电容器的类型 6.电容实际的电路模型 7.安装方法 ​编辑8.电容值 9.电容的耐压、封装 10.阻抗-频率特性 11.频率特性 12.等效串联电组ESR 13.电容器的温度特性…

[C++基础学习]----03-程序流程结构之选择结构详解

前言 本篇都是在自学C基础知识的基础上&#xff0c;加上本身理解所完成的&#xff0c;为了便于记录学习情况&#xff0c;使用更加容易理解的话术描述出来&#xff0c;方便使用。 在C程序中&#xff0c;选择结构&#xff08;也称为条件结构&#xff09;用于根据特定的条件执行不…

python 使用flask_httpauth和pyjwt实现登录权限控制

最近需要用到&#xff0c;学习了一下记录 首先安装依赖 pip install Flask-HTTPAuth pyjwt passlib Welcome to Flask-HTTPAuth’s documentation! — Flask-HTTPAuth documentation Welcome to PyJWT — PyJWT 2.8.0 documentation Passlib 1.7.4 documentation — Passl…

Unity类银河恶魔城学习记录15-1,2 p153 Audio Manager p154 Audio distance limiter

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili AudioManager.cs using System.Collections; using System.Collections.Gen…

Grafana 系列|Grafana 监控 TDengine集群

Grafana 监控 TDengine集群有两种方式&#xff1a; 一、 taosKeeper监控 TDengine 通过 taosKeeper 将服务器的 CPU、内存、硬盘空间、带宽、请求数、磁盘读写速度等信息定时写入指定数据库。TDengine 还将重要的系统操作&#xff08;比如登录、创建、删除数据库等&#xff0…

OpenHarmony语言基础类库【@ohos.util.HashSet (非线性容器HashSet)】

HashSet基于[HashMap]实现。在HashSet中&#xff0c;只对value对象进行处理。 HashSet和[TreeSet]相比&#xff0c;HashSet中的数据无序存放&#xff0c;即存放元素的顺序和取出的顺序不一致&#xff0c;而TreeSet是有序存放。它们集合中的元素都不允许重复&#xff0c;但Hash…

八国语言50种海外电子游戏源码 海外游戏开发BTC虚拟币支付 外国电子游艺 游戏源码交易平台 搭建教程

全新海外版的游戏竞猜玩法带搭建教程 系统支持八国语言&#xff0c;50种游戏&#xff0c;支持 Paypal、人工充值、BTC多种支付 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89105597 更多资源下载&#xff1a;关注我。

WEB攻防-PHP特性-CMS审计实例

前置知识&#xff1a;PHP函数缺陷 测试环境&#xff1a;MetInfo CMS 函数缺陷导致的任意文件读取 漏洞URL&#xff1a;/include/thumb.php?dir 漏洞文件位置&#xff1a;MetInfo6.0.0\app\system\include\module\old_thumb.class.php <?phpdefined(IN_MET) or exit(No…

Python用于高级异常检测和聚类的工具库之BanditPAM使用详解

概要 Python BanditPAM库是一个用于高级异常检测和聚类的工具,具有强大的特性和灵活的功能,可以发现数据中的异常点并进行有效的聚类分析。本文将详细介绍Python BanditPAM库的安装、特性、基本功能、高级功能以及总结。 安装 首先,需要安装Python BanditPAM库。 可以使用…

2024年智能手表行业线上市场销售数据分析

智能手表市场近几年随着各大厂商的加入&#xff0c;逐渐朝着专业化、智能化发展。从一开始被认为是“智商税”、“鸡肋产品”到如今可以成为人体心脑血管健康监测、专业运动测速、移动定位的“多功能电子管家”&#xff0c;智能手表市场仍在不断发展中。 根据鲸参谋数据显示&a…

Git -- 运用总结

文章目录 1. Git2. 基础/查阅2.1 基础/查阅 - git2.2 仓库 - remote2.3 清理 - rm/clean2.4 版本回退 - reset 3. 分支3.1 分支基础 - branch3.2 分支暂存更改 - stash3.3 分支切换 - checkout 4. 代码提交/拉取4.1 代码提交 - push4.2 代码拉取 - pull 1. Git 2. 基础/查阅 2…

2分钟自己写小游戏:使用js和css编写石头剪刀布小游戏、扫雷小游戏、五子棋小游戏。新手老手毕业论文都能用。

系列文章目录 【复制就能用1】2分钟玩转轮播图,unslider的详细用法 【复制就能用2】css实现转动的大风车&#xff0c;效果很不错。 【复制就能用3】2分钟自己写小游戏&#xff1a;剪刀石头布小游戏、扫雷游戏、五子棋小游戏 【复制就能用4】2024最新智慧医疗智慧医院大数据…

【声网】实现web端与uniapp微信小程序端音视频互动

实现web端与uniapp微信小程序端音视频互动 利用声网实现音视频互动 开通声网服务 注册声网账号 进入Console 成功登录控制台后&#xff0c;按照以下步骤创建一个声网项目&#xff1a; 展开控制台左上角下拉框&#xff0c;点击创建项目按钮。 在弹出的对话框内&#xff0c;依…

严把质量关,饮片追溯系统应用,信息化追溯助力用药安全-亿发

中药饮片作为我国中药产业的重要组成部分&#xff0c;在医药工业中发挥着至关重要的作用。近年来&#xff0c;中药饮片行业虽然取得了稳步增长&#xff0c;但同时也面临着产业集中度低、竞争激烈、质量良莠不齐等诸多挑战。为了应对这些问题&#xff0c;国家和各地纷纷加强中药…

URL路由基础与Django处理请求的过程分析

1. URL路由基础 对于高质量的Web应用来讲&#xff0c;使用简洁、优雅的URL设计模式非常有必要。Django框架允许设计人员自由地设计URL模式&#xff0c;而不用受到框架本身的约束。对于URL路由来讲&#xff0c;其主要实现了Web服务的入口。用户通过浏览器发送过来的任何请求&am…

如何在vue3+vite中优雅的使用iconify图标

前言 从Vue2迁移到Vue3&#xff0c;在使用上有着很大的差别。本文的话主要是针对图标的使用差别上进行分析&#xff0c;同时给出基于iconify图标库中unplugin-icons的用法。这里特殊说明一下&#xff1a;其实element-plus中用到的图标也是基于iconify图标库的&#xff0c;在我们…