Incorporamos el intérprete de Lua en el proyecto para el microcontrolador (stm32)



En aplicaciones bastante grandes, una parte importante del proyecto es la lógica empresarial. Es conveniente depurar esta parte del programa en una computadora, y luego incrustarla en el proyecto para el microcontrolador, esperando que esta parte se ejecute exactamente como se pretendía sin ninguna depuración (caso ideal).

Dado que la mayoría de los programas para microcontroladores están escritos en C / C ++, para estos propósitos generalmente usan clases abstractas que proporcionan interfaces a entidades de bajo nivel (si un proyecto se escribe solo usando C, a menudo se usan estructuras de puntero de función). Este enfoque proporciona el nivel requerido de abstracción sobre el hierro, sin embargo, está plagado de la necesidad de una compilación constante del proyecto con la programación posterior de la memoria no volátil del microcontrolador con un gran archivo de firmware binario.

Sin embargo, existe otra forma: utilizar un lenguaje de secuencias de comandos que le permita depurar la lógica empresarial en tiempo real en el dispositivo o cargar secuencias de trabajo directamente desde la memoria externa, sin incluir este código en el firmware del microcontrolador.

Elegí Lua como el lenguaje de script.

¿Por qué lua?


Hay varios lenguajes de secuencias de comandos que puede incrustar en un proyecto para un microcontrolador. Unos simples como BASIC, PyMite, Pawn ... Cada uno tiene sus pros y sus contras, una discusión de los cuales no está incluida en la lista de temas discutidos en este artículo.

Brevemente sobre lo que es bueno específicamente lua - se puede encontrar en el artículo "Lua en 60 minutos" . Este artículo me inspiró mucho y, para un estudio más detallado del tema, leí la guía oficial del autor del lenguaje Robert Jeruzalimsky " Programación en Lua " (disponible en la traducción oficial al ruso).

También me gustaría mencionar el proyecto eLua. En mi caso, ya tengo una capa de software de bajo nivel lista para interactuar tanto con los periféricos del microcontrolador como con otros periféricos necesarios ubicados en la placa del dispositivo. Por lo tanto, no he considerado este proyecto (ya que se reconoce que proporciona las capas para conectar el núcleo Lua con los periféricos del microcontrolador).

Sobre el proyecto en el que se integrará Lua


Por tradición , mi proyecto de sandbox se utilizará como la calidad del campo para los experimentos (enlace al commit con el lua ya integrado con todas las mejoras necesarias que se describen a continuación).

El proyecto se basa en el microcontrolador stm32f405rgt6 con 1 MB no volátil y 192 KB de RAM (actualmente se utilizan los 2 bloques más antiguos con una capacidad total de 128 KB).

El proyecto tiene un sistema operativo en tiempo real FreeRTOS para soportar la infraestructura periférica de hardware. Toda la memoria para tareas, semáforos y otros objetos FreeRTOS se asigna estáticamente en la etapa de enlace (ubicada en el área .bss de RAM). Todas las entidades FreeRTOS (semáforos, colas, pilas de tareas, etc.) son partes de objetos globales en las áreas privadas de sus clases. Sin embargo, el montón FreeRTOS todavía está asignado para admitir las funciones malloc , free , calloc (necesarias para funciones como printf ) que se redefinen para trabajar con él. Existe una API elevada para trabajar con tarjetas MicroSD (FatFS), así como para depurar UART (115200, 8N1).

Sobre la lógica de usar Lua como parte de un proyecto


Con el fin de depurar la lógica empresarial, se supone que los comandos se enviarán a través de UART, empaquetados (como un objeto separado) en líneas terminadas (que terminan con el carácter "\ n" + 0-terminador) y enviados a la máquina lua. En caso de ejecución fallida, salida mediante printf (ya que anteriormente estuvo involucrado en el proyecto). Cuando se depura la lógica, será posible descargar el archivo de lógica de negocios final del archivo de la tarjeta microSD (no incluido en el material de este artículo). Además, con el propósito de depurar Lua, la máquina se ejecutará dentro de un hilo FreeRTOS separado (en el futuro, se asignará un hilo separado para cada script de lógica de negocios depurado en el que se ejecutará con su entorno).

Inclusión del submódulo lua en el proyecto.


El espejo oficial del proyecto en github se usará como fuente de la biblioteca lua (ya que mi proyecto también se publica allí. Puede usar las fuentes directamente desde el sitio oficial ). Dado que el proyecto tiene un sistema establecido para ensamblar submódulos como parte del proyecto, CMakeLists individuales para cada submódulo, creé un submódulo separado en el que incluí esta bifurcación y CMakeLists para mantener un estilo de ensamblaje único.

CMakeLists construye las fuentes del repositorio lua como una biblioteca estática con los siguientes indicadores de compilación de submódulos (tomados del archivo de configuración de submódulos en el proyecto 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}" 

Y las banderas de especificación del procesador utilizado (establecido en las Listas CMake raíz ):

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

Es importante tener en cuenta la necesidad de que las CMakeLists raíz especifiquen una definición que permita no usar valores dobles (ya que el microcontrolador no tiene soporte de hardware para dobles. Solo flotante):

 add_definitions(-DLUA_32BITS) 

Bueno, solo queda informar al vinculador sobre la necesidad de ensamblar esta biblioteca e incluir el resultado en el diseño del proyecto final:

Parcela CMakeLists para vincular un proyecto con la 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") 

Definición de funciones para trabajar con memoria.


Como Lua en sí no se ocupa de la memoria, esta responsabilidad se transfiere al usuario. Sin embargo, cuando se utiliza la biblioteca lauxlib incluida y la función luaL_newstate de esta, la función l_alloc está vinculada como un sistema de memoria. Se define de la siguiente manera:

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

Como se mencionó al principio del artículo, el proyecto ya ha anulado las funciones malloc y gratuitas , pero no hay una función realloc . Necesitamos arreglarlo.

En el mecanismo estándar para trabajar con el montón FreeRTOS, el archivo heap_4.c utilizado en el proyecto no tiene una función para cambiar el tamaño de un bloque de memoria previamente asignado. En este sentido, es necesario realizar su implementación en base a malloc y de forma gratuita .

Dado que en el futuro es posible cambiar el esquema de asignación de memoria (usando otro archivo heap_x.c), se decidió no usar los interiores del esquema actual (heap_4.c), sino hacer un complemento de nivel superior. Aunque menos efectivo.

Es importante tener en cuenta que el método realloc no solo elimina el bloque antiguo (si existía) y crea uno nuevo, sino que también mueve los datos del bloque antiguo al nuevo. Además, si el bloque antiguo tenía más datos que el nuevo, el nuevo se llena con los antiguos hasta el límite y los datos restantes se descartan.

Si no se tiene en cuenta este hecho, su máquina podrá ejecutar dicho script tres veces desde la línea " a = 3 \ n ", después de lo cual caerá en un fallo grave. El problema se puede resolver después de estudiar la imagen residual de los registros en el controlador de fallas duras, desde el cual será posible descubrir que el bloqueo ocurrió después de intentar expandir la tabla en las entrañas del código del intérprete y sus bibliotecas. Si llama a un script como " print 'test' ", el comportamiento cambiará dependiendo de cómo se ensamble el archivo de firmware (en otras palabras, el comportamiento no está definido).

Para copiar datos del bloque antiguo al nuevo, necesitamos averiguar el tamaño del bloque antiguo. FreeRTOS heap_4.c (como otros archivos que proporcionan métodos de manejo de almacenamiento dinámico) no proporciona una API para esto. Por lo tanto, tienes que terminar el tuyo. Como base, tomé la función vPortFree y reduje su funcionalidad a la siguiente forma:

Código de función 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; } 

Ahora es pequeño, escriba realloc basado en malloc , gratis y vPortGetSizeBlock :

Código de implementación de Realloc basado en malloc, gratuito y 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; } 

Agregue soporte para trabajar con stdout


Como se sabe a partir de la descripción oficial, el intérprete lua en sí no puede trabajar con E / S. Para estos fines, una de las bibliotecas estándar está conectada. Para la salida, utiliza la secuencia stdout . La función luaopen_io de la biblioteca estándar es responsable de conectarse a la secuencia. Para admitir el trabajo con stdout (a diferencia de printf ), deberá anular la función fwrite . Lo redefiní en función de las funciones descritas en el artículo anterior .

Función Fwrite
 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; } 

Sin su definición, la función de impresión en lua se ejecutará con éxito, pero no habrá salida. Además, no habrá errores en la pila Lua de la máquina (ya que formalmente la función se ejecutó con éxito).

Además de esta función, necesitaremos la función fflush (para que funcione el modo interactivo, que se discutirá más adelante). Como esta función no se puede anular, tendrá que nombrarla de forma un poco diferente. La función es una versión reducida de la función fwrite y está destinada a enviar lo que ahora está en el búfer con su posterior limpieza (sin transferencia de carro adicional).

Función 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 cadenas de un puerto serial


Para obtener cadenas para una máquina lua, decidí escribir una clase simple de uart-terminal, que:

  • recibe datos en un puerto serie byte por byte (en interrupción);
  • agrega el byte recibido a la cola, desde donde lo recibe la secuencia;
  • en una secuencia de bytes, si esto no es un avance de línea, enviado de vuelta en la forma en que llegó;
  • si ha llegado un avance de línea (' \ r '), se envían 2 bytes de retorno de carro terminal (" \ n \ r ");
  • después de enviar la respuesta, se llama al controlador del byte que llegó (objeto de diseño de línea);
  • controla la presión de la tecla eliminar caracteres (para evitar eliminar caracteres de servicio de la ventana del terminal);

Enlaces a fuentes:

  • La interfaz de clase UART está aquí ;
  • La clase base UART está aquí y aquí ;
  • clase uart_terminal aquí y aquí ;
  • creando un objeto de clase como parte del proyecto aquí .

Además, observo que para que este objeto funcione correctamente, debe asignar una prioridad a la interrupción de UART en el rango permitido para trabajar con funciones FreeRTOS desde la interrupción. De lo contrario, puede obtener interesantes errores difíciles de depurar. En el ejemplo actual, las siguientes opciones para las interrupciones se establecen en el archivo FreeRTOSConfig.h .

Configuraciones en FreeRTOSConfig.h
 #define configPRIO_BITS 4 #define configKERNEL_INTERRUPT_PRIORITY 0XF0 //   FreeRTOS API   //   0x8 - 0xF. #define configMAX_SYSCALL_INTERRUPT_PRIORITY 0x80 

En el proyecto en sí, un objeto de la clase nvic establece la prioridad de la interrupción 0x9, que se incluye en el rango válido (la clase nvic se describe aquí y aquí ).

Formación de cuerdas para una máquina Lua


Los bytes recibidos del objeto uart_terminal se transfieren a una instancia de una clase simple serial_cli, que proporciona una interfaz mínima para editar la cadena y transferirla directamente al hilo en el que se ejecuta la máquina lua (llamando a la función de devolución de llamada). Al aceptar el carácter '\ r', se llama a una función de devolución de llamada. Esta función debe copiar una línea en sí misma y "liberar" el control (ya que la recepción de nuevos bytes está bloqueada durante una llamada. Esto no es un problema con flujos correctamente priorizados y una velocidad UART suficientemente baja).

Enlaces a fuentes:

  • archivos de descripción serial_cli aquí y aquí ;
  • creando un objeto de clase como parte del proyecto aquí .

Es importante tener en cuenta que esta clase considera que una cadena de más de 255 caracteres no es válida y la descarta. Esto es intencional, porque el intérprete lua le permite ingresar construcciones línea por línea, esperando el final del bloque.

Pasar una cadena al intérprete de Lua y su ejecución.


El propio intérprete de Lua no sabe cómo aceptar el código de bloque línea por línea, y luego ejecuta todo el bloque por sí solo. Sin embargo, si instala Lua en una computadora y ejecuta el intérprete en modo interactivo, podemos ver que la ejecución es línea por línea con la notación correspondiente a medida que escribe, que el bloque aún no está completo. Como el modo interactivo es el que se proporciona en el paquete estándar, podemos ver su código. Se encuentra en el archivo lua.c. Estamos interesados ​​en la función doREPL y todo lo que usa. Para no tener una bicicleta, para obtener las funciones del modo interactivo en el proyecto, hice un puerto de este código en una clase separada, a la que llamé lua_repl por el nombre de la función original, que usa printf para imprimir información en la consola y tiene un método público add_lua_string para agregar una línea recibida del objeto de clase serial_cli descrito anteriormente.

Referencias


La clase se realiza de acuerdo con el patrón Singleton Myers, ya que no es necesario proporcionar varios modos interactivos dentro del mismo dispositivo. Un objeto de la clase lua_repl recibe datos de un objeto de la clase serial_cli aquí .

Como el proyecto ya tiene un sistema unificado para inicializar y dar servicio a objetos globales, el puntero al objeto de la clase lua_repl se pasa al objeto de la clase global player :: base aquí . En el método de inicio de un objeto de class player :: base (declarado aquí . También se llama desde main), el método init del objeto de clase lua_repl se llama con la prioridad de la tarea FreeRTOS 3 (en el proyecto, puede asignar una prioridad de tarea de 1 a 4. Donde 1 Es la prioridad más baja y 4 es la más alta). Después de una inicialización exitosa, la clase global inicia el planificador FreeRTOS y el modo interactivo comienza su trabajo.

Problemas de portabilidad


A continuación se muestra una lista de los problemas que encontré durante el puerto Lua de la máquina.

Se ejecutan 2-3 scripts de una sola línea de asignación variable, luego todo cae en una falla grave


El problema fue con el método realloc. Se requiere no solo volver a seleccionar el bloque, sino también copiar el contenido del antiguo (como se describió anteriormente).

Al intentar imprimir un valor, el intérprete cae en una falla grave


Ya era más difícil detectar el problema, pero al final logré descubrir que snprintf se usaba para imprimir. Como lua almacena valores en doble (o flotante en nuestro caso), se requiere printf (y sus derivados) con soporte de coma flotante (escribí sobre las complejidades de printf aquí ).

Requisitos para la memoria no volátil (flash)


Aquí hay algunas medidas que hice para juzgar cuánta memoria no volátil (flash) debe asignarse para integrar la máquina Lua en el proyecto. La compilación se realizó con gcc-arm-none-eabi-8-2018-q4-major. Se usó la versión de Lua 5.4. A continuación, en las mediciones, la frase "sin Lua" significa la no inclusión del intérprete y los métodos de interacción con él y sus bibliotecas, así como un objeto de la clase lua_repl en el proyecto. Todas las entidades de bajo nivel (incluidas las anulaciones para las funciones printf y fwrite ) permanecen en el proyecto. El tamaño de almacenamiento dinámico de FreeRTOS es 1024 * 25 bytes. El resto está ocupado por entidades globales del proyecto.

La tabla de resumen de resultados es la siguiente (todos los tamaños en bytes):
Opciones de compilaciónSin luaSolo núcleoLua con biblioteca baseLua con bibliotecas base, corutina, tabla, cadenaluaL_openlibs
-O0 -g3103028220924236124262652308372
-O1 -g374940144732156916174452213068
-Os -g071172134228145756161428198400

Requisitos de RAM


Dado que el consumo de RAM depende completamente de la tarea, le daré una tabla resumen de la memoria consumida inmediatamente después de encender la máquina con un conjunto diferente de bibliotecas (se muestra mediante el comando print (collectgarbage ("count") * 1024 ).
ComposiciónRAM utilizada
Lua con biblioteca base4809
Lua con bibliotecas base, corutina, tabla, cadena6407
luaL_openlibs12769

En el caso de utilizar todas las bibliotecas, el tamaño de la RAM requerida aumenta significativamente en comparación con los conjuntos anteriores. Sin embargo, su uso en una parte considerable de las aplicaciones no es necesario.

Además, también se asignan 4 kb a la pila de tareas, en la que se ejecuta la máquina Lua.

Uso adicional


Para el uso completo de la máquina en el proyecto, deberá describir mejor todas las interfaces requeridas por el código de lógica de negocios para el hardware o los objetos de servicio del proyecto. Sin embargo, este es el tema de un artículo separado.

Resumen


Este artículo describe cómo conectar una máquina Lua a un proyecto para un microcontrolador, así como lanzar un intérprete interactivo completo que le permite experimentar con la lógica empresarial directamente desde la línea de comandos del terminal. Además, los requisitos para el hardware del microcontrolador se consideraron para diferentes configuraciones de la máquina Lua.

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


All Articles