Electronique

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

Mesure précise du temps d’exécution sur Cortex-M

Sur Cortex-M, il est possible d’utiliser le registre DWT CYCCNT pour avoir une mesure d’être précise du temps. Enfin, t’as tout à fait du temps : du nombre de cycles CPU écoulés. Un savant calcul avec la fréquence processeur redonne un temps… C’est utile pour mesurer les performances d’un morceau de code, en ayant conscience que le nombre de cycles nécessaires pour l’exécution doit tenir dans ce registre de 32 bits (ça laisse de la marge).

Voici comment s’en servir :

volatile int a;
volatile int b;
volatile int c;

void run() {

   std::cout << "Application starts" << std::endl;

   // Check it DWT is present
   if ((DWT->CTRL | DWT_CTRL_NOCYCCNT_Msk) == 1) {
      std::cout << "ERROR: DWT CYCCNT is not supported" << std::endl;

   } else {

      // Enable
      DWT->CTRL |= 1;

      // Restart counter
      DWT->CYCCNT = 0;

      // Do something
      c = (a + b) * (a - b) * 2;

      // Get cycles
      auto cycles = DWT->CYCCNT;
      std::cout << "Execution took " << (int) cycles << " cycles" << std::endl;
   }

   while (1) {
   }
}

En exécutant ce code sur une carte Nucleo de ST, équipée d’un STM32F413 (Cortex-M4), j’obtiens :

Application starts
Execution took 15 cycles

Si je remplace le calcul par un __NOP(), alors le nombre de cycle est de 1 et si je ne met rien, il est de… 0. Si je fais un délai de 1 seconde, avec HAL_Delay(1000), le nombre de cycles est de 32.028.150, ce qui est cohérent avec le fait que mon CPU tourne à 32 MHz.

Pour plus de détails, vous pouvez consulter le Reference Manual – ARM® v7-M Architecture.


Cortex-M : comment savoir si on est sous IT ?

En écrivant des wrappers C++ pour FreeRTOS, j’ai eu besoin de savoir si le code était appelé depuis une interruption ou pas. En effet, plusieurs fonctions de FreeRTOS possède une variante avec le suffixe fromISR : il faut appeler la version normale ou la version fromISR aux bons moments. Quand on utilise un Cortex-M, il faut regarder du côté du System Control Block (SCB) et plus particulièrement l’Interrupt Control and State Register (ICSR). Voici une fonction tout simple, prise sur stackoverflow, et qui fait le taf :

bool isInInterrupt() {
    return (SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) != 0;
}

Voici un exemple d’utilisation pour mon wrapper de sémaphore :

bool AbstractSemaphore::give() {
	BaseType_t result = isInInterrupt() ?
				xSemaphoreGiveFromISR(nativeHandle_m, nullptr) :
				xSemaphoreGive(nativeHandle_m);
	return result == pdTRUE;
}

Il m’est ainsi possible d’appeler la fonction give() sans me soucier de savoir si l’appel se fait depuis une IT ou pas. Attention, cela ne veut pas dire qu’il ne faut se soucier de rien ! En effet, il existe dans FreeRTOS un mécanisme empêchant l’appel des  fonctions dites syscalls depuis une IT dont la priorité est supérieure à un seuil. Ce seuil est configuré dans FreeRTOSConfig.h avec la constante configMAX_SYSCALL_INTERRUPT_PRIORITY (pour plus de détails, voir la documentation de FreeRTOS) :

/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions.  DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5

/* Interrupt priorities used by the kernel port layer itself.  These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

En fait, la valeur vraiment utile est celle de configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY : 5. Pour rappel, plus la valeur est grande, plus la priorité est faible. Dans ma configuration actuelle, cela signifie que je peux appeler give() depuis mon IT d’external interrupt seulement si la priorité d’interruption est configurée pour x => 5 :

NVIC_SetPriority(EXTI15_10_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), x, 0));

Le cas échéant, par exemple si x == 3, une assertion de FreeRTOS bloque l’exécution :

A la ligne 742 de port.c, on trouve :

configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );

Il est possible de rajouter sa propre assertion dans le wrapper C si on a une meilleure remontée des erreurs qu’un simple blocage de l’application. Il est aussi possible de redéfinir la macro configASSERT() dans FreeRTOSConfig.h (voir à nouveau la documentation), qui par défaut est définie ainsi :

#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );}

PSP et MSP sur ARM Cortex-M

Si vous jetez un oeil aux registres d’un processeur ARM Cortex-M, vous ne serez pas surpris de voir Stack Pointer (SP) mais vous serez peut-être plus dubitatifs en voyant qu’il y a aussi Main Stack Pointer (MSP) et Process Stack Pointer (PSP). Il y a en fait deux stack pointers (MSP et PSP) et le registre SP contient soit la valeur de l’un, soit la valeur de l’autre. Laquelle ? Il faut regarder le bit 1 (SPSEL) du registre Control : s’il est à 1, le processeur utilise le PSP ; sinon, il utilise le MSP.

Si vous vous demandez à quoi cela sert, la réponse est assez simple : c’est essentiellement fait pour améliorer la robustesse du système lors de l’utilisation d’un système d’exploitation. Chris Shore (de chez ARM) explique pourquoi dans cette discussion :

Having two separate stack pointers allows the operating system to be safer and more robust. Usually, you would configure the operating system to use Main Stack Pointer (MSP) and user applications to use Process Stack Pointer (PSP). The switch from one stack to another then happens automatically when an exception is handled.

The fact that the operating system and exception handlers use a different stack from the application means that the OS can protect its stack and prevent applications from accessing or corrupting it. You can also ensure that the OS does not run out of stack if the application consumes all the available PSP stack space – that means that there is always space on the stack to run an exception handler in the case of an error occurring.

Note that you don’t have to use both stack pointers. By default, the system will only use a single stack pointer (MSP) and must be manually configured to use PSP. Also, some Cortex-M microcontrollers do not support two stack pointers.

Il est très simple de constater ce comportement de changement de stack pointer. Dans une application utilisant un OS comme FreeRTOS, il suffit de mettre un breakpoint dans une task de l’OS et un autre dans un handler d’interruption. Quand les breakpoints sont touchés et votre application se met en pause, jettez un oeil aux registres et à la callstack. Dans le premier cas, le bit 1 de Control est à 1, on est bien en Process Stack Pointer : dans le second cas, ce bit est à 0, on est bien en Main Stack Pointer.

Voici ce que ça donne quand on est s’arrête dans une task. On constate que les registres SP et PSP sont égaux :

Le débogueur arrive même à nous dire que le PSP pointe vers une zone dans ucHeap. C’est cohérent avec le fait que j’ai créé dynamiquement la tâche FreeRTOS et donc que sa stack a été allouée dans le heap de FreeRTOS.

Voici ce que ça donne pour une interruption. On voit que la valeur des registres SP et MSP sont égales :

Le PSP nous permet de voir que la tâche qui a été interrompue était l’idle task. La callstack nous montre aussi que les interruptions se font dans le même contexte que l’OS. prvPortStartFirstStack() est la fonction par laquelle démarre FreeRTOS et donne la main aux tasks en modifiant le stack pointer.


XBee et XCTU sous Linux

Cela doit bien faire 2 ans que deux modules XBee (des Pro S1 de chez Digi) traînent dans mes tiroirs et je n’en avais rien fait. Ayant pour projet de faire des capteurs de température et d’humidité sans fil, je me suis dit que c’était le bon moment de les utiliser. L’idée est d’envoyer les données à un Raspberry Pi pour traitement. J’ai donc pris mon XBee Explrer Dongle, j’y ai connecté un module Xbee et je l’ai branché à mon PC. Digi fournit un logiciel pour gérer les modules XBee connectés à un PC, les configurer, envoyer et recevoir des données : il s’agit de XCTU. De nombreux tutoriels sur Internet disent que XCTU n’est disponible que sous Windows mais cette époque est révolue ! Vous pouvez télécharger les différentes versions pour Windows, Linux et Mac ici.

Installation et lancement

Si vous êtes sous Linux et que vous ne savez pas si vous devez opter pour la version 32 ou la version 64 bits, il vous suffit de taper la commande uname -a pour savoir si vous avez un processeur 32 ou 64 bits. Exemple dans mon cas :

~$ uname -a
Linux pgradot-xubuntu 4.4.0-53-generic #74-Ubuntu SMP 
Fri Dec 2 15:58:04 UTC 2016 i686 i686 i686 GNU/Linux

Le i686 m’indique que mon processeur est 32 bits.

Une fois le téléchargement terminé, rendez le fichier exécutable et lancez-le avec les droits administrateurs :

$ chmod u+x 40002880_G.run
$ sudo ./40002880_G.run

Un wizard vous guide dans l’installation de XCTU. Par défaut, le logiciel s’installe dans /opt/Digi où un sous-dossier XCTU-NG est créé. On y trouve launcher et app (le premier lançant le second). Si vous lancez l’un des deux sans les droits administrateurs, cela ne fera qu’ouvrir une petite fenêtre qui ne contient rien et que vous ne pourrez pas fermer… Ayez le réflexe sudo, ça marchera beaucoup mieux ! Vous verrez alors ceci :

xbee 1 startup

Ajout de modules

Vous pouvez ajouter des modules radios ou les découvrir en cliquant que les icônes en haut à gauche. L’ info-bulle vous avait sans doute déjà fait deviner le mode opératoire. Le mien étant branché via un dongle USB, il apparaît comme étant /dev/ttyUSB0 :

xbee 2 add device

XCTU récupère et affiche tout la configuration du nouveau module :

xbee 3 device added

Onglets configuration, console et réseau

XCTU est intuitif et l’info-bulle de droite nous dit qu’il existe 3 onglets, chacun correspondant à un mode :

  1. Le mode configuration, ouvert par défaut, permet de voir la configuration des modules comme montré précédemment mais aussi de la modifier et d’envoyer la nouvelle configuration au module.
  2. Le monde console permet soit d’interagir via des commandes AT avec un module, soit d’envoyer des paquets sur le réseau. Vous pouvez enregistrer des paquets et jouer des séquences d’envoi. Certainement très pratique pour des tests.
  3. Le mode réseau, pour avoir un aperçu de la topologie du réseau sans fil à portée.

Commandes AT

Si vous souhaitez envoyez des commandes AT à votre module, il suffit d’aller dans l’onglet console et d’appuyer sur le bouton Open (qui se transforme en bouton Close, visible en vert dans l’image ci-dessous). Vous tapez alors +++ dans à gauche, vous attendez une seconde et le module répond OK pour indiquer qu’il est bien passé en mode commande. Tapez alors vos commandes en les terminant par entrée et le module vous répondra. Sans activité de votre part pendant 3 secondes, le module sort automatiquement et silencieusement du mode commande et l’envoi d’une nouvelle commande sera alors sans réponse. Sparkun a écrit un très bon aide-mémoire pour les commandes AT disponibles.

xbee 4 at

Pour aller plus loin

Exploring XBees and XCTU par Sparkun