Nouveau

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( ;; );}
Publicités

Boost pour appeler du C++ depuis Python

Il y a plusieurs années, je m’étais intéressé à la manière d’appeler du code C depuis Python. C’était tellement compliqué que je n’ai jamais eu envie d’essayer. J’ai récemment découvert l’existence de Boost Python, et là, j’ai eu envie d’essayer ! C’est parti !

Créer son module Python

La première étape est bien sûr d’écrire quelques fonctions ou classes C++ et de faire un peu de magie pour faire les wrappers pour Python. Pour créer un module Python, il suffit de créer une bibliothèque dynamique avec ce code et CMake est bien sûr l’outil de choix pour cela. Voici un exemple :

On builde la bibliothèque :

$ mkdir build
$ cd build/
$ cmake ..
-- The C compiler identification is GNU 7.3.0
-- The CXX compiler identification is GNU 7.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Warning at /usr/share/cmake-3.10/Modules/FindBoost.cmake:1626 (message):
  No header defined for python3; skipping header check
Call Stack (most recent call first):
  CMakeLists.txt:5 (find_package)


-- Boost version: 1.65.1
-- Found the following Boost libraries:
--   python3
-- Found PythonInterp: /usr/bin/python3 (found suitable version "3.6.7", minimum required is "3") 
-- Found PythonLibs: /usr/lib/i386-linux-gnu/libpython3.6m.so (found suitable version "3.6.7", minimum required is "3") 
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pierre/Documents/boost_python/build

$ cmake --build .
Scanning dependencies of target mylibrary
[ 50%] Building CXX object CMakeFiles/mylibrary.dir/functions.cpp.o
[100%] Linking CXX shared library mylibrary.so
[100%] Built target mylibrary

On peut maintenant exécuter Python et tester notre module :

$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mylibrary
>>> mylibrary.say_hello()
Hello
>>> p = mylibrary.Printer("B&W printer")
>>> p.print("bla bla")
[B&W printer]bla bla
>>>help(mylibrary)
Help on module mylibrary:

NAME
    mylibrary

CLASSES
    Boost.Python.instance(builtins.object)
        Printer
    
    class Printer(Boost.Python.instance)
     |  Method resolution order:
     |      Printer
     |      Boost.Python.instance
     |      builtins.object
     |  
     |  Methods defined here:
     |  
     |  __init__(...)
     |      __init__( (object)arg1, (str)arg2) -> None :
     |      
     |          C++ signature :
     |              void __init__(_object*,std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
     |  
     |  __reduce__ = (...)
     |  
     |  print(...)
     |      print( (Printer)arg1, (str)arg2) -> None :
     |      
     |          C++ signature :
     |              void print(Printer {lvalue},std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
     |  
     |  ----------------------------------------------------------------------
     |  Data and other attributes defined here:
     |  
     |  __instance_size__ = 32
     |  
     |  ----------------------------------------------------------------------
     |  Methods inherited from Boost.Python.instance:
     |  
     |  __new__(*args, **kwargs) from Boost.Python.class
     |      Create and return a new object.  See help(type) for accurate signature.
     |  
     |  ----------------------------------------------------------------------
     |  Data descriptors inherited from Boost.Python.instance:
     |  
     |  __dict__
     |  
     |  __weakref__

FUNCTIONS
    compute(...)
        compute( (int)arg1, (int)arg2) -> int :
        
            C++ signature :
                int compute(int,int)
    
    sayHelloTo(...)
        sayHelloTo( (str)arg1) -> None :
        
            C++ signature :
                void sayHelloTo(std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
    
    say_hello(...)
        say_hello() -> None :
        
            C++ signature :
                void say_hello()

FILE
    /home/pierre/Documents/boost_python/build/mylibrary.so

Notez que import mylibrary fonction.ne parce que mylibrary.so est dans le dossier d’où Python est lancé. Il y a moyen de l’installer dans le dossier dédié de Python, comme expliqué ici, pour y avoir accès depuis n’importe où.

Pour aller plus loin

Il existe le GitHub parfait pour aller plus loin : boost::python examples.

Ah ! Si ça marchait toujours du premier coup…

Évidemment, tout n’a pas marché du premier coup… Je me suis tapé quelques erreurs sympas avant d’arriver à quelque chose de fonctionnel.

Il faut bien sûr que Boost et Python soient installés :

sudo apt install libboost-all-dev python3-dev

J’ai eu une erreur magnifique de compilation à cause d’un header Python :

$ make
[ 50%] Building CXX object CMakeFiles/mylibrary.dir/functions.cpp.o
In file included from /usr/include/boost/python/detail/prefix.hpp:13:0,
                 from /usr/include/boost/python/args.hpp:8,
                 from /usr/include/boost/python.hpp:11,
                 from /home/pierre/Documents/boost_python/functions.cpp:1:
/usr/include/boost/python/detail/wrap_python.hpp:50:11: fatal error: pyconfig.h: No such file or directory
 # include 
           ^~~~~~~~~~~~
compilation terminated.

Ce fichier est apporté par le paquet python3-dev et il était bien présent sur mon PC :

$ locate pyconfig.h
/usr/include/i386-linux-gnu/python2.7/pyconfig.h
/usr/include/i386-linux-gnu/python3.6m/pyconfig.h
/usr/include/python2.7/pyconfig.h
/usr/include/python3.6m/pyconfig.h

Le problème était donc d’ajouter le dossier /usr/include/python3.6m/ à l’include path. Une solution est de l’ajouter au path avant de compiler. Il y a beaucoup mieux en s’appuyant que les capacités de CMake à trouver Python : si Python est trouvé, la variable PYTHON_INCLUDE_DIRS est renseignée et il suffit de l’ajouter en tant qu’include directory comme fait dans mon CMakeLists.txt ci-dessus.

La première fois que j’ai tenté d’importer mon module, j’ai eu une erreur de version de Python :

$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mylibrary
Traceback (most recent call last):
  File "", line 1, in 
ImportError: /usr/lib/i386-linux-gnu/libboost_python-py27.so.1.65.1: undefined symbol: PyClass_Type
>>>
[3]+  Stopped                 python3

En effet, find_package(Boost COMPONENTS python) trouvait la variante pour Python 2. Il m’a fallu rajouter un 3, ce qui donne find_package(Boost COMPONENTS python3). La documentation de FindBoost a un paragraphe à ce sujet :

Note that Boost Python components require a Python version suffix (Boost 1.67 and later), e.g. python36 or python27 for the versions built against Python 3.6 and 2.7, respectively. This also applies to additional components using Python including mpi_python and numpy. Earlier Boost releases may use distribution-specific suffixes such as 2, 3 or 2.7. These may also be used as suffixes, but note that they are not portable.

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 :

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 !