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

目录


前言

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

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

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


原理图分析

矩阵按键

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

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


扫描算法

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

反转法

  1. 先对所有行 输入低电平 ,读取所有列 的输出。显然,没有按键按下 的列依然保持高电平有按键按下 的列则为低电平。 记录有按键按下的列号

  2. 翻转IO引脚输入输出关系 ,对所有列输入低电平 ,读取所有行 的输出。同理,也可以得到被按下按键的行号 。由于前后两次扫描 速度极快,远远大于人的反应速度 ,所以不存在反转 过程中松开更换按键的情况。

  3. 至此,可以唯一确定被按下键的位置。

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


扫描法

  1. 逐行输入0 ,读取所有列 的输出。显然,没有按键按下 的列依然保持高电平有按键按下 的列则为低电平。 一旦有某列为低电平 ,按键位置就被唯一确定

  2. 完成一次整体扫描 需要4次 。也可以逐列扫描,原理一致。

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


软件实现

1. 矩阵键盘检测

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

matrix_key.h

c 复制代码
#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 0


sbit 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

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,且所有行置1
		row = 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

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

c 复制代码
#ifndef _MATRIX_KEY_H_
#define _MATRIX_KEY_H_

#include "public.h"
// 将具体处理部分集成到另个文件中,减少耦合
#include "calculator.h" 

#define MATRIX_PORT	P1

// 矩阵按键单次响应(0)或连续响应(1)开关
#define MatrixKEY_MODE 0


sbit 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

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,且所有行置1
		row = 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

c 复制代码
#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

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;
				// 如果初始状态为0
				if(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

c 复制代码
#include "matrix_key.h"
#include "smg.h"


int main(void){
	while(1){
		// 矩阵按键扫描
		check_matrixKey_turn();
		// 数码管刷新
		smg_showString(smg_val, 1);
	}
}

总结

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

相关推荐
F1331689295734 分钟前
WD0407 40V 7A 超级肖特基二极管,应用于开关汽车工业控制
stm32·单片机·嵌入式硬件·汽车·51单片机
DKPT1 小时前
Java设计模式之行为型模式(责任链模式)介绍与说明
java·笔记·学习·观察者模式·设计模式
崔高杰1 小时前
微调性能赶不上提示工程怎么办?Can Gradient Descent Simulate Prompting?——论文阅读笔记
论文阅读·人工智能·笔记·语言模型
KhalilRuan2 小时前
Unity Demo——3D平台跳跃游戏笔记
笔记·游戏·unity·游戏引擎
MUTA️3 小时前
《MAE: Masked Autoencoders Are Scalable Vision Learners》论文精读笔记
人工智能·笔记·深度学习·transformer
使一颗心免于哀伤4 小时前
《设计模式之禅》笔记摘录 - 6.原型模式
笔记·设计模式
mit6.8244 小时前
矩阵 | 时域频域傅里叶变换
线性代数·矩阵
The_Killer.4 小时前
格密码--数学基础--02基变换、幺模矩阵与 Hermite 标准形
线性代数·矩阵·密码学
刘孬孬沉迷学习4 小时前
5G标准学习笔记15 --CSI-RS测量
网络·笔记·学习·5g·信息与通信·信号处理
এ᭄画画的北北4 小时前
力扣-240.搜索二维矩阵 II
算法·leetcode·矩阵