Previously in Part 6, we initialized the task scheduler and registered six tasks. All tasks are now marked as READY. The initialization phase is complete. Now we enter the main loop where actual task execution begins.
Where We Are in Code Execution
We’re at line 158 in main.c. All initialization functions have returned, all tasks are registered. The processor is about to execute:
UART_SendString("All tasks registered. Entering main loop...\r\n\r\n");
This message takes approximately 3.5ms to transmit at 115200 baud. Once transmitted, we reach:
/* Main loop */
while(1) {
The while(1) Loop – Why Infinite?
In embedded systems, while(1) creates an infinite loop. This is intentional:
- The microcontroller has no operating system to return to
- It must continuously monitor and execute tasks
- It will run until power is removed or reset occurs
Why not just use interrupts for everything? You might wonder why we need a main loop at all – couldn’t everything be interrupt-driven? While interrupts handle time-critical events, a main loop provides:
- Predictable task scheduling
- Easy debugging (you can trace execution flow)
- Lower stack usage (no nested interrupts)
- Simpler priority management
What happens to the return address? When main() is called, a return address is pushed onto the stack. Since we never return from main(), this address remains on the stack forever – wasting 4 bytes of RAM. This is acceptable because:
- It’s only 4 bytes out of 256KB
- The alternative (returning from main) would cause unpredictable behavior
- Most embedded systems accept this small waste
First Iteration – Time: ~154ms Since Boot
Let’s trace through the very first iteration of the main loop. The systick_counter is approximately 154ms (time taken for all initialization).
Where does 154ms come from? Breaking down initialization time:
- Vector table check: ~1ms
- SysTick init: <1ms
- UART init: ~1ms
- First UART messages: ~80ms (multiple lines)
- Power management init: ~2ms
- Task scheduler init: ~1ms
- Task registrations: ~50ms (6 tasks with debug messages)
- Final message: ~4ms
- Various small delays: ~14ms
- Total: ~154ms
This varies based on:
- Clock frequency
- UART baud rate
- Number of debug messages
- Flash wait states
Step 1: Call TaskScheduler_RunDispatcher()
/* Run the task dispatcher */
TaskScheduler_RunDispatcher();
Execution jumps to task_scheduler.c. Let’s follow what happens inside:
void TaskScheduler_RunDispatcher(void) {
uint32_t currentTime = systick_counter;
uint8_t highestPriorityReady = 0xFF;
uint8_t taskToRun = MAX_TASKS;
Current values:
currentTime= 154 (captured from systick_counter)highestPriorityReady= 0xFF (no priority found yet)taskToRun= 8 (MAX_TASKS, meaning no task selected)
Why capture systick_counter immediately? The systick_counter increments every millisecond via interrupt. By capturing it once at the start:
- All timing calculations use the same reference point
- Prevents inconsistencies if SysTick interrupt occurs during task checking
- More efficient than reading volatile variable multiple times
Why use 0xFF for “no priority”? Our priorities range from 0 (highest) to 3 (lowest). Using 0xFF (255) guarantees any real priority will be lower (better), since we check if (tasks[i].priority < highestPriorityReady).
Step 2: Search for Ready Tasks
The dispatcher now checks each task:
/* Find highest priority ready task */
for (uint8_t i = 0; i < MAX_TASKS; i++) {
if (tasks[i].state == TASK_STATE_READY && tasks[i].taskFunc != NULL) {
Why check both state AND taskFunc?
state == TASK_STATE_READY: Task is enabled and not suspendedtaskFunc != NULL: Slot actually contains a task
This double-check prevents crashes if a task slot was cleared but state wasn’t reset. It’s defensive programming – better to check twice than crash once.
Why linear search instead of a priority queue? With only 8 tasks maximum:
- Linear search takes microseconds
- Code is simple and predictable
- No dynamic memory allocation needed
- Easy to debug
- Priority queue overhead isn’t worth it for 8 items
Checking Task 0 (LED1_Green):
tasks[0].state= TASK_STATE_READY ✓tasks[0].taskFunc= Task_LED1_Blink (not NULL) ✓- Conditions met, continue checking timing:
/* Check if task is due to run */
uint32_t nextRun = tasks[i].lastExecuted + tasks[i].period_ms;
if (currentTime >= nextRun) {
Timing calculation:
tasks[0].lastExecuted= 0 (when registered)tasks[0].period_ms= 500nextRun= 0 + 500 = 500currentTime= 154- Is 154 >= 500? NO
What about systick_counter overflow? The systick_counter is uint32_t, so it overflows after 2^32 milliseconds (about 49.7 days). When it does:
- If lastExecuted = 4294967290 and period = 100
- nextRun = 4294967290 + 100 = 84 (overflow wraps around)
- currentTime = 50 (after overflow)
- Is 50 >= 84? NO – task waits correctly
The unsigned arithmetic handles overflow gracefully. However, tasks might miss one execution at the overflow boundary – acceptable for most applications.
Why >= instead of ==? Using >= ensures tasks run even if:
- The main loop was blocked slightly
- Multiple tasks prevented exact timing
- System woke from sleep mode late
Task 0 is not due yet. The loop continues checking tasks 1-5, but none are due at 154ms.
- Task 1: Next run at 1000ms
- Task 2: Next run at 2000ms
- Task 3: Next run at 3000ms
- Task 4: Next run at 5000ms
- Task 5: Next run at 5000ms
Step 3: No Task Execution
After checking all tasks:
/* Execute the selected task */
if (taskToRun < MAX_TASKS) {
Since taskToRun still equals 8 (MAX_TASKS), no task was selected. This entire block is skipped. The dispatcher returns to main().
Step 4: Check for UART Input
Back in main loop:
/* Handle user input */
if (UART_IsDataAvailable()) {
This calls:
bool UART_IsDataAvailable(void) {
return (USART3->SR & USART_SR_RXNE) != 0;
}
The function checks the RXNE bit in USART3’s status register. If no character was received, it returns false and the entire command processing block is skipped.
What if a character arrives during task execution? UART hardware:
- Receives bits independently
- Stores character in receive buffer
- Sets RXNE flag
- Waits for software to read it
The character remains buffered until next loop iteration. No data loss unless another character arrives before reading (overrun error).
How long can we wait before reading? At 115200 baud:
- One character every 87μs
- Main loop runs every ~1ms
- Safe for human typing (<10 chars/second)
- Would overflow with continuous data stream
Step 5: The 1ms Delay
/* Small delay to prevent CPU overload */
SysTick_Delay(1);
}
This delays for exactly 1ms. Now systick_counter = 155ms.
How does SysTick_Delay work?
void SysTick_Delay(uint32_t delay_ms) {
uint32_t start_time = systick_counter;
while ((systick_counter - start_time) < delay_ms) {
// Could add __WFI() here for power saving
}
}
The function:
- Records start time
- Loops until elapsed time >= delay_ms
- Returns when 1ms has passed
Is this a busy wait? Yes, currently it’s spinning in a loop. Better implementation:
while ((systick_counter - start_time) < delay_ms) {
__WFI(); // Wait For Interrupt - CPU sleeps
}
WFI puts CPU in sleep until next interrupt (like SysTick), saving power.
Step 6: Loop Repeats
The closing brace } sends execution back to while(1) {. The second iteration begins at time 155ms.
Iterations Continue Until Time 500ms
For the next 345 iterations (155ms to 499ms):
- TaskScheduler_RunDispatcher() checks all tasks
- No tasks are due
- UART is checked (likely no input)
- 1ms delay
- Loop repeats
Each iteration takes slightly more than 1ms due to execution time.
The First Task Execution – Time: 500ms
At iteration ~346, systick_counter reaches 500ms. Now things get interesting:
Inside RunDispatcher at 500ms
void TaskScheduler_RunDispatcher(void) {
uint32_t currentTime = systick_counter; // currentTime = 500
When checking Task 0:
uint32_t nextRun = tasks[i].lastExecuted + tasks[i].period_ms;
// nextRun = 0 + 500 = 500
if (currentTime >= nextRun) { // 500 >= 500? YES!
/* Task is due - check priority */
if (tasks[i].priority < highestPriorityReady) {
highestPriorityReady = tasks[i].priority;
taskToRun = i;
}
}
Why does the first execution happen at 500ms, not 0ms? When tasks are registered:
lastExecutedis set to current time (near 0)- First execution: lastExecuted + period = 0 + 500 = 500ms
- This creates a startup delay but ensures regular intervals
- Alternative: Set
lastExecuted = currentTime - periodfor immediate execution
Is the timing drift-free? Yes! Because:
- Next run based on lastExecuted, not actual run time
- If task runs at 501ms (1ms late)
- Next run still at 1000ms (500 + 500), not 1001ms
- Prevents accumulating timing errors
- Standard technique in schedulers
Priority check:
tasks[0].priority= 0 (TASK_PRIORITY_HIGH)highestPriorityReady= 0xFF- Is 0 < 0xFF? YES
- Set
highestPriorityReady= 0 - Set
taskToRun= 0
The loop continues checking other tasks, but none have better priority than 0.
What if another task also has priority 0? If multiple tasks share the highest priority:
- First one found (lowest array index) wins
- Others wait until next dispatcher call
- No round-robin within same priority
- Could be enhanced for fairness
Why continue checking after finding priority 0? Priority 0 is the best possible, so we could break early. But:
- Current code is simpler (no special cases)
- Loop overhead is minimal (6 iterations)
- Easier to add priority changes later
- Consistent behavior
Executing the Task
Now taskToRun = 0, so:
if (taskToRun < MAX_TASKS) { // 0 < 8? YES
Task_t* task = &tasks[taskToRun]; // task points to LED1_Green
/* Update state */
task->state = TASK_STATE_RUNNING;
The task state changes from READY to RUNNING.
Why change state to RUNNING?
- Prevents the same task from being selected again if interrupted
- Allows debugging to see which task is currently executing
- Could be used for watchdog monitoring in advanced systems
/* Measure execution time */
uint32_t startTime = systick_counter; // startTime = 500
/* Execute task function */
task->taskFunc(); // Calls Task_LED1_Blink()
What if the task crashes? If Task_LED1_Blink() causes a fault:
- The fault handler executes (HardFault_Handler)
- System typically resets or hangs
- Task state remains RUNNING, helping identify the culprit
- No protection without an RTOS or MPU (Memory Protection Unit)
Inside Task_LED1_Blink()
The processor jumps to main.c where Task_LED1_Blink is defined:
void Task_LED1_Blink(void) {
static uint32_t counter = 0;
counter++;
/* Toggle green LED on PB0 */
GPIOB->ODR ^= (1 << 0);
/* Every 10 executions, print status */
if (counter % 10 == 0) {
UART_SendString("[LED1] Green LED task running\r\n");
}
}
What happens:
- Static counter increments to 1
- PB0 toggles (LED turns ON if it was OFF)
- Counter is 1, not divisible by 10, so no UART message
- Function returns
Why use static counter? The static keyword means:
- Variable persists between function calls
- Initialized only once (to 0)
- Not stored on stack (won’t be lost)
- Each task can track its own execution count
Why XOR (^=) for toggling? GPIOB->ODR ^= (1 << 0) flips bit 0:
- If bit was 0, becomes 1 (LED ON)
- If bit was 1, becomes 0 (LED OFF)
- Single operation, no need to read current state
Why print only every 10 executions?
- Reduces UART traffic
- Status every 5 seconds (10 × 500ms) is reasonable
- Prevents terminal flooding
- Each UART message takes ~3ms to transmit
Back in RunDispatcher
/* Calculate execution time */
uint32_t executionTime = systick_counter - startTime;
// If task took 0.1ms, executionTime = 0 (due to 1ms resolution)
Why might execution time be 0? SysTick increments every 1ms. If a task executes in less than 1ms:
- startTime = 500
- Task executes in 0.1ms
- systick_counter still = 500 when task completes
- executionTime = 500 – 500 = 0
This is a limitation of 1ms resolution. For microsecond precision, you’d need:
- A hardware timer running at MHz speeds
- More complex time measurement
- Higher overhead
Performance metrics are updated:
/* Update max execution time */
if (executionTime > task->maxExecutionTime) {
task->maxExecutionTime = executionTime;
}
/* Calculate jitter */
uint32_t scheduledTime = task->lastExecuted + task->period_ms;
// scheduledTime = 0 + 500 = 500
uint32_t jitter = (currentTime > scheduledTime) ?
(currentTime - scheduledTime) : 0;
// jitter = 500 > 500 ? 0 : 0 = 0ms (perfect timing!)
What is jitter and why track it? Jitter measures timing accuracy:
- 0ms jitter: Task ran exactly when scheduled
- 5ms jitter: Task ran 5ms late
- High jitter indicates:
- System overload
- Long-running tasks blocking scheduler
- Too many high-priority tasks
Why only positive jitter? The conditional (currentTime > scheduledTime) ? ... : 0 ensures jitter is never negative. Tasks can’t run early in our scheduler – they can only be delayed.
Finally:
/* Update last executed time */
task->lastExecuted = currentTime; // Now = 500
/* Update state back to ready */
task->state = TASK_STATE_READY;
Task 0’s next execution will be at 500 + 500 = 1000ms.
Complete Timeline: First 2 Seconds
Let’s trace exactly when each task executes:
Time(ms) | Loop# | Dispatcher Action | LED States
---------|-------|-------------------------------------|-------------
0-499 | 1-345 | Checking tasks, none due | All OFF
500 | 346 | Execute Task 0 (LED1_Green) | Green ON
501-999 | 347- | Checking tasks, none due | Green ON
1000 | ~690 | Execute Task 0 (LED1_Green) | Green OFF
1000 | ~690 | Execute Task 1 (LED2_Blue) | Blue ON
1001-1499| 691- | Checking tasks, none due | Blue ON
1500 | ~1035 | Execute Task 0 (LED1_Green) | Green ON
1501-1999| 1036- | Checking tasks, none due | Green ON, Blue ON
2000 | ~1380 | Execute Task 0 (LED1_Green) | Green OFF
2000 | ~1380 | Execute Task 1 (LED2_Blue) | Blue OFF
2000 | ~1380 | Execute Task 2 (LED3_Red) | Red ON
Why These Task Periods? Understanding the Gaps
You might wonder why there are long periods (like 501-999ms) where no tasks execute. This is intentional:
The Chosen Task Periods
- LED1_Green: 500ms (2Hz) – Fast blinking, easy to see
- LED2_Blue: 1000ms (1Hz) – Standard 1-second blink
- LED3_Red: 2000ms (0.5Hz) – Slow blink
- UART_Status: 3000ms – Status updates every 3 seconds
- System_Monitor: 5000ms – Detailed info every 5 seconds
- Power_Saving: 5000ms – Power management demonstration
Why Not Execute Tasks More Frequently?
1. Visual Demonstration The LED periods (500ms, 1000ms, 2000ms) create distinct blinking patterns:
- Green blinks fastest (easily distinguishable)
- Blue blinks at 1Hz (common reference rate)
- Red blinks slowest (clearly different from others)
If all LEDs blinked at 100ms, they’d appear almost continuously on and hard to distinguish.
2. UART Readability Status messages every 3-5 seconds are readable. If they printed every 100ms:
- Terminal would flood with messages
- Impossible to read anything
- UART bandwidth wasted
3. Real-World Simulation These periods simulate real embedded applications:
- Temperature sensor: Read every 5 seconds
- Heartbeat LED: Blink every second
- Status report: Every few seconds
- Power saving: Periodic deep sleep
What Happens During “Idle” Periods?
During 501-999ms when no tasks execute:
- Main loop still runs every millisecond
- Dispatcher checks all tasks (takes microseconds)
- UART monitored for user commands
- System ready to respond immediately
This is actually efficient:
- Quick checks use minimal power
- System remains responsive
- No wasted CPU cycles on unnecessary work
Alternative Approach: Dense Task Scheduling
If we wanted constant activity:
LED1: Every 100ms
LED2: Every 100ms
LED3: Every 100ms
Problems:
- All LEDs appear “always on”
- No visual distinction
- Higher power consumption
- Not realistic for most applications
The Beauty of Sparse Scheduling
Our current scheduling:
- Demonstrates different rates clearly
- Leaves CPU mostly idle (good for power)
- Allows time for commands between tasks
- Simulates real-world patterns
- Easy to observe and debug
The “empty” periods aren’t wasted – they represent the reality that most embedded systems spend most of their time waiting for something to happen.
What If a Task Misbehaves?
Scenario 1: Task takes too long (blocks for 100ms)
void BadTask(void) {
SysTick_Delay(100); // Blocks for 100ms!
}
Consequences:
- All other tasks delayed by 100ms
- System appears to “freeze”
- Command response delayed
- High jitter for all tasks
Scenario 2: Task enters infinite loop
void ReallyBadTask(void) {
while(1); // Disaster!
}
Consequences:
- System completely hangs
- Watchdog timer would reset system (if enabled)
- No recovery without hardware intervention
Scenario 3: Task corrupts memory
void DangerousTask(void) {
uint32_t* ptr = (uint32_t*)0x20000000;
*ptr = 0xDEADBEEF; // Writing to random RAM!
}
Consequences:
- Unpredictable behavior
- Possible HardFault
- System crash
- No protection without MPU
This is why cooperative scheduling requires:
- Well-behaved tasks
- Short execution times
- No blocking operations
- Careful testing
- Watchdog timer as backup
System Robustness and Error Handling
What happens if dispatcher is called recursively? It can’t happen because:
- No task can call RunDispatcher
- Interrupts don’t call dispatcher
- Main loop is single-threaded
What if systick_counter stops incrementing? If SysTick interrupt fails:
- Tasks never become due
- System appears frozen but main loop continues
- Commands still work
- Would need watchdog reset
What if a task pointer becomes NULL? The dispatcher checks taskFunc != NULL:
- Prevents calling NULL pointer
- Task slot is skipped
- System continues normally
What if stack overflows? Stack overflow causes:
- Write to invalid memory
- Probable HardFault
- System reset (if fault handler resets)
- No recovery without stack monitoring
Error Detection Methods:
- Heartbeat LED: If stops blinking, system hung
- Watchdog timer: Resets system if not fed
- Stack canary: Detects stack overflow
- Assert checks: Validate assumptions
- Status messages: Show system health
Priority Handling at Time 1000ms
At exactly 1000ms, two tasks are ready:
- Task 0 (LED1_Green): Priority 0 (HIGH)
- Task 1 (LED2_Blue): Priority 1 (NORMAL)
The dispatcher loop finds both are due, but Task 0 has better priority (0 < 1), so:
- Task 0 executes first (toggles green LED)
- Dispatcher returns to main loop
- Next iteration immediately calls dispatcher again
- Now only Task 1 is due (Task 0 won’t run until 1500ms)
- Task 1 executes (toggles blue LED)
Why doesn’t the dispatcher run ALL ready tasks in one call? Our scheduler runs ONE task per dispatcher call because:
- Keeps each loop iteration time bounded
- Allows checking for commands between tasks
- Prevents high-priority tasks from starving command processing
- Simpler to debug and understand
What if two tasks have the same priority? When tasks have equal priority (e.g., both NORMAL):
- First one found in the array runs first
- This gives lower task IDs slight advantage
- For true fairness, you’d need round-robin within priority levels
Could priority inversion occur? Priority inversion (low-priority task blocking high-priority) can’t happen here because:
- No task can block another (no shared resources with locks)
- Tasks run to completion (cooperative)
- No preemption within tasks
Command Processing in Detail
When a user presses a key, here’s the exact flow:
Iteration with UART Input
if (UART_IsDataAvailable()) { // Returns true
char cmd = UART_ReceiveChar(); // Reads character from USART3->DR
UART_SendChar(cmd); /* Echo back */
Why poll UART instead of using interrupts? Polling UART in the main loop:
- Simpler code (no ISR complexity)
- No interrupt priority conflicts
- Commands processed between tasks (predictable)
- Good enough for human typing speeds
Interrupt-driven UART would be better for:
- High-speed data transfer
- Protocol implementations
- When every byte counts
Why echo characters back? Echoing provides user feedback:
- Confirms character was received
- Shows system is responsive
- Standard terminal behavior
- Helps with debugging connection issues
If user pressed ‘1’:
switch(cmd) {
case '1':
UART_SendString("\r\nEntering Sleep mode - press any key to wake up...\r\n");
SysTick_Delay(10); // Let UART finish transmitting
PowerMgmt_EnterLowPowerMode(POWER_MODE_SLEEP);
UART_SendString("Woke up from Sleep mode!\r\n");
break;
Why delay before sleeping? The 10ms delay ensures:
- All characters transmitted (52 chars × 87μs = 4.5ms)
- UART hardware FIFO empties
- User sees complete message
- No cut-off mid-transmission
What happens to tasks during sleep? In SLEEP mode:
- CPU stops but peripherals continue
- SysTick keeps counting
- Tasks appear to “pause”
- Wake-up continues where left off
- Some task executions might be delayed
Why the 1ms Delay is Critical
Without SysTick_Delay(1):
- The while(1) loop would run at current CPU speed (16MHz)
- Even at 16MHz, that’s millions of iterations per second
- TaskScheduler_RunDispatcher() would be called millions of times
- Each call checks 6 tasks, comparing timestamps
- CPU utilization would be 100%
- Power consumption would be ~15mA (continuous operation at 16MHz)
With the 1ms delay:
- Loop runs at approximately 1000Hz
- Tasks are checked once per millisecond
- Sufficient for 1ms timing resolution
- CPU is idle most of the time
- Power consumption reduced significantly
Why exactly 1ms? The 1ms delay is optimal because:
- Matches SysTick resolution (1ms)
- No point checking faster than timing granularity
- Fast enough for human response (< 10ms feels instant)
- Slow enough to save power
- Standard in many embedded systems
What’s the actual loop frequency? Not exactly 1000Hz because:
- Dispatcher execution takes time (~10-100μs)
- Task execution adds time
- Command processing adds time
- Real frequency: ~900-990Hz depending on load
Could we use 2ms or 5ms delay? Yes, but trade-offs:
- 2ms delay: Tasks might run 1ms late
- 5ms delay: Tasks might run 4ms late
- 10ms delay: Noticeable command response lag
- 0.5ms delay: Unnecessary overhead
How much power does the delay save? Rough calculation:
- Active current: 15mA
- Sleep current during WFI: ~5mA
- If loop takes 0.1ms active, 0.9ms sleeping
- Average: (0.1 × 15mA + 0.9 × 5mA) = 6mA
- Savings: 60% power reduction!
Steady State Operation
After initial task synchronization, the system settles into a pattern:
Every millisecond:
- Dispatcher checks all 6 tasks
- Executes any that are due (based on priority)
- Checks for UART input
- Delays 1ms
Task execution pattern:
- LED1: Every 500ms (2Hz)
- LED2: Every 1000ms (1Hz)
- LED3: Every 2000ms (0.5Hz)
- UART Status: Every 3000ms
- System Monitor: Every 5000ms
- Power Saving: Every 5000ms (enters stop mode every 50s)
Why do some iterations take longer? When multiple tasks run:
- 1000ms: LED1 + LED2 execute
- 2000ms: LED1 + LED2 + LED3 execute
- 5000ms: LED1 + System Monitor + Power Saving
Each task adds execution time:
- LED task: ~50μs
- UART message task: ~3ms
- System monitor: ~10ms
- Power saving: ~5ms
So iteration at 5000ms might take 15ms total!
Does this affect timing accuracy? Yes, slightly:
- Tasks scheduled for 5001ms might run at 5015ms
- This is why we track jitter
- Acceptable for most applications
- Use hardware timers for microsecond precision
The Complete Loop – One Full Iteration
Here’s exactly what happens in one complete loop iteration:
1. while(1) condition check (always true)
2. TaskScheduler_RunDispatcher() called
a. Capture current time
b. Check all 6 tasks for readiness
c. Find highest priority ready task
d. Execute it (if any)
e. Update performance metrics
f. Return to main
3. UART_IsDataAvailable() called
a. Check RXNE flag
b. If set, read character and process command
c. If not set, skip to next step
4. SysTick_Delay(1) called
a. Wait for systick_counter to increment
b. Return after 1ms
5. Loop closing brace - jump back to step 1
This cycle repeats forever, providing a stable, predictable execution environment for all tasks.
Memory and Performance Analysis
Stack Usage:
- Main loop: ~32 bytes (local variables)
- RunDispatcher: ~16 bytes
- Each task: varies (LED tasks ~8 bytes)
- Worst case: ~100 bytes total
- Available: 128KB RAM (plenty of margin)
Code Size:
- Main loop: ~200 bytes
- Dispatcher: ~300 bytes
- All tasks: ~500 bytes
- Total scheduler overhead: ~1KB
- Available: 2MB Flash (negligible usage)
Timing Overhead:
- Dispatcher (no tasks ready): ~5μs
- Dispatcher (one task ready): ~20μs
- UART check: ~2μs
- Total overhead per loop: <30μs
- Overhead percentage: 3% (30μs / 1000μs)
Optimization Possibilities:
- Sleep during delay: Use WFI instruction
- Tickless idle: Stop SysTick when no tasks due
- Binary heap: For many tasks (>20)
- DMA for UART: Non-blocking transfers
- Hardware timers: Per-task timing
But for 6 tasks, our simple approach is optimal – easy to understand, debug, and maintain.
Next: Part 8 – Command Interface Deep Dive →
This is Part 7 of the STM32 IoT Framework series.
Pingback: Part 6: The Ultimate Guide to Effortless MCU Scheduling - Learn By Building
Pingback: Part 8: How to Build Amazing UART Command Control - Learn By Building