Embedded

GPIO из Qt-приложения: libgpiod и доступ без root

Введение

Ключевая возможность Raspberry Pi, отсутствующая у обычного desktop-компьютера — GPIO-пины (General Purpose Input/Output), позволяющие напрямую управлять светодиодами, реле, считывать состояние кнопок и подключать различные модули, аналогично цифровым пинам Arduino из основного цикла. В статье разберём современный, рекомендуемый способ доступа к GPIO из Linux-приложения — библиотеку libgpiod, заменившую устаревший и менее безопасный прямой доступ через файл /sys/class/gpio.

Концепция

libgpiod — библиотека, предоставляющая безопасный, современный интерфейс доступа к GPIO через символьные устройства ядра Linux (/dev/gpiochipN), в отличие от устаревшего sysfs-интерфейса, который был официально объявлен устаревшим (deprecated) в ядре Linux. Доступ без прав root достигается через правильную настройку группы пользователей (gpio) и udev-правил, дающих обычному пользователю права на устройства /dev/gpiochipN без необходимости запускать всё приложение с повышенными привилегиями — что особенно важно для GUI-приложений Qt, которые крайне нежелательно запускать от root по соображениям безопасности.

Пример кода: GPIO из Qt-приложения

# Установка библиотеки и проверка доступных GPIO-чипов
sudo apt install libgpiod-dev gpiod

gpiodetect  # список доступных чипов GPIO
gpioinfo gpiochip0  # список линий конкретного чипа с их текущим состоянием
# Настройка доступа без root — добавление пользователя в группу gpio
# и udev-правило, выдающее группе gpio права на устройства GPIO
sudo usermod -aG gpio $USER

# /etc/udev/rules.d/99-gpio.rules
echo 'SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpio", MODE="0660"' | \
    sudo tee /etc/udev/rules.d/99-gpio.rules
sudo udevadm control --reload-rules
// gpiocontroller.h — обёртка над libgpiod для использования из Qt-приложения
#pragma once
#include <gpiod.h>
#include <QObject>
#include <stdexcept>

class GpioController : public QObject
{
    Q_OBJECT
public:
    explicit GpioController(const QString &chipName, unsigned int lineNumber, QObject *parent = nullptr)
        : QObject(parent)
    {
        m_chip = gpiod_chip_open_by_name(chipName.toStdString().c_str());
        if (!m_chip) {
            throw std::runtime_error("Не удалось открыть GPIO-чип: " + chipName.toStdString());
        }

        m_line = gpiod_chip_get_line(m_chip, lineNumber);
        if (!m_line) {
            gpiod_chip_close(m_chip);
            throw std::runtime_error("Не удалось получить линию GPIO");
        }
    }

    ~GpioController()
    {
        gpiod_line_release(m_line);
        gpiod_chip_close(m_chip);
    }

    void setupAsOutput()
    {
        if (gpiod_line_request_output(m_line, "qt-gpio-demo", 0) < 0) {
            throw std::runtime_error("Не удалось настроить линию как выход");
        }
    }

    void setValue(bool high)
    {
        gpiod_line_set_value(m_line, high ? 1 : 0);
    }

    bool readValue()
    {
        return gpiod_line_get_value(m_line) == 1;
    }

private:
    gpiod_chip *m_chip;
    gpiod_line *m_line;
};
// Использование — мигание светодиодом, подключённым к GPIO17, через таймер Qt
GpioController led("gpiochip0", 17);
led.setupAsOutput();

auto *timer = new QTimer();
bool state = false;
QObject::connect(timer, &QTimer::timeout, [&led, &state]() {
    state = !state;
    led.setValue(state);
});
timer->start(500); // мигание каждые 500 мс — событийно, не блокируя остальной интерфейс

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

GpioController инкапсулирует жизненный цикл объектов libgpiod (gpiod_chip, gpiod_line) через конструктор/деструктор C++, что гарантирует освобождение ресурсов (gpiod_line_release, gpiod_chip_close) даже при исключении или раннем выходе из текущего блока кода — паттерн RAII, последовательно применяемый во всех статьях основного цикла для управления ресурсами. Использование QTimer для мигания светодиодом (а не блокирующего цикла со sleep) показывает интеграцию GPIO-управления с циклом событий Qt — приложение остаётся отзывчивым к остальным событиям интерфейса, пока таймер периодически переключает состояние линии GPIO.

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

  • Использование устаревшего sysfs-интерфейса (/sys/class/gpio/export) вместо libgpiod в новом, разрабатываемом сейчас коде — несмотря на обилие старых примеров в интернете, использующих именно sysfs, этот интерфейс официально устарел в современных версиях ядра Linux и может быть полностью удалён в будущих версиях; весь новый код стоит писать с использованием libgpiod, даже если sysfs ещё технически работает в текущей версии ОС.
  • Запуск GUI Qt-приложения с правами root просто для упрощённого доступа к GPIO, минуя настройку группы gpio и udev-правил — запуск GUI-приложения с правами root существенно увеличивает потенциальный ущерб от любой уязвимости в самом приложении или используемых им библиотеках, и правильная настройка непривилегированного доступа (как показано в примере) — значимо более безопасная практика, оправдывающая немного больше первоначальной настройки.
  • Открытие одной и той же линии GPIO одновременно из нескольких разных процессов/приложений — libgpiod обычно не позволяет двум разным процессам одновременно владеть одной и той же линией в режиме вывода, и попытка такого конфликтующего доступа приведёт к ошибке получения линии в одном из приложений; для приложений, которым нужен общий доступ к одним и тем же GPIO-линиям из разных процессов, требуется явная координация (единственный процесс-владелец GPIO, общающийся с остальными через другой механизм межпроцессного взаимодействия).
  • Забытое освобождение GPIO-линии при аварийном завершении приложения (например, через сигнал SIGKILL, не дающий деструктору GpioController выполниться) — линия может остаться «занятой» предыдущим процессом до перезагрузки или явного освобождения вручную через gpioset/аналогичные инструменты командной строки, что требует явной обработки сигналов завершения (аналогично некоторым подводным камням из основного embedded-цикла) для штатного освобождения ресурсов даже при нештатном завершении.