
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
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; 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
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ón | Sin lua | Solo núcleo | Lua con biblioteca base | Lua con bibliotecas base, corutina, tabla, cadena | 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
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ón | RAM utilizada |
Lua con biblioteca base | 4809 |
Lua con bibliotecas base, corutina, tabla, cadena | 6407 |
luaL_openlibs | 12769 |
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.