
Нас часто спрашивают, чем Embox отличается от других ОС для микроконтроллеров, например, FreeRTOS? Сравнивать проекты между собой, конечно, правильно. Но параметры, по которым порой предлагают сравнение, лично меня повергают в легкое недоумение. Например, сколько нужно памяти для работы Embox? А какое время переключения между задачами? А в Embox поддерживается modbus? В данной статье на примере вопроса про modbus мы хотим показать, что отличием Embox является другой подход к процессу разработки.
Давайте разработаем устройство, в составе которого будет работать в том числе modbus server. Наше устройство будет простым. Ведь оно предназначено только для демонстрации modbus, Данное устройство будет позволять управлять светодиодами по протоколу Modbus. Для связи с устройством будем использовать ethernet соединение.
Modbus открытый коммуникационный протокол. Широко применяется в промышленности для организации связи между электронными устройствами. Может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232 и сети TCP/IP (Modbus TCP).
Протокол modbus достаточно простой чтобы реализовать его самостоятельно. Но поскольку любая новая реализация функциональности может содержать ошибки, давайте используем что-нибудь готовое.
Одной из самых популярных реализаций протокола modbus является открытый проект libmodbus. Его и будем использовать. Это позволит сократить время разработки и уменьшить количество ошибок. При этом мы сможем сосредоточиться на реализации бизнес логики, а не на изучении протокола.
Наш проект будем вести в отдельном репозитории. При желании все можно скачать и воспроизвести самостоятельно.
Разработка прототипа на Linux
Начнем с разработки прототипа на хосте. Для того чтобы можно было использовать libmodbus в качестве библиотеки его нужно скачать, сконфигурировать и собрать.
Для этих целей я набросал Makefile
libmodbus-$(LIBMODBUS_VER).tar.gz:
wget http://libmodbus.org/releases/libmodbus-$(LIBMODBUS_VER).tar.gz
$(BUILD_BASE)/libmodbus/lib/pkgconfig/libmodbus.pc : libmodbus-$(LIBMODBUS_VER).tar.gz
tar -xf libmodbus-$(LIBMODBUS_VER).tar.gz
cd libmodbus-$(LIBMODBUS_VER); \
./configure --prefix=$(BUILD_BASE)/libmodbus --enable-static --disable-shared; \
make install; cd ..;Собственно из параметров конфигурации мы используем только prefix чтобы собрать библиотеку локально. А поскольку мы хотим использовать библиотеку не только на хосте, соберем ее статическую версию.
Теперь нам нужен modbus сервер. В проекте libmodbus есть примеры, давайте на основе какого-нибудь простого сервера сделаем свою реализацию.
ctx = modbus_new_tcp(ip, port);
header_len = modbus_get_header_length(ctx);
query = malloc(MODBUS_TCP_MAX_ADU_LENGTH);
modbus_set_debug(ctx, TRUE);
mb_mapping = mb_mapping_wrapper_new();
if (mb_mapping == NULL) {
fprintf(stderr, "Failed to allocate the mapping: %s\n",
modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
listen_socket = modbus_tcp_listen(ctx, 1);
for (;;) {
client_socket = modbus_tcp_accept(ctx, &listen_socket);
if (-1 == client_socket) {
break;
}
for (;;) {
int query_len;
query_len = modbus_receive(ctx, query);
if (-1 == query_len) {
/* Connection closed by the client or error */
break;
}
if (query[header_len - 1] != MODBUS_TCP_SLAVE) {
continue;
}
mb_mapping_getstates(mb_mapping);
if (-1 == modbus_reply(ctx, query, query_len, mb_mapping)) {
break;
}
leddrv_updatestates(mb_mapping->tab_bits);
}
close(client_socket);
}
printf("exiting: %s\n", modbus_strerror(errno));
close(listen_socket);
mb_mapping_wrapper_free(mb_mapping);
free(query);
modbus_free(ctx);Здесь все стандартно. Пара мест, которые представляют интерес, это функции mb_mapping_getstates и leddrv_updatestates. Это как раз функционал, который и реализует наше устройство.
static modbus_mapping_t *mb_mapping_wrapper_new(void) {
modbus_mapping_t *mb_mapping;
mb_mapping = modbus_mapping_new(LEDDRV_LED_N, 0, 0, 0);
return mb_mapping;
}
static void mb_mapping_wrapper_free(modbus_mapping_t *mb_mapping) {
modbus_mapping_free(mb_mapping);
}
static void mb_mapping_getstates(modbus_mapping_t *mb_mapping) {
int i;
leddrv_getstates(mb_mapping->tab_bits);
for (i = 0; i < mb_mapping->nb_bits; i++) {
mb_mapping->tab_bits[i] = mb_mapping->tab_bits[i] ? ON : OFF;
}
}
Таким образом, нам нужны leddrv_updatestates, которая задает состояние светодиодов, и leddrv_getstates, которая получает состояние светодиодов.
static unsigned char leddrv_leds_state[LEDDRV_LED_N];
int leddrv_init(void) {
static int inited = 0;
if (inited) {
return 0;
}
inited = 1;
leddrv_ll_init();
leddrv_load_state(leddrv_leds_state);
leddrv_ll_update(leddrv_leds_state);
return 0;
}
...
int leddrv_getstates(unsigned char leds_state[LEDDRV_LED_N]) {
memcpy(leds_state, leddrv_leds_state, sizeof(leddrv_leds_state));
return 0;
}
int leddrv_updatestates(unsigned char new_leds_state[LEDDRV_LED_N]) {
memcpy(leddrv_leds_state, new_leds_state, sizeof(leddrv_leds_state));
leddrv_ll_update(leddrv_leds_state);
return 0;
}Так как мы хотим чтобы наше ПО работало и на плате и на хосте, нам понадобятся разные реализации функций установки и получения состояния светодиодов. Давайте для хоста хранить состояние в обычном файле. Это позволит получать состояние светодиодов в других процессах.
Например, если мы захотим проверить состояния через вебсайт мы запустим вебсайт и укажем в качестве источника данных этот файл.
void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {
int i;
int idx;
char buff[LEDDRV_LED_N * 2];
for (i = 0; i < LEDDRV_LED_N; i++) {
char state = !!leds_state[i];
fprintf(stderr, "led(%03d)=%d\n", i, state);
buff[i * 2] = state + '0';
buff[i * 2 + 1] = ',';
}
idx = open(LED_FILE_NAME, O_RDWR);
if (idx < 0) {
return;
}
write(idx, buff, (LEDDRV_LED_N * 2) - 1);
close(idx);
}
...
void leddrv_load_state(unsigned char leds_state[LEDDRV_LED_N]) {
int i;
int idx;
char buff[LEDDRV_LED_N * 2];
idx = open(LED_FILE_NAME, O_RDWR);
if (idx < 0) {
return;
}
read(idx, buff, (LEDDRV_LED_N * 2));
close(idx);
for (i = 0; i < LEDDRV_LED_N; i++) {
leds_state[i] = buff[i * 2] - '0';
}
}Нам нужно указать файл где будет сохранено начальное состояние светодиодов. Формат файла простой. Через запятую перечисляются состояние светодиодов, 1 — светодиод включен, а 0 -выключен. В нашем устройстве 80 светодиодов, точнее 40 пар светодиодов. Давайте предположим, что по умолчанию четные светодиоды будут выключены а нечетные включены. Содержимое файла
0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1Запускаем сервер
./led-server
led(000)=0
led(001)=1
...
led(078)=0
led(079)=1
Теперь нам нужен клиент для управления нашим устройством. Его тоже очень просто разрабатываем на базе примера из libmodbus
ctx = modbus_new_tcp(ip, port);
if (ctx == NULL) {
fprintf(stderr, "Unable to allocate libmodbus context\n");
return -1;
}
modbus_set_debug(ctx, TRUE);
modbus_set_error_recovery(ctx,
MODBUS_ERROR_RECOVERY_LINK |
MODBUS_ERROR_RECOVERY_PROTOCOL);
if (modbus_connect(ctx) == -1) {
fprintf(stderr, "Connection failed: %s\n",
modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
if (1 == modbus_write_bit(ctx, bit_n, bit_value)) {
printf("OK\n");
} else {
printf("FAILED\n");
}
/* Close the connection */
modbus_close(ctx);
modbus_free(ctx);
Запускаем клиент. Установим 78 светодиод, который по умолчанию выключен
./led-client set 78
Connecting to 127.0.0.1:1502
[00][01][00][00][00][06][FF][05][00][4E][FF][00]
Waiting for a confirmation...
<00><01><00><00><00><06><FF><05><00><4E><FF><00>
OKНа сервере увидим:
...
led(076)=0
led(077)=1
led(078)=1
led(079)=1
Waiting for an indication...
ERROR Connection reset by peer: readТо есть светодиод установлен. Давайте выключим его.
./led-client clr 78
Connecting to 127.0.0.1:1502
[00][01][00][00][00][06][FF][05][00][4E][00][00]
Waiting for a confirmation...
<00><01><00><00><00><06><FF><05><00><4E><00><00>
OKНа сервере увидим сообщение об изменении:
...
led(076)=0
led(077)=1
led(078)=0
led(079)=1
Waiting for an indication...
ERROR Connection reset by peer: readЗапустим http сервер. О разработке веб-сайтов мы рассказывали в статье. К тому же веб-сайт нам нужен только для более удобной демонстрации работы modbus. Поэтому не буду сильно вдаваться в подробности. Сразу приведу cgi скрипт:
#!/bin/bash
echo -ne "HTTP/1.1 200 OK\r\n"
echo -ne "Content-Type: application/json\r\n"
echo -ne "Connection: close\r\n"
echo -ne "\r\n"
if [ $REQUEST_METHOD = "GET" ]; then
echo "Query: $QUERY_STRING" >&2
case "$QUERY_STRING" in
"c=led_driver&a1=serialize_states")
echo [ $(cat ../emulate/conf/leds.txt) ]
;;
"c=led_driver&a1=serialize_errors")
echo [ $(printf "0, %.0s" {1..79}) 1 ]
;;
"c=led_names&a1=serialize")
echo '[ "one", "two", "WWWWWWWWWWWWWWWW", "W W W W W W W W " ]'
;;
esac
elif [ $REQUEST_METHOD = "POST" ]; then
read -n $CONTENT_LENGTH POST_DATA
echo "Posted: $POST_DATA" >&2
fiИ напомню что запустить можно с помощью любого http сервера с поддержкой CGI. Мы используем встроенный в python сервер. Запускаем следующей командой:
python3 -m http.server --cgi -d .Откроем наш сайт в браузере:

Установим 78 светодиод с помощью клиента:
./led-client -a 127.0.0.1 set 78
Connecting to 127.0.0.1:1502
[00][01][00][00][00][06][FF][05][00][4E][FF][00]
Waiting for a confirmation...
<00><01><00><00><00><06><FF><05><00><4E><FF><00>
OKсбросим 79 светодиод:
./led-client -a 127.0.0.1 clr 79
Connecting to 127.0.0.1:1502
[00][01][00][00][00][06][FF][05][00][4F][00][00]
Waiting for a confirmation...
<00><01><00><00><00><06><FF><05><00><4F><00><00>
OKНа сайте увидим разницу:

Собственно все, на Linux наша библиотека прекрасно работает.
Адаптация к Embox и запуск на эмуляторе
Библиотека libmodbus
Теперь нам нужно перенести код в Embox. начнем с самого проекта libmodbus.
Все просто. Нам нужно описание модуля (Mybuild):
package third_party.lib
@Build(script="$(EXTERNAL_MAKE)")
@BuildArtifactPath(cppflags="-I$(ROOT_DIR)/build/extbld/third_party/lib/libmodbus/install/include/modbus")
module libmodbus {
@AddPrefix("^BUILD/extbld/^MOD_PATH/install/lib")
source "libmodbus.a"
@NoRuntime depends embox.compat.posix.util.nanosleep
}Мы с помощью аннотации Build(script="$(EXTERNAL_MAKE)") указываем что используем Makefile для работы с внешними проектами.
С помощью аннотации BuildArtifactPath добавляем пути для поиска заголовочных файлов к тем модулям, которые будут зависеть от этой библиотеки.
И говорим что нам нужна библиотека source «libmodbus.a»
PKG_NAME := libmodbus
PKG_VER := 3.1.6
PKG_SOURCES := http://libmodbus.org/releases/$(PKG_NAME)-$(PKG_VER).tar.gz
PKG_MD5 := 15c84c1f7fb49502b3efaaa668cfd25e
PKG_PATCHES := accept4_disable.patch
include $(EXTBLD_LIB)
libmodbus_cflags = -UHAVE_ACCEPT4
$(CONFIGURE) :
export EMBOX_GCC_LINK=full; \
cd $(PKG_SOURCE_DIR) && ( \
CC=$(EMBOX_GCC) ./configure --host=$(AUTOCONF_TARGET_TRIPLET) \
prefix=$(PKG_INSTALL_DIR) \
CFLAGS=$(libmodbus_cflags) \
)
touch $@
$(BUILD) :
cd $(PKG_SOURCE_DIR) && ( \
$(MAKE) install MAKEFLAGS='$(EMBOX_IMPORTED_MAKEFLAGS)'; \
)
touch $@Makefile для сборки тоже простой и очевидный. Единственное, отмечу что используем внутренний компилятор ($(EMBOX_GCC) ) Embox и в качестве платформы (--host) передаем ту, которая задана в Embox ($(AUTOCONF_TARGET_TRIPLET)).
Подключаем проект к Embox
Напомню, что для удобства разработки мы создали отдельный репозиторий. Для того чтобы подключить его к Embox достаточно указать Embox где лежит внешний проект.
Делается это с помощью команды
make ext_conf EXT_PROJECT_PATH=<path to project> в корне Embox. Например,
make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrolmodbus-server
Исходный код modbus сервера не требует изменений. То есть мы используем тот же код, который разработали на хосте. Нам нужно добавить Mybuild:
package iocontrol.modbus.cmd
@AutoCmd
@Build(script="true")
@BuildDepends(third_party.lib.libmodbus)
@Cmd(name="modbus_server")
module modbus_server {
source "modbus_server.c"
@NoRuntime depends third_party.lib.libmodbus
}В котором с помощью аннотаций мы укажем что это у нас команда, а также что она зависит от библиотеки libmodbus.
Нам также понадобятся библиотеки эмуляции. Не буду приводить Mybuild для них, они тривиальны, лишь отмечу, что исходники также используются без изменени��.
Нам также нужно собрать нашу систему вместе с modbus сервером.
Добавляем наши модули в mods.conf:
include iocontrol.modbus.http_admin
include iocontrol.modbus.cmd.flash_settings
include iocontrol.modbus.cmd.led_names
include third_party.lib.libmodbus
include iocontrol.modbus.cmd.modbus_server
include iocontrol.modbus.cmd.led_driver
include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")
include iocontrol.modbus.lib.libleddrv_ll_stubА наш файл leds.txt со статусами светодиодов кладем в корневую файловую систему. Но так как нам нужен изменяемый файл, давайте добавим RAM disk и скопируем наш файл на этот диск. Содержимое system_start.inc:
"export PWD=/",
"export HOME=/",
"netmanager",
"service telnetd",
"service httpd http_admin",
"ntpdate 0.europe.pool.ntp.org",
"mkdir -v /conf",
"mount -t ramfs /dev/static_ramdisk /conf",
"cp leds.txt /conf/leds.txt",
"led_driver init",
"service modbus_server",
"tish",Этого достаточно запустим Embox на qemu:
./scripts/qemu/auto_qemumodbus и httpd сервера запускаются автоматически при старте. Установим такие же значения с помощью modbus клиента, только указав адрес нашего QEMU (10.0.2.16):
./led-client -a 10.0.2.16 set 78
Connecting to 10.0.2.16:1502
[00][01][00][00][00][06][FF][05][00][4E][FF][00]
Waiting for a confirmation...
<00><01><00><00><00><06><FF><05><00><4E><FF><00>
OKи соответственно
./led-client -a 10.0.2.16 clr 79
Connecting to 10.0.2.16:1502
[00][01][00][00][00][06][FF][05][00][4F][00][00]
Waiting for a confirmation...
<00><01><00><00><00><06><FF><05><00><4F><00><00>Откроем браузер:

Как и ожидалось все тоже самое. Мы можем управлять устройством через modbus протокол уже на Embox.
Запуск на микроконтроллере
Для запуска на микроконтроллере будем использовать STM32F4-discovery. На вышеприведенных скриншотах страниц браузера, видно что используется 80 ног вывода, объединенные в пары, и еще можно заметить что у этих пар есть другие свойства, например можно задать имя, или пара может подсвечиваться. На самом деле, код был взят из реального проекта и из него для упрощения были убраны лишние части. 80 выходных пинов было получено с помощью дополнительных микросхем сдвиговых регистров.
Но на плате STM32F4-discovery всего 4 светодиода. Было бы удобно задавать количество светодиодов, чтобы не модифицировать исходный код В Embox есть механизм позволяющий параметризировать модули. Нужно в описании модуля (Mybuild) добавить опцию
package iocontrol.modbus.lib
static module libleddrv {
option number leds_quantity = 80
...
}И можно будет использовать в коде
#ifdef __EMBOX__
#include <framework/mod/options.h>
#include <module/iocontrol/modbus/lib/libleddrv.h>
#define LEDDRV_LED_N OPTION_MODULE_GET(iocontrol__modbus__lib__libleddrv,NUMBER,leds_quantity)
#else
#define LEDDRV_LED_N 80
#endifПри этом менять этот параметр можно будет указав его в файле mods.conf
include iocontrol.modbus.lib.libleddrv(leds_quantity=4)если параметр не указывается, то используется тот который задан в модуле по умолчанию, то есть 80.
Нам нужно еще управлять реальными линиями вывода. Код следующий:
struct leddrv_pin_desc {
int gpio; /**< port */
int pin; /**< pin mask */
};
static const struct leddrv_pin_desc leds[] = {
#include <leds_config.inc>
};
void leddrv_ll_init(void) {
int i;
for (i = 0; i < LEDDRV_LED_N; i++) {
gpio_setup_mode(leds[i].gpio, leds[i].pin, GPIO_MODE_OUTPUT);
}
}
void leddrv_ll_update(unsigned char leds_state[LEDDRV_LED_N]) {
int i;
for (i = 0; i < LEDDRV_LED_N; i++) {
gpio_set(leds[i].gpio, leds[i].pin,
leds_state[i] ? GPIO_PIN_HIGH : GPIO_PIN_LOW);
}
}В файле mods.conf нам нужна конфигурация для нашей платы. К ней добавляем наши модули:
include iocontrol.modbus.http_admin
include iocontrol.modbus.cmd.flash_settings
include iocontrol.modbus.cmd.led_names
include third_party.lib.libmodbus
include iocontrol.modbus.cmd.modbus_server
include iocontrol.modbus.cmd.led_driver
include embox.service.cgi_cmd_wrapper(cmds_check=true, allowed_cmds="led_driver led_names flash_settings")
include iocontrol.modbus.lib.libleddrv(leds_quantity=4)
include iocontrol.modbus.lib.libleddrv_ll_stm32_f4_demoПо сути дела, те же модули как и для ARM QEMU, за исключением конечно драйвера.
Собираем, прошиваем, запускаем. И с помощью того же modbus клиента управляем светодиодами. Нужно только поставить прав��льный адрес, и не забыть что у нас всего 4 светодиода на плате.
Работу на плате stm32f4-discovery можно увидеть на этом коротком видео:
Выводы
На этом простом примере мы постарались показать, в чем же основное отличие Embox от других ОС для микроконтроллеров. В том числе которые имеют POSIX совместимость. Ведь мы по сути дела, взяли готовый модуль, разработали бизнес логику на Linux используя при этом несколько приложений. И запустили все это на нашей целевой платформе. Тем самым существенно упростив и ускорив саму разработку.
Да, конечно, приложение демонстрационное и не сложное. Сам протокол modbus так же можно было бы реализовать самостоятельно. Но в этом случае нам нужно было бы разбираться в протоколе modbus. А наш подход позволяет сосредоточится каждому специалисту на своей части. Ну и конечно, большинство проблем решаются на хосте, что гораздо более удобно чем разрабатывать напрямую на плате.
