Scheduling Tasks
An embedded application typically needs to execute multiple tasks at different periods (e.g., reading a temperature sensor every 500ms, updating a display every 100ms, and blinking a status LED every 1000ms). How we organize these tasks determines the predictability and stability of the system.
Super Loop vs. Timer-Controlled Dispatcher
1. The Super Loop (Polled Execution)
A super loop executes tasks sequentially in a continuous loop, using delay functions to manage timing:
int main(void) {
while (1) {
read_sensor();
update_display();
_delay_ms(100); // Wait between loops
}
}
- Pros: Easy to understand; does not require complex timer registers, interrupts, or function pointers.
- Cons: Timing is fragile. If
read_sensor()blocks for 50ms (e.g., waiting for an I/O flag), the display update and all subsequent loops are delayed, causing timing jitter.
2. The Timer-Controlled Dispatcher
A dispatcher splits execution into an asynchronous timer interrupt (counting ticks) and a synchronous main loop (executing tasks). This allows multiple independent periodic tasks to run off a single hardware timer.
- Pros: Precise periodic execution intervals; timing of one task is insulated from minor variations in others.
- Cons: Overrun hazard—if a task takes longer to execute than the timer period, it can delay other pending tasks.
Example: Scheduler Implementation in C
The scheduler represents each task using a control structure:
typedef struct {
uint32_t period; // Target execution period in milliseconds
void (*task_p)(void); // Pointer to the task function
uint32_t ticks; // Decrementing counter tracking when to run
} task_t;
The Tick Decrement (ISR Context)
A hardware timer is configured to fire at a regular interval (e.g., every 10ms, which is ). The ISR loops through all registered tasks and decrements their tick counters:
#define MS_PER_TICK 10
ISR(TIMER1_COMPA_vect) {
for (uint8_t i = 0; i < task_count; i++) {
if (task_list[i].ticks > 0) {
task_list[i].ticks--;
}
}
}
The Dispatcher (Main Thread Context)
The dispatcher runs continuously inside the main loop. It scans for tasks whose ticks have reached 0, executes them, and resets their tick countdown:
void dispatcher(void) {
for (uint8_t i = 0; i < task_count; i++) {
if (task_list[i].ticks == 0) {
// 1. Run the task callback
task_list[i].task_p();
// 2. Reset the tick counter based on the task's period
task_list[i].ticks = task_list[i].period / MS_PER_TICK;
}
}
}
int main(void) {
timer1_init_10ms_tick();
scheduler_add_task(100, update_display);
scheduler_add_task(500, read_sensor);
while(1) {
dispatcher(); // Poll and run ready tasks
}
}
In a 10ms tick dispatcher, what happens if a task registered with a 20ms period takes 35ms to execute?
Implement Tick Decrement Loop
// Inside the timer ISR, decrement ticks if they are greater than zero if (task_list[i].ticks 0) { task_list[i].--; }
References & Further Reading
- Pont, M. J. (2001). Patterns for Time-Triggered Embedded Systems. Addison-Wesley. Chapters 1–3 (Schedulers).
- VIA University College: Embedded Software 1 (ESW1) Lecture Notes - Section 10 (Periodic Task Scheduling).