Введение
Чтение состояния 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) на практике часто даёт неверные результаты при достаточно быстром вращении; для надёжной работы стоит использовать проверенную, готовую библиотеку декодирования квадратурных сигналов, а не реализовывать алгоритм с нуля без должного тестирования на реальном, быстро вращаемом энкодере.