Orange Pi Zero 3 — знайомимося з GPIO

GPIO (General-Purpose Input/Output або контакти загального призначення) — це фундаментальна концепція, що прийшла у світ потужних SoC(System-on-a-Chip) безпосередньо зі світу мікроконтролерів.

Це головний місток для взаємодії вашого одноплатного комп’ютера з простими зовнішніми пристроями, на додачу до більш складних апаратних інтерфейсів (таких як I2C, SPI, UART чи I2S).

Якщо спеціалізовані протоколи передають потоки даних, то GPIO дозволяє працювати на найнижчому рівні — керувати окремими цифровими сигналами. У кожного піна можна:

  • Зчитувати стан (input): Наприклад, перевірити, чи натиснута кнопка, є сигнал “1” чи “0”?.

  • Керувати станом (output): Наприклад, увімкнути світлодіод, активувати реле чи подати сигнал на інший компонент.

У цій статті я хочу зробити швидкий огляд методів роботи з GPIO на Orange Pi Zero 3 (базується на SoC Allwinner H618). Звісно, ці методи не унікальні і можуть бути застосовані й на інших одноплатних комп’ютерах (особливо на базі чіпів Allwinner), але фокус буде саме на Zero 3.

Увага: Важливе застереження! Логічний рівень на GPIO пінах Orange Pi Zero 3 становить 3.3V. Не варто подавити вищу напругу (наприклад, 5В) на будь-який пін, це 100-відсотково пошкодить процесор. Якщо це дісно потрібно, то використовуйте логічні перетворювачі рівнів (logic level shifter) для зовнішніх 5в пристроїв.

Orange Pi Zero 3 має декілька роз’ємів для доступу до GPIO:

  1. 26-контактний роз’єм (DIP26): Це основний роз’єм. Його розпіновка частково сумісна зі стандартом Raspberry Pi.

  2. 13-контактний роз’єм (SIP13): Додатковий роз’єм, що виводить ще декілька ліній GPIO та спеціалізовані інтерфейси (USB/Auidio/Composite).

  3. 3-контактний роз’єм UART: Окремий роз’єм для послідовної консолі


Розпіновка 26-контактного роз’єму (2x13)

Розпіновка 13-контактного роз’єму (1x13)

Для роботи з GPIO в Linux існує декілька основних механізмів:

  • Прямий доступ до регістрів: Це “найнижчий”, апаратний рівень. Цей метод вимагає прав суперкористувача (root), а головне — він є повністю специфічним для кожного SoC. Потрібно знати точні адреси апаратних регістрів, що робить цей підхід не портативним і складним.

  • GPIO Sysfs: Класичний спосіб взаємодії через файлову систему за шляхом /sys/class/gpio/. Хоча на деяких дистрибутивах (як-от Armbian) він все ще може бути увімкнений за замовчуванням, цей інтерфейс офіційно вважається застарілим. Його підтримку поступово видаляють з нових версій ядра Linux, тому покладатися на нього не варто. Посилання на документацію ядра.

  • GPIO Character Device: Це новий стандартний механізм, який працює через символьні пристрої у /dev/ (наприклад, /dev/gpiochip0, /dev/gpiochip1 і т.д.). Для зручної взаємодії з цими пристроями з простору користувача (userspace) використовується бібліотека libgpiod.

Розглянемо кожен із цих методів детальніше, щоб оцінити їхню зручність та практичність у реальних задачах.

Прямий доступ до регістрів

Спробуємо “поблимати” світлодіодом на піні PC5 без будь-яких бібліотек, спілкуючись із “залізом” напряму.

Ще одне застереження: Цей метод є надзвичайно небезпечним. Прямий запис у /dev/mem дає вам повний контроль над усією фізичною пам’яттю системи. Будь яка помилка в одній адресі чи значенні може призвести до того що система або зависне, або навіть пошкодить файлову систему. Тому краще мати окрему SD картку без важливих даних і експерементувати з нею.

Для початку встановимо потрібні інструменти — C++ компілятор та CMake для налаштування збірки:

sudo apt install g++ cmake

Нам знадобиться технічна документація (datasheet) на SoC. Orange Pi Zero 3 використовує H618, але для наших експериментів з GPIO він практично ідентичний Allwinner H616. Документацію на H616 можна знайти за адресою: H616 User Manual V1.0.

Маю зауважити, що H618 використовує механізм MMIO (Memory-Mapped Input/Output) — тобто введення/виведення відображене у пам’ять. Це означає, що всі регістри керування апаратурою не мають окремого простору, а знаходяться безпосередньо у загальному адресному просторі фізичної пам’яті. Щоб увімкнути пін, нам не потрібні спеціальні інструкції, як in/out на x86. Нам потрібно просто записати правильне значення за правильною фізичною адресою пам’яті.

Саме тому для прямого доступу до апаратури ми використаємо системний виклик mmap() для файлу-пристрою /dev/mem. Ця пара інструментів дозволяє відобразити реальний фізичний адресний простір процесора у віртуальний адресний простір нашої програми.

Це дає нам можливість читати й писати у фізичні регістри так, ніби це звичайні змінні у нашому коді.

Згідно з документацією (розділ 9.6.4. Register List), для регістрів GPIO виділено два основні блоки:

  • Для портів C, G, H, I — блок, що починається з адреси 0x0300B000.

  • Для порту L — блок, що починається з адреси 0x07022000.

Ми будемо експериментувати з портами з першого блоку. Базова адреса цього блоку (0x0300B000) якраз вирівняна по межі 4Кб сторінки пам’яті. Це дуже важливо, оскільки системний виклик mmap() вимагає, щоб зсув був кратним розміру сторінки. Нам пощастило, і ми можемо передавати цю адресу напряму, без додаткових обчислень.

Наш C++ код для “мапінгу” (відображення) цієї фізичної адреси у віртуальний простір нашої програми виглядатиме так:

constexpr off_t PIO_BASE_ADDR = 0x300B000;
constexpr size_t PAGE_SIZE = 4096;

int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
if (mem_fd < 0) {
    std::cerr << "Can't open /dev/mem: " << strerror(errno) << std::endl;
    return 1;
}

void* map_base = mmap(
    nullptr,
    PAGE_SIZE,
    PROT_READ | PROT_WRITE,  // нам потрібно читати та писати
    MAP_SHARED,
    mem_fd,       // файловий дескриптор /dev/mem
    PIO_BASE_ADDR // фізична адреса (має бути вирівняна по розміру сторінки 4Kb)
);
close(mem_fd); // дескриптор файлу більше не потрібен після mmap

if (map_base == MAP_FAILED) {
    std::cerr << "mmap error: " << strerror(errno) << std::endl;
    return 1;
}

Якщо код виконавcя без помилок, змінна map_base тепер є вказівником, що вказує на початок віртуальної пам’яті, яка “під капотом” насправді є нашими фізичними регістрами.

Памʼятаю з часів 8-бітних AVR, що для простого мерехтіння світлодіодом потрібно було зробити дві речі:

  • Налаштувати напрямок піна (вхід/вихід) у відповідному біті регістра напрямку (DDRx).

  • Записати одиницю чи нуль у біт регістра даних (PORTx).

Якщо ж було потрібно читати стан кнопки, додатково можна було увімкнути внутрішню підтяжку (pull-up) до VCC.

Цей фундаментальний принцип не змінився. Звісно, на H618 кожен пін має набагато більше опцій (мультиплексування для I2C/SPI/UART, вибір сили струму тощо), але базовий цифровий ввід/вивід працює дуже схоже.

Нам знадобляться два типи регістрів: конфігураційні (аналог DDR) та регістри даних (аналог PORT).

Згідно з документацією, вони мають наступні зміщення відносно базової адреси порту (PIO_BASE_ADDR + n * 0x24):

Регістр Зміщення Призначення
Pn_CFG0 n*0x0024+0x00 Port n Configure Register 0 (Піни 0-7)
Pn_CFG1 n*0x0024+0x04 Port n Configure Register 1 (Піни 8-15)
Pn_CFG2 n*0x0024+0x08 Port n Configure Register 2 (Піни 16-23)
Pn_CFG3 n*0x0024+0x0С Port n Configure Register 3 (Піни 24-31)
Pn_DAT n*0x0024+0x10 Port n Data Register

де n — це індекс порту.

Як ми вже бачили, нас цікавлять порти C, F, G, H, I. Для програмного доступу вони ідентифікуються числовими індексами, що відповідають їхньому порядковому номеру в алфавіті (A=0, B=1, C=2, D=3, E=4, F=5, і т.д.).

Для зручності в коді, ці індекси можна визначити у enum:
enum class GPIOPort: uint8_t {
    PC = 2,
    PF = 5,
    PG = 6,
    PH = 7,
    PI = 8
};

Найважливіша відмінність від AVR: кожному піну для налаштування функції (вхід, вихід, NAND, SDIO тощо) потрібно 4 біти (а не 1). Тому один 32-бітний регістр Pn_CFG може налаштувати лише 8 пінів.

  • Pn_CFG0 відповідає за піни 0-7

  • Pn_CFG1 — за піни 8-15

  • і так далі…

На щастя, регістр даних (Pn_DAT) влаштований простіше: один 32-бітний регістр містить дані всіх 32 пінів порту (1 біт на 1 пін), що вже більше схоже на звичний PORT в AVR.

Візьмемо для прикладу наш пін - PC5. Окрім входу та виходу, він може виконувати додаткові функції (мультиплексування), пов’язані з NAND або SD-картою. Оскільки PC5 — це 5-й пін порту C (нумерація з 0), за його налаштування відповідають 4 біти у регістрі PC_CFG0, починаючи з бітової позиції 5 * 4 = 20. Тобто нас цікавлять біти [23:20].

Згідно з документацією, ці біти (PC5_SELECT) означають:

  • 000: Input (Вхід)

  • 001: Output (Вихід)

  • 010: NAND_RE

  • 011: SDC2_CLK

  • 100: Reserved (Резерв)

  • 101: BOOT_SEL

Отже задача — записати туди 001, щоб налаштувати пін як Output.

constexpr uint8_t PIN_ID = 5; // номер піна який будемо перемикати
constexpr PIOPort PIN_PORT = PIOPort::PC; // порт піна

// volatile гарантує, що компілятор не оптимізує наші звернення до пам'яті
volatile uint32_t* reg32_ptr = (volatile uint32_t*)map_base;
uint32_t pin_port = static_cast(PIN_PORT);
uint32_t cfg0_off = pin_port * 0x24 + 0x00; // Зміщення до Pn_CFG0
uint32_t dat_off = pin_port * 0x24 + 0x10; // Зміщення до Pn_DAT

// записуємо 001 - Output
uint32_t current_value = *(reg32_ptr + cfg0_off/4); // читаємо Pn_CFG0
current_value &= ~(0xF << PIN_ID * 4); 
// скидаємо все в 0000 в потрібній бітовій позиції
current_value |= (0x1 << PIN_ID * 4);  // записуємо 1 на початку
*(reg32_ptr + cfg0_off/4) = current_value;

for(auto i = 0; i < 10; i++) { // 10 разів міняємо стан
    current_value = *(reg32_ptr + dat_off/4);
    if (!(current_value & (1 << PIN_ID))) {
        std::cout << "Pin is LOW, setting it HIGH" << std::endl;
        current_value |= (1 << PIN_ID);  // set bit to 1
    } else {
        std::cout << "Pin is HIGH, set LOW" << std::endl;
        current_value &= ~(1 << PIN_ID);  // clear bit
    }
    *(reg32_ptr + dat_off/4) = current_value;
    std::this_thread::sleep_for(500ms);
}
if (munmap(map_base, PAGE_SIZE) == -1) {
    std::cerr << "munmap error: " << strerror(errno) << std::endl;
}

Повний код цього прикладу можна знайти на GitHub.

Цей метод є “найближчим до заліза”, що має як унікальні переваги, так і дуже серйозні недоліки.

Переваги:

  • Максимальна швидкість: Це, безперечно, найшвидший спосіб керувати піном з простору користувача (user-space). Ви оминаєте будь-які абстракції ядра, додаткові системні виклики (окрім mmap на старті) чи парсинг файлів.

  • Нуль залежностей: Код не потребує жодних сторонніх бібліотек (libgpiod) чи увімкнених модулів ядра (як sysfs). Все, що потрібно — це доступ до /dev/mem.

  • Навчальна цінність: Цей підхід змушує вас відкрити документацію і побачити, як процесор насправді керує апаратурою на рівні бітів та регістрів.

  • Атомарний: Можна перемикати піни в одному порті одночасно.

Недоліки:

  • Потрібні права root: Робота з /dev/mem доступна лише суперкористувачу, що є величезною загрозою безпеці та поганою практикою для звичайних застосунків.

  • Надзвичайна небезпека: Помилка в одній адресі чи значенні може миттєво “повісити” систему, пошкодити файлову систему.

  • Повна відсутність портативності: Цей код буде працювати лише на Allwinner H616/H618 та, можливо, інших SoC Allwinner зі схожою картою регістрів. Він абсолютно марний на будь-якій іншій платформі (Raspberry Pi, Rockchip тощо).

  • Неможливість обробки переривань (Interrupts): Оскільки ми працюємо в user-space, ми не можемо підписатися на апаратні переривання (наприклад, “повідомити мене, коли натиснуто кнопку”). Єдиний доступний спосіб — це постійне опитування (polling) піна у циклі, що марнує цінні ресурси CPU.

Доступ до пінів через GPIO Sysfs

Тепер розглянемо “цивілізований”, хоча й застарілий метод - керування через GPIO Sysfs.

Додаткову інформацію по цьому методі можна знайти за посиланнями:

GPIO sysfs не потребує додаткових бібліотек чи залежностей. Вся взаємодія відбувається через читання та запис у файли в каталозі /sys/class/gpio.

Для початку роботи з піном його потрібно “експортувати”, щоб він з’явився у sysfs. Для цього потрібно записати глобальний індекс піна в спеціальний файл /sys/class/gpio/export.

Як розрахувати цей індекс? Формула проста:

індекс_піна = індекс_порта * 32 + номер_піна_в_порті

Індекси портів ми визначили раніше (PC = 2, PF = 5, і т.д.). Отже, для нашого тестового піна PC5:

  • Порт C має індекс 2.

  • Номер піна — 5.

індекс_піна= (2 * 32) + 5 = 69

До речі, якщо комусь ліньки рахувати, то ці глобальні індекси я вже вказав на розпіновкці на початку статті.

Ось приклад С++ коду для експортування потрібного нам PC5:

constexpr uint8_t PIN_ID = 5; // номер піна який будемо перемикати
constexpr PIOPort PIN_PORT = PIOPort::PC; // порт піна

uint16_t pinId = static_cast(PIN_PORT) * 32 + PIN_ID;
std::string pinStr = std::to_string(pinId);

// export pin
int exportFd = open("/sys/class/gpio/export", O_WRONLY);
if (exportFd < 0) {
    std::cerr << "Can't open /sys/class/gpio/export: " << strerror(errno) 
    	<< std::endl;
    return 1;
}
write(exportFd, pinStr.c_str(), pinStr.size());
close(exportFd);

Після виконання цього коду у вас з’явиться новий каталог: /sys/class/gpio/gpio69/. Давайте подивимось що в ньому:

# ls -l /sys/class/gpio/gpio69/
total 0
-rw-r--r-- 1 root root 4096 Nov  7 10:15 active_low
lrwxrwxrwx 1 root root    0 Nov  7 10:15 device -> ../../../gpiochip1
-rw-r--r-- 1 root root 4096 Nov  7 10:15 direction
-rw-r--r-- 1 root root 4096 Nov  7 10:15 edge
drwxr-xr-x 2 root root    0 Nov  7 10:15 power
lrwxrwxrwx 1 root root    0 Nov  7 10:15 subsystem -> ../../../../../../../class/gpio
-rw-r--r-- 1 root root 4096 Nov  7 10:15 uevent
-rw-r--r-- 1 root root 4096 Nov  7 10:15 value

Для простого блимання нам потрібні лише два файли:

  • direction - Напрямок. Приймає “in” або “out”. Нам потрібно записати “out”, щоб керувати світлодіодом. Доречі, при запису “out” пін за замовчуванням встановлюється в LOW.

  • value - стан. Цей файл можна як читати, так і записувати. Ми будемо писати ‘1’ або ‘0’.

  • інші файли призначени для конфігурування переривань, або інвертування сигналу. Для нашого експерименту вони зараз не потрібні.

Код на С++:

// configure pin direction
std::string gpioCtrlPath = "/sys/class/gpio/gpio" + pinStr;
std::string directionPath = gpioCtrlPath + "/direction";
int directionFd = open(directionPath.c_str(), O_WRONLY);

if (directionFd < 0) {
    std::cerr << "Can't open " << directionPath << ": " << strerror(errno) 
    	<< std::endl;
    return 1;
}

write(directionFd, "out", 3);
close(directionFd);

// open pin file
std::string valuePath = gpioCtrlPath + "/value";
int valueFd = open(valuePath.c_str(), O_RDWR);

if (valueFd < 0) {
    std::cerr << "Can't open " << valuePath << ": " << strerror(errno) 
    	<< std::endl;
    return 1;
}

// toggle pin value
char valueBuf;
for(auto i = 0; i < 10; i++) {
    valueBuf = (i % 2) ? '1' : '0';

    std::cout << "Setting pin to " << (i % 2 ? "HIGH" : "LOW") << std::endl;

    if (write(valueFd, &valueBuf, 1) != 1) {
        std::cerr << "Error writing value: " << strerror(errno) 
        	<< std::endl;
    close(valueFd);
        return 1;
    }
    std::this_thread::sleep_for(500ms);
}
close(valueFd);

І на останок потрібно підчистити за собою - а саме припинити експорт піна.

// unexport pin
int unexportFd = open("/sys/class/gpio/unexport", O_WRONLY);
if (unexportFd < 0) {
    std::cerr << "Can't open /sys/class/gpio/unexport: " << strerror(errno) 
    	<< std::endl;
    return 1;
}
write(unexportFd, pinStr.c_str(), pinStr.size());
close(unexportFd);

Повний код цього прикладу можна знайти на GitHub.

Переваги у порівнянні з прямим доступом до регістрів:

  • Безпека: Найголовніше — він безпечний. Ви не можете “повісити” систему, записавши щось не те. Ви працюєте у “пісочниці”, яку надає ядро.

  • Не потрібен root: Хоча файли /sys за замовчуванням належать root, можна легко налаштувати правила udev, щоб надати вашому користувачу доступ до групи gpio. Це стандартна практика.

  • Абстракція: Код більше не прив’язаний до H618. Йому не потрібні адреси на кшталт 0x0300B000, потрібен лише номер піна (69). Цей самий код (з іншим номером піна) буде працювати на Raspberry Pi, Rockchip чи будь-якому іншому Linux-пристрої, де увімкнено sysfs.

  • Підтримка переривань: GPIO Sysfs в глибинах Linux ядра оброблює апаратні переривання, і дає можливість підписатись на зміни стану піна через виклики poll()/select(), як наслідок не треба пусті цикли з первіркою піна, це знижує навантаження на CPU.

  • Простота: GPIO Sysfs можна викорисовувати навіть в shell скриптах.

Недоліки:

  • Застарілий: Спільнота розробників ядра Linux офіційно рекомендує не використовувати цей інтерфейс для нових розробок і переходити на libgpiod.

  • Повільний: Кожен запис у файл — це системний виклик, перемикання контексту (user-space -> kernel-space), парсинг рядка (“out”, “1”, “0”) всередині драйвера. Це дуже повільно. Ви не зможете генерувати тут швидкі ШІМ сигнали або керувати високошвидкісними протоколами (наприклад, емулювати SPI).

  • Не атомарний: Не можена одночасно змінити стан декількох пінів. Якщо вам потрібно, щоб PC5 та PC6 увімкнулися одночасно, sysfs не дає гарантій. Ви запишете в один файл, потім в інший — між цими діями буде затримка.

Бібліотека libgpiod

Ми розглянули GPIO sysfs і дійшли висновку, що він хоч і безпечний, але повільний, неатомарний, і що головне — офіційно застарілий. Гаразд, який же тоді сучасний та рекомендований в Linux підхід?

Це інтерфейс GPIO Character Device. Замість “файлів-в-директорії” для кожного піна, ядро тепер надає єдиний інтерфейс через файли пристроїв у /dev/gpiochip[n].

Для легкості взаємодії з цими пристроями з user-space була створена бібліотека libgpiod. А для shell скриптів - набір утиліт: gpiodetect, gpioinfo, gpioset, gpioget.

Давайте встановимо libgpiod та адаптуємо наш приклад з “блиманням” світлодіода під неї. Остання версія біблотеки (2.х.х) доступна через на Arbian через бекпорти, тому я встановлюю залежності саме таким чином:

sudo apt install gpiod libgpiod-dev -t oldstable-backports

API документацію на C-інтерфейс бібліотеки можна знайти ось тут - Документація на libgpiod.

Але поки почнемо експериментувати прямо в консолі. Переглянемо, які контролери GPIO наявні в системі, командою gpiodetect:

$ sudo gpiodetect
gpiochip0 [7022000.pinctrl] (32 lines)
gpiochip1 [300b000.pinctrl] (288 lines)

Також можна подивитись деталі по кожному з GPIO контролерів:

$ sudo gpioinfo gpiochip0
gpiochip0 - 32 lines:
        line   0:      unnamed       kernel   input  active-high [used]
        line   1:      unnamed       kernel   input  active-high [used]
        line   2:      unnamed       unused   input  active-high
        ...

На Raspberry Pi лінії часто мають зрозумілі назви, наприклад, “GPIO16” або “ID_SDA”. У мене на Armbian назв пінів, на жаль, немає.

Проте, здогадатися де потрібний пін PC5, можна проаналізувавши імена чіпів, вони містять фізичні адреси регістрів, які ми бачили у першому розділі:

  • gpiochip0 [7022000.pinctrl] - 7022000 це адреса регістрів порту L, отже gpiochip0 відповідає за цей порт.

  • gpiochip1 [300b000.pinctrl] - 300b000 а це, відповідно, адреса регістрів портів C, G, H, I. Саме він і потрібен.

В термінах libgpiod піни називаються “лініями” (lines). Індекс_лінії розраховуємо так само як і в попредньому методі: індекс_порта * 32 + номер_піна (для PC5 це 2 * 32 + 5 = 69).

Кроки для налаштування в libgpiod(v2) мають бути такими:

  • Створюємо gpiod_line_settings і вказуємо в ньому бажані параметри (напрямок, початковий стан тощо).

  • Створюємо gpiod_line_config і додаємо до нього наші settings разом з номерами ліній, до яких вони мають бути застосовані.

  • Створюємо gpiod_request_config і вказуємо, “ім’я” нашої програми (поле consumer).

  • Робимо запит до чіпа, викликом gpiod_chip_request_lines(), і передаємо йому обидві структури gpiod_line_config та gpiod_request_config. Якщо все вдало, ми отримуємо об’єкт gpiod_line_request — це наш “ключ” для керування лініями.

Спрощений код для ініціалізації та блимання буде наступним:

    std::string chipId = "gpiochip1"; // corresponds to 0x0300B000
    unsigned int pinId = static_cast(PIN_PORT) * 32 + PIN_ID;

    gpiod_chip *chip = gpiod_chip_open(("/dev/" + chipId).c_str());
    gpiod_line_settings* line_settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(line_settings, GPIOD_LINE_DIRECTION_OUTPUT);

    gpiod_line_config* line_config = gpiod_line_config_new();
    gpiod_line_config_add_line_settings(line_config, &pinId, 1, line_settings);

    gpiod_request_config *req_config = gpiod_request_config_new();
    gpiod_request_config_set_consumer(req_config, "my-blink-app");

    gpiod_line_request *request = gpiod_chip_request_lines(
        chip,
        req_config,
        line_config);

    // Блимаємо 10 разів
    gpiod_line_value value;
    for (int i = 0; i < 10; i++)
    {
        value = (i % 2) ? GPIOD_LINE_VALUE_ACTIVE : GPIOD_LINE_VALUE_INACTIVE;
        gpiod_line_request_set_value(request, pinId, value);
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    // звільняємо ресурси:
    gpiod_line_request_release(request);
    gpiod_line_config_free(line_config);
    gpiod_line_settings_free(line_settings);
    gpiod_chip_close(chip);

Повний код цього прикладу можна знайти на GitHub.

Переваги:

  • Рекомендований метод: Це стандартний інтерфейс, який підтримується ядром Linux.

  • Вища швидкість: Взаємодія відбувається через системні виклики ioctl(), а не через повільний парсинг файлів (як у sysfs). Це дозволяє керувати пінами значно швидше.

  • Атомарність: libgpiod дозволяє захопити і змінити стан кількох пінів одночасно (в межах одного запиту). Це критично для реалізації програмних протоколів (як SPI).

  • Надійність: в застосунку можна “захопити” лінію. Це запобігає конфліктам, коли дві програми намагаються одночасно керувати одним і тим самим піном.

  • Безпека: Як і sysfs, цей метод не вимагає прав root. Для пристроїв /dev/gpiochipN можна налаштувати правила udev.

Недоліки:

  • Більша складність API: Для простого “блимання” потрібно написати значно більше коду, ніж з GPIO sysfs.

  • Зовнішня залежність: Потребує встановлення бібліотеки libgpiod-dev для компіляції та libgpiod для запуску.

  • Плутанина з API: Існує C API (gpiod.h) та C++ API (gpiod.hpp). Крім того, API v2 суттєво відрізняється від v1, і це плутає при пошуку прикладів в Інтернеті.

Як налаштувати UDEV для Libgpio

Останій крок, налаштуємо UDEV так щоб звичайний користувач міг мати доступ до GPIO.

Ось покрокова інструкція:

Створення групи gpio

Якщо її ще немає в системі, створимо спеціальну групу gpio:

sudo groupadd --system gpio

Додавання користувача до групи

Додамо поточного користувача до цієї групи.

sudo usermod -a -G gpio ${USER}

Створення правила UDEV

Створимо новий файл правил для udev, який автоматично змінюватиме групу та права доступу для пристроїв gpiochip.

/etc/udev/rules.d/99-gpio.rules:

SUBSYSTEM=="gpio", KERNEL=="gpiochip*", GROUP="gpio", MODE="0660"

Перезавантаження правил UDEV

Щоб система застосувала нове правило, не чекаючи перезавантаження, виконайте:

sudo udevadm control --reload-rules
sudo udevadm trigger

Перевірка прав

Тепер перевіримо права доступу до файлів пристроїв. Треба переконатись, що група gpio тепер є власником gpiochip файлів:

$ ls -l /dev/gpiochip*
crw-rw---- 1 root gpio 254, 0 Nov  8 16:07 /dev/gpiochip0
crw-rw---- 1 root gpio 254, 1 Nov  8 16:07 /dev/gpiochip1

Права (crw-rw—-) та група (gpio) встановлені коректно.

На останок треба перелогінитись, це обовʼязквово для того щоб зміни в групах були застосовані для користувача.

Після цього, можна запускати скомпільований додаток без sudo, і він буде успішно блимати світлодіодом.

Коментарі

Популярні дописи з цього блогу

Огляд DC-DC Step-down Buck перетворювачів

ESP8266 модуль з OLED екраном (HW-364A)

Модуль PD тригер IP2721 на 15 та 20 вольт