Agregue modbus a Embox RTOS y úselo en STM32 y no solo

imagen

A menudo nos preguntan en qué se diferencia Embox de otros sistemas operativos de microcontroladores, por ejemplo, FreeRTOS. Por supuesto, es correcto comparar proyectos entre sí. Pero los parámetros por los que a veces se ofrece la comparación, personalmente me sumergen en un ligero desconcierto. Por ejemplo, ¿cuánta memoria necesita Embox para ejecutarse? ¿Cuál es el momento para cambiar de tarea? ¿Embox es compatible con modbus? En este artículo, usando el ejemplo de una pregunta sobre modbus, queremos mostrar que la diferencia entre Embox es un enfoque diferente al proceso de desarrollo.



Desarrollemos un dispositivo que incluirá un servidor modbus. Nuestro dispositivo será sencillo. Después de todo, está destinado únicamente a la demostración de Modbus. Este dispositivo permitirá controlar los LED mediante el protocolo Modbus. Para comunicarnos con el dispositivo, usaremos una conexión ethernet.



Modbus es un protocolo de comunicación abierto. Es ampliamente utilizado en la industria para organizar la comunicación entre dispositivos electrónicos. Se puede utilizar para transferir datos a través de líneas de comunicación serie RS-485, RS-422, RS-232 y redes TCP / IP (Modbus TCP).



El protocolo modbus es lo suficientemente simple como para implementarlo por su cuenta. Pero dado que cualquier nueva implementación de la funcionalidad puede contener errores, usemos algo ya hecho.



Una de las implementaciones más populares del protocolo modbus es el proyecto libmodbus de código abierto . Lo usaremos. Esto reducirá el tiempo de desarrollo y reducirá los errores. Al mismo tiempo, podremos centrarnos en la implementación de la lógica empresarial y no en el estudio del protocolo.



Nuestro proyecto se mantendrá en un repositorio separado . Si lo desea, puede descargarlo y reproducirlo todo usted mismo.



Desarrollo de prototipos de Linux



Comencemos por desarrollar un prototipo en el host. Para poder usar libmodbus como biblioteca, necesita descargarlo, configurarlo y construirlo.

Para este propósito, esbocé un 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 ..;
      
      





En realidad, a partir de los parámetros de configuración, solo usamos el prefijo para construir la biblioteca localmente. Y dado que queremos usar la biblioteca no solo en el host, crearemos una versión estática de la misma.



Ahora necesitamos un servidor Modbus. Hay ejemplos en el proyecto libmodbus, hagamos nuestra implementación basada en un servidor simple.



    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);
      
      





Todo es estándar aquí. Un par de lugares de interés son las funciones mb_mapping_getstates y leddrv_updatestates. Esta es exactamente la funcionalidad que implementa nuestro dispositivo.



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;
    }
}

      
      





Por lo tanto, necesitamos leddrv_updatestates, que establece el estado de los LED, y leddrv_getstates, que obtiene el estado de los LED.




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;
}
      
      





Dado que queremos que nuestro software funcione tanto en la placa como en el host, necesitamos diferentes implementaciones de las funciones para configurar y obtener el estado de los LED. Guardemos el estado del host en un archivo normal. Esto permitirá obtener el estado de los LED en otros procesos.



Por ejemplo, si queremos verificar los estados a través de un sitio web, lo lanzaremos y especificaremos este archivo como fuente de datos.



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';
    }
}
      
      





Necesitamos especificar el archivo donde se guardará el estado inicial de los LED. El formato de archivo es simple. Los estados de los LED se enumeran separados por comas, 1 - LED está encendido y 0 - apagado. Nuestro dispositivo tiene 80 LED, más precisamente 40 pares de LED. Supongamos que, por defecto, los LED pares estarán apagados y los impares encendidos. Contenido del archivo



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
      
      







Iniciamos el servidor

./led-server
led(000)=0
led(001)=1
...
led(078)=0
led(079)=1

      
      





Ahora necesitamos un cliente para administrar nuestro dispositivo. También es muy fácil desarrollarlo basándose en un ejemplo de 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);

      
      





Lanzamos el cliente. Instale 78 LED, que está apagado por defecto



./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
      
      





En el servidor veremos:



...
led(076)=0
led(077)=1
led(078)=1
led(079)=1
Waiting for an indication...
ERROR Connection reset by peer: read
      
      





Es decir, el LED está instalado. Vamos a apagarlo.



./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
      
      





En el servidor, veremos un mensaje sobre el cambio:



...
led(076)=0
led(077)=1
led(078)=0
led(079)=1
Waiting for an indication...
ERROR Connection reset by peer: read
      
      





Iniciemos el servidor http. Hablamos sobre el desarrollo de sitios web en el artículo . Además, solo necesitamos el sitio web para una demostración más conveniente de cómo funciona Modbus. Por tanto, no entraré en muchos detalles. Daré inmediatamente un script 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
      
      





Y permítame recordarle que puede comenzar a usar cualquier servidor http con soporte CGI. Estamos usando el servidor integrado de Python. Ejecutar con el siguiente comando:



python3 -m http.server --cgi -d .
      
      





Abramos nuestro sitio web en un navegador:







Instale 78 LED usando el cliente:



./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
      
      





restablecer el 79 LED:



./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
      
      





En el sitio web, veremos la diferencia: en







realidad, todo, nuestra biblioteca funciona muy bien en Linux.



Adaptación a Embox y ejecución en el emulador



Biblioteca libmodbus



Ahora necesitamos mover el código a Embox. comencemos con el proyecto libmodbus en sí.

Es simple. Necesitamos una descripción del módulo (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
}
      
      





Estamos usando anotación Construir(script = "$ (EXTERNAL_MAKE)") indicamos que usamos el Makefile para trabajar con proyectos externos.



Usando anotación ConstruirArtifactPath agrega rutas para buscar archivos de encabezado a los módulos que dependerán de esta biblioteca.



Y decimos que necesitamos la biblioteca fuente "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 $@
      
      





El archivo MAKE de compilación también es simple y directo. Lo único que noto es que usamos el compilador interno ( $ (EMBOX_GCC) ) Embox y como plataforma ( --host ) pasamos el configurado en Embox ( $ (AUTOCONF_TARGET_TRIPLET) ).



Conectamos el proyecto a Embox



Permítanme recordarles que para la conveniencia del desarrollo, hemos creado un repositorio separado. Para conectarlo a Embox, basta con decirle a Embox dónde se encuentra el proyecto externo.



Esto se hace usando el comando



make ext_conf EXT_PROJECT_PATH=<path to project> 
      
      





en la raíz de Embox. Por ejemplo,



 make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrol
      
      





servidor modbus



El código fuente del servidor Modbus no requiere ningún cambio. Es decir, estamos usando el mismo código que desarrollamos en el host. Necesitamos agregar 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
}
      
      





En el cual, con la ayuda de anotaciones, indicamos que este es nuestro comando, y también que depende de la librería libmodbus.



También necesitaremos bibliotecas de emulación. No daré Mybuild por ellos, son triviales, solo tenga en cuenta que las fuentes también se usan sin cambios.



También necesitamos construir nuestro sistema junto con un servidor Modbus.



Agregue nuestros módulos a 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
      
      





Y colocamos nuestro archivo leds.txt con estados de LED en el sistema de archivos raíz. Pero como necesitamos un archivo mutable, agreguemos un disco RAM y copiemos nuestro archivo en ese disco. Contenido de 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",
      
      





Esto es suficiente para ejecutar Embox en qemu:



./scripts/qemu/auto_qemu
      
      





Los servidores modbus y httpd se inician automáticamente al inicio. Establezcamos los mismos valores usando el cliente modbus, solo especificando la dirección de nuestra 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
      
      





y correspondientemente



./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>
      
      





Abramos el navegador:







Como era de esperar, todo es igual. Podemos controlar el dispositivo a través del protocolo modbus ya en Embox.



Ejecutando en un microcontrolador



Para ejecutar en un microcontrolador, usaremos STM32F4-discovery. En las capturas de pantalla anteriores de las páginas del navegador, puede ver que se utilizan 80 tramos de salida, combinados en pares, y también puede observar que estos pares tienen otras propiedades, por ejemplo, puede establecer un nombre o el par puede ser resaltado. De hecho, el código se tomó de un proyecto real y se eliminaron partes innecesarias para simplificar. Se obtuvieron 80 pines de salida utilizando circuitos integrados de registro de desplazamiento adicionales.



Pero solo hay 4 LED en la placa de descubrimiento STM32F4. Sería conveniente configurar el número de LED para no modificar el código fuente, Embox tiene un mecanismo que permite parametrizar módulos. Debe agregar la opción en la descripción del módulo (Mybuild)



package iocontrol.modbus.lib

static module libleddrv {
    option number leds_quantity = 80
...
}
      
      





Y será posible usar en el código.



#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
      
      





En este caso, puede cambiar este parámetro especificándolo en el archivo mods.conf



    include  iocontrol.modbus.lib.libleddrv(leds_quantity=4)
      
      





si no se especifica el parámetro, entonces se usa el que está configurado en el módulo por defecto, es decir, 80.



También necesitamos controlar las líneas de salida reales. El código es el siguiente:



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);
    }
}
      
      





En el archivo mods.conf, necesitamos la configuración de nuestra placa. Le agregamos nuestros módulos:



    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
      
      





De hecho, los mismos módulos que para ARM QEMU, con la excepción del controlador, por supuesto.



Recopilamos, flasheamos, lanzamos. Y con la ayuda del mismo cliente modbus, controlamos los LED. Solo necesitas poner la dirección correcta, y no olvides que solo tenemos 4 LEDs en la placa.



El funcionamiento de la placa stm32f4-discovery se puede ver en este breve video:





recomendaciones



Con este sencillo ejemplo, intentamos mostrar cuál es la principal diferencia entre Embox y otros sistemas operativos para microcontroladores. Incluidos los que cumplen con POSIX. Después de todo, básicamente tomamos un módulo listo para usar, desarrollamos lógica de negocios en Linux usando varias aplicaciones. Y lo lanzamos todo en nuestra plataforma objetivo. Por lo tanto, simplifica y acelera significativamente el desarrollo en sí.



Sí, claro, la aplicación es demo y no es complicada. El propio protocolo modbus también podría implementarse de forma independiente. Pero en este caso, necesitaríamos entender el protocolo modbus. Y nuestro enfoque permite que cada especialista se concentre en su parte. Y, por supuesto, la mayoría de los problemas se resuelven en el host, lo que es mucho más conveniente que desarrollarlo directamente en la placa.



All Articles