Skip to content

Edge-шлюз на FreeBSD 14.4: почему не Linux?

Система мониторинга

👁0просмотров

Edge-шлюз в цеху или на удаленном объекте - это не сервер в стойке с ИБП. Это устройство, которое может внезапно обесточиться, перегреться, подвергнуться сетевым атакам или работать с некачественными SD-картами/eMMC.

Linux (Ubuntu/Debian) - отличный выбор по умолчанию, но для специфических задач промышленного шлюза, FreeBSD вообще и версия 14.4 в частности, предлагает уникальные преимущества, которыми часто пренебрегают.

Почему FreeBSD 14.4, а не Linux?

Сетевой стек и безопасность.

Сетевой стек FreeBSD - это ее легендарная сильная сторона. Он лежит в основе многих коммерческих маршрутизаторов и файрволов, что говорит о его высочайшей стабильности и производительности под нагрузкой. Для платы, позиционирующейся как сетевое устройство, это ключевое преимущество.

В релизе 14.4 эта компонента получила новые укрепления:

  • Актуальные версии защищенного ПО: В систему интегрированы OpenSSH 10.0p2 и OpenSSL 3.0.16. Мы получаем самую свежую криптографическую защиту "из коробки", без необходимости донастройки.
  • Постквантовая безопасность: В OpenSSH, по умолчанию, включен гибридный алгоритм обмена ключами mlkem768x25519-sha256, что готовит нашу систему к будущим угрозам со стороны квантовых компьютеров.
  • Активное закрытие уязвимостей: Разработчики FreeBSD 14.4 устранили целый ряд критических уязвимостей, включая проблемы в ipfw (файрвол), jail (виртуализация) и сетевой подсистеме, что подтверждает серьезный подход к безопасности.

Огромным преимуществом данной ОС также является встроенный pf (Packet Filter) с синтаксисом, позволяющим за 10 минут настроить жесткий файрвол для защиты шлюза от внешнего мира, не устанавливая сторонних пакетов.

Устойчивость к сбоям питания: UFS как защита

Для встраиваемых систем, где риск внезапного отключения питания высок, целостность данных выходит на первый план. Безусловно, на сегодняшний день, FreeBSD предлагает мощное преимущество - OpenZFS 2.2.8.

Однако для SBC с 2 ГБ ОЗУ и загрузкой с SD-карты, ZFS - это как поставить двигатель от грузовика на велосипед. В данном случае, правильным бы был выбор в пользу UFS: легковесной, проверенной десятилетиями файловой системы, которая идеально подходит для встраиваемых систем. UFS использует ничтожное количество ресурсов, оставляя всю мощность процессора для маршрутизации пакетов, сбора метрик, работы брокера и функционирования базы данных.

Soft Updates гарантирует консистентность метаданных без тяжеловесных проверок, а журнал (Journal) позволяет восстановить файловую систему за секунды после внезапного отключения питания. Это дает тот же уровень защиты от сбоев питания, что и ZFS, но без просадки производительности на медленных SD-картах.

Хотя ZFS славится своими Boot Environments, в реальном Edge-деплое на SD-карте загрузка с ZFS сопряжена с рисками (нехватка памяти, баги загрузчика U-Boot). UFS предлагает иную философию - предсказуемость. Мы получаем классический MBR и загрузчик, который работает на 100% железа с первого раза.

Для защиты от неудачных обновлений мы используем проверенный временем подход: создание резервного образа корневой системы (dump/restore) или поддержание двух разделов (A/B-схема обновления), что гораздо проще в реализации на ограниченном железе, чем борьба с загрузчиком ZFS.

Давайте будем объективными. Linux предлагает Ext4 (устаревшую и без снапшотов) или Btrfs (сырую, особенно на ARM).

Четкое разделение Base System и Ports/Packages

Это фундаментальное архитектурное отличие FreeBSD от большинства Linux-дистрибутивов. В Linux, ядро, системные утилиты и пользовательские программы часто обновляются как единое целое, что может привести к непредсказуемым зависимостям.

Во FreeBSD:

  • Base System (Базовая система): Это ядро и все основные системные утилиты (init, shell, библиотеки), разрабатывающиеся и поддерживающиеся как единый, монолитный проект, что гарантирует их абсолютную совместимость и стабильность. Обновляя базовую систему, мы не рискуем сломать нами установленные приложения.
  • Ports и Packages: Всё стороннее программное обеспечение устанавливается отдельно от базовой системы. Мы можем выбрать гибкую систему Ports для компиляции из исходных кодов с любыми нужными нам опциями или использовать бинарные пакеты (pkg) для быстрой установки готовых приложений. Лично я, использую команду portmaster -dP имя_программы, которая объединяет два приведенных варианта. Эта изоляция означает, что мы можем экспериментировать с софтом, не опасаясь повредить ядро или критические системные файлы.

Поддержка ARM64 (aarch64): Официальная и зрелая

Вопрос поддержки архитектуры - ключевой при выборе ОС для SBC (Single Board Computer).

  • Официальная поддержка: FreeBSD 14.4 имеет официальную, первоклассную поддержку архитектуры aarch64 (ARM64), наряду с amd64 и i386. Это гарантирует серьезное отношение к платформе со стороны разработчиков и наличие бинарных обновлений через freebsd-update.
  • Поддержка Banana Pi M64: Плата основана на чипе Allwinner A64 (sun50i), который входит в список официально поддерживаемых SoC для FreeBSD/arm64. В отличие от BPI-M4 на Realtek RTD1395, для Allwinner A64 существует зрелая поддержка, включая драйверы для периферии. Для Banana Pi M64 также доступны сторонние наработки, такие как порт DRM-драйвера Mali-400 (lima) для FreeBSD/arm64. Стабильная работа с GPIO и I²C обеспечивается через стандартные драйверы (gpioc, iic).

Выбор FreeBSD 14.4 для Banana Pi M64 - это ставка на предсказуемость, безопасность и зрелую, проверенную десятилетиями архитектуру, где каждый компонент находится на своем месте и выполняет свою функцию.

Архитектура решения

Как инженеру, мне интересно было проверить, насколько FreeBSD 14.4 готова к реальной работе на ARM-одноплатниках. Все говорят про Raspberry Pi и Linux, а я решил пойти против течения.

"Умный дом" - это хорошо, это у меня есть и работает. Но это всегда компромис. Либо ты платишь деньги и отдаёшь данные, либо ты тратишь время и делаешь сам. Я выбрал второе.

То, что начиналось как эксперимент на выходных, быстро переросло в полноценный инженерный проект. Задача управления вентиляцией на основе данных датчика SCD30 казалась простой - 2-3 дня, пара скриптов, и готово. Как же я ошибался...

Схема решения

sequenceDiagram
    participant SCD as Датчик (SCD30)
    participant Bus as I²C/UART
    participant BP as Banana Pi M4 (FreeBSD 14.4)
    participant Go as Go-демон (EMA)
    participant DB as PostgreSQL
    participant DG as dashboard on Grafana

    SCD->>Bus: Отправка данных
    Bus->>BP: Передача по шине
    BP->>Go: Передача в демон
    Go->>DB: Сохранение сглаженных значений
    DB->>DG: Представление данных

Схема решения достаточно проста: Датчик качества воздуха SCD30, напрямую подключается к GPIO (SDA/SCL) Banana Pi и передает данные по шине I²C. Программа, оценивающая изменение параметров датчика, написана на языке программирования Golang и находится на этом же микрокомпьютере. Эта программа читает данные с датчика, рассчитывает прогноз, пишет данные в БД и отправляет их по протоколу MQTT на сервер. Кроме того, здесь установлен сервер Grafana, позволяющий организовывать дашборд с очень гибкими запросами данных непосредственно из PostgreSQL.

Для организации климатического контроллера, достаточно создать обратную связь подключением двигателя вентилятора через реле к контактам GPIO микрокомпьютера. И настроить управление работой вентилятора в той же программе на Golang.

Мы получаем очень компактное, монолитное решение, позволяющее выполнять достаточно большой объем задач. Фактически - весь спектр.

Выбор Go для FreeBSD обусловлен четырьмя факторами. Во-первых, статическая линковка создаёт единый бинарник без внешних зависимостей (в отличие от Python или Node.js), что критично для кастомных систем на SBC. Во-вторых, раннер FreeBSD на aarch64 отличается зрелостью, а Go обеспечивает отличную совместимость с этой архитектурой. В-третьих, низкое потребление ресурсов позволяет эффективно работать даже на Banana Pi с 2–4 ГБ ОЗУ, оставляя ресурсы базе данных. И, наконец, нативная работа с POSIX-потоками (горутины поверх системных тредов) даёт предсказуемую задержку при обработке данных с датчиков.

Важный нюанс для SBC (Banana Pi / Raspberry Pi)

На aarch64 FreeBSD, необходимо быть внимательным к версии Golang (она должна быть Go 1.19+), так как, в более ранних версиях, были проблемы с совместимостью системных вызовов на FreeBSD/ARM64.

Практическая реализация

Театр начинается с вешалки, а воспроизводимое развертывание - с образа. Да, с образом Banana Pi M64 под FreeBSD пришлось повозиться, но проблема решаема.

  • Шаг 1. Кастомный образ.

При подготовке загрузки Banana Pi M64 под FreeBSD собирался кастомный образ, на основе официального образа FreeBSD 14.4-RELEASE для aarch64. Такая схема обходит баг зависания оригинального U-Boot на eMMC и решает проблему конфликта больших загрузчиков с GPT-разметкой через использование классической структуры MBR.

  • Шаг 2. Разметка MicroSD-карты со смещением и расширение корневого раздела (на сервере FreeBSD);
bash
sudo dd if=u-boot-sunxi-with-spl.bin of=/dev/da1 bs=1k seek=8 conv=notrunc,sync
sudo gpart add -t fat32 -a 1m -s 100m /dev/da1
sudo gpart add -t freebsd /dev/da1
sudo newfs_msdos -F 16 -L EFI /dev/da1s1
sudo newfs -U -L rootfs /dev/da1s2

где /dev/da1 - MicroSD карта (можно проверить через camcontrol devlist)

  • Шаг 3. Перенос операционной системы FreeBSD (на сервере FreeBSD); Рапаковывался оригинальный образ, монтировались необходимые разделы и копировались на MicroSD
bash
/dev/ufs/rootfs         /               ufs     rw              1       1
/dev/msdosfs/EFI        /boot/efi       msdosfs rw,noatime      0       0
tmpfs                   /tmp            tmpfs   rw,mode=1777    0       0
  • Шаг 4. Первая загрузка (контроль через UART на Linux Manjaro)
Здесь находится полный текст загрузки FreeBSD на Banana Pi M64. Этот текст очень длинный и занимает много места. (развернуть)
WARNING: Cannot find freebsd,dts-version property, cannot check DTB compliance
Copyright (c) 1992-2023 The FreeBSD Project.
Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
        The Regents of the University of California. All rights reserved.
FreeBSD is a registered trademark of The FreeBSD Foundation.
FreeBSD 14.4-RELEASE releng/14.4-n273675-a456f852d145 GENERIC arm64
FreeBSD clang version 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd708029e0b2)
VT(simplefb): resolution 0x0
module scmi already present!
real memory  = 2147483648 (2048 MB)
avail memory = 2069479424 (1973 MB)
Starting CPU 1 (1)
Starting CPU 2 (2)
Starting CPU 3 (3)
FreeBSD/SMP: Multiprocessor System Detected: 4 CPUs
random: unblocking device.
random: entropy device external interface
kbd0 at kbdmux0
Deprecated code (to be removed in FreeBSD 15): MIDI sequencer: no longer needed or used
ofwbus0: <Open Firmware Device Tree>
simplebus0: <Flattened device tree simple bus> on ofwbus0
psci0: <ARM Power State Co-ordination Interface Driver> on ofwbus0
rtc0: <Allwinner RTC> mem 0x1f00000-0x1f003ff irq 61,62 on simplebus0
rtc0: registered as a time-of-day clock, resolution 1.000000s
ccu_a64ng0: <Allwinner A64 Clock Control Unit NG> mem 0x1c20000-0x1c203ff on simplebus0
ccu_sun8i_r0: <Allwinner SUN8I_R Clock Control Unit NG> mem 0x1f01400-0x1f014ff on simplebus0
gic0: <ARM Generic Interrupt Controller> mem 0x1c81000-0x1c81fff,0x1c82000-0x1c83fff,0x1c84000-0x1c85fff,0x1c86000-0x1c87fff irq 54 on simplebus0
gic0: pn 0x2, arch 0x2, rev 0x1, implementer 0x43b irqs 224
aw_r_intc_gicp0: <Allwinner R INTC> mem 0x1f00c00-0x1f00fff irq 63 on simplebus0
gpio0: <Allwinner GPIO/Pinmux controller> mem 0x1c20800-0x1c20bff irq 22,23,24 on simplebus0
gpiobus0: <OFW GPIO bus> on gpio0
iichb0: <Allwinner Integrated I2C Bus Controller> mem 0x1c2b000-0x1c2b3ff irq 42 on simplebus0
iicbus0: <OFW I2C bus> on iichb0
gpio1: <Allwinner GPIO/Pinmux controller> mem 0x1f02c00-0x1f02fff irq 66 on simplebus0
gpiobus1: <OFW GPIO bus> on gpio1
generic_timer0: <ARMv8 Generic Timer> irq 4,5,6,7 on ofwbus0
Timecounter "ARM MPCore Timecounter" frequency 24000000 Hz quality 1000
Event timer "ARM MPCore Eventtimer" frequency 24000000 Hz quality 1000
a10_timer0: <Allwinner timer> mem 0x1c20c00-0x1c20c2b irq 25,26 on simplebus0
Timecounter "a10_timer timer0" frequency 24000000 Hz quality 2000
aw_syscon0: <Allwinner syscon> mem 0x1c00000-0x1c00fff on simplebus0
aw_sid0: <Allwinner Secure ID Controller> mem 0x1c14000-0x1c143ff on simplebus0
mmc_pwrseq0: <MMC Simple Power sequence> on ofwbus0
mmc_pwrseq0: Node have a clocks property but no clocks named "ext_clock"
device_attach: mmc_pwrseq0 attach returned 6
awusbphy0: <Allwinner USB PHY> mem 0x1c19400-0x1c19413,0x1c1a800-0x1c1a803,0x1c1b800-0x1c1b803 on simplebus0
iichb1: <Allwinner RSB> mem 0x1f03400-0x1f037ff irq 67 on simplebus0
iicbus1: <OFW I2C bus> on iichb1
axp8xx_pmu0: <X-Powers AXP803 Power Management Unit> at addr 0x746 irq 68 on iicbus1
axp8xx_pmu0: cannot allocate resources for device
device_attach: axp8xx_pmu0 attach returned 6
mmc_pwrseq0: <MMC Simple Power sequence> on ofwbus0
mmc_pwrseq0: Node have a clocks property but no clocks named "ext_clock"
device_attach: mmc_pwrseq0 attach returned 6
axp8xx_pmu0: <X-Powers AXP803 Power Management Unit> at addr 0x746 irq 68 on iicbus1
axp8xx_pmu0: cannot allocate resources for device
device_attach: axp8xx_pmu0 attach returned 6
mmc_pwrseq0: <MMC Simple Power sequence> on ofwbus0
mmc_pwrseq0: Node have a clocks property but no clocks named "ext_clock"
device_attach: mmc_pwrseq0 attach returned 6
cpulist0: <Open Firmware CPU Group> on ofwbus0
cpu0: <Open Firmware CPU> on cpulist0
cpufreq_dt0: <Generic cpufreq driver> on cpu0
cpufreq0: <CPU frequency control> on cpu0
cpufreq_dt1: <Generic cpufreq driver> on cpu1
cpufreq1: <CPU frequency control> on cpu1
cpufreq_dt2: <Generic cpufreq driver> on cpu2
cpufreq2: <CPU frequency control> on cpu2
cpufreq_dt3: <Generic cpufreq driver> on cpu3
cpufreq3: <CPU frequency control> on cpu3
pmu0: <Performance Monitoring Unit> irq 0,1,2,3 on ofwbus0
pcm0: <simple-audio-card> on ofwbus0
a31dmac0: <Allwinner DMA controller> mem 0x1c02000-0x1c02fff irq 8 on simplebus0
aw_mmc0: <Allwinner Integrated MMC/SD controller> mem 0x1c0f000-0x1c0ffff irq 12 on simplebus0
aw_mmc1: <Allwinner Integrated MMC/SD controller> mem 0x1c10000-0x1c10fff irq 13 on simplebus0
aw_mmc2: <Allwinner Integrated MMC/SD controller> mem 0x1c11000-0x1c11fff irq 14 on simplebus0
musbotg0: <Allwinner USB DRD> mem 0x1c19000-0x1c193ff irq 17 on simplebus0
musbotg0: setting phy mode 3
usbus0: Dynamic FIFO sizing detected, assuming 16Kbytes of FIFO RAM
usbus0 on musbotg0
ehci0: <Generic EHCI Controller> mem 0x1c1a000-0x1c1a0ff irq 18 on simplebus0
usbus1: EHCI version 1.0
usbus1 on ehci0
ohci0: <Generic OHCI Controller> mem 0x1c1a400-0x1c1a4ff irq 19 on simplebus0
usbus2 on ohci0
ehci1: <Generic EHCI Controller> mem 0x1c1b000-0x1c1b0ff irq 20 on simplebus0
usbus3: EHCI version 1.0
usbus3 on ehci1
ohci1: <Generic OHCI Controller> mem 0x1c1b400-0x1c1b4ff irq 21 on simplebus0
usbus4 on ohci1
gpioc0: <GPIO controller> on gpio0
aw_wdog0: <Allwinner A31 Watchdog> mem 0x1c20ca0-0x1c20cbf irq 27 on simplebus0
i2s0: <Allwinner I2S> mem 0x1c22c00-0x1c22dff irq 33 on simplebus0
sun8icodec0: <Allwinner Codec> mem 0x1c22e00-0x1c233ff irq 34 on simplebus0
aw_thermal0: <Allwinner Thermal Sensor Controller> mem 0x1c25000-0x1c250ff irq 35 on simplebus0
uart0: <16750 or compatible> mem 0x1c28000-0x1c283ff irq 36 on simplebus0
uart0: console (115384,n,8,1)
uart1: <16750 or compatible> mem 0x1c28400-0x1c287ff irq 37 on simplebus0
iic0: <I2C generic I/O> on iicbus0
awg0: <Allwinner Gigabit Ethernet> mem 0x1c30000-0x1c3ffff irq 46 on simplebus0
miibus0: <MII bus> on awg0
rgephy0: <RTL8169S/8110S/8211 1000BASE-T media interface> PHY 0 on miibus0
rgephy0:  none, 10baseT, 10baseT-FDX, 10baseT-FDX-flow, 100baseTX, 100baseTX-FDX, 100baseTX-FDX-flow, 1000baseT, 1000baseT-master, 1000baseT-FDX, 1000baseT-FDX-master, 1000baseT-FDX-flow, 1000baseT-FDX-flow-master, auto, auto-flow
rgephy1: <RTL8169S/8110S/8211 1000BASE-T media interface> PHY 1 on miibus0
rgephy1:  none, 10baseT, 10baseT-FDX, 10baseT-FDX-flow, 100baseTX, 100baseTX-FDX, 100baseTX-FDX-flow, 1000baseT, 1000baseT-master, 1000baseT-FDX, 1000baseT-FDX-master, 1000baseT-FDX-flow, 1000baseT-FDX-flow-master, auto, auto-flow
awg0: Ethernet address: 02:ba:ec:fc:ae:fd
a64codec0: <Allwinner A64 Analog Codec> mem 0x1f015c0-0x1f015c3 on simplebus0
gpioc1: <GPIO controller> on gpio1
axp8xx_pmu0: <X-Powers AXP803 Power Management Unit> at addr 0x746 irq 68 on iicbus1
axp8xx_pmu0: cannot allocate resources for device
device_attach: axp8xx_pmu0 attach returned 6
iic1: <I2C generic I/O> on iicbus1
gpioled0: <GPIO LEDs> on ofwbus0
mmc_pwrseq0: <MMC Simple Power sequence> on ofwbus0
mmc_pwrseq0: Node have a clocks property but no clocks named "ext_clock"
device_attach: mmc_pwrseq0 attach returned 6
armv8crypto0: <AES-CBC,AES-XTS,AES-GCM>
Timecounters tick every 1.000 msec
pcm0: cpu node is missing
usbus0: 480Mbps High Speed USB v2.0
usbus1: 480Mbps High Speed USB v2.0
usbus2: 12Mbps Full Speed USB v1.0
usbus3: 480Mbps High Speed USB v2.0
usbus4: 12Mbps Full Speed USB v1.0
ugen1.1: <Generic EHCI root HUB> at usbus1
uhub0 on usbus1
uhub0: <Generic EHCI root HUB, class 9/0, rev 2.00/1.00, addr 1> on usbus1
ugen0.1: <Mentor Graphics OTG Root HUB> at usbus0
uhub1 on usbus0
uhub1: <Mentor Graphics OTG Root HUB, class 9/0, rev 2.00/1.00, addr 1> on usbus0
ugen2.1: <Generic OHCI root HUB> at usbus2
uhub2 on usbus2
uhub2: <Generic OHCI root HUB, class 9/0, rev 1.00/1.00, addr 1> on usbus2
ugen3.1: <Generic EHCI root HUB> at usbus3
uhub3 on usbus3
uhub3: <Generic EHCI root HUB, class 9/0, rev 2.00/1.00, addr 1> on usbus3
ugen4.1: <Generic OHCI root HUB> at usbus4
uhub4 on usbus4
uhub4: <Generic OHCI root HUB, class 9/0, rev 1.00/1.00, addr 1> on usbus4
CPU  0: ARM Cortex-A53 r0p4 affinity:  0
                   Cache Type = <64 byte D-cacheline,64 byte I-cacheline,VIPT ICache,64 byte ERG,64 byte CWG>
 Instruction Set Attributes 0 = <CRC32,SHA2,SHA1,AES+PMULL>
 Instruction Set Attributes 1 = <>
 Instruction Set Attributes 2 = <>
         Processor Features 0 = <AdvSIMD,FP,EL3 32,EL2 32,EL1 32,EL0 32>
         Processor Features 1 = <>
         Processor Features 2 = <>
      Memory Model Features 0 = <TGran4,TGran64,SNSMem,BigEnd,16bit ASID,1TB PA>
      Memory Model Features 1 = <8bit VMID>
      Memory Model Features 2 = <32bit CCIDX,48bit VA>
      Memory Model Features 3 = <>
      Memory Model Features 4 = <>
             Debug Features 0 = <DoubleLock,2 CTX BKPTs,4 Watchpoints,6 Breakpoints,PMUv3,Debugv8>
             Debug Features 1 = <>
         Auxiliary Features 0 = <>
         Auxiliary Features 1 = <>
AArch32 Instruction Set Attributes 5 = <CRC32,SHA2,SHA1,AES+VMULL,SEVL>
AArch32 Media and VFP Features 0 = <FPRound,FPSqrt,FPDivide,DP VFPv3+v4,SP VFPv3+v4,AdvSIMD>
AArch32 Media and VFP Features 1 = <SIMDFMAC,FPHP DP Conv,SIMDHP SP Conv,SIMDSP,SIMDInt,SIMDLS,FPDNaN,FPFtZ>
CPU  1: ARM Cortex-A53 r0p4 affinity:  1
CPU  2: ARM Cortex-A53 r0p4 affinity:  2
CPU  3: ARM Cortex-A53 r0p4 affinity:  3
Trying to mount root from ufs:/dev/ufs/rootfs [rw]...
gic0: using for IPIs
Release APs...done
TCP_ratelimit: Is now initialized
uhub2: 1 port with 1 removable, self powered
uhub4: 1 port with 1 removable, self powered
uhub1: 1 port with 1 removable, self powered
mmc0: <MMC/SD bus> on aw_mmc2
mmc0: No compatible cards found on bus
aw_mmc2: Spurious interrupt - no active request, rint: 0x00000004

mmc1: <MMC/SD bus> on aw_mmc1
mmc1: No compatible cards found on bus
aw_mmc1: Spurious interrupt - no active request, rint: 0x00000004

mmc2: <MMC/SD bus> on aw_mmc0
uhub0: 1 port with 1 removable, self powered
mmc2: Failed to set VCCQ for card at relative address 45928
mmcsd0: 63GB <SDHC NCard 1.0 SN 19302912 MFG 08/2022 by 137 0x0303> at mmc2 50.0MHz/4bit/32768-block
uhub3: 1 port with 1 removable, self powered
ugen3.2: <vendor 0x1a40 USB 2.0 Hub> at usbus3
uhub5 on uhub3
uhub5: <vendor 0x1a40 USB 2.0 Hub, class 9/0, rev 2.00/1.11, addr 2> on usbus3
Root mount waiting for: usbus3
uhub5: 4 ports with 4 removable, self powered
WARNING: / was not properly dismounted
Dual Console: Serial Primary, Video Secondary
Setting hostuuid: 30633239-3537-6162-6563-666361656600.
Setting hostid: 0x6d161e41.
Starting file system checks:
/dev/ufs/rootfs: DEFER FOR BACKGROUND CHECKING
Growing root partition to fill device
gpart: autofill: No space left on device
growfs: /dev/ufs/rootfs is not clean - run fsck.

WARNING: / was not properly dismounted
Mounting local filesystems:.
ELF ldconfig path: /lib /usr/lib /usr/lib/compat
32-bit compatibility ldconfig path: /usr/lib32
Setting hostname: generic.
Setting up harvesting: [CALLOUT],[UMA],[FS_ATIME],SWI,INTERRUPT,NET_NG,[NET_ETHER],NET_TUN,MOUSE,KEYBOARD,ATTACH,CACHED
Feeding entropy: .
lo0: link state changed to UP
awg0: link state changed to DOWN
awg0: link state changed to UP
Starting Network: lo0 awg0.
lo0: flags=1008049<UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 16384
        options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
        inet 127.0.0.1 netmask 0xff000000
        inet6 ::1 prefixlen 128
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
        groups: lo
        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
awg0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        options=8000b<RXCSUM,TXCSUM,VLAN_MTU,LINKSTATE>
        ether 02:ba:ec:fc:ae:fd
        inet6 fe80::ba:ecff:fefc:aefd%awg0 prefixlen 64 scopeid 0x1
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
        nd6 options=23<PERFORMNUD,ACCEPT_RTADV,AUTO_LINKLOCAL>
Starting devd.
Starting dhclient.
DHCPDISCOVER on awg0 to 255.255.255.255 port 67 interval 7
ip length 371 disagrees with bytes received 375.
accepting packet with data after udp payload.
DHCPOFFER from 192.168.0.1
unknown dhcp option value 0xfe
DHCPREQUEST on awg0 to 255.255.255.255 port 67
ip length 371 disagrees with bytes received 375.
accepting packet with data after udp payload.
DHCPACK from 192.168.0.1
unknown dhcp option value 0xfe
bound to 192.168.0.192 -- renewal in 21600 seconds.
route: message indicates error: File exists
add host 127.0.0.1: gateway lo0 fib 0: route already in table
route: message indicates error: File exists
add host ::1: gateway lo0 fib 0: route already in table
add net fe80::: gateway ::1
add net ff02::: gateway ::1
add net ::ffff:0.0.0.0: gateway ::1
add net ::0.0.0.0: gateway ::1
Updating motd:.
Creating and/or trimming log files.
Updating /var/run/os-release done.
Clearing /tmp (X related).
Starting syslogd.
Mounting late filesystems:.
Starting cron.
Generating RSA host key.
3072 SHA256:5lyMclFv79tB733AUJ5MwY0or+gAXvrF0Mx2qcw0n7I root@generic (RSA)
Generating ECDSA host key.
256 SHA256:ajI/iTM7hUuYdtz8Oqzh+LsU4Qfj9cyqZYQV3F5pWFg root@generic (ECDSA)
Generating ED25519 host key.
256 SHA256:VOwWyT8Mf+lORaqg+asmC9n84I5qnN0VQu1Z3DFykV8 root@generic (ED25519)
Performing sanity check on sshd configuration.
Starting sshd.
Starting background file system checks in 60 seconds.

Fri Jan  1 00:01:02 UTC 2010

FreeBSD/arm64 (generic) (ttyu0)

login:root 
Password:
Login incorrect
login: freebsd
Password:
FreeBSD 14.4-RELEASE (GENERIC) releng/14.4-n273675-a456f852d145

Welcome to FreeBSD!

Release Notes, Errata: https://www.FreeBSD.org/releases/
Security Advisories:   https://www.FreeBSD.org/security/
FreeBSD Handbook:      https://www.FreeBSD.org/handbook/
FreeBSD FAQ:           https://www.FreeBSD.org/faq/
Questions List:        https://www.FreeBSD.org/lists/questions/
FreeBSD Forums:        https://forums.FreeBSD.org/

Documents installed with the system are in the /usr/local/share/doc/freebsd/
directory, or can be installed later with:  pkg install en-freebsd-doc
For other languages, replace "en" with a language code like de or fr.

Show the version of FreeBSD installed:  freebsd-version ; uname -a
Please include that output and any error messages when posting questions.
Introduction to manual pages:  man man
FreeBSD directory layout:      man hier

To change this login announcement, see motd(5).
You can open up a new split-screen window in (n)vi with :N or :E and then
use ^w to switch between the two.
freebsd@generic:~ $

Дальнейшая работа (после успешного запуска ОС) по воспроизводимому развертыванию, для FreeBSD является простой и может быть сведена к банальному запуску скриптового файла, который установит и развернет все необходимое ПО и успешно его запустит.

Настройка I²C и работа с датчиком

Подключение датчика SCD30 по шине I²C оказалось не тривиальной задачей. Программирование интерфейса датчика Sensirion SCD30 на микрокомпьютере Banana Pi M64 под управлением FreeBSD 14.4, наглядно показывает, как сильно реальная низкоуровневая разработка на ARM-архитектурах отличается от абстрактного программирования на ПК.

Язык Go в среде FreeBSD является мощным инструментом для системного программирования и Edge AI вычислений, но он требует глубокого понимания специфики ядра ОС и физики работы аппаратных шин.

Проблемы и решения

Во-первых, попытка использовать классический последовательный API I²C (I2CSTART -> I2CWRITE -> I2CREAD -> I2CSTOP) полностью провалилась. Аппаратные контроллеры I²C (в частности, Allwinner на платах Banana Pi) на уровне драйверов ядра FreeBSD вообще не поддерживают устаревшее пошаговое ручное управление линиями. Единственный рабочий способ взаимодействия с периферией на таких платформах - это использование транзакций I2CRDWR через системный вызов ioctl.

Во-вторых, перенос кода «в лоб» из систем x86_64 (amd64) на ARM64 приводит к падению программы с ошибкой inappropriate ioctl for device (ENOTTY). Системные номера вызовов ioctl во FreeBSD (включая константу I2CRDWR) вычисляются динамически на базе размера управляющих структур C. Из-за различий в разрядности процессоров, выравнивании памяти (padding) и размерах указателей (uintptr), числовой код вызова I2CRDWR меняется (для x86_64 это 0xc010690b, а для Banana Pi на ARM64 - строго 0x80106906).

В-третьих, объединение команд записи и чтения в одну атомарную транзакцию (массив из двух сообщений nmsgs = 2 с Repeated START) приводило к тому, что датчик возвращал байты заглушки ошибки (0xDD). Датчик SCD30 аппаратно является медленным устройством с внутренним конвейером вычислений. При слишком быстрой смене направления шины контроллером ARM, он не успевает выставить данные. Решением, стало принудительное разделение транзакций на две изолированные операции (nmsgs = 1), с обязательной микропаузой в 20–25 миллисекунд (time.Sleep) между записью команды чтения и физическим вычитыванием буфера.

В-четвертых, при передаче структур в ядро через syscall.Syscall критически важно понимать внутреннее устройство типов данных самого языка Golang. Конструкция unsafe.Pointer(&msgs) берет адрес заголовка слайса Go (дескриптора из 24 байт, хранящего указатель, длину и емкость), из-за чего ядро FreeBSD читало из памяти мусор и возвращало искаженные данные. Для передачи «чистого» массива структур Си-типа в системный вызов необходимо строго передавать адрес его нулевого элемента: unsafe.Pointer(&msgs[0]).

В-пятых, для построения климатического контроллера (управления вентиляцией через GPIO), категорически, нежелательно использовать прямые триггеры (if CO2 > 1000). Из-за естественного шума датчика и колебаний воздуха реле начнет непрерывно щелкать (эффект дребезга), что сожжет обмотку вентилятора. В логику автоматизации на Go необходимо закладывать математические фильтры (например, метод Хольта для экспоненциального сглаживания тренда), программное окно гистерезиса (включение при 1000 ppm, выключение при 800 ppm) и временные защитные задержки (минимальное время работы вентилятора после старта - не менее 10 минут).

Пример кода

bash
% go version
go version go1.25.12 freebsd/arm64
  1. Функция i2cTransfer - это универсальный драйвер. Он берет массив структур msgs []iicMsg и отправляет их в ядро через системный вызов ioctl.
  2. Каждая структура iicMsg описывает один сегмент транзакции:
    • Slave - адрес устройства (SCD30).
    • Flags - направление: 0 для записи, IIC_M_RD для чтения.
    • Len - количество байт.
    • Buf - указатель на буфер (для записи - данные, для чтения - место, куда ядро запишет ответ).
  3. Ядро FreeBSD получает этот массив и выполняет физическую передачу по шине I²C.
go
func i2cTransfer(fd uintptr, msgs []iicMsg) error {
    var data iicRdwrData
    if len(msgs) > 0 {
        data.Msgs = uintptr(unsafe.Pointer(&msgs[0]))
    }
    data.Nmsgs = uint32(len(msgs))
    _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(I2CRDWR), uintptr(unsafe.Pointer(&data)))
    if errno != 0 {
        return errno
    }
    return nil
}

Парсер конечного результата

go
func parseSCD30Metrics(buf []byte) (float32, float32, float32) {
    // SCD30 возвращает 18 байт: 6 байт на каждое значение (CO2, Temp, Hum)
    // Каждое значение - 4 байта float32 + 2 байта CRC
    if len(buf) < 18 {
        return 0, 0, 0
    }
    
    // Пропускаем CRC байты (позиции 4, 5, 10, 11, 16, 17)
    co2Bits := binary.BigEndian.Uint32(buf[0:4])
    tempBits := binary.BigEndian.Uint32(buf[6:10])
    humBits := binary.BigEndian.Uint32(buf[12:16])
    
    co2 := math.Float32frombits(co2Bits)
    temp := math.Float32frombits(tempBits)
    hum := math.Float32frombits(humBits)
    
    return co2, temp, hum
}

На выходе:

bash
Таймер сработал, проверяем датчик...
Данные готовы к чтению
Сырые байты: 43 FC DA 51 28 AD 41 DD B4 15 BC 5A 42 29 DF 03 30 69 
Распарсено: CO2=505.7, T=27.7, H=42.5
[17:08:32] CO₂: 505.7 ppm | Прогноз(10м): 497.1 ppm | T: 27.7°C | H: 42.5%
Таймер сработал, проверяем датчик...
Данные готовы к чтению
Сырые байты: 43 F8 1E 30 B1 BD 41 DD B4 88 94 68 42 2B BD F5 E0 2F 
Распарсено: CO2=496.2, T=27.7, H=42.9
[17:10:32] CO₂: 496.2 ppm | Прогноз(10м): 498.7 ppm | T: 27.7°C | H: 42.9%

Проблема беспроводной связи на ARM-платформах под FreeBSD

При выборе Banana Pi M64 в качестве Edge-шлюза необходимо учитывать реалии поддержки беспроводных интерфейсов во FreeBSD 14.4. В отличие от Linux-дистрибутивов, где проприетарные драйверы для Wi-Fi/Bluetooth чипов (например, Realtek на BPI-M64) часто поставляются "из коробки", во FreeBSD поддержка современных встроенных беспроводных модулей на ARM64 ограничена.

Инженерное решение:

Вместо попыток "прикрутить" нестабильные mainline-драйверы или патчить ядро, я бы порекомендовал принять осознанное архитектурное решение, соответствующее промышленным стандартам:

  1. Основной канал связи: Гигабитный Ethernet. Промышленный шлюз должен быть подключен по кабелю. Это гарантирует стабильность, низкую задержку и отсутствие проблем с драйверами.
  2. Резервный канал (Failover): Для случаев, когда проводное подключение невозможно, используется проверенный USB Wi-Fi адаптер (свисток) с чипсетом, имеющим стабильную поддержку во FreeBSD, например, адаптеры на чипсетах RTL8188EU (urtwn) или MT7601U (run) хорошо работают во FreeBSD.
  3. Делегирование беспроводных задач: Сбор данных по Wi-Fi/Bluetooth может быть возложен не на шлюз, а на периферийные Edge-узлы, например ESP32. ESP32 отлично справляется с задачей передачи данных по Wi-Fi, а Banana Pi может выступать как стабильный, проводной агрегатор, работающий под управлением надежной ОС.

Заключение

FreeBSD - это не «серебряная пуля», способная решить любую задачу. Для десктопа или облачных микросервисов у Linux есть свои неоспоримые преимущества. Однако, когда речь идет о создании надежного, безопасного и предсказуемого Edge-шлюза на ARM-железе, FreeBSD предлагает инженеру именно то, что нужно: зрелый сетевой стек, строгую архитектуру системы, минималистичную философию и проверенную временем файловую систему UFS (с возможностью перехода на ZFS в будущем).

Да, мы столкнулись с вызовами: I²C на ARM64 потребовал глубокого погружения в системные вызовы, а загрузка с MBR вместо GPT стала осознанным выбором для совместимости с U-Boot. Но каждый из этих камней был пройден не путем "хаков", а через понимание того, как работает система.

Именно эта прозрачность и предсказуемость делают FreeBSD осознанным и профессиональным выбором для инженеров, которые ценят надежность выше модных трендов.

В следующей статье я разберу, как мы перешли от монолитного шлюза к распределенной архитектуре: автономный узел ESP32 + SCD40 + 4G-модем, отправляющий телеметрию по MQTT, и как Go-бэкенд обрабатывает этот поток с предиктивной аналитикой.

Использованные материалы

Released under the MIT License.