Actually Seeing How RTOS Scheduling Works with LEDs

Apr 5, 2026 min

Reading about RTOS scheduling is one thing. Actually seeing tasks preempt each other is completely different. I spent an afternoon wiring up some LEDs to my STM32 to make these concepts visible, and it finally clicked for me.

The setup

Three LEDs connected to PA0, PA1, PA2 on a Blue Pill. Each LED represents a different priority task. Plus 220Ω resistors because I don’t want to fry anything.

The idea was simple: instead of reading serial output or trusting that scheduling works, I wanted to actually watch it happen. Turns out this was way more educational than I expected.

Demo 1: Basic priority-based scheduling

First test was straightforward. High priority task blinks LED1 fast. Low priority task blinks LED3 slow. What happens when high priority task hogs the CPU?

The high priority task sleeps for 2 seconds with vTaskDelay. During that time the low priority LED blinks rapidly. Then high priority wakes up and runs a busy loop for 1 second with no delays. Watch what happens to the low priority LED.

It stops. Completely. Not slower, not delayed. Stopped.

That’s preemption. The moment high priority task became ready, the scheduler yanked the CPU away from low priority. Even though low priority wanted to keep running, even though it was in the middle of its blink pattern. Doesn’t matter. Priority 3 beats priority 1.

When high priority goes back to sleep, low priority instantly resumes where it left off. This visual proof made the whole scheduler concept click for me way better than any timing diagram.

Demo 2: Mutex protecting a shared resource

Second demo shows why mutexes matter. Two tasks trying to use the same LED. Without a mutex they’d fight over it and you’d see garbage. With a mutex they take turns cleanly.

Task 1 does three quick blinks. Task 2 does five very fast blinks. They alternate. The patterns never mix.

void task1(void *params) {
  while(1) {
    xSemaphoreTake(ledMutex, portMAX_DELAY);
    
    // Three blinks - LED is ours
    for(int i = 0; i < 3; i++) {
      digitalWrite(LED1, HIGH);
      vTaskDelay(pdMS_TO_TICKS(100));
      digitalWrite(LED1, LOW);
      vTaskDelay(pdMS_TO_TICKS(100));
    }
    
    xSemaphoreGive(ledMutex);
    vTaskDelay(pdMS_TO_TICKS(800));
  }
}

What I noticed: you never see two blinks from task 2 interrupted by task 1’s slower blinks. The mutex ensures exclusive access. One task finishes its complete pattern before the other gets the LED.

This is exactly how you’d protect I2C or SPI hardware. Same principle, just with actual hardware instead of an LED.

Demo 3: Priority inversion and why it matters

This one surprised me. I’d read about priority inversion but didn’t really get why it was a problem until I saw it happen.

Setup: Low priority task grabs a mutex and holds it. Medium priority task is a CPU hog. High priority task tries to grab the same mutex.

Watch LED2 (medium priority). When the high priority task tries to grab the resource that low priority holds, LED2 stops blinking.

That’s priority inheritance working. Here’s what happens:

Low priority task has the mutex. Medium priority preempts it because priority 2 beats priority 1. High priority wakes up and tries to grab the mutex. It blocks waiting for low priority to finish.

At this point FreeRTOS detects the problem. High priority task is blocked by low priority task. So the scheduler temporarily boosts low priority’s priority to match high priority. Now low priority is effectively priority 3.

Suddenly low priority can preempt medium priority (priority 3 beats priority 2). Low priority finishes, releases the mutex, high priority grabs it and runs.

The visual proof is LED2 stopping. Without priority inheritance, LED2 would keep running because medium priority would keep preempting low priority, and high priority would be stuck forever. With priority inheritance, low priority gets boosted and finishes quickly.

Why this matters for real systems

These demos use LEDs but the problems are real. I’m working on a motor controller with multiple tasks sharing SPI and I2C buses. Without proper mutex protection I’d get corrupted transactions. Without understanding preemption I wouldn’t know why my display updates were getting delayed.

The priority inversion demo especially helped me understand why I need to be careful with lock durations. If a low priority task holds a mutex for too long, even priority inheritance can’t save you from poor system response.

What I learned building these

First, physical feedback is way better than serial output. Watching an LED stop mid-blink when preemption happens makes the concept stick.

Second, these demos exposed bugs I didn’t know I had. Initial version of the priority inversion demo didn’t show LED2 stopping because I forgot to make the high priority task sleep long enough for low priority to grab the mutex first. Timing matters.

Third, seeing is believing. I thought I understood scheduling from reading documentation. Actually watching tasks fight for CPU time and seeing the scheduler resolve it made me understand the mechanisms at a much deeper level.

The code

All three demos are pretty simple. Total hardware cost was maybe €2 for the LEDs and resistors. But the learning value was way higher than reading another tutorial.

For the basic scheduling demo, the key was using a busy loop instead of vTaskDelay in the high priority task. That keeps the CPU occupied so you can actually see preemption stopping the low priority task.

For the mutex demo, using distinct blink patterns (three slow vs five fast) made it obvious when each task had the resource.

For priority inversion, the trick was making sure low priority grabbed the mutex before high priority woke up. Added a vTaskDelay in high priority’s setup to let low priority run first.

Next steps

These demos helped me understand the theory. Now I’m applying this to my actual motor control project. Multiple tasks reading encoders, updating control loops, communicating over WiFi. All the scheduling and synchronization principles are the same, just with real hardware instead of LEDs.

If you’re learning RTOS concepts, build these demos. Seriously. The hardware is cheap, the code is simple, and watching tasks actually preempt each other teaches you more than any amount of reading.

~Ajit George