¿Por qué es malo printf () para depurar sistemas embebidos?

16

Supongo que es malo intentar depurar un proyecto basado en microcontrolador usando printf().

Puedo entender que no tiene un lugar predefinido para la salida, y que podría consumir pines valiosos. Al mismo tiempo, he visto a personas consumir un pin UART TX para enviar al terminal IDE con una DEBUG_PRINT()macro personalizada .

tarabyte
fuente
12
¿Quién te dijo que es malo? "Usualmente no es el mejor" no es lo mismo que un "malo" no calificado.
Spehro Pefhany
66
Todo esto habla sobre la sobrecarga que hay, si todo lo que necesita hacer es enviar mensajes "Estoy aquí", no necesita printf en absoluto, solo una rutina para enviar una cadena a un UART. Eso, más el código para inicializar el UART es probablemente menos de 100 bytes de código. Agregar la capacidad de generar un par de valores hexadecimales no lo aumentará tanto.
tcrosley
77
@ChetanBhargava: los archivos de encabezado C generalmente no agregan código al ejecutable. Contienen declaraciones; si el resto del código no usa las cosas que se declaran, el código de esas cosas no se vincula. Si usa printf, por supuesto, todo el código que se necesita para implementar printfse vincula al ejecutable. Pero eso es porque el código lo usó, no por el encabezado.
Pete Becker
2
@ChetanBhargava Ni siquiera necesita incluir <stdio.h> si despliega su propia rutina simple para generar una cadena como la que describí (envíe caracteres al UART hasta que vea un '\ 0') '
tcrosley
2
@tcrosley Creo que este consejo probablemente sea discutible si tiene un buen compilador moderno, si usa printf en el caso simple sin una cadena de formato gcc y la mayoría de los demás lo reemplazan con una llamada simple más eficiente que hace mucho como usted describe.
Vality

Respuestas:

24

Puedo encontrar algunas desventajas de usar printf (). Tenga en cuenta que el "sistema integrado" puede variar desde algo con unos pocos cientos de bytes de memoria de programa a un sistema QNX RTOS montado en bastidor con gigabytes de RAM y terabytes de memoria no volátil.

  • Requiere un lugar para enviar los datos. Tal vez ya tenga un puerto de depuración o programación en el sistema, tal vez no. Si no lo hace (o el que tiene no funciona) no es muy útil.

  • No es una función ligera en todos los contextos. Esto podría ser un gran problema si tiene un microcontrolador con solo unos pocos K de memoria, porque el enlace en printf podría consumir 4K por sí mismo. Si tiene un microcontrolador de 32K o 256K, probablemente no sea un problema, y ​​mucho menos si tiene un gran sistema integrado.

  • Es de poca o ninguna utilidad para encontrar ciertos tipos de problemas relacionados con la asignación de memoria o las interrupciones, y puede cambiar el comportamiento del programa cuando se incluyen o no declaraciones.

  • Es bastante inútil para lidiar con cosas sensibles al tiempo. Sería mejor con un analizador lógico y un osciloscopio o un analizador de protocolos, o incluso un simulador.

  • Si tiene un gran programa y tiene que volver a compilar muchas veces a medida que cambia las declaraciones de printf y las cambia, podría perder mucho tiempo.

Para qué es bueno, es una forma rápida de generar datos de forma preformateada que todo programador de C sabe usar: curva de aprendizaje cero. Si necesita escupir una matriz para el filtro de Kalman que está depurando, puede ser bueno escupirlo en un formato que MATLAB pueda leer. Ciertamente mejor que mirar las ubicaciones de RAM una a la vez en un depurador o emulador .

No creo que sea una flecha inútil en el carcaj, pero debe usarse con moderación, junto con gdb u otros depuradores, emuladores, analizadores lógicos, osciloscopios, herramientas de análisis de código estático, herramientas de cobertura de código, etc.

Spehro Pefhany
fuente
3
La mayoría de las printf()implementaciones no son seguras para subprocesos (es decir, no son reentrantes), lo que no es un factor decisivo, sino algo a tener en cuenta al usarlo en un entorno de subprocesos múltiples.
JRobert
1
@JRobert saca a relucir un buen punto ... e incluso en un entorno sin un sistema operativo, es difícil hacer una depuración directa muy útil de los ISR. Por supuesto, si está haciendo printf () o matemática de punto flotante en un ISR, el enfoque probablemente esté desactivado.
Spehro Pefhany
@JRobert ¿Qué herramientas de depuración tienen los desarrolladores de software que trabajan en un entorno de subprocesos múltiples (en una configuración de hardware donde el uso de analizadores lógicos y osciloscopios no es práctico)?
Minh Tran
1
En el pasado, hice mi propio printf () seguro para subprocesos; usó el equivalente descalzo put () o putchar () para escupir datos muy concisos a un terminal; almacenó datos binarios en una matriz que volqué e interpreté después de la ejecución de la prueba; usó un puerto de E / S para parpadear un LED o generar pulsos para realizar mediciones de tiempo con un osciloscopio; escupe un número a un D / A y se mide con VOM ... ¡La lista es tan larga como tu imaginación e inversamente tan grande como tu presupuesto! :)
JRobert
19

Además de algunas otras buenas respuestas, el acto de enviar datos a un puerto a velocidades de transmisión en serie puede ser francamente lento con respecto a su tiempo de bucle y tener un impacto en la forma en que funciona el resto del programa (al igual que CUALQUIER depuración proceso).

Como otras personas le han estado diciendo, no hay nada "malo" en usar esta técnica, pero tiene, como muchas otras técnicas de depuración, sus limitaciones. Siempre que sepa y pueda lidiar con estas limitaciones, puede ser una ayuda extremadamente conveniente ayudarlo a obtener su código correcto.

Los sistemas integrados tienen una cierta opacidad que, en general, hace que la depuración sea un problema.

Scott Seidman
fuente
8
+1 para "los sistemas integrados tienen una cierta opacidad". Aunque me temo que esta declaración solo puede ser comprensible para aquellos que tienen una experiencia decente trabajando con incrustado, es un resumen agradable y conciso de la situación. Está cerca de una definición de "incrustado", en realidad.
njahnke
5

Hay dos problemas principales con los que se encontrará tratando de usar printfen un microcontrolador.

Primero, puede ser una molestia canalizar la salida al puerto correcto. No siempre. Pero algunas plataformas son más difíciles que otras. Algunos de los archivos de configuración pueden estar mal documentados y puede ser necesaria mucha experimentación.

El segundo es la memoria. Una printfbiblioteca completa puede ser GRANDE. Sin embargo, a veces no necesita todos los especificadores de formato y pueden estar disponibles versiones especializadas. Por ejemplo, el stdio.h proporcionado por AVR contiene tres diferentes printfde diferentes tamaños y funcionalidades.

Dado que la implementación completa de todas las características mencionadas se vuelve bastante grande, vfprintf()se pueden seleccionar tres tipos diferentes de opciones de enlazador. El valor predeterminado vfprintf()implementa todas las funciones mencionadas, excepto las conversiones de punto flotante. vfprintf()Está disponible una versión minimizada de que solo implementa las funciones básicas de conversión de enteros y cadenas, pero solo #se puede especificar la opción adicional utilizando indicadores de conversión (estos indicadores se analizan correctamente desde la especificación de formato, pero luego simplemente se ignoran).

Tuve una instancia en la que no había una biblioteca disponible y tenía una memoria mínima. Así que no tuve más remedio que usar una macro personalizada. Pero el uso de printfo no es realmente uno de los que se ajustará a sus requisitos.

Embedded.kyle
fuente
¿Podría el downvoter explicar qué es incorrecto en mi respuesta para que pueda evitar mi error en futuros proyectos?
embedded.kyle
4

Para agregar a lo que Spehro Pefhany estaba diciendo sobre "cosas sensibles al tiempo": tomemos un ejemplo. Digamos que tiene un giroscopio desde el cual su sistema integrado está tomando 1,000 mediciones por segundo. Desea depurar estas medidas, por lo que debe imprimirlas. Problema: imprimirlos hace que el sistema esté demasiado ocupado para leer 1,000 mediciones por segundo, lo que hace que el búfer del giroscopio se desborde, lo que hace que los datos corruptos se lean (e impriman). Entonces, al imprimir los datos, los ha corrompido, haciéndole pensar que hay un error en la lectura de los datos cuando tal vez en realidad no lo hay. Un llamado heisenbug.

njahnke
fuente
jajaja ¿Es "heisenbug" realmente un término técnico? Supongo que tiene que ver con la medición del estado de las partículas y el Principio de Heisenburg ...
Zeta
3

La razón más importante para no depurar con printf () es que generalmente es ineficiente, inadecuado e innecesario.

Ineficiente: printf () y sus familiares usan mucho flash y RAM en relación con lo que está disponible en un microcontrolador pequeño, pero la mayor ineficiencia está en la depuración real. Cambiar lo que se está registrando requiere volver a compilar y reprogramar el objetivo, lo que ralentiza el proceso. También usa un UART que de otro modo podría estar usando para hacer un trabajo útil.

Inadecuado: solo hay tantos detalles que puede generar a través de un enlace en serie. Si el programa se cuelga, no sabes exactamente dónde, solo la última salida que se completó.

Innecesario: muchos microcontroladores se pueden depurar de forma remota. Se pueden usar protocolos JTAG o patentados para pausar el procesador, echar un vistazo a los registros y la RAM, e incluso alterar el estado del procesador en ejecución sin tener que volver a compilar. Esta es la razón por la cual los depuradores son generalmente una mejor manera de depurar que imprimir declaraciones, incluso en una PC con toneladas de espacio y potencia.

Es lamentable que la plataforma de microcontroladores más común para los novatos, Arduino, no tenga un depurador. El AVR admite la depuración remota, pero el protocolo debugWIRE de Atmel es propietario e indocumentado. Puede usar una placa de desarrollo oficial para depurar con GDB, pero si lo tiene, probablemente ya no esté demasiado preocupado por Arduino.

Theran
fuente
¿No podría usar punteros de función para jugar con lo que se está registrando y agregar un montón de flexibilidad?
Scott Seidman
3

printf () no funciona por sí solo. Llama a muchas otras funciones, y si tiene poco espacio de pila, es posible que no pueda usarlo para depurar problemas cercanos a su límite de pila. Dependiendo del compilador y el microcontrolador, la cadena de formato también se puede colocar en la memoria, en lugar de hacer referencia desde flash. Esto puede agregarse significativamente si sazona su código con declaraciones printf. Este es un gran problema en el entorno de Arduino: los principiantes que usan docenas o cientos de declaraciones printf de repente se encuentran con problemas aparentemente aleatorios porque están sobrescribiendo su montón con su pila.

Adam Davis
fuente
2
Si bien agradezco los comentarios que proporciona el voto negativo en sí, sería más útil para mí y para otros si los que no están de acuerdo explicaran los problemas con esta respuesta. Todos estamos aquí para aprender y compartir conocimientos, considera compartir el tuyo.
Adam Davis
3

Incluso si uno quiere escupir datos en alguna forma de consola de registro, la printffunción generalmente no es una muy buena forma de hacerlo, ya que necesita examinar la cadena de formato pasada y analizarla en tiempo de ejecución; incluso si el código nunca usa otro especificador de formato que no sea %04X, el controlador generalmente necesitará incluir todo el código que se requeriría para analizar cadenas de formato arbitrario. Dependiendo del controlador exacto que uno esté usando, puede ser mucho más eficiente usar código como:

void log_string(const char *st)
{
  int ch;
  do
  {
    ch = *st++;
    if (ch==0) break;
    log_char(ch);
  } while(1);
}
void log_hexdigit(unsigned char d)
{
  d&=15;
  if (d > 9) d+=7;
  log_char(d+'0');
}
void log_hexbyte(unsigned char b)
{ log_hexdigit(b >> 4); log_hexdigit(b); }
void log_hexi16(uint16_t s)
{ log_hexbyte(s >> 8); log_hexbyte(s); }
void log_hexi32(uint32_t i)
{ log_hexbyte(i >> 24); log_hexbyte(i >> 16); log_hexbyte(i >> 8); log_hexbyte(i); }
void log_hexi32p(uint32_t *p) // On a platform where pointers are less than 32 bits
{ log_hexi32(*p); }

En algunos microcontroladores PIC, log_hexi32(l)probablemente tomaría 9 instrucciones y podría tomar 17 (si lestá en el segundo banco), mientras log_hexi32p(&l)que tomaría 2. La log_hexi32pfunción en sí podría escribirse para tener aproximadamente 14 instrucciones de largo, por lo que se pagaría sola si se llama dos veces .

Super gato
fuente
2

Un punto que ninguna de las otras respuestas ha mencionado: en un micro básico (IE solo hay el bucle main () y quizás un par de ISR en ejecución en cualquier momento, no un sistema operativo de subprocesos múltiples) si se bloquea / detiene / obtiene atrapado en un bucle, su función de impresión simplemente no sucederá .

Además, la gente ha dicho "no use printf" o "stdio.h ocupa mucho espacio", pero no tiene mucha alternativa: embedded.kyle menciona alternativas simplificadas, y ese es exactamente el tipo de cosas que probablemente debería ser haciendo, por supuesto, en un sistema embebido básico. Una rutina básica para arrojar algunos caracteres fuera del UART podría ser unos pocos bytes de código.

John U
fuente
Si su printf no sucede, ha aprendido mucho sobre dónde es problemático su código.
Scott Seidman
Suponiendo que solo tenga una impresión que pueda suceder, sí. Pero las interrupciones pueden activarse cientos de veces en el tiempo que toma una llamada printf () para sacar algo del UART
John U