Les besoins pour un système d'exploitation temps réel (RTOS)
Une question qui se pose pour développer une application embarquée: devons-nous utiliser un système d’exploitation (OS) ? La réponse dépend évidemment de beaucoup de facteurs, mais dans tous les cas les points suivants doivent être considérés:
-
Le besoin d’un certain niveau d’abstraction du matériel est évident. Le matériel est spécifique à chaque fabricant, même si ces fabricants adoptent un standard d’architecture comme ARM. La manipulation du matériel exige non seulement des connaissances de programmation, mais également une connaissance précise du matériel. Plus le niveau d’abstraction est élevé, moins le programmeur doit se soucier des détails de chaque matériel sur lequel le programme doit fonctionner.
-
Programmer une application en suivant le modèle super-loop (boucle d’événements séquentiels infinie) limite les possibilités de développement d’applications modulaires facilement testables. Il est nécessaire de pouvoir modéliser une application en tâches concurrentes communiquant entre elles.
Un des objectifs des systèmes d’exploitation est de permettre la création de code qui peut être facilement porté d’un matériel à un autre. C’est dans ce but que les systèmes d’exploitation permettent de cacher les détails matériels ou, en d’autres termes, de les abstraire. Au delà du matériel, les systèmes d’exploitation mettent également à disposition des programmeurs des outils permettant de gérer les ressources matérielles et de développer des entités d’exécution sous forme de processus/tâches/threads. Dans ce chapitre, nous démontrons l’utilité des abstractions fournies par les systèmes d’exploitation.
Exemple: programmation haut-niveau vs. bas-niveau
Contrairement aux applications fonctionnant par exemple dans un navigateur ou sur un appareil mobile, les programmes fonctionnant sur systèmes embarqués sont souvent développés avec un faible niveau d’abstraction, très proche du matériel. Du point de vue des langages de programmation, le plus bas niveau d’abstraction est bien sûr le langage assembleur. Celui-ci offre l’avantage de pouvoir produire du code très efficace et très optimisé, au prix d’une connaissance approfondie du processeur devant exécuter le code. Une programmation bas-niveau permet également un contrôle précis du matériel et un accès direct à la mémoire du microcontrôleur. Le grand désavantage d’une approche bas-niveau est le manque de portabilité du programme, dans le sens où le code source, écrit par le développeur, est spécifique à un certain matériel et doit donc être modifié pour fonctionner sur différents processeurs ou différents microcontrôleurs. Le code source est également plus difficile à lire, à maintenir et à réutiliser. Tout ceci résulte en général dans une productivité assez faible.
Ces désavantages sont autant de motivations à utiliser un langage haut-niveau tel que C++, en combinaison avec des librairies haut-niveau permettant d’abstraire les détails du matériel. De plus, de nos jours, les compilateurs atteignent un niveau d’optimisation largement suffisant – parfois même supérieur à ce que beaucoup de programmeurs sont capables de programmer. De plus, les outils de développement sont aussi suffisamment efficaces pour rendre le développement de systèmes embarqués utilisant un système d’exploitation aisé. Pour terminer, un langage orienté objet et la mise à disposition de librairies facilitent la production de code facilement réutilisable et testable.
Afin de démontrer les avantages de l’utilisation d’une approche haut-niveau, nous allons utiliser l’exemple le plus classique de la programmation embarquée: blinky.
Approche bas-niveau ou haut-niveau ?
L’approche bas-niveau réalise le mécanisme de clignotement en utilisant les registres du périphérique du microcontrôleur, alors que l’approche haut-niveau utilise un composant de la librairie Mbed OS. Cette différence est schématisée dans le diagramme ci-dessous:
Le programme blinky
Le programme blinky est un programme très simple utilisant le mécanisme de la super-loop (boucle infinie). A l’intérieur de la boucle, le programme appelle simplement deux méthodes:
- La première méthode inverse la valeur de la LED.
- La deuxième méthode crée une attente passive de la durée de l’intervalle de clignotement.
Le squelette du programme est donné ci-dessous:
// include mbed in both implementation for initialization and sleep
#include "mbed.h"
// Blinking rate in milliseconds
// sleep_for requires a std::chrono::duration parameter
static constexpr std::chrono::milliseconds BLINKING_RATE = 5000ms;
int main() {
// performs some initialization
while (true) {
// toggle the led
// wait/sleep for the blinking interval time
}
}
Ce squelette est utilisé pour réaliser l’approche bas-niveau et haut-niveau. Dans cet exemple, nous nous concentrons sur la réalisation du mécanisme de clignotement pour démontrer les différences des deux approches. Une approche entièrement bas-niveau devrait également réaliser le mécanisme d’attente sans utiliser la librairie Mbed OS.
Réalisation bas-niveau en utilisant les registres du GPIO
Nous réalisons tout d’abord le mécanisme de clignotement dans le langage C++, mais avec une approche proche du langage assembleur, en manipulant des adresses et des pointeurs. Le clignotement de la LED peut être réalisé en utilisant les registres du GPIOs requis et en y écrivant les valeurs requises. En C/C++, nous pouvons réaliser ceci avec des pointeurs contenant l’adresse des registres. Le code pour le mécanisme de clignotement bas-niveau est donné ci-dessous:
#include <stdint.h>
struct GPIO {
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDR;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFRH;
};
int main()
{
constexpr unsigned int ledPin = 0;
constexpr unsigned int ledPinMask = 1 << ledPin;
// Register map
volatile uint32_t* RCC_AHB1ENR = (uint32_t*)(0x40023800 + 0x30);
volatile GPIO* ledGPIO = (GPIO*)0x40021000;
// Enable RCC Clock for GPIO Port E
*RCC_AHB1ENR |= 0x10;
// configure the pin as output (mode 0x01)
ledGPIO->MODER |= 0x01 << (ledPin * 2);
while (true) {
// toggle the led by setting and resetting it
// alternatively in each iteration
uint32_t pin_state = ((ledGPIO->ODR & ledPinMask) ? 1 : 0);
if (pin_state) {
ledGPIO->BSRR = ledPinMask << 16; // turning off
} else {
ledGPIO->BSRR = ledPinMask; // turning on
}
// busy wait for the blinking interval time
// using a rough estimation...
// and dependent on the CPU speed.
for (uint64_t i = 0; i < 10000000; i++) {
asm("nop");
}
}
}
Lisez les commentaires dans le code attentivement afin de bien comprendre cette réalisation bas-niveau. Ce code est efficace - probablement presque aussi efficace que du code assembleur, mais il est presque illisible. Une compréhension des adresses utilisée ainsi que le nom des variables aident à une meilleure compréhension, mais il n’est en aucun cas portable. Les adresses utilisées dépendent de la cible – ainsi les adresses données dans le code ci-dessus ne fonctionnent que pour la cible DISCO_F412 utilisée dans le cadre de ce cours. En fait, le code ci-dessus compilera pour toutes les cibles, mais ne fonctionnera que pour notre cible. Le code permettant d’attendre est réalisé par une attente active, ce qui n’est pas souhaitable. Une attente passive est également plus difficile à réaliser sans RTOS.
Exercice Les besoins pour un système d’exploitation temps réel (RTOS)/1
Pourquoi n’est-il pas souhaitable d’effectuer des attentes actives (qui occupent le CPU) ?
Solution
- Une attente active empêchera tout d’abord le CPU d’entrer en mode d’économie d’énergie. Comme présenté dans le chapitre précédent, les processeurs Cortex-M sont dotés de la possibilité de gérer leur consommation avec différents Sleep modes. Faire travailler le CPU pour attendre empêche l’activation de tout mode Sleep.
- Dans le cadre d’une application multi-tâches et en fonction des différents algorithmes d’ordonnancement, si une tâche doit attendre il est alors préférable d’attendre à l’aide d’un
Timer
et de donner l’opportunité à l’ordonnanceur de planifier une autre tâche.
Réalisation haut-niveau à l’aide de l’API Mbed OS
L’API Mbed OS fournit non seulement au développeur un environnement RTOS (avec les outils associés), mais elle fournit également une API pour:
- les abstractions E/S
- La gestion de la mémoire
- La gestion du système de fichiers
- Le support pour la communication.
L’API Mbed OS offre probablement un des outils les plus simples et compréhensibles permettant de programmer les microcontrôleurs ARM Cortex-M. L’API offerte utilise le langage C++ et est donc orientée-objet, permettant d’encapsuler de nombreuses fonctionnalités dans des classes faciles à utiliser.
Démontrons maintenant ces points à l’aide du même programme blinky
. Comme déjà présenté dans le chapitre GPIO, nous utilisons la classe DigitalOut. Avec ce mécanisme, l’inversion de l’état de la LED peut être écrite sur une seule ligne: led = !led
. Le mécanisme d’attente peut également être facilement réalisé à l’aide de l’API Mbed OS, tout en transformant l’attente active en attente passive. Nous étudierons dans les chapitres liés à l’ordonnancement pourquoi une attente passive est requise. A ce stade, il est important de souligner qu’une attente passive permet au CPU d’entrer en mode sleep
ou même deep sleep
(voir Power management pour plus de détails).
Le programme blinky
écrit à l’aide de l’API Mbed OS est donné ci-dessous:
#include "mbed.h"
// Blinking rate in milliseconds
static const std::chrono::milliseconds BLINKING_RATE = 500ms;
int main() {
// Initialise the digital pin LED1 as an output
DigitalOut led1(LED1);
while (true) {
// toggle the led value. The statement below uses both
// the assignment and int() operator of the DigitalOut class
led1 = !led1;
// sleeps for the blinking interval time
ThisThread::sleep_for(BLINKING_RATE);
}
}
Les détails expliquant ce code ont déjà été donnés dans le chapitre Microcontrôleur et périphériques. Pour rappel, dans ce code, la déclaration de la variable led1
permettra d’initialiser correctement les ports et adresses. Le hardware est ainsi complètement abstrait pour le code de l’application et est donc invisible pour le programmeur. Le code est aussi entièrement portable et peut ainsi être utilisé pour toute cible compatible.
Pour une meilleure compréhension de l’encapsulation des ports et adresses, compilez le code et démarrez une session de debugging. En accomplissant un step into lors de l’exécution de l’instruction led1 = !led1
. Vous pourrez ainsi observer les valeurs des structures de données représentant la LED et les comparer au code bas-niveau donné plus haut.
Mbed OS et CMSIS
Afin de construire les abstractions utilisées dans le programme blinky haut-niveau, Mbed OS utilise le standard Cortex Microcontroller Software Interface Standard (CMSIS). CMSIS est une couche d’abstraction pour les processeurs de la famille Cortex-M indépendante des fabricants. Cette couche d’abstraction fournit une interface standardisée permettant de contrôler le processeur plus facilement et indépendamment du fabricant spécifique, de manière similaire à LibOpenCM3. Cela permet d’améliorer la portabilité du code entre tous les processeurs de la famille Cortex-M. La figure ci-dessous illustre cette couche d’abstraction.
La standardisation par CMSIS inclut:
- Les fonctions pour accéder au et configurer le système d’interruption, le bloc contrôle du système ainsi que les timers et tickers.
- L’accès aux registres spéciaux.
- L’accès aux instructions spéciales.
- Les fonctions d’initialisation.
Les avantages de l’utilisation de CMSIS sont:
- Un portage plus aisé du code d’application d’un processeur Cortex-M à un autre.
- Une réutilisation plus facile d’une application à l’autre par l’utilisation de librairies fonctionnant sur différents processeurs Cortex-M.
- Une meilleure compatibilité lors de l’intégration de composants software utilisant le même standard.
- Une plus grande densité de code et une plus petite empreinte mémoire grâce aux optimisations réalisées dans la couche CMSIS.
Pour conclure cette section et la présentation de cet exemple, nous montrons ci-dessous le diagramme de l’architecture CMSIS: