image
Нас часто спрашивают, чем 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_iocontrol

modbus-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_qemu

modbus и 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. А наш подход позволяет сосредоточится каждому специалисту на своей части. Ну и конечно, большинство проблем решаются на хосте, что гораздо более удобно чем разрабатывать напрямую на плате.