Делаем контроллер для умного дома

Делаем контроллер для умного дома и не только.

В предыдущей статье я описывал разработку системы в целом. В этой я опишу разработку контроллера, который отвечает за опрос датчиков и модулей ввода-вывода. «Зачем изобретать велосипед?» — спросите вы. Во-первых, это интересно, во вторых, как ни странно, нет OpenSource решения для подобного контроллера, покрывающего как программную так и аппаратную часть. Статья ориентирована на людей немного разбирающихся как в электронике так и в embedded linux development.

Сделать контроллер, скажите вы, это же так сложно — нужно делать плату, писать софт, печатать корпус. Но в реалии всё чуть-чуть ещё сложнее, вот во что это вылилось для меня, но вы в принципе правы:
1. аппаратная часть контроллера

— выбор cpu платы для контроллера
— выбор IO контроллера
— выбор блока питания
— структурная схема контроллера
— разработка кросс платы для контроллера
— разработка плат для RS-485 модулей
— производство плат

2. софт для контроллера

— выбор системы сборки для ядра linux и rootfs
— структура разделов SD карты
— выбор бутлоадера и загрузка нужного rootfs
— изменения в device tree
— выбор системы сбора дебажных трейсов
— написание билдсистемы
— написание коммуникационного ядра
— написание mqtt гейтвея (дискретные/аналоговые точки контроллера → топики mqtt)
— написание парсера гуглтаблицы и построение конфигурационного json файла для гейтвея
— написание point monitor для доступа к точкам контроллера
— монтирование readonly файловой системы

3. корпус контроллера

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

Пару слов о аппаратной части.

Наверно только самые отчаянные сейчас берут отдельно процессор, память, флеш, контроллер питания, еще пару сот компонентов и начинают лепить это всё вместе. Остальные же пользуются плодами трудов других людей, так быстрее и проще. Нужно всего лишь открыть браузер и написать «одноплатный компьютер» и весь остаток дня провести в выборе нужного. Мне нужно было много последовательных портов и желательно что бы плата поддерживала -40°C до +85°C, поэтому выбор пал на BeagleBone Black (BBB). Также на ВВВ вся периферия выведена на два разъёма PBD по 46 пинов с шагом 2.54, что удобно для макетирования и разработки кросс платы. Кросс плата нужна что бы объединить все компоненты на одной плате, у меня это — cpu плата, блок питания, IO контроллер, и платы каналов RS485. Также именно кросс плату нужно крепить к корпусу и на ней стоят разъемы для подключения питания и кабеля RS485.

oyjo6jzy2qtidg-9ehm5zmtnqtg.jpeg

Так, с cpu платой разобрались, следующее что надо решить — нужно ли на кросс плату ставить Input/Output (IO) контроллер или можно и без него. Я его заложил на плату, и успешно им пока не пользуюсь. Единственное что он делает это откладывает старт BBB на 1с после подачи питания и обслуживает кнопку reset.

Блок питания для контроллера я взял уже готовый MeanWell NSD10–12S5, разрабатывать его для единичного девайса — бессмысленная затея, просто подобрал его по потреблению и всё. На ЖКИ не обращайте внимание, он есть на плате, но поддержку я не реализовал.

uuubhxrvgg7imxkdmtk6rjsojtu.jpeg

hdwnl2uo05wm6isu1pudumcx8zk.jpeg

Пару слов о платах каналов RS485.

На кросс плату выведены 4 последовательных интерфейса BBB. Так что туда можно поставить любой тип канала который нужно, RS485, CAN, Zigbee модуль…

Мне нужны были каналы RS485, так что я сделал только их, они с автоматическим управлением приемом/передачей и с гальванической развязкой. Почему не использовать управление приёмо-передачей с ВВВ, да потому что TI официально перестали поддерживать строб для RS485 в драйвере serial устройства. Можно найти патч к драйверу, можно дописать самому, но зачем? Сделав канал самостробирующий, можно его будет ставить на любую плату, например на RaspberyPi, где никогда и не было такой поддержки, если была, то поправьте меня. Строб для драйвера rs485 формируется на attiny10, дешево и сердито.

Возвращаемся к софту.

Выбор системы сборки для ядра linux и rootfs.

Существует несколько подобного рода систем, самые популярные это Yocto и BuildRoot. Ели вам нужно разработать большой проект, если у вас много времени и желания писать рецепты, то Yocto — ваш выбор. С помощью же BuildRoot, можно собрать всё что нужно для простого запуска платы очень и очень просто, т.к. я делаю систему на Beaglebone Black (далее ВВВ) то:

  1. почитать что написано здесь habr.com/ru/post/448638
  2. make clean
  3. make beaglebone_defconfig
  4. make


Вот и всё. Теперь всё необходимое для запуска платы, лежит в папке /buildroot/output/images.

Выглядит всё очень просто и не интересно, так что можно сделать немного посложнее:

  1. интегрировать buildroot в свою билдсистему, скачивать его скриптом, не забыв использовать стабильный тэг, а не брать последний develop
  2. написать свой defconfig и перед сборкой buildroot подкидывать его скриптом в папку /buildroot/configs, не забываем что все дефконфиги должны заканчиваться на *_defconfig, иначе buildroot его не видит
  3. копировать свой post-build.sh в board/beaglebone/post-build.sh
  4. сделать prepare скрипт, который будет делать п1, п2 и п3 за вас


Как результат buildroot будет генерировать zImage и rootfs.tar

Выбор структуры разделов SD карты:

На этом, думаю, не стоит много акцентировать внимание.
Я сделал 4 раздела BOOT / ROOT_1 / ROOT_2 / DATA.
В BOOT разделе хранится всё необходимое для начальной загрузки: MLO, barebox.bin, barebox.env, am335x-boneblack.dtb, zImage, boot.txt.

ROOT_1 и ROOT_2 содержат rootfs, выбор которого вписан в файле boot.txt (см. ниже). Все эти разделы монтируются как readonly во избежания крешев файловой системы при выключении питания. DATA содержит проектные конфиги, при изменении которых нет необходимости пересобирать код.

Такая структура разделов в будущем позволит легко написать software update компоненту. Эта компонента будет перезаписывать один из разделов ROOT_1/ROOT_2, который сейчас не используется, а потом просто менять файл boot.txt, если не нужно менять ядро.

Выбор бутлоадера.

У меня было много экспериментов с бутлоадерами для BBB. Сначала я использовал, как все, U-Boot который генерирует BuildRoot. Но мне он не понравился, может быть, конечно, это дело привычки, но мне показалось что это перебор, уж очень он тяжелый и сложно конфигурируемый. Потом, я подумал что не плохо бы было стартануть систему быстро, секунды за 2–3, и подпилив X-Loader так что бы он грузил ядро, у меня это получилось, но опять же остался вопрос с конфигурированием, да и время старта для меня не критично (система на systemd грузится сама по себе медленно, даже если удалить всё не нужное).

В конце концов я остановился на barebox, его простота мне очень понравилась, плюс на сайте есть вся документация (www.barebox.org).

Например, что-бы грузить rootfs c первого или второго раздела нужно всего лишь:

1. на boot разделе сделать файл boot.txt который экспортит переменную типа «export BOOT_NUM=Х»

2. сделать два скрипта /env/boot/sdb1 /env/boot/sdb2 в которых описать параметры загрузки, например:

echo "botting with mmcblk0p2 as rootfs..."
	global.bootm.image=/boot/zImage
	global.bootm.oftree=/boot/am335x-boneblack.dtb
	global.linux.bootargs.console="console=ttyO0,115200"
	global.linux.bootargs.debug="earlyprintk ignore_loglevel"
	global.linux.bootargs.base="root=/dev/mmcblk0p2 ro rootfstype=ext4 rootwait"


3. сделать скрипт /env/boot/sd в котором в зависимости от BOOT_NUM стартовать sdb1 или sdb2 скрипт

4. установить переменную boot.default

nv boot.default=sd
	saveenv


5. далее меняя BOOT_NUM в boot.txt мы будем загружать rootfs с первого или второго раздела, что в будущем можно использовать для software update.

Изменения в device tree.

Поскольку для связи с модулями я использую MODBUS RTU по RS485, то мне нужно было включить практически все существующие на ВВВ последовательные порты. Для этого надо заэнэйблить их в device tree, т.к. по умолчанию большинство из них выключены.

Правильно бы было сделать свой патч для файла am335x-bone-common.dtsi из пакета билдрута и применять его каждый раз перед его сборкой, но лень победила, и я просто вытащил все нужные файлы, поменял всё что нужно и сбилдил руками.

Т.к. это делается один раз, то можно и так:

1. Создаем папку с файлами необходимые для сборки:

am335x-bone-common.dtsi
	am335x-boneblack-common.dtsi
	am335x-boneblack.dts
	am33xx-clocks.dtsi
	am33xx.dtsi
	am33xx.h
	gpio.h
	omap.h
	tps65217.dtsi


2. В файле am335x-bone-common.dtsi нужно правильно настроить пины и заэнейблить драйверы портов:

uart1_pins: pinmux_uart1_pins {
		pinctrl-single,pins = <
			AM33XX_IOPAD(0x980, PIN_INPUT_PULLUP | MUX_MODE0)
			AM33XX_IOPAD(0x984, PIN_OUTPUT_PULLDOWN | MUX_MODE0)
		>;
	};
	uart2_pins: pinmux_uart2_pins {
		pinctrl-single,pins = <
			AM33XX_IOPAD(0x950, PIN_INPUT_PULLUP | MUX_MODE1)
			AM33XX_IOPAD(0x954, PIN_OUTPUT_PULLDOWN | MUX_MODE1)
		>;
	};
	uart4_pins: pinmux_uart4_pins {
		pinctrl-single,pins = <
			AM33XX_IOPAD(0x870, PIN_INPUT_PULLUP | MUX_MODE6)
			AM33XX_IOPAD(0x874, PIN_OUTPUT_PULLDOWN | MUX_MODE6)
		>;
	};
	uart5_pins: pinmux_uart5_pins {
		pinctrl-single,pins = <
			AM33XX_IOPAD(0x8C4, PIN_INPUT_PULLUP | MUX_MODE4)
			AM33XX_IOPAD(0x8C0, PIN_OUTPUT_PULLDOWN | MUX_MODE4)
		>;
	};
	&uart1 {
		pinctrl-names = "default";
		pinctrl-0 = <&uart1_pins>;
		status = "okay";
	};
	&uart2 {
		pinctrl-names = "default";
		pinctrl-0 = <&uart2_pins>;
		status = "okay";
	};
	&uart4 {
		pinctrl-names = "default";
		pinctrl-0 = <&uart4_pins>;
		status = "okay";
	};
	&uart5 {
		pinctrl-names = "default";
		pinctrl-0 = <&uart5_pins>;
		status = "okay";
	};


3. Далее немного магии, и готовый файл am335x-boneblack.dtb лежит в этом же каталоге:

a. sudo apt-get install device-tree-compiler


b. запускаем препроцессор:

cpp -Wp,-MD,am335x-boneblack.dtb.d.pre.tmp -nostdinc -Iinclude -Isrc -Itestcase-data -undef -D__DTS__ -x assembler-with-cpp -o am335x-boneblack.dtb.dts.tmp am335x-boneblack.dts


c. запускаем сам компилятор:

dtc -O dtb -o am335x-boneblack.dtb -b 0 -i src -d am335x-boneblack.dtb.d.dtc.tmp am335x-boneblack.dtb.dts.tmp


4. am335x-boneblack.dtb нужно положить на boot раздел рядом с ядром и в скрипте запуска для barebox добавить следующую строчку — »global.bootm.oftree=/boot/am335x-boneblack.dtb»

Выбор системы сбора дебажных трейсов.

Как известно систем без багов не бывает, как и анализа многопоточной системы без трейсов. Очень удобно если эти трейсы не выводятся просто в консоль, а собираются чем-то специально для этого созданным, да так что бы можно было сортировать их по процессам, применять фильтры и т.д. И я как раз знаю одну неплохую систему, которую легко собрать как под host так и под target. Это DLT, если вы никогда не слышали об этом, то это не беда, все пробелы в знаниях можно легко покрыть прочитав at.projects.genivi.org/wiki/display/PROJ/Diagnostic+Log+and+Trace.
Данная система состоит из dlt-daemon и dlt-viewer. Как и понятно из названия dlt-daemon запускается на таргете, а dlt-viewer на хосте. Плюс к этому всему, к своему бинарнику, с которого мы хотим собирать трейсы, нужно прилинковать dlt либу.

oigob-peuxj5pdvmkdr0vzu5wkc.png

В общем удобно всё, как собирать трейсы так и анализировать их, рекомендую.

Написание билдсистемы.

Зачем писать билдсистему, ведь можно всё скачать с репозиториев, сбилдить руками, собрать на основе этого rootfs и вуалля, контроллер работает. Но повторить такой трюк через месяц будет сложнее, а через два — так вообще невозможно. Опять придётся вспоминать что, куда положить, что сбилдить и как запустить. Поэтому потратив немало времени сначала, вы экономите его потом, плюс вы получаете возможность удобно билдить под host и target. Билдсистема состоит из набора скриптов, которые сначала подготавливают host для билда, скачивают сторонние компоненты, такие как buildroot, mosquitto, DLT daemon, с их репозиториев, билдят их, раскладывают по своим местам. А уж потом можно запускать билд вашего проекта. Если билд под host, сделать не сложно, то с билдом под таргет всегда нужно повозиться, и лучше что бы это делал скрипт.

Buildroot можно сконфигурировать так что бы он вызывал post-build скрипт после того как он сформирует rootfs, которая будет лежать в buildroot/output/target. Это даёт отличную возможность вам положить туда всё то, что вам нужно. И тогда, образ файловой системы уже будет содержать всё необходимое для запуска вашей системы.

Рецепт примерно такой:

  1. надо скопировать свои бинарники куда-то в buildroot/output/target, например в /opt/bin
  2. если есть конфиги, то с ними сделать то же самое, только в /opt/etc
  3. скопировать сторонние бинарники, для меня это mosquitto, DLT daemon, их либы и конфиги
  4. Что бы при загрузке контроллера система стартанула сама — нужно скопировать ваши systemd сервисы, лучше их объединить в свой target и заэнэйблить его сделав симлинку в multi-user.
  5. скопировать модифицированный fstab (зачем, расскажу позже)


После этого вам достаточно распаковать buildroot/output/images/rootfs.tar на нужный раздел SD карты и включить питание.

build git repo: https://github.com/azhigaylo/build


Написание коммуникационного ядра.

Концепция этого стара как сам modbus.

У каждого устройства ввода-вывода в modbus сети есть регистры (16 bit), доступные на чтение, чтение/запись, в которых хранятся данные и через которые идет управление этими устройствами. У контроллера, в свою очередь, есть массивы дискретных (статус и byte значение) и аналоговых точек (статус и float значение), в которых он и хранит состояние всех параметров.

Так вот, задача коммуникационного ядра проста — собрать данные с устройств ввода-дывода по протоколу modbus, замапить их на точки контроллера и предоставить доступ к этим точкам для верхнего уровня. А если вам надо чем-то управлять, то всё в другую сторону — логическое устройство (об этом позже) должно быть подписано на точку контроллера и запись в эту точку инициирует трансляцию этого параметра в физическое устройство вода-вывода.

1vzbx4smmroaleak2dy4jol9gno.jpeg

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

Также я решил разделить логические устройства на две группы:

  1. Стандартные (модули Овен дискретного ввода/вывода), для которых заранее известны номера modbus регистров с данными, и достаточно только определить точки контроллера куда сохранить эти данные.
  2. Пользовательские устройства, для них надо самостоятельно описывать маппинг modbus регистров на точки контроллера.


Из всего выше сказанного логично иметь какой-то конфигуратор для контроллера, будь то просто json конфиг или самописная тула генерирующая бинарный конфиг, подходит что угодно. У меня второй вариант, потому что были идеи писать коммуникационное ядро так, что бы его можно было легко запускать не только на линукс борде, но и на Ардуине с FreeRtos, меняя PAL уровень в софте.

В конфигураторе для каждого устройства нужно установить номер rs485 порта контроллера, адрес устройства, и точку контроллера в которую отображается статус связи с устройством, плюс для каждого стандартного устройства описываются его каналы, а для пользовательского — маппинг его регистров на точки.

p7ghcubristizrus5hzq_zlfo5i.jpeg

xrgonkcogvsn-6j2vjktiiryrlw.jpeg

Такой конфигурационный файл, содержащий все необходимые данные по конструированию modbus сети, позволяет не модифицировать исходный код для проекта если надо добавить/удалить/изменить устройства ввода-вывода, достаточно изменить параметры в конфигураторе и сохранить их в конфиг файл.

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

core git repo: https://github.com/azhigaylo/homebrain_core


Написание mqtt гейтвея.

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

o_sfw0yj5i-ptu7ohxdqdvnmw68.jpeg

Т.к. параметров у меня довольно много, то постоянно возникали путаницы в файле конфигурации гейтвея, где был прописан маппинг точек контроллера на mqtt топики гейтвея. Помогла гугл таблица, и написание csv парсера этой таблицы в конфигурационный json файл для гейтвея.

b-wtzmutugygf-_8mj5odnnrguw.jpeg

gateway git repo
parser git repo

Написание point monitor.

Иногда очень полезно посмотреть что же сейчас происходит с точками контроллера, для этого я написал небольшое приложение который подключается непосредственно к коммуникационному ядру и считывает состояние дискретных и аналоговых точек. У меня довольно-таки туго с UI так что кое-как смог накидать приложение на QML, оно со скрипом заработало, можно считать точку, можно её записать, а мне большего и не надо.

pointmonitor git repo: https://github.com/azhigaylo/pointmonitor


Монтирование readonly файловой системы.

Обычно этому мало кто уделяет внимание, и даже в продакшен проектах можно встретить устройства в которых партишен с rootfs доступен на запись. Это рано или поздно приводит к крешу любой, даже самой устойчивой файловой системы. Т.к. контроллер может быть выключен в любой время, то это лишь вопрос времени/случая когда это произойдет. Что бы минимизировать эту вероятность надо немного повозиться с fstab и перед сборкой образа rootfs, положить его туда, как было описано выше. В fstab, во-первых, надо примонтировать файловую систему как readonly, а во вторых всё что может меняться замапить в tmpfs.

Мой fstab таков, у вас он может отличатся:

/dev/root / auto ro 0 1
tmpfs /tmp tmpfs nodev,nosuid,size=50M 0 0
tmpfs /srv tmpfs nodev,size=50M 0 0
tmpfs /var/log tmpfs defaults,noatime,size=50M 0 0
tmpfs /var/tmp tmpfs defaults,noatime,size=50M 0 0
tmpfs /var/run tmpfs defaults,noatime,size=50M 0 0
tmpfs /var/lib tmpfs defaults,noatime,size=10M 0 0


Корпус контроллера.

3D принтер давно вошёл в разделы маст хэв для каждого инженера-колхозника, у меня к сожалению его нет, но он стоит на работе. Последнее время ажиотаж у других сотрудников к нему пропал, этим я и пользуюсь, печатая всё что нужно и не нужно, в этом вы могли убедится, читая мой предыдущий пост.

Рисуем в FreeCAD, генерим gcode в Cura и получаем корпус, не забыв сделать посадочные места под плату, вырезы под разъёмы и охлаждение и закладные для клипс на din рейку.

7mycy6gz9axqfvspdk_cmyz5oc8.jpeg

j5i3dvtfcb8wkpn1t0uphq7d9lg.jpeg

Ну вот и всё, теперь у нас есть плата, софт на SD карте и корпус. Берем напильник (не шучу) и соединяем всё вместе, подключаем питание, кабели RS485 и всё начинает работать. А вы говорили сложно, сложно…

© Habrahabr.ru