Ну що ж, 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 є три типи пристроїв:
|
1 2 3 |
/dev/sda → block <span class="hljs-title function_">device</span> <span class="hljs-params">(диск — читання/запис блоками)</span> /dev/ttyS0 → <span class="hljs-type">char</span> <span class="hljs-title function_">device</span> <span class="hljs-params">(UART — потік байтів)</span> /dev/<span class="hljs-literal">null</span> → <span class="hljs-type">char</span> <span class="hljs-title function_">device</span> <span class="hljs-params">(спеціальний — поглинає все)</span> |
Character device — це потік байтів без внутрішньої структури. Ядро присвоює йому major і minor числа:
|
1 2 3 4 |
<span class="hljs-built_in">ls</span> -la /dev/ttyS0 <span class="hljs-comment"># crw-rw---- 1 root dialout 4, 64 Jun 15 12:00 /dev/ttyS0</span> <span class="hljs-comment"># ^ ^</span> <span class="hljs-comment"># major minor</span> |
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.
◆ Готуємо середовище
|
1 2 3 |
ssh pi+ <span class="hljs-built_in">mkdir</span> -p ~/kernel-modules/03-chrdev <span class="hljs-built_in">cd</span> ~/kernel-modules/03-chrdev |
◆ Для самих маленьких: звідки береться
ssh pi+По серії ти часто побачиш команди типу
ssh piчиssh pi+— це не окрема утиліта, а SSH host alias. Замістьssh alex@192.168.1.123щоразу, один раз описуєш плату в~/.ssh/config:
123 Host pi+<span class="hljs-type"> HostName</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">1.123</span><span class="hljs-type"> User</span> alexІ далі просто
ssh pi+. Коли плат у тебе не одна-дві, а ціла флотилія (у мене на цьому етапі 6 Raspberry Pi плюс Luckfox і Orange Pi), це рятує від постійного набивання IP руками. Туди ж можна додати ключі, порти, прапорці — наприклад, для Luckfox, який не любить pubkey-авторизацію:
1234 Host luckfox<span class="hljs-type"> HostName</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">1.125</span><span class="hljs-type"> User</span> root<span class="hljs-type"> PubkeyAuthentication</span> noМіж іншим, є ще один зручний спосіб —
ProxyJump. Якщо якась плата сидить у підмережі, куди немає прямого доступу, і достукатись до неї можна лише через проміжний хост, це пишеться в один рядок:
1234 Host orange-hidden<span class="hljs-type"> HostName</span> <span class="hljs-number">192.168</span>.<span class="hljs-number">2.50</span><span class="hljs-type"> User</span> crud<span class="hljs-type"> ProxyJump</span> pi+Тепер
ssh orange-hiddenспершу зайде наpi+, а звідти прокине з’єднання далі — жодних ручних тунелів. Для розподілених штук на кшталт майбутньої TDOA-флотилії, де ноди розкидані по сегментах мережі, це стане в нагоді.Дрібниця, але коли ти щодня стрибаєш між п’ятьма платами — це різниця між «набрав
ssh pi+» і «згадав який там був IP у 3B+».
Зверни увагу,що рядок ProxyJump pi+посилається на вже описаний вище alias — показує що хости в конфігу можна складати ланцюжком.
Перед тим як писати свій драйвер, хочу глянути, що вже є в системі — щоб розуміти, куди ми вписуємось, і не наступити комусь на major. Дивимось усі character devices і список зареєстрованих драйверів:
|
1 2 |
<span class="hljs-built_in">ls</span> -la /dev/ | grep <span class="hljs-string">"^c"</span> | <span class="hljs-built_in">head</span> -20 <span class="hljs-built_in">cat</span> /proc/devices | <span class="hljs-built_in">head</span> -30 |

|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Character devices: 1 mem 4 /dev/vc/0 4 <span class="hljs-built_in">tty</span> 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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
<span class="hljs-comment">/* * 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 */</span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">include</span> </span> <span class="hljs-meta">#<span class="hljs-keyword">define</span> DRIVER_NAME <span class="hljs-string">"mydev"</span></span> <span class="hljs-meta">#<span class="hljs-keyword">define</span> BUFFER_SIZE 256</span> <span class="hljs-type">static</span> <span class="hljs-type">dev_t</span> dev_num; <span class="hljs-type">static</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">cdev</span> my_cdev; <span class="hljs-type">static</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">class</span> *my_class; <span class="hljs-type">static</span> <span class="hljs-type">char</span> kbuf[BUFFER_SIZE]; <span class="hljs-type">static</span> <span class="hljs-type">size_t</span> kbuf_len; <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-title">DEFINE_MUTEX</span><span class="hljs-params">(mydev_mutex)</span></span>; <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title">mydev_open</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *inode, <span class="hljs-keyword">struct</span> file *filp)</span> </span>{ <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: open() called, pid=%d\n"</span>, current->pid); <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; } <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> <span class="hljs-title">mydev_release</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *inode, <span class="hljs-keyword">struct</span> file *filp)</span> </span>{ <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: release() called, pid=%d\n"</span>, current->pid); <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; } <span class="hljs-comment">/* * ⚠ Критичний момент: не можна просто memcpy між kernel і user! * Userspace і kernel живуть у різних адресних просторах. * copy_to_user() робить безпечну копію і перевіряє чи адреса user валідна. */</span> <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">ssize_t</span> <span class="hljs-title">mydev_read</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> file *filp, <span class="hljs-type">char</span> __user *ubuf, <span class="hljs-type">size_t</span> count, <span class="hljs-type">loff_t</span> *ppos)</span> </span>{ <span class="hljs-type">size_t</span> to_copy; <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> not_copied; <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: read() called, count=%zu, ppos=%lld\n"</span>, count, *ppos); <span class="hljs-built_in">mutex_lock</span>(&mydev_mutex); <span class="hljs-keyword">if</span> (*ppos >= kbuf_len) { <span class="hljs-built_in">mutex_unlock</span>(&mydev_mutex); <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: read() → EOF\n"</span>); <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; } to_copy = <span class="hljs-built_in">min</span>(count, kbuf_len - (<span class="hljs-type">size_t</span>)*ppos); not_copied = <span class="hljs-built_in">copy_to_user</span>(ubuf, kbuf + *ppos, to_copy); <span class="hljs-keyword">if</span> (not_copied) { <span class="hljs-built_in">mutex_unlock</span>(&mydev_mutex); <span class="hljs-built_in">pr_err</span>(<span class="hljs-string">"mydev: copy_to_user failed, %lu bytes not copied\n"</span>, not_copied); <span class="hljs-keyword">return</span> -EFAULT; } *ppos += to_copy; <span class="hljs-built_in">mutex_unlock</span>(&mydev_mutex); <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: read() → %zu bytes\n"</span>, to_copy); <span class="hljs-keyword">return</span> to_copy; } <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">ssize_t</span> <span class="hljs-title">mydev_write</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> file *filp, <span class="hljs-type">const</span> <span class="hljs-type">char</span> __user *ubuf, <span class="hljs-type">size_t</span> count, <span class="hljs-type">loff_t</span> *ppos)</span> </span>{ <span class="hljs-type">unsigned</span> <span class="hljs-type">long</span> not_copied; <span class="hljs-type">size_t</span> to_write; <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: write() called, count=%zu\n"</span>, count); <span class="hljs-built_in">mutex_lock</span>(&mydev_mutex); to_write = <span class="hljs-built_in">min</span>(count, (<span class="hljs-type">size_t</span>)(BUFFER_SIZE - <span class="hljs-number">1</span>)); not_copied = <span class="hljs-built_in">copy_from_user</span>(kbuf, ubuf, to_write); <span class="hljs-keyword">if</span> (not_copied) { <span class="hljs-built_in">mutex_unlock</span>(&mydev_mutex); <span class="hljs-built_in">pr_err</span>(<span class="hljs-string">"mydev: copy_from_user failed\n"</span>); <span class="hljs-keyword">return</span> -EFAULT; } kbuf[to_write] = <span class="hljs-string">'\0'</span>; kbuf_len = to_write; *ppos = to_write; <span class="hljs-built_in">mutex_unlock</span>(&mydev_mutex); <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: write() → saved %zu bytes: \"%s\"\n"</span>, to_write, kbuf); <span class="hljs-comment">/* * ⚠ Важливо: повертаємо count (що просив userspace), не to_write. * Якщо повернемо менше — shell вирішить що треба retry і викличе * write() ще раз з рештою. Повертай те що просили, якщо все ок. */</span> <span class="hljs-keyword">return</span> count; } <span class="hljs-type">static</span> <span class="hljs-type">const</span> <span class="hljs-keyword">struct</span> <span class="hljs-title class_">file_operations</span> mydev_fops = { .owner = THIS_MODULE, .open = mydev_open, .release = mydev_release, .read = mydev_read, .write = mydev_write, }; <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">int</span> __init <span class="hljs-title">mydev_init</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> </span>{ <span class="hljs-type">int</span> ret; ret = <span class="hljs-built_in">alloc_chrdev_region</span>(&dev_num, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, DRIVER_NAME); <span class="hljs-keyword">if</span> (ret < <span class="hljs-number">0</span>) { <span class="hljs-built_in">pr_err</span>(<span class="hljs-string">"mydev: alloc_chrdev_region failed: %d\n"</span>, ret); <span class="hljs-keyword">return</span> ret; } <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: registered with major=%d, minor=%d\n"</span>, <span class="hljs-built_in">MAJOR</span>(dev_num), <span class="hljs-built_in">MINOR</span>(dev_num)); <span class="hljs-built_in">cdev_init</span>(&my_cdev, &mydev_fops); my_cdev.owner = THIS_MODULE; ret = <span class="hljs-built_in">cdev_add</span>(&my_cdev, dev_num, <span class="hljs-number">1</span>); <span class="hljs-keyword">if</span> (ret < <span class="hljs-number">0</span>) { <span class="hljs-built_in">pr_err</span>(<span class="hljs-string">"mydev: cdev_add failed: %d\n"</span>, ret); <span class="hljs-keyword">goto</span> err_cdev; } <span class="hljs-comment">/* ⚠ ядро 6.4+: class_create приймає ОДИН аргумент (раніше було два) */</span> my_class = <span class="hljs-built_in">class_create</span>(DRIVER_NAME); <span class="hljs-keyword">if</span> (<span class="hljs-built_in">IS_ERR</span>(my_class)) { ret = <span class="hljs-built_in">PTR_ERR</span>(my_class); <span class="hljs-built_in">pr_err</span>(<span class="hljs-string">"mydev: class_create failed: %d\n"</span>, ret); <span class="hljs-keyword">goto</span> err_class; } <span class="hljs-keyword">if</span> (<span class="hljs-built_in">IS_ERR</span>(<span class="hljs-built_in">device_create</span>(my_class, <span class="hljs-literal">NULL</span>, dev_num, <span class="hljs-literal">NULL</span>, DRIVER_NAME))) { ret = -EINVAL; <span class="hljs-built_in">pr_err</span>(<span class="hljs-string">"mydev: device_create failed\n"</span>); <span class="hljs-keyword">goto</span> err_device; } <span class="hljs-built_in">memset</span>(kbuf, <span class="hljs-number">0</span>, BUFFER_SIZE); kbuf_len = <span class="hljs-number">0</span>; <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: /dev/%s created, ready\n"</span>, DRIVER_NAME); <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; err_device: <span class="hljs-built_in">class_destroy</span>(my_class); err_class: <span class="hljs-built_in">cdev_del</span>(&my_cdev); err_cdev: <span class="hljs-built_in">unregister_chrdev_region</span>(dev_num, <span class="hljs-number">1</span>); <span class="hljs-keyword">return</span> ret; } <span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">void</span> __exit <span class="hljs-title">mydev_exit</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> </span>{ <span class="hljs-built_in">device_destroy</span>(my_class, dev_num); <span class="hljs-built_in">class_destroy</span>(my_class); <span class="hljs-built_in">cdev_del</span>(&my_cdev); <span class="hljs-built_in">unregister_chrdev_region</span>(dev_num, <span class="hljs-number">1</span>); <span class="hljs-built_in">pr_info</span>(<span class="hljs-string">"mydev: /dev/%s removed\n"</span>, DRIVER_NAME); } <span class="hljs-built_in">module_init</span>(mydev_init); <span class="hljs-built_in">module_exit</span>(mydev_exit); <span class="hljs-built_in">MODULE_LICENSE</span>(<span class="hljs-string">"GPL"</span>); <span class="hljs-built_in">MODULE_AUTHOR</span>(<span class="hljs-string">"Alex"</span>); <span class="hljs-built_in">MODULE_DESCRIPTION</span>(<span class="hljs-string">"Character device driver: /dev/mydev з кільцевим буфером"</span>); <span class="hljs-built_in">MODULE_VERSION</span>(<span class="hljs-string">"0.1"</span>); |
Makefile:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
obj-m += mydev.o KDIR := /lib/modules/<span class="hljs-variable">$(<span class="hljs-built_in">shell</span> uname -r)</span>/build PWD := <span class="hljs-variable">$(<span class="hljs-built_in">shell</span> pwd)</span> <span class="hljs-section">all:</span> <span class="hljs-variable">$(MAKE)</span> -C <span class="hljs-variable">$(KDIR)</span> M=<span class="hljs-variable">$(PWD)</span> modules <span class="hljs-section">clean:</span> <span class="hljs-variable">$(MAKE)</span> -C <span class="hljs-variable">$(KDIR)</span> M=<span class="hljs-variable">$(PWD)</span> clean <span class="hljs-section">reload:</span> -sudo rmmod mydev 2>/dev/null sudo insmod mydev.ko dmesg | tail -10 |
◆ Збірка

|
1 2 3 4 5 6 7 8 9 10 11 12 |
<span class="hljs-built_in">make</span> <span class="hljs-built_in">make</span> -C /lib/modules/<span class="hljs-number">6.18</span><span class="hljs-number">.33</span>+rpt-rpi-v8/build M=/home/alex/kernel-modules/<span class="hljs-number">03</span>-chrdev modules <span class="hljs-built_in">make</span>[<span class="hljs-number">1</span>]: Entering directory <span class="hljs-string">'/usr/src/linux-headers-6.18.33+rpt-rpi-v8'</span> <span class="hljs-built_in">make</span>[<span class="hljs-number">2</span>]: Entering directory <span class="hljs-string">'/home/alex/kernel-modules/03-chrdev'</span> CC [M] mydev.o MODPOST Module.symvers CC [M] mydev.mod.o CC [M] .module-common.o LD [M] mydev.ko <span class="hljs-built_in">make</span>[<span class="hljs-number">2</span>]: Leaving directory <span class="hljs-string">'/home/alex/kernel-modules/03-chrdev'</span> <span class="hljs-built_in">make</span>[<span class="hljs-number">1</span>]: Leaving directory <span class="hljs-string">'/usr/src/linux-headers-6.18.33+rpt-rpi-v8'</span> |
Чисто, без warning’ів. Зверни увагу на class_create(DRIVER_NAME) в коді — один аргумент. На ядрі 6.18 (наш 3B+) старий двоаргументний виклик class_create(THIS_MODULE, name) просто не скомпілюється.
◆ Ґуля № 1 — Permission denied
|
1 2 3 4 5 6 7 8 |
sudo insmod mydev.ko <span class="hljs-built_in">ls</span> -l /dev/mydev crw------- 1 root root 236, 0 Jun 30 08:15 /dev/mydev <span class="hljs-built_in">echo</span> <span class="hljs-string">"hello kernel"</span> > /dev/mydev -bash: /dev/mydev: Permission denied |
device_create створив вузол з правами 0600 за замовчуванням — читати й писати може тільки root. Так поводиться драйвер без явного налаштування прав. Швидкий обхід — sudo
|
1 2 3 4 |
sudo bash -c <span class="hljs-string">'echo "hello kernel" > /dev/mydev'</span> sudo <span class="hljs-built_in">cat</span> /dev/mydev hello kernel |
Працює. Але правильне рішення виставити права через udev rule:
|
1 2 3 4 5 6 |
<span class="hljs-built_in">echo</span> <span class="hljs-string">'KERNEL=="mydev", MODE="0666"'</span> | sudo <span class="hljs-built_in">tee</span> /etc/udev/rules.d/99-mydev.rules sudo udevadm control --reload-rules sudo rmmod mydev && sudo insmod mydev.ko <span class="hljs-built_in">ls</span> -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:
|
1 2 |
<span class="hljs-built_in">echo</span> <span class="hljs-string">"hello kernel"</span> > /dev/mydev <span class="hljs-built_in">cat</span> /dev/mydev |
◆ Ґуля № 2 — чому cat не зависає (два виклики read)
Найцікавіша частина — дивимось у dmesg що насправді відбувається:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
sudo bash <span class="hljs-operator">-</span>c <span class="hljs-string">'echo "hello kernel" > /dev/mydev'</span> sudo cat <span class="hljs-operator">/</span>dev<span class="hljs-operator">/</span>mydev dmesg <span class="hljs-operator">|</span> tail <span class="hljs-number">-15</span> [ <span class="hljs-number">802.056291</span>] mydev: <span class="hljs-keyword">open</span>() <span class="hljs-keyword">called</span>, pid<span class="hljs-operator">=</span><span class="hljs-number">1483</span> [ <span class="hljs-number">802.056393</span>] mydev: write() <span class="hljs-keyword">called</span>, count<span class="hljs-operator">=</span><span class="hljs-number">13</span> [ <span class="hljs-number">802.056403</span>] mydev: write() → saved <span class="hljs-number">13</span> bytes: "hello kernel " [ <span class="hljs-number">802.056421</span>] mydev: <span class="hljs-keyword">release</span>() <span class="hljs-keyword">called</span>, pid<span class="hljs-operator">=</span><span class="hljs-number">1483</span> [ <span class="hljs-number">802.107888</span>] mydev: <span class="hljs-keyword">open</span>() <span class="hljs-keyword">called</span>, pid<span class="hljs-operator">=</span><span class="hljs-number">1487</span> [ <span class="hljs-number">802.107970</span>] mydev: read() <span class="hljs-keyword">called</span>, count<span class="hljs-operator">=</span><span class="hljs-number">262144</span>, ppos<span class="hljs-operator">=</span><span class="hljs-number">0</span> [ <span class="hljs-number">802.107989</span>] mydev: read() → <span class="hljs-number">13</span> bytes [ <span class="hljs-number">802.108034</span>] mydev: read() <span class="hljs-keyword">called</span>, count<span class="hljs-operator">=</span><span class="hljs-number">262144</span>, ppos<span class="hljs-operator">=</span><span class="hljs-number">13</span> [ <span class="hljs-number">802.108043</span>] mydev: read() → EOF [ <span class="hljs-number">802.108072</span>] mydev: <span class="hljs-keyword">release</span>() <span class="hljs-keyword">called</span>, pid<span class="hljs-operator">=</span><span class="hljs-number">1487</span> |
Розбираємо по рядках:
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 (вона може бути невалідна або взагалі належати чужому процесу).
|
1 |
<span class="hljs-attr">not_copied</span> = copy_to_user(ubuf, kbuf + *ppos, to_copy)<span class="hljs-comment">;</span> |
⚠ Зверни увагу:
copy_to_user/copy_from_userповертають кількість байтів, яку НЕ вдалось скопіювати (0 = повний успіх). Це протилежноmemcpy, який повертає вказівник на призначення. Переплутати легко, і код мовчки «працюватиме» неправильно, поки не натрапиш на реальну помилку копіювання.
◆ Tainted kernel і контекст живлення
|
1 2 3 4 5 |
<span class="hljs-built_in">cat</span> /proc/sys/kernel/tainted vcgencmd get_throttled 5120 throttled=0x50000 |
5120 — ядро позначене tainted одразу кількома прапорцями, серед них той самий, що бачили в dmesg при insmod:
|
1 |
[ <span class="hljs-number">597.659757</span>] mydev: loading <span class="hljs-keyword">out</span><span class="hljs-operator">-</span><span class="hljs-keyword">of</span><span class="hljs-operator">-</span>tree <span class="hljs-keyword">module</span> taints kernel. |
Будь-який модуль, зібраний поза офіційним деревом ядра, одразу «забруднює» ядро цим прапором — це не помилка, просто маркер для діагностики (якщо щось впаде, maintainer’и ядра одразу бачать, що в системі був сторонній код).
throttled=0x50000 — у логах під час сесії кілька разів проскакувало:
|
1 2 |
[ <span class="hljs-number">367.294979</span>] hwmon hwmon1:<span class="hljs-type"> Undervoltage</span> detected! [ <span class="hljs-number">373.342982</span>] hwmon hwmon1:<span class="hljs-type"> Voltage</span> 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в іншому.
◆ Керування модулями ядра
|
1 2 3 4 5 6 7 8 9 |
sudo insmod hello.ko <span class="hljs-comment"># завантажити модуль</span> sudo insmod hello.ko val=5 name=stm32 <span class="hljs-comment"># з параметрами (module_param)</span> sudo rmmod hello <span class="hljs-comment"># вивантажити (без .ko!)</span> sudo modprobe hello <span class="hljs-comment"># завантажити з /lib/modules + залежності</span> sudo modprobe -r hello <span class="hljs-comment"># вивантажити з залежностями</span> lsmod <span class="hljs-comment"># список завантажених модулів</span> lsmod | grep hello modinfo hello.ko <span class="hljs-comment"># автор, ліцензія, параметри, залежності</span> sudo depmod -a <span class="hljs-comment"># перебудувати таблицю залежностей</span> |
| Команда | Що робить | Коли треба |
|---|---|---|
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
|
1 2 3 4 5 6 7 8 9 |
dmesg <span class="hljs-comment"># весь кільцевий буфер ядра</span> dmesg <span class="hljs-operator">-</span>w <span class="hljs-comment"># «хвіст» у реальному часі (як tail -f)</span> dmesg <span class="hljs-operator">-</span><span class="hljs-built_in">T</span> <span class="hljs-comment"># людські timestamp замість секунд від boot</span> dmesg <span class="hljs-operator">-</span>C <span class="hljs-comment"># очистити буфер (зручно перед insmod)</span> dmesg <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> <span class="hljs-comment"># вивести І очистити</span> dmesg <span class="hljs-operator">-</span><span class="hljs-operator">-</span>level<span class="hljs-operator">=</span>err<span class="hljs-punctuation">,</span>warn <span class="hljs-comment"># тільки помилки й попередження</span> dmesg <span class="hljs-operator">|</span> grep hello <span class="hljs-comment"># фільтр по нашому модулю</span> journalctl <span class="hljs-operator">-</span>k <span class="hljs-comment"># те саме через systemd journal</span> journalctl <span class="hljs-operator">-</span>k <span class="hljs-operator">-</span>f <span class="hljs-comment"># follow</span> |
Рівні printk у коді модуля:
|
1 2 3 4 5 6 7 8 |
pr_emerg(<span class="hljs-string">"система падає<span class="hljs-subst">\n</span>"</span>); <span class="hljs-comment">// KERN_EMERG = 0</span> pr_err(<span class="hljs-string">"щось зламалось<span class="hljs-subst">\n</span>"</span>); <span class="hljs-comment">// KERN_ERR = 3</span> pr_warn(<span class="hljs-string">"обережно<span class="hljs-subst">\n</span>"</span>); <span class="hljs-comment">// KERN_WARNING= 4</span> pr_info(<span class="hljs-string">"все ок<span class="hljs-subst">\n</span>"</span>); <span class="hljs-comment">// KERN_INFO = 6</span> pr_debug(<span class="hljs-string">"деталі<span class="hljs-subst">\n</span>"</span>); <span class="hljs-comment">// KERN_DEBUG = 7 (треба DEBUG)</span> cat <span class="hljs-operator">/</span>proc<span class="hljs-operator">/</span>sys<span class="hljs-operator">/</span>kernel<span class="hljs-operator">/</span>printk # поточні рівні консолі echo <span class="hljs-number">8</span> <span class="hljs-operator">></span> <span class="hljs-operator">/</span>proc<span class="hljs-operator">/</span>sys<span class="hljs-operator">/</span>kernel<span class="hljs-operator">/</span>printk # показувати все аж до debug |
⚠
pr_debugмовчить, поки модуль не зібраний з-DDEBUGабо не ввімкнений через dynamic debug (/sys/kernel/debug/dynamic_debug/control).
◆ Збірка kernel-модуля
Мінімальний Makefile для out-of-tree модуля:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
obj-m += hello.o KDIR := /lib/modules/<span class="hljs-variable">$(<span class="hljs-built_in">shell</span> uname -r)</span>/build <span class="hljs-section">all:</span> make -C <span class="hljs-variable">$(KDIR)</span> M=<span class="hljs-variable">$(PWD)</span> modules <span class="hljs-section">clean:</span> make -C <span class="hljs-variable">$(KDIR)</span> M=<span class="hljs-variable">$(PWD)</span> clean make <span class="hljs-comment"># збірка -> hello.ko</span> uname -r <span class="hljs-comment"># версія ядра (має збігатись з headers!)</span> ls /lib/modules/<span class="hljs-variable">$(uname -r)</span>/build <span class="hljs-comment"># тут лежать kernel headers</span> sudo apt install raspberrypi-kernel-headers <span class="hljs-comment"># якщо build/ немає</span> |
⚠ Модуль зібраний під одну версію ядра не вставиться в іншу —
insmodдастьversion magic ... should be .... Перевірuname -rДО збірки. На нашому 3B+ це6.18.33+rpt-rpi-v8.
◆ sysfs / procfs — підглядаємо в ядро
|
1 2 3 4 5 6 7 8 9 |
<span class="hljs-built_in">ls</span> /sys/module/hello/parameters/ <span class="hljs-comment"># параметри module_param</span> <span class="hljs-built_in">cat</span> /sys/module/hello/parameters/val <span class="hljs-built_in">echo</span> 7 > /sys/module/hello/parameters/val <span class="hljs-comment"># змінити на льоту (якщо 0644)</span> <span class="hljs-built_in">cat</span> /proc/modules <span class="hljs-comment"># те, що читає lsmod</span> <span class="hljs-built_in">cat</span> /proc/devices <span class="hljs-comment"># зайняті major-номери (char і block)</span> <span class="hljs-built_in">cat</span> /proc/interrupts <span class="hljs-comment"># лічильники переривань по лініях</span> <span class="hljs-built_in">cat</span> /proc/iomem <span class="hljs-comment"># мапа фізичної пам'яті</span> <span class="hljs-built_in">cat</span> /proc/sys/kernel/tainted <span class="hljs-comment"># чи «забруднене» ядро (0 = чисте)</span> |
⚠
module_paramз правами0644можна міняти через sysfs після завантаження — але це НЕ викликаєprobe()повторно. Класична «ґуля»: думаєш, що змінив параметр і драйвер перечитав, а він прочитав значення один раз під часinsmod.
◆ Character device — /dev/mydev
|
1 2 3 4 5 6 |
<span class="hljs-built_in">cat</span> /proc/devices | grep mydev <span class="hljs-comment"># дізнатись виданий major</span> sudo <span class="hljs-built_in">mknod</span> /dev/mydev c 240 0 <span class="hljs-comment"># створити вручну: c = char, major, minor</span> <span class="hljs-built_in">ls</span> -l /dev/mydev <span class="hljs-comment"># перша літера 'c', далі major, minor</span> <span class="hljs-built_in">cat</span> /dev/mydev <span class="hljs-comment"># викликає .read у file_operations</span> <span class="hljs-built_in">echo</span> <span class="hljs-string">"hello"</span> > /dev/mydev <span class="hljs-comment"># викликає .write</span> sudo <span class="hljs-built_in">rm</span> /dev/mydev <span class="hljs-comment"># прибрати</span> |
⚠ Якщо в драйвері використовуєш
class_create+device_create, вузол/dev/mydevстворює udev автоматично —mknodруками тоді не потрібен.mknodлишається для випадку «голого»register_chrdevбез класу.
Права на автостворений вузол — device_create за замовчуванням дає 0600 (тільки root). Без правила echo/cat від звичайного користувача впадуть з Permission denied:
|
1 2 3 4 |
<span class="hljs-built_in">echo</span> <span class="hljs-string">'KERNEL=="mydev", MODE="0666"'</span> | sudo <span class="hljs-built_in">tee</span> /etc/udev/rules.d/99-mydev.rules sudo udevadm control --reload-rules sudo rmmod mydev && sudo insmod mydev.ko <span class="hljs-comment"># перестворити вузол з новим правилом</span> <span class="hljs-built_in">ls</span> -l /dev/mydev <span class="hljs-comment"># перевірити права</span> |
⚠ Вміст udev-правила (
KERNEL=="mydev", MODE="0666") пиши черезteeу файл, а НЕ вставляй у термінал як команду — bash спробує її виконати і впаде зcommand not found.
◆ GPIO з командного рядка — libgpiod 2.x
|
1 2 3 4 5 6 7 |
gpiodetect <span class="hljs-comment"># список gpiochip у системі</span> gpioinfo gpiochip0 <span class="hljs-comment"># усі лінії чіпа: напрям, споживач, прапори</span> gpioget <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> gpiochip0 <span class="hljs-number">17</span> <span class="hljs-comment"># прочитати стан лінії 17</span> gpioset <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> gpiochip0 <span class="hljs-number">17</span><span class="hljs-operator">=</span><span class="hljs-number">1</span> <span class="hljs-comment"># виставити 1</span> gpioset <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> gpiochip0 <span class="hljs-operator">-</span>t <span class="hljs-number">500</span>ms <span class="hljs-number">17</span><span class="hljs-operator">=</span><span class="hljs-number">1</span> <span class="hljs-comment"># блимати (--toggle)</span> gpiomon <span class="hljs-operator">-</span><span class="hljs-built_in">c</span> gpiochip0 <span class="hljs-number">17</span> <span class="hljs-comment"># ловити edge-події в реальному часі</span> gpiofind GPIO17 <span class="hljs-comment"># знайти лінію за іменем</span> |
⚠ libgpiod 2.x ламає сумісність з 1.x. У 1.x чіп передавався позиційно:
gpioget gpiochip0 17. У 2.x — через-c/--chip, а флаг-t/--toggleз’явився саме у 2.x. На свіжому Pi OS у тебе майже напевно 2.x — стара шпаргалка з інтернету не спрацює.
Raspberry-специфічне (Bookworm і новіше):
|
1 2 3 |
pinctrl <span class="hljs-keyword">get</span> <span class="hljs-number">17</span> <span class="hljs-meta"># стан і функція піна (замінив raspi-gpio)</span> pinctrl <span class="hljs-keyword">set</span> <span class="hljs-number">17</span> op dh <span class="hljs-meta"># output, drive high</span> pinctrl <span class="hljs-keyword">set</span> <span class="hljs-number">17</span> ip pu <span class="hljs-meta"># input, pull-up</span> |
Старий sysfs-інтерфейс
/sys/class/gpio/export— deprecated ще з ядра 4.8 і прибраний у нових. Цікавий, тільки як історичний контраст (це окрема тема — userspace GPIO проти kernel module).
◆ Device Tree та overlay
|
1 2 3 4 5 6 7 8 |
<span class="hljs-built_in">ls</span> /proc/device-tree/ <span class="hljs-comment"># активне DT у вигляді файлової системи</span> dtc -@ -I dts -O dtb -o my.dtbo my-overlay.dts <span class="hljs-comment"># компілюємо overlay</span> sudo <span class="hljs-built_in">cp</span> my.dtbo /boot/firmware/overlays/ dtoverlay -l <span class="hljs-comment"># завантажені overlay</span> dtoverlay -a <span class="hljs-comment"># доступні overlay</span> sudo dtoverlay my <span class="hljs-comment"># завантажити на льоту (без reboot)</span> sudo dtoverlay -r my <span class="hljs-comment"># вивантажити</span> fdtdump my.dtbo <span class="hljs-comment"># дамп бінарного DT у читабельне</span> |

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

Читаємо 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.



◆ Дрібниці, які економлять час
|
1 2 3 4 5 |
watch -n1 <span class="hljs-string">'dmesg | tail -5'</span> <span class="hljs-comment"># живий моніторинг останніх логів</span> sudo dmesg -C && sudo insmod x.ko && dmesg <span class="hljs-comment"># чистий лог одного тесту</span> <span class="hljs-built_in">echo</span> 1 | sudo <span class="hljs-built_in">tee</span> /sys/... <span class="hljs-comment"># запис у sysfs з-під sudo (тільки tee, не >)</span> ssh pi+ <span class="hljs-comment"># 3B+ (192.168.1.123) — наш робочий</span> ssh pi <span class="hljs-comment"># 3B (192.168.1.109)</span> |