The heart of any Real Time Operating System is the task scheduler. Applications consist of multiple tasks that share the processor by yielding its use when it is (temporarily) not needed, a task of higher priority exists and can run, or when a certain amount of time (the “time slice”) has run out.
I decided to investigate the freeRTOS system that is freely available and can be found on most Arduino board models. In this blog post, I will talk about freeRTOS on 8-bit AVR microcontrollers like those found in the Arduino Uno R3 (note — not the new R4). I started with the basic example, listed below. It has three tasks, Task2, Task3, and Task4, that each toggle a digital pin (pins 2, 3, and 4) at maximum rate. The loop() function is a fourth task that runs when no other task is runnable. It is considered the “idle” task and also toggles a digital pin (pin 5) at maximum rate. There is “behind the scenes” code that starts the scheduler when setup() returns and calls loop() from the hidden idle task.
Baseline Example Source Code
#include <Arduino_FreeRTOS.h>
#define STACK_SIZE 128 // In words (256 bytes)
#define RUN_TIME 2000 // In microseconds 2000
// The time slice is ≈15ms
void Task2(void *pvParameters);
void Task3(void *pvParameters);
void Task4(void *pvParameters);
void setup() {
Serial.begin(9600);
while (!Serial)
;
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(4, OUTPUT);
pinMode(5, OUTPUT);
// Create all tasks at priority 1
xTaskCreate(Task2, "T2", STACK_SIZE, NULL, 1, NULL);
xTaskCreate(Task3, "T3", STACK_SIZE, NULL, 1, NULL);
xTaskCreate(Task4, "T4", STACK_SIZE, NULL, 1, NULL);
// Scheduler starts automatically after setup returns
}
void Task2(void *pvParameters) {
static bool p2State = false;
for (;;) {
unsigned long time2 = micros();
for (; micros() - time2 < RUN_TIME;) {
digitalWrite(2, (p2State = !p2State));
}
vTaskDelay(1);
}
}
void Task3(void *pvParameters) {
static bool p3State = false;
for (;;) {
unsigned long time3 = micros();
for (; micros() - time3 < RUN_TIME;) {
digitalWrite(3, (p3State = !p3State));
}
vTaskDelay(1);
}
}
void Task4(void *pvParameters) {
static bool p4State = false;
for (;;) {
unsigned long time4 = micros();
for (; micros() - time4 < RUN_TIME;) {
digitalWrite(4, (p4State = !p4State));
}
vTaskDelay(1);
}
}
// This runs as the (hidden) Idle Task
void loop() {
// put your main code here, to run repeatedly:
static bool p5State = false;
digitalWrite(5, (p5State = !p5State));
}
Basic Operation
When a task is run it will continue to run until it either yields the processor or the current time slice (which is roughly 15ms) ends. The three tasks here note their starting time and toggle the pin until 2ms from that starting time then call vTaskDelay(1) which yields the processor until the next time slice. The tasks, being all of the same priority, will execute in turn if they are runnable. We see the following (pins 2, 3, 4, and 5 from top to bottom):
Task 2 runs for 2ms, then task 3 runs for 2 ms, then task 4 runs for 2 ms. At this point all three of these tasks are now blocked and cannot run until the next time slice. So the idle task runs until the end of the time slice. At this point the process repeats.
No Explicit Yields
Now what happens if we remove the vTaskDelay calls? Each task will want to run forever. However because of the time slicing, they cannot run beyond the end of the time slice. We see:
Each task now runs for a full time slice and the three tasks run in successive time slices. The idle task never runs.
No Delay Until Next Time Slice
The argument to vTaskDelay is a count of the number of time slices to delay until. If we use vTaskDelay(0), the processor will be yielded but the task is immediately eligible to run again. This is the same as using the taskYIELD() function. Here is the result of replacing the vTaskDelay(1) calls with vTaskDelay(0) or taskYIELD():
We see that the tasks run in turn, but there is still time left in the slice so the tasks run again. Eventually a task gets cut off before its time is up. When it gets to continue later its 2ms from the start time will be well over and the task will yield, losing its slot. You can see that execution is very erratic and time will not be evenly distributed. This is certainly not a good operating mode!
Time Slicing Suspended
One quick partial fix is to turn off the time slicing when we need a task to run to completion. Here Task 2 has been modified to run for 30ms:
void Task2(void *pvParameters) {
static bool p2State = false;
for (;;) {
vTaskSuspendAll();
unsigned long time2 = micros();
for (; micros() - time2 < 30000;) {
digitalWrite(2, (p2State = !p2State));
}
xTaskResumeAll();
}
}
When run we get:
Task 3 and Task 4 can be treated the same way. But at this point we can use another solution which is a change in configuration settings that was done for the Uno R4.
I’ve got Arduino boards with four different microcontroller families that have slight differences in the way FreeRTOS was included. This gives unfortunate incompatibilities that will be discussed in the next blog post.
For full information about FreeRTOS visit https://www.freertos.org/Documentation/RTOS_book.html and download Mastering the FreeRTOS Real Time Kernel, as well as the reference manual.
Comments
Pingback: FreeRTOS Implementations On Newer Arduino Boards - Tom Almy's Blog