Arduino 4×4 矩阵按键开发

最近更新于 2024-05-05 12:32

前言

file
file

这种矩阵键盘,从左到右四根线依次对应一到四行,最后四根线对应一至四列,按键的时候某行和某列导通就能确定是按下的是哪个键了。后面叙述 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);
}

file

单键检测

逐行逐列扫描

接线对照表

\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); // 避免抖动
}

file

外置电路

逐行逐列扫描是最直接的方法,但缺点是占用的引脚太多,因此考虑外置特殊电路来减少使用的引脚。考虑到两种方案:

加电阻

此方案占用 4 个模拟引脚,再加一个 GND,实际算独占 4 个引脚。
按键 1-4 行分别接到 A0-A3,第1列接到 GND ,第2-4列分别串联不同阻值的电阻后,再接到 GND。
A0-A3 负责读取(上拉电阻),因为不同列串联了不同阻值的电阻,那么不同列出现导通时模拟引脚就可以读取到不同的数值大小,并和电压有关。因为 Arduino UNO 的芯片中内置的上拉电阻是 20kΩ,所以我的方案是在第 2-4 列分别接 10kΩ、20kΩ 和 30kΩ 的电阻。
file

接线如表

\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);
}

验证可行
file

加(电阻+数模转换模块)

加电阻的方案已经减少了一半的独占引脚,但还是有 4 个独占的。考虑到使用的是模拟引脚,那么就可以使用数模转换模块,比如这里计划使用 PCF8591(树莓派没有模拟引脚,之前买来在树莓派上使用的,同时这个方案在树莓派上也可以采用),这个模块刚好有 4 个模拟读引脚。而信号输出是通过 IIC,那么就可以共用线路,不同设备是可以通过地址区分的,相当于完全不独自占用 Arduino 引脚。

这里 PCF8591 使用库为:
PCF8591_library 1.1.1:https://github.com/xreef/PCF8591_library

我查一下资料,没找到说 PCF8591 支持上拉电阻的,使用的库也没有提供上拉电阻的操作,那应该就是不支持了,上拉电阻就得自己实现了,电路会更复杂一点。
file

我没专门学过电路这些,不是很懂怎么画图,就画了一个接线的草图。
file

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);
}

验证可行
file

Arduino 4×4 矩阵按键开发
Scroll to top