OS - Kasimashi/Systemes-embarques GitHub Wiki
Un système d'exploitation permet de faire le lien entre l'utilisateur (le monde applicatif : application et logiciel) , et le matériel.
Dans le monde matériel on aura par exemple, la mémoire (permet le stockage des données et variables temporaires), le CPU, les périphériques d'entrés/sorties (Clavier ...). Le système d'exploitation est composé lui même d'un élément central appelé le noyau (ou kernel), qui fournis :
- Un ensemble de fonctionalités qui sont l'execution et l'ordonnancement des tâches.
- La gestion des ressources mémoires
- La manipulation d'un système de fichier (filesystem)
- Gestion de l'accès aux ressources matérielles (Drivers).
Sans rentrer dans le fonctionnement interne de l'OS: il y a plusieurs point d'entrés pour un développeur pour interargir avec l'OS.
- l'API de programmation d'application : permet d'interférer avec l'OS pour la conception d'applications.
- Parfois l'interface graphique ou IHM (Permet aux utilisateurs d'interférer directement avec l'OS via une interface)
- Les commandes : par des commandes on arrive à obtenir les informations liés à l'OS.
Il est bon de faire la différence entre un RTOS (Real-Time Operating System) et un GPOS (General Purpose Operating System).
Ce dernier est caractérisé par :
- L'utilisation d'une interface graphique pour faciliter la communication avec les utilisateurs.
- Il permet aussi la gestion des ressources matériel via des drivers.
- Permet l'execution d'applications
- Permet la gestion dynamique de la mémoire.
Exemple : Windows, Linux, MacOS
Adaptés aux systèmes embarqués c'est à dire à des systèmes qui sont :
- A empreinte mémoire faible permerttant une implémentation dans un microcontroleur.
- Déterministe : on sait avant le résultat et le temps d'execution d'une tâche.
- Préemptif : ils donneront l'execution aux tâches les plus prioritaires.
Dans les RTOS les plus populaires on trouve en 2022 :
- Deos (DDC-I)
- embOS (SEGGER)
- FreeRTOS (Amazon)
- Integrity (Green Hills Software)
- Keil RTX (ARM)
- LynxOS (Lynx Software Technologies)
- MQX (Philips NXP / Freescale)
- Nucleus (Mentor Graphics)
- Neutrino (BlackBerry)
- PikeOS (Sysgo)
- SafeRTOS (Wittenstein)
- ThreadX (Microsoft Express Logic)
- µC/OS II/III (Micrium)
- VxWorks (Wind River)
- Zephyr (Linux Foundation)
- Mbed OS (Mbed)
Plus d'informations : https://en.wikipedia.org/wiki/Comparison_of_real-time_operating_systems
Un système temps réel est un système en boucle fermée : Il permet un traitement continue d'une tâche.
Un programme se sachant rien faire tout seul nous allons utiliser des bibliothèques, des variables, des structures, pour procéder en boucle fermée nous utiliserons la structure suivante :
#include <stdio.h>
#define TOTO 1
void init()
{
// Initisalisation
}
void main()
{
// Init plateforme matérielle
init() ;
while (1)
{
read(); // Lire les entrées
process(); // Traitement
output(); // Ecrire les sorties (actionneurs).
}
}
Le système temps réel nécéssite d'avoir :
- Une exactitude logique : les sorties doivent être correcte dans leurs valeurs. Exemple : Correction d'une trajectoire pour le guidage d'un missile.
- Une exactitude temporelle : les sorties doivent être correcte en termes de temps d'arrivées (Avant des échéances bien définies). Exemple : Transmission d'un signal téléphonique : délais de bout en bout : 250ms.
Dans un RTOS : un résultat juste hors délais est considéré comme un résultat faux
Des exemple de contraintes concrètes :
Les contraintes de temps : déclenchement de l'airbag d'une voiture, synchronisation son et image d'un téléviseur ...
Ils peuvent être classés en plusieurs catégories :
La plupart des systèmes temps réels sont hybrides et doivent en plus recpecter des contraintes de coût, d'espace, de consommation d'énergie et de matériel.
Un retard dans l'obtention du résultat (non respect d'une contrainte temporelle) le rend inutile et entraine des conséquences dramatiques :
- Sur le fonctionnement du système
- Sur l'intégrité des personnes.
- Le cout financier qui est d'ordre supérieur ou égal à celui du système.
Exemple : Détection de missile, pilote automatique des avions, régulateur de vitesse pour l'automobile.
Un retard dans l'obtention du résultat (non respect d'une contrainte temporelle) peut le rendre inutile sans entrainer de conséquences dramatiques.
Exemple : Distributeur de billets, téléphones portables, jeux vidéos.
On distingue deux types de noyaux RTOS, les noyaux préemptifs, et les noyaux non préemptif ou collaboratif.
L'algorithme Round Robin : est un algorithme rotatif dont le principe est le suivant. Il mémorise dans une FIFO la liste des processus prêts , à chaques tâches on va allouer un quantum de temps d'execution (10-100ms). Après écoulement du temps, la tâche est préempté par la tâche suivante qui s'execute. Si la tâche se bloque ou se termine avant la fin de son quantum, le processeur est immédiatement alloué à la tâche suivante dans la file d'attente. L'avantage certain d'un round robin est qu'un processus long ne peut pas retarder excessivement l'execution d'une autre execution. On offre ainsi l'illusion de l'exécution simultannées des tâches (pseudo-parallélisme). Pour le round robin le choix du quantum est primordiale .
- Si le quantum est trop grand : on augmente le temps de réponse d'une tâche.
- Si le quantum est trop court : on perdra du temps dans la commutation de contexte.
Pour un réglage correct compter un quantum égale à 5 fois le temps de commutation de contexte.
On distingue tout d'abord deux types d'architecture:
- Les architectures mono-tâches qui attendent une suite de tâche : attendre un signal (Interruption, état d'un capteur), agir en fonction, le dimensionnement du processeur dépend du temps de réponse.
- Les architectures multitâches : avec une éxécution pseudo-parallèle de plusieurs tâches (Dans le cas d'une architecture monocoeur le temps CPU est partagé avec une impression de traitement simultannée) avec des problèmes qui se posent au niveau de : L'accès au processeur (donne à chaques tâches un temps d'execution), l'accès concurrent à la mémoire , l'accès aux périphériques. Il faut alors prévoir un ordonnancement permettant au système de remplir son rôle.
C'est quoi au juste une tâche ?
C'est une fonction C avec une boucle infinie contenant le code d'éxécution de la tâche :
void TaskName( void *pvParameters)
{
for(;;)
{
... //Code de la tâche
}
}
Une tâche doit posséder une priorité et sa propre zone mémoire pile (stack). La pile aura pour rôle de sauvegarder les variables locales de la tache ainsi qu'une structure de donnée TCB (Task-Control Block) contentant :
- Un identifiant ou code d'identification propre à chaques tâche
- Une priorité ou niveau de priorité
- Un état : Prête, bloquée, en cours, suspendue
- Un contexte concenant les informations liés à l'execution de la tâche comme : le program counter (PC), le stack pointer (SP), ...
Exemple pour Micrium :
struct os_tcb {
CPU_STK *StkPtr; /* Pointer to current top of stack */
void *ExtPtr; /* Pointer to user definable data for TCB extension */
#if (OS_CFG_DBG_EN == DEF_ENABLED)
CPU_CHAR *NamePtr; /* Pointer to task name */
#endif
#if ((OS_CFG_DBG_EN == DEF_ENABLED) || (OS_CFG_STAT_TASK_STK_CHK_EN == DEF_ENABLED))
CPU_STK *StkLimitPtr; /* Pointer used to set stack 'watermark' limit */
#endif
OS_TCB *NextPtr; /* Pointer to next TCB in the TCB list */
OS_TCB *PrevPtr; /* Pointer to previous TCB in the TCB list */
#if (OS_CFG_TASK_TICK_EN == DEF_ENABLED)
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_LIST *TickListPtr; /* Pointer to tick list if task is in a tick list */
#endif
(...)
Dans un RTOS le noyau va utiliser une zone mémoire RAM de taille configurable nommée "Heap" qui contient la pile et le TCB correspondant à chaque tâches gérée par le noyau.
Une tâche peut avoir différents états à savoir :
- Suspendu
- Prête
- Running
- Blocked
Certaines caractéristiques sont communes à de nombreux RTOS. L'une de ces similitudes est la fonction de blocage et de non blocage des appels.
Exemple Fonctions de blocages pour FreeRTOS :
- Cas particuler des fils d'attentes :
XQueueSend()
ouxQueueSendToBack()
: envoi d'une donnée vers une queue de messages à la suite des éléments déjà présents. Cette fonction n'est bloquante que si la queue de messages est pleine.XQueueReceive()
: Récupère la donnée en tête d'une queue de message : l'élément lu est retiré de la file d'attente. Le second élément passe alors en tête de file. Cette fonction n'est bloquante que si la queue de messages est vide. - Les obtentions de sémaphores :
taskYIELD()
est utilisé pour demander un changement de contexte vers une autre tâche. Cependant, s'il n'y a pas d'autres tâches à une priorité supérieure ou égale à la tâche qui appelle taskYIELD(), le planificateur RTOS sélectionnera simplement la tâche qui a appelé taskYIELD() pour s'exécuter à nouveau.
Un processus est un ensemble de tâches, qui possède un espace mémoire d'addressage en 3 parties :
- Le Heap Segment
- Le Data Segment
- Le Text Segment ou Code
Ce context switch fournit :
- Une sauvegarde des registres internes
- Le choix de la tâche suivante à exécuter
- Le contexte de la nouvelle tâche
Les sauvegardes des contextes se font par des PUSH (pousser dans la PILE) ou des PULL (lire de la PILE)
Un Sémaphore est un outil utilisé pour la protection de ressources partagées (variables, périphériques, espaces mémoires ...) Le principe de fonctionnement des sémaphores est très proche de celui des queues de messages Sous FreeRTOS il n'y a aucun fichier source propre aux sémaphores. Les API pour la gestion des sémaphores ne sont que des macros appelant des fonctions propres aux queues de messages (queue.c).
Un sémaphore binaire peut être vu comme une variable booléenne associée à une file d'attente préservant l'ordre d'arrivée des tâches (cf. queues de messages). Un sémaphore binaire Pris (Take) par une tâche ne peut plus l'être par une autre, ni même par elle-même tant qu'il n'est pas explicitement Rendu (Give). Une tâche cherchant à prendre un sémaphore binaire déjà pris se verra bloquée (blocked) jusqu'à ce que la ressource soit relâchée.
Prenons un exemple de scénario :
Le scénario précédent reflète par exemple une application où 3 tâches de même priorité cherchent simultanément à envoyer des données via un UART. La ressource partagée est alors l'UART qui est protégé par un sémaphore binaire. Durant ce laps de temps nous nous trouvons dans une section critique qui peut cependant être préemptée par le noyau. Les différentes tâches utiliseront donc l'UART à tour de rôle (exclusion mutuelle)
Mutex signifie MUTual EXclusion (ou exclusion mutuelle), il s'agit du concept très rapidement présenté durant le scénario présenté ci-dessus. Une exclusion mutuelle traduit la notion de protection de ressources partagées. Sous FreeRTOS, elle peut notamment être réalisée à partir d'un sémaphore binaire (inversion de priorité) ou d'un mutex (héritage de priorité). Pour FreeRTOS, le principe de fonctionnement de ces deux outils est exactement le même à ceci près que le Mutex effectue un héritage de priorité.
Illustrons le concept d'héritage de priorité :
Héritage de priorité : La tâche Task Low2 vient de prendre une ressource (Mutex) et elle ne la rendra qu'une fois avoir fini ce pourquoi elle l'a pris. Cependant la tâche Task Low1 est également prête (état ready), l'ordonnanceur applique donc le round-robin et partage le temps CPU entre les deux tâches de même priorité. Imaginons maintenant que la tâche Task High (de priorité supérieure) cherche également à prendre la ressource (Mutex). Ceci est impossible, le MUTEX ayant déjà été pris et elle se fait donc bloquer, c'est ce que l'on appelle l'inversion de priorité. Une tâche de haute priorité se fait bloquer par une tâche de plus basse priorité, le système de priorité choisi par le développeur est inversé. Cependant avec l'héritage de priorité, la tâche Task Low2 hérite temporairement de la priorité de Task High et pourra donc potentiellement finir de s'exécuter plus rapidement que sans héritage (plus de round-robin avec la tâche de même priorité) De façon général, dans un système temps réel une tâche ne doit jamais (ou le moins possible !) rester bloquée par une tâche de moindre priorité.
Le principe de fonctionnement d'un sémaphore à compteur est identique à celui d'un sémaphore binaire sauf que la ressource protégée peut maintenant être prise (Take) à plusieurs reprises. Elle peut être prise soit par la même tâche, soit par une nouvelle. Exemple de sémaphore à 4 jetons :
Une queue de messages (messages queue ou queue) ou boîte aux lettres (mailbox) est un outil logiciel asynchrone permettant des communications voir des synchronisations entre tâches. Une queue comporte deux files d'attentes : une stockant l'ordre d'arrivée des messages et une seconde stockant l'ordre d'arrivée des tâches. Une queue de messages contient un nombre fini d'éléments dont la taille est configurable. Elle est régie par le principe de fonctionnement d'une FIFO (First In First Out). Le premier entré dans la file sera le premier à en sortir. Généralement la mémoire pour une queue de messages est allouée sur la heap. Une queue de message peut avoir plusieurs écrivains et plusieurs lecteurs. Cependant généralement elles sont utilisés avec des écrivains multiples et un seul lecteur.
Les écrivains sont les tâches pouvant écrire dans une queue de messages. Les lecteurs sont donc celles susceptibles de la lire. Un élément lu est retiré de la file d'attente. Si un lecteur cherche à lire une queue de messages vide, il se fait bloquer jusqu'à l'arrivée d'un nouveau message. Pour des tâches de même priorité, ce sera la première bloquée qui sera la première réveillée (FIFO). FreeRTOS étant un exécutif temps réel, si une tâche de plus haute priorité se fait bloquer, elle passe en tête de file.
Les deux principales fonctions proposées par FreeRTOS sont :
-
xQueueSend()
ouxQueueSendToBack()
: envoi d'une donnée vers une queue de messages à la suite des éléments déjà présents. Cette fonction n'est bloquante que si la queue de messages est pleine. -
XQueueReceive()
: Récupère la donnée en tête d'une queue de messages. L'élément lu est retiré de la file d'attente. Le second élément passe alors en tête de file. Cette fonction n'est bloquante que si la queue de messages est vide. -
xQueueSendToFront()
écrit une donnée en tête de file d'attente. Les données sont alors prioritaires et placées en tête de la queue de message Dans l'exemple ci-dessous le noyau travaille en mode coopératif.
Ce scénario présente une application avec deux écrivains et un lecteur :
Certaines fonctions pour la gestion de queue de messages et de sémaphores utilisent un Timeout ou temps mort. Lorsqu'une tâche est bloquée après l'appel de l'une de ces fonctions, celle ci se réveillera (passage à l'état prêt) automatiquement après un laps de temps nommé Timeout, même si l'événement attendu n'est pas arrivé. L'utilisation du Timeout permet de garantir la robustesse d'un code en forçant par exemple le réveil d'une tâche en attente d'un événement devant être envoyé par une tâche boguée.
Si nous ne souhaitons pas utiliser cette fonctionnalité (Timeout infini), il suffit
de passer en paramètre la macro portMAX_DELAY
.
Emplacement de stockage simple pour partager une seule variable Peut être considérée comme une file d'attente à élément unique.
C'est un mécanisme fournit par l'OS pour la communication inter-tâche, pratique pour envoyer des données d'une tâche vers une autre. Elle donne sous forme d'un pointeur la variable à transmettre pour deux raisons : N'importe quelle quantité de donnée peut être envoyé. Et n'importe quel type de donnée également.
C'est un autre moyen pour la communication inter-proccess. Ce qui ne faut surtout pas faire une nouvelle fois et d'utiliser une mémoire partagée dans un contexte multi-threading.
C'est une communication interprocess plus compliquée à comprendre, c'est environs identique à une queue. Mais à la place de traiter un mot de donnée, il va traiter des chaîne d'octets de données prédéfini.
C'est très courant dans une application d'avoir besoin de mémoire dynamique, pour appeler de la mémoire dynamique il est courant d'utiliser la fonction malloc
: cependant il y a deux problèmes quand un malloc échoue :
- Que ce passe t-il si la fonction malloc échoue ? (Pas assez de mémoire)
- Le timing de malloc ? On a aucune idée de combien de temps cela prend.
Imaginons une heap qui est juste un bloc de 10Ko, on appelle la fonction malloc en demandant 3Ko de mémoire, il reste donc 7Ko. Maintenant allouons encore 4Ko de mémoire donc il reste 3Ko. Maintenant on libère les 3 premiers 3Ko de mémoire, il reste donc 6 Ko de mémoire libre. Et là si on demande 4ko de mémoire comme la mémoire demandée n'est pas contiguë, on échoue. Il est impossible de passer outre.
Le partitionnement est une solution à ce problème. Quand on partitionne une zone mémoire on fixe un morceau de taille fixe de la mémoire. Typiquement si on a plusieurs partition avec des chunks de tailles différentes
Presque chaques RTOS est équipé d'une source de temps, généralement donnée par un timer hardware qui interrompt le CPU à une certaine fréquence (appelée le tick rate). Ce tick rate est généralement choisi entre 10Hz et 1KHz (1KHz étant la fréquence la plus répandu).
Ce timer est généralement appelé sous plusieurs noms à savoir : Clock Tick, System Tick or, RTOS Tick. Plus le Tick rate est élevé plus le RTOS va prendre la main sur votre système.
Cette source de temps est utilisé dans les tâches pour le delay (sleep). Il permet ausi de donner des APIs de timeout qui peuvent être utilisés par des tâches qui sont en attente d'événements. Plus le Tick Rate est important, plus la résolution des délais et des timeouts est bonne. Par exemple, une tâche peut décider d'attendre l'arrivée dun packet ethernet, attend un certain temps, ensuite par en timeout.
Le pseudo-code ci dessous montre comment peux fonctionner un délais dans un service RTOS dans une tâche. Une chose à noter est que si on veut faire un délais d'un tick (c'est à dire 1ms si le Tick Rate est à 1000Hz), il se peux que vous ayez besoin de spécifier 2 ticks. La raison est que le CPU peux executer qu'une seule tâche à la fois à un temps donnée. S'il s'agit d'une tâche de faible priorité, il peut finir par s'exécuter juste avant l'arrivée du prochain tick, ce qui entraînerait potentiellement la réexécution immédiate de la tâche. Si vous aviez besoin de ce délai de 1 milliseconde, vous pourriez en fait vous retrouver sans délai du tout !
void Task (void)
{
// Task initialization
while (1) {
Delay(#ticks);
Perform some periodic work;
}
}
Le pseudo-code suivant montre comment une tâche peut attendre un événement (conversion ADC, paquet Ethernet, etc.) et spécifier un délai d'attente. Dans ce cas, le délai d'attente est utilisé pour éviter d'attendre indéfiniment que l'événement se produise. Le délai d'attente peut indiquer une défaillance matérielle ou peut être un résultat attendu.
void Task (void)
{
OS_ERR err;
// Task initialization
while (1) {
err = WaitForEvent(&EventObject, timeout);
if (err == TIMEOUT_OCCURRED) {
Event didn’t happen within timeout;
} else {
Event occurred, process;
}
}
}
- Idée recue n°1 : le RTOS Tick est le planificateur
Un RTOS exécute le planificateur chaque fois qu'un événement se produit, ce qui amène généralement le RTOS à déterminer si une tâche plus importante doit s'exécuter. L'interruption Tick (c'est-à-dire l'événement) n'est qu'un des nombreux événements d'un système basé sur RTOS qui provoque l'exécution du planificateur ! Ainsi, si un paquet Ethernet arrive et que c'est l' événement qu'une tâche attend, le RTOS déterminera s'il doit exécuter cette tâche immédiatement après le retour de l'ISR (pour l'Ethernet).
- Idée reçue n°2 : Le RTOS Tick est toujours nécessaire avec un RTOS
Bien que la plupart des RTOS aient effectivement besoin d'un Clock Tick, si votre application ne met jamais une tâche en veille ou n'utilise jamais de délais d'attente, vous pouvez simplement l'omettre de votre système ! Bien sûr, vous devez vous assurer que vous n'utilisez pas vraiment cette fonctionnalité (tic RTOS) car sinon les tâches ne se comporteront pas comme prévu. Si vous n'êtes pas sûr, activez simplement le RTOS Tick.
- Idée reçue n°3 : l'interruption RTOS Tick doit être la priorité la plus élevée
Ce n'est absolument pas vrai et en fait, je pourrais faire valoir qu'il devrait s'agir de l'une des interruptions les moins prioritaires de votre système et sinon, certainement inférieure aux interruptions en temps réel de votre application. Vous voudrez probablement accorder plus d'importance à l'échantillonnage des entrées analogiques, au contrôle d'un moteur à grande vitesse, à la réponse au trafic TCP/IP, etc. Le RTOS Tick est destiné aux retards grossiers et aux temporisations.
- Idée reçue n°4 : le taux de tic du RTOS doit être précis
Encore une fois, le but du tick est de fournir des délais et des délais d'attente grossiers . Si votre application nécessite une interruption périodique très précise, vous devez utiliser un temporisateur séparé et faire de son interruption une priorité élevée.
Idée reçue n°5 : Le Clock Tick doit être dédié au RTOS
Il est généralement vrai que lorsque vous affectez un temporisateur matériel au RTOS, il est utilisé pour les retards et les temporisations mais, dans certains cas, vous souhaiterez peut-être intercepter l'interruption du temporisateur et effectuer certaines opérations avant que le RTOS n'exécute sa fonction. Plus précisément, certains RTOS (comme uC/OS-III) vous permettent d'appeler une fonction définie par l'utilisateur (c'est-à-dire un crochet , alias un rappel ) avant d'appeler les services de tick RTOS. Le pseudo-code ressemble à l'extrait ci-dessous.
RTOS_TickISR(void)
{
Call user defined function; // i.e. a hook (a.k.a. callback)
Call the RTOS tick services;
}
Vous voudrez utiliser cette fonctionnalité si vous manquez de minuteries matérielles et/ou si vous souhaitez disposer d'une source de temps assez précise. L'appel de votre code avant le RTOS garantira qu'il ne sera pas soumis à la gigue de tâche causée par le planificateur RTOS.
- Idée reçue n°6 : vous avez toujours besoin d'une minuterie matérielle
Encore une fois, si vous manquez de minuteries matérielles et que votre système embarqué est alimenté par une source d'alimentation CA, vous pouvez simplement extraire la fréquence de ligne (votre ingénieur matériel devra s'impliquer) et obtenir 50 ou 60 Hz (selon le pays ) ou, détecter les passages à zéro de la ligne électrique obtenant ainsi le double de la fréquence (100 ou 120 Hz). Il s'avère que, du moins aux États-Unis, la précision à long terme est relativement stable et précise. Par conséquent, les horloges murales électriques alimentées en courant alternatif sont assez bonnes pour garder l'heure.
La résolution des tâches retardées est d'un tick, cependant, cela ne signifie pas que sa précision est d'un tick d'horloge.
Dans cet exemple, une tâche de priorité plus élevée et des ISR s'exécutent avant la tâche de priorité inférieure, qui doit être retardée d'un tick. Les tâches tentent de retarder de 20 ms, mais en raison de leur priorité, elles s'exécutent à des intervalles variables.
Cela provoque une gigue dans l'exécution de la tâche
Si la tâche de haute priorité prolonge son exécution au-delà d'un top d'horloge, la tâche de basse priorité peut même être retardée de deux tops d'horloge et manquer son échéance.
Ces situations existent avec tous les noyaux temps réel et sont liées à la charge de traitement du processeur et éventuellement à une conception système incorrecte. Solution possible :
- Si un délai minimum de 1 tick est nécessaire, retardez de 2 ticks.
- Accélérer la cadence de votre CPU.
- Augmenter le temps entre deux interruptions ticks.
- Reévaluer et réorganisez les priorités des tâches.
- Ecrivez le code sensible en language assembleur.
- Choisir un processeur plus rapide.