FreeRTOS Learning Notes: Software Structure of FreeRTOS

FreeRTOS Learning Notes: Software Structure of FreeRTOS

EEWorld

Electronic News, Sharp Interpretation

Technical Dry Goods, Updated Daily

FreeRTOS Learning Notes: Software Structure of FreeRTOS

 

I started learning FreeRTOS from the official document “Mastering the FreeRTOS Real Time Kernel”. The code and reference manual I used is version 9.0.0. I haven’t used other RTOS, so I have no intention of commenting on its advantages and disadvantages. Of course, it is undoubtedly an excellent and popular embedded RTOS. It is also very quick to get started. In this article, I will record how to add FreeRTOS code to an existing project as a reference (there are many articles online about how to use FreeRTOS, how to create tasks, etc. This part of the content is not the primary focus of my learning notes series because I want to share what I learned from my analysis and practice of FreeRTOS code, how it works, and what benefits it brings).

File Composition

You can find the link to download the source code package on the freertos.org website. Referring to the version 9.0.0 code I used, there are 6 C source files in the root directory:

croutine.cevent_groups.clist.cqueue.ctasks.ctimers.c  

These files are the core code of FreeRTOS, some of which are optional. Then there are two subdirectories: include and portable. The header files in the include directory contain macro definitions used by the system core, as well as API data structures, function prototypes, etc. used in programming. The files in the portable directory provide some functions that will be called by FreeRTOS core code. The implementation of these functions is related to the running environment or there are multiple implementation methods. For example, because FreeRTOS supports multiple hardware platforms, code related to platform implementation (such as functions written in assembly language) is placed in the subdirectory corresponding to the compiler and CPU type. The files in the portable directory are not core to the system; in addition to the files provided in the FreeRTOS code package, users can also write functions according to their own environment.

FreeRTOS Learning Notes: Software Structure of FreeRTOS

In the generated target module files, according to the minimum requirements that can run, tasks.c and list.c are essential (because FreeRTOS’s internal data structure uses linked lists, so list.c is needed). In addition, there must be a hardware implementation-related module. For example, I used /portable/GCC/ARM_CM3/port.c for STM32. In my program source files, at least two header files need to be included: FreeRTOS.h and task.h. If other optional function APIs are used, the corresponding header files must be included, as specified in the manual. If there are warnings or errors during compilation stating that something is not defined, check if any header files are missing.

Configuration (Optional)

FreeRTOS is very flexible, and many functions can be trimmed if not needed. To reduce memory waste, the length and bit width of certain data structures can also be specified. Therefore, FreeRTOS requires users to write a FreeRTOSConfig.h header file to define several parameters to configure the system’s functions and features.  

When using FreeRTOS for the first time, you don’t have to write this configuration file yourself; just find a project in the Demo directory of the code package that is similar to your environment, copy the FreeRTOSConfig.h from it, and make slight adjustments as needed. If you are using STM32, you can also borrow one from the official ST (for example, from the FreeRTOS example project in CubeF4). There are several macro definitions closely related to system resources that need to be noted:

#define configCPU_CLOCK_HZ ( SystemCoreClock )#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )#define configMAX_PRIORITIES ( 5 )#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 130 )#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 75 * 1024 ) )  

configCPU_CLOCK_HZ is the clock frequency of the CPU, which FreeRTOS needs to know in order to configure the hardware timer correctly. configTICK_RATE_HZ specifies the clock interrupt frequency used by FreeRTOS, which determines the unit of the time interval used. configMAX_PRIORITIES specifies how many task priorities exist, and should be kept to a minimum while still being sufficient. configMINIMAL_STACK_SIZE is the minimum value allocated for the task stack, and FreeRTOS will allocate the stack for the Idle task based on this value. configTOTAL_HEAP_SIZE determines how much memory is allocated for FreeRTOS to manage itself, which is used when dynamically creating tasks and other data structures, as explained in the next section.  

Be sure to pay attention to the two macro definitions that determine task scheduling characteristics:  

configUSE_PREEMPITON specifies whether to use preemptive task scheduling—i.e., whether running tasks can be scheduled even if they do not voluntarily yield control.  

configUSE_TIME_SLICING specifies whether to use time-slice scheduling for tasks of the same priority (under the premise of preemptive scheduling).  

Some optional features need to be enabled by specifying macros, such as configUSE_QUEUE_SETS, configUSE_TIMERS, configUSE_COUNTING_SEMAPHORES, configUSE_MUTEXES,

configUSE_TASK_NOTIFICATIONS, etc. If not specified, FreeRTOS will use default values, which should be noted in the manual.  

You can also directly check the conditional compilation statements in FreeRTOS.h regarding the handling of undefined configXXX_XXXX macros. Most are defined as 0 by default, and a few are defined as 1. There are also a few macros that do not provide default values, and if they are not defined in FreeRTOSConfig.h, it will result in an error.

Memory Management

I mentioned earlier that each task in FreeRTOS needs to allocate memory for the stack and TCB data structure. There are two memory allocation methods—static allocation, which is determined at compile time; and dynamic allocation, which is determined at runtime. Typically, we will use dynamic memory allocation, in which case we need to allow the FreeRTOS core to request a certain size of memory in chunks, similar to the C language’s malloc() and free() mechanisms.  

So why doesn’t FreeRTOS directly use the malloc() and free() functions in the C standard library? The documentation mentions several considerations: (1) the issue of reentrancy with multiple task calls to C library functions. (2) the complexity of implementation and code efficiency. (3) increasing the complexity of program linking and debugging.  

The memory allocation and deallocation functions used by FreeRTOS are pvPortMalloc() and vPortFree(). In the /portable/MemMang directory, there are several versions of files, each implementing these two functions:

  heap_1.c only allocates memory and cannot be freed. It is used in situations where memory is only created and not destroyed, so there is no issue of memory fragmentation.  heap_2.c can free allocated memory, using the best-fit principle.  heap_3.c manages memory using the standard library’s malloc() and free() functions, adding extra code to avoid reentrancy.  heap_4.c is an improvement on heap_2, which can merge adjacent free memory blocks.  heap_5.c can manage several non-contiguous blocks of SRAM.  

For heap_1.c, heap_2.c, and heap_4.c implementations, the configTOTAL_HEAP_SIZE macro must be defined in the configuration file, which allocates a large fixed block of memory at compile time for FreeRTOS to use as a heap. All memory requested by tasks comes from this predetermined block of memory. Heap_3.c is managed by C library functions, so this macro is not needed. For heap_5.c, obviously, this description is not enough; it requires users to provide the address and size information for each block of SRAM, so it requires users to call the vPortDefineHeapRegions() function to specify memory details.

FreeRTOS Learning Notes: Software Structure of FreeRTOS If you are not satisfied with these options, you can also write your own memory management functions (this is part of portable, and customizing it according to specific needs is its original intention). In fact, for small and well-defined applications, such as just creating one or two tasks to run, dynamic memory allocation is not necessary. It just requires a bit more effort when creating tasks to manually specify the memory addresses for the stack and TCB, and it can work just as well. This is the static creation method supported by FreeRTOS. Since version 9.0.0, the memory management code can be completely omitted. You need to define the configSUPPORT_STATIC_ALLOCATION macro to 1 in the configuration file to support using statically allocated memory to create objects. There is also a macro configSUPPORT_DYNAMIC_ALLOCATION (default value is 1) that determines whether dynamic memory allocation is supported.  

To create objects using statically allocated memory, you need to call API functions that end with Static. For example, xQueueCreateStatic(), which has the following prototype:

QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength, UBaseType_t uxItemSize, uint8_t *pucQueueStorageBuffer, StaticQueue_t *pxQueueBuffer );  

And the conventional version API function for dynamically allocated memory is xQueueCreate():

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );  

It can be seen that the static memory method requires users to provide the memory addresses used by the objects as parameters, which may involve more than one block. Using dynamic memory directly allocates from the heap as needed, making the program easier to write.  

In addition, if the option for static memory is enabled, FreeRTOS also requires users to provide a vApplicationGetIdleTaskMemory() function to obtain the stack memory address, TCB memory address, and stack size for the Idle task. This is because the system will use xTaskCreateStatic() to create the Idle task. For the same reason, when using Timer functionality, a vApplicationGetTimerTaskMemory() function must be provided.

System Built-in Tasks

FreeRTOS always has one task in the running state. If there is nothing to do at the moment, a task representing “system idle” will run—this is the Idle task. When the FreeRTOS scheduler starts, this task is implicitly created. The Idle task has the lowest priority and must yield to any more important tasks unless they are all blocked or suspended.  

The Idle task has one purpose: to manage the hardware’s low-power mode, which I will discuss in another article.  

In addition to the Idle task, FreeRTOS also has a Timer task, which is also known as the Daemon Task, used to assist in completing some functions, such as software timers. This task is optional and is enabled only when the configuration option configUSE_TIMERS is set to 1. Its code is in timers.c. The Timer task’s role is to handle time-related transactions, but these transactions cannot be handled in the clock interrupt ISR (they do not respond to hardware requests and do not have such high real-time requirements), and they must be managed by the scheduler. 

For example, when creating a software timer, a callback function is specified:

TimerHandle_t xTimerCreate(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction );

When the specified time arrives, pxCallbackFunction() will be called by the Timer task, meaning it will be executed in the context of the Timer task, not in the context of the task that created this software timer. 

The Timer task’s other function is to execute the functions specified by xTimerPendFunctionCall(), xTimerPendFunctionCallFromISR(), as I wrote in the (5)th note. These specified functions, along with the callback functions for software timers, are executed sequentially, all using the Timer task’s stack.

System Used Interrupts

To implement time-related functions, hardware timers must be used. FreeRTOS has a function xTaskIncrementTick() that is called by the hardware timer’s ISR. Since the core code is hardware platform-independent, it cannot be written in interrupt form, hence this method is used. In fact, it is equivalent to the system using a timer interrupt, but the ISR belongs to the portable layer, and users can design it freely. This interrupt is not necessarily exclusively for FreeRTOS; for example, some hardware operations can also be performed, as long as xTaskIncrementTick() is called periodically and promptly. To deepen understanding, you can see how different hardware platforms handle port.c. In the implementation of FreeRTOS on ARM Cortex-m0/m3/m4/m7 platforms, PendSV interrupts and SVC interrupts are also used, which are software-generated interrupts, and their ISR is part of the scheduler. However, in the implementation on other hardware platforms, there may not be similar software interrupts available.

Initialization and Task Creation

In a project using FreeRTOS, it is almost inevitable to create tasks, even if it is just one task. For example, you can create the first task to run in main() using the xTaskCreate() API function:

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, const char * const pcName, const uint16_t usStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask);

The first parameter pxTaskCode is the main function (entry address) of the task, and then pcName is a string representing the task name. usStackDepth is very important, as it determines how much memory is allocated for the task stack. pvParameters is used to pass parameters to the task function, uxPriority is the initial task priority, and pxCreatedTask is used to save the task handle, which is the address of the TCB. When created successfully, this function returns pdTRUE, and then the task is in the ready state and can run at any time.  

In fact, the tasks created earlier can only run after the scheduler is activated. When the main program has completed the system initialization work, such as configuring on-chip devices, creating necessary tasks, various communication objects, etc., you can call vTaskStartScheduler() to activate the scheduler. Generally, this function does not return, so the remaining operations will be the functions of each task.vTaskStartScheduler() will also automatically create the Idle task and Timer task, and then select the highest priority task to start running.

Several Simple and Common APIs

When using FreeRTOS features on a new platform, you can use some APIs to implement simple multitasking. Here are a few listed, none of which involve inter-task communication.vTaskDelay() is most commonly used to generate delays. The calling task is immediately blocked, and it resumes to the ready state after the specified number of Timer Ticks has passed. Note that there is an uncertain interval between the time of the next Tick (i.e., the time the hardware timer interrupt occurs) and the time vTaskDelay() is called, so when precise delays are required, consider whether this function meets the requirements.  

vTaskDelayUntil() has similar functionality, except that it specifies an absolute time, while vTaskDelay() is a relative time from the time of the call.  

xTaskGetTickCount() returns the number of Ticks that the scheduler has run, which is the total execution time.  

vTaskSuspend() and vTaskResume() can suspend a task and resume a suspended task, respectively.  

vTaskSuspendAll() and vTaskResumeAll(): These two appear to be extensions of the previous two APIs, but the principles are completely different. vTaskSuspendAll() disables the scheduler, allowing the current task to continue executing while interrupts are still allowed. The limitation is that you cannot call other APIs until you restore the scheduler with vTaskResumeAll(). Unlike critical sections, where hardware interrupts are masked to ensure that task switching does not occur, APIs can still be used.  

vTaskList() receives a character buffer and generates a text format list of all tasks, including status information.  

Summary:Getting started with FreeRTOS is not difficult; you just need to add a few files to the existing project to turn it into a multitasking one. The necessary steps are to decide on configuration options, write the FreeRTOSConfig.h file, and determine the memory management method.

Recommended Reading

Dry Goods | FreeRTOS Learning Notes—Experiment: Serial Port Background Print

Dry Goods | FreeRTOS Learning Notes—Interrupts and Task Switching

Dry Goods | FreeRTOS Learning Notes—Application Scenarios

Dry Goods | FreeRTOS Learning Notes—Stack (Key for Task Switching)

Dry Goods | FreeRTOS Learning Notes—Task States and Switching

Dry Goods | FreeRTOS Learning Notes—Inter-task Communication

Dry Goods | Skills Get√, Simple Measurement of Operational Amplifiers

Dry Goods | PCB High-Frequency Circuit Board Routing Issues

Dry Goods | 40 Animated Images to Help You Understand the Working Principles of Various Common Sensors

Dry Goods | Exploring Losses in Switching Power Supplies

FreeRTOS Learning Notes: Software Structure of FreeRTOS

The following WeChat public accounts belong to

EEWorld (www.eeworld.com.cn)

Welcome to long press the QR code to follow!

FreeRTOS Learning Notes: Software Structure of FreeRTOS

EEWorld Subscription Account: Electronic Engineering World

FreeRTOS Learning Notes: Software Structure of FreeRTOS

EEWorld Service Account: Electronic Engineering World Welfare Society

Leave a Comment

×