RTOS: Difference between revisions
Line 308: | Line 308: | ||
=Semaphores= | =Semaphores= | ||
So semaphores allow more than one resource. You still have all of the problems with updating if you use semaphores for the resource.<br> | So semaphores allow more than one resource. You still have all of the problems with updating if you use semaphores for the resource.<br> | ||
[[File:RTOS semaphore2.png|300px]] | [[File:RTOS semaphore2.png|300px]]<br> | ||
Therefore they are typically used in a producer/consumer. | Therefore they are typically used in a producer/consumer. |
Revision as of 04:48, 29 December 2024
Introduction
Thought I would do a page on RTOS so I have the terminology right. May do the same for Zephyr and Embassy to see how they compare.
RTOS Platforms
For me I have come across the different platforms
- Zephyr running on my Nordic boards
- FreeRTOS no used but the course drove me to this
- IDF RTOS running on my ESP32 (not sure the difference)
Arduino
I guess most people of familiar with the Arduino approach to software with sketches. I dislike the ecosystem and try to avoid it but this is what the course used.
void foo1() {
// Do some function
}
void foo2() {
// Do some function
}
void setup() {
// Do some setup
}
void loop() {
// Do something forever
foo1();
foo2();
}
Using Arduino with RTOS
The purpose of this page we will use only one processor, whereas the ESP32 I am using has two. To disable this we will use the following code. It will be omitted from code examples to save space.
// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
Tasks
In RTOS a task is a unit of work. It have these states
To create a task we use the following code.
// Task to run forever
xTaskCreatePinnedToCore( // Use xTaskCreate() in vanilla FreeRTOS
toggleLED_1, // Function to be called
"Toggle 1", // Name of task
1024, // Stack size (bytes in ESP32, words in FreeRTOS)
NULL, // Parameter to pass to function
1, // Task priority (0 to configMAX_PRIORITIES - 1)
NULL, // Task handle
app_cpu); // Run on one core for demo purposes (ESP32 only)
I guess this is mainly self explanatory. There are other APIs to create a task. I have used xTaskCreate, but I think we are using this to ensure only on CPU is useda task
Scheduler
Clearly the most important part of the deal. This picture was provided with more information than my tiny brain could handle. I guess the salient points were.
- The default timeslice is 1 ms and known as a "tick"
- The hardware time is configure to create an interrupt every tick
- The and the ISR for the timer runs the scheduler
- This chooses which task to run
- The highest priority task is selected
- If there are two, then it uses round robin
- If a task with a higher priority becomes ready it is run immediately rather than wait for the scheduler
- A hardware interrupt is always considered to have a higher priority than any tasks
- You can change priority of a create task two.
Now the pretty picture. Where the pink is an ISR
Memory
Quite liked this picture, although do understand the difference between the heap and the stack, it is a nice picture to remind you.
For RTOS this might be how the memory is allocated.
The task control block for a task holds properties about the task. e.g. Priority stack pointer. The kernel object like semaphores and queues also get allocated on the heap. There are different strategies for allocating memory in FreeRTOS depending on your needs. The demo showed mentioned
- use pvPortMalloc as it is thread safe
- you can use xPortGetFreeHeapSize() to get the amount free in bytes
- use pvPortFree to free the memory
Queues
With all that theory time for some code. Queue can be a way to ensure data is passed to a process in the correct order. This is the pretty picture for the queues challenge
/**
* Solution to 05 - Queue Challenge
*
* One task performs basic echo on Serial. If it sees "delay" followed by a
* number, it sends the number (in a queue) to the second task. If it receives
* a message in a second queue, it prints it to the console. The second task
* blinks an LED. When it gets a message from the first queue (number), it
* updates the blink delay to that number. Whenever the LED blinks 100 times,
* the second task sends a message to the first task to be printed.
*
* Date: January 18, 2021
* Author: Shawn Hymel
* License: 0BSD
*/
// Use only core 1 for demo purposes
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif
// Settings
static const uint8_t buf_len = 255; // Size of buffer to look for command
static const char command[] = "delay "; // Note the space!
static const int delay_queue_len = 5; // Size of delay_queue
static const int msg_queue_len = 5; // Size of msg_queue
static const uint8_t blink_max = 100; // Num times to blink before message
static const int LED_BUILTIN = 23;
// Pins (change this if your Arduino board does not have LED_BUILTIN defined)
static const int led_pin = LED_BUILTIN;
// Message struct: used to wrap strings (not necessary, but it's useful to see
// how to use structs here)
typedef struct Message {
char body[20];
int count;
} Message;
// Globals
static QueueHandle_t delay_queue;
static QueueHandle_t msg_queue;
//*****************************************************************************
// Tasks
// Task: command line interface (CLI)
void doCLI(void *parameters) {
Message rcv_msg;
char c;
char buf[buf_len];
uint8_t idx = 0;
uint8_t cmd_len = strlen(command);
int led_delay;
// Clear whole buffer
memset(buf, 0, buf_len);
// Loop forever
while (1) {
// See if there's a message in the queue (do not block)
if (xQueueReceive(msg_queue, (void *)&rcv_msg, 0) == pdTRUE) {
Serial.print(rcv_msg.body);
Serial.println(rcv_msg.count);
}
// Read characters from serial
if (Serial.available() > 0) {
c = Serial.read();
// Store received character to buffer if not over buffer limit
if (idx < buf_len - 1) {
buf[idx] = c;
idx++;
}
// Print newline and check input on 'enter'
if ((c == '\n') || (c == '\r')) {
// Print newline to terminal
Serial.print("\r\n");
// Check if the first 6 characters are "delay "
if (memcmp(buf, command, cmd_len) == 0) {
// Convert last part to positive integer (negative int crashes)
char* tail = buf + cmd_len;
led_delay = atoi(tail);
led_delay = abs(led_delay);
// Send integer to other task via queue
if (xQueueSend(delay_queue, (void *)&led_delay, 10) != pdTRUE) {
Serial.println("ERROR: Could not put item on delay queue.");
}
}
// Reset receive buffer and index counter
memset(buf, 0, buf_len);
idx = 0;
// Otherwise, echo character back to serial terminal
} else {
Serial.print(c);
}
}
}
}
// Task: flash LED based on delay provided, notify other task every 100 blinks
void blinkLED(void *parameters) {
Message msg;
int led_delay = 500;
uint8_t counter = 0;
// Set up pin
pinMode(LED_BUILTIN, OUTPUT);
// Loop forever
while (1) {
// See if there's a message in the queue (do not block)
if (xQueueReceive(delay_queue, (void *)&led_delay, 0) == pdTRUE) {
// Best practice: use only one task to manage serial comms
strcpy(msg.body, "Message received ");
msg.count = 1;
xQueueSend(msg_queue, (void *)&msg, 10);
}
// Blink
digitalWrite(led_pin, HIGH);
vTaskDelay(led_delay / portTICK_PERIOD_MS);
digitalWrite(led_pin, LOW);
vTaskDelay(led_delay / portTICK_PERIOD_MS);
// If we've blinked 100 times, send a message to the other task
counter++;
if (counter >= blink_max) {
// Construct message and send
strcpy(msg.body, "Blinked: ");
msg.count = counter;
xQueueSend(msg_queue, (void *)&msg, 10);
// Reset counter
counter = 0;
}
}
}
//*****************************************************************************
// Main (runs as its own task with priority 1 on core 1)
void setup() {
// Configure Serial
Serial.begin(115200);
// Wait a moment to start (so we don't miss Serial output)
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.println();
Serial.println("---FreeRTOS Queue Solution---");
Serial.println("Enter the command 'delay xxx' where xxx is your desired ");
Serial.println("LED blink delay time in milliseconds");
// Create queues
delay_queue = xQueueCreate(delay_queue_len, sizeof(int));
msg_queue = xQueueCreate(msg_queue_len, sizeof(Message));
// Start CLI task
xTaskCreatePinnedToCore(doCLI,
"CLI",
1024, // Will crash change to 2024
NULL,
1,
NULL,
app_cpu);
// Start blink task
xTaskCreatePinnedToCore(blinkLED,
"Blink LED",
1024,
NULL,
1,
NULL,
app_cpu);
// Delete "setup and loop" task
vTaskDelete(NULL);
}
void loop() {
// Execution should never get here
}
Doing this course made it very much easier to fix. The ESP32 crashes with
The line "Stack canary watchpoint triggered" points to the task with a problem. In our case it was the CLI task. We can debug this with Serial.println statements but the reason it failed was not a large enough stack size. I would not have guessed this before the course.
Mutexes
familiar ground with some terms to remember.
- critical section is a segment of code that needs to be executed as a single unit without interruption
- Mutual exclusion (mutex) is a technique that prevents multiple threads from accessing a shared resource at the same time
So no surprises there. Note the API is xSemaphore but these are mutexes. For FreeRTOS there are several APIs to do this depending on you situation but here are the main ones.
// Create a mutex
xSemaphoreCreateMutex()
// Take the mutex
// It will wait until mutex available for portMAX_DELAY and return false if not
xSemaphoreTake(mutex, portMAX_DELAY);
// Start Critical Code here
...
// End Critical Code here
// Release the mutex so that the creating function can finish
xSemaphoreGive(mutex);
Semaphores
So semaphores allow more than one resource. You still have all of the problems with updating if you use semaphores for the resource.
Therefore they are typically used in a producer/consumer.