Articles tagués “cortex m

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) 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 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 &lt; count) {
      auto c = uart.read();
      line[received] = c;
      ++received;

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

   return received;
}

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.