STM32 з нуля без HAL: Місяць 4, Тиждень 3: пишемо /dev/mydev — character device driver. Частина 13
Ну що ж, 13 тижнів позаду, за цей час отримав багато повідомлень. Що радує — це те, що ця серія статей і інші мої опуси для когось стали тригером для написання своїх серій і статей, хтось робить це заради піару, хтось розвиває україномовний контент, а хтось ділиться безцінним досвідом. Хай там як, я щасливий, бо мене питали, навіщо це все, ти графоман чи блажений, навіщо стільки часу витрачати на це все. Ну ладно, якби ти шукав роботу і хотів пропіаритись, чи володів безцінним досвідом, який вартий уваги. А ти просто робиш, описуєш це і публікуєш. Так, просто описую, бо це пригода, бо це мій стиль життя.
Якщо ти читаєш мої опуси і знаходиш їх корисними, чи вони надихнули тебе на щось подібне, напиши мені особисто чи в коментарі, це не займе багато часу, але це маленьке «Дякую» покаже іншим, що такий контент потрібен українською, хоча б тому, що на цих текстах можливо будуть навчати україномовні моделі, і ти будеш зашитий в її тензори.
◆ ASIC, FPGA, Jetson — де ми в цьому всьому
Після виходу попередньої статті прийшов цікавий коментар від Evgen Ryabko — людини з досвідом в avionics і medical embedded (laryngoscope на Qualcomm SOM + STM32 + GStreamer, IEC-62304). Він звернув увагу, що в серії мова постійно йде про ASIC, і це правда. Варто один раз пояснити, де ми в загальному embedded ландшафті.
ASIC (Application-Specific Integrated Circuit) — це те, що у нас:
- BCM2837 на Raspberry Pi — фіксований silicon, ARM Cortex-A53, виготовлений один раз;
- RV1106 на Luckfox — теж ASIC, але з NPU всередині;
- STM32F103 — ASIC, Cortex-M3.
FPGA (Field-Programmable Gate Array) — програмована логіка. Купляєш чіп, завантажуєш bitstream і він стає тим що тобі треба — власний процесор, кастомний DSP, паралельний відеопроцесор. Мова — VHDL або Verilog, не C. Toolchain — Vivado або Quartus, не gcc. Це зовсім інший світ, і він не в нашій серії.
Jetson (NVIDIA) — теж ASIC, але з потужним GPU. Технічно ближче до нас (embedded Linux + AI), але це протилежне нашій тезі «AI на слабкому залізі». Jetson Nano коштує від $100, споживає 5-10W, і призначений для людей яким потрібен GPU. Нам він не потрібен.
Наша серія свідомо залишається в зоні ASIC без GPU: MCU + Linux SBC + Buildroot + kernel modules. Це те залізо, що є у більшості читачів, те, що реально використовується в IoT і embedded продуктах, де немає бюджету на GPU і немає сенсу в FPGA.
До речі, проєкт Evgena — Qualcomm SOM + STM32 MCU + GStreamer для медичного пристрою — це буквально та сама архітектура, що я намагаюсь зробити тут, тільки в IEC-62304 продакшні. Різниця в масштабі і сертифікації, а не в принципах. Якщо у вас є досвід з подібними проєктами — пишіть в коментарях, цікаво порівнювати.
◆ Як Linux бачить пристрої
Перш, ніж перейдемо до коду — коротко про те, що таке character device. У Linux є три типи пристроїв:
/dev/sda → block device (диск — читання/запис блоками) /dev/ttyS0 → char device (UART — потік байтів) /dev/null → char device (спеціальний — поглинає все)
Character device — це потік байтів без внутрішньої структури. Ядро присвоює йому major і minor числа:
ls -la /dev/ttyS0 # crw-rw---- 1 root dialout 4, 64 Jun 15 12:00 /dev/ttyS0 # ^ ^ # major minor
4 — номер драйвера (UART driver). 64 — конкретний пристрій у межах цього драйвера. Ядро по major числу знаходить драйвер, по minor — конкретний device instance.
Наш mydev.ko буде:
- Запитувати major number динамічно (
alloc_chrdev_region). - Реєструватись як character device (
cdev_add). - Автоматично створювати
/dev/mydevчерез udev (device_create). - Реалізовувати
open,read,write,release.
◆ Готуємо середовище
ssh pi+ mkdir -p ~/kernel-modules/03-chrdev cd ~/kernel-modules/03-chrdev
◆ Для самих маленьких: звідки береться
ssh pi+По серії ти часто побачиш команди типу
ssh piчиssh pi+— це не окрема утиліта, а SSH host alias. Замістьssh [email protected]щоразу, один раз описуєш плату в~/.ssh/config:Host pi+ HostName 192.168.1.123 User alexІ далі просто
ssh pi+. Коли плат у тебе не одна-дві, а ціла флотилія (у мене на цьому етапі 6 Raspberry Pi плюс Luckfox і Orange Pi), це рятує від постійного набивання IP руками. Туди ж можна додати ключі, порти, прапорці — наприклад, для Luckfox, який не любить pubkey-авторизацію:Host luckfox HostName 192.168.1.125 User root PubkeyAuthentication noМіж іншим, є ще один зручний спосіб —
ProxyJump. Якщо якась плата сидить у підмережі, куди немає прямого доступу, і достукатись до неї можна лише через проміжний хост, це пишеться в один рядок:Host orange-hidden HostName 192.168.2.50 User crud ProxyJump pi+Тепер
ssh orange-hiddenспершу зайде наpi+, а звідти прокине з’єднання далі — жодних ручних тунелів. Для розподілених штук на кшталт майбутньої TDOA-флотилії, де ноди розкидані по сегментах мережі, це стане в нагоді.Дрібниця, але коли ти щодня стрибаєш між п’ятьма платами — це різниця між «набрав
ssh pi+» і «згадав який там був IP у 3B+».
Зверни увагу,що рядок ProxyJump pi+посилається на вже описаний вище alias — показує що хости в конфігу можна складати ланцюжком.
Перед тим як писати свій драйвер, хочу глянути, що вже є в системі — щоб розуміти, куди ми вписуємось, і не наступити комусь на major. Дивимось усі character devices і список зареєстрованих драйверів:
ls -la /dev/ | grep "^c" | head -20 cat /proc/devices | head -30

Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 5 ttyprintk 7 vcs 10 misc 13 input 14 sound 29 fb 81 video4linux 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 204 ttyAMA 216 rfcomm 226 drm 236 mydev 237 cec 238 media 239 gpiomem 240 binder 241 hidraw 242 rpmb
⚠ Будь уважний: це дві окремі команди, не одна. Якщо набрати їх в один рядок через кому (
grep "^c", cat /proc/devices) — shell не зрозуміє коми як розділювача (це не Python), іgrepсприймеcatза ім’я файлу:grep: cat: No such file or directory. Розділювач команд у shell —;або новий рядок, а не кома.

І ось що цікаво у виводі /proc/devices. Статичні драйвери сидять на маленьких номерах — 1 mem, 4 tty, 5 console, 10 misc. А все, що ядро роздає динамічно, лізе з верхнього краю вниз: 242 rpmb, 241 hidraw, 240 binder, 239 gpiomem, 238 media, 237 cec. Тому коли наш alloc_chrdev_region попросить вільний major, ми опинимось десь поряд — у мене вийшло 236. Ось чому хардкодити major руками — погана ідея: система й сама знає, що вільно, треба тільки попросити.
До речі, тут же у виводі вже присутні майбутні герої — gpiochip0, gpiochip1, gpiomem. До них ми ще дійдемо.
◆ Код: mydev.c
/*
* mydev.c — character device driver
*
* Місяць 4, Тиждень 3. Перший /dev/ пристрій з нуля.
*
* Що робить:
* echo "hello" > /dev/mydev — зберігає в kernel буфер
* cat /dev/mydev — читає назад
*
* Концепції:
* - alloc_chrdev_region / unregister_chrdev_region
* - cdev_init / cdev_add / cdev_del
* - class_create / device_create / device_destroy
* - copy_to_user / copy_from_user
* - file_operations: open / read / write / release
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DRIVER_NAME "mydev"
#define BUFFER_SIZE 256
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static char kbuf[BUFFER_SIZE];
static size_t kbuf_len;
static DEFINE_MUTEX(mydev_mutex);
static int mydev_open(struct inode *inode, struct file *filp)
{
pr_info("mydev: open() called, pid=%d\n", current->pid);
return 0;
}
static int mydev_release(struct inode *inode, struct file *filp)
{
pr_info("mydev: release() called, pid=%d\n", current->pid);
return 0;
}
/*
* ⚠ Критичний момент: не можна просто memcpy між kernel і user!
* Userspace і kernel живуть у різних адресних просторах.
* copy_to_user() робить безпечну копію і перевіряє чи адреса user валідна.
*/
static ssize_t mydev_read(struct file *filp, char __user *ubuf,
size_t count, loff_t *ppos)
{
size_t to_copy;
unsigned long not_copied;
pr_info("mydev: read() called, count=%zu, ppos=%lld\n", count, *ppos);
mutex_lock(&mydev_mutex);
if (*ppos >= kbuf_len) {
mutex_unlock(&mydev_mutex);
pr_info("mydev: read() → EOF\n");
return 0;
}
to_copy = min(count, kbuf_len - (size_t)*ppos);
not_copied = copy_to_user(ubuf, kbuf + *ppos, to_copy);
if (not_copied) {
mutex_unlock(&mydev_mutex);
pr_err("mydev: copy_to_user failed, %lu bytes not copied\n",
not_copied);
return -EFAULT;
}
*ppos += to_copy;
mutex_unlock(&mydev_mutex);
pr_info("mydev: read() → %zu bytes\n", to_copy);
return to_copy;
}
static ssize_t mydev_write(struct file *filp, const char __user *ubuf,
size_t count, loff_t *ppos)
{
unsigned long not_copied;
size_t to_write;
pr_info("mydev: write() called, count=%zu\n", count);
mutex_lock(&mydev_mutex);
to_write = min(count, (size_t)(BUFFER_SIZE - 1));
not_copied = copy_from_user(kbuf, ubuf, to_write);
if (not_copied) {
mutex_unlock(&mydev_mutex);
pr_err("mydev: copy_from_user failed\n");
return -EFAULT;
}
kbuf[to_write] = '\0';
kbuf_len = to_write;
*ppos = to_write;
mutex_unlock(&mydev_mutex);
pr_info("mydev: write() → saved %zu bytes: \"%s\"\n", to_write, kbuf);
/*
* ⚠ Важливо: повертаємо count (що просив userspace), не to_write.
* Якщо повернемо менше — shell вирішить що треба retry і викличе
* write() ще раз з рештою. Повертай те що просили, якщо все ок.
*/
return count;
}
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_release,
.read = mydev_read,
.write = mydev_write,
};
static int __init mydev_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, DRIVER_NAME);
if (ret < 0) {
pr_err("mydev: alloc_chrdev_region failed: %d\n", ret);
return ret;
}
pr_info("mydev: registered with major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
cdev_init(&my_cdev, &mydev_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("mydev: cdev_add failed: %d\n", ret);
goto err_cdev;
}
/* ⚠ ядро 6.4+: class_create приймає ОДИН аргумент (раніше було два) */
my_class = class_create(DRIVER_NAME);
if (IS_ERR(my_class)) {
ret = PTR_ERR(my_class);
pr_err("mydev: class_create failed: %d\n", ret);
goto err_class;
}
if (IS_ERR(device_create(my_class, NULL, dev_num, NULL, DRIVER_NAME))) {
ret = -EINVAL;
pr_err("mydev: device_create failed\n");
goto err_device;
}
memset(kbuf, 0, BUFFER_SIZE);
kbuf_len = 0;
pr_info("mydev: /dev/%s created, ready\n", DRIVER_NAME);
return 0;
err_device:
class_destroy(my_class);
err_class:
cdev_del(&my_cdev);
err_cdev:
unregister_chrdev_region(dev_num, 1);
return ret;
}
static void __exit mydev_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("mydev: /dev/%s removed\n", DRIVER_NAME);
}
module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alex");
MODULE_DESCRIPTION("Character device driver: /dev/mydev з кільцевим буфером");
MODULE_VERSION("0.1");
Makefile:
obj-m += mydev.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean reload: -sudo rmmod mydev 2>/dev/null sudo insmod mydev.ko dmesg | tail -10
◆ Збірка

make make -C /lib/modules/6.18.33+rpt-rpi-v8/build M=/home/alex/kernel-modules/03-chrdev modules make[1]: Entering directory '/usr/src/linux-headers-6.18.33+rpt-rpi-v8' make[2]: Entering directory '/home/alex/kernel-modules/03-chrdev' CC [M] mydev.o MODPOST Module.symvers CC [M] mydev.mod.o CC [M] .module-common.o LD [M] mydev.ko make[2]: Leaving directory '/home/alex/kernel-modules/03-chrdev' make[1]: Leaving directory '/usr/src/linux-headers-6.18.33+rpt-rpi-v8'
Чисто, без warning’ів. Зверни увагу на class_create(DRIVER_NAME) в коді — один аргумент. На ядрі 6.18 (наш 3B+) старий двоаргументний виклик class_create(THIS_MODULE, name) просто не скомпілюється.
◆ Ґуля № 1 — Permission denied
sudo insmod mydev.ko ls -l /dev/mydev crw------- 1 root root 236, 0 Jun 30 08:15 /dev/mydev echo "hello kernel" > /dev/mydev -bash: /dev/mydev: Permission denied
device_create створив вузол з правами 0600 за замовчуванням — читати й писати може тільки root. Так поводиться драйвер без явного налаштування прав. Швидкий обхід — sudo
sudo bash -c 'echo "hello kernel" > /dev/mydev' sudo cat /dev/mydev hello kernel
Працює. Але правильне рішення виставити права через udev rule:
echo 'KERNEL=="mydev", MODE="0666"' | sudo tee /etc/udev/rules.d/99-mydev.rules sudo udevadm control --reload-rules sudo rmmod mydev && sudo insmod mydev.ko ls -l /dev/mydev crw-rw-rw- 1 root root 236, 0 Jun 30 08:19 /dev/mydev
⚠ Тут легко наступити на ще одні граблі: вміст udev-правила треба покласти у файл через
tee, а не вставляти як команду в термінал —KERNEL=="mydev", MODE="0666", набраний прямо в shell, bash спробує виконати як команду й видасть помилкуcommand not found.
Тепер без sudo:
echo "hello kernel" > /dev/mydev cat /dev/mydev
◆ Ґуля № 2 — чому cat не зависає (два виклики read)
Найцікавіша частина — дивимось у dmesg що насправді відбувається:
sudo bash -c 'echo "hello kernel" > /dev/mydev' sudo cat /dev/mydev dmesg | tail -15 [ 802.056291] mydev: open() called, pid=1483 [ 802.056393] mydev: write() called, count=13 [ 802.056403] mydev: write() → saved 13 bytes: "hello kernel " [ 802.056421] mydev: release() called, pid=1483 [ 802.107888] mydev: open() called, pid=1487 [ 802.107970] mydev: read() called, count=262144, ppos=0 [ 802.107989] mydev: read() → 13 bytes [ 802.108034] mydev: read() called, count=262144, ppos=13 [ 802.108043] mydev: read() → EOF [ 802.108072] mydev: release() called, pid=1487
Розбираємо по рядках:
write() called, count=13—echo "hello kernel"дає 13 байтів, а не 12.echoсам додає\nу кінці — звідси+1.open/releaseз різними pid (1483 і 1487) —echo > fileіcat fileце два окремих процеси, кожен зі своїм відкриттям і закриттям файлового дескриптора.read() called, count=262144— це розмір внутрішнього буфераcat, не нашого.catзавжди просить «скільки є», а драйвер віддає скільки реально має.- Перший
read()повертає 13 байтів,pposзсувається на 13. - Другий
read()викликається зppos=13. У коді є перевіркаif (*ppos >= kbuf_len) return 0;— це і є EOF.catбачить 0 байтів і зупиняється.
Якби цієї перевірки не було (або вона завжди повертала б to_copy навіть коли даних більше нема), cat подумав би, що дані ще є, викликав read() знову — і отримав би нескінченний потік однакових байтів. Класична «зависло» character device, з якою стикається майже кожен, хто пише перший драйвер.
◆ Ґуля № 3 — copytouser / copyfromuser, а не memcpy
Прямий доступ до користувацького вказівника з kernel space заборонений — userspace і kernel живуть у різних адресних просторах, і ядро не довіряє адресі, яку йому передав userspace (вона може бути невалідна або взагалі належати чужому процесу).
not_copied = copy_to_user(ubuf, kbuf + *ppos, to_copy);
⚠ Зверни увагу:
copy_to_user/copy_from_userповертають кількість байтів, яку НЕ вдалось скопіювати (0 = повний успіх). Це протилежноmemcpy, який повертає вказівник на призначення. Переплутати легко, і код мовчки «працюватиме» неправильно, поки не натрапиш на реальну помилку копіювання.
◆ Tainted kernel і контекст живлення
cat /proc/sys/kernel/tainted vcgencmd get_throttled 5120 throttled=0x50000
5120 — ядро позначене tainted одразу кількома прапорцями, серед них той самий, що бачили в dmesg при insmod:
[ 597.659757] mydev: loading out-of-tree module taints kernel.
Будь-який модуль, зібраний поза офіційним деревом ядра, одразу «забруднює» ядро цим прапором — це не помилка, просто маркер для діагностики (якщо щось впаде, maintainer’и ядра одразу бачать, що в системі був сторонній код).
throttled=0x50000 — у логах під час сесії кілька разів проскакувало:
[ 367.294979] hwmon hwmon1: Undervoltage detected! [ 373.342982] hwmon hwmon1: Voltage normalised
0x50000 означає що недостатнє живлення траплялось в цьому сеансі раніше, але зараз все в нормі (молодші біти, що відповідають за активну проблему прямо зараз, не виставлені). Важливо це записати собі окремо: якщо колись з’явиться дивна поведінка модуля чи SD-картки, перше, що варто перевірити — не сам код, а vcgencmd get_throttled. Просадки живлення на Pi 3B+ під навантаженням — не рідкість, і легко прийняти їх симптом за баг драйвера.
◆ Підсумок
Що ми пройшли за цей тиждень. Почали з того, як Linux взагалі бачить пристрої — три типи, major і minor числа, character device як потік байтів. Далі написали mydev.c з нуля: попросили в ядра вільний major через alloc_chrdev_region, зареєстрували cdev з cdev_add, віддали udev створення /dev/mydev через class_create + device_create, і реалізували чотири операції file_operations — open, read, write, release. Між kernel і userspace ходили тільки через copy_to_user / copy_from_user, бо прямий memcpy по користувацькому вказівнику — це шлях до oops.
І по дорозі набили нормальні такі гулі, кожну на реальному залізі:
device_createдає вузол з правами0600—echoвід звичайного користувача впирається в Permission denied, поки не додаси udev rule зMODE="0666".catвикликаєread()двічі: перший раз бере дані, другий отримує 0 (EOF) — і саме ця перевірка*ppos >= kbuf_lenрятує від нескінченного циклу.copy_to_user/copy_from_userповертають кількість байтів, яку не скопіювали (0 = успіх) — протилежно інтуїції відmemcpy.class_createна ядрі 6.4+ приймає один аргумент замість двох — старі приклади з інтернету просто не компілюються.
Це той самий мінімум, що відрізняє «вмію вантажити чужі модулі» від «можу написати свій драйвер». Далі його можна нарощувати — ioctl для команд, poll для асинхронності, кільцевий буфер замість одного слоту — але каркас уже стоїть.
Наступного тижня — HC-SR04. Беремо ультразвуковий далекомір і робимо з нього character device, але цього разу пасивним читанням буфера не обійтись: треба смикнути trigger-пін, дочекатись echo, і виміряти час між фронтами сигналу. А час між фронтами в Linux означає одне — переривання. Зайдемо в interrupt handling з ядра: request_irq, обробка по фронту, вимірювання тривалості імпульсу. І cat /dev/hcsr04 покаже відстань у сантиметрах. Там на нас чекає нова порція ґуль — interrupt-driven вимірювання на Linux, який і близько не реального часу, це окрема пригода.
◆ Дивись також
◆ Словник embedded термінів
Мінімум, який нам мою думку має знати junior embedded-розробник, і про який можуть спитати на співбесіді. Не факт, але це покриває bare-metal STM32 і Linux kernel modules з нашої серії. Колонка «💬 на співбесіді» — типовий ракурс питання.
◆ Загальні поняття
| Термін | Що це | 💬 Питання |
|---|---|---|
| Bare-metal | Код, що працює прямо на залізі без ОС. Сам керуєш регістрами, перериваннями, пам’яттю | «Чим bare-metal відрізняється від програмування під Linux?» |
| Firmware | Прошивка — програма, зашита в пам’ять пристрою | — |
| RTOS | Операційна система реального часу (FreeRTOS, Zephyr). Гарантує час реакції на подію | «Навіщо RTOS, якщо є super-loop у while(1)?» |
| HAL | Hardware Abstraction Layer — шар, що ховає роботу з регістрами за зручними функціями (gpio_write) | «Що дає свій HAL і чим він гірший за прямі регістри?» |
| Toolchain | Набір інструментів збірки: компілятор, лінкер, асемблер (arm-none-eabi-gcc) | — |
| Cross-compilation | «Чому не можна просто gcc на ПК для STM32?» | |
| SoC | System on Chip — процесор + периферія на одному кристалі (RV1106, BCM2837) | — |
◆ Пам’ять і C
| Термін | Що це | 💬 Питання |
|---|---|---|
volatile | Каже компілятору «не оптимізуй це, значення може змінитись ззовні» | Класика. «Навіщо volatile для регістрів і змінних з ISR?» |
| MMIO | Memory-Mapped I/O — регістри периферії доступні як звичайні адреси пам’яті | «Як CPU спілкується з GPIO?» |
.text | Секція з кодом програми (read-only, у Flash) | «Де живе код, а де змінні?» |
.data | Ініціалізовані глобальні змінні (копіюються з Flash у RAM при старті) | — |
.bss | Неініціалізовані глобальні (обнуляються в Reset_Handler) | «Що відбувається з .bss до main()?» |
| Stack | Стек — локальні змінні, адреси повернення. Росте вниз | «Stack vs heap, де що?» |
| Heap | Купа — malloc. У bare-metal зазвичай уникають | «Чому malloc погано в embedded?» |
| Linker script | .ld-файл: де у пам’яті лежать секції, де стек, де вектори | «Навіщо лінкер-скрипт?» |
| Endianness | Порядок байтів: little-endian (ARM) vs big-endian | «У якому порядку зберігається uint32_t?» |
Чому volatile критичний (готова відповідь): Компілятор бачить while (!(SR &amp;amp; RXNE)); і думає: «SR я вже прочитав, він не міняється — зациклю на старому значенні». Без volatile цикл зависне назавжди. volatile змушує читати з пам’яті щоразу. Те саме з прапором, який ставить ISR: без volatile основний код його «не помітить».
◆ Периферія
| Термін | Що це | 💬 Питання |
|---|---|---|
| GPIO | General Purpose I/O — універсальний цифровий пін (вхід/вихід) | — |
| UART | Послідовний інтерфейс, 2 дроти (TX/RX), без тактового сигналу | «Скільки дротів у UART? Що таке baud rate?» |
| I²C | Двопровідна шина (SDA/SCL), багато пристроїв на адресах | «Чим I²C відрізняється від SPI?» |
| SPI | Швидка шина (MOSI/MISO/SCK/CS), full-duplex | «Коли SPI, коли I²C?» |
| PWM | Широтно-імпульсна модуляція — це метод керування потужністю, що базується на зміні тривалості електричних імпульсів, а не їхньої амплітуди. Замість постійного живлення пристрій отримує серію швидких ввімкнень та вимкнень, що дозволяє ефективно регулювати напругу без виділення зайвого тепла |
◆ Переривання і час
| Термін | Що це | 💬 Питання |
|---|---|---|
| Interrupt / IRQ | Сигнал, що змушує CPU кинути все й виконати обробник | Класика. «Interrupt vs polling?» |
| ISR | Interrupt Service Routine — функція-обробник переривання | «Що НЕ можна робити в ISR?» |
| Polling | Активне опитування в циклі замість переривання | — |
| Vector table | Таблиця адрес обробників на початку Flash (у нас — startup.c) | «Що лежить за адресою 0×08000000?» |
| NVIC | Контролер переривань у Cortex-M (пріоритети, вмикання) | — |
| SysTick | Системний таймер ядра Cortex-M (у нас — delay_ms) | «Як зробити затримку без бібліотек?» |
| DMA | Прямий доступ до пам’яті без участі CPU | «Навіщо DMA при роботі з UART/ADC?» |
| Watchdog | Сторожовий таймер: перезавантажує систему, якщо її «повісило» | «Як захиститись від зависання?» |
| Race condition | Гонитва: результат залежить від порядку доступу до спільних даних | — |
Що не можна в ISR (готова відповідь): ISR має бути коротким. Не можна: довгі затримки, printf/malloc, блокуючі операції. Спільні з основним кодом дані — тільки через volatile (а в Linux — через блокування). Ідея: ISR ставить прапор / кладе байт у буфер, а важку роботу робить основний цикл.
◆ Завантаження
| Термін | Що це | 💬 Питання |
|---|---|---|
| Bootloader | Перша програма після ввімкнення; готує систему й запускає основну | «Що відбувається від подачі живлення до main()?» |
| Reset_Handler | Перша функція bare-metal: копіює .data, чистить .bss, кличе main() | «Хто викликає main() на STM32?» |
| U-Boot | Універсальний bootloader для embedded Linux | «Роль U-Boot у boot-ланцюгу?» |
| SPL | Secondary Program Loader — мінімальний перший етап (вліз у малий SRAM) | — |
| Boot sequence | ROM → SPL → U-Boot → kernel → init → userspace | «Намалюй ланцюг завантаження Linux» |
| RCC / clock gating | Тактування периферії: вимкнений блок не працює, поки не ввімкнеш такт | «Чому GPIO мовчить, хоч код правильний?» |
◆ Linux kernel
| Термін | Що це | 💬 Питання |
|---|---|---|
| udev | User-space демон, що автоматично створює/видаляє /dev/* вузли, коли ядро повідомляє про новий пристрій | «Звідки береться /dev/sdb1, коли встромляєш флешку?» |
mknod | Ручне створення device-вузла (mknod /dev/x c major minor). Потрібен тільки якщо драйвер НЕ використовує class_create/device_create | «Чим відрізняється ручний mknod від автоматичного udev?» |
class_create / device_create | API ядра: драйвер сам реєструє клас і просить udev створити /dev/... без ручного mknod | «Як /dev/mydev з’являється сам після insmod?» |
| Device permissions (udev rules) | За замовчуванням device_create дає 0600 (тільки root). Права налаштовуються через /etc/udev/rules.d/*.rules, наприклад KERNEL=="mydev", MODE="0666" | «Чому echo > /dev/mydev дає Permission denied навіть коли драйвер запущений?» |
| User space / kernel space | Розділення: програми (обмежені права) vs ядро (повний доступ) | Класика. «Чим відрізняються?» |
| System call | Місток із user space у kernel (read, write, open) | «Як userspace просить ядро щось зробити?» |
| Kernel module | Код, що додається в ядро на льоту (insmod/rmmod) | «Навіщо модуль, а не вкомпілювати в ядро?» |
| Character device | Пристрій, з яким працюють побайтово як з потоком (/dev/ttyS0) | «char vs block device?» |
| Block device | Пристрій блоками з довільним доступом (диск, SD) | — |
| major / minor | Старший номер = драйвер, молодший = конкретний пристрій | «Що значать цифри в ls -l /dev/...?» |
file_operations | Структура з вказівниками на open/read/write/release драйвера | «Як cat читає твій /dev/mydev?» |
| sysfs / procfs | Віртуальні ФС, що показують стан ядра (/sys, /proc) | — |
| Device Tree | Опис заліза для ядра у вигляді дерева (.dts → .dtb) | «Як ядро дізнається, яка периферія є на платі?» |
printk | printf ядра; вивід читаємо через dmesg | — |
Platform driver / probe | Драйвер, що «знаходить» своє залізо через DT; probe() викликається при збігу | «Що таке probe і коли він спрацьовує?» |
copy_to_user / copy_from_user | Безпечне копіювання між kernel і user space | «Чому не можна просто memcpy з userspace-вказівника?» |
| Kernel oops / panic | oops — локальна помилка (модуль), panic — фатальна (система стає) | «Різниця між oops і panic?» |
| Tainted kernel | «Забруднене» ядро (сторонній модуль, oops) — впливає на підтримку | — |
user vs kernel space (готова відповідь): User space — де живуть програми, з обмеженими правами й власною віртуальною пам’яттю; впала програма — впала тільки вона. Kernel space — ядро з повним доступом до заліза; помилка тут валить усю систему. Перехід — лише через системні виклики. Тому copy_from_user: вказівник з userspace не можна довіряти напряму (може бути невалідним або чужим).
◆ Конкурентність
| Термін | Що це | 💬 Питання |
|---|---|---|
| Mutex | Блокування, що може спати; для довгих секцій, тільки в контексті процесу | «mutex vs spinlock — коли що?» |
| Spinlock | Блокування, що крутиться в циклі (busy-wait); для коротких секцій, можна в ISR | — |
| Semaphore | Лічильник доступів; дозволяє N одночасних власників | — |
| Atomic | Операція, яку не можна перервати посередині (atomic_inc) | «Як безпечно інкрементувати лічильник?» |
mutex vs spinlock (готова відповідь): Mutex може заснути, поки чекає — тому не можна в ISR (там спати не можна). Spinlock крутиться в циклі й не спить — підходить для ISR, але марнує CPU, тож тільки для дуже коротких секцій. Правило: довга критична секція в контексті процесу — mutex; коротка або в перериванні — spinlock.
◆ «Підступні» питання
| Питання | Коротка правильна відповідь |
|---|---|
Навіщо volatile? | Заборонити оптимізацію змінних, що міняються ззовні (регістри, прапори з ISR) |
Чому не malloc в ISR? | Може блокувати/спати, фрагментує купу, недетермінований час — ISR має бути швидким |
sudo echo 1 > /sys/... не працює, чому? | > виконує шел від користувача, не від root. Треба echo 1 | sudo tee |
echo > /dev/mydev дає Permission denied, хоч модуль завантажений? | device_create за замовчуванням ставить 0600 (тільки root). Треба sudo або udev rule з MODE="0666" |
| Чому GPIO мовчить, хоч код вірний? | Не ввімкнене тактування порту (RCC / clock gating) |
| Stack чи heap росте вгору? | Stack зазвичай вниз, heap вгору — назустріч одне одному |
| Що буде при колізії stack і heap? | Stack overflow / пошкодження даних — у bare-metal без MMU тихо і боляче |
| Чому модуль під інше ядро не вставляється? | Version magic: .ko прив’язаний до версії й конфігу ядра |
| open-drain навіщо? | Кілька пристроїв на одній лінії (I²C), wired-AND, рівні через pull-up |
◆ Що читати далі
- RM0008 — Reference Manual STM32F103 (головний довідник по регістрах)
- Linux Device Drivers 3rd ed. — lwn.net/Kernel/LDD3/ (безкоштовно)
- The Linux Programming Interface — Michael Kerrisk (по системних викликах)
- The Linux Kernel Module Programming Guide — tldp.org
📌 Команди до цих термінів.
◆ Додаток: довідник команд
Усе, що ми використовували в статтях
11–12 (kernel modules на Raspberry Pi), зібране в одному місці. Плюс кілька команд, які знадобляться далі. Тримай під рукою — у kernel-розробці пів роботи цеdmesgв одному вікні йmakeв іншому.
◆ Керування модулями ядра
sudo insmod hello.ko # завантажити модуль sudo insmod hello.ko val=5 name=stm32 # з параметрами (module_param) sudo rmmod hello # вивантажити (без .ko!) sudo modprobe hello # завантажити з /lib/modules + залежності sudo modprobe -r hello # вивантажити з залежностями lsmod # список завантажених модулів lsmod | grep hello modinfo hello.ko # автор, ліцензія, параметри, залежності sudo depmod -a # перебудувати таблицю залежностей
| Команда | Що робить | Коли треба |
|---|---|---|
insmod | вставляє конкретний .ko | під час розробки, локальний файл |
modprobe | шукає в /lib/modules, тягне залежності | коли модуль вже встановлений |
rmmod | вивантажує | після тесту |
modinfo | метадані модуля | перевірити module_param, ліцензію |
⚠ Якщо
rmmodкажеModule is in use— хтось тримає/dev/...відкритим або лічильник посилань (refcount) не нуль. Дивисьlsmod(третя колонка — used by).rmmod -fтільки якщо ядро зібране зCONFIG_MODULE_FORCE_UNLOAD— інакше шлях один:reboot.
◆ Логи ядра — dmesg і printk
dmesg # весь кільцевий буфер ядра dmesg -w # «хвіст» у реальному часі (як tail -f) dmesg -T # людські timestamp замість секунд від boot dmesg -C # очистити буфер (зручно перед insmod) dmesg -c # вивести І очистити dmesg --level=err,warn # тільки помилки й попередження dmesg | grep hello # фільтр по нашому модулю journalctl -k # те саме через systemd journal journalctl -k -f # follow
Рівні printk у коді модуля:
pr_emerg("система падає\n"); // KERN_EMERG = 0
pr_err("щось зламалось\n"); // KERN_ERR = 3
pr_warn("обережно\n"); // KERN_WARNING= 4
pr_info("все ок\n"); // KERN_INFO = 6
pr_debug("деталі\n"); // KERN_DEBUG = 7 (треба DEBUG)
cat /proc/sys/kernel/printk # поточні рівні консолі
echo 8 > /proc/sys/kernel/printk # показувати все аж до debug
⚠
pr_debugмовчить, поки модуль не зібраний з-DDEBUGабо не ввімкнений через dynamic debug (/sys/kernel/debug/dynamic_debug/control).
◆ Збірка kernel-модуля
Мінімальний Makefile для out-of-tree модуля:
obj-m += hello.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean make # збірка -> hello.ko uname -r # версія ядра (має збігатись з headers!) ls /lib/modules/$(uname -r)/build # тут лежать kernel headers sudo apt install raspberrypi-kernel-headers # якщо build/ немає
⚠ Модуль зібраний під одну версію ядра не вставиться в іншу —
insmodдастьversion magic ... should be .... Перевірuname -rДО збірки. На нашому 3B+ це6.18.33+rpt-rpi-v8.
◆ sysfs / procfs — підглядаємо в ядро
ls /sys/module/hello/parameters/ # параметри module_param cat /sys/module/hello/parameters/val echo 7 > /sys/module/hello/parameters/val # змінити на льоту (якщо 0644) cat /proc/modules # те, що читає lsmod cat /proc/devices # зайняті major-номери (char і block) cat /proc/interrupts # лічильники переривань по лініях cat /proc/iomem # мапа фізичної пам'яті cat /proc/sys/kernel/tainted # чи «забруднене» ядро (0 = чисте)
⚠
module_paramз правами0644можна міняти через sysfs після завантаження — але це НЕ викликаєprobe()повторно. Класична «ґуля»: думаєш, що змінив параметр і драйвер перечитав, а він прочитав значення один раз під часinsmod.
◆ Character device — /dev/mydev
cat /proc/devices | grep mydev # дізнатись виданий major sudo mknod /dev/mydev c 240 0 # створити вручну: c = char, major, minor ls -l /dev/mydev # перша літера 'c', далі major, minor cat /dev/mydev # викликає .read у file_operations echo "hello" > /dev/mydev # викликає .write sudo rm /dev/mydev # прибрати
⚠ Якщо в драйвері використовуєш
class_create+device_create, вузол/dev/mydevстворює udev автоматично —mknodруками тоді не потрібен.mknodлишається для випадку «голого»register_chrdevбез класу.
Права на автостворений вузол — device_create за замовчуванням дає 0600 (тільки root). Без правила echo/cat від звичайного користувача впадуть з Permission denied:
echo 'KERNEL=="mydev", MODE="0666"' | sudo tee /etc/udev/rules.d/99-mydev.rules sudo udevadm control --reload-rules sudo rmmod mydev && sudo insmod mydev.ko # перестворити вузол з новим правилом ls -l /dev/mydev # перевірити права
⚠ Вміст udev-правила (
KERNEL=="mydev", MODE="0666") пиши черезteeу файл, а НЕ вставляй у термінал як команду — bash спробує її виконати і впаде зcommand not found.
◆ GPIO з командного рядка — libgpiod 2.x
gpiodetect # список gpiochip у системі gpioinfo gpiochip0 # усі лінії чіпа: напрям, споживач, прапори gpioget -c gpiochip0 17 # прочитати стан лінії 17 gpioset -c gpiochip0 17=1 # виставити 1 gpioset -c gpiochip0 -t 500ms 17=1 # блимати (--toggle) gpiomon -c gpiochip0 17 # ловити edge-події в реальному часі gpiofind GPIO17 # знайти лінію за іменем
⚠ libgpiod 2.x ламає сумісність з 1.x. У 1.x чіп передавався позиційно:
gpioget gpiochip0 17. У 2.x — через-c/--chip, а флаг-t/--toggleз’явився саме у 2.x. На свіжому Pi OS у тебе майже напевно 2.x — стара шпаргалка з інтернету не спрацює.
Raspberry-специфічне (Bookworm і новіше):
pinctrl get 17 # стан і функція піна (замінив raspi-gpio) pinctrl set 17 op dh # output, drive high pinctrl set 17 ip pu # input, pull-up
Старий sysfs-інтерфейс
/sys/class/gpio/export— deprecated ще з ядра 4.8 і прибраний у нових. Цікавий, тільки як історичний контраст (це окрема тема — userspace GPIO проти kernel module).
◆ Device Tree та overlay
ls /proc/device-tree/ # активне DT у вигляді файлової системи dtc -@ -I dts -O dtb -o my.dtbo my-overlay.dts # компілюємо overlay sudo cp my.dtbo /boot/firmware/overlays/ dtoverlay -l # завантажені overlay dtoverlay -a # доступні overlay sudo dtoverlay my # завантажити на льоту (без reboot) sudo dtoverlay -r my # вивантажити fdtdump my.dtbo # дамп бінарного DT у читабельне

Підключення в /boot/firmware/config.txt:
dtoverlay=my dtparam=audio=on
⚠ На Bookworm конфіг переїхав у
/boot/firmware/config.txt(раніше/boot/config.txt). Помилки DT під час boot шукай уdmesg | grep -i overlay.
◆ Інформація про систему
uname -a # все про ядро uname -r # тільки версія (для headers і збірки) cat /proc/cpuinfo # ядра, ревізія, модель плати cat /proc/meminfo # пам'ять lscpu # архітектура, кеші, ядра vcgencmd measure_temp # температура SoC (Raspberry-специфічне) vcgencmd get_throttled # чи був тротлінг/просадка живлення file hello.ko # архітектура й тип бінаря
◆ Дебаг і аналіз бінарів
objdump -d hello.ko # дизасемблер nm hello.ko # таблиця символів readelf -a hello.ko # секції, символи, заголовки ELF addr2line -e vmlinux # адреса з kernel oops -> файл:рядок strace ./myapp # системні виклики userspace-програми ltrace ./myapp # виклики бібліотек strings hello.ko # витягти текстові рядки size hello.ko # розмір .text / .data / .bss

Читаємо kernel oops: у дампі шукай рядок PC is at <функція>+0x.../0x... та Call trace: — це стек викликів у момент падіння. +0x.. це зсув від початку функції; через addr2line або objdump мапиться на конкретний рядок коду. NULL-дереференс зазвичай виглядає як звернення за адресою близько 0000000000000000.
⚠ Після oops модуль часто лишається «напівживий»:
lsmodпоказує його, аrmmodне дає. Ядро при цьому позначається tainted (cat /proc/sys/kernel/tainted≠ 0). Чистий стан — тільки післяreboot.



◆ Дрібниці, які економлять час
watch -n1 'dmesg | tail -5' # живий моніторинг останніх логів sudo dmesg -C && sudo insmod x.ko && dmesg # чистий лог одного тесту echo 1 | sudo tee /sys/... # запис у sysfs з-під sudo (тільки tee, не >) ssh pi+ # 3B+ (192.168.1.123) — наш робочий ssh pi # 3B (192.168.1.109)
10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівдавай щось серйозне і практичне, напр.:
www.instagram.com/reel/DYbya2LiLfk
Терпіння. Ми ж йдемо крок за кроком і ща планом
за планому оце було 0 коментарів, якби не каструлезрада
dou.ua/...rums/topic/60357/#3095450
P.S.
Я так розумію, після дюжини статей на ДОУ про ембедед перебираєш оферами?
Так 0 коментарів це нормально. Не люблю коментарі. Стосовно каструлезради, ну таке собі, людям зайнятись немає чим. Та ні, не перебираю, немає з чого🙂 то тільки ти мене підвищив з охоронця в атб 🤗
А мені в ПП навіть на ДОУ писали
Ну мені теж писали в приват, якщо правильно зрозумів та що то змінює?
Якщо не збираєшся їхати в Харків, то нічого
Мій рідний Харків зараз під дронами. Постійно
Коментарі повинні бути із серії «added value». Все інще засмічує і заважає.
В цьому проекті на жаль була помилка в дизайні заліза. Через деякий час , чіп не міг розпізнати базові команди. Проблема з живленням я думаю. Тобто FPGA design мав баг. Повернули назад допрацювати розробникам. В перспективі весь проект ставатиме ASIC. Це коли буде масове виробництво.