Incorporamos o intérprete Lua no projeto do microcontrolador (stm32)



Em aplicativos bastante grandes, uma parte significativa do projeto é a lógica de negócios. É conveniente depurar esta parte do programa no computador e, em seguida, incorporá-la ao projeto do microcontrolador, esperando que essa peça seja executada exatamente como foi planejada sem nenhuma depuração (caso ideal).

Como a maioria dos programas para microcontroladores é escrita em C / C ++, para esses fins, eles geralmente usam classes abstratas que fornecem interfaces para entidades de baixo nível (se um projeto é gravado apenas usando C, as estruturas de ponteiros de função são usadas com freqüência). Essa abordagem fornece o nível de abstração necessário sobre o ferro, no entanto, é repleta de necessidade de recompilação constante do projeto, seguida pela programação da memória não volátil do microcontrolador com um grande arquivo de firmware binário.

No entanto, existe outra maneira - usar uma linguagem de script que permita depurar a lógica de negócios em tempo real no próprio dispositivo ou carregar scripts de trabalho diretamente da memória externa, sem incluir esse código no firmware do microcontrolador.

Eu escolhi Lua como a linguagem de script.

Por que Lua?


Existem várias linguagens de script que você pode incorporar em um projeto para um microcontrolador. Alguns simples, do tipo BASIC, PyMite, Peão ... Cada um tem seus prós e contras, cuja discussão não está incluída na lista de problemas discutidos neste artigo.

Brevemente sobre o que é bom especificamente lua - pode ser encontrado no artigo "Lua em 60 minutos" . Este artigo me inspirou muito e, para um estudo mais detalhado da questão, li o manual oficial do autor do idioma Robert Jeruzalimsky " Programação em Lua " (disponível na tradução oficial para o russo).

Eu também gostaria de mencionar o projeto eLua. No meu caso, eu já tenho uma camada de baixo nível de software pronta para interação com os periféricos do microcontrolador e outros periféricos necessários localizados na placa do dispositivo. Portanto, não considerei esse projeto (pois é reconhecido por fornecer as próprias camadas para conectar o núcleo Lua aos periféricos do microcontrolador).

Sobre o projeto no qual Lua será incorporada


Por tradição , meu projeto sandbox será usado como a qualidade do campo para experimentos (link para o commit com a lua já integrada com todas as melhorias necessárias descritas abaixo).

O projeto é baseado no microcontrolador stm32f405rgt6 com 1 MB não volátil e 192 KB de RAM (atualmente são usados ​​os 2 blocos mais antigos com uma capacidade total de 128 KB).

O projeto possui um sistema operacional FreeRTOS em tempo real para suportar a infraestrutura periférica de hardware. Toda a memória para tarefas, semáforos e outros objetos do FreeRTOS é alocada estaticamente no estágio de vinculação (localizado na área .bss da RAM). Todas as entidades do FreeRTOS (semáforos, filas, pilhas de tarefas etc.) fazem parte de objetos globais nas áreas particulares de suas classes. No entanto, o heap do FreeRTOS ainda é alocado para dar suporte às funções malloc , free , calloc (necessárias para funções como printf ) que são redefinidas para trabalhar com ele. Há uma API elevada para trabalhar com cartões MicroSD (FatFS), bem como para depurar UART (115200, 8N1).

Sobre a lógica de usar Lua como parte de um projeto


Para fins de depuração da lógica de negócios, assume-se que os comandos serão enviados via UART, empacotados (como um objeto separado) em linhas finalizadas (terminando com o caractere "\ n" + 0-terminador) e enviados para a máquina lua. Em caso de execução malsucedida, imprima por meio de printf (já que estava envolvido anteriormente no projeto). Quando a lógica é depurada, será possível fazer o download do arquivo final da lógica comercial do arquivo do cartão microSD (não incluído no material deste artigo). Além disso, com a finalidade de depurar Lua, a máquina será executada dentro de um thread do FreeRTOS separado (no futuro, um thread separado será alocado para cada script de lógica de negócios depurado no qual será executado com seu ambiente).

Inclusão do submódulo lua no projeto


O espelho oficial do projeto no github será usado como fonte da biblioteca lua (já que meu projeto também está publicado lá. Você pode usar as fontes diretamente do site oficial ). Como o projeto possui um sistema estabelecido para montar submódulos como parte do projeto, CMakeLists individuais para cada submódulo, criei um submódulo separado no qual incluí esse fork e o CMakeLists para manter um estilo de construção único.

CMakeLists cria as fontes do repositório lua como uma biblioteca estática com os seguintes sinalizadores de compilação do submodulo (extraídos do arquivo de configuração do submodule no projeto principal):

SET(C_COMPILER_FLAGS "-std=gnu99;-fshort-enums;-fno-exceptions;-Wno-type-limits;-ffunction-sections;-fdata-sections;") SET(MODULE_LUA_COMP_FLAGS "-O0;-g3;${C_COMPILER_FLAGS}" 

E sinalizadores de especificação do processador usado (definido na raiz CMakeLists ):

 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) 

É importante observar a necessidade de o CMakeLists raiz especificar uma definição que permita não usar valores duplos (já que o microcontrolador não possui suporte de hardware para double. Only float):

 add_definitions(-DLUA_32BITS) 

Bem, resta apenas informar o vinculador sobre a necessidade de montar esta biblioteca e incluir o resultado no layout do projeto final:

Gráfico de CMakeLists para vincular um projeto à biblioteca lua
 add_subdirectory(${CMAKE_SOURCE_DIR}/bsp/submodules/module_lua) ... target_link_libraries(${PROJECT_NAME}.elf PUBLIC # -Wl,--start-group       #      . #  Lua    ,      #  . "-Wl,--start-group" ..._... MODULE_LUA ..._... "-Wl,--end-group") 

Definindo funções para trabalhar com memória


Como o próprio Lua não lida com memória, essa responsabilidade é transferida para o usuário. No entanto, ao usar a biblioteca empacotada lauxlib e a função luaL_newstate , a função l_alloc é vinculada como um sistema de memória. É definido da seguinte forma:

 static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { (void)ud; (void)osize; /* not used */ if (nsize == 0) { free(ptr); return NULL; } else return realloc(ptr, nsize); } 

Conforme mencionado no início do artigo, o projeto já substituiu as funções malloc e free , mas não há função realloc . Precisamos consertar isso.

No mecanismo padrão para trabalhar com o heap do FreeRTOS, o arquivo heap_4.c usado no projeto não possui uma função para redimensionar um bloco de memória alocado anteriormente. A este respeito, é necessário fazer a sua implementação com base em malloc e livre .

Como no futuro é possível alterar o esquema de alocação de memória (usando outro arquivo heap_x.c), foi decidido não usar os interiores do esquema atual (heap_4.c), mas criar um suplemento de nível superior. Embora menos eficaz.

É importante observar que o método realloc não apenas exclui o bloco antigo (se existir) e cria um novo, mas também move os dados do bloco antigo para o novo. Além disso, se o bloco antigo tiver mais dados que o novo, o novo será preenchido com os antigos até o limite e os dados restantes serão descartados.

Se esse fato não for levado em consideração, sua máquina poderá executar esse script três vezes a partir da linha " a = 3 \ n ", após o que ocorrerá uma falha grave. O problema pode ser resolvido após o estudo da imagem residual dos registros no manipulador de falhas graves, a partir da qual será possível descobrir que a falha ocorreu após tentar expandir a tabela nas entranhas do código do interpretador e de suas bibliotecas. Se você chamar um script como " print 'test' ", o comportamento mudará dependendo de como o arquivo de firmware é montado (em outras palavras, o comportamento é indefinido).

Para copiar dados do bloco antigo para o novo, precisamos descobrir o tamanho do bloco antigo. O FreeRTOS heap_4.c (como outros arquivos que fornecem métodos de manipulação de heap) não fornece uma API para isso. Portanto, você tem que terminar o seu. Como base, peguei a função vPortFree e reduzi sua funcionalidade da seguinte forma:

Código da função VPortGetSizeBlock
 int vPortGetSizeBlock (void *pv) { uint8_t *puc = (uint8_t *)pv; BlockLink_t *pxLink; if (pv != NULL) { puc -= xHeapStructSize; pxLink = (BlockLink_t *)puc; configASSERT((pxLink->xBlockSize & xBlockAllocatedBit) != 0); configASSERT(pxLink->pxNextFreeBlock == NULL); return pxLink->xBlockSize & ~xBlockAllocatedBit; } return 0; } 

Agora é pequeno, escreva realloc com base em malloc , free e vPortGetSizeBlock :

Código de implementação Realloc baseado em malloc, free e vPortGetSizeBlock
 void *realloc (void *ptr, size_t new_size) { if (ptr == nullptr) { return malloc(new_size); } void* p = malloc(new_size); if (p == nullptr) { return p; } size_t old_size = vPortGetSizeBlock(ptr); size_t cpy_len = (new_size < old_size)?new_size:old_size; memcpy(p, ptr, cpy_len); free(ptr); return p; } 

Adicione suporte para trabalhar com stdout


Como é conhecido pela descrição oficial, o próprio interpretador lua não pode trabalhar com E / S. Para esses fins, uma das bibliotecas padrão está conectada. Para saída, ele usa o fluxo stdout . A função luaopen_io da biblioteca padrão é responsável pela conexão com o fluxo. Para dar suporte ao trabalho com stdout (diferente do printf ), você precisará substituir a função fwrite . Eu o redefini com base nas funções descritas no artigo anterior .

Função de escrita
 size_t fwrite(const void *buf, size_t size, size_t count, FILE *stream) { stream = stream; size_t len = size * count; const char *s = reinterpret_cast<const char*>(buf); for (size_t i = 0; i < len; i++) { if (_write_char((s[i])) != 0) { return -1; } } return len; } 

Sem sua definição, a função de impressão em lua será executada com êxito, mas não haverá saída. Além disso, não haverá erros na pilha Lua da máquina (uma vez que formalmente a função foi executada com sucesso).

Além dessa função, precisaremos da função fflush (para que o modo interativo funcione, que será discutido posteriormente). Como essa função não pode ser substituída, você precisará nomeá-la um pouco diferente. A função é uma versão simplificada da função fwrite e destina-se a enviar o que está agora no buffer com sua limpeza subsequente (sem transferência adicional de carro).

Função Mc_fflush
 int mc_fflush () { uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } 


Recuperando seqüências de caracteres de uma porta serial


Para obter strings para uma máquina lua, decidi escrever uma classe simples uart-terminal, que:

  • recebe dados em uma porta serial byte a byte (em interrupção);
  • adiciona o byte recebido à fila, de onde o fluxo o recebe;
  • em um fluxo de bytes, se não for um feed de linha, enviado de volta no formato em que chegou;
  • se um feed de linha chegou (' \ r '), são enviados 2 bytes de retorno de carro do terminal (" \ n \ r ");
  • depois de enviar a resposta, o manipulador do byte que chegou (objeto de layout de linha) é chamado;
  • controla o pressionamento da tecla de exclusão de caracteres (para evitar a exclusão de caracteres de serviço da janela do terminal);

Links para fontes:

  • A interface da classe UART está aqui ;
  • A classe base do UART está aqui e aqui ;
  • classe uart_terminal aqui e aqui ;
  • criando um objeto de classe como parte do projeto aqui .

Além disso, observo que, para que esse objeto funcione corretamente, você precisa atribuir uma prioridade à interrupção do UART no intervalo permitido para trabalhar com as funções do FreeRTOS a partir da interrupção. Caso contrário, você poderá obter erros interessantes de difícil depuração. No exemplo atual, as seguintes opções para interrupções são definidas no arquivo FreeRTOSConfig.h .

Configurações no FreeRTOSConfig.h
 #define configPRIO_BITS 4 #define configKERNEL_INTERRUPT_PRIORITY 0XF0 //   FreeRTOS API   //   0x8 - 0xF. #define configMAX_SYSCALL_INTERRUPT_PRIORITY 0x80 

No próprio projeto, um objeto da classe nvic define a prioridade da interrupção 0x9, incluída no intervalo válido (a classe nvic é descrita aqui e aqui ).

Formação de strings para uma máquina Lua


Os bytes recebidos do objeto uart_terminal são transferidos para uma instância de uma classe simples serial_cli, que fornece uma interface mínima para editar a string e transferi-la diretamente para o encadeamento no qual a lua-machine é executada (chamando a função de retorno de chamada). Ao aceitar o caractere '\ r', uma função de retorno de chamada é chamada. Essa função deve copiar uma linha para si mesma e "liberar" o controle (uma vez que a recepção de novos bytes é bloqueada durante uma chamada. Isso não é um problema com fluxos priorizados corretamente e com uma velocidade UART suficientemente baixa).

Links para fontes:

  • arquivos de descrição serial_cli aqui e aqui ;
  • criando um objeto de classe como parte do projeto aqui .

É importante observar que essa classe considera inválida uma string com mais de 255 caracteres e a descarta. Isso é intencional, porque o intérprete lua permite inserir construções linha por linha, aguardando o final do bloco.

Passando uma string para o interpretador Lua e sua execução


O próprio interpretador Lua não sabe como aceitar o código do bloco linha por linha e, em seguida, executa o bloco inteiro por si próprio. No entanto, se você instalar Lua em um computador e executar o interpretador no modo interativo, podemos ver que a execução é linha a linha com a notação correspondente à medida que você digita, que o bloco ainda não está completo. Como o modo interativo é o que é fornecido no pacote padrão, podemos ver seu código. Está localizado no arquivo lua.c. Estamos interessados ​​na função doREPL e em tudo que ela usa. Para não criar uma bicicleta, para obter as funções do modo interativo no projeto, criei uma porta desse código em uma classe separada, que chamei lua_repl pelo nome da função original, que usa printf para imprimir informações no console e tem um método público add_lua_string para adicionar uma linha recebida do objeto de classe serial_cli descrito acima.

Referências:


A classe é feita de acordo com o padrão singleton de Myers, já que não há necessidade de fornecer vários modos interativos no mesmo dispositivo. Um objeto da classe lua_repl recebe dados de um objeto da classe serial_cli aqui .

Como o projeto já possui um sistema unificado para inicialização e manutenção de objetos globais, o ponteiro para o objeto da classe lua_repl é passado para o objeto da classe global player :: base aqui . No método start de um objeto da classe player :: base (declarado aqui . Também é chamado de main), o método init do objeto da classe lua_repl é chamado com a prioridade da tarefa FreeRTOS 3 (no projeto, você pode atribuir a prioridade da tarefa de 1 a 4. Onde 1 É a prioridade mais baixa e 4 é a mais alta). Após uma inicialização bem-sucedida, a classe global inicia o agendador FreeRTOS e o modo interativo inicia seu trabalho.

Problemas de portabilidade


Abaixo está uma lista dos problemas que encontrei durante a porta Lua da máquina.

2-3 scripts de linha única de atribuição de variáveis ​​são executados, então tudo cai em falha grave


O problema estava com o método realloc. É necessário não apenas selecionar novamente o bloco, mas também copiar o conteúdo do antigo (conforme descrito acima).

Ao tentar imprimir um valor, o intérprete cai em falha grave


Já era mais difícil detectar o problema, mas no final consegui descobrir que o snprintf era usado para impressão. Como lua armazena valores em double (ou float no nosso caso), printf (e seus derivados) com suporte a ponto flutuante é necessário (escrevi sobre os meandros de printf aqui ).

Requisitos para memória não volátil (flash)


Aqui estão algumas medidas que eu fiz para julgar quanta memória não volátil (flash) precisa ser alocada para integrar a máquina Lua no projeto. A compilação foi realizada usando gcc-arm-none-eabi-8-2018-q4-major. A versão do Lua 5.4 foi usada. Abaixo nas medições, a frase “sem Lua” significa a não inclusão do intérprete e métodos de interação com ele e suas bibliotecas, bem como um objeto da classe lua_repl no projeto. Todas as entidades de baixo nível (incluindo substituições para as funções printf e fwrite ) permanecem no projeto. O tamanho da pilha do FreeRTOS é 1024 * 25 bytes. O restante é ocupado por entidades globais do projeto.

A tabela de resumo dos resultados é a seguinte (todos os tamanhos em bytes):
Opções de compilaçãoSem luaApenas núcleoLua com biblioteca baseLua com base de bibliotecas, corotina, tabela, stringluaL_openlibs
-O0 -g3103028220924236124262652308372
-O1 -g374940144732156916174452213068
-Os -g071172134228145756161428198400

Requisitos de RAM


Como o consumo de RAM depende inteiramente da tarefa, fornecerei uma tabela de resumo da memória consumida imediatamente após ligar a máquina com um conjunto diferente de bibliotecas (ele é exibido pela impressão (comando collectgarbage ("count") * 1024 )).
Composição:RAM usada
Lua com biblioteca base4809
Lua com base de bibliotecas, corotina, tabela, string6407
luaL_openlibs12769

No caso de usar todas as bibliotecas, o tamanho da RAM necessária aumenta significativamente em comparação com os conjuntos anteriores. No entanto, seu uso em uma parte considerável dos aplicativos não é necessário.

Além disso, 4 kb também são alocados para a pilha de tarefas, na qual a máquina Lua é executada.

Uso adicional


Para uso total da máquina no projeto, você precisará descrever melhor todas as interfaces exigidas pelo código de lógica de negócios para os objetos de hardware ou serviço do projeto. No entanto, este é o tópico de um artigo separado.

Sumário


Este artigo descreveu como conectar uma máquina Lua a um projeto para um microcontrolador, além de iniciar um intérprete interativo completo que permite experimentar a lógica de negócios diretamente da linha de comando do terminal. Além disso, os requisitos para o hardware do microcontrolador foram considerados para diferentes configurações da máquina Lua.

Source: https://habr.com/ru/post/pt459602/


All Articles