
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
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; 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
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ção | Sem lua | Apenas núcleo | Lua com biblioteca base | Lua com base de bibliotecas, corotina, tabela, string | luaL_openlibs |
---|
-O0 -g3 | 103028 | 220924 | 236124 | 262652 | 308372 |
-O1 -g3 | 74940 | 144732 | 156916 | 174452 | 213068 |
-Os -g0 | 71172 | 134228 | 145756 | 161428 | 198400 |
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 base | 4809 |
Lua com base de bibliotecas, corotina, tabela, string | 6407 |
luaL_openlibs | 12769 |
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.