Static distribution of FreeRTOS objects

    By default, all objects in the FreeRTOS system are distributed dynamically - queues, semaphores, timers, tasks (threads), and mutexes. The programmer sees only the "heap" - the area where memory is dynamically allocated at the request of a program or system, and what is happening inside is not clear. How much is left? Unknown Does anything take more than you need? Who knows? Personally, I prefer to solve the problems of organizing memory even at the stage of writing the firmware, without bringing to runtime errors when the memory unexpectedly ended.

    This article is a logical continuation of yesterday.about the static distribution of objects in the memory of the microcontroller, only now in relation to FreeRTOS objects. Today we will learn how to place FreeRTOS objects statically, which will allow us to more clearly understand what is happening in the microcontroller's RAM, how exactly our objects are located and how much they occupy.

    But just taking and starting to place FreeRTOS objects statically does not require much intelligence - starting with version 9.0, FreeRTOS provides functions for creating objects placed statically. Such functions have a Static suffix in the name and these functions have excellent documentation with examples. We will write convenient and beautiful C ++ wrappers over FreeRTOS functions that will not only place objects statically, but also hide all the giblets, as well as provide a more convenient interface.

    This article is intended for beginner programmers, but who are already familiar with the basics of FreeRTOS and the primitives of synchronizing multithreaded programs. Go.

    FreeRTOS is an operating system for microcontrollers. Well, ok, not a full OS, but a library that allows you to run several tasks in parallel. FreeRTOS also allows tasks to exchange messages through message queues, use timers, and synchronize tasks using semaphores and mutexes.

    In my opinion, any firmware where you need to simultaneously do two (or more) tasks can be solved much easier and more elegantly if you use FreeRTOS. For example, read the readings from slow sensors and serve the display at the same time. Only so that without brakes, while the sensors are read. In general, must have! I strongly recommend for study.

    As I said and wrote in a previous article, I don’t really like the approach of creating objects dynamically if we know their number and size at the compilation stage. If such objects are placed statically, then we can get a clearer and more understandable picture of the memory allocation in the microcontroller, and therefore avoid surprises when the memory suddenly ended.

    We will consider FreeRTOS memory organization issues using the BluePill board on the STM32F103C8T6 microcontroller as an example. In order not to worry about the compiler and the build system, we will work in the ArduinoIDE environment, installing support for this board. There are several implementations of Arduino for STM32 - in principle, any will do. I have stm32duino installed according to the instructions from the Readme.md project, the bootloader as saidin this article . FreeRTOS version 10.0 is installed through the ArduinoIDE library manager. Compiler - gcc 8.2

    We will think up a small experimental task for ourselves. There may not be much practical sense in this task, but all the synchronization primitives that are in FreeRTOS will be used. Something like this:

    • 2 tasks (threads) work in parallel
    • a timer also works, which from time to time sends a notification to the first task using a semaphore in signal-wait mode
    • the first task, having received notification from the timer, sends a message (random number) to the second task through the queue
    • the second, having received the message, prints it to the console
    • let the first task also prints something to the console, and so that they do not fight the console will be protected by the mutex.
    • the queue size could be limited to one element, but in order to make it more interesting we put 1000

    The standard implementation (according to the documentation and tutorials) may look like this.

    #include 
    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() {}

    Let's see what happens in the memory of the microcontroller, if you compile such code. By default, all FreeRTOS objects are placed in dynamic memory. FreeRTOS provides as many as 5 implementations of memory managers, which are difficult to implement, but in general they have the same task - to cut pieces of memory for the needs of FreeRTOS and the user. Pieces are cut either from the general heap of the microcontroller (using malloc) or use their own separate heap. What kind of heap is used for us is not important - anyway, we can’t look inside the heap.

    For example, for a heap of the name FreeRTOS it will look like this (output from the objdump utility)

    ...
    200009dc l 	O .bss      	00002000 ucHeap
    ...
    

    Those. we see one big piece, inside which all FreeRTOS objects are cut - semaphores, mutexes, timers, queues, and even the tasks themselves. The last 2 points are very important. Depending on the number of elements, the queue can be quite large, and tasks are guaranteed to take up a lot of space due to the stack, which is also allocated along with the task.

    Yes, this is a minus of multitasking - each task will have its own stack. Moreover, the stack must be large enough so that it contains not only the calls and local variables of the task itself, but also the interrupt stack, if this occurs. Well, since an interrupt can happen at any time, then every task should have a reserve on the stack in case of interruption. Moreover, CortexM microcontrollers can have nested interrupts, so the stack must be large enough to accommodate all interrupts if they happen simultaneously.

    The size of the task stack is set when the task is created by the parameter of the xTaskCreate function. The stack size cannot be less than the configMINIMAL_STACK_SIZE parameter (specified in the FreeRTOSConfig.h configuration file) - this is the same reserve for interrupts. The heap size is set by the configTOTAL_HEAP_SIZE parameter and in this case is 8kb.

    Now try to guess if all our objects will fit in a 8kb heap? And a couple of objects? And a few more tasks?
    With certain FreeRTOS settings, all objects did not fit in the heap. And it looks like this: the program simply does not work. Those. everything is compiled, poured, but then the microcontroller just hangs and that's it. And go guess that the problem is exactly the size of the heap. I had to increase a bunch to 12kb.

    Stop, what are the variables xTimer, xQueue, xSemaphore, and xMutex? Don't they describe the objects we need? No, these are only handles - pointers to a certain (opaque) structure, which describes the synchronization objects themselves

    200009cc g 	O .bss     	00000004 xTimer
    200009d0 g     O .bss    	00000004 xSemaphore
    200009cc g 	O .bss     	00000004 xQueue
    200009d4 g 	O .bss    	00000004 xMutex
    

    As I already mentioned, I propose to repair all this mess in the same way as in the previous article - we will distribute all our objects statically at the compilation stage. The static distribution functions become available if the configSUPPORT_STATIC_ALLOCATION parameter is set to 1. in the FreeRTOS configuration file.

    Let's start with the queues. Here is how documentation on FreeRTOS offers to allocate queues

    struct AMessage
     {
                	char ucMessageID;
                	char ucData[ 20 ];
     };
     #define QUEUE_LENGTH 10
     #define ITEM_SIZE sizeof( uint32_t )
     // xQueueBuffer will hold the queue structure.
     StaticQueue_t xQueueBuffer;
     // ucQueueStorage will hold the items posted to the queue.  Must be at least
     // [(queue length) * ( queue item size)] bytes long.
     uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
     void vATask( void *pvParameters )
     {
     QueueHandle_t xQueue1;
                	// Create a queue capable of containing 10 uint32_t values.
                	xQueue1 = xQueueCreate( QUEUE_LENGTH, // The number of items the queue can hold.
                                                                                                                	ITEM_SIZE       	  // The size of each item in the queue
                                                                                                                	&( ucQueueStorage[ 0 ] ), // The buffer that will hold the items in the queue.
                                                                                                                	&xQueueBuffer ); // The buffer that will hold the queue structure.
                	// The queue is guaranteed to be created successfully as no dynamic memory
                	// allocation is used.  Therefore xQueue1 is now a handle to a valid queue.
                	// ... Rest of task code.
     }

    In this example, the queue is described by three variables:

    • The ucQueueStorage array is where the queue elements will be placed. The queue size is set by the user for each queue individually.
    • The xQueueBuffer structure - here lives the description and status of the queue, current size, lists of pending tasks, as well as other attributes and fields needed by FreeRTOS to work with the queue. The name for the variable, in my opinion, is not entirely successful, in FreeRTOS itself this thing is called QueueDefinition (description of the queue).
    • The variable xQueue1 is the identifier of the queue (handle). All queue management functions, as well as some others (for example, internal functions for working with timers, semaphores, and mutexes) accept such a handle. In fact, this is just a pointer to QueueDefinition, but we don’t know this (as it were), and therefore the handle will have to be pulled separately.

    To do as in the example, of course, will not be a problem. But personally, I don’t like to have as many as 3 variables per entity. A class that can encapsulate it is already asking for it. Only one problem - the size of each queue may vary. In one place you need a larger queue, in another a couple of elements are enough. Since we want to queue statically, we must somehow specify this size at compile time. You can use the template for this.

    template
    class Queue
    {
      QueueHandle_t xHandle;
      StaticQueue_t x QueueDefinition;
      T         	xStorage[size];
    public:
      Queue()
      {
    	xHandle = xQueueCreateStatic(size,
                     	sizeof(T),
                         reinterpret_cast(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);
      }
    };

    At the same time, the functions of sending and receiving messages, which were immediately convenient for us, also settled in this class.

    The queue will be declared as a global variable, something like this

    Queue xQueue;

    Message sending

    	xQueue.send(value);

    Receive message

    	int value;
    	xQueue.receive(&value);
    

    Now let's deal with semaphores. And although technically (inside FreeRTOS) semaphores and mutexes are implemented through queues, semantically these are 3 different primitives. Therefore, we will implement them in separate classes.

    The implementation of the semaphore class will be quite trivial - it simply stores several variables and declares several functions.

    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);
      }
    };

    Semaphore declaration

    Sema xSema;

    Semaphore capture

      xSema.take();

    Semaphore release

      xSema.give();

    Now 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);
      } 
    };

    As you can see, the mutex class is almost identical to the semaphore class. But as I said semantically, these are different entities. Moreover, the interfaces of these classes are not complete, and they will expand in completely different directions. So, the giveFromISR () and takeFromISR () methods can be added to the semaphore to work with the semaphore in the interrupt, while the mutex only has the tryLock () method added - it has no other operations semantically.

    I hope you know the difference between a binary semaphore and a mutex.
    I always ask this question at interviews and, unfortunately, 90% of the candidates do not understand this difference. In fact, a semaphore can be captured and released from different threads. Above, I mentioned the signal-wait semaphore mode when one thread sends a signal (calls give ()), and the other waits for a signal (with the take () function).

    Mutex, on the contrary, can only be released from the same stream (task) that captured it. I’m not sure that FreeRTOS monitors this, but some operating systems (for example, Linux) follow this quite strictly.

    Mutex can be used in style C, i.e. directly call lock () / unlock (). But since we are writing in C ++, we can take advantage of the charms of RAII and write a more convenient wrapper that will capture and release the mutex itself.

    class MutexLocker
    {
      Mutex & mtx;
    public:
      MutexLocker(Mutex & mutex)
    	: mtx(mutex)
      {
    	mtx.lock();
      }
      ~MutexLocker()
      {
    	mtx.unlock();
      }
    };

    When leaving the scope, the mutex will be automatically freed.

    This is especially convenient if there are several exits from the function and you do not need to constantly remember the need to free resources.

    	MutexLocker lock(xMutex);
    	Serial.println(value);
      } // mutex will be unlocked here

    Now is the turn of the timers.

    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);
    	}
    };

    In general, everything here is similar to the previous classes, I will not dwell in detail. Perhaps the API leaves much to be desired, well, or at least requires expansion. But my goal is to show the principle, and not bring it to the state of production ready.

    And finally, the tasks. Each task has a stack and must be placed in memory in advance. We will use the same technique as with queues - we will write a template class

    template
    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);
      }
    };

    Since task objects are now declared as global variables, they will be initialized as global variables - before calling main (). This means that the parameters that are transferred to the tasks should also be known at this stage. This nuance should be taken into account if in your case something is passed that needs to be calculated before creating the task (I just have NULL there). If this still does not suit you, consider the option with local static variables from the previous article .

    Compile and get the error:

    tasks.c:(.text.vTaskStartScheduler+0x10): undefined reference to `vApplicationGetIdleTaskMemory'
    timers.c:(.text.xTimerCreateTimerTask+0x1a): undefined reference to `vApplicationGetTimerTaskMemory'

    Here's the thing. Each OS has a special task - Idle Task (the default task, the task of not doing anything). The operating system performs this task if all other tasks cannot be performed (for example, sleeping, or waiting for something). In general, this is the most common task, only with the lowest priority. But here it is being created inside the FreeRTOS kernel and we cannot influence its creation. But since we started placing tasks statically, we need to somehow tell the OS where you want to place the control unit and the stack of this task. That's what FreeRTOS is for and asks us to define a special function vApplicationGetIdleTaskMemory ().

    A similar situation is with the task of timers. Timers in the FreeRTOS system do not live on their own - a special task is spinning in the OS, which serves these timers. And this task also requires a control block and a stack. And just like that, the OS asks us to indicate where they are using the vApplicationGetTimerTaskMemory () function.

    The functions themselves are trivial and simply return the corresponding pointers to statically allocated objects.

    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;
    }

    Let's see what we got.

    I’ll hide the code of the helpers under the spoiler, you just saw it
    template
    class Queue
    {
      QueueHandle_t xHandle;
      StaticQueue_t xQueueDefinition;
      T         	xStorage[size];
    public:
      Queue()
      {
    	xHandle = xQueueCreateStatic(size,
                     	sizeof(T),
                         reinterpret_cast(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
    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;
    }


    The code for the entire program.

    Timer xTimer("Timer", 1000, pdTRUE, NULL, vTimerCallback);
    Sema xSema;
    Mutex xMutex;
    Queue xQueue;
    Task task1(vTask1, "Task 1", NULL, tskIDLE_PRIORITY);
    Task 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() {}

    You can disassemble the resulting binary and see what and how it is located (the output of objdump is slightly tinted for better readability):

    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

    The goal is achieved - now everything is in full view. Each object is visible and its size is understandable (well, except that compound objects of the Task type consider all their spare parts in one piece). Compiler statistics are also extremely accurate and this time very useful.

    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


    Although FreeRTOS allows you to create and delete tasks, queues, semaphores and mutexes on the fly, in many cases this is not necessary. As a rule, it is enough to create all the objects at the start once and they will work until the next reboot. And this is a good reason to distribute such objects statically at the compilation stage. As a result, we get a clear understanding of the memory occupied by our objects, where what lies and how much free memory remains.

    It is obvious that the proposed method is suitable only for placing objects whose lifetime is comparable to the lifetime of the entire application. Otherwise, you should use dynamic memory.

    In addition to the static placement of FreeRTOS objects, we also wrote convenient wrappers over the FreeRTOS primitives, which made it possible to simplify the client code somewhat, and

    you can also simplify the encapsulation of the Interface (for example, not checking the return code or not using timeouts). It is also worth noting that the implementation is incomplete - I did not bother with the implementation of all possible methods of sending and receiving messages through the queue (for example, from an interrupt, sending to the beginning or the end of the queue), I did not implement synchronization primitives from interrupts, counting (non-binary) semaphores, and much more.

    I was too lazy to bring this code to the “take and use” state, I just wanted to show the idea. But who needs a ready-made library, I just came across the frt library. Everything in it is practically the same, only brought to mind. Well, the interface is a little different.

    An example from the article is here .

    Thank you all for reading this article to the end. I will be glad to constructive criticism. It will also be interesting for me to discuss the nuances in the comments.

    Also popular now: