默认情况下,FreeRTOS系统中的所有对象都是动态分布的-队列,信号量,计时器,任务(线程)和互斥体。 程序员只能看到“堆”,即在程序或系统的请求下动态分配内存的区域,内部情况尚不清楚。 还剩多少? 不明 有什么比您需要的更多吗? 谁知道 就个人而言,我更喜欢解决内存组织问题,即使在编写固件的阶段,也不会在内存意外终止时导致运行时错误。
本文是
昨天关于逻辑在微控制器内存中静态分布的逻辑
延续 ,只是现在与FreeRTOS对象有关。 今天,我们将学习如何静态放置FreeRTOS对象,这将使我们能够更清楚地了解微控制器RAM中正在发生的事情,对象的定位位置以及它们占用的空间。
但是只需要静态地开始放置FreeRTOS对象并不需要太多的精力-从9.0版开始,FreeRTOS提供了用于创建静态放置的对象的功能。 此类函数的名称带有静态后缀,并且这些函数具有出色的示例文档。 我们将通过FreeRTOS函数编写方便,美观的C ++包装程序,这些函数不仅可以静态放置对象,还可以隐藏所有内脏组件,并提供更方便的界面。
本文适用于初学者,但他们已经熟悉FreeRTOS的基础知识以及同步多线程程序的原语。 走吧
FreeRTOS是微控制器的操作系统。 好吧,不是完整的操作系统,而是一个允许您并行运行多个任务的库。 FreeRTOS还允许任务通过消息队列交换消息,使用计时器以及使用信号量和互斥锁同步任务。
我认为,如果您使用FreeRTOS,则需要同时执行两个(或多个)任务的任何固件都可以轻松,轻松地解决。 例如,从慢速传感器读取读数并同时为显示屏提供服务。 仅在不读取传感器的情况下使制动器不制动。 一般来说,必须有! 我强烈建议学习。
正如我在上一篇文章中说过和写过的,如果我们在编译阶段知道对象的数量和大小,我就不太喜欢动态创建对象的方法。 如果将这些对象静态放置,那么我们可以对微控制器中的内存分配有更清晰,更易理解的了解,从而避免在内存突然终止时产生意外。
我们将以STM32F103C8T6微控制器上的BluePill板为例来考虑FreeRTOS内存组织问题。 为了不担心编译器和构建系统,我们将在ArduinoIDE环境中工作,并为此板安装支持。 有STM32的Arduino的几种实现-原则上可以做到。 我已经按照Readme.md项目(
本文中提到的引导程序)的说明安装了
stm32duino 。 FreeRTOS版本10.0是通过ArduinoIDE库管理器安装的。 编译器-GCC 8.2
我们将提出一个小的实验任务。 此任务可能没有太大的实际意义,但是将使用FreeRTOS中的所有同步原语。 像这样:
- 2个任务(线程)并行工作
- 计时器也可以工作,它不时使用信号等待模式中的信号量将通知发送到第一个任务
- 从计时器收到通知的第一个任务,通过队列将消息(随机数)发送给第二个任务
- 第二个收到消息后,将其打印到控制台
- 让第一个任务也将某些内容打印到控制台,这样它们就不会与控制台发生冲突,将受到互斥锁的保护。
- 队列的大小可以限制为一个元素,但是为了使其更有趣,我们将1000
标准实现(根据文档和教程)可能看起来像这样。
#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() {}
让我们看看如果编译这样的代码,在微控制器的存储器中会发生什么。 默认情况下,所有FreeRTOS对象都放置在动态内存中。 FreeRTOS提供了多达5种难以实现的内存管理器实现,但总的来说,它们具有相同的任务-为FreeRTOS和用户的需求削减内存。 从微控制器的常规堆(使用malloc)或使用它们自己的单独堆中切割碎片。 使用哪种堆并不重要-无论如何,我们无法查看堆内部。
例如,对于名称为FreeRTOS的堆,它将看起来像这样(从objdump实用程序输出)
... 200009dc l O .bss 00002000 ucHeap ...
即 我们看到了一个大块,所有FreeRTOS对象都在其中被切掉-信号量,互斥量,计时器,队列,甚至任务本身。 最后两点非常重要。 根据元素的数量,队列可能会非常大,并且由于堆栈(与任务一起分配),保证了任务会占用大量空间。
是的,这是多任务处理的缺点-每个任务都有自己的堆栈。 此外,堆栈必须足够大,以便它不仅包含任务本身的调用和局部变量,而且还包含中断堆栈(如果发生)。 好吧,由于可以随时发生中断,因此在发生中断的情况下,每个任务都应在堆栈上保留一个空间。 此外,CortexM微控制器可以具有嵌套的中断,因此堆栈必须足够大以容纳所有同时发生的中断。
通过xTaskCreate函数的参数创建任务时,将设置任务堆栈的大小。 堆栈大小不能小于configMINIMAL_STACK_SIZE参数(在FreeRTOSConfig.h配置文件中指定)-这与中断保留相同。 堆大小由configTOTAL_HEAP_SIZE参数设置,在这种情况下为8kb。
现在尝试猜测我们所有的对象是否都适合8kb的堆? 还有几个对象? 还有一些任务?在某些FreeRTOS设置下,所有对象都不适合堆。 它看起来像这样:该程序根本无法运行。 即 一切都被编译,注入,但随后微控制器挂起,仅此而已。 猜猜问题出在堆的大小上。 我不得不增加一束到12kb。
停止,什么是变量xTimer,xQueue,xSemaphore和xMutex? 他们不描述我们需要的对象吗? 不,这些只是句柄-指向某个(不透明)结构的指针,该结构描述了同步对象本身
200009cc g O .bss 00000004 xTimer 200009d0 g O .bss 00000004 xSemaphore 200009cc g O .bss 00000004 xQueue 200009d4 g O .bss 00000004 xMutex
正如我已经提到的,我建议以与上一篇文章相同的方式修复所有这些混乱-我们将在编译阶段静态分发所有对象。 如果在FreeRTOS配置文件中将configSUPPORT_STATIC_ALLOCATION参数设置为1,则静态分发功能将变为可用。
让我们从线条开始。 这是FreeRTOS上的文档如何提供分配队列的方法
struct AMessage { char ucMessageID; char ucData[ 20 ]; }; #define QUEUE_LENGTH 10 #define ITEM_SIZE sizeof( uint32_t )
在此示例中,队列由三个变量描述:
- ucQueueStorage数组是放置队列元素的位置。 用户为每个队列分别设置队列大小。
- xQueueBuffer结构-在此处保留队列的描述和状态,当前大小,待处理任务的列表以及FreeRTOS处理该队列所需的其他属性和字段。 在我看来,变量的名称并不完全成功,在FreeRTOS本身中,此名称称为QueueDefinition(队列的描述)。
- 变量xQueue1是队列(句柄)的标识符。 所有队列管理功能以及其他一些功能(例如,用于计时器,信号灯和互斥锁的内部功能)都接受此类句柄。 实际上,这只是指向QueueDefinition的指针,但我们不知道(实际上),因此必须单独拉动手柄。
当然,按照示例进行操作不是问题。 但就我个人而言,我不希望每个实体拥有多达3个变量。 可以封装它的类已经在要求它了。 唯一的问题-每个队列的大小可能会有所不同。 在一个地方,您需要一个更大的队列,在另一个地方,几个元素就足够了。 由于我们要静态排队,因此必须在编译时以某种方式指定此大小。 您可以为此使用模板。
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); } };
同时,对我们来说很方便的发送和接收消息的功能也在该类中得到了解决。
队列将被声明为全局变量,像这样
Queue<int, 1000> xQueue;
讯息发送
xQueue.send(value);
接收讯息
int value; xQueue.receive(&value);
现在让我们处理信号量。 尽管在技术上(在FreeRTOS内部)信号量和互斥量是通过队列实现的,但从语义上讲,它们是3个不同的原语。 因此,我们将在单独的类中实现它们。
信号量类的实现非常简单-它仅存储多个变量并声明多个函数。
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); } };
信号量声明
Sema xSema;
信号量捕获
xSema.take();
信号量释放
xSema.give();
现在互斥
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); } };
如您所见,互斥锁类几乎与信号量类相同。 但是正如我在语义上所说的,这些是不同的实体。 而且,这些类的接口还不完善,它们将在完全不同的方向上扩展。 因此,可以在信号量中添加GiveFromISR()和takeFromISR()方法,以与中断中的信号量一起工作,而互斥锁仅添加了tryLock()方法-语义上没有其他操作。
我希望您知道二进制信号量和互斥量之间的区别。我总是在面试中问这个问题,不幸的是,有90%的候选人不了解这种差异。 实际上,可以从不同的线程捕获并释放信号量。 上面我提到了当一个线程发送信号(调用Give()),另一个线程等待信号(带有take()函数)时的信号等待信号量模式。
相反,互斥锁只能从捕获它的同一流(任务)中释放。 我不确定FreeRTOS是否会对此进行监视,但是某些操作系统(例如Linux)非常严格地遵循此规则。
Mutex可以用于样式C,即 直接调用lock()/ unlock()。 但是,由于我们使用C ++编写,因此我们可以利用RAII的魅力,并编写一个更方便的包装程序,以捕获和释放互斥锁本身。
class MutexLocker { Mutex & mtx; public: MutexLocker(Mutex & mutex) : mtx(mutex) { mtx.lock(); } ~MutexLocker() { mtx.unlock(); } };
离开示波器时,互斥锁将自动释放。
如果该函数有多个出口,并且您不需要经常记住释放资源的需求,这将特别方便。
MutexLocker lock(xMutex); Serial.println(value); }
现在轮到计时器了。
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); } };
总的来说,这里的一切都与以前的课程相似,我将不作详细介绍。 也许该API还有很多不足之处,或者至少需要扩展。 但是我的目标是展示原理,而不是使其处于生产就绪状态。
最后是任务。 每个任务都有一个堆栈,必须事先放在内存中。 我们将使用与队列相同的技术-我们将编写模板类
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); } };
由于任务对象现在被声明为全局变量,因此在调用main()之前,它们将被初始化为全局变量。 这意味着在此阶段还应该知道传输到任务的参数。 如果在您的情况下,在创建任务之前需要计算某些内容(我那里只有NULL),则应考虑这一细微差别。 如果仍然不适合您,请考虑使用
上一篇文章中带有局部静态变量的选项。
编译并得到错误:
tasks.c:(.text.vTaskStartScheduler+0x10): undefined reference to `vApplicationGetIdleTaskMemory' timers.c:(.text.xTimerCreateTimerTask+0x1a): undefined reference to `vApplicationGetTimerTaskMemory'
就是这个 每个操作系统都有一个特殊的任务-空闲任务(默认任务,什么都不做的任务)。 如果无法执行所有其他任务(例如,休眠或等待某事),则操作系统将执行此任务。 通常,这是最常见的任务,只有最低优先级。 但是这里是在FreeRTOS内核中创建的,我们不能影响它的创建。 但是,由于我们开始静态地放置任务,因此我们需要以某种方式告诉OS您要将控制单元和此任务的堆栈放置在何处。 这就是FreeRTOS的目的,它要求我们定义一个特殊的函数vApplicationGetIdleTaskMemory()。
计时器的任务与此类似。 FreeRTOS系统中的计时器不能单独使用-操作系统中有一项特殊的任务,它为这些计时器提供服务。 并且此任务还需要一个控制块和一个堆栈。 就像这样,操作系统要求我们指示他们使用vApplicationGetTimerTaskMemory()函数的位置。
这些函数本身并不重要,只是将相应的指针返回到静态分配的对象。
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; }
让我们看看我们得到了什么。
我将帮手的代码隐藏在扰流板下,您刚刚看到了 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; }
整个程序的代码。
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() {}
您可以反汇编生成的二进制文件,并查看其位置和位置(objdump的输出略有色,以提高可读性):
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
目标已实现-现在一切都已全面可见。 每个对象都是可见的,并且其大小是可以理解的(好吧,除了Task类型的复合对象将所有备用零件都视为一件)。 编译器统计信息也非常准确,这次非常有用。
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.
结论
尽管FreeRTOS允许您动态创建和删除任务,队列,信号量和互斥量,但是在许多情况下,这不是必需的。 通常,一次创建所有对象就足够了,它们将一直工作到下一次重新启动。 这是在编译阶段静态分配此类对象的一个很好的理由。 结果,我们对对象占用的内存有了清晰的了解,它位于何处以及剩余多少可用内存。
显然,提出的方法仅适用于放置寿命可与整个应用程序的寿命相媲美的对象。 否则,您应该使用动态内存。
除了静态放置FreeRTOS对象外,我们还在FreeRTOS原语上编写了方便的包装器,这使我们能够在某种程度上简化客户端代码并封装
必要时可以简化接口(例如,不检查返回码或不使用超时)。 还值得注意的是,实现方式是不完整的-我没有为通过队列发送和接收消息的所有可能方法(例如,从中断,发送到队列的开头或结尾)的实现方法打扰,我没有实现中断的同步原语,计数(非二进制)信号量,还有更多。
我太懒了,无法将此代码带入“使用”状态,我只是想展示一下这个想法。 但是谁需要一个现成的库,我就遇到了
frt库 。 只是想起其中的所有内容几乎是相同的。 好吧,界面有点不同。
本文的一个示例在
这里 。
谢谢大家阅读本文至最后。 我很乐意接受建设性的批评。 讨论评论中的细微差别对我来说也很有趣。