Freqüentemente, somos questionados sobre como o Embox difere de outros sistemas operacionais microcontrolados, por exemplo, FreeRTOS? Obviamente, é correto comparar projetos entre si. Mas os parâmetros pelos quais a comparação às vezes é oferecida, pessoalmente me deixam um pouco perplexo. Por exemplo, quanta memória a Embox precisa para funcionar? Qual é a hora de alternar entre as tarefas? A Embox suporta modbus? Neste artigo, usando o exemplo de uma pergunta sobre modbus, queremos mostrar que a diferença entre a Embox é uma abordagem diferente para o processo de desenvolvimento.
Vamos desenvolver um dispositivo que incluirá um servidor modbus. Nosso dispositivo será simples. Afinal, ele se destina apenas à demonstração do modbus, este dispositivo permitirá o controle de LEDs pelo protocolo Modbus. Para nos comunicarmos com o dispositivo, usaremos uma conexão Ethernet.
Modbus é um protocolo de comunicação aberto. É amplamente utilizado na indústria para organizar a comunicação entre dispositivos eletrônicos. Pode ser utilizado para transferência de dados via linhas de comunicação serial RS-485, RS-422, RS-232 e redes TCP / IP (Modbus TCP).
O protocolo modbus é simples o suficiente para implementar você mesmo. Mas, uma vez que qualquer nova implementação da funcionalidade pode conter bugs, vamos usar algo pronto.
Uma das implementações mais populares do protocolo modbus é o projeto libmodbus de código aberto . Nós vamos usar isso. Isso reduzirá o tempo de desenvolvimento e os erros. Ao mesmo tempo, poderemos nos concentrar na implementação da lógica de negócios, e não no estudo do protocolo.
Nosso projeto será mantido em um repositório separado . Se desejar, você pode baixar e jogar tudo sozinho.
Desenvolvimento de protótipo Linux
Vamos começar desenvolvendo um protótipo no host. Para poder usar o libmodbus como uma biblioteca, você precisa fazer o download, configurá-lo e compilá-lo.
Para este propósito, eu esbocei um 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 ..;
Na verdade, a partir dos parâmetros de configuração, usamos apenas prefixo para construir a biblioteca localmente. E como queremos usar a biblioteca não apenas no host, construiremos uma versão estática dela.
Agora precisamos de um servidor modbus. Existem exemplos no projeto libmodbus, vamos fazer nossa implementação com base em algum servidor simples.
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);
Tudo é padrão aqui. Alguns locais de interesse são as funções mb_mapping_getstates e leddrv_updatestates. Esta é exatamente a funcionalidade que nosso dispositivo implementa.
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;
}
}
Portanto, precisamos de leddrv_updatestates, que define o estado dos LEDs, e leddrv_getstates, que obtém o estado dos LEDs.
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;
}
Como queremos que nosso software funcione tanto na placa quanto no host, precisamos de diferentes implementações das funções para definir e obter o estado dos LEDs. Vamos armazenar o estado do host em um arquivo regular. Isso permitirá que o status dos LEDs seja obtido em outros processos.
Por exemplo, se quisermos verificar os estados por meio de um site, lançaremos o site e especificaremos esse arquivo como a fonte de dados.
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';
}
}
Precisamos especificar o arquivo onde o estado inicial dos LEDs será salvo. O formato do arquivo é simples. Os estados dos LEDs são listados separados por vírgulas, 1 - LED aceso e 0 - apagado. Nosso dispositivo possui 80 LEDs, mais precisamente 40 pares de LEDs. Vamos supor que por padrão os LEDs pares estarão desligados e os ímpares acesos. Conteúdo do arquivo
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
Nós iniciamos o servidor
./led-server led(000)=0 led(001)=1 ... led(078)=0 led(079)=1
Agora precisamos de um cliente para gerenciar nosso dispositivo. Tambémé muito fácil desenvolvê-lo com base em um exemplo da 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);
Lançamos o cliente. Instale 78 LED, que está desligado por padrão
./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
No servidor veremos:
... led(076)=0 led(077)=1 led(078)=1 led(079)=1 Waiting for an indication... ERROR Connection reset by peer: read
Ou seja, o LED está instalado. Vamos desligar.
./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
No servidor, veremos uma mensagem sobre a mudança:
... led(076)=0 led(077)=1 led(078)=0 led(079)=1 Waiting for an indication... ERROR Connection reset by peer: read
Vamos iniciar o servidor http. Falamos sobre o desenvolvimento de sites no artigo . Além disso, só precisamos do site para uma demonstração mais conveniente de como funciona o modbus. Portanto, não irei entrar em muitos detalhes. Vou fornecer imediatamente um 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
E deixe-me lembrá-lo que você pode começar a usar qualquer servidor http com suporte CGI. Estamos usando o servidor embutido do python. Execute com o seguinte comando:
python3 -m http.server --cgi -d .
Vamos abrir nosso site em um navegador:
Instale 78 LED usando o 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
redefina o LED 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
No site, veremos a diferença:
Na verdade, tudo, nossa biblioteca funciona muito bem no Linux.
Adaptação para Embox e execução no emulador
Biblioteca Libmodbus
Agora precisamos mover o código para Embox. vamos começar com o próprio projeto libmodbus.
É simples. Precisamos de uma descrição do 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 anotação Construir(script = "$ (EXTERNAL_MAKE)") indicamos que usamos o Makefile para trabalhar com projetos externos.
Usando anotação ConstruirArtifactPath adiciona caminhos para localizar arquivos de cabeçalho para os módulos que dependerão desta biblioteca.
E dizemos que precisamos da biblioteca fonte "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 $@
O build makefile também é simples e direto. A única coisa que observo é que usamos o compilador interno ( $ (EMBOX_GCC) ) Embox e como plataforma ( --host ) passamos aquele configurado no Embox ( $ (AUTOCONF_TARGET_TRIPLET) ).
Conectamos o projeto à Embox
Deixe-me lembrá-lo de que, para a conveniência do desenvolvimento, criamos um repositório separado. Para conectá-lo à Embox, basta informar à Embox onde está localizado o projeto externo.
Isso é feito usando o comando
make ext_conf EXT_PROJECT_PATH=<path to project>
na raiz do Embox. Por exemplo,
make ext_conf EXT_PROJECT_PATH=~/git/embox_project_modbus_iocontrol
servidor modbus
O código-fonte do servidor modbus não requer nenhuma alteração. Ou seja, estamos usando o mesmo código que desenvolvemos no host. Precisamos adicionar 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 }
Em que, com a ajuda de anotações, indicamos que este é o nosso comando, e também que depende da biblioteca libmodbus.
Também precisaremos de bibliotecas de emulação. Não vou dar Mybuild para eles, eles são triviais, apenas observe que os fontes também são usados sem alterações.
Também precisamos construir nosso sistema junto com um servidor modbus.
Adicione nossos módulos ao 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
E colocamos nosso arquivo leds.txt com status de LED no sistema de arquivos raiz. Mas como precisamos de um arquivo mutável, vamos adicionar um disco RAM e copiar nosso arquivo para esse disco. System_start.inc content:
"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",
Isso é o suficiente para executar o Embox no qemu:
./scripts/qemu/auto_qemu
Os servidores modbus e httpd iniciam automaticamente na inicialização. Vamos definir os mesmos valores usando o cliente modbus, apenas especificando o endereço do nosso 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
e correspondentemente
./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>
Vamos abrir o navegador:
Como esperado, está tudo igual. Podemos controlar o dispositivo via protocolo modbus já na Embox.
Executando em um microcontrolador
Para rodar em um microcontrolador, usaremos STM32F4-discovery. Nas capturas de tela acima das páginas do navegador, você pode ver que 80 pernas de saída são usadas, combinadas em pares, e você também pode notar que esses pares têm outras propriedades, por exemplo, você pode definir um nome, ou o par pode ser em destaque. Na verdade, o código foi retirado de um projeto real e partes desnecessárias foram removidas dele para simplificar. 80 pinos de saída foram obtidos usando CIs de registro de deslocamento adicionais.
Mas existem apenas 4 LEDs na placa de descoberta STM32F4. Seria conveniente definir o número de LEDs para não alterar o código-fonte Embox possui um mecanismo que permite parametrizar os módulos. Você precisa adicionar a opção na descrição do módulo (Mybuild)
package iocontrol.modbus.lib static module libleddrv { option number leds_quantity = 80 ... }
E será possível usar no 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
Neste caso, você pode alterar este parâmetro especificando-o no arquivo mods.conf
include iocontrol.modbus.lib.libleddrv(leds_quantity=4)
se o parâmetro não for especificado, então aquele que está definido no módulo por padrão é usado, ou seja, 80.
Também precisamos controlar as linhas reais de saída. O código é o seguinte:
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);
}
}
No arquivo mods.conf, precisamos da configuração de nossa placa. Adicionamos nossos módulos a ele:
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
Na verdade, os mesmos módulos do ARM QEMU, com exceção do driver, é claro.
Coletamos, flash, lançamos. E com a ajuda do mesmo cliente modbus, controlamos os LEDs. Basta colocar o endereço correto, e não se esqueça que temos apenas 4 LEDs na placa.
A operação da placa stm32f4-discovery pode ser vista neste breve vídeo:
descobertas
Usando este exemplo simples, tentamos mostrar qual é a principal diferença entre Embox e outros sistemas operacionais para microcontroladores. Incluindo aqueles que são compatíveis com POSIX. Afinal, basicamente pegamos um módulo pronto, desenvolvemos lógica de negócios no Linux usando vários aplicativos. E lançamos tudo em nossa plataforma de destino. Assim, simplificando e agilizando significativamente o próprio desenvolvimento.
Sim, claro, o aplicativo é de demonstração e não é complicado. O próprio protocolo modbus também pode ser implementado de forma independente. Mas, neste caso, precisaríamos entender o protocolo modbus. E nossa abordagem permite que cada especialista se concentre em sua parte. E, claro, a maioria dos problemas é resolvida no host, o que é muito mais conveniente do que desenvolver diretamente na placa.