This post will discuss the FreeRTOS itself. One of its main capabilities is to simulate a multitasking. We know each processor core can only be processing one task at a time, but when you change the task being processed quickly enough, you can pretend that all are being processed at the same time. FreeRTOS does that with the use of the scheduler, which is basically the piece of code that will choose which task will run. Every once in a while, the scheduler will stop the current task execution and handle the processor's control, along with the task's stack, to another task, that eventually will be stopped (or will hold its own execution, i.e. through a delay) and another will be assigned and so on.
Stack
Each task usually needs some memory to store some variables. This memory, on bare metal projects, is usually the RAM itself, with a well defined address within it. When talking about an RTOS, the RAM is divided into pieces, called stacks [1], and each piece will hold the task's memory (the size of the piece is defined when the task is created, on
xCreateTask()'s parameters). This piece will also hold a few more information relevant to the task, such as the point where the code was stopped the last time, the processor's registers values, etc, thus it needs a minimal size, which is defined, in Blinky project, as
configMINIMAL_STACK_SIZE. The developer should always think well how much memory the task needs in order not to have problems. The stack is usually bigger than it actually needs, as you better spare memory and don't have a stack overflow problem. Other points to be reviewed before determining the size are [2]:
- The function call nesting depth
- The number and size of function scope variable declarations
- The number of function parameters
Priorities
Imagine you have three tasks running the whole time doing what they're were meant to do. When one task execution is stopped, how does the scheduler choose which task will run next? If the tasks have different priorities (defined on
xCreateTask()'s parameters), then the highest priority will run [3]. Care must be taken in this situation, otherwise, one task may never run (imagine two tasks that never stop executing by they're own have higher priority than another: when one task is running, the other is always waiting in front of the line, so that the lowest priority is always in the back and never runs). The lowest priority is defined by
tskIDLE_PRIORITY and the higher the number, the higher the priority.
Task states
I've talked about a few of the states, like when the task is blocked (for example, when the Sender stops for a delay), running or waiting to be executed, so let's check those states thoroughly [4]. The tasks can be in 4 states: Running (it is using the processor), Ready (it is waiting to be processed), Blocked (it is waiting for some event, temporal - such as a delay - or external - such as a queue or semaphore event; in this case, it is not running nor waiting to be processed) or Suspended (it explicitly entered this state through the function
vTaskSuspend() and can only exit by another explicit call of the function
xTaskResume(); in this state, it is also not running nor waiting to be processed).
The task will enter the Ready state when it is created, when the scheduler stops its execution, when the event that was blocking it occurs or when it exits the Suspended state. It will only enter the Running state when the scheduler says so.
Idle Task
If there are no tasks on Ready state (waiting to run), there is always one simple task, with lowest priority level possible that will use the processor: the Idle Task [5]. Basically, what it does is to free the memory from deleted tasks (if there are any and if your application allow that). When at least one task has the priority as the Idle, it is possible to tell the Idle task to yield the execution as soon as possible when these other tasks are in a Ready state [6]. It is possible to run some code whenever the Idle Task is called by the use of the Idle Task Hook, although it is imperative that the function doesn't block the task (for instance, using a
vTaskDelay() or waiting for a semaphore), if that is the case, you should write a task with the Idle Task priority (this will use more RAM, but will permit more flexibility).
configASSERT()
This function is quite important during the development phase. It is often used to check if the task execution is OK and can continue. For instance, when the Sender and Receiver tasks check whether the parameters are right, it does so with the use of the
configASSERT() [7]. This function will call another function when there is a problem:
vAssertCalled(), which, in this project, will stop the execution and place the application on a loop. This function should be written by the developer in order to facilitate the debug of the code. When the project is mature enough, it can be suppressed to avoid code overhead.
[1]
http://www.freertos.org/Stacks-and-stack-overflow-checking.html
[2]
http://www.freertos.org/FAQMem.html#StackSize
[3]
http://www.freertos.org/RTOS-task-priority.html
[4]
http://www.freertos.org/RTOS-task-states.html
[5]
http://www.freertos.org/RTOS-idle-task.html
[6]
http://www.freertos.org/a00110.html#configIDLE_SHOULD_YIELD
[7]
http://www.freertos.org/a00110.html#configASSERT