Standardmäßig werden alle Objekte im FreeRTOS-System dynamisch verteilt - Warteschlangen, Semaphoren, Timer, Aufgaben (Threads) und Mutexe. Der Programmierer sieht nur den "Heap" - den Bereich, in dem Speicher auf Anforderung eines Programms oder Systems dynamisch zugewiesen wird und in dem nicht klar ist, was im Inneren geschieht. Wie viel ist noch übrig? Unbekannt Nimmt etwas mehr als Sie brauchen? Wer weiß? Persönlich ziehe ich es vor, die Probleme beim Organisieren des Speichers bereits beim Schreiben der Firmware zu lösen, ohne Laufzeitfehler zu verursachen, wenn der Speicher unerwartet beendet wird.
Dieser Artikel ist eine logische
Fortsetzung von gestern über die statische Verteilung von Objekten im Speicher des Mikrocontrollers, nur jetzt in Bezug auf FreeRTOS-Objekte. Heute lernen wir, wie man FreeRTOS-Objekte statisch platziert, um besser zu verstehen, was im RAM des Mikrocontrollers geschieht, wie genau sich unsere Objekte befinden und wie viel sie belegen.
Das statische Aufnehmen und Starten von FreeRTOS-Objekten erfordert jedoch nicht viel Intelligenz. Ab Version 9.0 bietet FreeRTOS Funktionen zum Erstellen statisch platzierter Objekte. Solche Funktionen haben ein statisches Suffix im Namen und diese Funktionen haben eine ausgezeichnete Dokumentation mit Beispielen. Wir werden praktische und schöne C ++ - Wrapper über FreeRTOS-Funktionen schreiben, die nicht nur Objekte statisch platzieren, sondern auch alle Innereien verbergen und eine bequemere Oberfläche bieten.
Dieser Artikel richtet sich an Programmierer für Anfänger, die jedoch bereits mit den Grundlagen von FreeRTOS und den Grundelementen der Synchronisierung von Multithread-Programmen vertraut sind. Lass uns gehen.
FreeRTOS ist ein Betriebssystem für Mikrocontroller. Okay, kein vollständiges Betriebssystem, sondern eine Bibliothek, mit der Sie mehrere Aufgaben parallel ausführen können. Mit FreeRTOS können Aufgaben auch Nachrichten über Nachrichtenwarteschlangen austauschen, Zeitgeber verwenden und Aufgaben mithilfe von Semaphoren und Mutexen synchronisieren.
Meiner Meinung nach kann jede Firmware, bei der Sie zwei (oder mehr) Aufgaben gleichzeitig ausführen müssen, viel einfacher und eleganter gelöst werden, wenn Sie FreeRTOS verwenden. Lesen Sie beispielsweise die Messwerte von langsamen Sensoren und bedienen Sie gleichzeitig das Display. Nur so, dass ohne Bremsen, während die Sensoren gelesen werden. Im Allgemeinen muss haben! Ich empfehle dringend für das Studium.
Wie ich bereits in einem früheren Artikel sagte und schrieb, mag ich den Ansatz, Objekte dynamisch zu erstellen, nicht wirklich, wenn wir ihre Anzahl und Größe bei der Kompilierung kennen. Wenn solche Objekte statisch platziert werden, erhalten wir ein klareres und verständlicheres Bild der Speicherzuordnung im Mikrocontroller und vermeiden daher Überraschungen, wenn der Speicher plötzlich endet.
Wir werden Probleme mit der FreeRTOS-Speicherorganisation anhand der BluePill-Karte auf dem STM32F103C8T6-Mikrocontroller als Beispiel betrachten. Um sich keine Sorgen um den Compiler und das Build-System zu machen, werden wir in der ArduinoIDE-Umgebung arbeiten und die Unterstützung für dieses Board installieren. Es gibt mehrere Implementierungen von Arduino für STM32 - im Prinzip reicht jede aus. Ich habe
stm32duino gemäß den Anweisungen aus dem Readme.md-Projekt installiert, einem Bootloader, wie
in diesem Artikel erwähnt . FreeRTOS Version 10.0 wird über den ArduinoIDE-Bibliotheksmanager installiert. Compiler - gcc 8.2
Wir werden uns eine kleine experimentelle Aufgabe einfallen lassen. Diese Aufgabe hat möglicherweise nicht viel praktischen Sinn, aber alle in FreeRTOS enthaltenen Synchronisationsprimitive werden verwendet. So etwas wie das:
- 2 Aufgaben (Threads) arbeiten parallel
- Es funktioniert auch ein Timer, der von Zeit zu Zeit eine Benachrichtigung an die erste Aufgabe sendet, indem er ein Semaphor im Signal-Wartemodus verwendet
- Die erste Aufgabe, die eine Benachrichtigung vom Zeitgeber erhalten hat, sendet eine Nachricht (Zufallszahl) über die Warteschlange an die zweite Aufgabe
- Der zweite, der die Nachricht erhalten hat, druckt sie auf der Konsole aus
- Lassen Sie die erste Aufgabe auch etwas auf die Konsole drucken, und damit sie nicht kämpfen, wird die Konsole durch den Mutex geschützt.
- Die Warteschlangengröße könnte auf ein Element begrenzt sein, aber um es interessanter zu machen, setzen wir 1000
Die Standardimplementierung (gemäß Dokumentation und Tutorials) sieht möglicherweise so aus.
#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() {}
Mal sehen, was im Speicher des Mikrocontrollers passiert, wenn Sie solchen Code kompilieren. Standardmäßig werden alle FreeRTOS-Objekte im dynamischen Speicher abgelegt. FreeRTOS bietet bis zu 5 Implementierungen von Speichermanagern, die schwer zu implementieren sind, aber im Allgemeinen die gleiche Aufgabe haben - Speicherteile für die Anforderungen von FreeRTOS und des Benutzers zu schneiden. Stücke werden entweder aus dem allgemeinen Haufen des Mikrocontrollers (unter Verwendung von Malloc) geschnitten oder verwenden ihren eigenen separaten Haufen. Welche Art von Heap für uns verwendet wird, ist nicht wichtig - wir können sowieso nicht in den Heap schauen.
Für einen Heap mit dem Namen FreeRTOS sieht es beispielsweise so aus (Ausgabe des Dienstprogramms objdump).
... 200009dc l O .bss 00002000 ucHeap ...
Das heißt, Wir sehen ein großes Stück, in das alle FreeRTOS-Objekte geschnitten sind - Semaphoren, Mutexe, Timer, Warteschlangen und sogar die Aufgaben selbst. Die letzten 2 Punkte sind sehr wichtig. Abhängig von der Anzahl der Elemente kann die Warteschlange sehr groß sein, und Aufgaben nehmen aufgrund des Stapels, der auch zusammen mit der Aufgabe zugewiesen wird, garantiert viel Platz ein.
Ja, dies ist ein Minus des Multitasking - jede Aufgabe hat ihren eigenen Stapel. Darüber hinaus muss der Stapel groß genug sein, damit er nicht nur die Aufrufe und lokalen Variablen der Aufgabe selbst enthält, sondern auch den Interrupt-Stapel, falls dies auftritt. Nun, da ein Interrupt jederzeit auftreten kann, sollte jede Aufgabe im Falle einer Unterbrechung eine Reserve auf dem Stapel haben. Darüber hinaus können CortexM-Mikrocontroller verschachtelte Interrupts aufweisen, sodass der Stapel groß genug sein muss, um alle Interrupts aufzunehmen, wenn sie gleichzeitig auftreten.
Die Größe des Aufgabenstapels wird festgelegt, wenn die Aufgabe durch den Parameter der Funktion xTaskCreate erstellt wird. Die Stapelgröße darf nicht kleiner sein als der Parameter configMINIMAL_STACK_SIZE (in der Konfigurationsdatei FreeRTOSConfig.h angegeben) - dies ist dieselbe Reserve für Interrupts. Die Heap-Größe wird durch den Parameter configTOTAL_HEAP_SIZE festgelegt und beträgt in diesem Fall 8 KB.
Versuchen Sie nun zu erraten, ob alle unsere Objekte in einen 8-KB-Haufen passen. Und ein paar Gegenstände? Und noch ein paar Aufgaben?Bei bestimmten FreeRTOS-Einstellungen passten nicht alle Objekte in den Heap. Und es sieht so aus: Das Programm funktioniert einfach nicht. Das heißt, alles wird kompiliert, gegossen, aber dann hängt der Mikrocontroller einfach und das wars. Und raten Sie mal, dass das Problem genau die Größe des Haufens ist. Ich musste ein paar auf 12kb erhöhen.
Stop, was sind die Variablen xTimer, xQueue, xSemaphore und xMutex? Beschreiben sie nicht die Objekte, die wir brauchen? Nein, dies sind nur Handles - Zeiger auf eine bestimmte (undurchsichtige) Struktur, die die Synchronisationsobjekte selbst beschreibt
200009cc g O .bss 00000004 xTimer 200009d0 g O .bss 00000004 xSemaphore 200009cc g O .bss 00000004 xQueue 200009d4 g O .bss 00000004 xMutex
Wie ich bereits erwähnt habe, schlage ich vor, all dieses Durcheinander auf die gleiche Weise wie im vorherigen Artikel zu reparieren - wir werden alle unsere Objekte in der Kompilierungsphase statisch verteilen. Die statischen Verteilungsfunktionen werden verfügbar, wenn der Parameter configSUPPORT_STATIC_ALLOCATION in der FreeRTOS-Konfigurationsdatei auf 1 gesetzt ist.
Beginnen wir mit den Zeilen. Hier erfahren Sie, wie die Dokumentation zu FreeRTOS das Zuweisen von Warteschlangen bietet
struct AMessage { char ucMessageID; char ucData[ 20 ]; }; #define QUEUE_LENGTH 10 #define ITEM_SIZE sizeof( uint32_t )
In diesem Beispiel wird die Warteschlange durch drei Variablen beschrieben:
- Im Array ucQueueStorage werden die Warteschlangenelemente platziert. Die Warteschlangengröße wird vom Benutzer für jede Warteschlange einzeln festgelegt.
- Die xQueueBuffer-Struktur - hier werden die Beschreibung und der Status der Warteschlange, die aktuelle Größe, Listen ausstehender Aufgaben sowie andere Attribute und Felder aufgeführt, die FreeRTOS für die Arbeit mit der Warteschlange benötigt. Der Name für die Variable ist meiner Meinung nach nicht ganz erfolgreich, in FreeRTOS selbst heißt dieses Ding QueueDefinition (Beschreibung der Warteschlange).
- Die Variable xQueue1 ist die Kennung der Warteschlange (Handle). Alle Warteschlangenverwaltungsfunktionen sowie einige andere (z. B. interne Funktionen zum Arbeiten mit Timern, Semaphoren und Mutexen) akzeptieren ein solches Handle. Tatsächlich ist dies nur ein Zeiger auf QueueDefinition, aber wir wissen dies nicht (sozusagen), und daher muss der Griff separat gezogen werden.
Wie im Beispiel zu tun, ist natürlich kein Problem. Ich persönlich möchte jedoch nicht bis zu 3 Variablen pro Entität haben. Eine Klasse, die es kapseln kann, fragt bereits danach. Nur ein Problem - die Größe jeder Warteschlange kann variieren. An einem Ort benötigen Sie eine größere Warteschlange, an einem anderen reichen ein paar Elemente aus. Da wir statisch anstehen möchten, müssen wir diese Größe beim Kompilieren irgendwie angeben. Hierfür können Sie die Vorlage verwenden.
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); } };
Gleichzeitig wurden in dieser Klasse auch die Funktionen des Sendens und Empfangens von Nachrichten festgelegt, die für uns unmittelbar günstig waren.
Die Warteschlange wird als globale Variable deklariert
Queue<int, 1000> xQueue;
Nachrichten senden
xQueue.send(value);
Nachricht empfangen
int value; xQueue.receive(&value);
Nun beschäftigen wir uns mit Semaphoren. Und obwohl technisch (innerhalb von FreeRTOS) Semaphoren und Mutexe durch Warteschlangen implementiert werden, sind dies semantisch 3 verschiedene Grundelemente. Daher werden wir sie in separaten Klassen implementieren.
Die Implementierung der Semaphorklasse ist recht trivial - sie speichert einfach mehrere Variablen und deklariert mehrere Funktionen.
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); } };
Semaphor-Deklaration
Sema xSema;
Semaphor-Erfassung
xSema.take();
Semaphor-Veröffentlichung
xSema.give();
Jetzt 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); } };
Wie Sie sehen können, ist die Mutex-Klasse fast identisch mit der Semaphor-Klasse. Aber wie ich semantisch sagte, sind dies verschiedene Entitäten. Darüber hinaus sind die Schnittstellen dieser Klassen nicht vollständig und werden in völlig unterschiedliche Richtungen erweitert. Daher können die Methoden giveFromISR () und takeFromISR () zum Semaphor hinzugefügt werden, um mit dem Semaphor im Interrupt zu arbeiten, während dem Mutex nur die Methode tryLock () hinzugefügt wird - es gibt keine anderen semantischen Operationen.
Ich hoffe, Sie kennen den Unterschied zwischen einem binären Semaphor und einem Mutex.Ich stelle diese Frage immer bei Interviews und leider verstehen 90% der Kandidaten diesen Unterschied nicht. Tatsächlich kann ein Semaphor von verschiedenen Threads erfasst und freigegeben werden. Oben habe ich den Signal-Warte-Semaphor-Modus erwähnt, wenn ein Thread ein Signal sendet (Aufrufe give ()) und der andere auf ein Signal wartet (mit der Funktion take ()).
Im Gegenteil, Mutex kann nur von demselben Stream (Aufgabe) freigegeben werden, der es erfasst hat. Ich bin nicht sicher, ob FreeRTOS dies überwacht, aber einige Betriebssysteme (z. B. Linux) halten sich strikt daran.
Mutex kann in Stil C verwendet werden, d.h. Rufen Sie direkt lock () / entsperren () auf. Da wir jedoch in C ++ schreiben, können wir die Reize von RAII nutzen und einen bequemeren Wrapper schreiben, der den Mutex selbst erfasst und freigibt.
class MutexLocker { Mutex & mtx; public: MutexLocker(Mutex & mutex) : mtx(mutex) { mtx.lock(); } ~MutexLocker() { mtx.unlock(); } };
Beim Verlassen des Bereichs wird der Mutex automatisch freigegeben.
Dies ist besonders praktisch, wenn die Funktion mehrere Exits enthält und Sie sich nicht ständig an die Notwendigkeit erinnern müssen, Ressourcen freizugeben.
MutexLocker lock(xMutex); Serial.println(value); }
Jetzt sind die Timer an der Reihe.
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); } };
Im Allgemeinen ist hier alles ähnlich wie in den vorherigen Klassen, ich werde nicht im Detail darauf eingehen. Vielleicht lässt die API zu wünschen übrig oder erfordert zumindest eine Erweiterung. Mein Ziel ist es jedoch, das Prinzip zu zeigen und es nicht produktionsbereit zu machen.
Und schließlich die Aufgaben. Jede Aufgabe hat einen Stapel und muss vorab im Speicher abgelegt werden. Wir werden die gleiche Technik wie bei Warteschlangen verwenden - wir werden eine Vorlagenklasse schreiben
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); } };
Da Aufgabenobjekte jetzt als globale Variablen deklariert werden, werden sie vor dem Aufruf von main () als globale Variablen initialisiert. Dies bedeutet, dass zu diesem Zeitpunkt auch die Parameter bekannt sein sollten, die auf die Aufgaben übertragen werden. Diese Nuance sollte berücksichtigt werden, wenn in Ihrem Fall etwas übergeben wird, das vor dem Erstellen der Aufgabe berechnet werden muss (ich habe dort nur NULL). Wenn dies immer noch nicht zu Ihnen passt, ziehen Sie die Option mit lokalen statischen Variablen
aus dem vorherigen Artikel in Betracht.
Kompilieren Sie und erhalten Sie den Fehler:
tasks.c:(.text.vTaskStartScheduler+0x10): undefined reference to `vApplicationGetIdleTaskMemory' timers.c:(.text.xTimerCreateTimerTask+0x1a): undefined reference to `vApplicationGetTimerTaskMemory'
Hier ist das Ding. Jedes Betriebssystem hat eine spezielle Aufgabe - Leerlaufaufgabe (die Standardaufgabe, die Aufgabe, nichts zu tun). Das Betriebssystem führt diese Aufgabe aus, wenn nicht alle anderen Aufgaben ausgeführt werden können (z. B. Schlafen oder Warten auf etwas). Im Allgemeinen ist dies die häufigste Aufgabe, nur mit der niedrigsten Priorität. Aber hier wird es im FreeRTOS-Kernel erstellt und wir können seine Erstellung nicht beeinflussen. Da wir jedoch damit begonnen haben, Aufgaben statisch zu platzieren, müssen wir dem Betriebssystem irgendwie mitteilen, wo Sie die Steuereinheit und den Stapel dieser Aufgabe platzieren möchten. Dafür ist FreeRTOS gedacht und fordert uns auf, eine spezielle Funktion vApplicationGetIdleTaskMemory () zu definieren.
Eine ähnliche Situation besteht bei Timern. Timer im FreeRTOS-System leben nicht von alleine - eine besondere Aufgabe ist das Drehen im Betriebssystem, das diese Timer bedient. Und diese Aufgabe erfordert auch einen Steuerblock und einen Stapel. Und einfach so fordert uns das Betriebssystem auf, anzugeben, wo sie die Funktion vApplicationGetTimerTaskMemory () verwenden.
Die Funktionen selbst sind trivial und geben einfach die entsprechenden Zeiger auf statisch zugewiesene Objekte zurück.
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; }
Mal sehen, was wir haben.
Ich werde den Code der Helfer unter dem Spoiler verstecken, du hast ihn gerade gesehen 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; }
Der Code für das gesamte Programm.
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() {}
Sie können die resultierende Binärdatei zerlegen und sehen, was und wie sie sich befindet (die Ausgabe von objdump ist zur besseren Lesbarkeit leicht getönt):
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
Das Ziel ist erreicht - jetzt ist alles im Blick. Jedes Objekt ist sichtbar und seine Größe ist verständlich (außer dass zusammengesetzte Objekte des Aufgabentyps alle ihre Ersatzteile in einem Stück betrachten). Compiler-Statistiken sind ebenfalls äußerst genau und diesmal sehr nützlich.
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.
Fazit
Mit FreeRTOS können Sie zwar Aufgaben, Warteschlangen, Semaphoren und Mutexe im laufenden Betrieb erstellen und löschen, dies ist jedoch in vielen Fällen nicht erforderlich. In der Regel reicht es aus, alle Objekte beim Start einmal zu erstellen und sie funktionieren bis zum nächsten Neustart. Dies ist ein guter Grund, solche Objekte in der Kompilierungsphase statisch zu verteilen. Als Ergebnis erhalten wir ein klares Verständnis des Gedächtnisses unserer Objekte, wo was liegt und wie viel freies Gedächtnis übrig bleibt.
Es ist offensichtlich, dass das vorgeschlagene Verfahren nur zum Platzieren von Objekten geeignet ist, deren Lebensdauer mit der Lebensdauer der gesamten Anwendung vergleichbar ist. Andernfalls sollten Sie dynamischen Speicher verwenden.
Zusätzlich zur statischen Platzierung von FreeRTOS-Objekten haben wir auch praktische Wrapper über die FreeRTOS-Grundelemente geschrieben, wodurch wir den Client-Code etwas vereinfachen und auch kapseln konnten
Die Schnittstelle kann bei Bedarf vereinfacht werden (z. B. ohne Überprüfung des Rückkehrcodes oder ohne Verwendung von Zeitüberschreitungen). Es ist auch erwähnenswert, dass die Implementierung unvollständig ist. Ich habe mich nicht mit der Implementierung aller möglichen Methoden zum Senden und Empfangen von Nachrichten über die Warteschlange befasst (z. B. von einem Interrupt über das Senden an den Anfang oder das Ende der Warteschlange). Ich habe keine Synchronisationsprimitive aus Interrupts implementiert und (nicht-binäre) Semaphoren gezählt. und vieles mehr.
Ich war zu faul, um diesen Code in den Zustand "Nehmen und Verwenden" zu bringen. Ich wollte nur die Idee zeigen. Aber wer eine fertige Bibliothek braucht, der ist gerade auf die
Bibliothek gestoßen . Alles darin ist praktisch gleich, nur in Erinnerung gerufen. Nun, die Oberfläche ist etwas anders.
Ein Beispiel aus dem Artikel finden Sie
hier .
Vielen Dank, dass Sie diesen Artikel bis zum Ende gelesen haben. Ich werde mich über konstruktive Kritik freuen. Es wird für mich auch interessant sein, die Nuancen in den Kommentaren zu diskutieren.