Real-Time Operating Systems (RTOS)#

Introduction to Real-Time Constraints and RTOS#

Real-Time Constraints:

  • Real-time systems are systems where correctness depends not only on logical output but also on the timing of the output.

  • Hard Real-Time: Missing a deadline can cause catastrophic failure (e.g., medical devices, automotive braking systems).

  • Soft Real-Time: Missing a deadline reduces performance but is tolerable (e.g., video playback, gaming).

Real-Time Operating System (RTOS):

  • An RTOS is an operating system specifically designed to manage hardware resources and task execution with strict timing requirements.

  • Provides deterministic behavior by ensuring tasks meet their deadlines with predictable scheduling and resource allocation.

Key Features:

  1. Deterministic Task Scheduling: Ensures tasks are executed within their time constraints.

  2. Task Prioritization: Allows critical tasks to preempt lower-priority ones.

  3. Inter-task Communication: Mechanisms like queues, semaphores, and message passing.

  4. Resource Management: Manages hardware resources efficiently to avoid conflicts and bottlenecks.

Task Scheduling, Context Switching, and Inter-task Communication#

Task Scheduling:

  • Determines the order in which tasks execute based on priority, timing, or dependencies.

  • Scheduling Algorithms:

    1. Rate Monotonic Scheduling (RMS): Fixed priorities based on task frequency.

    2. Earliest Deadline First (EDF): Dynamic priorities based on deadlines.

    3. Round Robin (RR): Equal time slices for tasks in a cyclic order.

Context Switching:

  • The process of saving a task’s state and loading another’s to switch execution between tasks.

  • Essential for multitasking in an RTOS but introduces overhead, which must be minimized in real-time systems.

Inter-task Communication:

  • Mechanisms to exchange data between tasks safely and efficiently:

    1. Queues: FIFO buffers for data transfer between tasks.

    2. Semaphores: Synchronize access to shared resources.

    3. Mutexes: Protect critical sections to prevent race conditions.

    4. Message Passing: Structured communication between tasks.

Setting Up and Using a Basic RTOS (FreeRTOS, for example)#

FreeRTOS Overview:

  • A popular open-source RTOS that is lightweight and suitable for microcontrollers.

  • Provides task management, timers, queues, semaphores, and mutexes.

Steps to Set Up FreeRTOS:

  1. Download and Configure FreeRTOS:

    • Download from FreeRTOS.org.

    • Select the port for your microcontroller (e.g., ARM Cortex-M, AVR).

  2. Integrate FreeRTOS with Your Project:

    • Include the FreeRTOS source files in your project directory.

    • Configure the FreeRTOSConfig.h file to match your hardware settings (e.g., clock frequency, stack size).

  3. Create Tasks:

    • Use the xTaskCreate function to define tasks with specific priorities and entry functions.

    • xTaskCreate(vTaskFunction, “TaskName”, STACK_SIZE, NULL, PRIORITY, &TaskHandle);

  4. Start the Scheduler:

    • Start the FreeRTOS scheduler with vTaskStartScheduler();.

  5. Use Synchronization and Communication Mechanisms:

    • Implement semaphores, mutexes, or queues as needed.

Example Code Snippet:

void Task1(void *pvParameters) {

while (1) {

// Task code

vTaskDelay(pdMS_TO_TICKS(1000)); // Delay for 1 second

}

}

void Task2(void *pvParameters) {

while (1) {

// Task code

vTaskDelay(pdMS_TO_TICKS(500)); // Delay for 500ms

}

}

int main() {

xTaskCreate(Task1, “Task 1”, 100, NULL, 1, NULL);

xTaskCreate(Task2, “Task 2”, 100, NULL, 2, NULL);

vTaskStartScheduler(); // Start FreeRTOS scheduler

while (1); // Code never reaches here

}

When and Why to Use an RTOS#

When to Use an RTOS:

  1. Real-Time Requirements:

    • Applications with strict deadlines (e.g., industrial automation, robotics).

  2. Complex Task Management:

    • Multiple concurrent tasks with different priorities and dependencies.

  3. Need for Modularity:

    • Separating functionality into independent tasks improves code organization and scalability.

  4. Resource Sharing:

    • Multiple tasks need to access shared peripherals or memory.

Why Use an RTOS:

  1. Deterministic Scheduling:

    • Ensures predictable behavior for time-critical tasks.

  2. Simplified Multitasking:

    • Built-in task management eliminates the need for custom scheduling logic.

  3. Portability:

    • RTOS-based applications are easier to port between hardware platforms.

  4. Efficient Resource Management:

    • Synchronization primitives (e.g., semaphores) prevent race conditions and deadlocks.

When Not to Use an RTOS:

  • For simple applications with minimal timing constraints or tasks, where a bare-metal approach suffices.

Using an RTOS like FreeRTOS can significantly enhance the performance and maintainability of complex embedded systems with real-time constraints.

Simple cooperative task scheduler#

A cooperative task scheduler is a type of multitasking system where tasks voluntarily yield control back to the scheduler to allow other tasks to run. Unlike preemptive schedulers, where the scheduler interrupts tasks to switch execution, a cooperative scheduler relies on each task to explicitly indicate when it has finished its work or when it is safe for another task to execute.

Key Features of a Cooperative Task Scheduler#

  • Voluntary Yielding:

    • Tasks must explicitly call a function (e.g., yield()) or complete execution to return control to the scheduler.

  • Non-Preemptive:

    • The scheduler does not forcibly interrupt or switch tasks.

    • Tasks run until they yield, making it simpler to manage but prone to issues if a task runs too long.

  • Simpler Implementation:

    • Since tasks do not preempt each other, the scheduler does not need complex mechanisms like context switching or interrupt handling.

  • Deterministic:

    • The execution flow is predictable because tasks execute in a defined order based on their scheduling policy.

Advantages of a Cooperative Scheduler#

  • Low Overhead:

    • Requires minimal resources since there’s no need for preemption or context switching.

  • Simplicity:

    • Easier to implement and debug compared to preemptive multitasking systems.

  • Predictability:

    • Tasks execute in a well-defined sequence, making it suitable for some real-time systems where precise task timing is required.

  • No Race Conditions:

    • Because only one task runs at a time, resource conflicts are minimized (as long as the developer ensures correct resource sharing).

Disadvantages of a Cooperative Scheduler#

  • Responsibility on Tasks:

    • Tasks must be designed to yield control periodically. A poorly designed task can block other tasks indefinitely.

  • Poor Fault Isolation:

    • If one task misbehaves (e.g., enters an infinite loop), the entire system can stall.

  • Limited Real-Time Capabilities:

    • It is challenging to guarantee strict timing constraints in systems with many tasks.

  • Less Suitable for Complex Systems:

    • As the number of tasks grows, ensuring fairness and responsiveness becomes harder without preemption.

How It Works#

  • Initialization:

    • The scheduler initializes a list of tasks with their associated execution routines.

  • Task Execution:

    • The scheduler loops through the list of tasks and executes them one by one.

    • A task runs until it voluntarily yields or completes its job.

  • Yielding:

    • The task calls a yield() function to hand control back to the scheduler, allowing another task to execute.

  • Repeat:

    • The scheduler repeats the process, giving tasks a chance to execute in a cooperative manner.

Example Use Cases#

  • Small Embedded Systems:

    • Cooperative schedulers are ideal for resource-constrained environments with simple task requirements.

  • Applications with Predictable Workloads:

    • Systems where tasks are well-behaved and designed to share CPU time fairly.

  • Teaching and Prototyping:

    • A great starting point for understanding multitasking concepts in embedded systems.

Comparison: Cooperative vs Preemptive Scheduling#


Feature Cooperative Scheduler Preemptive Scheduler


Task Control Tasks voluntarily yield Scheduler forcibly preempts control. tasks.

Implementation Simple, no context More complex, requires switching required. context switching.

Timing Harder to achieve strict Better for real-time Guarantees timing. systems.

Fault Isolation A faulty task can block Faulty tasks can be others. preempted.

Resource Usage Low (no interrupts/context Higher due to preemption switching). overhead.#

A cooperative task scheduler is a lightweight and efficient choice for simple applications where tasks are predictable and cooperative. However, it requires careful design to ensure fairness and prevent issues like task starvation or blocking.

An example of a cooperative scheduler that schedules tasks at intervals of 10ms, 20ms, 40ms, 80ms, 160ms, 320ms, 640ms, and 1280ms, with a time slice of 5ms is shown below.

This implementation assumes that a hardware timer triggers the scheduler every 5ms, allowing the system to track elapsed time and execute tasks at their respective intervals.

#include <stdio.h>

#include <stdbool.h>

#include <stdint.h>

#define MAX_TASKS 8 // Total number of tasks

// Task structure

typedef struct {

void (*taskFunc)(void); // Function pointer to the task

uint32_t period; // Task period in milliseconds

uint32_t lastRunTime; // Last time the task was executed

} Task;

// Task array

Task taskList[MAX_TASKS];

// Simulated system time in milliseconds

volatile uint32_t systemTime = 0;

// Simulated 5ms timer interrupt (increment system time)

void timerISR(void) {

systemTime += 5; // Simulates a hardware timer that triggers every 5ms

}

// Cooperative scheduler function

void scheduler_run(void) {

while (1) {

for (int i = 0; i < MAX_TASKS; i++) {

// Check if the task is ready to run

if ((systemTime - taskList[i].lastRunTime) >= taskList[i].period) {

taskList[i].lastRunTime = systemTime; // Update last run time

taskList[i].taskFunc(); // Execute the task

}

}

}

}

// Task functions

void task10ms(void) { printf(“Task 10ms running at %lu ms\n”, systemTime); }

void task20ms(void) { printf(“Task 20ms running at %lu ms\n”, systemTime); }

void task40ms(void) { printf(“Task 40ms running at %lu ms\n”, systemTime); }

void task80ms(void) { printf(“Task 80ms running at %lu ms\n”, systemTime); }

void task160ms(void) { printf(“Task 160ms running at %lu ms\n”, systemTime); }

void task320ms(void) { printf(“Task 320ms running at %lu ms\n”, systemTime); }

void task640ms(void) { printf(“Task 640ms running at %lu ms\n”, systemTime); }

void task1280ms(void) { printf(“Task 1280ms running at %lu ms\n”, systemTime); }

int main(void) {

// Initialize tasks

taskList[0] = (Task){task10ms, 10, 0};

taskList[1] = (Task){task20ms, 20, 0};

taskList[2] = (Task){task40ms, 40, 0};

taskList[3] = (Task){task80ms, 80, 0};

taskList[4] = (Task){task160ms, 160, 0};

taskList[5] = (Task){task320ms, 320, 0};

taskList[6] = (Task){task640ms, 640, 0};

taskList[7] = (Task){task1280ms, 1280, 0};

// Simulate the scheduler running with a 5ms timer

while (1) {

timerISR(); // Simulate a 5ms interrupt

scheduler_run();

}

return 0;

}

Explanation:#

  • Timer Simulation:

    • timerISR increments the system time every 5ms, simulating a periodic hardware timer.

  • Task Periodicity:

    • Each task is assigned a period (e.g., 10ms, 20ms).

    • The scheduler checks if the difference between the current system time and the task’s last run time is greater than or equal to the task’s period.

  • Task Execution:

    • If a task is ready to run, it executes, and the lastRunTime is updated to the current system time.

  • Scheduler Loop:

    • Continuously checks all tasks in the taskList to see if they are ready to execute.

Output Example:#

Here’s a simulated output for 50ms of system time:

Task 10ms running at 10 ms

Task 20ms running at 20 ms

Task 10ms running at 20 ms

Task 40ms running at 40 ms

Task 10ms running at 40 ms

Task 20ms running at 40 ms

Key Features:#

  • Each task runs at its specified interval.

  • The system time and periodic checks ensure precise scheduling.

  • This approach is efficient and simple for systems without preemptive multitasking.