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: sysfsecho 1 > /sys/class/leds/ACT/brightnessСкрипти, утиліти, ad-hoc
Шлях 3: libgpiodgpioset -c 0 17=1Сучасні застосунки
Шлях 4: kernel modulegpiod_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
  • Маленька бредборд (опціонально, але зручніше)

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

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:

$ 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:

$ 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.2Pi 3B+ Rev 1.3
GPIO chipgpiochip1 (brcmvirt-gpio)gpiochip0 (pinctrl-bcm2835)
Як працюєFirmware mailbox до VideoCoreПрямо через SoC регістри
Active levelhighlow
can_sleepТак (не можна з IRQ)Ні
Native nameSTATUSLEDG

Чому так? На старіших ревізіях 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/:

$ ls /sys/class/leds/
ACT  default-on  mmc0  PWR

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

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

$ 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 картки. Але це лише за замовчуванням, можна перепризначити:

# серцебиття замість 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 напряму, без жодних тригерів. Спочатку треба відв’язати тригер (інакше він буде переписувати твої команди):

$ 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:

$ 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 цикл:

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 має для цього спеціальний прапор:

# 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

/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

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

/* отримуємо 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 видалили з ядра

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

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 успадкувало цю зміну.

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

/* було: */
del_timer_sync(&blink_timer);

/* стало: */
timer_delete_sync(&blink_timer);

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

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

$ 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 і перезавантажуємось:

$ sudo cp our-led.dtbo /boot/firmware/overlays/
$ echo 'dtoverlay=our-led' | sudo tee -a /boot/firmware/config.txt
$ sudo reboot

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

$ ls /proc/device-tree/ | grep our-led
our-led

$ cat /proc/device-tree/our-led/compatible
alex,our-led

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

$ 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 зайнятий нашим драйвером:

$ 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 помітив дивну річ:

[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_param callback’и викликались ДО probe, коли led_gpio ще NULL.

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

if (!led_gpio) {
    pr_warn(DRIVER_NAME ": led_gpio not ready\n");
    return;
}

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

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

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

# вимкнути
$ 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!

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Але урок глибший. Kernel API еволюціонує постійно код з туторіалу 2020 року на ядрі 2026 року просто не збрається, а отже досвід застаріває, а колись казали, що досвід не пропьєш

то LED таки не блимає, бо автор «пропив»?

Дякую за цікаву статтю, пане Олекандр. Ще й з фотками.
Для новачків — корисний приклад.

Припущу,

знайомство з повним циклом kernel driver development

займе не одну статтю, і краще відразу звузити знайомство до можливостей окремої інфраструктури (наявного заліза).
Ще можна буде показати userspace-kernelspace interaction з userspace С-програми.

Дякую пане Микола за такий теплий зворотній звя’зок. Обов’язково візьму до уваги ваші рекомендації

Підписатись на коментарі