Skip to main content

Утилита modbus-slave. Эмулятор Modbus RTU датчиков

· 4 min read
dmn
maintainer

Когда разрабатываешь систему мониторинга или SCADA, часто нужно протестировать опрос датчиков — но реального оборудования под рукой нет. Или нужно показать демо заказчику без физических устройств. Или хочется отладить логику мастера не выезжая на объект.

Именно для этого мы написали modbus_slave — эмулятор Modbus RTU slave устройств на C, который работает на Linux и Windows, не требует зависимостей и умеет отдавать реальные данные из файлов.

Что умеет

  • Эмулирует до 30 независимых Modbus RTU устройств на одном последовательном порту
  • Каждое устройство отвечает на FC03 (Read Holding Registers), 20 регистров
  • Значения регистров — случайные (для тестирования) или из файла (реальные данные)
  • Работает на Linux x86_64, aarch64 (NAPI2, RK3568, Raspberry Pi) и Windows x64
  • Поддерживает RS-485 — RTS direction control через DTS/GPIO
  • Режим демона с логами в syslog и systemd service для автозапуска
  • Защита от устаревших данных — если скрипт-источник упал, регистры возвращают нули
  • Один статический бинарь без зависимостей — скопировал и запустил

Архитектура

Идея простая: один процесс слушает RS-485 шину и отвечает на запросы от любого из 30 адресов. Для мастера это выглядит как несколько физических устройств на линии — он не видит разницы.

Мастер (PC/ПЛК)          RS-485 шина          modbus_slave (NAPI2)
mbpoll -a 1 ──────────────────────────► slave ID 1 → /tmp/cpu.dat
mbpoll -a 2 ──────────────────────────► slave ID 2 → /tmp/time.dat
mbpoll -a 3 ──────────────────────────► slave ID 3 → random

Регистры читаются из обычных текстовых файлов — одно число на строку. Файлы живут в /tmp (tmpfs — RAM диск), SD карта не изнашивается.

Быстрый старт

Установка на NAPI2 / aarch64

# Скачать статический бинарь
wget https://github.com/lab240/modpoll-slave/raw/main/bin/modbus_slave_aarch64
chmod +x modbus_slave_aarch64

# Запустить — 3 датчика, порт ttyS7
./modbus_slave_aarch64 -p /dev/ttyS7 -b 115200 -a 3

Запуск на Windows

modbus_slave.exe -p COM4 -b 115200 -a 3

Проверка с mbpoll

mbpoll -m rtu -b 115200 -P none -a 1 -r 1 -c 20 /dev/ttyUSB0

Реальные данные из файлов

Самое интересное — каждому slave можно привязать файл с реальными значениями. Формат простой: одно целое число на строку.

# Запуск: slave 1 читает данные CPU, slave 2 — время, slave 3 — рандом
./modbus_slave -p /dev/ttyS7 -b 115200 -a 3 \
-f 1:/tmp/cpu.dat \
-f 2:/tmp/time.dat

Файл обновляется внешним скриптом атомарно через mv — никакой гонки данных:

# Температура ядер CPU (°C × 100)
while true; do
for zone in /sys/class/thermal/thermal_zone*/temp; do
val=$(( $(cat $zone) / 10 ))
echo $val
done > /tmp/cpu.tmp
mv /tmp/cpu.tmp /tmp/cpu.dat
sleep 5
done

Значение 4523 в регистре означает 45.23 °C — стандартное соглашение для передачи дробных чисел в Modbus.

Защита от падения скриптов

Если скрипт обновления данных упал — файл перестаёт обновляться, но данные в нём остаются старые. Мастер продолжал бы читать устаревшие значения.

Параметр -t задаёт максимальный возраст файла:

./modbus_slave -p /dev/ttyS7 -b 115200 -f 1:/tmp/cpu.dat -t 10

Если файл не обновлялся более 10 секунд — все регистры возвращают 0. Это сразу видно мастеру и SCADA системе. В лог пишется предупреждение (не чаще раза в минуту чтобы не спамить):

file /tmp/cpu.dat is stale (45s > 10s), returning zeros

По умолчанию t=10. Отключить: -t 0.

Режим демона и systemd

# Запуск как демон
./modbus_slave -d -p /dev/ttyS7 -b 115200 -a 3 -f 1:/tmp/cpu.dat

# Статус
./modbus_slave -s

# Остановка
./modbus_slave -k

# Логи
journalctl -t modbus_slave -f

Для автозапуска при загрузке — systemd service:

sudo cp modbus_slave /usr/local/bin/
sudo cp service/modbus_slave.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now modbus_slave

Статистика запросов пишется в лог каждые 60 секунд:

stats: slaves=3 total_ok=12480 total_err=0

Windows

На Windows всё то же самое, только порт называется COM4 вместо /dev/ttyS7, файлы в C:\temp\ вместо /tmp/, и -bg вместо -d:

modbus_slave.exe -bg -p COM4 -b 115200 -a 3 ^
-f 1:C:\temp\cpu.dat ^
-f 2:C:\temp\time.dat

Логи пишутся в C:\temp\modbus_slave.log.

Сборка из исходников

Код разделён на три файла:

ФайлСодержимое
modbus_core.hВся логика Modbus: CRC16, FC03, файлы, статистика
modbus_slave.cLinux: termios, fork, syslog, /proc
modbus_slave_win.cWindows: Win32 API, CreateFile, DCB
# Linux x86_64
gcc -O2 -Wall -o modbus_slave src/modbus_slave.c

# Linux aarch64 статический
aarch64-linux-gnu-gcc -O2 -Wall -static -o modbus_slave_aarch64 src/modbus_slave.c

# Windows .exe с Linux
x86_64-w64-mingw32-gcc -O2 -Wall -o modbus_slave.exe src/modbus_slave_win.c

Итог

modbus_slave решает конкретную задачу — дать возможность разрабатывать и тестировать Modbus мастер без реального железа, или превратить одноплатный компьютер в многоканальный шлюз данных в Modbus.

Код открытый, собирается одной командой, работает на том же NAPI2 где и всё остальное.

Репозиторий: github.com/lab240/modpoll-slave

Утилита mbscan - быстрый поиск Modbus устройств на линии

· 5 min read
dmn
maintainer

247 адресов за 2.5 секунды, ноль зависимостей, один .c файл. Рассказываем, зачем мы написали свой сканер Modbus-шины и как он работает.


Проблема: «а что вообще висит на шине?»

Кто работал с Modbus RTU, знает ситуацию: подключаешь шлюз к RS-485 шине, а там десяток устройств с неизвестными адресами. Или один датчик, но кто-то поставил ему адрес 117 вместо документированного 1. Или устройство просто не отвечает — и непонятно, проблема в адресе, скорости, чётности или в самом устройстве.

Стандартный подход - mbpoll или любой Modbus-клиент, которым вручную перебираешь адреса. Это работает, но медленно и неудобно: 247 возможных адресов, на каждый нужно отправить запрос, подождать таймаут, проверить ответ.

Мы решили автоматизировать это одной утилитой.

Репозиторий: github.com/lab240/mbscan


Что такое mbscan

mbscan - консольная утилита для сканирования Modbus RTU шины. Открывает последовательный порт, последовательно опрашивает диапазон адресов функцией FC03 (Read Holding Registers) и выводит найденные устройства с содержимым регистров.

Один файл на C, никаких внешних библиотек. Встроенная реализация CRC16, POSIX-совместимый код. Работает на Linux x86_64, aarch64, OpenWrt, Raspberry Pi — везде, где есть терминальный API POSIX.

Быстрый старт

# Сканируем всё на /dev/ttyUSB0 (по умолчанию: 115200-8N1, таймаут 100мс)
mbscan -p /dev/ttyUSB0

# Быстрый скан с таймаутом 10мс
mbscan -p /dev/ttyUSB0 -o 10

# Конкретный диапазон, читаем 4 регистра
mbscan -p /dev/ttyUSB0 -f 1 -t 30 -c 4

# 9600 бод, чётность 8E1
mbscan -p /dev/ttyS1 -b 9600 -d 8E1

Вывод выглядит так:

mbscan: scanning /dev/ttyUSB0 115200-8N1, addresses 1-247, timeout 100ms
mbscan: reading 4 register(s) starting at 0

Found slave 125: [0]=125 [1]=1 [2]=830 [3]=794

mbscan: done. Found 1 device(s).

Нашёл устройство на адресе 125, прочитал 4 регистра — готово.


Параметры

mbscan -p PORT [опции]

-p PORT Последовательный порт (обязательный)
-b BAUD Скорость (по умолчанию: 115200)
-d PARAMS Формат данных: 8N1, 8E1, 8O1, 7E1 и т.д. (по умолчанию: 8N1)
-f FROM Начальный адрес (по умолчанию: 1)
-t TO Конечный адрес (по умолчанию: 247)
-o MS Таймаут на адрес в мс (по умолчанию: 100)
-r REG Начальный регистр, 0-based (по умолчанию: 0)
-c COUNT Количество регистров для чтения (по умолчанию: 1)
-v Подробный вывод
-h Справка

Как это работает внутри

Алгоритм прямолинейный, но дьявол в деталях:

  1. Открывает последовательный порт, настраивает скорость, чётность, количество стоп-битов через termios.

  2. Для каждого адреса в диапазоне:

    • Сбрасывает буфер порта от предыдущих данных
    • Формирует 8-байтовый запрос Modbus RTU FC03 с CRC16
    • Отправляет запрос и ждёт ответ с настроенным таймаутом
    • Валидирует ответ: проверяет CRC, адрес slave, код функции
    • Если всё сходится — выводит найденное устройство с содержимым регистров
  3. Между запросами выдерживает межкадровую паузу Modbus (3.5 символьных времени) — это требование протокола, без него устройства могут путать конец одного кадра и начало другого.

CRC16 реализован встроенный - нет зависимости от libmodbus или других библиотек. Весь код в одном файле mbscan.c.


Скорость сканирования

Скорость определяется таймаутом на адрес. Если устройство не отвечает — ждём полный таймаут. Если отвечает — переходим к следующему сразу после получения ответа.

ТаймаутПолный скан (1–247)Когда использовать
10 мс~2.5 секКороткие кабели, лабораторный стенд
50 мс~12 секБольшинство установок
100 мс~25 секПо умолчанию, надёжно
200 мс~50 секДлинные линии RS-485

На практике 10 мс хватает для стенда с коротким кабелем. Для промышленных линий с десятками метров RS-485 лучше ставить 50-100 мс — на длинных линиях задержки растут из-за переотражений и ёмкости кабеля.


Сборка

Нативная компиляция

cd src
gcc -O2 -Wall -o mbscan mbscan.c

Статическая сборка (один бинарник без зависимостей):

gcc -O2 -Wall -static -o mbscan mbscan.c

Пакет для OpenWrt

Каталог mbscan кладётся в дерево пакетов OpenWrt:

cp -r mbscan /path/to/openwrt/package/
cd /path/to/openwrt
echo "CONFIG_PACKAGE_mbscan=y" >> .config
make package/mbscan/compile -j$(nproc)

Результат — .ipk (или .apk) пакет в bin/packages/*/base/.

Кросс-компиляция для aarch64

Если есть тулчейн OpenWrt:

/path/to/openwrt/staging_dir/toolchain-aarch64_generic_gcc-*/bin/aarch64-openwrt-linux-gcc \
-O2 -Wall -static -o mbscan-linux-aarch64 src/mbscan.c

Готовые бинарники для x86_64 и aarch64 доступны на странице Releases.


Интеграция с luci-app-mbpoll

mbscan - не просто самостоятельная утилита. Он используется как бэкенд для вкладки Scan Bus в веб-интерфейсе luci-app-mbpoll - нашем LuCI-приложении для опроса Modbus-устройств.

Схема простая: пользователь задаёт параметры порта и диапазон адресов в браузере, LuCI вызывает mbscan на устройстве, парсит вывод и отображает найденные устройства в таблице. Не нужно заходить по SSH, не нужно помнить синтаксис - всё через веб-интерфейс.

Репозиторий luci-app-mbpoll: github.com/lab240/luci-app-mbpoll


Где используется

Основная платформа - промышленные IoT-шлюзы NapiLab Napi на базе Rockchip RK3308 под управлением OpenWrt. Napi имеет встроенный RS-485 на /dev/ttyS1 и два USB-порта для дополнительных адаптеров — типичная конфигурация для Modbus-шлюза.

Но mbscan работает на любом Linux с последовательным портом: обычный x86_64 с USB-RS485 адаптером (CH341, CP2102, FTDI), Raspberry Pi, любая embedded-плата.


Лицензия

GPL-2.0 - как и OpenWrt, как и остальные наши инструменты.

Сборка и запуск Zigbee2mqtt для OpenWRT

· 5 min read
dmn
maintainer

Инструкция по сборке Zigbee2MQTT под musl/aarch64 на хост-машине с Docker и запуску на OpenWrt.


Почему это нетривиально

OpenWrt использует musl libc вместо стандартного glibc. Это означает:

  • Официальные бинарники Node.js с nodejs.org (glibc) не запустятся
  • Пакет node в фидах OpenWrt — только host-инструмент для сборки (PKG_HOST_ONLY=1), в прошивку не попадает
  • Entware для aarch64 не содержит Node.js
  • Нативные модули (@serialport/bindings-cpp) нужно компилировать под musl

Решение: собирать всё в Docker-контейнере на базе Alpine Linux (тоже использует musl).


Требования

Железо

Проверено на NAPI-C (rk3308\512Мб\4Гб Nand) c прошивкой OpenWRT (NapiWRT). Репозиторий: https://github.com/lab240/napi-openwrt-build/

Программное обеспечение на хост-машине

  • Docker
  • Git

Шаг 1: Подготовка носителя

OpenWrt по умолчанию создаёт rootfs раздел ~104 МБ. Для Zigbee2MQTT нужно минимум 500 МБ свободного места.

В нашей сборке NapiWRT это решено автоматически через два uci-defaults скрипта которые при первой загрузке расширяют rootfs до конца носителя:

files/etc/uci-defaults/70-rootpt-resize — расширяет раздел и перезагружается:

if [ ! -e /etc/rootpt-resize ] \
&& type parted > /dev/null \
&& lock -n /var/lock/root-resize
then
ROOT_BLK="$(readlink -f /sys/dev/block/"$(awk -e \
'$9=="/dev/root"{print $3}' /proc/self/mountinfo)")"
ROOT_DISK="/dev/$(basename "${ROOT_BLK%/*}")"
ROOT_PART="${ROOT_BLK##*[^0-9]}"
echo "70-rootpt-resize: expanding ${ROOT_DISK} partition ${ROOT_PART} to 100%..."
parted -f -s "${ROOT_DISK}" resizepart "${ROOT_PART}" 100%
echo "70-rootpt-resize: done, rebooting..."
mount_root done
touch /etc/rootpt-resize
reboot
fi
exit 1

files/etc/uci-defaults/80-rootfs-resize — расширяет файловую систему через losetup и перезагружается:

if [ ! -e /etc/rootfs-resize ] \
&& [ -e /etc/rootpt-resize ] \
&& type losetup > /dev/null \
&& type resize2fs > /dev/null \
&& lock -n /var/lock/root-resize
then
ROOT_BLK="$(readlink -f /sys/dev/block/"$(awk -e \
'$9=="/dev/root"{print $3}' /proc/self/mountinfo)")"
ROOT_DEV="/dev/${ROOT_BLK##*/}"
echo "80-rootfs-resize: resizing filesystem on ${ROOT_DEV}..."
LOOP_DEV="$(losetup -f)"
losetup "${LOOP_DEV}" "${ROOT_DEV}"
resize2fs -f "${LOOP_DEV}"
losetup -d "${LOOP_DEV}"
echo "80-rootfs-resize: done, rebooting..."
mount_root done
touch /etc/rootfs-resize
reboot
fi
exit 1

Необходимые пакеты в .config сборки:

CONFIG_PACKAGE_parted=y
CONFIG_PACKAGE_losetup=y
CONFIG_PACKAGE_resize2fs=y

Шаг 2: Установка Node.js на устройство

Node.js для musl/aarch64 предоставляет проект unofficial-builds от nodejs.org.

# На устройстве
cd /tmp
wget https://unofficial-builds.nodejs.org/download/release/v22.22.0/node-v22.22.0-linux-arm64-musl.tar.gz
mkdir -p /opt/node
tar xzf node-v22.22.0-linux-arm64-musl.tar.gz -C /opt/node --strip-components=1
rm node-v22.22.0-linux-arm64-musl.tar.gz

# Добавляем в PATH
export PATH=/opt/node/bin:$PATH

# Проверяем
node --version # v22.22.0
npm --version # 10.9.4

Официальный бинарник с nodejs.org (linux-arm64 без суффикса musl) не запустится — он скомпилирован под glibc.


Готовый архив

Если вы не хотите собирать самостоятельно — готовый архив Zigbee2MQTT для musl/aarch64 доступен в релизах репозитория:

👉 https://github.com/lab240/napi-openwrt-build/releases

Скачайте файл zigbee2mqtt-2.9.1-openwrt-aarch64-musl.tar.gz и перейдите сразу к Шагу 4.


Шаг 3: Сборка Zigbee2MQTT на хост-машине

Zigbee2MQTT содержит нативные модули (@serialport/bindings-cpp) которые нужно компилировать под целевую платформу. Делаем это в Docker с Alpine (musl) под arm64.

На хост-машине

# Клонируем репозиторий
git clone --depth 1 https://github.com/Koenkk/zigbee2mqtt.git ~/zigbee2mqtt-arm

# Собираем в Docker под Alpine/arm64/musl
docker run --rm -v ~/zigbee2mqtt-arm:/app \
--platform linux/arm64 \
node:22-alpine \
sh -c "apk add python3 make g++ linux-headers && \
cd /app && \
npm install && \
npm rebuild @serialport/bindings-cpp --build-from-source && \
npm run build && \
tar czf /app/z2m.tar.gz --dereference -C /app ."

Ключевые флаги:

  • --platform linux/arm64 — целевая архитектура aarch64
  • node:22-alpine — Alpine использует musl как OpenWrt, Node.js 22 соответствует требованиям Z2M
  • linux-headers — нужны для компиляции @serialport/bindings-cpp
  • --build-from-source — компилируем нативные модули вместо использования prebuilt glibc бинарников
  • --dereference — разворачиваем симлинки в tar (иначе они сломаются при распаковке)

Время сборки: 3–5 минут.


Шаг 4: Копирование на устройство

# С устройства (через scp)
scp dmn@<IP_ХОСТА>:~/zigbee2mqtt-arm/z2m.tar.gz /opt/

# Распаковываем
rm -rf /opt/zigbee2mqtt
mkdir /opt/zigbee2mqtt
tar xzf /opt/z2m.tar.gz -C /opt/zigbee2mqtt/
rm /opt/z2m.tar.gz

Шаг 5: Зависимости на устройстве

Для работы нативных модулей нужна libstdc++:

apk update
apk add libstdcpp6

Шаг 6: Запуск

export PATH=/opt/node/bin:$PATH
cd /opt/zigbee2mqtt
npm start

При успешном запуске:

Starting Zigbee2MQTT without watchdog.
Onboarding page is available at http://0.0.0.0:8080/

Откройте браузер: http://<IP_устройства>:8080/ — онбординг страница для настройки координатора и MQTT.


Конфигурация

После онбординга конфиг сохраняется в /opt/zigbee2mqtt/data/configuration.yaml:

mqtt:
server: mqtt://localhost # Mosquitto уже установлен в базовой сборке Napi
serial:
port: /dev/ttyUSB0 # Порт Zigbee-координатора
adapter: ember # или znp — зависит от координатора

Автозапуск через procd

Создаём init-скрипт /etc/init.d/zigbee2mqtt:

#!/bin/sh /etc/rc.common

START=99
STOP=10
USE_PROCD=1

start_service() {
procd_open_instance
procd_set_param env PATH=/opt/node/bin:/usr/sbin:/usr/bin:/sbin:/bin
procd_set_param command /opt/node/bin/node /opt/zigbee2mqtt/index.js
procd_set_param dir /opt/zigbee2mqtt
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
chmod +x /etc/init.d/zigbee2mqtt
/etc/init.d/zigbee2mqtt enable
/etc/init.d/zigbee2mqtt start

Итог: что получили

КомпонентВерсияИсточник
Node.js22.22.0unofficial-builds.nodejs.org (musl/arm64)
npm10.9.4в составе Node.js
Zigbee2MQTT2.9.xсобран в Docker/Alpine/arm64
libstdc++из репозитория OpenWrtapk
Mosquittoиз образавстроен в сборку Napi

Известные ограничения

  • Node.js не входит в стандартный образ — устанавливается вручную в /opt
  • При обновлении прошивки /opt сохраняется (на отдельном разделе или eMMC)
  • udevadm недоступен — автообнаружение адаптера не работает, порт указывается вручную в конфиге

OpenWrt для Napi - архитектура и сборка

· 12 min read
dmn
maintainer

Статья для тех, кто хочет собрать OpenWrt под платы NapiLab Napi самостоятельно и понимать, что именно происходит на каждом шаге — от патча U-Boot до первого входа по SSH.


Зачем вообще собирать OpenWrt для Napi?

NapiLab Napi — промышленный одноплатный компьютер (SBC) и системный модуль (SOM) на базе Rockchip RK3308. Платформа ориентирована на промышленный IoT: сбор данных с датчиков, шлюзы Modbus TCP/RTU, MQTT-брокеры, удалённый мониторинг.

Ванильный OpenWrt доступен для "родственной" платы RockPi-S, но не знает особенностей Napi: нет device tree дополнительных портов, нет правильной конфигурации U-Boot, нет пакетов для промышленного применения. Наш репозиторий — это набор патчей, DTS, uci-defaults и пакетов, которые превращают чистый снапшот OpenWrt в готовый промышленный одноплатник.

Если хотите сразу попробовать без сборки — готовые образы доступны на странице загрузок napiworld.ru.

Что даёт кастомная сборка

  • Стабильный MAC-адрес — генерируется из OTP-данных чипа, не меняется после перезагрузки
  • Правильный Device Tree — UART1 и UART2 в нужных режимах, Bluetooth отключён
  • Готовый стек Modbus TCPmbusd + веб-интерфейс luci-app-mbusd из коробки
  • MQTT-брокерmosquitto уже установлен и настроен
  • Поддержка LTE-модемов — Quectel EP06 работает без дополнительных танцев
  • Первый старт без консоли — все настройки применяются через uci-defaults автоматически

Поддерживаемое железо

Все платы используют один и тот же SoC — Rockchip RK3308, поэтому собирается одна прошивка для всей линейки:

ПлатаХранилищеТип
NapiLab Napi-C4 ГБ NAND — 32 ГБ eMMCПромышленный SBC
NapiLab Napi-P4 ГБ NAND — 32 ГБ eMMCПромышленный SBC
NapiLab Napi-Slot4 ГБ NAND — 32 ГБ eMMCSOM
Radxa ROCK Pi SРеференсная плата, тот же RK3308

Характеристики RK3308

КомпонентДетали
CPUQuad-core ARM Cortex-A35, 1.3 ГГц
RAM256 МБ / 512 МБ DDR3
Ethernet100 Мбит/с (GMAC + PHY RTL8201F)
USB2× USB 2.0 Host
UART3× UART (ttyS0 — консоль, ttyS1, ttyS2)
Wi-FiRTL8723DS (802.11b/g/n)

Структура репозитория: что куда кладётся

./
├── files/
│ └── etc/
│ └── uci-defaults/ # Скрипты первого старта
│ ├── 91-bash
│ ├── 92-timezone
│ ├── 93-console-password
│ ├── 94-macaddr
│ ├── 95-network
│ ├── 96-hostname
│ ├── 97-luci-theme
│ └── 99-dhcp

├── package/
│ ├── boot/
│ │ └── uboot-rockchip/
│ │ ├── Makefile
│ │ └── patches/
│ │ └── 108-board-rockchip-add-napilab-napic.patch # Патч U-Boot
│ └── luci-app-mbusd/ # Веб-интерфейс для mbusd
│ ├── Makefile
│ ├── htdocs/luci-static/resources/view/
│ │ └── mbusd.js
│ └── root/
│ ├── etc/uci-defaults/luci-app-mbusd
│ └── usr/share/
│ ├── luci/menu.d/luci-app-mbusd.json
│ └── rpcd/acl.d/luci-app-mbusd.json

└── target/linux/rockchip/
├── files/arch/arm64/boot/dts/rockchip/
│ └── rk3308-napi-c.dts # Кастомный Device Tree
└── image/
└── armv8.mk # Описание целевого образа

Разберём каждую часть подробно.


U-Boot: почему нужен патч и что он делает

OpenWrt собирает U-Boot из исходников вместе с прошивкой. Для RK3308 есть готовая конфигурация для Radxa ROCK Pi S — мы взяли её за основу, так как схемотехника близка к Napi.

Патч 0001-napic-rk3308-defconfig.patch

Патч добавляет новый вариант napic-rk3308 в систему сборки U-Boot:

+++ b/configs/napic-rk3308_defconfig
@@ -0,0 +1,42 @@
+CONFIG_ARM=y
+CONFIG_ARCH_ROCKCHIP=y
+CONFIG_SYS_TEXT_BASE=0x00600000
+CONFIG_ROCKCHIP_RK3308=y
+CONFIG_TARGET_EVB_RK3308=y
+CONFIG_DEFAULT_DEVICE_TREE="rk3308-napi-c"
+CONFIG_DISTRO_DEFAULTS=y
+CONFIG_SYS_MALLOC_F_LEN=0x4000
+CONFIG_BAUDRATE=1500000
+CONFIG_BOOTDELAY=0
...

Ключевые настройки:

  • CONFIG_DEFAULT_DEVICE_TREE="rk3308-napi-c" — указываем U-Boot использовать наш DTS
  • CONFIG_BAUDRATE=1500000 — нестандартная скорость консоли (1.5 Мбод), типичная для Rockchip
  • CONFIG_BOOTDELAY=0 — не ждём прерывания при старте (промышленное применение)

Как собрать только U-Boot

make package/boot/uboot-rockchip/compile VARIANT=napic-rk3308 -j$(nproc)

Флаг VARIANT=napic-rk3308 говорит системе сборки использовать именно наш defconfig.


Device Tree (DTS): описываем железо ядру

Device Tree — это описание аппаратной конфигурации платы в текстовом формате. Ядро Linux не знает про периферию «само по себе», ему нужно явно сказать: «вот тут UART, вот тут Ethernet, вот GPIO».

Файл rk3308-napi-c.dts

Берём за основу rk3308-rock-pi-s.dts (Radxa ROCK Pi S — ближайший аналог по схемотехнике) и переопределяем то, что отличается у Napi.

/dts-v1/;
#include "rk3308.dtsi"
#include "rk3308-rock-pi-s.dtsi"

/ {
model = "NapiLab Napi-C";
compatible = "napilab,napi-c", "rockchip,rk3308";
};

/* UART1 → RS-485 через mbusd */
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart1_xfer>;
};

/* UART2 — доступен как /dev/ttyS2 */
&uart2 {
status = "okay";
};

/* Bluetooth отключаем — не нужен в промышленном применении */
&bluetooth {
status = "disabled";
};

Что важно в этом DTS:

uart1 — маппится на /dev/ttyS1. Это главный последовательный порт, к которому подключаются RS-485 устройства Modbus. mbusd будет слушать именно его.

uart2 — маппится на /dev/ttyS2, доступен для дополнительных устройств.

bluetooth disabled — RTL8723DS предоставляет и Wi-Fi, и Bluetooth через один чип. Bluetooth нам не нужен и только занимает UART, поэтому отключаем на уровне DTS — никаких лишних сервисов, никаких потерь производительности.

Где лежит DTS в дереве OpenWrt

target/linux/rockchip/files/arch/arm64/boot/dts/rockchip/rk3308-napi-c.dts

OpenWrt копирует файлы из target/linux/<arch>/files/ поверх исходников ядра перед компиляцией. Это стандартный механизм добавления новых DTS без форка ядра.


uci-defaults: автоматическая настройка при первом старте

uci-defaults — это скрипты, которые OpenWrt запускает один раз при первой загрузке и затем удаляет. Они позволяют настроить систему до того, как пользователь зашёл в веб-интерфейс или по SSH.

Скрипты лежат в:

target/linux/rockchip/armv8/base-files/etc/uci-defaults/

Нумерация определяет порядок выполнения. Разберём каждый:


91-bash — bash как оболочка по умолчанию

#!/bin/sh
# Меняем /bin/ash на /bin/bash для root
sed -i 's|/bin/ash|/bin/bash|' /etc/passwd

По умолчанию OpenWrt использует ash (BusyBox). Для работы с промышленными скриптами, которые рассчитаны на bash-синтаксис (массивы, [[, $RANDOM, process substitution), нужен настоящий bash. Скрипт делает одно изменение в /etc/passwd.


92-timezone — московское время

#!/bin/sh
uci set system.@system[0].timezone='MSK-3'
uci set system.@system[0].zonename='Europe/Moscow'
uci commit system

Промышленные устройства работают в конкретном часовом поясе. Временна́я метка в логах и данных должна быть правильной сразу, без ручной настройки. MSK-3 — это UTC+3 (Москва).


93-console-password — пароль на серийную консоль

#!/bin/sh
# Включаем запрос пароля на ttyS0
uci set system.@system[0].ttylogin='1'
uci commit system

По умолчанию OpenWrt пускает на консоль без пароля — удобно при разработке, неприемлемо в продакшне. Скрипт включает запрос пароля на ttyS0 (консоль 1.5 Мбод).


94-macaddr — стабильный MAC из OTP

Это самый важный скрипт. Проблема: у RK3308 нет встроенного уникального MAC-адреса в eFuse — он генерируется случайно при каждой загрузке. Это катастрофа для промышленного применения: DHCP-сервер каждый раз выдаёт другой IP, ARP-таблицы засоряются, устройство теряется в сети.

Решение: генерировать MAC детерминированно из OTP (One-Time Programmable) памяти чипа. OTP содержит уникальные данные, которые прошиваются на заводе и никогда не меняются.

#!/bin/sh

# Читаем OTP и берём MD5 от него
MAC=$(cat /sys/bus/nvmem/devices/rockchip-otp0/nvmem | md5sum | \
sed 's/\(..\)\(..\)\(..\)\(..\)\(..\)\(..\).*/02:\1:\2:\3:\4:\5/')

# Применяем MAC к интерфейсу
uci set network.@device[0].macaddr="$MAC"
uci commit network

Разбор команды по частям:

  1. /sys/bus/nvmem/devices/rockchip-otp0/nvmem — бинарный файл с содержимым OTP через интерфейс nvmem ядра
  2. md5sum — хешируем бинарные данные, получаем 32 hex-символа
  3. sed — берём первые 12 символов и форматируем как MAC
  4. Первый байт 02 — бит Local (bit 1 = 1) установлен, бит Multicast (bit 0 = 0) сброшен. Это стандарт для locally-administered MAC

Результат: каждая плата Napi получает один и тот же MAC при каждой загрузке, но разные платы имеют разные MAC — уникальность гарантирована уникальностью OTP.


95-network — настройка Ethernet без бриджа

#!/bin/sh

# Убираем дефолтный бридж br-lan
uci set network.lan.device='eth0'
uci set network.lan.type=''
uci delete network.@bridge-vlan[0] 2>/dev/null

uci commit network

Стандартный OpenWrt создаёт бридж br-lan из всех Ethernet-портов — это логично для роутера с несколькими портами. У Napi один Ethernet-порт, бридж избыточен. Скрипт переводит lan напрямую на eth0, убирая лишний сетевой уровень.


96-hostname — имя устройства

#!/bin/sh
uci set system.@system[0].hostname='napiwrt'
uci commit system

napiwrt — имя по умолчанию. Устройство будет видно в сети как napiwrt.local (через mDNS). Пользователь может сменить имя через LuCI.


97-luci-theme — тема веб-интерфейса

#!/bin/sh
uci set luci.main.mediaurlbase='/luci-static/openwrt-2020'
uci commit luci

Тема openwrt-2020 — современный Bootstrap-based интерфейс. Тема bootstrap (старая) выглядит устаревшей. Устанавливаем сразу нужную.


99-dhcp — конфигурация DHCP

#!/bin/sh

# Убираем dnsmasq с lan-интерфейса — устройство само получает IP по DHCP
uci set dhcp.lan.ignore='1'
uci commit dhcp

Napi в типовой конфигурации — не роутер, а промышленный шлюз. Он не должен раздавать DHCP в сеть, он должен получать IP сам. Скрипт отключает DHCP-сервер на lan.


Пакеты: что и зачем включено в сборку

Промышленный стек

ПакетНазначение
mbusdШлюз Modbus RTU → Modbus TCP. Слушает /dev/ttyS1 (RS-485) и пробрасывает на TCP-порт
luci-app-mbusdВеб-интерфейс для mbusd: старт/стоп, конфигурация порта, мониторинг
mbpollCLI-инструмент для опроса Modbus-устройств с командной строки
mosquittoMQTT-брокер. Устройства публикуют данные в топики, приложения подписываются
mosquitto-clientCLI-клиент: mosquitto_pub и mosquitto_sub для отладки

Поддержка USB-Serial адаптеров

kmod-usb-serial-ch341   # WCH CH340/CH341 (самые распространённые)
kmod-usb-serial-cp210x # Silicon Labs CP2102 и серия
kmod-usb-serial-ftdi # FTDI FT232 и совместимые
kmod-usb-serial-pl2303 # Prolific PL2303

Napi имеет 2× USB 2.0. Через USB-Serial можно подключить дополнительные RS-485/RS-232 адаптеры или устройства с USB-интерфейсом.

Поддержка LTE

kmod-usb-net-qmi-wwan   # QMI-протокол для LTE-модемов
uqmi # Пользовательский инструмент для управления QMI

Поддержка Quectel EP06 (Cat-6 LTE). Модем подключается через USB, управляется через QMI. uqmi позволяет настроить APN, поднять PPP-соединение, смотреть сигнал.

Сетевые инструменты

openssh-sftp-server   # SFTP — копирование файлов через SSH без FTP
luci-ssl-wolfssl # HTTPS для LuCI (wolfSSL — лёгкая альтернатива OpenSSL)
tcpdump # Захват трафика прямо на устройстве
ethtool # Диагностика Ethernet

Административные утилиты

bash    # Полноценная оболочка
htop # Мониторинг процессов
nano # Редактор для тех, кто не любит vi
screen # Мультиплексор терминала — незаменим при работе через последовательный порт

luci-app-mbusd: веб-интерфейс для Modbus-шлюза

Пакет luci-app-mbusd — наша собственная разработка. mbusd — отличный Modbus-шлюз, но управляется только через конфиг-файл и командную строку. Для промышленного применения нужен удобный веб-интерфейс.

Что умеет luci-app-mbusd

  • Запуск / остановка / перезапуск службы mbusd через кнопки в браузере
  • Включение / отключение автозапуска при загрузке
  • Live-статус процесса с отображением реальных параметров запуска
  • Отображение IP-адреса и порта, на котором слушает шлюз
  • Полная конфигурация: последовательный порт, скорость, чётность, стоп-биты, параметры Modbus

Интерфейс написан как стандартное LuCI-приложение на Lua + HTML, следует конвенциям OpenWrt UCl API.


Сборка: пошаговая инструкция

1. Зависимости (Ubuntu/Debian)

sudo apt install build-essential clang flex bison g++ gawk gcc-multilib \
gettext git libncurses-dev libssl-dev python3-distutils rsync unzip zlib1g-dev

2. Клонируем OpenWrt

git clone https://github.com/openwrt/openwrt.git
cd openwrt

# Обновляем фиды (репозитории пакетов)
./scripts/feeds update -a
./scripts/feeds install -a

3. Накладываем кастомизации

Архив с кастомизациями берём из релизов репозитория:

# Распаковываем наш архив поверх дерева OpenWrt
tar xzf napic-openwrt-YYYYMMDD-HHMM-v1.0.tar.gz -C /path/to/openwrt/

Архив содержит все файлы из нашего репозитория в том же дереве каталогов, что и OpenWrt. После распаковки:

  • target/linux/rockchip/ — дополнен нашим DTS и uci-defaults
  • package/boot/uboot-rockchip/patches/ — содержит патч U-Boot
  • package/luci-app-mbusd/ — добавлен наш пакет
  • .config — готовая конфигурация сборки

4. Собираем U-Boot

make package/boot/uboot-rockchip/compile VARIANT=napic-rk3308 -j$(nproc)

U-Boot для Rockchip RK3308 состоит из нескольких стадий:

  • TPL (Tertiary Program Loader) — инициализация DDR
  • SPL (Secondary Program Loader) — инициализация минимального железа
  • U-Boot proper — полноценный загрузчик

Все три стадии собираются автоматически, результат упаковывается в idbloader.img + u-boot.itb.

5. Собираем прошивку

make -j$(nproc)

Система сборки OpenWrt:

  1. Компилирует кросс-тулчейн (gcc, binutils, musl libc)
  2. Компилирует ядро Linux с нашим DTS
  3. Компилирует все выбранные пакеты
  4. Упаковывает rootfs + ядро + U-Boot в финальный образ

Время сборки на современном железе (8 ядер): 30–60 минут при первой сборке, 5–10 минут при пересборке с изменениями.

6. Результат сборки

bin/targets/rockchip/armv8/
└── openwrt-rockchip-armv8-napilab_napic-ext4-sysupgrade.img.gz

Образ содержит таблицу разделов GPT, U-Boot, ядро, rootfs — всё в одном файле.


Прошивка

Если не хотите собирать самостоятельно — готовые образы доступны на странице загрузок napiworld.ru.

# Распаковываем
gunzip openwrt-rockchip-armv8-napilab_napic-ext4-sysupgrade.img.gz

# Пишем на носитель (замените /dev/sdX на реальное устройство!)
dd if=openwrt-rockchip-armv8-napilab_napic-ext4-sysupgrade.img \
of=/dev/sdX \
bs=4M \
status=progress
sync

⚠️ Внимательно проверьте /dev/sdX командой lsblk перед записью. Ошибка в имени устройства приведёт к затиранию данных.


Первый запуск

После записи образа и подачи питания:

  1. U-Boot стартует, инициализирует DDR, находит ядро в разделе
  2. Ядро загружается, парсит наш DTS, инициализирует периферию
  3. OpenWrt init запускает скрипты uci-defaults (один раз)
  4. Устройство получает IP по DHCP (MAC стабилен — DHCP-сервер выдаст тот же IP)
  5. LuCI доступен по http://<IP>/

Параметры доступа по умолчанию

ПараметрЗначение
IPDHCP (стабильный MAC гарантирует постоянный lease)
Веб-интерфейсhttp://<IP>/ → LuCI
SSHroot@<IP> (пароль не установлен, задаётся при первом входе)
КонсольttyS0, 1 500 000 бод

Типичные вопросы

Почему скорость консоли 1.5 Мбод?

Это стандарт Rockchip для отладочных UART. На такой скорости загрузочные сообщения U-Boot и ядра отображаются без задержек. Требуется адаптер USB-UART с поддержкой нестандартных скоростей (CP2102, FTDI — работают, CH340 — часто нет).

Почему за основу взяли ROCK Pi S, а не официальный RK3308 EVB?

ROCK Pi S — хорошо поддерживаемая в апстриме OpenWrt плата на RK3308. Её конфигурация U-Boot и DTS проверены сообществом, регулярно обновляются. EVB (Evaluation Board от Rockchip) в OpenWrt поддерживается хуже.

Можно ли добавить свои пакеты?

Да. Добавьте пакеты в .config (через make menuconfig или напрямую) и пересоберите. Кастомный пакет можно положить в package/ или добавить внешний фид.

Как обновить прошивку через LuCI?

System → Backup / Flash Firmware → Flash new firmware image. Загрузите sysupgrade.img.gz. OpenWrt сохранит пользовательские настройки (/etc/config/) если не снять галочку «Keep settings».


Скриншоты

Командная строка через консоль, ssh

Главная страница

Настройка сети

Обновление, бекап, рестор

Пакеты

Mbusd

Настройка и тестирование CAN интерфейса в Linux

· One min read
dmn
maintainer

Настройка CAN интерфейса

Поднимаем и проверяем CAN интерфейс.

Интересно, что CAN в Linux это сетевой интерфейс. На него нельзя повесить IP, но Linux управлять можно через ip link.

ip link set can0 down
ip link set can0 type can bitrate 500000 restart-ms 100
ip link set can0 up

Проверка loopback

Проверяем loop (Аналог ping localhost).

Ставим пакет:

apt install can-utils

Тестирование

В одной сессии слушаем can0:

root@napi2:~# candump -L can0

В другой на этот же интерфейс шлем посылку:

cansend can0 123#11223344

Должны получить ответ в сессии, где слушали:

(1769774861.028890) can0 123#11223344

#can #napi2

Управление светодиодами Ethernet через MDIO

· One min read
dmn
maintainer

Утилита для управления mdio (в частности, подсветкой лампочек Ethernet): https://github.com/wkz/phytool

Классический вид

Левый (зелёный) - линк, правый (рыжий) - данные:

  1. Выбрать страницу LED конфигурации:
./phytool write wan/0/0x1f 0x0d04
  1. (Опционально) отключить EEE LED влияние:
./phytool write wan/0/0x11 0x0000
  1. Установить: LED1=Link(any speed), LED2=Activity:
./phytool write wan/0/0x10 0xC160
  1. Вернуть страницу 0:
./phytool write wan/0/0x1f 0x0000

Альтернативный вид

Слева (зелёный) Link+Act (подмешивает в один диод и линк и моргание данными) 10mbit, справа (рыжий) - Link + Act 100+Mbit:

  1. Выбрать страницу LED конфигурации:
./phytool write wan/0/0x1f 0x0d04
  1. (Опционально) отключить EEE LED влияние:
./phytool write wan/0/0x11 0x0000
  1. Установить: LED1=Link+Act 10mbit, LED2=Link+Act 100+Mbit:
./phytool write wan/0/0x10 0x6251
  1. Вернуть страницу 0:
./phytool write wan/0/0x1f 0x0000

Получение уникального ID процессора Rockchip

· One min read
dmn
maintainer

Получение уникального ID процессора

Встал вопрос, как получить уникальный ID процессора для идентификации конкретного экземпляра Napi-C/Napi-P/Napi-S.

Способ через OTP

Мы нашли такой способ:

ID=$(dd if=/sys/bus/nvmem/devices/rockchip-otp0/nvmem bs=1 skip=8 count=8 2>/dev/null | xxd -p)
echo -n "$ID" | sha256sum

Описание команд

  • dd - читает 8 байт из OTP памяти процессора Rockchip начиная с 8-го байта
  • xxd -p - конвертирует двоичные данные в hex строку
  • sha256sum - создает SHA256 хеш для обеспечения уникальности

Анализ сигналов Modbus RS485 на анализаторе

· One min read
dmn
maintainer

Анализ сигналов Modbus RS485

Покажем передачу modbus пакетов, отображенную на анализаторе цифровых сигналов.

Оборудование

  • Хост: Napi-C с программным RTS
  • Датчик: учебный Modbus Napi-датчик

Конфигурация каналов

  • Канал 1: RTS хоста
  • Канал 2: RX хоста
  • Канал 3: TX хоста
  • Канал 4: RX датчика
  • Канал 5: TX датчика

Последовательность обмена

  1. Хост поднимает сигнал RTS (передача)
  2. Посылает запрос по линии TX
  3. Датчик принимает запрос через RX
  4. Датчик передает ответ через TX
  5. Хост принимает ответ через RX

#rs485 #rts

Настройка параметров последовательного порта в Linux

· One min read
dmn
maintainer

Настройка последовательного порта

Установка параметров последовательного порта в Linux на примере ttyS1

Просмотр текущих параметров

stty -F /dev/ttyS1 -a

Пример вывода:

speed 115200 baud; line = 0;
-brkint -icrnl -imaxbel
opost -onlcr
cs8 -parenb -cstopb

Расшифровка параметров

  • speed 115200 baud — текущая скорость
  • cs8 — 8 бит данных
  • -parenb — без бита четности (N)
  • -cstopb — 1 стоп-бит (если было cstopb → 2 стоп-бита)

Установка параметров порта

stty -F /dev/ttyS1 115200 cs8 -cstopb -parenb -ixon -ixoff -crtscts

Тестирование порта

Дальше можно прямо из командной строки:

echo "test" > /dev/ttyS1