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 :
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:
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.
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:
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.
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:
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.
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:
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:
D'autres briques offrent des niveaux d'abstraction supérieurs pour intégrer facilement des fonctionnalités plus complexes :
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);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:
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.
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:
$ cd <application_dir> $ make
$ 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.
La structure du Makefile d'une application est en général divisée en 3 parties:
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 :
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:
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 :
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
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 :
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 += -DDEBUG_ASSERT_VERBOSE -DLOG_LEVEL=4
$ 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!
RIOT est un système d'exploitation :
Quelle est la licence de RIOT?
Dans sa configuration minimale, une application utilisant RIOT :
Dans la liste suivante, quels blocs composent la couche d'abstraction matérielle de RIOT ?
Quelles piles protocolaires orientées IP sont disponibles dans RIOT?
Quels dossiers de RIOT contiennent du code dépendant du matériel?
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?
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?