本文最后更新于5 天前,其中的信息可能已经过时,如有错误请发送邮件到1739584917@qq.com
我们知道矩阵按键的读取是通过不断进行行列扫描的方式检测按键状态来实现的,以下是基于arduino平台的4×4矩阵按键读取代码示例:
// 定义行和列的引脚
const int ROWS = 4;
const int COLS = 4;
// 行线连接到Arduino的引脚2、3、4、5
int rowPins[ROWS] = {2, 3, 4, 5};
// 列线连接到Arduino的引脚6、7、8、9
int colPins[COLS] = {6, 7, 8, 9};
// 定义按键的字符映射(4x4矩阵按键)
int keys[ROWS][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
// 扫描矩阵按键并返回按下的按键
int getKey() {
for (int i = 0; i < ROWS; i++) {
// 拉低当前行
digitalWrite(rowPins[i], LOW);
// 检查每一列的状态
for (int j = 0; j < COLS; j++) {
if (digitalRead(colPins[j]) == LOW) { // 如果列线为低电平
digitalWrite(rowPins[i], HIGH); // 恢复行线状态
return keys[i][j]; // 返回对应的按键值
}
}
// 恢复当前行为高电平
digitalWrite(rowPins[i], HIGH);
}
return 0; // 没有按键按下
}
void setup() {
// 初始化串口通信
Serial.begin(9600);
// 设置行线为输出模式,并初始化为高电平
for (int i = 0; i < ROWS; i++) {
pinMode(rowPins[i], OUTPUT);
digitalWrite(rowPins[i], HIGH);
}
// 设置列线为输入模式,并启用内部上拉电阻
for (int j = 0; j < COLS; j++) {
pinMode(colPins[j], INPUT_PULLUP);
}
}
void loop() {
int key = getKey(); // 获取按下的按键
if (key != 0) { // 如果有按键按下
Serial.println(key); // 打印按键值
delay(20); // 简单防抖
}
}
51单片机的可以参考https://lolife.top/?p=710这篇文字里面的矩阵按键模块部分。
采用这种方式实现实现弊端很明显,每次读取都需要通过一定的延时消抖并且耦合性太高,不容易拓展。
优化方式:通过辅助变量进行按位运算,捕捉按键的上下降沿,节省消抖的时间。
int Key_Val, Key_Old,Key_Down, Key_Up;
void getKey() {
int temp=0;
for (int i = 0; i < ROWS; i++) {
// 拉低当前行
digitalWrite(rowPins[i], LOW);
// 检查每一列的状态
for (int j = 0; j < COLS; j++) {
if (digitalRead(colPins[j]) == LOW) { // 如果列线为低电平
temp = keys[i][j];
}
}
// 恢复当前行为高电平
digitalWrite(rowPins[i], HIGH);
}
/**********************************************************/
Key_Val = temp; // 实时读取键码值
Key_Down = Key_Val & (Key_Old ^ Key_Val); // 捕捉按键下降沿
Key_Up = ~Key_Val & (Key_Old ^ Key_Val); // 捕捉按键上降沿
Key_Old = Key_Val; // 辅助扫描变量
//所有按键流程在此处理
if(Key_Up){//有按键抬起
//TODO:
}
if(Key_Down){//有按键按下
//TODO:
}
}
unsigned long timer=0;
void loop(){
if(millis()-timer>10){//降低执行频率
getKey();
timer=millis;
}
}
通过这种方式实现,省去了按键延时的堵塞。但会导致全部的按键读取流程都会集中在一个函数内,如果项目较小那这种方式确实是不错的方式,但当项目大起来时,将所有流程集中在一个函数显然过于有拥挤,对于其他的函数的通信来说也同样不方便。
优化方式:采用队列(消息缓冲)的方式解耦,所有按键消息都存入缓冲队列,不需要理会业务逻辑,让其他的业务函数去读取处理。
最终的优化版本:
为了方便复用,做成了类的方式,采用单例模式
头文件
//Keyboard.h
#ifndef KEYBOARD_H
#define KEYBOARD_H
#include <Arduino.h>
#include <queue>
#include <map>
static const byte ROWS = 4;
static const byte COLS = 3;
static uint8_t keys[ROWS][COLS] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
{10, 11, 12}};
static byte rowPins[ROWS] = {39, 38, 37, 36};
static byte colPins[COLS] = {42, 41, 40};
//以上为默认构造参数
struct KeyVal//按键消息类
{
uint8_t Key_Val;
//0表示释放,1表示按下,2表示长按
uint8_t pressed;
KeyVal()
{
Key_Val = 0;
pressed = 0;
}
KeyVal(uint8_t key, uint8_t pressed) : Key_Val(key), pressed(pressed) {}
};
class Keyboard
{
public:
static Keyboard &getInstance()
{
static Keyboard kb((uint8_t *)keys, rowPins, colPins, ROWS, COLS);
return kb;
}
Keyboard(const Keyboard &) = delete;
Keyboard &operator=(const Keyboard &) = delete;
public:
void init();
void run();
KeyVal getKeyVal();
private:
Keyboard() {};
Keyboard(uint8_t *keys, uint8_t *rowPins, uint8_t *colPins, uint8_t ROWS, uint8_t COLS);
uint8_t *_keys;
uint8_t *_rowPins;
uint8_t *_colPins;
uint8_t _ROWS;
uint8_t _COLS;
uint8_t Key_Val, Key_Old,Key_Down, Key_Up;
std::queue<KeyVal> Key_Buff;//缓冲队列
unsigned long longPressDuration; // 长按持续时间
std::map<uint8_t, unsigned long> keyPressTimes; // 存储按键按下的时间戳
};
#endif
源文件
//Keyboard.cpp
#include <Keyboard.h>
#define MAX_BUFFER_SIZE 16//缓冲队列大小
void Keyboard::init()
{
// 初始化行引脚为输出,并设置为高电平
for (int i = 0; i < _ROWS; i++)
{
pinMode(_rowPins[i], OUTPUT);
digitalWrite(_rowPins[i], HIGH);
}
// 初始化列引脚为输入,并启用内部上拉电阻
for (int i = 0; i < _COLS; i++)
{
pinMode(_colPins[i], INPUT_PULLUP);
}
}
void Keyboard::run()
{
uint8_t temp = 0;
unsigned long currentTime = millis(); // 获取当前时间
for (uint8_t i = 0; i < _ROWS; i++)
{
// 将当前行设置为低电平
digitalWrite(_rowPins[i], LOW);
// 检查每一列
for (uint8_t j = 0; j < _COLS; j++)
{
// 如果当前列为低电平,表示按键被按下
if (digitalRead(_colPins[j]) == LOW)
{
temp = *(_keys + i * _COLS + j);
}
}
// 将当前行恢复为高电平
digitalWrite(_rowPins[i], HIGH);
}
Key_Val = temp; // 实时读取键码值
Key_Down = Key_Val & (Key_Old ^ Key_Val); // 捕捉按键下降沿
Key_Up = ~Key_Val & (Key_Old ^ Key_Val); // 捕捉按键上降沿
Key_Old = Key_Val; // 辅助扫描变量
if(Key_Buff.size() < MAX_BUFFER_SIZE)
{
if (Key_Down)
{
Key_Buff.push({Key_Down, 1});
keyPressTimes[Key_Down] = currentTime; // 记录按键按下的时间戳
}
if (Key_Up)
{
if(currentTime - keyPressTimes[Key_Up] >= longPressDuration)
{
Key_Buff.push({Key_Up, 2}); // 2 表示长按
}
else
{
Key_Buff.push({Key_Up, 0});
}
keyPressTimes.erase(Key_Up); // 按键释放,移除时间戳
}
}
}
KeyVal Keyboard::getKeyVal()
{
if(!Key_Buff.empty())
{
KeyVal key = Key_Buff.front();
Key_Buff.pop();
return key;
}
return KeyVal{0, 0};
}
Keyboard::Keyboard(uint8_t *keys, uint8_t *rowPins, uint8_t *colPins, uint8_t ROWS, uint8_t COLS)
{
this->_keys = keys;
this->_rowPins = rowPins;
this->_colPins = colPins;
this->_ROWS = ROWS;
this->_COLS = COLS;
this->longPressDuration = 2000;
}
调用方法:
#include <Arduino.h>
#include "Keyboard.h"
//按键刷新率
#define KEY_REFRESH_FLAG 10
unsigned long Key_Refresh = 0;
Keyboard &keyboard = Keyboard::getInstance();//单例
void setup()
{
keyboard.init();//初始化
}
void loop()
{
if (millis() - Key_Refresh > KEY_REFRESH_FLAG)
{
Key_Refresh = millis();
keyboard.run();
}
test();
}
//业务函数
void test()
{
// 按键处理:
key = keyboard.getKeyVal();
// key = Keyboard::getInstance().getKeyVal();
if (!key.Key_Val) // 没有按键消息
return;
// 0表示释放,1表示按下,2表示长按
if (key.Key_Val == 1 && key.pressed == 0) // 1键抬起
{
// TODO:
}
if (key.Key_Val == 2 && key.pressed == 0) // 2键抬起
{
// TODO:
}
if (key.Key_Val == 1 && !key.pressed == 1) // 1键按下
{
// TODO:
}
if (key.Key_Val == 2 && !key.pressed == 1) // 2键按下
{
// TODO:
}
if (key.Key_Val == 1 && !key.pressed == 2) // 1键长按
{
// TODO:
}
if (key.Key_Val == 2 && !key.pressed == 2) // 2键长按
{
// TODO:
}
}
部分代码参考B站up主Alice_西风的蓝桥杯大模板