STM32 з нуля без HAL: Місяць 4, Тиждень 2. LED driver з ядра: чотири шляхи до однієї лампочки. Частина 12

У минулій статті ми написали перший kernel module — hello.ko, який тільки друкував у dmesg привіт і пока. Сьогодні робимо крок вперед: керуємо реальним залізом з kernel space. Здавалося б, що складного — блимнути LED. Але на цьому шляху знайшлося стільки знахідок, що тиждень навчання перетворився на цікаву пригоду.

◆ Чотири шляхи до однієї лампочки

Виявилось, що той самий LED можна засвітити чотирма принципово різними способами. Кожен з них має своє місце у Linux архітектурі, і кожен показує свій рівень розуміння системи:

Шлях Як працює Хто це робить
Шлях 1: тригери echo heartbeat > /sys/class/leds/ACT/trigger Будь-хто, навіть не програміст
Шлях 2: sysfs echo 1 > /sys/class/leds/ACT/brightness Скрипти, утиліти, ad-hoc
Шлях 3: libgpiod gpioset -c 0 17=1 Сучасні застосунки
Шлях 4: kernel module gpiod_set_value(led, 1) з kernel space Драйверщик (це наша ціль)

Покажу всі чотири на тій самій залізячці.

◆ Залізо: ACT LED та фізичний LED на GPIO 17

Для перших двох шляхів нічого паяти не треба — використовуємо вбудований ACT LED (зелений на самій Pi). Для шляхів 3-4 додамо фізичний LED на бредборді:

  • LED 5мм (будь-який колір)
  • Резистор 220-330 Ом (для 3.3В Pi GPIO)
  • Jumper’и F2M × 2
  • Маленька бредборд (опціонально, але зручніше)

Підключення:

◆ Та сама команда — два архітектурні світи

Перш ніж писати код, покажу невеличке дослідження. Я переставляв ту саму SD картку між двома платами — Pi 3 Model B Rev 1.2 і Pi 3 Model B Plus Rev 1.3. Одна команда, але два радикально різних результати:

На Pi 3B Rev 1.2:

На Pi 3B+ Rev 1.3:

⚠ Та сама SD картка, те саме ядро 6.18.33 — а ACT LED живе у двох різних світах. На 3B він на віртуальному GPIO chip який спілкується з firmware VideoCore через mailbox API. На 3B+ — на звичайному pinctrl-bcm2835 як native SoC pin.

Pi 3B Rev 1.2 Pi 3B+ Rev 1.3
GPIO chip gpiochip1 (brcmvirt-gpio) gpiochip0 (pinctrl-bcm2835)
Як працює Firmware mailbox до VideoCore Прямо через SoC регістри
Active level high low
can_sleep Так (не можна з IRQ) Ні
Native name STATUSLEDG

Чому так? На старіших ревізіях Pi 3B Foundation економила на пінах SoC — LED фізично під’єднаний до окремої лінії якою керує VideoCore firmware, не основний CPU. Linux може ним керувати тільки через mailbox-протокол. На Pi 3B+ Foundation переробила silicon і вивела LED на звичайний GPIO chip.

Для нашого Linux LED subsystem це абсолютно прозоро — і там, і там echo > /sys/class/leds/ACT/trigger працює однаково. У цьому й сила абстракції: kernel приховує від userspace яка саме реалізація заліза під капотом.

От такі справи, пішов кави попив і попалось відео де щасливий пенсіонер показує свою мега круту приблуду. Він взяв тен від старої стіралки, механічний таймер часу від старої духовки, бочечка капронова в неї вмонтував кран і тен. І з гордістю демонструє як це працює. Таке відчуття, що він як мінімум сконструював реактивний двигун, а не винайшов бойлер із мотлоху. Згодом стає зрозуміла причина такої гордості, виявляється дружина дуже високо оцінила цей девайс і тепер руки можна мити теплою водою. Це так мило. Написав йому, а що по безпеці а якщо не буде води чи тен проб’є, він показав що в нього там ще купа автоматів на ці випадки. Я прийшов і подивися на свій стіл, мабуть я теж такий чудік як і він. Просто не уявляю життя без винаходів велосипедів і впевнений, що коли мені буде 70+ я буду робити теж саме, бо хтось колекціонує марки, хтось спінінги, хтось любить колупатись в двигунах і це щастя. А без чого немислиме життя для тебе? Пишіть в коментарях.

◆ Шлях 0: магія тригерів (нуль коду)

Linux має готову LED subsystem яка вже знає про твій ACT LED. Дивимось у /sys/class/leds/:

ACT і PWR — це реальні LED’и. default-on і mmc0 — це псевдо-LED, артефакти Device Tree (не зважай на них).

Що ACT уміє з коробки:

У квадратних дужках — поточний тригер. За замовчуванням це [mmc0] — LED мигає на читання/запис SD картки. Але це лише за замовчуванням, можна перепризначити:

⚠ Знахідка для майбутніх debug сесій: панік-тригер. Якщо твій kernel module колись «повісить» систему, ACT LED заблимає характерно — фізично побачиш що сталась паніка навіть без HDMI монітора. Може зекономити години діагностики.

◆ Шлях 1: ручне керування через sysfs

Можна керувати LED напряму, без жодних тригерів. Спочатку треба відв’язати тригер (інакше він буде переписувати твої команди):

Працює і на 3B, і на 3B+ ідентично — це і є магія абстракції, просто не перестаю цьому дивуватись). Linux LED subsystem пофіг чи воно brcmvirt чи pinctrl-bcm2835.

◆ Шлях 2: libgpiod для фізичного LED

Тепер переходимо до фізичного LED на GPIO 17. Підключив на бредборді як описано вище. Беремо нову libgpiod 2.x утиліту gpioset:

Стривай, чому висить? Та що за фігня. Виявляється це принциповий дизайн нового API. На відміну від старого sysfs (де echo > value повертав prompt одразу), libgpiod 2.x тримає GPIO у заданому стані поки сам процес живий. Ctrl+C → GPIO звільнений, термінал повертається.

Це ownership-based модель. Колись sysfs API любив залишати GPIO «залипшим» якщо забув unexport. Новий API звільняє автоматично коли процес помирає.

Як же тоді блимати?

Перша моя спроба — bash цикл:

Запускаю — LED просто горить, не блимає. Чому?

--hold-period — це затримка перед виходом, не «тримати у цьому стані потім скинути». gpioset ставить 17=1, чекає 300мс, виходить, залишаючи GPIO 17 у стані 1 через BCM2835 GPIO_OUT persistence (це специфіка Pi — register не скидається при release). Кожна ітерація просто ставить 17=1 знову. LED горить постійно.

Правильний спосіб — вбудована функція —toggle

Виявляється, libgpiod 2.x має для цього спеціальний прапор:

Це і є той сюжет — при переході зі старого API на новий не треба переписувати ту саму логіку bash циклами, просто щоб ти це знав). Новий API має свою філософію — послідовності через --toggle, demon через -z, ownership через process lifetime.

◆ Шлях 3: пишемо свій kernel module

Тепер головне — kernel module. І це батенька не команди в шелі, тут код всередині ядра. Це повний production pattern: platform driver з Device Tree match, GPIO через consumer API, kernel timer для blink, runtime зміна параметрів через sysfs.

Архітектура

Будемо робити так:

  • Device Tree overlay описує нашу LED-ноду з compatible string alex,our-led та посиланням на GPIO 17
  • Kernel module реєструється як platform driver і шукає той самий compatible string
  • Kernel автоматично match’ить DT ноду з нашим драйвером — викликає probe()
  • У probe ми отримуємо GPIO через devm_gpiod_get() (з автоматичним cleanup)
  • Module params з callback’ами дозволяють runtime зміну через /sys/module/.../parameters/
  • Kernel timer реалізує blink режим у ядрі, без участі userspace

Device Tree overlay: our-led-overlay.dts

Що тут важливо:

  • compatible = "alex,our-led" — це наш custom identifier. Ядро шукатиме driver який його обробляє.
  • led-gpio = <&gpio 17 0> — посилання на GPIO controller (&gpio), номер пiна (17), flags (0 = active high).
  • status = "okay" — нода активна. Якщо disabled — ядро її ігнорує.

Kernel module: led_driver.c

Не буду заливати весь файл, покажу ключові частини.

devm_ префікс — це managed resources. Коли модуль вивантажується, GPIO звільниться автоматично, без явного gpiod_put(). Це сучасний embedded Linux pattern, який економить тонни boilerplate коду і запобігає leak’ам.

◆ Ґуля № 1: deltimersync видалили з ядра

Перша спроба зібрати — отримую таке:

Стривай, це ж класична функція kernel timer. Як це «не знайдено»?

⚠ У Linux 6.15 (травень 2025) розробники прибрали del_timer_sync() і del_timer() — після кількох років deprecated статусу. Тепер треба використовувати timer_delete_sync() і timer_delete(). Моє ядро 6.18.33 від червня 2026 успадкувало цю зміну.

Виправлення тривіальне — одне слово замінити:

Але урок глибший. Kernel API еволюціонує постійно код з туторіалу 2020 року на ядрі 2026 року просто не збрається, а отже досвід застаріває, а колись казали, що досвід не пропьєш. Брехали( Тому не тре запам’ятовувати функції напам’ять, а краще вміти читати linux/timer.h та kernel changelog для своєї версії.

◆ Збірка, активація, запуск

Активуємо overlay і перезавантажуємось:

Після reboot — перевіряємо що ядро побачило нашу DT ноду:

Завантажуємо модуль:

LED заблимав! Перевіряємо що GPIO зайнятий нашим драйвером:

Бачимо consumer |led — це наше devm_gpiod_get(dev, "led", ...) у дії. Той самий стовпчик, де у штатних драйверів є |ACT або |PWR.

◆ Ґуля № 2: «led_gpio not ready» — порядок ініціалізації

У dmesg помітив дивну річ:

Два warning’и «led_gpio not ready» перед probe! Як таке могло статись?

⚠ Це порядок ініціалізації platform driver: спочатку парсяться параметри модуля з callback’ами (mode=2 і blink_ms=300 викликали mode_set і blink_ms_set), і тільки потім ядро шукає DT match і викликає probe. Тому module_param callback’и викликались ДО probe, коли led_gpio ще NULL.

На щастя, у мене у apply_mode() був guard:

Без цього guard’у був би kernel oops замість warning. Це класична embedded knee — параметри модуля парсяться до того як драйвер прив’язується до hardware.

◆ Runtime керування через sysfs

Найкрутіше у нашому модулі — параметри можна міняти на льоту, без вивантаження:

Завдяки kernel_param_ops callback’у кожен echo трігерить apply_mode(), який перепрограмовує kernel timer і GPIO state. Без вивантаження модуля. Без переривання сервісу. Як у справжніх драйверах.

◆ Підсумки, що далі

Підсумок другого тижня:

  • Розібрались з Linux LED subsystem і тригерами (Шлях 0)
  • Знайшли архітектурну різницю Pi 3B vs 3B+ — той самий ACT LED на різних GPIO chip’ах
  • Перейшли на libgpiod 2.x і зрозуміли --toggle для блимання (Шлях 2)
  • Зіткнулись з API еволюцією: del_timer_sync
  • Написали повноцінний platform driver з Device Tree match
  • Знайшли цікавий нюанс порядку ініціалізації — module_param callback’и викликаються до probe
  • Реалізували runtime керування через sysfs з kernel_param_ops

Це можливо міг би бути туторіал «blink LED», знайомство з повним циклом kernel driver development: DT match → probe → resource acquisition → state machine → cleanup, але не факт. Забігаючи на перед це той самий патерн в HC-SR04 driver який ми будемо писати наступного тижня.

Підпишись, щоб не пропустити розповідь третього тижня — character device driver. /dev/mydev з повним file_operations: open, read, write, release. Ну це прям рівень драйвера, на якому пишуть реальні відкриті sensor драйвери в kernel tree.

До зустрічі — Hello, /dev!

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *