Nouveau

WebStorm et Node.js

Quand tu fais de l’embarqué, tu peux retrouver à faire de tout, y compris du Node.js pour émuler la communication Bluetooth Low Energy de ton produit IoT. Quand tu utilises PyCharm pour les scripts Python et CLion pour le soft C++, quoi de plus logique que d’installer WebStorm, l’IDE de JetBrains pour le JavaScript ? Tout est déjà prêt pour faire du Node.js. Tu fais « Open » pour ouvrir le dossier de projet :

Il détecte la présence du package.json et une pop-up te demande si tu veux installer les dépendances avec npm :

Quand tu exécutes le projet, il sélectionne automatique ton interpréteur Node.js et tout roule !

Tout ? Pas exactement… L’éditeur de code ne reconnait pas les fonctions spécifiques Node.js. Par exemple, il ne reconnait pas require, Buffer ou encore super_ et propose de créer ces fonctions…

Bizarrement, la fonctionnalité « Coding assistance for Node.js » n’est pas activée par défaut, il faut le faire soi-même dans les « Settings » :

Maintenant, tout roule !

Caster, c’est mal

En C comme en C++, on se retrouve de temps en temps à caster une variable (de l’anglais to casttranstyper en français). Pourtant, caster, c’est mal. Il faut le faire avec parcimonie et avoir de bonnes raisons quand on décide de le faire. Juste « faire taire un warning du compilateur » n’est pas une bonne raison. Quand on caste, on dit au compilateur : « considère cette variable comme étant d’un autre type, ne fais pas de vérification dessus ». Dans un code parfaitement écrit, on en devrait jamais avoir besoin de caster puisque les types seraient toujours bons. La réalité est un peu différente et il faut éviter de cacher les problèmes potentiellement graves.

Voici un premier exemple :

void print(float* p) {
    printf("%f", *p);
}

int main() {
    int i = 31415;
    print(&i);
}

Ce code génère un warning en C :

warning: passing argument 1 of 'print' from incompatible pointer type [-Wincompatible-pointer-types]
     print(&i);
           ^
main.c:3:6: note: expected 'float *' but argument is of type 'int *'
 void print(float* p) {
      ^~~~~

Il génère une erreur en C++ :

error: cannot convert 'int*' to 'float*' for argument '1' to 'void print(float*)'
     print(&i);

Dans les 2 cas, caster permet de faire taire le compilateur mais le résultat est bien sûr faux, ça affiche 0.000000. Le cast permet ici de considérer une adresse vers un type comme étant une adresse vers un autre type. On ne change pas la valeur de l’adresse et donc à l’emplacement mémoire, printf() trouve des bits qui correspondent à un entier de 32 bits (probablement codé en complément à 2) et les lit en considérant qu’ils représentent un flottant (logiquement codé en IEEE754). Il n’y a quasiment aucune chance que les 2 représentations donnent la même valeur et l’affichage est faux.

Le problème de ce premier code était assez évident. Prenons un second code un peu plus subtil :

void print(float p) {
	printf("%f", p);
}

int main() {
	int i = 31415;
	print(i);
}

Compilé avec les options -Wall -Wextra, ce code ne génère pas de warning. On pourrait se dire que tout va bien, les langages C et C++ autorisant les conversions entre nombres. Autorisées ne veut pas forcément dire parfaites. Ajoutons l’option -Wconversion pour voir ce que GCC a beau à nous dire :

warning: conversion to 'float' from 'int' may alter its value [-Wconversion]
  print(i);

Que vérifie exactement ce warning ? Voici sa documentation :

-Wconversion
Warn for implicit conversions that may alter a value. This includes conversions between real and integer, like abs (x) when x is double; conversions between signed and unsigned, like unsigned ui = -1; and conversions to smaller types, like sqrtf (M_PI). Do not warn for explicit casts like abs ((int) x) and ui = (unsigned) -1, or if the value is not changed by the conversion like in abs (2.0). Warnings about conversions between signed and unsigned integers can be disabled by using -Wno-sign-conversion.

For C++, also warn for confusing overload resolution for user-defined conversions; and conversions that never use a type conversion operator: conversions to void, the same type, a base class or a reference to them. Warnings about conversions between signed and unsigned integers are disabled by default in C++ unless -Wsign-conversion is explicitly enabled.

Bien sûr, un cast empêche le warning d’apparaître. Warning ou pas, la sortie console est bien 31415.000000. En revanche, si i = 987654321, alors la sortie devient 987654336.000000… Et de manière marrante, si i = 987654336 alors la sortie est bien 987654336.000000 et est donc correcte. Vous êtes surpris qu’un code qui passe bien avec -Wall -Wextra puisse produire des résultats erronés ? Hé hé… Premièrement, ces deux options sont le minimum vital et vous pouvez (devez ?) en ajouter d’autres, par exemple -Wwrite-strings. Deuxièmement, les conversions entre nombres, que ce soit lors de simples affectations ou lors de calculs, réservent bien des surprises et vous devriez vous méfiez. -Wconversion est fait pour ça ; -Wsign-conversion et -Wfloat-conversion sont un peu moins brutaux (ils sont activés par -Wconversion automatiquement). Il existe aussi -Wdouble-promotion qui peut révéler des possibles pertes de performances.

Ici, ce n’est que de l’affichage, ce n’est pas catastrophique, mais si c’était des calculs pour la stabilisation votre drône DIY, vous rigoleriez moins ! ; )

Voici un dernier exemple bien plus violent :

void modify(char* p) {
	p[0] += 1;
}

int main() {
	char* message = (char*) "hello";
	modify(message);
}

Ce code cache un warning en C++ :

warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
  char* message = "hello";
                  ^~~~~~~

En C, il faut rajouter l’option explicitement car elle n’est pas inclus dans -Wall -Wextra. Que se passe t-il si on exécute ce code ? Une erreur de segmentation, tout simplement, puisque le programme va tenter d’écrire dans une zone mémoire qui est en lecture seule.

Cet article a prouvé au passage que le système de typage est plus fort en C++ qu’en C mais ce n’était pas son but premier. Le but était de vous montrer que le système de typage est là pour vous aider et que caster vous laisse seul face à vos erreurs au lieu de bénéficier de l’aide de votre compilateur. Rares sont les codes C et C++ sans cast, surtout si vous faites joujou avec du code très bas niveau. Essayez toujours de ne pas avoir à caster, utiliser les opérateur de cast en C++ pour expliciter le but du cast, méfiez-vous des conversions implicites et activer les options de votre compilateur pour qu’il vous aide à détecter autant d’erreurs que possibles.

std::variant en C++17

Il y a peu, j’ai lu des trucs sur std::variant, une des nouvelles fonctionnalités de C++17. std::variant est vendu comme étant une union type-safe. Vous n’êtes pas sans ignorer les risques liés à l’utilisation des unions : vous avez une union entre un entier et d’une string, vous affectez un entier à votre union, vous essayez de lire la string, ça vous renvoie n’importe quoi. Sauf que vous n’avez aucun moyen de le savoir… std::variant vient régler ces problèmes. Ce template de classes permet de tester le membre actif et lance des exceptions si on tente d’accéder à un autre membre.

Le lendemain de mes lectures, j’ai eu besoin de faire évoluer du code et je me suis dit que std::variant pourrait répondre à mon besoin. Oui, j’ai la chance de coder en C++17 au boulot ! Le code en question est concerne la gestion d’un bus SPI. Un thread est chargé de protéger l’accès au bus, de le reconfigurer si besoin, de sélectionner / désélectionner les périphériques, de faire des statistiques d’utilisation. Quand un autre thread a besoin de faire des lectures et des écritures sur le bus, il poste une requête qui est traité de manière asynchrone. Ça fonctionne très bien mais la manière dont est implémenté la classe Request n’est pas parfaite. Voici à quoi elle ressemble :

enum class RequestType {
	READ, WRITE, WRITE_READ
};

struct Request {
	RequestType type;
	
	std::uint8_t* const readData;
	const std::size_t readLength;

	const std::uint8_t* const writeData;
	const std::size_t writeLength;
};

Voici des exemples de création de requêtes :

	std::array<std::uint8_t, 15> readBuffer;
	std::array<std::uint8_t, 15> writeBuffer;

	Request read{RequestType::READ, readBuffer.data(), readBuffer.size(),
                                nullptr, 0};

	Request writeRead{RequestType::WRITE_READ, readBuffer.data(), readBuffer.size(),
                                writeBuffer.data(), writeBuffer.size()};

	Request bad{RequestType::WRITE, readBuffer.data(), readBuffer.size(),
                                nullptr, 0};

On voit plusieurs défauts :

  • des paramètres qui ne servent pas (pour read),
  • des paramètres dans un ordre pas forcément logique (pour writeRead, il faut donner les paramètres lecture puis d’écriture alors qu’on s’attendrait à l’inverse),
  • des paramètres qui peuvent être non consistants (pour bad mais un constructeur avec des assertions pourrait contrecarrer de telles erreurs).

Enfin, le plus gros défaut est le bazar induit par l’ajout d’un nouveau type de requête. J’ai par exemple besoin de rajouter d’un type de requête pour faire deux écritures consécutives. Soit je réutilise les membres readData et readLength mais c’est dégueulasse ; soit je rajoute deux membres à la structure et je dois retoucher toutes les créations de requêtes existantes…

C’est là que je me suis dit que j’allais testé std::variant pour améliorer la situation. L’idée est de définir 4 structures, une pour chaque type de requête, puis d’instancier std::variant avec ces 4 types. Se pose ensuite la question de traiter les requêtes dans la classe SpiBus. Le switch/case sur le champ Request::type (qui n’existe plus) peut se remplacer par des appels à std::variant::index() et à std::get(), mais il y a mieux. En effet, std::variant vient avec son copain std::visit. Cette fonction prend en paramètre un visiteur et un std::variant, détermine le membre actif et appelle l’opérateur du visiteur prenant en paramètre le type du membre actif.

Mes expérimentations m’ont amenés à ce code :

#include <cassert>
#include <cstdint>
#include <variant>
struct ReadRequest {
ReadRequest(std::uint8_t* data, std::size_t length) :
data(data),
length(length) {
assert(data != nullptr);
}
std::uint8_t* const data;
const std::size_t length;
};
struct WriteRequest {
WriteRequest(const std::uint8_t* data, std::size_t length) :
data(data),
length(length) {
assert(data != nullptr);
}
const std::uint8_t* const data;
const std::size_t length;
};
struct WriteReadRequest {
WriteReadRequest(const std::uint8_t* writeData, std::size_t writeLength,
std::uint8_t* readData, std::size_t readLength) :
readData(readData),
readLength(readLength),
writeData(writeData),
writeLength(writeLength) {
assert(readData != nullptr);
assert(writeData != nullptr);
}
std::uint8_t* const readData;
const std::size_t readLength;
const std::uint8_t* const writeData;
const std::size_t writeLength;
};
struct DoubleWriteRequest {
DoubleWriteRequest(const std::uint8_t* firstData, std::size_t firstLength,
const std::uint8_t* secondData, std::size_t secondLength) :
firstData(firstData),
firstLength(firstLength),
secondData(secondData),
secondLength(secondLength) {
assert(firstData != nullptr);
assert(secondData != nullptr);
}
const std::uint8_t* const firstData;
const std::size_t firstLength;
const std::uint8_t* const secondData;
const std::size_t secondLength;
};
using Request =std::variant<ReadRequest,
WriteRequest,
WriteReadRequest,
DoubleWriteRequest>;
//———————————————————————————
extern "C" {
void spi_driver_read(std::uint8_t* data, std::size_t length);
void spi_driver_write(const std::uint8_t* data, std::size_t length);
}
class SpiBus {
public:
void execute(const Request& request) {
RequestProcessor requestProcessor;
std::visit(requestProcessor, request);
}
private:
class RequestProcessor {
public:
void operator()(const ReadRequest& request) {
spi_driver_read(request.data, request.length);
}
void operator()(const WriteRequest& request) {
spi_driver_write(request.data, request.length);
}
void operator()(const WriteReadRequest& request) {
spi_driver_write(request.writeData, request.writeLength);
spi_driver_read(request.readData, request.readLength);
}
void operator()(const DoubleWriteRequest& request) {
spi_driver_write(request.firstData, request.firstLength);
spi_driver_write(request.secondData, request.secondLength);
}
};
};
//——————————————————————————–
#include <array>
int main() {
SpiBus bus;
std::array<std::uint8_t, 15> buffer;
ReadRequest rr{buffer.data(), buffer.size()};
bus.execute(rr);
//ReadRequest bad_rr{nullptr, 42}; // triggers assert()
WriteRequest wr{buffer.data(), buffer.size()};
bus.execute(wr);
WriteReadRequest wrr{buffer.data(), 2, buffer.data(), 13};
bus.execute(wrr);
DoubleWriteRequest dwr{buffer.data(), 10, buffer.data(), 5};
bus.execute(dwr);
}

view raw
main.cpp
hosted with ❤ by GitHub

Rajouter la nouvelle requête DoubleWrite a été simple : j’ai créé ma nouvelle structure, je l’ai ajouté à la liste des types de Request et j’ai implémenté un nouvel opérateur dans RequestProcessor. Si on oublie cette dernière étape, le code ne compile pas donc on est certain que le dispatch se fera bien si le code compile. La création de requête plus simple et moins sujette à erreur. Le traitement est aussi simplifié : chaque type de requête est traité dans sa propre fonction dans laquelle on ne voit pas les membres qui ne servent à rien. Comprendre : je ne peux plus utiliser readData alors que je traite une requête de type WRITE par exemple.

Il se pose encore la question de la consommation mémoire de tout ce beau monde, parce que dans mon cas, ça doit tourner dans un Cortex-M4. Encore et toujours, Compiler Explorer est là. Il s’avère que ça génère pas mal de code… Trop pour que je puisse en faire une capture d’écran. Cliquez-ici et regarder ce que ça donne. Ça pique un peu mais c’est logique, il y a des vérifications pour lancer des exceptions en cas de mauvaise utilisation de l’union, même si ici, grâce à std::visit, on ne risque rien normalement. Il s’avère que les exceptions sont désactivées dans mon projet, donc je peux l’option -fno-exceptions et voir ce que ça donne (cliquez pour agrandir) :

Yeah ! C’est bon ça !

Au final, j’ai implémenté tout ça dans le vrai code et j’ai a gagné un peu de place en flash puisque j’ai viré du code de vérification lors de la création de requêtes. Et le même jour j’ai rajouté très facilement un cinquième type de requête… Bref, une bonne amélioration !

Bonne gestion des variantes !

Quelques trucs avec CMake

CMake, c’est bien, c’est puissant, c’est utilisé partout sur GitHub. Seulement, passés les tutoriels simples, CMake, c’est un mur pas vraiment évident à escalader. J’ai créé ce blog pour ne pas oublier les trucs et astuces qui pourraient me resservir, alors quoi de plus naturel que de faire un article avec quelques trucs sur CMake ? Parce qu’il y a certains trucs que j’ai mis très longtemps à trouver…

CMake est un outil qui évolue vite. En cherchant comment réaliser une action particulière, on trouve des tas de solutions différentes sur le net et beaucoup sont obsolètes. J’ai essayé de lister ici les techniques modernes mais malheureusement elles seront peut-être obsolètes dans quelques temps… D’ici là, enjoy  : )

Choisir les versions des langages C et C++

La solution simple moderne est de définir les variables dédiées : CMAKE_C_STANDARD et CMAKE_CXX_STANDARD. Par exemple pour utiliser C++11 :

set(CMAKE_CXX_STANDARD 11)

Vous pouvez lire cet article pour plus de détails et de manière de choisir les versions des langages : Enabling C++11 And Later In CMake par Craig Scott.

Attention ! Par défaut, GCC compilera avec les extensions ! Si ce n’est pas ce que vos voulez, dites à CMake que vous en voulez pas d’extensions :

set(CMAKE_CXX_EXTENSIONS OFF)

Ajouter des options de compilations

La solution la plus simple et portable est d’utiliser add_compile_options(). Par exemple pour utiliser l’option -Wall :

add_compile_options(-Wall)

-Wall sera ajouté pour le C et le C++, pour tous les cibles, pour toutes les configurations.

Si vous ne souhaitez ajouter cette option qu’à une seule cible, il faut utiliser target_compile_options() à la place. Certains, comme Boost, pensent qu’on ne devrait utiliser que target_compile_options().

Si vous ne souhaitez que cette option que pour le C ou que pour C++, vous pourriez être tentés de modifier directement les variables CMAKE_lang_FLAGS_config, comme par exemple :

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -Wsuggest-override")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og")

Il est toutefois possible d’utiliser une generator expression pour faire ça (et c’est visiblement le style CMake moderne) :

# uniquement en C++
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Wsuggest-override>)

# uniquement en debug
add_compile_options($<$<CONFIG:Debug>:-Og>)

# uniquement en C++ en debug
add_compile_options($<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Debug>>:-Og>)

NOTE : Chaque fois que vous devez modifier une variable interne de CMake, pensez à ajouter à sa valeur courante et non pas simplement à l’écraser (voir l’item 1 de cet article) ! Sauf si c’est vraiment ce que vous voulez faire…

NOTE : Chaque fois que vous ajoutez des options spécifiques à un compilateur, n’oubliez pas que vous casser la possible d’utiliser votre projet avec un autre compilateur. Si vous voulez continuer à supporter plusieurs compilateurs, il faudrait tester le compilateur choisi et s’adapter, soit avec des if() / endif() soit directement avec des generator expressions.

Spécifier le type de build

Vous avez un projet CMake et vous faites :

cmake -G "Unix Makefiles" .

Vous obtenez un jeu de makefiles répondant au type de build par défaut, mais lequel ? Debug ou Release ? Et bien, rien… Il n’y a pas de mode par défaut, ce qui signifie que la variable CMAKE_BUILD_TYPE est vide ! Cela implique que les variables comme CMAKE_CXX_FLAGS_DEBUG n’est pas utilisées, qu’une générateur expression comme $<CONFIG:Debug> sera égale à 0, etc. Cela signifie aussi que CMake n’ajoute pas automatiquement des options comme -g ou -O2 pour gcc.

N’oubliez donc pas de choisir !

cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug .
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release .

Ajouter des options particulières à certains fichiers ou dossiers

Il est possible d’ajouter des options de compilations pour un fichier en particulier grâce à set_source_files_properties(). C’est très utile pour optimiser un fichier en particulier :

set_source_files_properties(chemin/vers/mon_fichier.cpp PROPERTIES COMPILE_FLAGS -O3)

Attention ! Il s’agit bien d’ajouter une option, pas de remplacer les options déjà appliquées. Si vous voulez par exemple ne plus avoir les warnings générés par -Wconversion, ajoutez l’option -Wno-conversion pour l’annuler.

Il est possible de passer une liste de fichiers et on peut ainsi ruser pour modifier un dossier :

file(GLOB_RECURSE MON_DOSSIER chemin/vers/mon/dossier/*.cpp)
set_source_files_properties(${MON_DOSSIER} PROPERTIES COMPILE_FLAGS -O3)

Afficher la taille de l’exécutable après le build

Vous avez sans doute envie garder un œil sur la taille de votre exécutable. C’est particulièrement le cas si vous travaillez sur un projet embarqué. Une solution simple est d’appeler size (ou un programme équivalent de votre toolchain) grâce à add_custom_command() en tant que post build action. Voici un exemple avec une toolchain GCC pour ARM :

add_custom_command(TARGET program.out
                   POST_BUILD
                   COMMAND arm-none-eabi-size program.out)

Créer un fichier fichier hex à partir de l’exécutable elf et supprimer ce hex quand ‘clean’ est invoquée

Cette astuce (très spécifique pour des projets embarqués, je l’avoue) est aussi basé sur add_custom_command() :

add_custom_command(TARGET program.out
                   POST_BUILD
                   COMMAND arm-none-eabi-objcopy.exe -I elf32-little -O ihex
                           program.out program.hex)

Si besoin, j’ai écrit il y a longtemps un article sur (arm-none-eabi-)objcopy et les paramètres à utiliser pour réaliser une telle conversion.

Ce fichier n’est bien sûr pas supprimé quand vous faites appel à la cible clean puisque CMake n’est pas vraiment au courant de ce fichier program.hex… Je ne n’ai pas trouvé de solution portable pour le supprimer mais si vous utilisez make derrière CMake, vous pouvez utiliser la variable ADDITIONAL_MAKE_CLEAN_FILES :

set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES ${PROJECT_NAME}.hex)

Exclure des éléments du build quand on utilise GLOB_RECURSE

Utiliser file(GLOB_RECURSE …) est souvent considéré comme une mauvaise pratique en CMake. Mais bon, des fois c’est pratique… Si voulez exclure certains fichiers ou dossiers, il faut ensuite modifier la liste obtenue. Voici un exemple :

# Lister les fichiers dans 'source/'
file(GLOB_RECURSE SOURCE_FILES source/*.cpp source/*.hpp source/*.c source/*.h)

# Enlever le dossier 'source/stm32f4xx'
list(FILTER SOURCE_FILES EXCLUDE REGEX ${CMAKE_SOURCE_DIR}/source/stm32f4xx/*)

# Enlever le fichier 'source/debug/Hardfault.c'
list(REMOVE_ITEM SOURCE_FILES ${CMAKE_SOURCE_DIR}/source/debug/Hardfault.c)

pthread

Si votre projet sous Linux utilise des pthreads et si vous vous mangez des undefined references vers des fonctions de pthread lors de l’édition des liens, alors commencez par vérifier que la bibliothèque est bien installée sur votre système puis regardez du côté du module FindThreads. Voici un bout de code à rajouter à votre CMakeLists.txt :

find_package (Threads)
add_executable (myapp main.cpp ...)
target_link_libraries (myapp ${CMAKE_THREAD_LIBS_INIT})

 

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.