【51单片机实验笔记】开关篇(二) 矩阵按键

目录

  • 前言
  • 原理图分析
    • 矩阵按键
    • 扫描算法
  • 软件实现
    • 1. 矩阵键盘检测
    • 2. 简易计算器实现
  • 总结


前言

本节内容,我们学习一下矩阵按键,它是独立按键阵列形式,常见的应用即键盘

本节涉及到的封装源文件可在《模块功能封装汇总》中找到。

本节完整工程文件已上传GitHub,仓库地址,欢迎下载交流!


原理图分析

矩阵按键

上一节中,我们实现了多个独立按键驱动检测,我们每个按键都连接了一个IO。但当所需按键较多时,比如需要100个独立按键,使用之前的接线方式显然会消耗非常多的IO口资源。参考LED点阵思想,采用并联结构矩阵按键可以有效解决这个问题。类似地,我们采用动态扫描的方式检测每个按键

图1 矩阵按键
图2 矩阵按键原理图

扫描算法

具体来说,有两种主流扫描方法,各有特点。现介绍如下:

反转法
1. 先对所有行输入低电平,读取所有列的输出。显然,没有按键按下的列依然保持高电平有按键按下的列则为低电平 记录有按键按下的列号
2. 翻转IO引脚输入输出关系,对所有列输入低电平,读取所有行的输出。同理,也可以得到被按下按键的行号由于前后两次扫描速度极快,远远大于人的反应速度,所以不存在反转过程中松开更换按键的情况。
3. 至此,可以唯一确定被按下键的位置。

优点: 只需扫描2次就能确定按键位置,效率高
缺点: 依赖于硬件IO翻转速度。只能检测单键组合键无法检测。


扫描法
1. 逐行输入0,读取所有列的输出。显然,没有按键按下的列依然保持高电平有按键按下的列则为低电平 一旦有某列为低电平,按键位置就被唯一确定
2. 完成一次整体扫描需要4次。也可以逐列扫描,原理一致。

优点: 只要扫描到就能直接确定按键位置,实现简单。
缺点: 完成一次整体扫描需要4次,效率稍低。只适用于较小规模矩阵按键


软件实现

1. 矩阵键盘检测

实现了反转法扫描法两种检测扫描方法实验现象为按下一个键蜂鸣器短鸣数码管显示对应(0~F)的数值。

matrix_key.h

#ifndef _MATRIX_KEY_H_
#define _MATRIX_KEY_H_#include "delay.h"
#include "beep.h"
#include "smg.h"#define MATRIX_PORT	P1// 矩阵按键单次响应(0)或连续响应(1)开关
#define MatrixKEY_MODE 0sbit ROW_PORT_1 = P1^7;
sbit ROW_PORT_2 = P1^6;
sbit ROW_PORT_3 = P1^5; // 共用了蜂鸣器引脚
sbit ROW_PORT_4 = P1^4;sbit COL_PORT_1 = P1^3;
sbit COL_PORT_2 = P1^2;
sbit COL_PORT_3 = P1^1;
sbit COL_PORT_4 = P1^0;void check_matrixKey_turn();
void check_matrixKey_scan();#endif

matrix_key.c

#include "matrix_key.h"
/** **  @brief    实现了矩阵按键的两种扫描方式**			   1. 与数码管、蜂鸣器联动**			   2. 按下一个键,数码管显示对应(0~F)的数值**			   3. 按下至未松开过程中,屏蔽其他按键**  @author   QIU**  @date     2023.05.08**//*-------------------------------------------------------------------*/// 存储按下的行列
u8 row, col;
// 按键当前状态,true按下中,false已释放
u8 key_now_state = false;/****  @brief   读取电平**  @param   state: 0-列,1-行**  @retval  返回列(行)数**/
u8 read_port(bit state){u8 dat;if(state) dat = MATRIX_PORT >> 4; // 如果是行,取高四位else dat =  MATRIX_PORT & 0x0f;   // 如果是列,取低四位// 从左上开始为第一行,第一列switch(dat){// 0000 1110 第4列(行)case 0x0e: return 4;// 0000 1101 第3列(行)case 0x0d: return 3;// 0000 1011 第2列(行)case 0x0b: return 2;// 0000 0111 第1列(行)case 0x07: return 1;// 0000 1111 没有按下case 0x0f: return 0;// 多键同时按下不响应default: return 0;}
}/****  @brief   矩阵按键处理**  @param   参数说明**  @retval  返回值**/
void key_pressed(){u8 key_val;// 如果不是连续模式if(!MatrixKEY_MODE) key_now_state = true; // 蜂鸣器响应,第三行连接P1.5,不响beep_once(50, 2000);// 计算显示的字符key_val = (row-1)*4 + (col - 1);if(key_val >= 0 && key_val <= 9) key_val += '0';else key_val += 'A' - 10;// 字符显示smg_showChar(key_val, 1, false);
}/****  @brief   (反转法)检测按键(单键),按住过程中屏蔽其他按键。同列需全部松开才能再次响应**  @param   无**  @retval  无**/
void check_matrixKey_turn(){// 所有行置低电平,列置高电平MATRIX_PORT = 0x0f;// 读取所有列电平col = read_port(0);// 如果有效键按下,延时消抖if(col) delay_ms(10);else {key_now_state = false;return;} // 注意,if else还是需要括号的,与case 不同// 所有列置低电平,行置高电平MATRIX_PORT = 0xf0;// 读取所有行电平row = read_port(1);// 如果有键按下(当前未按下),响应if(row && !key_now_state) key_pressed();else return;
}/****  @brief   (扫描法)检测按键,本例扫描列**  @param   无**  @retval  无**/
void check_matrixKey_scan(){u8 i;for(i=0;i<4;i++){MATRIX_PORT = ~(0x08>>i); // 逐列置0,且所有行置1row = read_port(1); // 读取行if(!row && col == i+1)key_now_state = false; // 当前扫描列无有效键按下else if(row && !key_now_state){       // 有效键按下且为松开状态delay_ms(10);row = read_port(1); // 再次读取行if(row) {col = i+1;key_pressed();} }}
}

main.c

#include "matrix_key.h"
#include "smg.h"/** **  @brief    实验现象:矩阵按键按下,数码管显示对应数字,同时蜂鸣器作按键提示音**  @author   QIU**  @date     2023.05.08**//*-------------------------------------------------------------------*/void main(){smg_showChar(' ', 1, false);while(1){// 反转法// check_matrixKey_turn();// 扫描法check_matrixKey_scan();}
}

注意:由于开发板矩阵按键共用了蜂鸣器引脚P1.5,因此按下按键的时候蜂鸣器会发出响声。此为硬件电路问题,属正常现象


2. 简易计算器实现

模仿计算器键位分布定义键位如下图所示。
在这里插入图片描述
配合数码管显示,可以实现简单的加减乘除四则运算,支持浮点数负数计算。

为了减少各模块的耦合,将计算器具体处理部分抽至calculator.hcalculator.c中,原本矩阵键盘源代码文件几乎无需改动

matrix_key.h

#ifndef _MATRIX_KEY_H_
#define _MATRIX_KEY_H_#include "public.h"
// 将具体处理部分集成到另个文件中,减少耦合
#include "calculator.h" #define MATRIX_PORT	P1// 矩阵按键单次响应(0)或连续响应(1)开关
#define MatrixKEY_MODE 0sbit ROW_PORT_1 = P1^7;
sbit ROW_PORT_2 = P1^6;
sbit ROW_PORT_3 = P1^5; // 共用了蜂鸣器引脚
sbit ROW_PORT_4 = P1^4;sbit COL_PORT_1 = P1^3;
sbit COL_PORT_2 = P1^2;
sbit COL_PORT_3 = P1^1;
sbit COL_PORT_4 = P1^0;void check_matrixKey_turn();
void check_matrixKey_scan();#endif

matrix_key.c

#include "matrix_key.h"/** **  @brief    实现了矩阵按键的两种扫描方式**  @author   QIU**  @date     2024.02.14**//*-------------------------------------------------------------------*/// 存储按下的行列
u8 row, col;
// 按键当前状态,true按下中,false已释放
u8 key_now_state = false;/****  @brief   读取电平**  @param   state: 0-列,1-行**  @retval  返回列(行)数**/
u8 read_port(bit state){u8 dat;if(state) dat = MATRIX_PORT >> 4; // 如果是行,取高四位else dat =  MATRIX_PORT & 0x0f;   // 如果是列,取低四位// 从左上开始为第一行,第一列switch(dat){// 0000 1110 第4列(行)case 0x0e: return 4;// 0000 1101 第3列(行)case 0x0d: return 3;// 0000 1011 第2列(行)case 0x0b: return 2;// 0000 0111 第1列(行)case 0x07: return 1;// 0000 1111 没有按下case 0x0f: return 0;// 多键同时按下不响应default: return 0;}
}/****  @brief   矩阵按键处理函数**  @param   参数说明**  @retval  返回值**/
void key_pressed(){// 如果不是连续模式if(!MatrixKEY_MODE) key_now_state = true; // 计算器处理函数calculator_deal_key(row, col);
}/****  @brief   (反转法)检测按键(单键),按住过程中屏蔽其他按键。同列需全部松开才能再次响应**  @param   无**  @retval  无**/
void check_matrixKey_turn(){// 所有行置低电平,列置高电平MATRIX_PORT = 0x0f;// 读取所有列电平col = read_port(0);// 如果有效键按下,延时消抖if(col){// 当且仅当松开状态才进一步检测if(!key_now_state) delay_ms(10);else return;}else{key_now_state = false;return;} // 所有列置低电平,行置高电平MATRIX_PORT = 0xf0;// 读取所有行电平row = read_port(1);// 如果有键按下(当前未按下),响应if(row && !key_now_state) key_pressed();else return;
}/****  @brief   (扫描法)检测按键,本例扫描列**  @param   无**  @retval  无**/
void check_matrixKey_scan(){u8 i;for(i=0;i<4;i++){MATRIX_PORT = ~(0x08>>i); // 逐列置0,且所有行置1row = read_port(1); // 读取行if(!row && col == i+1)key_now_state = false; // 当前扫描列无有效键按下else if(row && !key_now_state){       // 有效键按下且为松开状态delay_ms(10);row = read_port(1); // 再次读取行if(row) {col = i+1;key_pressed();} }}
}

calculator.h

#ifndef __CALCULATOR_H__
#define __CALCULATOR_H__#include "public.h"// 键值枚举
typedef enum{KEY_0 = 0,KEY_1,KEY_2,KEY_3,KEY_4,KEY_5,KEY_6,KEY_7,KEY_8,KEY_9,Dot,Addition,Subtraction,Multiplication,Division,Calculation
}Key_Value;extern u8 xdata smg_val[];void calculator_deal_key(u8, u8);#endif

calculator.c

#include "calculator.h"
#include "beep.h"
#include "smg.h"
/** **  @brief    计算器相关函数封装**  @author   QIU**  @date     2024.02.17**/
/*-------------------------------------------------------------------*/// 管理一个用于数码管显示的字符数组,以'\0'结尾
u8 xdata smg_val[10] = {'0', 0};
// 前数值,当前数值
double pre_value = 0, now_value = 0;
// 小数点后位数,整数部分位数
u8 dot_num = 0, pre_dot_num = 0, integer_num = 0, pre_integer_num = 0;
// 存储上一个运算符(默认为加法)
u8 pre_operator_val = Addition;
// 小数点启用标志,新数据输入标志
bit flag_dot = false, flag_new_data = true;
// 矩阵键盘键值数组(4 x 4)
u8 code Matrix_Key_Value[4][4] = {{KEY_7, KEY_8, KEY_9, Addition},{KEY_4, KEY_5, KEY_6, Subtraction},{KEY_1, KEY_2, KEY_3, Multiplication},{KEY_0, Dot, Calculation, Division}
};/****  @brief   根据按键,更新数码管显示值**  @param   参数说明**  @retval  返回值**/
void update_smg_value(u8 row, u8 col){// 取出当前键值u8 key_val = Matrix_Key_Value[row - 1][col - 1];switch(key_val){// KEY_0,未进入小数部分,且初始为0的状态下,按下无响应case KEY_0: if(!flag_dot && integer_num == 0 && smg_val[0] == '0') return;case KEY_1:case KEY_2:	case KEY_3:case KEY_4:	case KEY_5:case KEY_6:	case KEY_7:case KEY_8:	case KEY_9:	// 每次操作符后首次按键,清空显示字符串if(flag_new_data){flag_new_data = false;// 清空smg_val数组memset(smg_val, 0, sizeof(smg_val));flag_dot = false;// 两数的小数最大个数即为运算结果的小数个数pre_dot_num = MAX(pre_dot_num, dot_num);pre_integer_num = integer_num;dot_num = integer_num = 0;}if(flag_dot){// 已按下小数点时,小数部分dot_num++;smg_val[integer_num + dot_num] = key_val + '0';		}else{// 还未按下小数点时,整数部分smg_val[integer_num] = key_val + '0';integer_num++;}break;case Dot:// 如果按下运算符后,直接按小数点无效。if(flag_new_data && integer_num != 0) return;else flag_new_data = false;// 如果未进入小数状态,该键有效if(!flag_dot){flag_dot = true;// 如果初始状态为0if(integer_num == 0 && smg_val[0] == '0'){integer_num++;smg_val[integer_num] = '.';}else{smg_val[integer_num] = '.';}}break;case Addition:case Subtraction:case Multiplication:case Division:case Calculation:// 只有当输入过新数据或者上个运算符为等号时,运算符键才有效if(!flag_new_data || pre_operator_val == Calculation){double val;int num;// 将现有显示的字符串转为数值pre_value = now_value;now_value = atof(smg_val);switch(pre_operator_val){case Addition: val = pre_value + now_value; num = MAX(pre_dot_num, dot_num); break;case Subtraction: val = pre_value - now_value; num = MAX(pre_dot_num, dot_num); break;case Multiplication: val = pre_value * now_value; num = pre_dot_num + dot_num; break;case Division: val = pre_value / now_value; num = 6; break;case Calculation: val = now_value; num = MAX(pre_dot_num, dot_num); break;}sprintf(smg_val, "%.*f", num, val); pre_operator_val = key_val; flag_new_data = true;// 再更新为当前值now_value = atof(smg_val);}break;default:break;}
}// 计算器键值处理
void calculator_deal_key(u8 row, u8 col){// 蜂鸣器响应,第三行连接P1.5,不响beep_once(50, 2000);// 更新数码管的值update_smg_value(row, col);
}

main.c

#include "matrix_key.h"
#include "smg.h"int main(void){while(1){// 矩阵按键扫描check_matrixKey_turn();// 数码管刷新smg_showString(smg_val, 1);}
}

总结

矩阵按键检测方法与其阵列方式息息相关。现在,我们可以尝试在任意的小项目中加入按键模块

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

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

相关文章

websocket数据帧格式

客户端、服务端数据的交换&#xff0c;离不开数据帧格式的定义。因此&#xff0c;在实际讲解数据交换之前&#xff0c;我们先来看下WebSocket的数据帧格式。 WebSocket客户端、服务端通信的最小单位是帧&#xff08;frame&#xff09;&#xff0c;由1个或多个帧组成一条完整的消…

基于协同过滤的时尚穿搭推荐系统

项目&#xff1a;基于协同过滤的时尚穿搭推荐系统 摘 要 基于协同过滤的时尚穿搭推荐系统是一种能自动从网络上收集信息的工具&#xff0c;可根据用户的需求定向采集特定数据信息的工具&#xff0c;本项目通过研究服饰流行的分析和预测的分析和预测信息可视化时尚穿搭推荐系统…

2月12作业

1.会出现段错误&#xff0c;因为p申请的堆区内存未返回给str&#xff0c;导致str仍然指向NULL&#xff0c;无法将"hello world"拷贝给str 2.会出现段错误&#xff0c;因为p是一个局部变量&#xff0c;函数结束时&#xff0c;p将被释放&#xff0c;不能返回它的地址 …

C++中的volatile:穿越编译器的屏障

C中的volatile&#xff1a;穿越编译器的屏障 在C编程中&#xff0c;我们经常会遇到需要与硬件交互或多线程环境下访问共享数据的情况。为了确保程序的正确性和可预测性&#xff0c;C提供了关键字volatile来修饰变量。本文将深入解析C中的volatile关键字&#xff0c;介绍其作用、…

浅谈电商场景中的扣除库存问题

库存 一、场景二、扣减时机1.下单时扣库存2.支付完成扣库存3.预扣除 三、库存存储方案1.数据库存储2.数据库缓存混合存储 四、整体方案1.单数据库方案2.主从数据库方案3.主从数据库缓存方案4.数据库缓存混合存储 五、其他情况1.秒杀QPS过高2.Redis QPS过高3.Master DB QPS过高4…

使用ShardingJDBC实现分库分表

一、测试环境 JDK&#xff1a;1.8SpringBoot&#xff1a;2.7.17MySQL驱动&#xff1a;5.1.49MyBatis&#xff1a;2.3.1shardingJDBC&#xff1a;5.1.0 二、核心依赖 <!-- mysql 驱动 --> <dependency><groupId>mysql</groupId><artifactId>mysq…

网站架构演变、LNP+Mariadb数据库分离、Web服务器集群、Keepalived高可用

目录 day02 深入理解程序的数据存储 验证 配置NFS服务器 配置代理服务器 配置名称解析 day02 深入理解程序的数据存储 程序将文字数据保存到数据库中程序将非文字数据&#xff08;如图片、视频、压缩包等&#xff09;保存到相应的文件目录中 验证 发一篇文章&#xf…

Manifest merger failed with multiple errors, see logs

问题 Manifest merger failed with multiple errors, see logs详细问题 笔者进行Android 项目开发&#xff0c;修改AndroidManifest.xml代码后&#xff0c;控制台报错 AndroidManifest.xml报错核心代码 <manifest><uses-permission android:name"android.perm…

StringBuilder/StringBuffer类(Java)

StringBuilder/StringBuffer类 当对字符串进行修改的时候&#xff0c;使用 StringBuffer / StringBuilder 类更方便。和 String 类不同的是&#xff0c;StringBuffer 和 StringBuilder 类的对象能够被多次的修改&#xff0c;并且不产生新的未使用对象。方法类似 public class…

力扣:300. 最长递增子序列

动态规划: 1. 先定义dp数组来表示在下标为i时最长递增子序列&#xff0c;先初始化一下每个下标的值为dp【i】1。同时我们要判断在下标i之前的最长的递增子序列为多少&#xff0c;在判断当前的下标i是否满足递增的条件满足的话就进行dp【i】的重新赋值。之后要更新接受的最长递…

【C语言】长篇详解,字符系列篇1-----“混杂”的各种字符类型字符转换和strlen的模拟实现【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本期系列为【C语言】长篇详解&#xff0c;字符系列篇1-----“混杂”的各种字符函数……&#xff0c;图文讲解各种字符函数&#xff0c;带大家更深刻理解C语言中各种字符函数的应用&#xff0c;感谢观看&#xff0c;支持的可以给个赞哇。 前言…

内存块与内存池

&#xff08;1&#xff09;在运行过程中&#xff0c;MemoryPool内存池可能会有多个用来满足内存申请请求的内存块&#xff0c;这些内存块是从进程堆中开辟的一个较大的连续内存区域&#xff0c;它由一个MemoryBlock结构体和多个可供分配的内存单元组成&#xff0c;所有内存块组…

Java学习笔记------static

static 创建Javabean类 public class student {private int age;private String name;private String gender;public student() {}public student(int age, String name, String gender) {this.age age;this.name name;this.gender gender;}/*** 获取* return age*/public…

MySQL的索引类型

目录 1. 主键索引 (PRIMARY KEY) 2. 唯一索引 (UNIQUE) 3. 普通索引 (INDEX) 4. 全文索引 (FULLTEXT) 5. 空间索引 (SPATIAL) 6. 组合索引 (COMPOSITE INDEX) 7. 前缀索引 (PREFIX INDEX) 8. 覆盖索引 (COVERING INDEX) 1. 主键索引 (PRIMARY KEY) 描述&#xff1a;表…

使用Python编写脚本-根据端口号杀掉进程

我的GitHub&#xff1a;Powerveil - GitHub 我的Gitee&#xff1a;Powercs12 - Gitee 皮卡丘每天学Java 从前段开始遇到一个问题&#xff0c;服务在启动的时候总是端口被占用&#xff0c;发现还是Java程序&#xff0c;但是当时并没有启动Java程序&#xff0c;电脑出问题了。 一…

请解释Java中的字节码是什么,它与源代码和机器码有什么关系?

请解释Java中的字节码是什么&#xff0c;它与源代码和机器码有什么关系&#xff1f; 在Java编程语言中&#xff0c;源代码&#xff08;即.java文件&#xff09;经过编译器编译后会生成字节码&#xff08;即.class文件&#xff09;&#xff0c;字节码是一种中间代码&#xff0c…

AndroidFrameWork切换帧率

文章目录 参考资料 简述 一. SurfaceFlinger接受帧率变化1.1 SurfaceFlinger.setDesiredActiveConfig1.2 SurfaceFlinger.repaintEverythingForHWC1.3 Scheduler.resyncToHardwareVsync1.3.1 Scheduler.setVsyncPeriod1.3.2 VSyncReactor.setPeriod1.3.3 VSyncReactor.startPe…

【Linux】Framebuffer 应用

# 前置知识 LCD 操作原理 在 Linux 系统中通过 Framebuffer 驱动程序来控制 LCD。 Frame 是帧的意思&#xff0c; buffer 是缓冲的意思&#xff0c;这意味着 Framebuffer 就是一块内存&#xff0c;里面保存着一帧图像。 Framebuffer 中保存着一帧图像的每一个像素颜色值&…

Tomcat要点总结

一、Tomcat 服务中部署 WEB 应用 1.什么是Web应用 &#xff08;1&#xff09; WEB 应用是多个 web 资源的集合。简单的说&#xff0c;可以把 web 应用理解为硬盘上的一个目录&#xff0c; 这个目录用于管理多个 web 资源。 &#xff08;2&#xff09;Web 应用通常也称之为…

每日一个shell脚本之一键部署Agent提高工作效率!

每日一个shell脚本之一键部署Agent提高工作效率&#xff01; 源码参上 #!/usr/bin/bash # **************************************# CSDN: M乔木 # qq邮箱: 2776617348qq.com # 解释器: 这是一个shell脚本 # **…