De vez en cuando en mis proyectos, tengo que usar printf junto con un puerto serie (UART o una abstracción a través de USB que imita un puerto serie). Y, como de costumbre, pasa mucho tiempo entre sus aplicaciones y me las arreglo para olvidar por completo todos los matices que deben tenerse en cuenta para que funcione normalmente en un proyecto grande.
En este artículo, he compilado mis propios matices principales que surgen al usar printf en programas para microcontroladores, ordenados por evidencia desde la más obvia hasta la completamente no obvia.
Breve introducción
De hecho, para usar printf en programas para microcontroladores, es suficiente:
- incluir el archivo de encabezado en el código del proyecto;
- redefinir la función del sistema _write para que salga al puerto serie;
- Describa los apéndices de las llamadas al sistema que requiere el vinculador (_fork, _wait y otros);
- use printf call en el proyecto.
De hecho, no todo es tan simple.
Describa todos los talones, no solo los usados.
La presencia de un montón de enlaces vagos en el diseño del proyecto al principio es sorprendente, pero después de leer un poco, queda claro qué y por qué. En todos mis proyectos, estoy conectando este
submódulo . Por lo tanto, en el proyecto principal, redefiniré solo los métodos que necesito (solo _escribir en este caso), y el resto permanecerá sin cambios.
Es importante tener en cuenta que todos los apéndices deben ser funciones C. No C ++ (o envuelto en "C" externa). De lo contrario, el diseño fallará (recuerde el cambio de nombre durante el ensamblaje con G ++).
En _write viene 1 caracter
A pesar de que el prototipo del método _write tiene un argumento que pasa la longitud del mensaje mostrado, tiene un valor de 1 (de hecho, nosotros mismos haremos que siempre sea 1, pero más sobre eso más adelante).
int _write (int file, char *data, int len) { ... }
En Internet, a menudo puede ver
tal implementación de este método:
Implementación frecuente de la función _write int uart_putc( const char ch) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); {} USART_SendData(USART2, (uint8_t) ch); return 0; } int _write_r (struct _reent *r, int file, char * ptr, int len) { r = r; file = file; ptr = ptr; #if 0 int index; for(index=0; index<len; index++) { if (ptr[index] == '\n') { uart_putc('\r'); } uart_putc(ptr[index]); } #endif return len; }
Tal implementación tiene las siguientes desventajas:
- baja productividad;
- inseguridad de transmisión;
- incapacidad para utilizar el puerto serie para otros fines;
Bajo rendimiento
El rendimiento lento se debe al envío de bytes utilizando recursos del procesador: debe supervisar el registro de estado en lugar de utilizar el mismo DMA. Para resolver este problema, puede preparar el búfer para enviar por adelantado, y al recibir el carácter del final de la línea (o llenar el búfer) enviar. Este método requiere una memoria intermedia, pero mejora significativamente el rendimiento con el envío frecuente.
Ejemplo de implementación de _write con un búfer #include "uart.h" #include <errno.h> #include <sys/unistd.h> extern mc::uart uart_1; extern "C" { // uart. static const uint32_t buf_size = 254; static uint8_t tx_buf[buf_size] = {0}; static uint32_t buf_p = 0; static inline int _add_char (char data) { tx_buf[buf_p++] = data; if (buf_p >= buf_size) { if (uart_1.tx(tx_buf, buf_p, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } buf_p = 0; } return 0; } // Putty \r\n // . static inline int _add_endl () { if (_add_char('\r') != 0) { return -1; } if (_add_char('\n') != 0) { return -1; } 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; } int _write (int file, char *data, int len) { len = len; // . if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) { errno = EBADF; return -1; } // // \n. if (*data != '\n') { if (_add_char(*data) != 0) { return -1; } } else { if (_add_endl() != 0) { return -1; } } return 1; } }
Aquí, el objeto uart, uart_1, es responsable de enviar directamente usando dma. El objeto utiliza métodos FreeRTOS para bloquear el acceso de terceros al objeto en el momento de enviar datos desde el búfer (tomar y devolver mutex). Por lo tanto, nadie puede usar el objeto uart mientras envía desde otro hilo.
Algunos enlaces:
- _escribe aquí el código de función como parte de un proyecto real
- la interfaz de la clase uart está aquí
- implementación de la interfaz de clase uart bajo stm32f4 aquí y aquí
- instanciación de la clase uart como parte del proyecto aquí
Streaming inseguridad
Esta implementación también permanece desprotegida, ya que nadie se molesta en el flujo vecino de FreeRTOS para comenzar a enviar otra línea a printf y, por lo tanto, tritura el búfer que se está enviando actualmente (el mutex dentro del uart protege el objeto de ser utilizado en diferentes flujos, pero los datos no se transmiten a ellos ) En caso de que exista el riesgo de que se llame a printf de otro subproceso, se requiere implementar un objeto de capa que bloquee el acceso a printf por completo. En mi caso particular, solo un hilo interactúa con printf, por lo que las complicaciones adicionales solo reducirán el rendimiento (captura y liberación constante de mutex dentro de la capa).
Incapacidad para usar el puerto serie para otros fines
Dado que enviamos solo después de que se haya recibido toda la cadena (o que el búfer esté lleno), en lugar del objeto uart, puede llamar al método del convertidor a alguna interfaz de nivel superior para la posterior transferencia de paquetes (por ejemplo, entrega con una garantía de acuerdo con el protocolo de transmisión similar a los paquetes transacción modbus). Esto le permitirá usar un uart tanto para mostrar información de depuración como, por ejemplo, para la interacción del usuario con la consola de administración (si hay una disponible en el dispositivo). Será suficiente escribir un descompresor en el lado del receptor.
Por defecto, la salida flotante no funciona
Si usa newlib-nano, entonces, por defecto, printf (así como todos sus derivados como sprintf / snprintf ... y otros) no admiten la salida de valores flotantes. Esto se resuelve fácilmente agregando los siguientes indicadores de enlace al proyecto.
SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_")
Vea la lista completa de banderas
aquí .
El programa se congela en algún lugar de las entrañas de printf
Este es otro defecto en las banderas de enlace. Para que el firmware se configure con la versión deseada de la biblioteca, debe especificar explícitamente los parámetros del procesador.
SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_")
Vea la lista completa de banderas
aquí .
printf obliga al microcontrolador a tener una falla grave
Puede haber al menos dos razones:
- problemas de pila;
- problemas con _sbrk;
Problemas de pila
Este problema realmente se manifiesta cuando se usa FreeRTOS o cualquier otro sistema operativo. El problema está usando el búfer. El primer párrafo dice que en _write viene 1 byte cada uno. Para que esto suceda, debe prohibir el uso de almacenamiento en búfer en su código antes de usar printf por primera vez.
setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0);
De la descripción de la función se deduce que uno de los siguientes valores se puede establecer de la misma manera:
#define _IOFBF 0 #define _IOLBF 1 #define _IONBF 2
Sin embargo, esto puede provocar un desbordamiento de la pila de tareas (o interrupciones si de repente eres una persona muy mala que llama a printf por interrupciones).
Desde el punto de vista técnico, es posible organizar las pilas con mucho cuidado para cada flujo, pero este enfoque requiere una planificación cuidadosa y es difícil detectar los errores que conlleva. Una solución mucho más simple es recibir un byte cada uno, almacenarlo en su propio búfer y luego enviarlo en el formato requerido, analizado anteriormente.
Problemas con _sbrk
Este problema fue para mí personalmente el más implícito. ¿Y qué sabemos sobre _sbrk?
- Otro trozo que debe implementarse para admitir una parte considerable de las bibliotecas estándar;
- requerido para asignar memoria en el montón;
- utilizado por todo tipo de métodos de biblioteca como malloc, gratis.
Personalmente, en mis proyectos en el 95% de los casos, uso FreeRTOS con métodos redefinidos new / delete / malloc que usan un montón de FreeRTOS. Entonces, cuando asigno memoria, estoy seguro de que la asignación está en el montón FreeRTOS, que ocupa una cantidad predeterminada de memoria en el área bss. Puedes mirar la capa
aquí . Entonces, puramente técnico, no debería haber ningún problema. Una función simplemente no debería llamarse. Pero pensemos, si ella llama, entonces, ¿dónde intentará recuperar su memoria?
Recordemos el diseño de la RAM del proyecto "clásico" para microcontroladores:
- .data;
- .bss;
- espacio vacio
- pila inicial
En datos, tenemos los datos iniciales de los objetos globales (variables, estructuras y otros campos del proyecto global). En bss, campos globales que tienen un valor cero inicial y, con cuidado, un montón de FreeRTOS. Es solo una matriz en la memoria. con el que funcionan los métodos del archivo heap_x.c. El siguiente es el espacio vacío, después del cual (o más bien desde el final) es la pila. Porque FreeRTOS se usa en mi proyecto, luego esta pila se usa solo hasta que se inicia el planificador. Y, por lo tanto, su uso, en la mayoría de los casos, está limitado a los colobytes (de hecho, generalmente un límite de 100 bytes).
¿Pero dónde, entonces, se asigna la memoria usando _sbrk? Eche un vistazo a las variables que usa del script de enlazador.
void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ...
Ahora los encontramos en la secuencia de comandos del enlazador (mi secuencia de comandos es ligeramente diferente de la que proporciona st, sin embargo, esta parte es casi la misma allí):
__stack = ORIGIN(SRAM) + LENGTH(SRAM); __main_stack_size = 1024; __main_stack_limit = __stack - __main_stack_size; ... flash, ... .bss (NOLOAD) : ALIGN(4) { ... . = ALIGN(4); __bss_end = .; } >SRAM __heap_start = __bss_end; __heap_end = __main_stack_limit;
Es decir, utiliza memoria entre la pila (1 kb desde 0x20020000 hacia abajo con 128 kb RAM) y bss.
Entendido Pero tenía una redefinición de los métodos malloc, free y otros. Usar _sbrk después de todo no es necesario? Al final resultó que, una necesidad. Además, este método no utiliza printf, sino el método para configurar el modo de almacenamiento en búfer:
setvbuf (o más bien _malloc_r, que no se declara como una función débil en la biblioteca. A diferencia de malloc, que se puede reemplazar fácilmente).

Como estaba seguro de que no se usaba sbrk, coloqué un montón de FreeRTOS (sección bss) cerca de la pila (porque sabía con certeza que la pila se usó 10 veces menos de lo requerido).
Soluciones al problema 3:
- sangría entre bss y la pila;
- anule _malloc_r para que no se llame a _sbrk (separe un método de la biblioteca);
- reescribe sbrk a través de malloc y gratis.
Me decidí por la primera opción, ya que no fue exitoso reemplazar el _malloc_r estándar (que está dentro de libg_nano.a (lib_a-nano-mallocr.o)) (el método no se declaró como __attribute__ ((débil)), sino excluir solo una función de la bi-biblioteca No logré vincular). Realmente no quería reescribir sbrk para una llamada.
La solución final fue asignar particiones separadas en RAM para la pila inicial y _sbrk. Esto asegura que las secciones no se apilan unas encima de otras durante la fase de configuración. Dentro de sbrk también hay un cheque para salir de la sección. Tuve que hacer una pequeña corrección para que al detectar una transición al exterior, el flujo se bloqueara en un ciclo while (ya que el uso de sbrk ocurre solo en la etapa inicial de inicialización y debe procesarse en la etapa de depuración del dispositivo).
Mem.ld modificado MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1M CCM_SRAM (RW) : ORIGIN = 0x10000000, LENGTH = 64K SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 126K SBRK_HEAP (RW) : ORIGIN = 0x2001F800, LENGTH = 1K MAIN_STACK (RW) : ORIGIN = 0x2001FC00, LENGTH = 1K }
Cambios en section.ld __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP);
Puede mirar
mem.ld y
section.ld en mi proyecto sandbox
en este commit .
UPD 07/12/2019: se corrigió la lista de banderas para trabajar printf con valores flotantes. Corregí el enlace a las CMakeLists en funcionamiento con compilaciones corregidas y banderas de diseño (hubo matices con el hecho de que las banderas deberían enumerarse una por una y a través de ";", mientras que en una línea o en líneas diferentes no importa).