Outils pour utilisateurs

Outils du site


cours:informatique:iot:iot_par_la_pratique_inria:320_presentation_riot

Présentation de RIOT

Caractéristiques de RIOT

Caractéristiques système

RIOT possède les caractéristiques attendues d'un système d'exploitation pour microcontrôleur: il est temps-réel et consomme très peu d'espace mémoire. En effet, en règle générale, les microcontrôleurs sont très contraints en mémoire (on parle de kilo octets de RAM et de ROM) et fonctionnent beaucoup moins rapidement qu'un microprocesseur moderne : on parle de MHz quand la vitesse d'un microprocesseur est de l'ordre du GHz. Une autre caractéristique importante des microcontrôleurs est leur capacité à “s'endormir” pour minimiser leur consommation d'énergie: on parle de quelques micro ampères dans les cas extrêmes. Un objet fonctionnant sur microcontrôleur peut donc fonctionner sur batterie pendant plusieurs mois, voire plusieurs années. Comme nous le verrons plus tard, RIOT propose un mécanisme original pour gérer l'état d'endormissement du microcontrôleur et ainsi minimiser sa consommation d'énergie.

Pour pouvoir exploiter au mieux les caractéristiques d'un microcontrôleur, le système d'exploitation RIOT s'articule autour d'une architecture de type micro-kernel. Ce micro-kernel ne contient que les briques essentielles au fonctionnement du système :

  • Une gestion multi-tâches qui permet d'avoir plusieurs contextes d'exécution concurrents sur un même microcontrôleur. Les contextes d'exécution sont totalement indépendants les uns des autres : chacun gère son propre espace mémoire. Ces contextes d'exécution/tâches sont communément appelés thread.
  • Un ordonnanceur temps-réel qui s'occupe de basculer d'un contexte d'exécution à un autre en fonction de leur état. Cet ordonnanceur est dit tickless dans le sens où tout réordonnancent s'effectue à la suite d'une interruption matérielle et non par attente active (i.e avec une boucle d'attente).
  • Un mécanisme de communication entre tâches pour échanger des informations, sous la forme de messages, entre différents contextes d'exécution.
  • Des mécanismes de synchronisation pour gérer la concurrence lorsque plusieurs threads accèdent en écriture/lecture à des ressources partagées, comme des variables en mémoire ou des périphériques du microcontrôleur. Parmi ces mécanismes de synchronisation, on retrouve les classiques mutex et les sémaphores.

Dans sa configuration minimale, RIOT ne nécessite que 2.8kB de RAM et 3.2kB de ROM.

La structure d'une application RIOT est intimement liée au concept de thread puisqu'elle se compose, dans sa version la plus simple, d'au moins deux threads:

  • Le thread principal, ou thread main dans lequel s'exécute la fonction main du programme de l'application.
  • Le thread d'attente, ou thread idle, qui est un contexte d'exécution ayant la plus faible priorité et dans lequel le système bascule lorsque tous les autres threads sont bloqués ou terminés. C'est aussi ce thread qui a la charge de la gestion des modes de consommation. En effet, si tous les autres threads ont terminé leur travail, cela veut dire que le système n'a plus rien à faire et qu'il peut donc se mettre en veille.

Note: depuis la version 2020.07, le thread idle est optionel sur les architectures ARM Cortex-M. Cela permet d'économiser l'espace mémoire (RAM) utilisé par ce thread. Dans le cas où le thread idle n'est pas utilisé, c'est le scheduler qui prend directement en charge la gestion des modes de consommation.

Le mécanisme de communication entre thread est aussi utile pour gérer des interruptions externes efficacement et sans risque de perte d'un état intermédiaire. En effet, une interruption pouvant arriver à n'importe quel moment, il faut pouvoir garantir l'état du système une fois cette interruption traitée. Le principe de la gestion efficace des interruptions dans RIOT est donc d’envoyer un message au thread en charge du traitement de l'interruption depuis le contexte de cette-dernière. De cette manière, la gestion de l'interruption peut se faire dans un contexte sûr (celui du thread recevant le message), sans perte d'état pour le système.

On parle aussi dans ce cas de temps-réel mou : le système garantit le traitement de tout évènement extérieur (une interruption) mais ne garantit pas qu'il sera traité immédiatement.

Couche d'abstraction matérielle

Autour de ce micro-kernel temps-réel vient ensuite se greffer une couche d'abstraction matérielle qui va permettre à RIOT de s'exécuter sur une grande variété de cibles matérielles.

Cette couche d'abstraction matérielle se divise en 4 niveaux:

  1. Le niveau cpu, c'est le bloc de plus bas niveau car il touche au coeur de l'objet. C'est dans ce bloc fonctionnel que sont définis certains éléments communs à toutes les plateformes et qui vont permettre aux applications de savoir quelles fonctionnalités sont fournies par un microcontrôleur (générateur de nombre aléatoire, écriture sur la flash, EEPROM, etc.).
  2. Le niveau de la carte(board) où sont définies certaines variables (ou macros) communes à toutes les plateformes. Par exemple, certaines cartes exposent un accès à certains bus série (SPI, I2C) avec leur configuration et d'autres non. Pour chaque carte est défini un microcontrôleur.
  3. Le niveau périphériques: RIOT définit des API génériques pour les principaux types de périphériques dont disposent les microcontrôleurs, i.e UART, SPI, I2C, PWM, etc. Ce sont ces API génériques qui permettent d'écrire du code adapté à des plateformes matérielles différentes sans modification.
  4. Le niveau des pilotes où sont regroupés tous les pilotes radio, les pilotes de capteurs et d'actionneurs. C'est la couche dépendante du hardware de plus haut niveau puisqu'elle s'appuie elle-même largement sur la couche periph pour accéder de manière générique aux différentes plateformes.

Lorsqu'on compile une application RIOT, il faut toujours spécifier la cible pour laquelle on va produire un firmware. Cela donne la correspondance suivante : une application ⇒ une carte ⇒ un modèle de microcontrôleur.

Grâce à cette couche d'abstraction matérielle, le support matériel de RIOT est aujourd'hui particulièrement étoffé : le système peut fonctionner sur plusieurs architectures matérielles, du 8bit au 32 bit, sur ARM, AVR, MIPS, RISC-V, Xtensa. Le système est donc utilisable sur des cartes vendues par les principaux fabricants: Microchip, NXP, STMicroelectronics, Nordic, TI, Espressif, etc.

Un système modulaire

Comme on l'a vu précédemment, RIOT utilise une architecture micro-kernel autour de laquelle le développeur ajoute les modules nécessaires à son application. Ce mécanisme modulaire permet de n'utiliser que ce qui est réellement nécessaire à une application et donc de limiter sa taille (en kB) sur la mémoire du microcontrôleur.

Le système RIOT fournit tout un écosystème de modules permettant de developper des applications rapidement et efficacement sans avoir à réinventer la roue.

Ces modules sont regroupés en plusieurs catégories:

  • Les bibliothèques système: ces modules sont totalement indépendants du matériel et fournissent des fonctionnalités utiles pour développer une application. Par exemple, le module shell fournit une interface pour implémenter facilement un interpréteur de commandes via l'entrée-sortie standard (stdio), qui utilise généralement le lien série (UART) de la carte. On trouve des modules de cryptographie, un module de formatage efficace (en mémoire) des chaînes de caractères. Le module xtimer offre des fonctionnalités avancées pour ajouter des délais ou programmer des actions (callbacks) à différents moments de l'exécution de l'application.
  • Les pilotes de capteurs et d'actionneurs: ces modules reposent généralement sur la HAL (Hardware Abstraction Layer) pour pouvoir être utilisés sur tout type de microcontrôleur. Ils sont très pratiques pour ajouter facilement dans une application la lecture d'un capteur, contrôler un moteur ou un afficheur externe sans avoir à écrire soi-même ce code. On trouve également dans cette catégorie des pilotes pour des supports de stockage externe, comme des cartes SD ou encore des pilotes d'interface de communication (radio, CAN, etc).
  • Les modules de protocoles réseaux sont en fait une sous-catégorie des bibliothèques systèmes et fournissent des implémentations pour les protocoles couramment utilisés en IoT : CoAP, MQTT-SN, etc. Nous y reviendrons en détails dans le module 4 dédié à la “Communication réseau”.
  • Les paquetages externes sont des modules permettant d'importer des codes sources externes à RIOT dans une application. En effet, pour des raisons de maintenance de la base de code, il n'est pas possible d'ajouter tel quel du code provenant d'un autre projet. Par contre, il est toujours utile de pouvoir l'intégrer facilement dans son application. Dans les paquetages externes de RIOT, on retrouve des projets de tous types: interpréteurs embarqués pour Javascript ou LUA, systèmes de fichiers comme LittleFS ou fatfs, pile réseau complète comme lwip ou OpenThread, mais aussi des bibliothèques de cryptographie, des librairies graphiques voire même des bibliothèques de machine learning (uTensor, TensorFlow-Lite).

Le système de compilation de RIOT s'occupe également de gérer les dépendances entre m odules pour s'assurer de la cohérence de l'application qui sera générée : par exemple, pour charger un pilote utilisant le bus I2C et le système xtimer, il suffit simplement de charger le module correspondant à ce pilote et le système de compilation s'occupe de charger automatiquement les modules pour I2C et pour xtimer.

Les piles protocolaires réseaux

RIOT supporte la plupart des protocoles réseaux couramment utilisés pour l'Internet de Objets grâce à ce qu'on appelle des piles protocolaires réseaux. On parle de piles parce qu'elles implémentent les différentes couches du modèle OSI1) défini pour faire communiquer des équipements informatiques.

Parmi ces piles réseaux, certaines sont regroupées dans une famille destinée à la communication orientée IP (Internet Protocol). Cette famille implémente des protocoles compatibles avec l'Internet actuel et en pratique permettent de faire communiquer des objets avec d'autres équipements sur Internet (comme des ordinateurs, des smartphones; etc.). Ces piles réseaux IP reposent sur des technologies de communication filaire comme Ethernet ou radio comme le WiFi, 802.15.4 ou BLE. Ces protocoles seront abordés en détails dans le module 4 sur la “Communication réseau”.

Les piles réseaux fournies par RIOT le sont soit nativement, comme GNRC2), soit sous forme de paquetage externe, comme OpenThread(une implémentation open-source des spécifications de Thread) ou lwIP.

Enfin, d'autres piles réseaux sont également disponibles:

  • Pour le protocole CAN3), largement utilisé dans l'industrie automobile. Cette pile est implémentée nativement dans RIOT.
  • Pour le support du Bluetooth Low Energy4). Cette pile est fournie sous forme d'un paquetage du projet NimBLE.
  • Pour le support des réseaux LoRaWAN. Cette pile est fournie sous forme d'un paquetage du projet Loramac-node.

Organisation du code source

Le code source de RIOT est disponible librement sur GitHub.

Comme nous l'avons vu précédemment dans cette séquence et comme l'illustre la figure ci-dessous, l'architecture de RIOT s'articule autour de plusieurs briques essentielles:

  • Sa couche d'abstraction matérielle où se trouve tout le code dépendant du matériel et fournissant les interfaces de programmation communes;
  • Son noyau qui gère les différents contextes d'exécution, la communication entre les tâches, les priorités, etc.
  • Des bibliothèques systèmes (xtimer, etc.), des paquetages, etc.

D'autres briques offrent des niveaux d'abstraction supérieurs pour intégrer facilement des fonctionnalités plus complexes :

  • l'API netdev sert de pont entre des pilotes de communications hétérogènes (radio, Ethernet, etc) et les piles protocolaires (comme GNRC, LwIP ou OpenThread);
  • L'API SAUL (Sensor Actuator Uber Layer) fournit une interface commune permettant de lire les données de capteurs similaires (température, pression, etc.).

Le code source est organisé comme suit:

  • boards: contient le code spécifique au support des cartes. Le code de chaque carte supportée se trouve dans son propre dossier. Pour chaque carte, on définit ainsi le modèle du CPU utilisé, la configuration des horloges internes, les configurations par défaut des périphériques (UART, SPI, etc). On retrouve également la configuration par défaut du port série (par exemple si /dev/ttyACM0 est créé lorsqu'on branche la carte par USB sur un ordinateur), la configuration relative aux outils de programmation (OpenOCD, AVRdude, etc).
  • core: contient le code relatif au noyau, à la gestion des thread, la communication inter-processus.
  • cpu: contient le code spécifique au support des microcontrôleurs (fichier d'entête des fabricants, définitions, pilotes de periphériques internes). C'est aussi là que pour chaque architecture est implémentée la séquence d'initialisation du CPU (ou point d'entrée). Par exemple, cette fonction s'appelle reset_handler_default pour l'architecture ARM CortexM.
  • dist: contient le code des outils de gestion du projet RIOT. Ces outils sont généralement des scripts et sont utilisés par exemple par le système d'intégration continue de RIOT pour vérifier la qualité du code ou plus directement pour permettre au développeur de programmer une carte facilement.
  • doc: contient les fichiers statiques de documentation Doxygen qui permet de générer la documentation de RIOT automatiquement à partir du code source.
  • drivers: contient les pilotes haut-niveau pour les modules externes (capteurs, actionneurs, radios). C'est aussi dans ce dossier que se trouvent les définitions des API pour les périphériques internes au CPU (les implémentations concrètes se trouvant dans le dossier CPU).
  • examples: contient plusieurs exemples d'applications.
  • makefiles: contient une partie du code du système de compilation de RIOT.
  • pkg: contient tous les paquetages externes compatibles avec RIOT. Chaque paquetage externe se trouve dans son propre dossier.
  • sys: contient le code de toutes les bibliothèques systèmes, de la pile réseau GNRC (dans sys/net).
  • tests: contient le code des tests unitaires et des applications de test. En général, à chaque fonctionnalité/pilote/paquetage est associée une application de test donc ces applications peuvent aussi servir d'exemple (et il est aussi recommandé de regarder là pour avoir des exemples d'utilisation).

Le système de compilation

Introduction

Le système de compilation de RIOT correspond à l'ensemble des fichiers permettant de passer du code source d'une application écrite en C (ou C++) au code binaire (le firmware) qui sera écrit sur la mémoire flash du microcontrôleur. Ce système de compilation contient aussi les règles pour lancer la procédure d'écriture sur la mémoire flash, ouvrir un terminal série pour lire la sortie standard de l'application, lancer un déboggeur et plein d'autres choses utiles.

Le système de compilation de RIOT s'appuie sur l'outil make pour effectuer toutes ces actions. make est un outil largement répandu pour compiler du code source en langage machine directement exécutable mais il peut aussi être utilisé pour d'autres types de tâches. Ces tâches sont communément appelées cibles (ou target). Pour effectuer toutes les opérations nécessaires aux différentes tâches realisées par le système de compilation de RIOT (compiler, flasher, ouvrir un terminal, etc.) make a besoin de recettes. Les recettes make sont écrites dans les fichiers Makefile qui se trouvent un peu partout dans le code source de RIOT. La connaissance de la syntaxe utilisée pour écrire un Makefile n'est pas nécessaire pour aller plus loin dans ce Mooc. En effet, le système de compilation de RIOT se charge de la plupart des choses complexes et, comme nous le verrons plus loin, seules certaines variables doivent être renseignées.

Principe

Pour générer un firmware à partir du code d'une application, il faut lancer make en lui donnant en paramètre le Makefile qui se trouve dans le dossier de cette application. Par défaut, make cherche dans le dossier courant un fichier Makefile donc la compilation peut se faire de 2 façons:

  • En se plaçant dans le dossier de l'application et en invoquant make directement:
$ cd <application_dir>
$ make
  • En utilisant l'option -C de make pour spécifier le dossier de l'application :
$ make -C <application_dir>

Cette méthode est utile si on compile des applications qui se trouvent dans différents dossiers car il n'est alors pas nécessaire de changer de dossier à chaque fois.

C'est toujours le fichier Makefile de l'application qui doit être vu par make.

Variables et cibles pour le développement

La structure du Makefile d'une application est en général divisée en 3 parties:

  1. Définition de variables générales;
  2. Ajout des modules, fonctionnalités, paquetages nécessaires à l'application
  3. Inclusion des règles définies par le système de compilation

Dans la première partie, une variable est toujours définie : RIOTBASE. Cette variable correspond au chemin vers le dossier où se trouve le code source de RIOT. Elle servira ensuite à inclure les règles génériques (le fameux fichier Makefile.include) en fin de Makefile, dans l'étape 3.

D'autres variables, comme BOARD et APPLICATION sont aussi définies dans cette première partie :

  • BOARD contient le nom de la cible matérielle pour laquelle sera compilée notre application. L'utilisation de l'operateur ?= au lieu de = permet de surcharger cette variable depuis la ligne de commande.
  • APPLICATION contient le nom de l'application et est utilisée par exemple comme nom pour les fichiers de firmware générés (ces fichiers ont l'extension elf, bin ou hex).

Dans la seconde partie du Makefile sont ajoutés les modules nécessaires à l'application. Le nom d'un module correspond en général au nom du dossier dans lequel il se trouve dans le code de RIOT. En fonction du type de module à charger, 3 variables sont utilisées:

  • USEMODULE contient la liste des modules systèmes chargés dans l'application.
  • FEATURES_REQUIRED contient la liste des fonctionnalités du CPU nécessaires à l'application. Par exemple, pour les bus UART et SPI, on chargera les modules periph_uart et periph_spi.
  • USE_PKG contient la liste des paquetages à charger dans l'application. Les noms de paquetages possibles correspondants aux noms des sous-dossiers du dossier pkg.

Ces trois variables contiennent des listes donc on utilisera l'opérateur += pour les modifier.

Enfin le Makefile d'une application se ponctue par l'inclusion du fichier Makefile.include qui se trouve à la racine du code de RIOT.

Voici un exemple du fichier Makefile d'une application:

# Give a name to the application
APPLICATION = example_application

# If not already specified, use native as default target board
BOARD ?= native

# List the modules, features, packages needed by this application.
USEMODULE += xtimer

# Specify the path the RIOT code base
RIOTBASE ?= $(CURDIR)/../../RIOT

# Final step: include the Makefile.include file, which contains the build
# system logic and definitions of make targets
include $(RIOTBASE)/Makefile.include

Le fichier Makefile.include contient le code permettant de gérer les outils nécessaires en fonction de la cible (BOARD) choisie, des modules chargés, des paquetages. C'est aussi à partir de ce fichier que sont définies les cibles correspondant aux différentes tâches à réaliser :

  • all s'occupera de compiler le code de l'application et donc d'appeler le bon compilateur en fonction de l'architecture du CPU de la cible matérielle choisie. Appeler make sans cible revient à appeler la cible all.
  • flash appelle l'outil de programmation de la carte pour écrire sur la mémoire flash le firmware produit par la cible all. Parmi ces outils de programmation généralement utilisés on rencontre OpenOCD, JLink, Avrdude, Edbg, etc Ils dépendent de l'interface de programmation de la carte ou d'un éventuel programmeur externe branché sur la carte (JLink, ST-Link).
  • term appelle l'outil de connexion au port série de la carte. Cette cible permet d'afficher sur un ordinateur les messages écrits sur le port série de la carte.

Avec make, il est aussi possible d'invoquer plusieurs tâches en une commande, make se chargeant de les appeler dans le bon ordre (si le Makefile est bien écrit !). C'est ce qui permet par exemple de compiler, de flasher et de se connecter au port série de la carte en une commande :

$ make -C <application_dir> flash term

Autres cibles et variables utiles

En plus des cibles de base vues précédemment, le système de compilation fournit d'autres cibles qui peuvent s'avérer très utiles :

  • info-build retourne les informations utiles au sujet de la compilation d'une application : liste des cartes supportées, cpu, liste des modules chargés, etc).
  • flash-only est une alternative à flash pour ne pas recompiler avant de flasher(puisque flash dépend de all et pas flash-only).
  • list-ttys renvoie la liste des ports séries disponibles sur le PC hôte ainsi que les informations sur les cartes connectées (numéro de série). Cette cible est utile lorsque l'on travaille avec plusieurs cartes branchées en même temps sur machine locale).
  • debug permet de débogger une application s'exécutant directement sur le hardware à l'aide de GDB (GNU Debugger). Par exemple, pour une carte se programmant avec l'outil OpenOCD, cette cible:
    1. Lance un serveur GDB localement. Ce serveur exécutera les commandes de GDB directement sur la carte.
    2. Lance le client GDB, se connecte au serveur GDB et charge l'application. Il est alors possible d'utiliser les commandes de déboggage GDB pour contrôler l'exécution de son application (point d'arrêt, valeur de variable, affichage de la position dans le code C ou assembleur, etc.)

Pour obtenir la liste complète des cibles make disponibles, il suffit de taper make puis d'utiliser la touche <tab>. L'autocomplétion du shell affichera alors cette liste.

D'autres variables utiles peuvent aussi être utilisées dans le Makefile d'une application ou passées en ligne de commande. La liste complète de ces variables est documentée dans le fichier makefiles/vars.inc.mk.

Parmi ces variables, on utilise souvent:

  • CFLAGS pour paramétrer plus finement la compilation de son application. Avec cette variable il est possible de passer des valeurs de macros de préprocesseur pour activer ou désactiver la compilation de certaines parties du code. Par exemple:
    • l'option -DLOG_LEVEL=4 change le niveau de logging dans l'application à verbose qui affichera alors plus de messages. Attention cependant, la taille du firmware augmentera également.
    • l'option -DDEBUG_ASSERT_VERBOSE permet d'afficher plus de messages de debug en cas de FAILED ASSERTION (i.e un appel à la fonction assert avec une assertion fausse) : le message contiendra le nom du fichier et la ligne où s'est produit l'erreur, ce qui rend sa résolution plus facile. Utilisée dans le Makefile, la variable CFLAGS doit être modifiée en utilisant l'opérateur +=:
CFLAGS += -DDEBUG_ASSERT_VERBOSE -DLOG_LEVEL=4
  • DEVELHELP pour activer des vérifications diverses comme la compilation en mode debug, les assertions, l'affichage du nom des threads, etc.
  • PORT pour spécifier un autre port série que celui par défaut lorsque plusieurs cartes sont branchées en même temps. Cette variable est utilisée par la cible term de make pour ouvrir le terminal série. Par exemple, si le port série par défaut est /dev/ttyACM0 mais que list-ttys donne /dev/ttyACM1 pour le port série de la carte visible qui nous intéresse, on pourra passer PORT=/dev/ttyACM1 pour connecter le terminal sur le bon port.
  • *IOTLAB_NODE** pour utiliser de manière transparente des cartes hébergées sur la plateforme IoT-LAB. Cette variable peut contenir soit la valeur auto-ssh pour choisir automatiquement une carte correspondant à la valeur de BOARD soit directement le nom complet d'une carte sur la plateforme, par exemple m3-42.grenoble.iot-lab.info:
$ make BOARD=iotlab-m3 IOTLAB_NODE=auto-ssh -C examples/hello-world flash term

La commande précédente compilera l'application examples/hello-world pour la carte iotlab-m3, puis flashera le firmware généré sur une carte iotlab-m3 au hasard (parmi celle d'une expérience en cours !) et enfin ouvrira un terminal sur le port série de la carte. Cela revient à travailler comme si la carte était branchée directement sur l'ordinateur!

Quiz

RIOT est un système d'exploitation :

  • Permettant de piloter des microcontrôleurs;
  • De type micro-kernel;
  • multi-thread;
  • temps-réel;

Quelle est la licence de RIOT?

  • LGPL

Dans sa configuration minimale, une application utilisant RIOT :

  • nécessite moins de 3kB de RAM.
  • est composée d'un thread principal et d'un thread d'attente.

Dans la liste suivante, quels blocs composent la couche d'abstraction matérielle de RIOT ?

  • Le code lié à un CPU
  • Le code lié à une carte
  • Le code des pilotes de périphérique

Quelles piles protocolaires orientées IP sont disponibles dans RIOT?

  • GNRC
  • lwIP
  • OpenThread

Quels dossiers de RIOT contiennent du code dépendant du matériel?

  • boards
  • cpu
  • drivers

Parmi les commandes suivantes, lesquelles permettent de compiler une application RIOT se trouvant dans le dossier /tmp/riot-app sans changer de dossier et sans spécifier de cible spécifique?

  • make -C /tmp/riot-app
  • make -C /tmp/riot-app all

Dans l'application spécifiée dans la question précédente, quelles cibles faut-il passer à make pour compiler, écrire sur la flash et ouvrir un terminal?

  • flash term
  • term flash
  • all flash term
1)
Open Systems Interconnection
2)
GeNeRiC
3)
Controller Area Network
4)
BLE
cours/informatique/iot/iot_par_la_pratique_inria/320_presentation_riot.txt · Dernière modification : 2023/03/20 22:44 de yoann