Articles tagués “stm32

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() etfgets() 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) a l’option -u _printf_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, 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 iosream 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 < count) {
      auto c = uart.read();
      line[received] = c;
      ++received;

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

   return received;
}