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