20.1 What is multi-tasking in an embedded controller
An embedded controller like the ATmega328P MCU has multi-tasking capability which means that the controller can be programmed (with the help of a specialized software called Scheduler or Real Time Operating System, RTOS) to execute many tasks concurrently/simultaneously. Concurrency is not true parallelism but semi-parallelism. In true parallelism, one MCU/processor is dedicated for each tasks. If there are two tasks, then there are two processors, and hence the name Dual Core.
By nature, the MCU is a serial device. It has no ability to execute two tasks in parallel (at the same time). It has to perform them in sequence meaning one after another. This causes a serious problem in practical situation as the second task has to wait until the first task is completed. For example:
The first task (Task-1) is the acquisition, processing, display, and transmission of a Boiler Temperature and say it takes about 100 ms; the second task (Task-2) is to monitor the temperature of a hazardous area for possible breakout of fire and then initiate "alarm".
In the above example, the waiting for 100 ms completion time for Task-1 could be too long and should not be tolerated; because, in the meantime, the hazardous area might has started smoking indicating a possible breakout of fire.
The above unwanted situation can be easily avoided by programming the MCU (with the help of RTOS) to execute both tasks concurrently where the MCU spends a "tiny time slice (say: 15 ms Tick, Fig-1)" for each task in round-robin basis. This way, the execution phase of each task gradually proceeds towards end.
Here, in fact, the tasks are being executed one-after-another; but, the RTOS makes us believing that the tasks are being executed concurrently (semi parallelism) by switching the MCU from one task to another at tremendous speed (Fig-1).
Figure-1:
20.2 What does MCU do during 15 ms time slice when it turns to Task-1?
During compilation process, the sketch of Task-1 is converted into binary codes which are loaded into the flash memory of the MCU. Taking the average execution time of one instruction (on a 16 MHz Arduino UNOR3) as 1 cycle (0.0000000625 sec), the MCU executes .015/0.0000000625 = 240,000 instructions within the 15 ms time slice period before switching to Task-2.
Some instructions may take 2 cycles time for execution; if so, then the MCU will always execute an integral number of instructions whose total execution time must be <= 0.015 sec. The rest of the time just goes unused (I guess).
20.3 What is the role of Scheduler/FreeRTOS?
In FreeRTOS Arduino UNOR3 platform, the time slice is 15 ms, and a time slice is known as a “Tick.” A hardware timer is configured to generate an interrupt every 15 ms. The ISR for that timer runs the scheduler/FreeRTOS, which chooses the task to run next. At each Tick interrupt, the task with the highest priority is chosen to run. The Scheduler is a complex program loaded into the flash memory of UNO along with the sketch.
FreeRTOS allows us to set priorities for tasks, which allows the scheduler to preempt lower priority tasks with higher priority tasks. The scheduler is a piece of software inside the FreeRTOS operating system in charge of figuring out which task should run at eachTtick.
20.4 Conceptual view of a multi-task (multi-thread) program under FreeRTOS
Figure-2:
In Arduino IDE environment, after startup of the MCU, the control program executes the main() function and then enters into the setup() function.
int main(void) // Normal Arduino main.cpp. Normal execution order.
{
init();
initVariant(); // Our initVariant() diverts execution from here.`
}
void initVariant(void)
{
setup(); // The Arduino setup() function.`
vTaskStartScheduler(); // Initialise and run the FreeRTOS scheduler.
}
void setup(void)
{
xTaskCreate(Task_A, "TaskA", 128, NULL, 4, NULL );
xTaskCreate(Task_B, "TaskB", 128, NULL, 4, NULL );
xTaskCreate(Task_C, "TaskC", 128, NULL, 4, NULL );
}
If Task A, Task B, and Task C (Fig-2) are created in the Setup() function using xTaskCreate() function of the Arduino_FreeRTOS.h Library, the Arduino UNO system begins to work in multi-tasking environment by triggering the Scheduler/FreeRTOS. Task A, Task B, and Task C (NOT Task loop()) appear to run concurrently in their respective endless local while(true){} loops assuming that a task in not ended after a single execution.
If the tasks are assigned equal priority, then the scheduler/FreeRTOS serves them in round-robin fashion spending 15 ms (Tick Period) for each task. The Scheduler/FreeRTOS configures a hardware timer of the MCU to generate an interrupting signal at 15 ms (tick) interval, which triggers the scheduler to switch across the tasks.
If no task is created in the setup() function using xTaskCreate() function of the Arduino_FreeRTOS.h Library, the control program executes the setup() function and then enters into the loop() function. The system begins to work in "normal fashion" under Arduino IDE.
int main(void) // Normal Arduino main.cpp. Normal execution order.
{
init();
initVariant(); // Our initVariant() diverts execution from here.
setup(); // The Arduino setup() function.
for (;;)
{
loop(); // The Arduino loop() function.
if (serialEventRun) serialEventRun();
}
return 0;
}
The FreeRTOS idle task is used to run the loop() function whenever there is no unblocked FreeRTOS task available to run. In the trivial case, where there are no configured FreeRTOS tasks, the loop() function will be run exactly as normal, with the exception that a short scheduler interrupt will occur every 15 milli-seconds (configurable).
void vApplicationIdleHook( void )
{
loop(); // The Arduino loop() function.
if (serialEventRun) serialEventRun();
}
20.5 Task State Diagram in FreeRTOS
Figure-3:
20.6 Example-1
Create Arduino_FreeRTOS.h based sketch to blink the two LEDs of the following circuit (Fig-4) concurrently; where, LED1 will blink at 1-sec interval and LED2 will blink at 2-sec interval.
Figure-4:
Sketch:
#include <Arduino_FreeRTOS.h>
TaskHandle_t myTaskA;
TaskHandle_t myTaskB;
#define LED1 5
#define LED2 6
void setup()
{
pinMode(LED1, OUTPUT); //LED1
pinMode(LED2, OUTPUT); //LED3
xTaskCreate(myTaskFunctionA, "MyTaskA", 128, NULL, 1, &myTaskA);
xTaskCreate(myTaskFunctionB, "MyTaskB", 128, NULL, 1, &myTaskB);
}
void loop()
{
//leave it empty for Arduino UNO
}
void myTaskFunctionA(void *pvParameters)//LED1
{
for (;;)
{
digitalWrite(LED1, HIGH);
vTaskDelay(pdMS_TO_TICKS(500)); // Delay for 1/2 second
digitalWrite(LED1, LOW);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void myTaskFunctionB(void *pvParameters)//LED2
{
for (;;)
{
digitalWrite(LED2, HIGH);
vTaskDelay(pdMS_TO_TICKS(1000)); // Delay for 1 second
digitalWrite(LED2, LOW);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
Timing Diagram:
Figure-5:
When tasks are created, they are included in the “ready to run list”. Both tasks are created with equal priority; so, they will be executed on round-robin basis.
At time t1, myTaskA is executed to turn On LED1. After that the instruction vTaskDelay(pdMS_TO_TICKS(500)) is executed. The task enters into blocked state, and it is marked as unready in the “ready to run list”. The task remains in blocked state until the 500 ms time delay is elapsed.
At time t2, the 15 ms apart Time Tick arrives; the Scheduler finds myTaskB as a ready task, and the task is executed; LD2 is turned On. After that the instruction vTaskDelay(pdMS_TO_TICKS(500)) is executed. The task enters into blocked state, and it is marked as unready in the “ready to run list”. The task remains in blocked state until the 500 ms time delay is elapsed.
From time t3 to t4, the Tick has interrupted the MCU multiple times; the Scheduler has been alerted, but it has not executed any tasks as they are in unready states.
At t4, myTaskA is ready; at Tick point, the Scheduler will execute the instructions digitalWrite(LED1, LOW) to turn Off LED1. After that, the task will enter into blocked state.
During the blocked period, the MCU just spends time doing nothing. Is it possible to assign some tasks to the MCU?
20.7 Example-2
The following setup of Fig-20.1 is composed of "Arduino UNO R3 + Arduino_FreeRTOS" Platform; where tasks would be running concurrently and would be outputting messages at often on Serial Monitor (SM). The Tasks are:
1. LED2 blinks at 4-sec interval.
2. When Pot1 is at fully CCW position, LED1 blinks at 1 Hz interval; when Pot1 is at fully CW position, LED1 blinks at 20 Hz interval.
3. When button K1 is pressed, the MCU is interrupted and L (built-in LED of UNO) blinks for 5 times at 1-sec interval.
Figure-6:
Sketch:
To implement the objectives of Fig-1, I have prepared the following sketch by consulting the beautiful Examples of the Arduino IDE. The sketch is working fine and responds very well to interrupt switch (K1) and Pot1.
#include <Arduino_FreeRTOS.h>
#include <queue.h>
#include <semphr.h>
QueueHandle_t integerQueue;
SemaphoreHandle_t interruptSemaphore;
#define INT0 2
// define tasks for Blink & AnalogRead
void TaskBlinkL( void *pvParameters ); //blinks L at 1 sec interval
void TaskBlinkLED2( void *pvParameters ); //blinks LED2 at 2 sec interval
void TaskAnalogReadA1( void *pvParameters ); //task that publiseh value of A1-pin in Queue
void TaskPwm( void *pvParameters ); //blink rate of LED1 depends on value of A1-pin
void setup()
{
Serial.begin(9600);
while (!Serial)
{
; // wait for serial port to connect.
}
pinMode(13, OUTPUT);
pinMode(INT0, INPUT_PULLUP);
//----initialize PWM (Mode 14 FPWM) at DPin-9 -----
pinMode(9, OUTPUT); //Ch-A
TCCR1A = 0x00; //reset
TCCR1B = 0x00; //TC1 reset and OFF
//fOC1A/B = clckSys/(N*(1+ICR1); Mode-14 FPWM; OCR1A controls duty cycle
// 1 Hz = 16000000/(256*(1+ICR1) N=1,8,64,256,1024==> ICR1 = 62499
// 20 Hz ; ICR1 = 3124
TCCR1A |= (1 << WGM11); //Mode-14 Fast PWM
TCCR1B |= (1 << WGM13) | (1 << WGM12); //Mode-14 Fast PWM
TCCR1A |= (1 << COM1A1) | (0 << COM1A0); //Non-invert: HIGH-LOW
ICR1 = 62499; // TOP for 1Hz frequnecy
OCR1A = 31250; //= 50% duty cycle
TCNT1 = 0;
TCCR1B |= (1 << CS12);//TC1 statrt with N = 256;
//--------------------------------------------
// Now set up tasks to run independently.
interruptSemaphore = xSemaphoreCreateBinary();
if (interruptSemaphore != NULL)
{
attachInterrupt(digitalPinToInterrupt(2), interruptHandler, LOW);// Attach interrupt
}
//-----------------------------------------------
integerQueue = xQueueCreate(
10,
sizeof (int)); //Queue length, queue item size
if (integerQueue != NULL)
{
xTaskCreate( // Create task that consumes the queue if it was created.
TaskPwm, // Task function
"Serial", // A name just for humans
128, // This stack size can be checked & adjusted by reading the Stack Highwater
NULL,
1, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
NULL);
xTaskCreate( // Create task that publish data in the queue if it was created.
TaskAnalogReadA1,
"AnalogReadA1",
128, // Stack size
NULL,
1, // Priority
NULL );
}
//----------------------------------------------------
xTaskCreate(
TaskBlinkL,
"BlinkL", // A name just for humans
128, // This stack size can be checked & adjusted by reading the Stack Highwater
NULL,
1, // Priority, with 4 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
NULL );
xTaskCreate(
TaskBlinkLED2,
"BlinkLED2", // A name just for humans
128, // This stack size can be checked & adjusted by reading the Stack Highwater
NULL,
1, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
NULL );
xTaskCreate(
TaskPwm,
"PWMLED1",
128, // Stack size
NULL,
1, // Priority
NULL );
// Now the task scheduler, which takes over control
//of scheduling individual tasks, is automatically started.
}
void loop() //called upon by Task Idle of FreeRTOS
{
}
//---------------------------------------------
void TaskBlinkLED2(void *pvParameters) // This is a task.
{
(void) pvParameters;
pinMode(11, OUTPUT);
while (true) // A Task shall never return or exit.
{
digitalWrite(11, HIGH); // turn the LED on (HIGH is the voltage level)
vTaskDelay( 2000 / portTICK_PERIOD_MS ); // wait for 2 second
digitalWrite(11, LOW); // turn the LED off by making the voltage LOW
vTaskDelay( 2000 / portTICK_PERIOD_MS ); // wait for 2 second
}
}
//--------------------------------------------
void TaskAnalogReadA1(void *pvParameters)
{
(void) pvParameters;
while (true)
{
// Read the input on analog pin 0:
int adcValue = analogRead(A1);
xQueueSend(integerQueue, &adcValue, portMAX_DELAY);
vTaskDelay(1);// One tick delay (15ms) in between reads for stability
}
}
void TaskPwm(void * pvParameters)
{
(void) pvParameters;
int valueFromQueue = 0;
while (true)
{
if (xQueueReceive(integerQueue, &valueFromQueue, portMAX_DELAY) == pdPASS)
{
Serial.print("ADC-A1 value received via Queue: ");
Serial.println(valueFromQueue, DEC); //0 to 1023
Serial.println("===================");
ICR1 = map(valueFromQueue, 0, 1023, 62499, 3124); //1Hz (FCCW) - 20Hz(FCW)
OCR1A = ICR1 / 2; //maintains 50% duty cycle
vTaskDelay(1000 / portTICK_PERIOD_MS );
}
}
}
//------------------------------
void interruptHandler()
{
xSemaphoreGiveFromISR(interruptSemaphore, NULL);
}
//-------------------------------
void TaskBlinkL(void *pvParameters)
{
(void) pvParameters;
while (true)
{
if (xSemaphoreTake(interruptSemaphore, portMAX_DELAY) == pdPASS)
{
for (int i = 0; i < 5; i++)
{
digitalWrite(13, HIGH);
vTaskDelay(500 / portTICK_PERIOD_MS ); //500 ms delay
digitalWrite(13, LOW);
vTaskDelay(500 / portTICK_PERIOD_MS );
}
}
}
}