In the embedded field, embedded real-time operating systems are becoming increasingly widely used. Using an embedded real-time operating system (RTOS) can more reasonably and effectively utilize CPU resources, simplify application software design, shorten system development time, and better ensure the system’s real-time performance and reliability.
FreeRTOS is a mini real-time operating system kernel. As a lightweight operating system, its features include: task management, time management, semaphores, message queues, memory management, logging functions, software timers, coroutines, etc., which can basically meet the needs of smaller systems.
Since RTOS requires a certain amount of system resources (especially RAM), only a few real-time operating systems such as μC/OS-II, embOS, salvo, and FreeRTOS can run on small RAM microcontrollers. Compared to commercial operating systems like μC/OS-II and embOS, FreeRTOS is a completely free operating system, characterized by open source, portability, scalability, and flexible scheduling policies, making it easy to port to various microcontrollers. Its latest version is 10.3.1.
The task scheduling mechanism is an important concept in embedded real-time operating systems and is also its core technology. For preemptive kernels, a higher-priority task can preempt the CPU usage of a lower-priority task as soon as it is ready, improving the system’s real-time response capability. Unlike μC/OS-II, FreeRTOS does not limit the number of system tasks, supports both priority scheduling algorithms and round-robin scheduling algorithms, and thus uses a doubly linked list instead of a ready task table for task scheduling. The data structures for the linked list and linked list nodes defined by the system are as follows:
// Define linked list structure
typedef struct xLIST{
unsigned portSHORT usNumberOfItems; // usNumberOfItems is the length of the linked list, 0 indicates the list is empty
volatile xListItem * pxHead; // pxHead is the head pointer of the linked list
volatile xListItem * pxIndex; // pxIndex points to the current node of the linked list
volatile xListItem xListEnd; // xListEnd is the tail node of the linked list
} xList;
// Define linked list node structure
struct xLIST_ITEM {
port Tick type; // port Tick Type is the tick count data type,
xItem Value; // xItem Value is used for time management, can be chosen as 16-bit or 32-bit as needed
volatile struct xLIST_ITEM * pxNext; // points to the previous node of the linked list
void * pvOwner; // points to the task control block of this linked list node
void * pvContainer; // points to the linked list of this linked list node
};
In FreeRTOS, each task corresponds to a task control block (TCB), defined as follows:
typedef struct tskTaskControlBlock {
portSTACK_TYPE * pxTopOfStack; // points to the end of the task stack
portSTACK_TYPE * pxStack; // points to the start of the task stack
unsigned portSHORT usStackDepth; // defines the stack depth
signed portCHAR pcTaskName[tskMAX_TASK_NAME_LEN]; // task name
unsigned portCHAR ucPriority; // task priority
xListItem xGenericListItem; // used to insert TCB into the ready list or waiting list
xListItem xEventListItem; // used to insert TCB into the event list (e.g., message queue)
unsigned portCHAR ucTCBNumber; // used for logging
} tskTCB;
FreeRTOS defines the ready task list array as xList pxReadyTasksLists[portMAX_PRIORITIES]. Here, portMAX_PRIORITIES is the maximum priority defined by the system. To make a task with priority n enter the ready state, the corresponding TCB’s node xGenericListItem must be inserted into the list pxReadyTasksLists[n], and the pvContainer in xGenericListItem must point to pxReadyTasksLists[n] to achieve this.
When performing task scheduling, the scheduling algorithm first implements priority scheduling. The system searches for the first non-zero priority in the ready task list array in descending order of priority, and this priority is the current highest ready priority, based on which priority scheduling is implemented. If there is only one ready task at this priority, that task enters the running state; if there are multiple ready tasks at this priority, a round-robin scheduling algorithm is used to implement multitasking.
If the round-robin scheduling algorithm is executed at priority n, the system first executes
(pxReadyTasksLists[n])→pxIndex=(pxReadyTasksLists[n])→pxIndex→pxNext to get the next node pointed to by the current node, then uses this node’s pvOwner pointer to obtain the corresponding task control block, and finally makes the task control block corresponding to this task enter the running state. Thus, in FreeRTOS, the context switch time between tasks of the same priority is one clock tick period.
For example, in Figure 1, if the system’s maximum task number is portMAX_PRIORITIES, at a certain moment during task scheduling, we find pxReadyTasksLists.usNumberOfItems=O(i=2…portMAX_PRIORITIES) and pxReadyTasksLists[1].usNumberOfItems=3. Thus, the kernel knows that the current highest ready priority is 1, and there are already three tasks in the ready state at this priority. Since there are multiple ready tasks at the highest ready priority, the system needs to execute the round-robin scheduling algorithm to implement task switching; through the pointer pxIndex, we can see that task 1 is the current task, and task 1’s pxNext node points to task 2, so the system points pxIndex to task 2 and executes task 2 to implement task scheduling. When the next clock tick arrives, if the highest ready priority is still 1, as shown in Figure 1, the system will point pxIndex to task 3 and execute task 3.
To speed up task scheduling, FreeRTOS tracks the current highest ready priority through the variable ucTopReadyPriority. When a task is added to the ready list, if this task’s priority is higher than ucTopReadyPriority, this task’s priority is assigned to ucTopReadyPriority. Thus, when performing priority scheduling, the scheduling algorithm does not start searching from portMAX_PRIORITIES but from ucTopReadyPriority. This speeds up the search and shortens the kernel shutdown time.
2.2 Implementation of Task Management The effective management of multiple tasks is a primary function of the operating system. FreeRTOS can implement creating tasks, deleting tasks, suspending tasks, resuming tasks, setting task priorities, obtaining task-related information, and other functions. Below, we mainly discuss the implementation of task creation and deletion in FreeRTOS. When calling the sTaskCreate() function to create a new task, FreeRTOS first allocates the required memory for the new task. If memory allocation is successful, it initializes the task control block’s task name, stack depth, and task priority, then initializes the task control block’s stack according to the stack growth direction. Next, FreeRTOS adds the currently created task to the ready task list. If the current task’s priority is the highest, this priority is assigned to the variable ucTopReadyPriority (its function is described in section 2.1). If the task scheduler is already running and the current created task’s priority is the highest, a task switch occurs.
Unlike μC/OS-II, task deletion in FreeRTOS is performed in two steps. When the user calls the vTaskDelete() function, the first step of task deletion is executed: FreeRTOS first removes the task to be deleted from the ready task list and the event waiting list, then adds this task to the task deletion list. If the deleted task is the currently running task, the system executes the task scheduling function, completing the first step of task deletion. When the system idle task, i.e., the prvIdleTask() function, runs, if it finds tasks waiting to be deleted in the task deletion list, it performs the second step of task deletion, which is to release the memory occupied by that task and remove that task from the task deletion list, thus completely deleting the task. It is worth noting that in FreeRTOS, when the system is configured as a non-preemptive kernel, the idle task also implements the switching function for each task.
By comparing the specific codes of μC/OS-II and FreeRTOS, it is found that the two-step deletion strategy helps reduce kernel shutdown time and the execution time of the task deletion function, especially when deleting multiple tasks.
2.3 Implementation of Time Management The typical time management function provided by FreeRTOS is vTaskDelay(). Calling this function can implement delaying a task for a specific period. In FreeRTOS, if a task wants to delay for xTicksToDelay clock ticks, the system kernel adds the total number of clock ticks that have run in the current system (defined as xTickCount, 32-bit length) to xTicksToDelay to get the clock tick number xTimeToWake when the task will be awakened next. Then, the kernel removes this task’s task control block from the ready list, assigns xTimeToWake as the node value to the task’s xItemValue, and inserts the task control block into different lists according to the value of xTimeToWake. If xTimeToWake > xTickCount, meaning no overflow occurred during the calculation, the kernel inserts the task control block into the pxDelayedTaskList list; if xTimeToWake occurs every clock tick, the kernel increments the current xTickCount by 1. If the result of xTickCount is 0, an overflow occurs, and the kernel uses pxOverflowDelayedTaskList as the current list; otherwise, the kernel uses pxDelayedTaskList as the current list. The kernel compares xTickCount with each node’s xTimeToWake in the list. If xTickCount is equal to or greater than xTimeToWake, it indicates that the delay time has arrived, and the task should be removed from the waiting list and added to the ready list.
Thus, unlike μC/OS-II, FreeRTOS uses an “addition” method to implement time management. The advantage is that the execution time of the time tick function is basically independent of the number of tasks, while μC/OS-II’s OSTimeTick() execution time is proportional to the number of tasks created in the application. Therefore, when there are many tasks, FreeRTOS’s time management method can effectively speed up the execution speed of the clock tick interrupt program.
2.4 Memory Allocation Strategy Every time a task, queue, or semaphore is created, FreeRTOS requires a certain amount of RAM to be allocated. Although using malloc() and free() functions can achieve memory allocation and release, these two functions have the following disadvantages: they are not available in all embedded systems, occupy an indeterminate amount of program space, lack reentrancy, and have uncertain execution time. Therefore, in addition to using malloc() and free() functions, FreeRTOS also provides two other memory allocation strategies, allowing users to choose different memory allocation strategies based on actual needs.
The first method is to simply divide a large block of memory into several small blocks according to the size of the required memory, with each small block corresponding to the required memory size. The advantage of this approach is its simplicity, and the execution time can be strictly determined, making it suitable for systems where all tasks and queues are created before kernel scheduling; the disadvantage is that memory cannot be effectively released, and the application program cannot delete tasks or queues during system operation.
The second method is to use linked lists to allocate memory, allowing for dynamic creation and deletion of tasks or queues. The system organizes free memory blocks in ascending order based on their size. When an application program requests a block of memory, the system searches the free memory list in order to find the smallest free memory block that meets the memory request. To improve memory usage efficiency, if the free memory block is larger than the requested memory, the system splits this free memory block into two: one part meets the memory request, and the other part is inserted into the list as a new free memory block.
Below, we introduce the implementation of method 2 using Figure 2. Assuming that the total RAM for dynamic allocation is 8KB, the system first initializes the free memory block list, treating the entire 8KB RAM as one free memory block. After the application program requests 1KB and 2KB of memory, the size of the free memory block becomes 5KB. After the 2KB memory is used up, the system needs to insert the 2KB back into the existing free memory block list. Since 2KB < 5KB, it is inserted before the 5KB memory block. If the application program then requests 3KB of memory, the smallest free memory block that meets the request in the free memory list is 5KB, so the 5KB memory is split into two parts: the 3KB part meets the memory request, and the 2KB part is inserted as a new free memory block. Subsequently, when the 1KB memory is used up and needs to be released, the system will insert the 1KB memory back into the free memory list in order.
The advantage of method 2 is that it can efficiently use memory according to task needs, especially when different tasks require different sizes of memory. The disadvantage of method 2 is that it cannot mix the memory released by the application program with the original free memory, so if the application program frequently requests and releases “random” sizes of memory, it may cause a lot of memory fragmentation. This requires the application program to request and release memory sizes to be “limited” fixed values (for example, in Figure 2, the requested and released memory sizes are fixed at 1KB, 2KB, or 3KB). Another disadvantage of method 2 is that the execution time has a certain degree of uncertainty.
μC/OS-II provides a memory management mechanism that manages large contiguous blocks of memory by partitioning them, with each partition containing an integer number of equally sized memory blocks. Since each partition is the same size, even frequent requests and releases of memory will not cause memory fragmentation issues, but its disadvantage is that the memory utilization rate is relatively low. When the sizes of requested and released memory are fixed (for example, both are 2KB), FreeRTOS’s method 2 memory allocation strategy can achieve similar memory management effects as μC/OS-II.
2.5 Porting FreeRTOS The FreeRTOS operating system can be easily ported to work on different processors, and it has provided ports for various processors such as ARM, MSP430, AVR, PIC, C8051F, etc. The porting of FreeRTOS on different processors is similar to that of μC/OS-II, so this article will not elaborate on the porting of FreeRTOS. In addition, the TCP/IP protocol stack μIP has been ported to FreeRTOS, and specific code can be found on the FreeRTOS website.
2.6 Limitations of FreeRTOS Compared to the common μC/OS-II operating system, FreeRTOS has both advantages and disadvantages. Its shortcomings are reflected in the system’s service functions; for example, FreeRTOS only provides implementations of message queues and semaphores and cannot send messages to the message queue in a last-in-first-out order. On the other hand, FreeRTOS is just an operating system kernel, requiring third-party extensions such as GUI (Graphical User Interface), TCP/IP protocol stack, FS (File System), etc., to implement a more complex system, unlike μC/OS-II, which can seamlessly integrate with μC/GUI, μC/FS, μC/TCP-IP, etc.
As a lightweight operating system, FreeRTOS provides features including: task management, time management, semaphores, message queues, memory management, logging functions, etc., which can basically meet the needs of smaller systems. The FreeRTOS kernel supports priority scheduling algorithms, allowing each task to be assigned a certain priority based on its importance, with the CPU always allowing the highest priority ready task to run first. The FreeRTOS kernel also supports round-robin scheduling algorithms, allowing different tasks to share CPU time when there are no higher priority tasks ready.
The FreeRTOS kernel can be configured as either a preemptive kernel or a non-preemptive kernel according to user needs. When FreeRTOS is set as a preemptive kernel, ready high-priority tasks can preempt the CPU usage of low-priority tasks, ensuring that the system meets real-time requirements; when FreeRTOS is set as a non-preemptive kernel, ready high-priority tasks can only run after the currently running task voluntarily releases CPU usage, which can improve CPU operating efficiency.
Disclaimer Note: The views expressed in this article are solely those of the author and do not guarantee or promise the content’s accuracy or legality. Readers should verify the authenticity and legality of the content themselves. If you find any errors in the source attribution of images, text, or video content, or if your rights have been infringed, please inform us, and we will promptly modify or delete it.