Pourquoi printf () est-il mauvais pour le débogage de systèmes embarqués?

16

Je suppose que c'est une mauvaise chose d'essayer de déboguer un projet basé sur un microcontrôleur en utilisant printf().

Je peux comprendre que vous n'avez pas d'endroit prédéfini pour la sortie et qu'il pourrait consommer des broches précieuses. En même temps, j'ai vu des gens consommer une broche UART TX pour la sortie vers le terminal IDE avec une DEBUG_PRINT()macro personnalisée .

tarabyte
la source
12
Qui vous a dit que c'était mauvais? "Habituellement pas le meilleur" n'est pas la même chose qu'un "mauvais" non qualifié.
Spehro Pefhany
6
Toutes ces discussions sur la surcharge, si tout ce que vous avez à faire est de générer des messages "Je suis là", vous n'avez pas besoin du tout de printf, juste une routine pour envoyer une chaîne à un UART. Cela, plus le code pour initialiser l'UART est probablement inférieur à 100 octets de code. L'ajout de la possibilité de sortir quelques valeurs hexadécimales n'augmentera pas beaucoup.
tcrosley
7
@ChetanBhargava - Les fichiers d'en-tête C n'ajoutent généralement pas de code à l'exécutable. Ils contiennent des déclarations; si le reste du code n'utilise pas les éléments déclarés, le code de ces éléments n'est pas lié. Si vous utilisez printf, bien sûr, tout le code nécessaire à l'implémentation printfest lié à l'exécutable. Mais c'est parce que le code l'a utilisé, pas à cause de l'en-tête.
Pete Becker
2
@ChetanBhargava Vous n'avez même pas besoin d'inclure <stdio.h> si vous lancez votre propre routine simple pour sortir une chaîne comme je l'ai décrit (sortie des caractères vers l'UART jusqu'à ce que vous voyez un '\ 0') '
tcrosley
2
@tcrosley Je pense que ce conseil est probablement sans objet si vous avez un bon compilateur moderne, si vous utilisez printf dans le cas simple sans une chaîne de format gcc et la plupart des autres le remplacent par un appel simple plus efficace qui fait beaucoup comme vous le décrivez.
Vality

Réponses:

24

Je peux trouver quelques inconvénients à utiliser printf (). Gardez à l'esprit que le "système embarqué" peut aller de quelque chose avec quelques centaines d'octets de mémoire de programme à un système QNX RTOS complet monté en rack avec des gigaoctets de RAM et des téraoctets de mémoire non volatile.

  • Cela nécessite un endroit pour envoyer les données. Peut-être que vous avez déjà un port de débogage ou de programmation sur le système, peut-être pas. Si vous ne le faites pas (ou celui que vous avez ne fonctionne pas), ce n'est pas très pratique.

  • Ce n'est pas une fonction légère dans tous les contextes. Cela pourrait être un gros problème si vous avez un microcontrôleur avec seulement quelques K de mémoire, car la liaison dans printf peut consommer 4K à elle seule. Si vous avez un microcontrôleur 32K ou 256K, ce n'est probablement pas un problème, et encore moins si vous avez un gros système embarqué.

  • Il est peu ou pas utile pour trouver certains types de problèmes liés à l'allocation de mémoire ou aux interruptions, et peut changer le comportement du programme lorsque des instructions sont incluses ou non.

  • C'est assez inutile pour traiter des choses sensibles au timing. Vous feriez mieux avec un analyseur logique et un oscilloscope ou un analyseur de protocole, ou même un simulateur.

  • Si vous avez un gros programme et que vous devez recompiler plusieurs fois en changeant les instructions printf et en les changeant, vous pourriez perdre beaucoup de temps.

Ce qu'il est bon - c'est un moyen rapide de sortir des données d'une manière préformatée que chaque programmeur C sait utiliser - courbe d'apprentissage zéro. Si vous avez besoin de cracher une matrice pour le filtre Kalman que vous déboguez, il pourrait être agréable de le cracher dans un format que MATLAB pourrait lire. Certainement mieux que de regarder les emplacements de la RAM un par un dans un débogueur ou un émulateur .

Je ne pense pas que ce soit une flèche inutile dans le carquois, mais elle devrait être utilisée avec parcimonie, avec gdb ou d'autres débogueurs, émulateurs, analyseurs logiques, oscilloscopes, outils d'analyse de code statique, outils de couverture de code, etc.

Spehro Pefhany
la source
3
La plupart des printf()implémentations ne sont pas thread-safe (c'est-à-dire non-rentrantes), ce qui n'est pas un tueur de transactions, mais quelque chose à garder à l'esprit lors de son utilisation dans un environnement multi-thread.
JRobert
1
@JRobert soulève un bon point .. et même dans un environnement sans système d'exploitation, il est difficile de faire un débogage direct très utile des ISR. Bien sûr, si vous faites des printf () ou des mathématiques en virgule flottante dans un ISR, l'approche est probablement désactivée.
Spehro Pefhany
@JRobert Quels sont les outils de débogage des développeurs de logiciels travaillant dans un environnement multithread (dans un environnement matériel où l'utilisation d'analyseurs logiques et d'oscilloscopes n'est pas pratique)?
Minh Tran
1
Dans le passé, j'ai roulé mon propre printf () thread-safe; utilisé des équivalents pieds nus put () ou putchar () pour cracher des données très concises à un terminal; stocké des données binaires dans un tableau que j'ai vidé et interprété après le test; utilisé un port d'E / S pour faire clignoter une LED ou pour générer des impulsions pour effectuer des mesures de synchronisation avec un oscilloscope; cracher un nombre à un D / A & mesuré avec VOM ... La liste est aussi longue que votre imagination et inversement aussi grande que votre budget! :)
JRobert
19

En plus de quelques autres bonnes réponses, le fait d'envoyer des données à un port à des débits en bauds série peut être carrément lent en ce qui concerne le temps de votre boucle, et avoir un impact sur la façon dont le reste de votre programme fonctionne (tout comme n'importe quel débogage processus).

Comme d'autres personnes vous l'ont dit, il n'y a rien de "mauvais" à utiliser cette technique, mais elle a, comme beaucoup d'autres techniques de débogage, ses limites. Tant que vous connaissez et pouvez gérer ces limitations, cela peut être une option extrêmement pratique pour vous aider à obtenir votre code correctement.

Les systèmes embarqués ont une certaine opacité qui, en général, rend le débogage un peu problématique.

Scott Seidman
la source
8
+1 pour "les systèmes embarqués ont une certaine opacité". Bien que je crains que cette déclaration ne soit compréhensible que par ceux qui ont une expérience décente de travail avec Embedded, elle constitue un résumé agréable et concis de la situation. Il est proche d'une définition de «intégré», en fait.
njahnke
5

Il y a deux problèmes principaux que vous rencontrerez en essayant d'utiliser printfsur un microcontrôleur.

Tout d'abord, il peut être difficile de diriger la sortie vers le bon port. Pas toujours. Mais certaines plateformes sont plus difficiles que d'autres. Certains fichiers de configuration peuvent être mal documentés et beaucoup d'expérimentation peuvent être nécessaires.

Le second est la mémoire. Une printfbibliothèque complète peut être GRANDE. Parfois, vous n'avez pas besoin de tous les spécificateurs de format et des versions spécialisées peuvent être disponibles. Par exemple, le stdio.h fourni par AVR contient trois différentsprintf de tailles et fonctionnalités différentes.

Étant donné que l'implémentation complète de toutes les fonctionnalités mentionnées devient assez importante, trois versions différentes de vfprintf()peuvent être sélectionnées à l'aide des options de l'éditeur de liens. La valeur par défaut vfprintf()implémente toutes les fonctionnalités mentionnées, à l'exception des conversions en virgule flottante. Une version réduite de vfprintf()est disponible qui implémente uniquement les fonctionnalités de base de conversion d'entiers et de chaînes, mais seule l' #option supplémentaire peut être spécifiée à l'aide d'indicateurs de conversion (ces indicateurs sont analysés correctement à partir de la spécification de format, mais simplement ignorés).

J'avais une instance où aucune bibliothèque n'était disponible et j'avais peu de mémoire. Je n'ai donc pas eu d'autre choix que d'utiliser une macro personnalisée. Mais l'utilisation de printfou non fait vraiment partie de vos besoins.

embedded.kyle
la source
Le votant pourrait-il expliquer ce qui est incorrect dans ma réponse afin que je puisse éviter mon erreur dans les projets futurs?
embedded.kyle
4

Pour ajouter à ce que Spehro Pefhany disait à propos des "choses sensibles au timing": prenons un exemple. Disons que vous avez un gyroscope à partir duquel votre système embarqué prend 1 000 mesures par seconde. Vous souhaitez déboguer ces mesures, vous devez donc les imprimer. Problème: les imprimer entraîne une surcharge du système pour lire 1 000 mesures par seconde, ce qui provoque un débordement de la mémoire tampon du gyroscope, ce qui provoque la lecture (et l'impression) de données corrompues. Et donc, en imprimant les données, vous avez corrompu les données, ce qui vous fait penser qu'il y a un bug dans la lecture des données alors qu'il n'y en a peut-être pas. Un soi-disant heisenbug.

njahnke
la source
lol! "Heisenbug" est-il vraiment un terme technique? Je suppose que cela a à voir avec la mesure de l'état des particules et le principe de Heisenburg ...
Zeta.Investigator
3

La raison principale pour ne pas déboguer avec printf () est qu'il est généralement inefficace, inadéquat et inutile.

Inefficace: printf () et kin utilisent beaucoup de mémoire flash et de RAM par rapport à ce qui est disponible sur un petit microcontrôleur, mais l'inefficacité la plus importante réside dans le débogage réel. Changer ce qui est enregistré nécessite de recompiler et de reprogrammer la cible, ce qui ralentit le processus. Il utilise également un UART que vous pourriez autrement utiliser pour effectuer un travail utile.

Insuffisant: il n'y a que peu de détails que vous pouvez produire sur une liaison série. Si le programme se bloque, vous ne savez pas exactement où, juste la dernière sortie terminée.

Inutile: de nombreux microcontrôleurs peuvent être débogués à distance. JTAG ou des protocoles propriétaires peuvent être utilisés pour mettre le processeur en pause, consulter les registres et la RAM, et même modifier l'état du processeur en cours d'exécution sans avoir à recompiler. C'est pourquoi les débogueurs sont généralement un meilleur moyen de débogage que les instructions d'impression, même sur un PC avec beaucoup d'espace et de puissance.

Il est regrettable que la plate-forme de microcontrôleur la plus courante pour les débutants, Arduino, ne dispose pas d'un débogueur. L'AVR prend en charge le débogage à distance, mais le protocole debugWIRE d'Atmel est propriétaire et non documenté. Vous pouvez utiliser un tableau de développement officiel pour déboguer avec GDB, mais si vous l'avez, vous n'êtes probablement plus trop inquiet pour Arduino.

Theran
la source
Ne pourriez-vous pas utiliser des pointeurs de fonction pour jouer avec ce qui est enregistré et ajouter tout un tas de flexibilité?
Scott Seidman
3

printf () ne fonctionne pas seul. Il appelle de nombreuses autres fonctions, et si vous avez peu d'espace de pile, vous ne pourrez peut-être pas du tout l'utiliser pour déboguer des problèmes proches de votre limite de pile. Selon le compilateur et le microcontrôleur, la chaîne de formatage peut également être placée en mémoire, plutôt que référencée à partir de la mémoire flash. Cela peut s'additionner considérablement si vous poivrez votre code avec des instructions printf. C'est un gros problème dans l'environnement Arduino - les débutants utilisant des dizaines ou des centaines d'instructions printf rencontrent soudainement des problèmes apparemment aléatoires car ils écrasent leur tas avec leur pile.

Adam Davis
la source
2
Bien que j'apprécie les commentaires que le downvote lui-même fournit, il serait plus utile pour moi et pour les autres si ceux qui n'étaient pas d'accord expliquaient les problèmes avec cette réponse. Nous sommes tous ici pour apprendre et partager des connaissances, pensez à partager les vôtres.
Adam Davis
3

Même si l'on veut cracher des données vers une certaine forme de console de journalisation, la printffonction n'est généralement pas un très bon moyen de le faire, car elle doit examiner la chaîne de format passée et l'analyser au moment de l'exécution; même si le code n'utilise jamais d'autre spécificateur de format que%04X , le contrôleur devra généralement inclure tout le code qui serait nécessaire pour analyser les chaînes de format arbitraires. Selon le contrôleur exact que l'on utilise, il peut être beaucoup plus efficace d'utiliser du code comme:

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

Sur certains microcontrôleurs PIC, cela log_hexi32(l)prendrait probablement 9 instructions et pourrait en prendre 17 (s'il se ltrouve dans la deuxième banque), tandis que log_hexi32p(&l)cela prendrait 2. La log_hexi32pfonction elle-même pourrait être écrite pour avoir environ 14 instructions, donc elle se paierait si elle était appelée deux fois .

supercat
la source
2

Un point qu'aucune des autres réponses n'a mentionné: dans un micro de base (IE, il n'y a que la boucle principale () et peut-être quelques ISR en cours d'exécution à tout moment, pas un système d'exploitation multithread) s'il se bloque / s'arrête / obtient coincé dans une boucle, votre fonction d'impression ne se produira tout simplement pas .

De plus, les gens ont dit "n'utilisez pas printf" ou "stdio.h prend beaucoup d'espace" mais pas beaucoup d'alternative - embedded.kyle fait mention d'alternatives simplifiées, et c'est exactement le genre de chose que vous devriez probablement être faire comme une évidence sur un système embarqué de base. Une routine de base pour injecter quelques caractères hors de l'UART pourrait être de quelques octets de code.

John U
la source
Si votre printf ne se produit pas, vous avez beaucoup appris où votre code est problématique.
Scott Seidman
En supposant que vous n'ayez qu'un seul printf qui pourrait arriver, oui. Mais les interruptions peuvent se déclencher des centaines de fois dans le temps qu'il faut à un appel printf () pour retirer quoi que ce soit de l'UART
John U