Введение
Ключевая возможность 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-цикла) для штатного освобождения ресурсов даже при нештатном завершении.