STM32 з нуля без HAL: Місяць 4, Тиждень 3: пишемо /dev/mydev — character device driver. Частина 13

💡 Усі статті, обговорення, новини для початківців — в одному місці. Приєднуйтесь до Junior спільноти!

Ну що ж, 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)
HALHardware Abstraction Layer — шар, що ховає роботу з регістрами за зручними функціями (gpio_write)«Що дає свій HAL і чим він гірший за прямі регістри?»
ToolchainНабір інструментів збірки: компілятор, лінкер, асемблер (arm-none-eabi-gcc)
Cross-compilation«Чому не можна просто gcc на ПК для STM32?»
SoCSystem on Chip — процесор + периферія на одному кристалі (RV1106, BCM2837)

◆ Пам’ять і C

ТермінЩо це💬 Питання
volatileКаже компілятору «не оптимізуй це, значення може змінитись ззовні»Класика. «Навіщо volatile для регістрів і змінних з ISR?»
MMIOMemory-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;amp; RXNE)); і думає: «SR я вже прочитав, він не міняється — зациклю на старому значенні». Без volatile цикл зависне назавжди. volatile змушує читати з пам’яті щоразу. Те саме з прапором, який ставить ISR: без volatile основний код його «не помітить».

◆ Периферія

ТермінЩо це💬 Питання
GPIOGeneral 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?»
ISRInterrupt 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-ланцюгу?»
SPLSecondary Program Loader — мінімальний перший етап (вліз у малий SRAM)
Boot sequenceROM → SPL → U-Boot → kernel → init → userspace«Намалюй ланцюг завантаження Linux»
RCC / clock gatingТактування периферії: вимкнений блок не працює, поки не ввімкнеш такт«Чому GPIO мовчить, хоч код правильний?»

◆ Linux kernel

ТермінЩо це💬 Питання
udevUser-space демон, що автоматично створює/видаляє /dev/* вузли, коли ядро повідомляє про новий пристрій«Звідки береться /dev/sdb1, коли встромляєш флешку?»
mknodРучне створення device-вузла (mknod /dev/x c major minor). Потрібен тільки якщо драйвер НЕ використовує class_create/device_create«Чим відрізняється ручний mknod від автоматичного udev?»
class_create / device_createAPI ядра: драйвер сам реєструє клас і просить 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)«Як ядро дізнається, яка периферія є на платі?»
printkprintf ядра; вивід читаємо через dmesg
Platform driver / probeДрайвер, що «знаходить» своє залізо через DT; probe() викликається при збігу«Що таке probe і коли він спрацьовує?»
copy_to_user / copy_from_userБезпечне копіювання між kernel і user space«Чому не можна просто memcpy з userspace-вказівника?»
Kernel oops / panicoops — локальна помилка (модуль), 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)
👍ПодобаєтьсяСподобалось12
До обраногоВ обраному6
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

давай щось серйозне і практичне, напр.:
www.instagram.com/reel/DYbya2LiLfk

Терпіння. Ми ж йдемо крок за кроком і ща планом

за планому оце було 0 коментарів, якби не каструлезрада
dou.ua/...​rums/topic/60357/#3095450
P.S.
Я так розумію, після дюжини статей на ДОУ про ембедед перебираєш оферами?

Так 0 коментарів це нормально. Не люблю коментарі. Стосовно каструлезради, ну таке собі, людям зайнятись немає чим. Та ні, не перебираю, немає з чого🙂 то тільки ти мене підвищив з охоронця в атб 🤗

А мені в ПП навіть на ДОУ писали

Ну мені теж писали в приват, якщо правильно зрозумів та що то змінює?

Якщо не збираєшся їхати в Харків, то нічого

Мій рідний Харків зараз під дронами. Постійно

Коментарі повинні бути із серії «added value». Все інще засмічує і заважає.

В цьому проекті на жаль була помилка в дизайні заліза. Через деякий час , чіп не міг розпізнати базові команди. Проблема з живленням я думаю. Тобто FPGA design мав баг. Повернули назад допрацювати розробникам. В перспективі весь проект ставатиме ASIC. Це коли буде масове виробництво.

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