Embedded

GPIO-прерывания и обработка кнопок/энкодеров в реальном времени

Введение

Чтение состояния GPIO через readValue() из статьи 343 в цикле опроса (polling) расходует процессорное время даже когда состояние не меняется, и может пропустить короткие, быстрые изменения сигнала между опросами. В статье разберём событийный подход — прерывания GPIO, уведомляющие приложение немедленно при изменении сигнала, без постоянного активного опроса, что особенно важно для отзывчивой обработки нажатий кнопок и вращения энкодеров.

Концепция

libgpiod поддерживает запрос линии в режиме отслеживания событий (edge detection) — нарастающий фронт (rising edge, переход с низкого уровня на высокий), спадающий (falling edge), или оба — и блокирующее ожидание следующего события через gpiod_line_event_wait(), что эффективнее активного опроса, поскольку поток приложения не расходует CPU в ожидании, а блокируется на уровне ядра до фактического появления события. Для интеграции с циклом событий Qt такое блокирующее ожидание выполняется в отдельном потоке (аналогично паттерну GrpcWorker из статьи 327), передающем обнаруженные события в основной поток через сигналы.

Пример кода: GPIO-прерывания и обработка

// gpiobuttonwatcher.h — отдельный поток, ожидающий нажатий кнопки через GPIO-прерывания
#pragma once
#include <QThread>
#include <gpiod.h>

class GpioButtonWatcher : public QThread
{
    Q_OBJECT
public:
    GpioButtonWatcher(const QString &chipName, unsigned int lineNumber, QObject *parent = nullptr)
        : QThread(parent), m_chipName(chipName), m_lineNumber(lineNumber) {}

protected:
    void run() override
    {
        gpiod_chip *chip = gpiod_chip_open_by_name(m_chipName.toStdString().c_str());
        gpiod_line *line = gpiod_chip_get_line(chip, m_lineNumber);

        // Запрос линии с отслеживанием СПАДАЮЩЕГО фронта — типично для кнопки,
        // подключённой с pull-up резистором (нажатие замыкает на землю, сигнал падает)
        gpiod_line_request_falling_edge_events(line, "qt-button-watcher");

        while (!isInterruptionRequested()) {
            // Тайм-аут 200 мс — периодически проверяем флаг прерывания потока,
            // не блокируясь полностью бесконечно (что не дало бы штатно завершить поток)
            timespec timeout{0, 200000000};
            int result = gpiod_line_event_wait(line, &timeout);

            if (result == 1) {
                gpiod_line_event event;
                gpiod_line_event_read(line, &event);
                emit buttonPressed(); // сигнал передаётся в основной поток автоматически, потокобезопасно
            }
            // result == 0 означает тайм-аут без события — нормальная ситуация, просто проверяем флаг снова
        }

        gpiod_line_release(line);
        gpiod_chip_close(chip);
    }

signals:
    void buttonPressed();

private:
    QString m_chipName;
    unsigned int m_lineNumber;
};
// Энкодер — два GPIO-пина (A и B), сигналы которых меняются в определённой последовательности
// при вращении, что позволяет определить направление вращения
class RotaryEncoderWatcher : public QThread
{
    Q_OBJECT
protected:
    void run() override
    {
        // Концептуально: оба пина A и B настраиваются на отслеживание ОБОИХ фронтов,
        // и направление вращения определяется по тому, какой из двух пинов изменился первым
        // (полная реализация декодирования квадратурного сигнала выходит за рамки примера)
        while (!isInterruptionRequested()) {
            // ... ожидание событий на обоих пинах, анализ последовательности изменений ...
        }
    }

signals:
    void rotatedClockwise();
    void rotatedCounterClockwise();
};

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

gpiod_line_request_falling_edge_events() настраивает линию на отслеживание именно интересующего типа изменения сигнала (спадающий фронт для кнопки с pull-up резистором), а gpiod_line_event_wait() с явным тайм-аутом (200 мс) — компромисс между эффективностью истинно блокирующего ожидания и необходимостью периодически проверять флаг isInterruptionRequested() для штатного, управляемого завершения потока при закрытии приложения (аналогично паттерну из GrpcWorker, статья 327) — без тайм-аута поток заблокировался бы полностью неопределённо долго, не давая возможности корректно его остановить. Сигнал buttonPressed(), испускаемый из рабочего потока, автоматически и потокобезопасно доставляется в слоты, подключённые в основном потоке — это стандартный, многократно использованный в цикле паттерн межпоточного взаимодействия через сигналы и слоты Qt.

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

  • Дребезг контактов механической кнопки (bouncing), приводящий к множественным быстрым ложным срабатываниям прерывания при одном физическом нажатии — аналогично подводному камню из основного Arduino-цикла про кнопки, аппаратный дребезг контактов не устраняется самим механизмом прерываний, и требуется программная защита (игнорирование повторных событий в течение короткого интервала после первого, например 50 мс) поверх обработки самого прерывания.
  • Использование слишком короткого тайм-аута gpiod_line_event_wait() (например, 1 мс вместо 200 мс в примере) «для большей отзывчивости остановки потока» — это превращает блокирующее ожидание прерывания обратно в фактический активный опрос с высокой частотой, расходующий заметно больше CPU, чем разумно для типичного случая нечастых нажатий кнопки; тайм-аут стоит выбирать достаточно большим для эффективности (минимизации расхода CPU), но не настолько большим, чтобы заметно задерживать штатное завершение потока при закрытии приложения.
  • Отсутствие учёта того, что несколько разных GPIO-линий, требующих одновременного отслеживания событий, обычно требуют несколько отдельных потоков (как в примере с раздельными GpioButtonWatcher для каждой кнопки), поскольку gpiod_line_event_wait() блокируется на конкретной, заранее заданной линии — для большого количества одновременно отслеживаемых линий стоит рассмотреть gpiod_line_event_wait_bulk(), позволяющий ожидать события сразу на нескольких линиях в одном потоке, что эффективнее множества отдельных потоков для каждой отдельной линии.
  • Игнорирование декодирования квадратурного сигнала энкодера упрощённо, без учёта дребезга и потерянных событий при быстром вращении — реализация определения направления вращения энкодера значительно сложнее простого отслеживания одной кнопки, и наивная реализация (как упрощённо показано в концептуальном примере RotaryEncoderWatcher) на практике часто даёт неверные результаты при достаточно быстром вращении; для надёжной работы стоит использовать проверенную, готовую библиотеку декодирования квадратурных сигналов, а не реализовывать алгоритм с нуля без должного тестирования на реальном, быстро вращаемом энкодере.