У минулій статті ми написали перший 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
- Маленька бредборд (опціонально, але зручніше)

Підключення:
|
1 2 |
Pi pin 11 (GPIO 17) → резистор → anode LED (+, довша ніжка) Pi pin 9 (GND) → cathode LED (–, коротша ніжка з плоским краєм) |
◆ Та сама команда — два архітектурні світи
Перш ніж писати код, покажу невеличке дослідження. Я переставляв ту саму SD картку між двома платами — Pi 3 Model B Rev 1.2 і Pi 3 Model B Plus Rev 1.3. Одна команда, але два радикально різних результати:
На Pi 3B Rev 1.2:
|
1 2 3 4 |
$ sudo cat /sys/kernel/debug/gpio | grep -B 1 -A 1 ACT gpiochip1: 2 GPIOs, parent: platform/soc:firmware:virtgpio, brcmvirt-gpio, can sleep: gpio-0 ( |ACT ) out hi |
На Pi 3B+ Rev 1.3:
|
1 2 3 4 |
$ sudo cat /sys/kernel/debug/gpio | grep -B 1 -A 1 ACT gpiochip0: 54 GPIOs, parent: platform/3f200000.gpio, pinctrl-bcm2835: gpio-29 (STATUS_LED_G |ACT ) out lo |
⚠ Та сама 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/:
|
1 2 |
$ ls /sys/class/leds/ ACT default-on mmc0 PWR |
ACT і PWR — це реальні LED’и. default-on і mmc0 — це псевдо-LED, артефакти Device Tree (не зважай на них).
Що ACT уміє з коробки:
|
1 2 3 4 |
$ cat /sys/class/leds/ACT/trigger none default rc-feedback kbd-scrolllock kbd-numlock ... timer oneshot heartbeat backlight cpu cpu0 cpu1 cpu2 cpu3 default-on input panic actpwr mmc1 [mmc0] rfkill-any rfkill-none rfkill0 rfkill1 |
У квадратних дужках — поточний тригер. За замовчуванням це [mmc0] — LED мигає на читання/запис SD картки. Але це лише за замовчуванням, можна перепризначити:
|
1 2 3 4 5 6 7 8 9 10 11 |
# серцебиття замість SD активності $ echo heartbeat | sudo tee /sys/class/leds/ACT/trigger # завантаженість CPU $ echo cpu0 | sudo tee /sys/class/leds/ACT/trigger # мигає при kernel panic (поки воно ще працює) $ echo panic | sudo tee /sys/class/leds/ACT/trigger # повернути назад до SD $ echo mmc0 | sudo tee /sys/class/leds/ACT/trigger |
⚠ Знахідка для майбутніх debug сесій: панік-тригер. Якщо твій kernel module колись «повісить» систему, ACT LED заблимає характерно — фізично побачиш що сталась паніка навіть без HDMI монітора. Може зекономити години діагностики.
◆ Шлях 1: ручне керування через sysfs
Можна керувати LED напряму, без жодних тригерів. Спочатку треба відв’язати тригер (інакше він буде переписувати твої команди):
|
1 2 3 |
$ echo none | sudo tee /sys/class/leds/ACT/trigger $ echo 1 | sudo tee /sys/class/leds/ACT/brightness # увімкнути $ echo 0 | sudo tee /sys/class/leds/ACT/brightness # вимкнути |
Працює і на 3B, і на 3B+ ідентично — це і є магія абстракції, просто не перестаю цьому дивуватись). Linux LED subsystem пофіг чи воно brcmvirt чи pinctrl-bcm2835.
◆ Шлях 2: libgpiod для фізичного LED
Тепер переходимо до фізичного LED на GPIO 17. Підключив на бредборді як описано вище. Беремо нову libgpiod 2.x утиліту gpioset:
|
1 2 |
$ sudo gpioset -c 0 17=1 # увімкнути # ... висить ... |
Стривай, чому висить? Та що за фігня. Виявляється це принциповий дизайн нового API. На відміну від старого sysfs (де echo > value повертав prompt одразу), libgpiod 2.x тримає GPIO у заданому стані поки сам процес живий. Ctrl+C → GPIO звільнений, термінал повертається.
Це ownership-based модель. Колись sysfs API любив залишати GPIO «залипшим» якщо забув unexport. Новий API звільняє автоматично коли процес помирає.
Як же тоді блимати?
Перша моя спроба — bash цикл:
|
1 2 3 4 |
for i in 1 2 3 4 5; do sudo gpioset --chip gpiochip0 --hold-period 300ms 17=1 sleep 0.3 done |
Запускаю — LED просто горить, не блимає. Чому?
⚠
--hold-period— це затримка перед виходом, не «тримати у цьому стані потім скинути». gpioset ставить 17=1, чекає 300мс, виходить, залишаючи GPIO 17 у стані 1 через BCM2835 GPIO_OUT persistence (це специфіка Pi — register не скидається при release). Кожна ітерація просто ставить 17=1 знову. LED горить постійно.
Правильний спосіб — вбудована функція —toggle
Виявляється, libgpiod 2.x має для цього спеціальний прапор:
|
1 2 3 4 5 6 7 8 9 |
# 500мс ON, 500мс OFF, нескінченно $ sudo gpioset -c 0 -t 500,500 17=1 # heartbeat-стиль: 100мс ON, 1с OFF $ sudo gpioset -c 0 -t 100,1000 17=1 # демонізувати, термінал звільняється $ sudo gpioset -z -c 0 -t 500,500 17=1 $ sudo pkill gpioset # для зупинки |
Це і є той сюжет — при переході зі старого 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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/dts-v1/; /plugin/; / { compatible = "brcm,bcm2837"; fragment@0 { target-path = "/"; __overlay__ { our_led: our-led { compatible = "alex,our-led"; label = "our-led"; led-gpio = <&gpio 17 0>; status = "okay"; }; }; }; }; |
Що тут важливо:
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
Не буду заливати весь файл, покажу ключові частини.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* отримуємо GPIO через DT — не hard-coded номер 17! */ static int led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW); if (IS_ERR(led_gpio)) { pr_err("led_driver: не вдалось отримати GPIO\n"); return PTR_ERR(led_gpio); } timer_setup(&blink_timer, blink_callback, 0); apply_mode(); return 0; } /* compatible string має співпадати з DT overlay */ static const struct of_device_id led_of_match[] = { { .compatible = "alex,our-led" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_of_match); |
⚠
devm_префікс — це managed resources. Коли модуль вивантажується, GPIO звільниться автоматично, без явногоgpiod_put(). Це сучасний embedded Linux pattern, який економить тонни boilerplate коду і запобігає leak’ам.
◆ Ґуля № 1: deltimersync видалили з ядра
Перша спроба зібрати — отримую таке:
|
1 2 3 4 5 |
led_driver.c: In function 'apply_mode': led_driver.c:72:5: error: implicit declaration of function 'del_timer_sync' [-Wimplicit-function-declaration] 72 | del_timer_sync(&blink_timer); | ^~~~~~~~~~~~~~ |
Стривай, це ж класична функція kernel timer. Як це «не знайдено»?
⚠ У Linux 6.15 (травень 2025) розробники прибрали
del_timer_sync()іdel_timer()— після кількох років deprecated статусу. Тепер треба використовуватиtimer_delete_sync()іtimer_delete(). Моє ядро 6.18.33 від червня 2026 успадкувало цю зміну.
Виправлення тривіальне — одне слово замінити:
|
1 2 3 4 5 |
/* було: */ del_timer_sync(&blink_timer); /* стало: */ timer_delete_sync(&blink_timer); |
Але урок глибший. Kernel API еволюціонує постійно код з туторіалу 2020 року на ядрі 2026 року просто не збрається, а отже досвід застаріває, а колись казали, що досвід не пропьєш. Брехали( Тому не тре запам’ятовувати функції напам’ять, а краще вміти читати linux/timer.h та kernel changelog для своєї версії.
◆ Збірка, активація, запуск
|
1 2 3 4 5 6 7 8 |
$ make make -C /lib/modules/6.18.33+rpt-rpi-v8/build M=$(pwd) modules CC [M] led_driver.o MODPOST Module.symvers CC [M] led_driver.mod.o LD [M] led_driver.ko dtc -@ -I dts -O dtb -o our-led.dtbo our-led-overlay.dts DT overlay скомпільований: our-led.dtbo |
Активуємо overlay і перезавантажуємось:
|
1 2 3 |
$ sudo cp our-led.dtbo /boot/firmware/overlays/ $ echo 'dtoverlay=our-led' | sudo tee -a /boot/firmware/config.txt $ sudo reboot |
Після reboot — перевіряємо що ядро побачило нашу DT ноду:
|
1 2 3 4 5 |
$ ls /proc/device-tree/ | grep our-led our-led $ cat /proc/device-tree/our-led/compatible alex,our-led |
Завантажуємо модуль:
|
1 2 3 4 5 |
$ sudo insmod led_driver.ko mode=2 blink_ms=300 $ dmesg | tail -10 [117.420962] led_driver: probe для our-led [117.421011] led_driver: mode=blink, period=300 ms [117.421016] led_driver: готовий, mode=2, blink_ms=300 |
LED заблимав! Перевіряємо що GPIO зайнятий нашим драйвером:
|
1 2 |
$ sudo cat /sys/kernel/debug/gpio | grep gpio-17 gpio-17 (GPIO17 |led ) out hi |
Бачимо consumer |led — це наше devm_gpiod_get(dev, "led", ...) у дії. Той самий стовпчик, де у штатних драйверів є |ACT або |PWR.
◆ Ґуля № 2: «led_gpio not ready» — порядок ініціалізації
У dmesg помітив дивну річ:
|
1 2 3 4 |
[117.420678] led_driver: led_gpio not ready [117.420688] led_driver: led_gpio not ready [117.420962] led_driver: probe для our-led [117.421011] led_driver: mode=blink, period=300 ms |
Два warning’и «led_gpio not ready» перед probe! Як таке могло статись?
⚠ Це порядок ініціалізації platform driver: спочатку парсяться параметри модуля з callback’ами (
mode=2іblink_ms=300викликалиmode_setіblink_ms_set), і тільки потім ядро шукає DT match і викликає probe. Томуmodule_paramcallback’и викликались ДО probe, колиled_gpioще NULL.
На щастя, у мене у apply_mode() був guard:
|
1 2 3 4 |
if (!led_gpio) { pr_warn(DRIVER_NAME ": led_gpio not ready\n"); return; } |
Без цього guard’у був би kernel oops замість warning. Це класична embedded knee — параметри модуля парсяться до того як драйвер прив’язується до hardware.
◆ Runtime керування через sysfs
Найкрутіше у нашому модулі — параметри можна міняти на льоту, без вивантаження:
|
1 2 3 4 5 6 7 8 9 |
# вимкнути $ echo 0 | sudo tee /sys/module/led_driver/parameters/mode # увімкнути постійно $ echo 1 | sudo tee /sys/module/led_driver/parameters/mode # блимати швидко $ echo 100 | sudo tee /sys/module/led_driver/parameters/blink_ms $ echo 2 | sudo tee /sys/module/led_driver/parameters/mode |
Завдяки 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!