最近更新于 2024-05-05 12:32
前言
这种矩阵键盘,从左到右四根线依次对应一到四行,最后四根线对应一至四列,按键的时候某行和某列导通就能确定是按下的是哪个键了。后面叙述 1-4 行就是对应 1、2、3、4 引脚,1-4 列就是 5、6、7、8 引脚
测试环境
Arduino UNO
Arduino IDE 2.1.1
按键检测逻辑
\begin{array}{|c|c|}
\hline
矩阵按键 & Arduino \\
\hline
1 & 6 \\
\hline
5 & GND \\
\hline
\end{array}
这样接线是为了检测第一行第一列的按键。Arduino 的引脚 6 用来读取按键状态,先把 6 设置上拉电阻输入模式(Arduino UNO 内置了 20kΩ 上拉电阻),此时引脚 6 就会保持在高电位,当按键按下时,相当于把 GND 和 6 导通,此时 6 检测到低电平,即代表按下按键。
void setup()
{
Serial.begin(9600);
pinMode(6, INPUT_PULLUP);
}
void loop()
{
Serial.println(digitalRead(6));
delay(100);
}
单键检测
逐行逐列扫描
接线对照表
\begin{array}{|c|c|}
\hline
矩阵按键 & Arduino \\
\hline
1 & 6 \\
\hline
2 & 7 \\
\hline
3 & 8 \\
\hline
4 & 9 \\
\hline
5 & 10 \\
\hline
6 & 11 \\
\hline
7 & 12 \\
\hline
8 & 13 \\
\hline
\end{array}
首先将 4 个行引脚设置为高电平输出,再将列引脚设置为输入,并上拉电阻。通过读取列引脚来判断按键,没有按下的时候,因为上拉电阻的原因,会读取到高电平,如果有按键按下,导致某行和某列导通,因为行引脚是高电平,读取到的也还是高电平。而如果在同一时间只有一行设置为低电平,再检测列引脚,且按下的这个键就在唯一设置为低电平的行,那么在按下键所在的列就能检测到低电平。
根据这个思路,依次将一行设置为低电平,末尾会恢复高电平。在当前行依次读取每列的电平,如果出现了低电平,则说明设置为低电平的行和当前检测的列导通了,那么就确定了当前按下的行列。
const int row_pins[4] = {6, 7, 8, 9}; // 行引脚
const int col_pins[4] = {10, 11, 12, 13}; // 列引脚
// 矩阵按键字符映射
const char keys [4][4] =
{
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
char read_key()
{
char key = '\0';
for (int row = 0; row < 4; ++row)
{
// 将遍历到的行引脚设置为低电平
digitalWrite(row_pins[row], LOW);
for (int col = 0; col < 4; ++col)
{
// 遍历列引脚检测到低电平则确定行列
if (digitalRead(col_pins[col]) == LOW)
{
key = keys[row][col];
break;
}
}
// 行引脚恢复高电平
digitalWrite(row_pins[row], HIGH);
if (key != '\0')
{
break;
}
}
return key;
}
void setup()
{
Serial.begin(9600);
// 行引脚设置为输出模式,并置为高电平
for (int row = 0; row < 4; ++row)
{
pinMode(row_pins[row], OUTPUT);
digitalWrite(row_pins[row], HIGH);
}
// 列引脚设置为输入模式(上拉电阻)
for (int col = 0; col < 4; ++col)
{
pinMode(col_pins[col], INPUT_PULLUP);
}
}
void loop()
{
char key = read_key();
if (key != '\0')
{
Serial.println(key);
}
delay(200); // 避免抖动
}
外置电路
逐行逐列扫描是最直接的方法,但缺点是占用的引脚太多,因此考虑外置特殊电路来减少使用的引脚。考虑到两种方案:
加电阻
此方案占用 4 个模拟引脚,再加一个 GND,实际算独占 4 个引脚。
按键 1-4 行分别接到 A0-A3,第1列接到 GND ,第2-4列分别串联不同阻值的电阻后,再接到 GND。
A0-A3 负责读取(上拉电阻),因为不同列串联了不同阻值的电阻,那么不同列出现导通时模拟引脚就可以读取到不同的数值大小,并和电压有关。因为 Arduino UNO 的芯片中内置的上拉电阻是 20kΩ,所以我的方案是在第 2-4 列分别接 10kΩ、20kΩ 和 30kΩ 的电阻。
接线如表
\begin{array}{|c|c|c|}
\hline
矩阵按键 & 串联电阻(kΩ) & Arduino \\
1 & & A0 \\
\hline
2 & & A1 \\
\hline
3 & & A2 \\
\hline
4 & & A3 \\
\hline
5 & & GND \\
\hline
6 & 10 & GND \\
\hline
7 & 20 & GND \\
\hline
8 & 30 & GND \\
\hline
\end{array}
经过测试,第 1 列导通时,读取数值大约为 18;第二列导通时,读取数值大约为 250;第三列导通时,读取数值大约为 400;第 4 列导通时,读取数值大约为 500。根据测试结果来估计,Arduino 在上拉电阻时,内阻实际大约为 30kΩ(模拟信号数值范围为 0-1023,导通 30kΩ 列读取数值大约 500,说明电压分了一半,也就是内阻也是这么多,用其他列的数值简单计算也符合)。
通过读取模拟信号的数值范围可以确定列了,而行是独立对应一个引脚,在哪个引脚读取到符合条件范围的值就能确定所在行了。无按键按下的时候,开路,读引脚上拉电阻就会读取到 1023 左右的值,可以明确区分是否按下。根据这个逻辑,就可以写代码了。
// 矩阵按键字符映射
const char keys [4][4] =
{
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
// 根据模拟信号值判断所在列
int get_col(const int analog_num)
{
if (analog_num > 450)
{
return 3; // 第 4 列按下
}
else if (analog_num > 350)
{
return 2; // 第 3 列按下
}
else if (analog_num > 150)
{
return 1; // 第 2 列按下
}
else
{
return 0; // 第 1 列按下
}
}
char read_key()
{
int analog_num;
if ((analog_num = analogRead(A0)) < 600) // 低于 600 判断某行有按键按下
{
return keys[0][get_col(analog_num)];
}
else if ((analog_num = analogRead(A1)) < 600)
{
return keys[1][get_col(analog_num)];
}
else if ((analog_num = analogRead(A2)) < 600)
{
return keys[2][get_col(analog_num)];
}
else if ((analog_num = analogRead(A3)) < 600)
{
return keys[3][get_col(analog_num)];
}
return '\0';
}
void setup()
{
Serial.begin(9600);
for (int row = A0; row <= A3; ++row)
{
pinMode(row, INPUT_PULLUP);
}
}
void loop()
{
char key = read_key();
if (key != '\0')
{
Serial.println(key);
}
delay(200);
}
验证可行
加(电阻+数模转换模块)
加电阻的方案已经减少了一半的独占引脚,但还是有 4 个独占的。考虑到使用的是模拟引脚,那么就可以使用数模转换模块,比如这里计划使用 PCF8591(树莓派没有模拟引脚,之前买来在树莓派上使用的,同时这个方案在树莓派上也可以采用),这个模块刚好有 4 个模拟读引脚。而信号输出是通过 IIC,那么就可以共用线路,不同设备是可以通过地址区分的,相当于完全不独自占用 Arduino 引脚。
这里 PCF8591 使用库为:
PCF8591_library 1.1.1:https://github.com/xreef/PCF8591_library
我查一下资料,没找到说 PCF8591 支持上拉电阻的,使用的库也没有提供上拉电阻的操作,那应该就是不支持了,上拉电阻就得自己实现了,电路会更复杂一点。
我没专门学过电路这些,不是很懂怎么画图,就画了一个接线的草图。
AIN0、AIN1、AIN2、AIN3 四个模拟读引脚旁边都接了一个电阻,电阻的另一端接到 5V 引脚,这个就是上拉电阻,把读引脚挂到高电位,提高稳定性,不然没有按键的时候读值很容易波动。
PCF8591 的模拟数值范围和 Arduino 的模拟引脚不同,取值范围是 0-255。根据测试结果,第一列导通时,数值大约为 1;第二列导通时,数值大约为 86;第三列导通时,数值大约为 128;第四列导通时,数值大约为 155。
代码实现逻辑还是和前面一种方案一样
#include "PCF8591.h"
#define PCF8591_I2C_ADDRESS 0x48 // I2C 地址
PCF8591 pcf8591(PCF8591_I2C_ADDRESS);
const char keys [4][4] =
{
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};
// 根据模拟信号值判断所在列
int get_col(const int analog_num)
{
if (analog_num > 140)
{
return 3; // 第 4 列按下
}
else if (analog_num > 110)
{
return 2; // 第 3 列按下
}
else if (analog_num > 50)
{
return 1; // 第 2 列按下
}
else
{
return 0; // 第 1 列按下
}
}
char read_key()
{
PCF8591::AnalogInput ai = pcf8591.analogReadAll();
if (ai.ain0 < 180) // 低于 180 判断某行有按键按下
{
return keys[0][get_col(ai.ain0)];
}
else if (ai.ain1 < 180)
{
return keys[1][get_col(ai.ain1)];
}
else if (ai.ain2 < 180)
{
return keys[2][get_col(ai.ain2)];
}
else if (ai.ain3 < 180)
{
return keys[3][get_col(ai.ain3)];
}
return '\0';
}
void setup()
{
Serial.begin(9600);
pcf8591.begin();
}
void loop()
{
char key = read_key();
if (key != '\0')
{
Serial.println(key);
}
delay(200);
}
验证可行