Par défaut, tous les objets du système FreeRTOS sont distribués dynamiquement - files d'attente, sémaphores, temporisateurs, tâches (threads) et mutex. Le programmeur ne voit que le "tas" - la zone où la mémoire est allouée dynamiquement à la demande d'un programme ou d'un système, et ce qui se passe à l'intérieur n'est pas clair. Combien reste-t-il? Inconnu Est-ce que quelque chose prend plus que ce dont vous avez besoin? Qui sait? Personnellement, je préfère résoudre les problèmes d'organisation de la mémoire même au stade de l'écriture du firmware, sans apporter d'erreurs d'exécution lorsque la mémoire s'est terminée de façon inattendue.
Cet article est une
suite logique
d'hier sur la distribution statique des objets dans la mémoire du microcontrôleur, uniquement maintenant par rapport aux objets FreeRTOS. Aujourd'hui, nous apprendrons à placer des objets FreeRTOS de manière statique, ce qui nous permettra de comprendre plus clairement ce qui se passe dans la RAM du microcontrôleur, comment exactement nos objets sont situés et combien ils occupent.
Mais simplement prendre et commencer à placer des objets FreeRTOS statiquement ne nécessite pas beaucoup d'intelligence - à partir de la version 9.0, FreeRTOS fournit des fonctions pour créer des objets placés statiquement. Ces fonctions ont un suffixe statique dans le nom et ces fonctions ont une excellente documentation avec des exemples. Nous écrirons des wrappers C ++ pratiques et beaux sur les fonctions FreeRTOS qui non seulement placeront les objets statiquement, mais cacheront également tous les abats, ainsi que fourniront une interface plus pratique.
Cet article est destiné aux programmeurs débutants, mais qui connaissent déjà les bases de FreeRTOS et les primitives de synchronisation des programmes multithread. Allons-y.
FreeRTOS est un système d'exploitation pour microcontrôleurs. Eh bien, pas un système d'exploitation complet, mais une bibliothèque qui vous permet d'exécuter plusieurs tâches en parallèle. FreeRTOS permet également aux tâches d'échanger des messages via des files d'attente de messages, d'utiliser des minuteries et de synchroniser les tâches à l'aide de sémaphores et de mutex.
À mon avis, tout micrologiciel où vous devez effectuer simultanément deux tâches (ou plus) peut être résolu beaucoup plus facilement et plus élégamment si vous utilisez FreeRTOS. Par exemple, lisez les lectures des capteurs lents et servez l'affichage en même temps. Seulement pour que sans freins, pendant la lecture des capteurs. En général, doit avoir! Je recommande fortement pour l'étude.
Comme je l'ai dit et écrit dans un article précédent, je n'aime pas vraiment l'approche de création dynamique d'objets si nous connaissons leur nombre et leur taille au stade de la compilation. Si de tels objets sont placés statiquement, alors nous pouvons obtenir une image plus claire et plus compréhensible de l'allocation de mémoire dans le microcontrôleur, et donc éviter les surprises lorsque la mémoire s'est soudainement terminée.
Nous considérerons les problèmes d'organisation de la mémoire FreeRTOS en utilisant la carte BluePill sur le microcontrôleur STM32F103C8T6 comme exemple. Afin de ne pas vous soucier du compilateur et du système de construction, nous travaillerons dans l'environnement ArduinoIDE, en installant le support pour cette carte. Il existe plusieurs implémentations d'Arduino pour STM32 - en principe, tout le monde le fera. J'ai installé
stm32duino conformément aux instructions du projet Readme.md, un chargeur de démarrage tel que mentionné
dans cet article . FreeRTOS version 10.0 est installé via le gestionnaire de bibliothèque ArduinoIDE. Compilateur - gcc 8.2
Nous proposerons une petite tâche expérimentale. Il peut ne pas y avoir beaucoup de sens pratique dans cette tâche, mais toutes les primitives de synchronisation qui sont dans FreeRTOS seront utilisées. Quelque chose comme ça:
- 2 tâches (threads) fonctionnent en parallèle
- une minuterie fonctionne également, qui envoie de temps en temps une notification à la première tâche à l'aide d'un sémaphore en mode signal-attente
- la première tâche, après avoir reçu une notification du temporisateur, envoie un message (nombre aléatoire) à la deuxième tâche via la file d'attente
- le second, ayant reçu le message, l'imprime sur la console
- laissez la première tâche imprime également quelque chose sur la console, et afin qu'ils ne combattent pas, la console sera protégée par le mutex.
- la taille de la file d'attente pourrait être limitée à un élément, mais pour le rendre plus intéressant, nous avons mis 1000
L'implémentation standard (selon la documentation et les tutoriels) peut ressembler à ceci.
#include <STM32FreeRTOS.h> TimerHandle_t xTimer; xSemaphoreHandle xSemaphore; xSemaphoreHandle xMutex; xQueueHandle xQueue; void vTimerCallback(TimerHandle_t pxTimer) { xSemaphoreGive(xSemaphore); } void vTask1(void *) { while(1) { xSemaphoreTake(xSemaphore, portMAX_DELAY); int value = random(1000); xQueueSend(xQueue, &value, portMAX_DELAY); xSemaphoreTake(xMutex, portMAX_DELAY); Serial.println("Test"); xSemaphoreGive(xMutex); } } void vTask2(void *) { while(1) { int value; xQueueReceive(xQueue, &value, portMAX_DELAY); xSemaphoreTake(xMutex, portMAX_DELAY); Serial.println(value); xSemaphoreGive(xMutex); } } void setup() { Serial.begin(9600); vSemaphoreCreateBinary(xSemaphore); xQueue = xQueueCreate(1000, sizeof(int)); xMutex = xSemaphoreCreateMutex(); xTimer = xTimerCreate("Timer", 1000, pdTRUE, NULL, vTimerCallback); xTimerStart(xTimer, 0); xTaskCreate(vTask1, "Task 1", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL); xTaskCreate(vTask2, "Task 2", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL); vTaskStartScheduler(); } void loop() {}
Voyons ce qui se passe dans la mémoire du microcontrôleur, si vous compilez un tel code. Par défaut, tous les objets FreeRTOS sont placés dans la mémoire dynamique. FreeRTOS fournit jusqu'à 5 implémentations de gestionnaires de mémoire qui sont difficiles à implémenter, mais en général, ils ont la même tâche - couper des morceaux de mémoire pour les besoins de FreeRTOS et de l'utilisateur. Les morceaux sont coupés soit dans le tas général du microcontrôleur (à l'aide de malloc), soit dans leur propre tas séparé. Le type de tas utilisé pour nous n'est pas important - de toute façon, nous ne pouvons pas regarder à l'intérieur du tas.
Par exemple, pour un tas du nom FreeRTOS, il ressemblera à ceci (sortie de l'utilitaire objdump)
... 200009dc l O .bss 00002000 ucHeap ...
C'est-à-dire nous voyons une grande pièce, à l'intérieur de laquelle tous les objets FreeRTOS sont coupés - sémaphores, mutex, temporisateurs, files d'attente et même les tâches elles-mêmes. Les 2 derniers points sont très importants. En fonction du nombre d'éléments, la file d'attente peut être assez grande et les tâches sont assurées de prendre beaucoup d'espace en raison de la pile, qui est également allouée avec la tâche.
Oui, c'est moins le multitâche - chaque tâche aura sa propre pile. De plus, la pile doit être suffisamment grande pour contenir non seulement les appels et les variables locales de la tâche elle-même, mais également la pile d'interruption, si cela se produit. Eh bien, comme une interruption peut survenir à tout moment, chaque tâche doit avoir une réserve sur la pile en cas d'interruption. De plus, les microcontrôleurs CortexM peuvent avoir des interruptions imbriquées, donc la pile doit être suffisamment grande pour accueillir toutes les interruptions si elles se produisent simultanément.
La taille de la pile de tâches est définie lorsque la tâche est créée par le paramètre de la fonction xTaskCreate. La taille de la pile ne peut pas être inférieure au paramètre configMINIMAL_STACK_SIZE (spécifié dans le fichier de configuration FreeRTOSConfig.h) - il s'agit de la même réserve pour les interruptions. La taille du segment de mémoire est définie par le paramètre configTOTAL_HEAP_SIZE et dans ce cas est de 8 Ko.
Essayez maintenant de deviner si tous nos objets tiendront dans un tas de 8 Ko? Et quelques objets? Et quelques tâches supplémentaires?Avec certains paramètres FreeRTOS, tous les objets ne tenaient pas dans le tas. Et cela ressemble à ceci: le programme ne fonctionne tout simplement pas. C'est-à-dire tout est compilé, versé, mais le microcontrôleur se bloque et c'est tout. Et devinez que le problème est exactement la taille du tas. J'ai dû augmenter un groupe à 12 Ko.
Arrêtez, quelles sont les variables xTimer, xQueue, xSemaphore et xMutex? Ne décrivent-ils pas les objets dont nous avons besoin? Non, ce ne sont que des poignées - pointeurs vers une certaine structure (opaque), qui décrit les objets de synchronisation eux-mêmes
200009cc g O .bss 00000004 xTimer 200009d0 g O .bss 00000004 xSemaphore 200009cc g O .bss 00000004 xQueue 200009d4 g O .bss 00000004 xMutex
Comme je l'ai déjà mentionné, je propose de réparer tout ce désordre de la même manière que dans l'article précédent - nous distribuerons tous nos objets statiquement au stade de la compilation. Les fonctions de distribution statiques deviennent disponibles si le paramètre configSUPPORT_STATIC_ALLOCATION est défini sur 1 dans le fichier de configuration FreeRTOS.
Commençons par les lignes. Voici comment la documentation sur FreeRTOS propose d'allouer des files d'attente
struct AMessage { char ucMessageID; char ucData[ 20 ]; }; #define QUEUE_LENGTH 10 #define ITEM_SIZE sizeof( uint32_t )
Dans cet exemple, la file d'attente est décrite par trois variables:
- Le tableau ucQueueStorage est l'endroit où les éléments de file d'attente seront placés. La taille de la file d'attente est définie par l'utilisateur pour chaque file d'attente individuellement.
- La structure xQueueBuffer - ici vit la description et l'état de la file d'attente, la taille actuelle, les listes des tâches en attente, ainsi que d'autres attributs et champs nécessaires à FreeRTOS pour travailler avec la file d'attente. Le nom de la variable, à mon avis, n'est pas entièrement réussi, dans FreeRTOS lui-même, cette chose est appelée QueueDefinition (description de la file d'attente).
- La variable xQueue1 est l'identifiant de la file d'attente (handle). Toutes les fonctions de gestion de file d'attente, ainsi que certaines autres (par exemple, les fonctions internes pour travailler avec des temporisateurs, des sémaphores et des mutex) acceptent une telle poignée. En fait, ce n'est qu'un pointeur vers QueueDefinition, mais nous ne le savons pas (pour ainsi dire), et donc la poignée devra être tirée séparément.
Faire comme dans l'exemple, bien sûr, ne sera pas un problème. Mais personnellement, je n'aime pas avoir jusqu'à 3 variables par entité. Une classe qui peut l'encapsuler le demande déjà. Un seul problème - la taille de chaque file d'attente peut varier. Dans un endroit, vous avez besoin d'une file d'attente plus grande, dans un autre, quelques éléments suffisent. Puisque nous voulons faire la queue statiquement, nous devons en quelque sorte spécifier cette taille au moment de la compilation. Vous pouvez utiliser le modèle pour cela.
template<class T, size_t size> class Queue { QueueHandle_t xHandle; StaticQueue_t x QueueDefinition; T xStorage[size]; public: Queue() { xHandle = xQueueCreateStatic(size, sizeof(T), reinterpret_cast<uint8_t*>(xStorage), &xQueueDefinition); } bool receive(T * val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueReceive(xHandle, val, xTicksToWait); } bool send(const T & val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueSend(xHandle, &val, xTicksToWait); } };
Dans le même temps, les fonctions d'envoi et de réception de messages, qui nous étaient immédiatement pratiques, se sont également installées dans cette classe.
La file d'attente sera déclarée comme une variable globale, quelque chose comme ceci
Queue<int, 1000> xQueue;
Envoi de message
xQueue.send(value);
Recevoir un message
int value; xQueue.receive(&value);
Voyons maintenant les sémaphores. Et bien que techniquement (à l'intérieur de FreeRTOS) les sémaphores et les mutex soient implémentés via des files d'attente, sémantiquement ce sont 3 primitives différentes. Par conséquent, nous les implémenterons dans des classes distinctes.
L'implémentation de la classe sémaphore sera assez triviale - elle stocke simplement plusieurs variables et déclare plusieurs fonctions.
class Sema { SemaphoreHandle_t xSema; StaticSemaphore_t xSemaControlBlock; public: Sema() { xSema = xSemaphoreCreateBinaryStatic(&xSemaControlBlock); } BaseType_t give() { return xSemaphoreGive(xSema); } BaseType_t take(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xSema, xTicksToWait); } };
Déclaration du sémaphore
Sema xSema;
Capture de sémaphore
xSema.take();
Sortie du sémaphore
xSema.give();
Maintenant mutex
class Mutex { SemaphoreHandle_t xMutex; StaticSemaphore_t xMutexControlBlock; public: Mutex() { xMutex = xSemaphoreCreateMutexStatic(&xSemaControlBlock); } BaseType_t lock(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xMutex, xTicksToWait); } BaseType_t unlock() { return xSemaphoreGive(xMutex); } };
Comme vous pouvez le voir, la classe mutex est presque identique à la classe sémaphore. Mais comme je l'ai dit sémantiquement, ce sont des entités différentes. De plus, les interfaces de ces classes ne sont pas complètes et elles se développeront dans des directions complètement différentes. Ainsi, les méthodes giveFromISR () et takeFromISR () peuvent être ajoutées au sémaphore pour fonctionner avec le sémaphore dans l'interruption, tandis que le mutex n'a que la méthode tryLock () ajoutée - il n'a aucune autre opération sémantiquement.
J'espère que vous connaissez la différence entre un sémaphore binaire et un mutex.Je pose toujours cette question lors des entretiens et, malheureusement, 90% des candidats ne comprennent pas cette différence. En fait, un sémaphore peut être capturé et libéré à partir de différents threads. Ci-dessus, j'ai mentionné le mode sémaphore signal-attente lorsqu'un thread envoie un signal (appels give ()), et l'autre attend un signal (avec la fonction take ()).
Mutex, au contraire, ne peut être libéré qu'à partir du même flux (tâche) qui l'a capturé. Je ne suis pas sûr que FreeRTOS surveille cela, mais certains systèmes d'exploitation (par exemple, Linux) le suivent assez strictement.
Mutex peut être utilisé dans le style C, c'est-à-dire appeler directement lock () / unlock (). Mais puisque nous écrivons en C ++, nous pouvons profiter des charmes de RAII et écrire un wrapper plus pratique qui capturera et libérera le mutex lui-même.
class MutexLocker { Mutex & mtx; public: MutexLocker(Mutex & mutex) : mtx(mutex) { mtx.lock(); } ~MutexLocker() { mtx.unlock(); } };
En quittant la portée, le mutex sera automatiquement libéré.
Cela est particulièrement pratique s'il existe plusieurs sorties de la fonction et que vous n'avez pas besoin de vous rappeler constamment la nécessité de libérer des ressources.
MutexLocker lock(xMutex); Serial.println(value); }
C'est maintenant au tour des minuteries.
class Timer { TimerHandle_t xTimer; StaticTimer_t xTimerControlBlock; public: Timer(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction) { xTimer = xTimerCreateStatic(pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, &xTimerControlBlock); } void start(TickType_t xTicksToWait = 0) { xTimerStart(xTimer, xTicksToWait); } };
En général, tout ici est similaire aux classes précédentes, je ne m'attarderai pas sur les détails. Peut-être que l'API laisse beaucoup à désirer, ou du moins nécessite une expansion. Mais mon objectif est de montrer le principe, et non de le mettre en état de production prêt.
Et enfin, les tâches. Chaque tâche a une pile et doit être placée en mémoire à l'avance. Nous utiliserons la même technique qu'avec les files d'attente - nous écrirons une classe modèle
template<const uint32_t ulStackDepth> class Task { protected: StaticTask_t xTaskControlBlock; StackType_t xStack[ ulStackDepth ]; TaskHandle_t xTask; public: Task(TaskFunction_t pxTaskCode, const char * const pcName, void * const pvParameters, UBaseType_t uxPriority) { xTask = xTaskCreateStatic(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, xStack, &xTaskControlBlock); } };
Puisque les objets de tâche sont maintenant déclarés comme variables globales, ils seront initialisés en tant que variables globales - avant d'appeler main (). Cela signifie que les paramètres qui sont transférés aux tâches doivent également être connus à ce stade. Cette nuance doit être prise en compte si dans votre cas quelque chose est passé qui doit être calculé avant de créer la tâche (j'ai juste NULL là). Si cela ne vous convient toujours pas, envisagez l'option avec des variables statiques locales
de l'article précédent .
Compilez et obtenez l'erreur:
tasks.c:(.text.vTaskStartScheduler+0x10): undefined reference to `vApplicationGetIdleTaskMemory' timers.c:(.text.xTimerCreateTimerTask+0x1a): undefined reference to `vApplicationGetTimerTaskMemory'
Voici le truc. Chaque système d'exploitation a une tâche spéciale - Tâche inactive (la tâche par défaut, la tâche de ne rien faire). Le système d'exploitation effectue cette tâche si toutes les autres tâches ne peuvent pas être effectuées (par exemple, dormir ou attendre quelque chose). En général, c'est la tâche la plus courante, uniquement avec la priorité la plus faible. Mais ici, il est créé à l'intérieur du noyau FreeRTOS et nous ne pouvons pas influencer sa création. Mais depuis que nous avons commencé à placer des tâches statiquement, nous devons en quelque sorte dire au système d'exploitation où vous souhaitez placer l'unité de contrôle et la pile de cette tâche. C'est à cela que sert FreeRTOS et nous demande de définir une fonction spéciale vApplicationGetIdleTaskMemory ().
Une situation similaire est avec la tâche des minuteries. Les temporisateurs du système FreeRTOS ne vivent pas seuls - une tâche spéciale tourne dans le système d'exploitation, qui sert ces temporisateurs. Et cette tâche nécessite également un bloc de contrôle et une pile. Et juste comme ça, le système d'exploitation nous demande d'indiquer où ils utilisent la fonction vApplicationGetTimerTaskMemory ().
Les fonctions elles-mêmes sont triviales et renvoient simplement les pointeurs correspondants aux objets alloués statiquement.
extern "C" void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize) { static StaticTask_t Idle_TCB; static StackType_t Idle_Stack[configMINIMAL_STACK_SIZE]; *ppxIdleTaskTCBBuffer = &Idle_TCB; *ppxIdleTaskStackBuffer = Idle_Stack; *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; } extern "C" void vApplicationGetTimerTaskMemory (StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, uint32_t *pulTimerTaskStackSize) { static StaticTask_t Timer_TCB; static StackType_t Timer_Stack[configTIMER_TASK_STACK_DEPTH]; *ppxTimerTaskTCBBuffer = &Timer_TCB; *ppxTimerTaskStackBuffer = Timer_Stack; *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; }
Voyons ce que nous avons.
Je vais cacher le code des aides sous le spoiler, tu viens de le voir template<class T, size_t size> class Queue { QueueHandle_t xHandle; StaticQueue_t xQueueDefinition; T xStorage[size]; public: Queue() { xHandle = xQueueCreateStatic(size, sizeof(T), reinterpret_cast<uint8_t*>(xStorage), &xQueueDefinition); } bool receive(T * val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueReceive(xHandle, val, xTicksToWait); } bool send(const T & val, TickType_t xTicksToWait = portMAX_DELAY) { return xQueueSend(xHandle, &val, xTicksToWait); } }; class Sema { SemaphoreHandle_t xSema; StaticSemaphore_t xSemaControlBlock; public: Sema() { xSema = xSemaphoreCreateBinaryStatic(&xSemaControlBlock); } BaseType_t give() { return xSemaphoreGive(xSema); } BaseType_t take(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xSema, xTicksToWait); } }; class Mutex { SemaphoreHandle_t xMutex; StaticSemaphore_t xMutexControlBlock; public: Mutex() { xMutex = xSemaphoreCreateMutexStatic(&xMutexControlBlock); } BaseType_t lock(TickType_t xTicksToWait = portMAX_DELAY) { return xSemaphoreTake(xMutex, xTicksToWait); } BaseType_t unlock() { return xSemaphoreGive(xMutex); } }; class MutexLocker { Mutex & mtx; public: MutexLocker(Mutex & mutex) : mtx(mutex) { mtx.lock(); } ~MutexLocker() { mtx.unlock(); } }; class Timer { TimerHandle_t xTimer; StaticTimer_t xTimerControlBlock; public: Timer(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction) { xTimer = xTimerCreateStatic(pcTimerName, xTimerPeriodInTicks, uxAutoReload, pvTimerID, pxCallbackFunction, &xTimerControlBlock); } void start(TickType_t xTicksToWait = 0) { xTimerStart(xTimer, xTicksToWait); } }; template<const uint32_t ulStackDepth> class Task { protected: StaticTask_t xTaskControlBlock; StackType_t xStack[ ulStackDepth ]; TaskHandle_t xTask; public: Task(TaskFunction_t pxTaskCode, const char * const pcName, void * const pvParameters, UBaseType_t uxPriority) { xTask = xTaskCreateStatic(pxTaskCode, pcName, ulStackDepth, pvParameters, uxPriority, xStack, &xTaskControlBlock); } }; extern "C" void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize) { static StaticTask_t Idle_TCB; static StackType_t Idle_Stack[configMINIMAL_STACK_SIZE]; *ppxIdleTaskTCBBuffer = &Idle_TCB; *ppxIdleTaskStackBuffer = Idle_Stack; *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; } extern "C" void vApplicationGetTimerTaskMemory (StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer, uint32_t *pulTimerTaskStackSize) { static StaticTask_t Timer_TCB; static StackType_t Timer_Stack[configTIMER_TASK_STACK_DEPTH]; *ppxTimerTaskTCBBuffer = &Timer_TCB; *ppxTimerTaskStackBuffer = Timer_Stack; *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; }
Le code de l'ensemble du programme.
Timer xTimer("Timer", 1000, pdTRUE, NULL, vTimerCallback); Sema xSema; Mutex xMutex; Queue<int, 1000> xQueue; Task<configMINIMAL_STACK_SIZE> task1(vTask1, "Task 1", NULL, tskIDLE_PRIORITY); Task<configMINIMAL_STACK_SIZE> task2(vTask2, "Task 2", NULL, tskIDLE_PRIORITY); void vTimerCallback(TimerHandle_t pxTimer) { xSema.give(); MutexLocker lock(xMutex); Serial.println("Test"); } void vTask1(void *) { while(1) { xSema.take(); int value = random(1000); xQueue.send(value); } } void vTask2(void *) { while(1) { int value; xQueue.receive(&value); MutexLocker lock(xMutex); Serial.println(value); } } void setup() { Serial.begin(9600); xTimer.start(); vTaskStartScheduler(); } void loop() {}
Vous pouvez démonter le binaire résultant et voir quoi et comment il se trouve (la sortie de objdump est légèrement teintée pour une meilleure lisibilité):
0x200000b0 .bss 512 vApplicationGetIdleTaskMemory::Idle_Stack 0x200002b0 .bss 92 vApplicationGetIdleTaskMemory::Idle_TCB 0x2000030c .bss 1024 vApplicationGetTimerTaskMemory::Timer_Stack 0x2000070c .bss 92 vApplicationGetTimerTaskMemory::Timer_TCB 0x200009c8 .bss 608 task1 0x20000c28 .bss 608 task2 0x20000e88 .bss 84 xMutex 0x20000edc .bss 4084 xQueue 0x20001ed0 .bss 84 xSema 0x20001f24 .bss 48 xTimer
L'objectif est atteint - maintenant tout est bien en vue. Chaque objet est visible et sa taille est compréhensible (enfin, sauf que les objets composés du type Tâche considèrent toutes leurs pièces de rechange en une seule pièce). Les statistiques du compilateur sont également extrêmement précises et cette fois très utiles.
Sketch uses 20,800 bytes (15%) of program storage space. Maximum is 131,072 bytes. Global variables use 9,332 bytes (45%) of dynamic memory, leaving 11,148 bytes for local variables. Maximum is 20,480 bytes.
Conclusion
Bien que FreeRTOS vous permette de créer et de supprimer des tâches, des files d'attente, des sémaphores et des mutex à la volée, dans de nombreux cas, cela n'est pas nécessaire. En règle générale, il suffit de créer tous les objets au démarrage une fois et ils fonctionneront jusqu'au prochain redémarrage. Et c'est une bonne raison de distribuer de tels objets statiquement au stade de la compilation. En conséquence, nous obtenons une compréhension claire de la mémoire occupée par nos objets, où se trouvent et combien de mémoire libre reste.
Il est évident que la méthode proposée ne convient que pour placer des objets dont la durée de vie est comparable à la durée de vie de l'ensemble de l'application. Sinon, vous devez utiliser la mémoire dynamique.
En plus du placement statique des objets FreeRTOS, nous avons également écrit des wrappers pratiques sur les primitives FreeRTOS, ce qui nous a permis de simplifier quelque peu le code client et d'encapsuler
L'interface peut être simplifiée si nécessaire (par exemple, ne pas vérifier le code retour, ou ne pas utiliser de timeouts). Il convient également de noter que l'implémentation est incomplète - je n'ai pas pris la peine de mettre en œuvre toutes les méthodes possibles d'envoi et de réception de messages via la file d'attente (par exemple, à partir d'une interruption, envoi au début ou à la fin de la file d'attente), je n'ai pas implémenté de primitives de synchronisation à partir des interruptions, comptage des sémaphores (non binaires), et bien plus.
J'étais trop paresseux pour amener ce code à l'état "prendre et utiliser", je voulais juste montrer l'idée. Mais qui a besoin d'une bibliothèque prête à l'emploi, je suis juste tombé sur la
bibliothèque frt . Tout y est pratiquement le même, seulement rappelé. Eh bien, l'interface est un peu différente.
Un exemple de l'article est
ici .
Merci à tous d'avoir lu cet article jusqu'au bout. Je me ferai un plaisir de formuler des critiques constructives. Il sera également intéressant pour moi de discuter des nuances dans les commentaires.