Horloge système (SysTick)
Le noyau temps réel FreeRTOS utilise le compteur de ticks de l'horloge système pour mesurer le temps. À chaque incrémentation du tick, le noyau vérifie si des tâches bloquées doivent être réveillées. Lors d'une interruption de tick, une tâche réveillée peut avoir une priorité plus élevée que la tâche interrompue.
États des tâches
- Exécution : La tâche est en cours d'exécution sur le processeur.
- Prête : La tâche est prête à s'exécuter mais attend son tour.
- Bloquée : La tâche attend un événement, comme une temporisation ou une ressource.
- Suspendue : La tâche est volontairement suspendue par l'application.
Commutation de tâches
Préemptif
Les tâches de priorité supérieure peuvent interrompre immédiatement les tâches de priorité inférieure.
Par tranches de temps
Les tâches de même priorité s'exécutent à tour de rôle pendant des intervalles de temps définis.
Listes de tâches
Liste prête : pxReadyTasksList[priorité]
Sur une architecture 32 bits, un registre de 32 bits représente les priorités de 0 à 31, où 31 est la plus élevée.
Liste bloquée : pxDelayedTaskList
Liste suspendue : xSuspendedTaskList
Commutation de contexte
La commutation de contexte se produit lorsqu'une tâche de priorité supérieure devient prête ou lors d'une temporisation.
Fin de tâche
Auto-suppression avec vTaskDelete(NULL)
La tâche se spuprime elle-même, et la tâche idle libère la structure TCB.
Suppression externe avec vTaskDelete(handle)
Une tâche supprime une autre tâche, puis libère la structure TCB.
Tâche idle
Une tâche idle est créée automatiquement avec la priorité 0 (la plus basse). Si des tâches se créent et suppriment fréquemment, la tâche idle doit avoir le temps de s'exécuter pour libérer les ressources. Utilisez vTaskDelay() au lieu de mDelay() pour permettre la planification.
Synchronisation et exclusion mutuelle
Lorsque plusieurs tâches accèdent à une ressource partagée comme le port série, une synchronisation est nécessaire pour éviter les conflits. Les mécanismes inclus : notifications de tâches, files d'attente, groupes d'événements, sémaphores et mutex. Ils offrent des opérations comme acquisition/libération, blocage/réveil et temporisation.
Notifications de tâches
Basées sur une valeur dans le TCB de la tâche, pouvant être écrasées. La notification est envoyée à une tâche spécifique et ne peut être lue que par elle-même.
Files d'attente
Permettent de stocker plusieurs éléments de données. Les tâches et les interruptions peuvent écrire et lire des données.
Création
QueueHandle_t handleFile = xQueueCreate(5, sizeof(int32_t));
Réinitialisation
xQueueReset(handleFile);
Écriture
xQueueSend(handleFile, &valeurDonnee, delaiAttente);
xQueueSendToBack(handleFile, &valeurDonnee, delaiAttente);
xQueueSendToBackFromISR(handleFile, &valeurDonnee, &reveilPrioritaire);
xQueueSendToFront(handleFile, &valeurDonnee, delaiAttente);
xQueueSendToFrontFromISR(handleFile, &valeurDonnee, &reveilPrioritaire);
Lecture
xQueueReceive(handleFile, &bufferReception, delaiAttente);
xQueueReceiveFromISR(handleFile, &bufferReception, &tacheReveillee);
Suppression
vQueueDelete(handleFile);
Consultation
uxQueueMessagesWaiting(handleFile);
uxQueueSpacesAvailable(handleFile);
Écrasement et aperçu
Pour les files de longueur 1, xQueueOverwrite() écrase les données sans blocage. xQueuePeek() copie les données sans les supprimer.
xQueueOverwrite(handleFile, &valeurDonnee);
xQueueOverwriteFromISR(handleFile, &valeurDonnee, &reveilPrioritaire);
xQueuePeek(handleFile, &bufferReception, delaiAttente);
xQueuePeekFromISR(handleFile, &bufferReception);
Exemple d'utilisation de base
QueueHandle_t fileDonnees;
int main(void) {
prvConfigurationMateriel();
fileDonnees = xQueueCreate(5, sizeof(int32_t));
if (fileDonnees != NULL) {
xTaskCreate(fonctionEnvoyeur, "Env1", 1000, (void *)100, 1, NULL);
xTaskCreate(fonctionEnvoyeur, "Env2", 1000, (void *)200, 1, NULL);
xTaskCreate(fonctionRecepteur, "Recept", 1000, NULL, 2, NULL);
vTaskStartScheduler();
}
return 0;
}
static void fonctionEnvoyeur(void *parametres) {
int32_t valeurEnvoyee = (int32_t)parametres;
for (;;) {
if (xQueueSendToBack(fileDonnees, &valeurEnvoyee, 0) != pdPASS) {
printf("Échec d'envoi à la file.\r\n");
}
}
}
static void fonctionRecepteur(void *parametres) {
int32_t valeurRecue;
const TickType_t attente = pdMS_TO_TICKS(100UL);
for (;;) {
if (xQueueReceive(fileDonnees, &valeurRecue, attente) == pdPASS) {
printf("Reçu = %d\r\n", valeurRecue);
} else {
printf("Échec de réception de la file.\r\n");
}
}
}
Exemple avec identification de source
typedef enum { idVitesseMoteur, idConsigneVitesse } SourceID_t;
typedef struct { SourceID_t idDonnee; int32_t valeurDonnee; } Donnee_t;
static const Donnee_t donneesEnvoi[2] = {
{ idVitesseMoteur, 10 },
{ idConsigneVitesse, 5 }
};
QueueHandle_t filePartagee;
int main(void) {
prvConfigurationMateriel();
filePartagee = xQueueCreate(5, sizeof(Donnee_t));
if (filePartagee != NULL) {
xTaskCreate(envoyeurDonnees, "TacheCAN", 1000, (void *)&donneesEnvoi[0], 2, NULL);
xTaskCreate(envoyeurDonnees, "TacheHMI", 1000, (void *)&donneesEnvoi[1], 2, NULL);
xTaskCreate(recepteurDonnees, "Recept", 1000, NULL, 1, NULL);
vTaskStartScheduler();
}
return 0;
}
static void envoyeurDonnees(void *parametres) {
for (;;) {
if (xQueueSendToBack(filePartagee, parametres, pdMS_TO_TICKS(100UL)) != pdPASS) {
printf("Échec d'envoi.\r\n");
}
}
}
static void recepteurDonnees(void *parametres) {
Donnee_t donneeRecue;
for (;;) {
if (xQueueReceive(filePartagee, &donneeRecue, 0) == pdPASS) {
if (donneeRecue.idDonnee == idVitesseMoteur) {
printf("CAN, Vitesse = %d\r\n", donneeRecue.valeurDonnee);
} else {
printf("HMI, Consigne = %d\r\n", donneeRecue.valeurDonnee);
}
}
}
}
La planification des tâches montre que les tâches de haute priorité remplissent la file, puis la tâche de lecture consomme les données, permettant la réécriture.
Groupes d'événements
Chaque événement est représenté par un bit (1 pour actif, 0 pour inactif). Ils permettent de signaler des combinaisons d'événements sans transférer de données, avec un effet de diffusion réveillant toutes les tâches en attente.
Sémaphores
Basés sur une valeur de comptage. L'acquisition décrémente le compteur, la libération l'incrémente.
Création d'un sémaphore binaire
SemaphoreHandle_t handleSemBinaire = xSemaphoreCreateBinary();
SemaphoreHandle_t handleSemBinaireStatique = xSemaphoreCreateBinaryStatic(&tamponStatique);
Création d'un sémaphore à comptage
SemaphoreHandle_t handleSemCompteur = xSemaphoreCreateCounting(10, 0);
SemaphoreHandle_t handleSemCompteurStatique = xSemaphoreCreateCountingStatic(10, 0, &tamponStatique);
Suppression
vSemaphoreDelete(handleSemaphore);
Acquisition et libération
xSemaphoreGive(handleSemaphore);
xSemaphoreGiveFromISR(handleSemaphore, &reveilPrioritaire);
xSemaphoreTake(handleSemaphore, delaiAttente);
Mutex
Un mutex (exclusion mutuelle) est un verrou binaire où seul le possesseur peut le libérer. Il protège les ressources critiques contre les accès non atomiques et garantit la réentrance des fonctions. L'initialisation est à 1, l'acquisition bloque les autres tâches, et la libération réveille les tâches en attente.
Portage de FreeRTOS
Obtention du code source
Le code source officiel est disponible sur le site de FreeRTOS pour la portabilité vers différentes architectures matérielles.