Arduino

Кнопочная матрица 4х4: сканирование клавиатуры

Введение

Кнопочная матрица 4×4 (16 кнопок, организованных в виде клавиатуры с цифрами и символами) подключается всего 8 проводами (4 строки + 4 столбца) вместо 16, которые потребовались бы для индивидуального подключения каждой кнопки — за счёт техники сканирования матрицы, аналогичной мультиплексированию из предыдущих статей, но применённой не к выводу, а ко вводу данных. В статье разберём принцип сканирования и обработку нажатий клавиш.

Концепция

Каждая кнопка матрицы физически соединяет одну строку и один столбец при нажатии — сканирование заключается в последовательной активации каждой строки (по одной) и проверке, какие столбцы при этом замкнуты (что означает нажатую кнопку на пересечении активной строки и замкнутого столбца). Готовая библиотека Keypad.h инкапсулирует эту логику, предоставляя простой интерфейс получения нажатой клавиши без необходимости вручную реализовывать алгоритм сканирования.

Пример кода

// firmware/src/main.cpp — кнопочная матрица 4х4 через библиотеку Keypad
#include <Keypad.h>

const byte ROWS = 4;
const byte COLS = 4;

char keyLayout[ROWS][COLS] = {
    {'1', '2', '3', 'A'},
    {'4', '5', '6', 'B'},
    {'7', '8', '9', 'C'},
    {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = {2, 3, 4, 5};
byte colPins[COLS] = {6, 7, 8, 9};

Keypad keypad = Keypad(makeKeymap(keyLayout), rowPins, colPins, ROWS, COLS);

void setup()
{
    Serial.begin(9600);
    delay(100);
    Serial.println("READY");
}

void loop()
{
    const char key = keypad.getKey();
    if (key != NO_KEY) {
        Serial.print("KEYPAD:");
        Serial.println(key);
    }
}
// keypadinputwidget.h — приём нажатий клавиатуры на стороне Qt, например для ввода PIN-кода
#pragma once
#include <QObject>
#include <QString>
#include "arduinoconnection.h"

class KeypadInputWidget : public QObject
{
    Q_OBJECT
public:
    explicit KeypadInputWidget(ArduinoConnection *connection, QObject *parent = nullptr)
        : QObject(parent)
    {
        connect(connection, &ArduinoConnection::lineReceived, this, &KeypadInputWidget::onLineReceived);
    }

private slots:
    void onLineReceived(const QString &line)
    {
        if (!line.startsWith("KEYPAD:")) return;

        const QChar key = line.mid(7).at(0);

        if (key == '#') {
            emit pinEntryConfirmed(m_enteredPin);
            m_enteredPin.clear();
        } else if (key == '*') {
            m_enteredPin.clear(); // символ '*' используется как сброс введённого значения
        } else if (key.isDigit()) {
            m_enteredPin.append(key);
            emit pinDigitEntered(m_enteredPin.length());
        }
    }

signals:
    void pinDigitEntered(int currentLength);
    void pinEntryConfirmed(const QString &pin);

private:
    QString m_enteredPin;
};

Пояснения к коду

Keypad инициализируется картой символов (keyLayout), соответствующей физической раскладке кнопок матрицы, и массивами пинов строк/столбцов — keypad.getKey() инкапсулирует весь алгоритм сканирования и дебаунса, возвращая либо символ нажатой клавиши, либо специальное значение NO_KEY, если в данный момент ничего не нажато; это значительно проще и надёжнее, чем реализация сканирования матрицы вручную.

KeypadInputWidget показывает практическую логику использования клавиатурного ввода для типичного сценария — ввод PIN-кода, где цифровые клавиши накапливаются в m_enteredPin, # подтверждает введённый код (испуская сигнал с полным значением), а * сбрасывает накопленный ввод — этот паттерн структурно похож на интерфейс банкомата или домофона с кнопочной панелью, что является одним из самых типичных применений этого конкретного модуля.

Подводные камни

  • Использование произвольных, не ШИМ-зависимых цифровых пинов для строк/столбцов матрицы конфликтует с другими модулями, уже занимающими эти пины в более сложном проекте, объединяющем несколько модулей одновременно — при подключении сразу многих модулей к одной плате Arduino важно заранее распланировать использование пинов, чтобы избежать конфликтов, особенно учитывая, что у Arduino Uno относительно немного цифровых пинов в сумме.
  • Библиотека Keypad по умолчанию выполняет сканирование синхронно внутри вызова getKey(), что означает небольшую, но не нулевую задержку при каждом вызове — для большинства практических случаев (нечастые нажатия клавиш) это не проблема, но если getKey() вызывается в очень «горячем» цикле наряду с другой требовательной по времени логикой (мультиплексирование матрицы светодиодов), суммарное время выполнения loop() стоит явно учитывать.
  • Дребезг контактов кнопок матрицы, хотя библиотека Keypad обычно справляется с этим внутренне через встроенный механизм дебаунса, при очень дешёвых или изношенных матрицах иногда может потребовать дополнительной, явной настройки чувствительности дебаунса через keypad.setDebounceTime(), если стандартные настройки библиотеки дают ложные повторные срабатывания на конкретном физическом экземпляре матрицы.
  • Передача только символа нажатой клавиши без какой-либо защиты протокола от потери отдельных нажатий при быстром вводе — если пользователь нажимает клавиши матрицы значительно быстрее, чем успевает выполниться полный цикл loop() Arduino (что особенно вероятно, если в том же loop() происходит другая, более тяжёлая по времени работа), отдельные нажатия могут быть упущены; для критичных по надёжности сценариев ввода (например, защищённый код доступа) стоит явно протестировать поведение при быстром, «пулемётном» нажатии клавиш и убедиться, что прошивка успевает обработать каждое нажатие.