printf() et scanf() sur MCU

Bon nombre de développeurs embarqués pensent que printf() et scanf() sont réservés au monde PC et qu’ils doivent déboguer à la LED et aux combinaisons de boutons-poussoirs. Il est pourtant souvent facile d’avoir une UART réservée au debug pour lire et écrire des bytes et ainsi utiliser printf() et scanf(). Plus que printf() et scanf(), il s’agit en fait d’avoir une sortie standard (stdout) et une entrée standard (stdin) et ainsi d’avoir accès aux fonctions de la bibliothèque standard. Si stdin n’est pas forcément utile tous les jours, stdout l’est vraiment pour mettre des logs et ainsi suivre l’exécution de son programme.

Comment faire ?

En voilà une bonne question ! Il n’y a pas de réponse type malheureusement… Ça dépend de votre toolchain et surtout de la libc utilisée. Il faut fouiller dans la documentation et / ou sur Internet pour trouver quoi faire. En général, il va s’agir de redéfinir deux fonctions, une pour la lecture et l’autre pour l’écriture.

Voici quelques exemples :

Si vous utilisez newlib comme lib C, vous devriez lire Howto: Porting newlib – A Simple Guide.

Ça va me coûter quoi ?

La réponse est malheureusement la même que pour la question précédente. Ça dépend aussi bien sûr de ce que vous utilisez.

De simples puts() et fgets() ne seront pas très coûteux. Un printf("%d", value) coûtera un peu plus. Un printf("%f", value) coûtera encore plus. Et bien sûr, quand vous commencez à faire #include <iostream> et à jouer avec std::cout et std::cin, alors là, ça peut monter très vite… Je vous encourage à faire des essais, à voir ce qui coûte un peu, beaucoup, trop et trouver des compromis entre réutiliser des fonctions standards et implémenter certains fonctions simplifiées par vous même. Certains linkers sont plus intelligents que d’autres et ont des flags particuliers pour embarqué ou pas le code correspondant à un formateur particulier. Par exemple, ld (le linker de GCC) à les options -u _printf_float et -u _scanff_float pour pouvoir utiliser %f.

Avec la toolchain GNU pour ARM que j’utilise sur STM32, les printf() coûtent peu, en flash comme en RAM, y compris avec le formateur %f. En revanche, le simple fait d’inclure iostream (sans rien toucher à ce qu’il y a dedans) coûte 140k de flash et 6k de RAM. J’ai donc créé mes propres fonctions avec plusieurs surcharges pour avoir avec une syntaxe type stream << value << otherValue et j’utilise printf() de la bibliothèque standard. J’ai trouvé un bon compromis entre temps de développement, occupation mémoire et fonctionnalités.

Des fois, vous n’avez aucune contrainte. Dans certains applications particulières, par exemple des logiciels de tests ou de configuration qui ne sortent pas de l’usine, j’utilise à fond std::cout et std::cin. J’utilise la bibliothèque standard C++ de manière décomplexée, avec notamment std::getline et std::map. Oui, j’ai un terminal sur mon système embarqué, ça marche nickel et ça tient en quelques lignes de code !

Enfin, soyez conscients que toutes les fonctionnalités des bibliothèques standard C comme C++ peuvent utiliser de l’allocation dynamique (sauf quelques unes qui le précisent explicitement comme std::array). J’ai par exemple constaté avec ma toolchain qu’inclure iostream allouait de la mémoire (avant main() donc) et que le première appel (mais pas les suivants) à printf() faisait aussi une allocation.

Un exemple d’implémentation ?

Oui, avec la toolchain GNU pour ARM, il faut implémenter deux fonctions que le linker prendra magiquement. Attention, elles doivent avoir un linkage C !

#include "drivers/Uart.hpp"

static drivers::Uart uart(USART3);
extern "C" {

int _write(int /*fd*/, const void *buf, size_t count) {
	uart.sendSeveral(buf, count);
	return count;
}

int _read(int /*fd*/, const void* buf, size_t count) {
   std::size_t received = 0;
   auto line = (std::uint8_t*) buf;

   while (received &amp;amp;lt; count) {
      auto c = uart.read();
      line[received] = c;
      ++received;

      if (c == '\n') {
         // Stop when '\n' is received
         break;
      }
   }

   return received;
}

3 Réponses

  1. Hey, ne pas vérifier le numéro de FD dans _write et _read, c’est mal! Si un petit malin fait un open d’un autre descripteur de fichier, tout atterit dans l’UART. Tu pourrais rajouter un if (fd == 0), au moins!

    Tu as aussi un < qui s’est glissé dans ton code, et le _read manque peut-être d’un timeout (normalement il devrait s’arrêter dès qu’il n’y a plus de caractères disponibles, les IO bufferisées étant plutôt implémentées au niveau au dessus par les FILE* et avec setbuf/setvbuf). Bon ok, ça n’arrange pas trop les choses pour ce que tu en fait 🙂

    J'aime

    4 janvier 2019 à 11:34

  2. Hello!

    L’occupation mémoire c’est une chose, mais le gros problème de ces fonctions, c’est quand on est dans un contexte temps réel. Un printf avec un format un peu compliqué, ça peut prendre un temps pas négligeable et parfois ça met le bazar. Sans compter le temps d’envoyer les choses sur une UART.

    Heureusement, les gens de chez ARM ont pensé à tout. Si tu as une sonde JTAG connectée, tu peux aussi utiliser le semihosting:
    http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0471g/Bgbjjgij.html

    En gros, l’idée c’est que les appels à printf, etc vont être compilés en instructions BKPT (breakpoint) qui vont ensuite être interceptée par le matériel de debug. Du coup, le gros du traitement peut être fait sur le PC qui sert à débugger, et on a pas besoin de bloquer une UART pour ça. Et ça fait un câble de moins!

    On peut en plus aller plus loin avec ce système, puisque cela peut permettre d’accéder à des fichiers du PC directement avec fopen, etc. Du coup, possibilité de logger dans plusieurs fichiers, de récupérer des paramètres, de faire un bootloader qui vient récupérer tout seul le firmware sur le PC, …

    et quand on a pas ça sous la main, y’a toujours moyen de logger en binaire (sans formater les entiers mais en les stockant dans un buffer) et de faire le formattage sur PC avec une appli qui décode le protocole. Du coup on a juste à envoyer la chaine de format suivie de tous les paramètres sans réfléchir, et on s’évite de perdre trop de temps dans les parties temps réel!

    J'aime

    4 janvier 2019 à 11:34

  3. Hello,

    Ca fait plaisir de te lire !

    J’avais pensé au FD mais ce n’était pas franchement important dans notre cas. Et pour le code montré ici, je voulais le garder le plus simple possible (ce n’est pas le vrai code en fait).

    Il me semblait évident de ne pas faire de printf à côté « importants » de l’application. Je n’ai pas jugé bon de le préciser mais je te suis tout à fait d’accord !

    J’ai déjà utilisé me semi-hosting, mais j’ai souvent eu des performances très faibles. Le printf prennent beaucoup de temps. Ca rendait certains projets limites non utilisables. L’UART c’est relativement efficace, c’est facile à désactiver, ça fonctionne même sans sonde JTAG branché. Bref, il y a des pour et de contre, comme toujours.

    Intéressant cette histoire de pouvoir ouvrir des fichiers du PC depuis la carte ! Ca ouvre des possibilités super intéressantes. Bon, de là à les appliquer demain dans un projet, c’est autre chose…

    J'aime

    23 janvier 2019 à 4:24

Votre commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l’aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l’aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l’aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l’aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.