Fondamentaux de FreeRTOS et configuration de l'environnement de simulation Linux

Aperçu de FreeRTOS

FreeRTOS est un système d'exploitation temps réel (RTOS) open source conçu pour les microcontrôleurs et les processeurs embarqués. Il prend en charge l'ordonnancement préemptif, coopératif et par tranches de temps, ce qui le rend idéal pour les applications nécessitant une réactivité déterministe. Cette analyse technique se base sur l'architecture du noyau FreeRTOS version 10.4.1.

Configurasion du simulateur POSIX/Linux

Pour tester et déboguer les applications FreeRTOS sans matériel cible, il est possible d'utiliser l'environnement de simulation POSIX sous Linux. Cette approche permet d'exécuter le noyau RTOS directement sur un système hôte, facilitant ainsi le développement et l'analyse.

Installation des dépendances

Le simulateur nécessite la bibliothèque libpcap pour la gestion des interfaces réseau. Voici les commandes d'installation selon votre environnement :

# Pour les distributions basées sur Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y libpcap-dev

# Pour les systèmes basés sur RPM (Fedora/CentOS/RHEL)
sudo dnf install -y libpcap-devel

# Pour macOS (via Homebrew)
brew install libpcap

Compilation et exécution

Après avoir récupéré les sources du simulateur FreeRTOS pour POSIX, la compilatoin s'effectue via l'outil make dans le répertoire principal du projet. L'exécutable généré peut ensuite être lancé directement depuis le terminal :

make -j$(nproc)
./build/posix_simulator_executable

Architecture et gestion des tâches

Le cœur de FreeRTOS repose sur son gestionnaire de tâches. Comprendre le cycle de vie, les priorités et la gestion de la mémoire de ces tâches est essentiel pour développer des applications embarquées robustes.

Système multitâche préemptif

FreeRTOS utilise un ordonnanceur préemptif. Cela signifie qu'une tâche de haute priorité peut interrompre immédiatement une tâche de priorité inférieure dès qu'elle devient prête à s'exécuter, garantissant ainsi le respect des contraintes temporelles critiques du système.

Caractéristiques des tâches

Chaque tâche s'exécute dans son propre contexte et possède son propre environnement d'exécution. L'ordonnanceur assure qu'à tout instant, une seule tâche occupe le processeur. Lors d'un changement de contexte, l'état du processeur (registres, pointeurs) est sauvegardé dans la pile dédiée de la tâche, permettant une reprise transparente ultérieurement. Les tâches doivent être conçues pour être réentrantes si elles partagent des ressources dans un environnement préemptif.

États d'une tâche

Une tâche évolue constamment parmi quatre états distincts :

  • En exécution (Running) : La tâche possède actuellement le contrôle du processeur. Sur une architecture monocœur, une seule tâche peut être dans cet état à la fois.
  • Prête (Ready) : La tâche est éligible à l'exécution mais n'a pas encore obtenu le processeur, généralement parce qu'une tâche de priorité égale ou supérieure est en cours d'exécution.
  • Bloquée (Blocked) : La tâche est en attente d'un événement externe (expiration d'un délai via vTaskDelay(), disponibilité d'un sémaphore, réception dans une file d'attente). Cet état inclut généralement un délai d'expiration (timeout).
  • Suspendue (Suspended) : La tâche est explicitement mise en pause par l'ordonnanceur et ne sera pas planifiée tant qu'elle n'est pas explicitement reprise. Contrairement à l'état bloqué, il n'y a pas de timeout automatique.

Priorités et ordonnancement

Les priorités sont configurables de 0 (la plus basse, généralement assignée à la tâche idle) à configMAX_PRIORITIES - 1 (la plus haute). L'ordonnanceur sélectionne toujours la tâche prête avec la priorité la plus élevée. Si l'option configUSE_TIME_SLICING est activée, les tâches de même priorité partagent le temps processeur via un algorithme de type tourniquet (Round-Robin).

Bloc de contrôle de tâche (TCB)

Chaque tâche est représentée en mémoire par une structure de données appelée Task Control Block (TCB). Voici une représentation restructurée de cette structure, mettant en évidence les éléments critiques pour la gestion du contexte et l'optimisation mémoire :

typedef struct TaskControlBlockStruct {
    volatile StackData_t * currentStackPointer;  // Doit impérativement être le premier champ pour le contexte matériel
    
    #if (ENABLE_MPU_PROTECTION == 1)
        MemoryProtectionConfig mpuConfig;        // Configuration de la protection mémoire spécifique au port
    #endif

    ListItem_t taskStateNode;                    // Nœud pour les listes d'état (Ready, Blocked, Suspended)
    ListItem_t taskEventNode;                    // Nœud pour les listes d'attente d'événements
    uint32_t taskPriorityLevel;                  // Niveau de priorité (0 = minimum)
    
    StackData_t * stackBaseAddress;              // Adresse de début de la pile allouée
    char taskIdentifier[ MAX_NAME_LENGTH ];      // Nom de la tâche pour le débogage

    #if (TRACK_STACK_OVERFLOW == 1)
        StackData_t * stackLimitAddress;         // Adresse limite pour la détection de débordement
    #endif

    #if (ENABLE_MUTEX_SUPPORT == 1)
        uint32_t originalPriority;               // Priorité de base pour l'héritage de priorité
        uint32_t ownedMutexCount;                // Nombre de mutex actuellement détenus
    #endif

    #if (ENABLE_TASK_NOTIFICATIONS == 1)
        volatile uint32_t notificationValue;     // Valeur de notification directe
        volatile uint8_t notificationStatus;     // État de la notification
    #endif

    uint8_t isStaticallyAllocated;               // Indicateur d'allocation (statique vs dynamique)
} TaskControlBlock_t;

Gestion de la pile des tâches

La pile (stack) est indispensable pour sauvegarder le contexte d'exécution (registres du CPU, variables locales) lors des préemptions. Lors de la création d'une tâche via des méthodes dynamiques, le noyau alloue automatiquement cet espace mémoire.

La taille réelle de la pile dépend de l'architecture matérielle cible. Le type de base pour les éléments de la pile est défini dans les macros du port d'adaptation :

// Définition du type de données pour la pile selon l'architecture matérielle
#if defined(ARCH_64_BIT)
    typedef uint64_t StackData_t;
    #define STACK_WORD_SIZE_BYTES 8
#else
    typedef uint32_t StackData_t;
    #define STACK_WORD_SIZE_BYTES 4
#endif

// Macro pour calculer la taille mémoire réelle à allouer en octets
#define CALCULATE_STACK_MEMORY_SIZE(requested_words) \
    ((requested_words) * STACK_WORD_SIZE_BYTES)

Ainsi, si une tâche requiert 128 mots de pile, l'empreinte mémoire réelle sera de 512 octets sur un processeur 32 bits, et de 1024 octets sur une architecture 64 bits.

Étiquettes: FreeRTOS RTOS POSIX simulation multitâche

Publié le 10 juillet à 04h15