Нехай Luckfox думає, а STM32 діє: face detection від камери до реле. Частина 10
Серія «STM32 з нуля без HAL» · Частина 10 · Місяць 3
У передній статті ми змусили NPU видавати координати обличчя в консоль. Гарно — але рядки в терміналі це не система. Система — коли хтось підходить до камери і фізично клацає реле. Саме це ми зробимо сьогодні.
Фінальний результат: обличчя в кадрі → Luckfox детектує через NPU → фото летить на сервер → DeepFace звіряє з базою → SSH команда на Luckfox → STM32 отримує on по UART → реле спрацьовує.
Розберемо усі етапи окремо.
◆ Архітектура — три вузли
Камера (MIPI) ↓ Luckfox Pico Pro (192.168.1.125) NPU: RetinaFace детектує обличчя curl: відправляє ROI фото на сервер UART1 (/dev/ttyS1): отримує команди від сервера через STM32 ↓ ethernet PC сервер (192.168.1.121) Flask: приймає фото DeepFace: звіряє з базою known_faces/ SSH: якщо OK → команда на Luckfox → stm32_controller.py ↓ SSH → UART STM32 Blue Pill Отримує on/off по UART Керує реле через SN74HC595N
Чому саме так розподілені ролі:
Luckfox детектує — NPU апаратно, швидко, в реальному часі і CPU на це не витрачається.
Сервер розпізнає — база облич може рости, моделі змінюватись, поріг підлаштовуватись. Все це без перезбірки прошивки.
STM32 діє — надійний, детермінований. Отримав команду — виконав. Без Linux overhead.
До речі, поки налаштовував ethernet між Luckfox і сервером — задумався: а як в реальних автономних системах з’єднують vision модулі з мозком? CAN? Automotive Ethernet? Luckfox не має WiFi — тільки ethernet і UART. Для стаціонарної системи це нормально, для мобільного робота — обмеження. Якщо є досвід з robotics або automotive — цікаво почути в коментарях.
◆ Модифікація main.cc — мінімально і чисто
Оригінальний main.cc лише виводить в консоль координати. Тому нам треба додати два блоки: зберегти ROI обличчя як JPEG і відправити на сервер через curl.
Зупинимось на ключових рішеннях.
IP сервера краще задати в аргументи, а не в коді:
// Не так: #define SERVER_URL "http://192.168.1.121:5000/face" // перезбірка при зміні IP // А так — п'ятий аргумент командного рядка: const char *server_url = argv[4];
Міняєш IP — просто інший аргумент при запуску. Без make.
Throttling — не більше одного запиту на 5 секунд:
static time_t last_send = 0;
time_t now = time(NULL);
if (now - last_send >= 5) {
cv::imwrite("/tmp/face.jpg", face_img);
// curl POST...
last_send = now;
}
Без throttling — при кожному кадрі де є обличчя буде летіти запит. Це
Відправка — асинхронно:
char cmd[512]; snprintf(cmd, sizeof(cmd), "curl -s -X POST %s " "-F \"image=@/tmp/face.jpg\" " "-F \"norm=%.3f\" " "-o /tmp/face_result.txt &", // & = у фон server_url, norm); system(cmd);
& в кінці — curl іде у фоновий процес. Детекція не зупиняється поки curl чекає відповідь.
⚠
system(curl ...)— не найелегантніше рішення з точки зору C++. Правильніше використовуватиlibcurlAPI напряму. Та тут думаю це більш читабельно — кожен розуміє що відбувається без знання libcurl.
Збірка — підводний камінь з file(GLOB):
CMakeLists використовує file(GLOB SRC_FILES "${SRC_DIR}/*.cc") — бере всі .cc файли в директорії. Якщо перейменував старий main.cc наприклад на mainOld.cc — CMake підхопить обидва файли і отримаєш конфлікт символів:
make[2]: *** Error 1 ← конфлікт main() з двох файлів
Рішення: видаляти старий файл, не перейменовуватиі взагалі так вже давно нічто не робить, окрім мене)).
◆ Сервер — три варіанти і що з них вийшло
Для розпізнавання облич по базі є кілька Python бібліотек. Спробував усі три — ось чесний результат.
Варіант 1 — face_recognition + dlib
Найпопулярніший на GitHub, найбільше туторіалів.
pip install face_recognition ERROR: Failed building wheel for dlib subprocess.CalledProcessError: Command '['cmake', '--build', ...]' returned non-zero exit status 2
⚠
dlib— це C++ бібліотека яка компілюється при встановленні. Потребує:sudo apt install cmake libopenblas-dev liblapack-dev libx11-dev pip install dlib pip install face_recognitionЯкщо і після цього не збирається — залежить від версії gcc і cmake. На Ubuntu 22.04 зазвичай проходить після встановлення залежностей. В мене з першого разу не пішла, тому пішов далі.
Варіант 2 — DeepFace + TensorFlow
pip install deepface
Встановлюється без компіляції. Під капотом може використовувати VGG-Face, FaceNet, ArcFace — вибираєш модель. Хто забув, ми використовуємо FaceNet + cosine similarity.
ERROR: ModuleNotFoundError: No module named 'tf_keras'
⚠ TensorFlow 2.21 відокремив Keras в окремий пакет.
deepfaceще не оновив залежності автоматично:pip install tf-keras
Після цього запускається. Перший виклик DeepFace.verify() автоматично скачує модель (~92MB FaceNet weights) в ~/.deepface/weights/. Це один раз.
⚠ Перший POST запит може прийти поки модель ще качається. Flask обробляє його в окремому потоці — система не падає, але перший результат може бути повільним.
Варіант 3 — cosine similarity на numpy
Найлегший підхід — без важких бібліотек взагалі. До того ж, FaceNet вже рахує
import numpy as np def cosine_similarity(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
Потребує модифікації main.cc щоб відправляв вектор out_fp32[128] разом з фото. Я пішов далі, але якщо хочеш найлегше рішення без TensorFlow, тоді бери це.
Я обрав Варіант 2 — DeepFace встановлюється, працює одразу, база облич — просто папка з фото, ну і оскільки я трохи грався з tensorflow.js і продовжую це робити з tensorflow і TensorFlow Lite, то для мене вибір очевидний)).
◆ Enrollment — як додати себе в базу
Найелегантніший момент системи.
Щоб сервер тебе впізнав — потрібне еталонне фото. Де його взяти? Я просто підійшов до камери. Система сфоткала моє обличчя і зберегла в received/. Я взяв це фото і поклав в базу:
cp received/face_20260521_103524_UNKNOWN.jpg known_faces/Alex.jpg
Перезавантажую базу без рестарту сервера:
curl -X POST http://localhost:5000/reload
Наступний раз — OK:Alex. Enrollment через саму систему.
Для кількох людей — та сама схема. Кожен підходить до камери, фото падає в received/, перекладаєш в known_faces/Ім'я.jpg, робиш /reload.
◆ SSH ключі на Buildroot — чому не спрацювало
Щоб сервер міг по SSH заходити на Luckfox без пароля — треба SSH ключ. Генеруємо і копіюємо:
ssh-keygen -t rsa -f ~/.ssh/luckfox -N "" ssh-copy-id -o PubkeyAuthentication=no -i ~/.ssh/luckfox.pub [email protected]
Перевіряємо:
ssh -i ~/.ssh/luckfox -o IdentitiesOnly=yes [email protected] "echo test" [email protected]'s password: ← питає пароль — ключ не спрацював
Дивимось на платі:
ls -la /root/.ssh/ ls -la / | grep root drwx------ 2 root root .ssh/ ← права правильні: 700 -rw------- 1 root root authorized_keys ← права правильні: 600 drwx------ 4 1000 1000 /root ← а ось це проблема!
/root директорія належить uid 1000 а не root (uid 0). Buildroot створює її під іншим користувачем. SSH демон перевіряє власника home директорії — і якщо вона не належить користувачу який логіниться, відхиляє ключ з міркувань безпеки. Навіть якщо authorized_keys має правильні права.
Виправити можна через chown root:root /root — але на Buildroot це не персистентно між перезавантаженнями якщо /root монтується з tmpfs.
Практичне рішення — sshpass:
sudo apt install sshpass sshpass -pluckfox ssh \ -o StrictHostKeyChecking=no \ -o PubkeyAuthentication=no \ [email protected] "echo test"
Так, передавати пароль через аргумент — не найкраща практика безпеки. Та для домашньої лабораторії і статті — прийнятно.
◆ Реле спрацьовує
Сервер отримує фото → DeepFace порівнює з базою:
Alex: dist=0.073 verified=True [20260521_113501] norm(rknn)=1.442 | dist(deepface)=0.073 | OK:Alex [реле] відкриваємо для Alex на 5с [STM32] on → ok
dist=0.073 при порозі 0.40 — DeepFace дуже впевнений. Два числа в логу:
normвід RKNN на Luckfox — детекція на NPUdistвід DeepFace на PC — розпізнавання по базі
Захист від паралельних викликів:
Luckfox відправляє фото раз на 5 секунд. Але Flask обробляє запити в кількох потоках — без захисту реле спрацьовує кілька разів підряд:
relay_lock = threading.Lock()
relay_active = False
def relay_open(name):
global relay_active
with relay_lock:
if relay_active:
print(" [реле] вже активне — пропускаємо")
return
relay_active = True
# ... on → sleep → off → relay_active = False
Тепер в логах:
[реле] відкриваємо для Alex на 5с ← перше спрацювання [реле] вже активне — пропускаємо ← паралельні запити ігноруються [реле] вже активне — пропускаємо [STM32] on → ok [STM32] off → ok [реле] закрито ← готове до наступного
◆ Клонування плати
Питання яке виникає коли система працює: як перенести на іншу плату?
update.img в ~/luckfox-pico/output/image/ — це Buildroot образ. Він містить все що зібрано через make: ядро, rootfs, наші пакети (go-dashboard, статичний IP). Прошивається одною командою:
./tools/linux/Linux_Upgrade_Tool/upgrade_tool uf output/image/update.img
⚠ Але файли які клали через
scp— бінарник детекції, моделі,stm32_controller.py— в образі не збережені. Вони живуть в/root/який не входить в rootfs образу.
Після прошивки нової плати треба скопіювати:
scp -o PubkeyAuthentication=no -r \ luckfox_pico_rknn_example/install/uclibc/luckfox_pico_retinaface_facenet_demo \ root@NEW_IP:/root/ scp -o PubkeyAuthentication=no \ stm32_controller.py root@NEW_IP:/root/
Щоб файли були в образі автоматично — треба Buildroot overlay або пакет.
◆ Гайд-шпаргалка
Одноразово — збірка бінарника на PC:
export LUCKFOX_SDK_PATH=/home/alex/luckfox-pico cd luckfox_pico_rknn_example ./build.sh # вибираємо: 1 (uclibc) → 1 (retinaface_facenet) scp -o PubkeyAuthentication=no -r \ install/uclibc/luckfox_pico_retinaface_facenet_demo \ [email protected]:/root/ scp -o PubkeyAuthentication=no \ stm32_controller.py [email protected]:/root/
Кожного разу — запуск:
# Термінал 1: сервер на PC cd ~/Sasha/www/STM32/python python3 face_server_deepface.py # Термінал 2: детекція на платі ssh -o PubkeyAuthentication=no [email protected] RkLunch-stop.sh cd /root/luckfox_pico_retinaface_facenet_demo ./luckfox_pico_retinaface_facenet \ model/RetinaFace.rknn \ model/mobilefacenet.rknn \ model/test.jpg \ http://192.168.1.121:5000/face
Додати людину в базу:
# Підійди до камери → фото в received/ cp received/face_TIMESTAMP_UNKNOWN.jpg known_faces/Maria.jpg curl -X POST http://localhost:5000/reload
Перевірити стан:
curl http://192.168.1.121:5000/status
Перевірити STM32 окремо:
# На платі STM32_PORT=/dev/ttyS1 python3 /root/stm32_controller.py on STM32_PORT=/dev/ttyS1 python3 /root/stm32_controller.py off
◆ Що далі
Десять статей позаду — Luckfox вже мабуть набрид, стало нудно і це момент коли треба зупинитись і подивитись з висоти. Так, а що вже є?
Місяць 1: писали STM32 регістрами без HAL, побачили що під капотом Arduino-стилю.
Місяць 2: побудували перший міст від bare-metal до Linux і там STM32 вже шле дані через UART у Buildroot-зібраний Luckfox.
Місяць 3, який закриває ця стаття: Luckfox став мозком — камера, NPU, розпізнавання облич, а STM32 виконавчий орган з реле.
Що ж я хотів зробити? Якщо взяти всі десять статей разом, то це повинен бути не випадковий набір туторіалів, а одна цікава історія: як просто зробити так, щоб маленька залізячка бачила, думала і діяла.
Але залишилось невирішеним питання яке досі обходив стороною: а що там всередині самого Linux? Ми писали userspace код, який звертався до /dev/ttyS1, /dev/video0, до драйверів які хтось колись написав. Я туди не лазив. Але думаю вже час залізти і туди.
Місяць 4 — Linux Kernel Modules на Raspberry Pi
З наступної статті будемо разом знайомитись з kernel space. Кажуть це окремий світ зі своїми правилами: немає printf, є printk; немає malloc, є kmalloc; помилився з вказівником — не segfault, а kernel panic і reboot. Кажуть, embedded middle від junior відрізняється саме цим — здатністю писати свій драйвер, а не тільки використовувати чужі.
Приблизний план на місяць, але це не точно:
- Стаття 11 (наступна): Hello world kernel module. Перший
.koфайл,insmod,dmesg, перші ґулі від нового ядра 6.12 яке вже не має старого пакетаraspberrypi-kernel-headers - Стаття 12: GPIO LED driver з ядра. Перша різниця між sysfs способом (як ми звикли з userspace) і власним driver-ом
- Стаття 13: character device driver —
/dev/mydevз повнимfile_operations, як це зроблено в усіх «справжніх» драйверах - Стаття 14: HC-SR04 ultrasonic sensor driver —
cat /dev/hcsr04→127cm. Interrupt handling, тут вже не іграшки
Платформа — Raspberry Pi 3 Model B. Pi 3B+ резервую для Місяця 5 (FreeRTOS + OpenCV), решта йде в експерименти, візьму поки що звідси.
Паралельний маршрут — Zephyr на Pi Pico
Поки ця серія йде по основному на рейках «STM32 + Linux», та в мене є ще — «Збираємо комп’ютер як Neotron, але по-своєму». Там Zephyr RTOS на Pi Pico, це трохи інший спосіб думати про embedded: там не Linux ядро з його MMU і userspace, а mainline RTOS з Device Tree, kbuild і дисципліною mainline-проєктів. Хочу щоб ці дві серії доповнювали одна одну — тут ми дивимось як ядро живе, там як жити без ядра.
Відкриті питання
- Чи варто переносити написані kernel modules з Pi на Luckfox? Інша архітектура (Cortex-A7 vs A53), Buildroot замість Debian, uclibc замість glibc. Підозрюю це буде боляче і точно буде окрема ґуля
- STM32 ↔ Pi через SPI як kernel driver —
/dev/stm32sensorз боку ядра, мій HAL з боку залізки. Якщо встигну в Місяць 4 — буде окрема стаття.
Вже не встигну бо треную Катю — wake-word модель на 50KB з AUC 0.98, повністю своя CNN. Українське слово в терміналі реагує в real-time:
). Як робив напишу в себе в блозі, можливо також і на DOU
І питання яке хочеться поставити вам: коли в роботі реально дійшло до написання свого kernel module, а не використання чужого? Це вже той досвід що відрізняє читачів які пишуть в коментарях «та я це вже років п’ять», від тих хто чесно каже «ніколи не доводилось». Пишіть, цікаво подивитись пропорцію.
До зустрічі за тиждень — Hello, kernel!
Репозиторій: github.com/pipicosim800-maker/stm32F103
◆ P.S. SD карта, swap і додаткове сховище
Поки налаштовував систему — знайшов вільний SanDisk Ultra 32GB. Вставив в Luckfox і прийшла ідея: система працює, фото летять в received/ на PC — але що якщо перенести сховище прямо на плату?
Але спочатку тре вирішити важливіше питання — пам’ять.
Скільки RAM на RV1106:
free -h Mem: 53.3M 23.8M 10.3M Swap: 0 0 0
53MB RAM і нуль swap. RetinaFace все одно запускається — він відносно легкий. Але якщо захочу YOLOv8 або важчі моделі:
⚠
Can't allocate memory— класична помилка RKNN при нестачі RAM. Автор статті на Habr про YOLOv8 на Luckfox Pico Mini зіткнувся з тим самим. Рішення — swap.
Перевіряємо SD карту:
ls /dev/mmcblk* df -h | grep mmcblk /dev/mmcblk1p1 14.1G 1.6G 12.3G 12% /mnt/sdcard
Карта змонтована як /mnt/sdcard. Замість перерозбивки — простіший шлях через swap файл:
dd if=/dev/zero of=/mnt/sdcard/swapfile bs=1M count=512 mkswap /mnt/sdcard/swapfile chmod 600 /mnt/sdcard/swapfile swapon /mnt/sdcard/swapfile free -h Mem: 53.3M 24.3M 2.1M Swap: 512.0M 0 512.0M
512MB swap — YOLOv8 тепер запуститься.
Автозапуск після перезавантаження:
cat > /etc/init.d/S90autoswap << 'EOF' #!/bin/sh case $1 in start) swapon /mnt/sdcard/swapfile ;; stop) swapoff /mnt/sdcard/swapfile ;; *) exit 1 ;; esac EOF chmod +x /etc/init.d/S90autoswap
Що це дає, наприклад для Автобота:
SD карта на мобільній платформі це зручно:
received/з фото облич — прямо на карті, не на PC- логи детекції — зберігаємо локально
- embeddings бази — переносити карту замість SSH
# Переносимо received/ на SD mkdir -p /mnt/sdcard/received # В face_server_deepface.py: # SAVE_DIR = "/mnt/sdcard/received" ← якщо сервер на платі
Виймаєш карту і переносиш дані на PC фізично. Без мережі, без SSH. Гадаю для польових умов використання мого Автобота це саме те.

6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівГарна серія, респект
Часто запитують «з чого почати вхід в ембедед», буду тепер всім давати посилання сюди )
Ого, клас. Дякую!!!
а чому не на stm32 напряму, є ж ті які з NPU huggingface.co/STMicroelectronics
Дякую, не знав що таке є! STM32N6 з Neural-ART — це справді окремий клас, 600 GOPS прямо на MCU. NPU на Luckfox відкрив для себе випадково, а тут цілий чіп заточений під це. Але $100+ проти $14 за Luckfox — для хобі-проектів різниця відчутна. Цікавий напрямок для окремого експерименту, можливо в майбутньому. Дякую що звернули увагу!
хоч це і не у топік, але на векторних інструкціях esp32 вже давно можна yolo та face recognition запускати, є навіть готовий набір з камерою і екранчиком щоб побудувати смарт замок github.com/...sp32_cam_face_recognition
Чому не у топік? Саме сюди, хоч це esp32, але ж корисна інформація, яка буде в одному місці. Дякую