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

news2024/12/27 11:44:14

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

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

解决

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.coloradmin.cn/o/1627581.html

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

相关文章

微信小程序开发:2.小程序组件

常用的视图容器类组件 View 普通的视图区域类似于div常用来进行布局效果 scroll-view 可以滚动的视图&#xff0c;常用来进行滚动列表区域 swiper and swiper-item 轮播图的容器组件和轮播图的item项目组件 View组件的基本使用 案例1 <view class"container"&…

LT9611UXC双端口 MIPI DSI/CSI 转 HDMI2.0,带音频

1. 说明 LT9611UXC 是一款高性能 MIPI DSI/CSI 至 HDMI2.0 转换器。MIPI DSI/CSI 输入具有可配置的单端口或双端口&#xff0c;具有 1 个高速时钟通道和 1~4 个高速数据通道&#xff0c;工作速率最高为 2Gbps/通道&#xff0c;可支持高达 16Gbps 的总带宽。 LT9611UXC 支持突发…

Fluent.Ribbon创建Office的RibbonWindow菜单

链接&#xff1a; Fluent.Ribbon文档 优势&#xff1a; 1. 可以创建类似Office办公软件的复杂窗口&#xff1b; 2. 可以应用自定义主题风格界面

视频滚动字幕一键批量轻松添加,解锁高效字幕编辑,提升视频质量与观众体验

视频已成为我们获取信息、娱乐休闲的重要渠道。一部成功的视频作品&#xff0c;除了画面精美、音质清晰外&#xff0c;字幕的添加也是至关重要的一环。字幕不仅能增强视频的观感&#xff0c;还能提升信息的传达效率&#xff0c;让观众在享受视觉盛宴的同时&#xff0c;更加深入…

SpringCloud系列(18)--将服务提供者Provider注册进Consul

前言&#xff1a;在上一章节中我们把服务消费者Consumer注册进了Zookeeper&#xff0c;并且成功通过服务消费者Consumer调用了服务提供者Provider&#xff0c;而本章节则是关于如何将服务提供者Provider注册进Consul里 准备环境&#xff1a; 先安装Consul&#xff0c;如果没有…

mac资源库的东西可以删除吗?提升Mac运行速度秘籍 Mac实用软件

很多小伙伴在使用mac电脑处理工作的时候&#xff0c;就会很疑惑&#xff0c;电脑的运行速度怎么越来越慢&#xff0c;就想着通过删除mac资源库的东西&#xff0c;那么mac资源库的东西可以删除吗&#xff1f;删除了会不会造成电脑故障呢&#xff1f; 首先&#xff0c;mac资源库…

【面试经典 150 | 二叉树】完全二叉树的节点个数

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;遍历统计方法二&#xff1a;二分查找位运算 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对…

北斗引路,太阳为源,定位报警,保护渔业,安全护航!

2022年1月&#xff0c;农业农村部发布《“十四五”全国渔业发展规划》明确提出&#xff0c;到2025年&#xff0c;渔业质量效益和竞争力明显增强&#xff0c;渔业基础设施和装备条件明显改善&#xff0c;渔业治理体系和治理能力现代化水平明显提高&#xff0c;实现产业更强、生态…

剖析线程池:深入理解Java中的线程池构造和调优技巧

使用Executors工具类创建线程池 Executors的主要方法与默认配置 Executors 工具类是 Java 中创建线程池的标准方法之一&#xff0c;它提供了许多静态方法来创建不同类型的线程池。以下是一些常用的 Executors 方法及其作用&#xff1a; newFixedThreadPool(int nThreads): 创…

Git如何配合Github使用

1.安装Git https://git-scm.com/ ##2.配置 Git 安装完成后&#xff0c;你需要设置 Git 的用户名和邮箱地址&#xff0c;这样在提交代码时就能知道是谁提交的。你可以在命令行中输入以下命令来配置&#xff1a; git config --global user.name "Your Name" git con…

政安晨:【Keras机器学习示例演绎】(十八)—— 图像字幕

目录 设置 下载数据集 准备数据 将文本数据向量化 构建用于训练的tf.data.Dataset管道 构建模型 模型训练 检查样本预测结果 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras机器学习实战 希望政安晨的博客能够对…

ChuanhuChatGPT集成百川大模型

搭建步骤&#xff1a; 拷贝本地模型&#xff0c;把下载好的Baichuan2-7B-Chat拷贝到models目录下 修改modules\models\base_model.py文件&#xff0c;class ModelType增加Baichuan Baichuan 16 elif "baichuan" in model_name_lower: model_type ModelType.Ba…

8点法估计基础矩阵

估计基础矩阵 文章目录 估计基础矩阵8点法归一化 8点法 8点法 根据两幅图像中8个对应点对之间的关系&#xff0c;采用SVD求 解最小二乘方 约束&#xff1a;det(F) 0 假设已知N对点的对应关系&#xff1a; { x i , x i ′ } i 1 N \{x_i,x^{\prime}_i\}_{i1}^N {xi​,xi′​…

第一个大型汽车ITU-T车载语音通话质量实验室投入使用

中国汽车行业蓬勃发展&#xff0c;尤其是新能源汽车风起云涌&#xff0c;无论是国内还是海外需求旺盛的趋势下&#xff0c;除乘用车等紧凑型车外&#xff0c;中型汽车如MPV、小巴、小型物流车&#xff0c;大型汽车如重卡、泥头车等亦加入了手机互联、智驾的科技行列&#xff0c…

力扣题目:轮转数组

力扣题目&#xff1a;轮转数组 题目链接: 189.轮转数组 题目描述 代码思路 根据从轮转前到轮转后到数组变化&#xff0c;我们可以将数组元素分成两个部分&#xff0c;一个部分数轮转后从右边调到前面&#xff0c;一部分仅仅从左边向右移动。发现这个规律后&#xff0c;将数组…

软件工程的介绍

软件工程 这一章的内容其实还是蛮多的,大概一共有10个章节,分别是下面的一些内容,但是呢,这一章的内容其实是比较偏向文科类的,也就是说,记忆的内容其实占有很大的篇幅,在该考试科目当中呢,其实也是主要影响上午题部分的选择题的考察,基本的分值呢,在10分左右,分值占…

python自定义交叉熵损失,再和pytorch api对比

背景 我们知道&#xff0c;交叉熵本质上是两个概率分布之间差异的度量&#xff0c;公式如下 其中概率分布P是基准&#xff0c;我们知道H(P,Q)>0&#xff0c;那么H(P,Q)越小&#xff0c;说明Q约接近P。 损失函数本质上也是为了度量模型和完美模型的差异&#xff0c;因此可以…

input框添加验证(如只允许输入数字)中文输入导致显示问题的解决方案

文章目录 input框添加验证(如只允许输入数字)中文输入导致显示问题的解决方案问题描述解决办法 onCompositionStart与onCompositionEnd input框添加验证(如只允许输入数字)中文输入导致显示问题的解决方案 问题描述 测试环境&#xff1a;react antd input (react的事件与原生…

如何在TestNG中忽略测试用例

在这篇文章中&#xff0c;我们将讨论如何在TestNG中忽略测试用例。TestNG帮助我们忽略使用Test注释的情况&#xff0c;我们可以在不同的级别上忽略这些情况。 首先&#xff0c;只忽略一个测试方法或测试用例。第二&#xff0c;忽略一个类及其子类中的所有情况。第三个是&#…

QT中基于TCP的网络通信

QT中基于TCP的网络通信 QTcpServer公共成员函数信号 QTcpSocket公共成员函数信号 通信流程服务器端通信流程代码 客户端通信流程代码 使用Qt提供的类进行基于TCP的套接字通信需要用到两个类&#xff1a; QTcpServer&#xff1a;服务器类&#xff0c;用于监听客户端连接以及和客…