Part 6: The Ultimate Guide to Effortless MCU Scheduling

Previously in Part 5, we initialized the power management system, giving our STM32F429ZI the ability to sleep and wake up efficiently. Now we’re ready to initialize the task scheduler – the system that will orchestrate multiple tasks running at different rates.

Where We Are in the Code

PowerMgmt_Configure() has just returned to main(), and execution continues to the next line:

/* Initialize the task scheduler */
TaskScheduler_Init();

This single function call sets up the infrastructure for managing multiple concurrent tasks without using a full Real-Time Operating System (RTOS).

What is a Task Scheduler?

Before diving into the code, let’s understand what we’re building:

A task scheduler is a system that manages the execution of multiple functions (tasks) at specified intervals. Think of it as a conductor orchestrating musicians – each plays their part at the right time.

Our scheduler features:

  • Up to 8 concurrent tasks
  • Each task has its own execution period (e.g., every 500ms)
  • Priority-based execution (HIGH, NORMAL, LOW, IDLE)
  • Performance monitoring (execution time, jitter)
  • Dynamic task management (start, stop, suspend, resume)

Why not use an RTOS? An RTOS adds complexity and memory overhead. For many embedded applications, a simple cooperative scheduler is sufficient and more predictable.

The Journey Begins: Jumping to TaskScheduler_Init

When the processor encounters TaskScheduler_Init(), it jumps from main.c to the TaskScheduler_Init function in task_scheduler.c:

void TaskScheduler_Init(void) {
    /* Clear all task entries */
    memset(tasks, 0, sizeof(tasks));

We’re now inside the task scheduler initialization function.

Understanding the Task Data Structure

At the top of task_scheduler.c, there’s a global array:

/* Task array - supports MAX_TASKS concurrent tasks */
static Task_t tasks[MAX_TASKS];

Where MAX_TASKS is defined as 8 in task_scheduler.h. Each task is represented by this structure:

typedef struct {
    void (*taskFunc)(void);        /* Task function pointer */
    uint32_t period_ms;           /* Task period in milliseconds */
    uint32_t lastExecuted;        /* Last execution time */
    uint32_t maxExecutionTime;    /* Max execution time measured */
    uint32_t maxJitter;           /* Maximum scheduling jitter */
    TaskState_t state;            /* Current task state */
    uint8_t priority;             /* Task priority (0 = highest) */
    uint8_t taskId;               /* Unique task identifier */
    char name[16];                /* Task name for debugging */
} Task_t;

Let’s understand each field:

  • taskFunc: A pointer to the function that will be called periodically
  • period_ms: How often to run this task (e.g., 500 = every 500ms)
  • lastExecuted: Timestamp of last execution (uses systick_counter)
  • maxExecutionTime: Longest time this task has taken to execute
  • maxJitter: Maximum deviation from scheduled execution time
  • state: STOPPED, READY, RUNNING, or SUSPENDED
  • priority: 0-3, where 0 is highest priority
  • taskId: Unique identifier assigned when registered
  • name: Human-readable name for debug messages

Step 1: Clearing the Task Array

/* Clear all task entries */
memset(tasks, 0, sizeof(tasks));

This line clears the entire tasks array. Let’s calculate the size:

  • Each Task_t is approximately 40 bytes
  • MAX_TASKS = 8
  • Total: 8 × 40 = 320 bytes cleared

Memory visualization:

tasks array in RAM:
┌─────────────┐
│  Task 0     │ ← All fields set to 0
├─────────────┤
│  Task 1     │ ← All fields set to 0
├─────────────┤
│  Task 2     │ ← All fields set to 0
├─────────────┤
│  ...        │
├─────────────┤
│  Task 7     │ ← All fields set to 0
└─────────────┘

After memset():

  • All function pointers are NULL
  • All periods are 0
  • All states are TASK_STATE_STOPPED (which equals 0)
  • All names are empty strings

Step 2: Reset Global Counters

/* Reset counters */
nextTaskId = 0;
registeredTaskCount = 0;

These are static variables at the top of task_scheduler.c:

/* Task counter for unique IDs */
static uint8_t nextTaskId = 0;

/* Registered task count */
static uint8_t registeredTaskCount = 0;

What these do:

  • nextTaskId: Increments each time a task is registered, ensuring unique IDs
  • registeredTaskCount: Tracks how many tasks are currently registered

Step 3: Debug Message

/* Log initialization */
UART_SendString("\r\n[SCHEDULER] Task scheduler initialized\r\n");

This sends a confirmation message via UART. The message takes about 3.5ms to transmit at 115200 baud.

Step 4: Return to main()

}

TaskScheduler_Init() completes and returns to main(). The scheduler is now ready to accept task registrations.

What Just Happened

While TaskScheduler_Init() seems simple, it has prepared critical infrastructure:

1. Memory Allocation

Unlike dynamic schedulers that use malloc(), our scheduler uses static allocation:

  • Predictable: Always uses exactly 320 bytes
  • Safe: No memory fragmentation or allocation failures
  • Fast: No heap management overhead

2. Clean State

By clearing everything, we ensure:

  • No garbage data in task structures
  • All tasks start in STOPPED state
  • Function pointers are NULL (safe to check)

3. Ready for Registration

The scheduler can now accept up to 8 task registrations. Each task will get:

  • A unique ID (starting from 0)
  • A slot in the tasks array
  • Performance monitoring

Back in main() – Registering Tasks

Right after TaskScheduler_Init() returns, we’re back in main() with an empty scheduler ready to accept tasks. Now main() will register several tasks. Let’s understand this process step by step.

What Does “Registering a Task” Mean?

Think of the scheduler as a manager with 8 empty desks. Registering a task means:

  1. Assigning a function to one of these desks
  2. Telling the manager how often to call this function
  3. Setting the priority (who goes first if multiple are ready)
  4. Giving it a name for debugging

The First Task Registration

Let’s look at the very first task registration in main():

/* Register demo tasks with different priorities and periods */
uint8_t taskId;

First, main() declares a variable taskId to store the ID that will be assigned to each task.

Now the actual registration:

/* High priority LED task - Green LED */
if (TaskScheduler_RegisterTask(Task_LED1_Blink, 500, TASK_PRIORITY_HIGH, "LED1_Green", &taskId) == TASK_OK) {
    TaskScheduler_StartTask(taskId);
}

Let’s break down the parameters:

  1. Task_LED1_Blink – The function to call (blinks green LED)
  2. 500 – Call it every 500 milliseconds
  3. TASK_PRIORITY_HIGH – This is priority 0 (highest)
  4. "LED1_Green" – A name for debug messages
  5. &taskId – Where to store the assigned ID

Step Inside TaskScheduler_RegisterTask

When this function is called, execution jumps to task_scheduler.c. Let’s follow what happens step by step.

Step 1: Check If There’s Room

TaskError_t TaskScheduler_RegisterTask(void (*taskFunc)(void),
                                      uint32_t period_ms,
                                      uint8_t priority,
                                      const char* name,
                                      uint8_t* taskId) {
    /* Check if we have space for more tasks */
    if (registeredTaskCount >= MAX_TASKS) {
        return TASK_ERROR_FULL;
    }

First check: Do we have space? We can only handle 8 tasks total.

  • registeredTaskCount is currently 0 (no tasks yet)
  • MAX_TASKS is 8
  • So we have space!

Step 2: Validate the Function Pointer

    /* Validate function pointer */
    if (taskFunc == NULL) {
        return TASK_ERROR_INVALID_PARAM;
    }

Second check: Is the function pointer valid?

  • We’re passing Task_LED1_Blink, which is a real function
  • It’s not NULL, so we continue

Step 3: Find an Empty Slot

    /* Find empty slot */
    uint8_t slot = MAX_TASKS;
    for (uint8_t i = 0; i < MAX_TASKS; i++) {
        if (tasks[i].state == TASK_STATE_STOPPED && tasks[i].taskFunc == NULL) {
            slot = i;
            break;
        }
    }

Now we search for an empty desk in our array of 8 tasks:

  • Start at task 0
  • Is it empty? (state == STOPPED && taskFunc == NULL)
  • Yes! So slot = 0
  • Break out of the loop

Step 4: Fill in the Task Information

Now we have slot 0 available. Let’s fill it with our LED task:

    /* Initialize task */
    tasks[slot].taskFunc = taskFunc;      // tasks[0].taskFunc = Task_LED1_Blink

This stores the function pointer. Now tasks[0] knows which function to call.

    tasks[slot].period_ms = period_ms;    // tasks[0].period_ms = 500

This sets how often to call it – every 500ms.

    tasks[slot].lastExecuted = systick_counter;  // tasks[0].lastExecuted = current time

This records when the task was registered. If systick_counter is currently 50, then lastExecuted = 50.

    tasks[slot].state = TASK_STATE_READY;       // tasks[0].state = READY

The task is ready to run (but not running yet).

    tasks[slot].priority = priority;             // tasks[0].priority = 0 (HIGH)

Priority 0 is the highest priority.

    tasks[slot].taskId = nextTaskId++;           // tasks[0].taskId = 0, then nextTaskId becomes 1

Assign ID 0 to this task, then increment nextTaskId for the next task.

Step 5: Copy the Name

    /* Set name if provided */
    if (name != NULL) {
        strncpy(tasks[slot].name, name, sizeof(tasks[slot].name) - 1);
        tasks[slot].name[sizeof(tasks[slot].name) - 1] = '\0';
    }

This code safely copies the task name. Let me explain why it’s written this way:

The Problem with Regular String Copy

In C, strings are just arrays of characters ending with ‘\0’ (null terminator). If we used regular strcpy():

strcpy(tasks[slot].name, name);  // DANGEROUS!

What if someone passed a 50-character name but our buffer is only 16 characters? It would write past the end of our array, corrupting memory!

Understanding the Task Name Buffer

In our Task_t structure:

char name[16];  // Can hold 15 characters + 1 null terminator

Breaking Down the Safe Copy

Let’s trace through with our example name “LED1_Green”:

  1. Check if name was provided: if (name != NULL) { We’re passing “LED1_Green”, which is not NULL, so we continue.
  2. Copy with size limit: strncpy(tasks[slot].name, name, sizeof(tasks[slot].name) - 1);
    • tasks[slot].name = destination (where to copy TO)
    • name = source (what we’re copying FROM) = “LED1_Green”
    • sizeof(tasks[slot].name) = 16 (the size of our array)
    • sizeof(tasks[slot].name) - 1 = 15 (leave room for ‘\0’)
    strncpy will copy at most 15 characters from “LED1_Green”.
  3. Ensure null termination:tasks[slot].name[sizeof(tasks[slot].name) - 1] = '\0';
    • sizeof(tasks[slot].name) - 1 = 15 (the last position in our array)
    • This line forces position 15 to be ‘\0’

Visual Example

Let’s see what happens in memory:

Case 1: Normal name “LED1_Green” (10 characters)

Position: 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15
Content: 'L''E''D''1''_''G''r''e''e''n''\0' ?  ?  ?  ? '\0'
                                         ↑                ↑
                                         |                |
                            Original null terminator      |
                                                   We force this to '\0'

Case 2: Long name “SuperLongTaskNameThatWontFit” (28 characters)

Without protection - BUFFER OVERFLOW:
Position: 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18...
Content: 'S''u''p''e''r''L''o''n''g''T''a''s''k''N''a''m''e''T''h'...
                                                            ↑
                                                            |
                                                Writing past our array!

With our protection:
Position: 0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15
Content: 'S''u''p''e''r''L''o''n''g''T''a''s''k''N''a''\0'
                                                         ↑
                                                         |
                                                   Forced null terminator

The name gets truncated to “SuperLongTaskNa” but our program doesn’t crash!

Why Two Steps?

You might wonder why we need both strncpy AND manually setting ‘\0’:

  • strncpy copies up to 15 characters
  • If the source string is shorter than 15, strncpy adds ‘\0’ automatically
  • If the source string is 15 or longer, strncpy does NOT add ‘\0’
  • So we always force position 15 to be ‘\0’ just to be safe

In Our Example

For “LED1_Green”:

  1. strncpy copies all 10 characters plus ‘\0’
  2. We force position 15 to ‘\0’ (redundant but safe)
  3. Result: tasks[0].name contains “LED1_Green\0”

This defensive programming prevents buffer overflows that could crash our system or cause unpredictable behavior.

Step 6: Return the Task ID

    /* Return task ID if requested */
    if (taskId != NULL) {
        *taskId = tasks[slot].taskId;    // Store 0 in the taskId variable in main()
    }

This stores the assigned ID (0) back in main()’s taskId variable.

Step 7: Update Counter and Log

    registeredTaskCount++;                // Now equals 1
    
    /* Log registration */
    char debug_msg[100];
    snprintf(debug_msg, sizeof(debug_msg),
             "[SCHEDULER] Registered task '%s' (ID: %d, Period: %lums)\r\n",
             tasks[slot].name, tasks[slot].taskId, tasks[slot].period_ms);
    UART_SendString(debug_msg);
    
    return TASK_OK;
}

The debug message will show:

[SCHEDULER] Registered task 'LED1_Green' (ID: 0, Period: 500ms)

Back in main() – Starting the Task

After registration succeeds, main() immediately starts the task:

if (TaskScheduler_RegisterTask(...) == TASK_OK) {
    TaskScheduler_StartTask(taskId);    // taskId is 0
}

Wait, didn’t we already set state to READY? Yes, but StartTask does additional things:

  • Resets the lastExecuted time to “now”
  • Sends a debug message
  • Could do other initialization in the future

What’s Our Task Array Like Now?

After registering the first task:

Task Array:
[0] LED1_Green - Function: Task_LED1_Blink, Period: 500ms, Priority: HIGH, State: READY
[1] Empty
[2] Empty
[3] Empty
[4] Empty
[5] Empty
[6] Empty
[7] Empty

Can We Add Tasks Later?

YES! Tasks can be registered at any time:

  • During initialization (what we’re doing now)
  • During runtime based on conditions
  • In response to user commands
  • From within other tasks

The only limit is the total number (8).

The Complete Registration Flow

Let’s see the complete flow for all 6 tasks in main():

// Task 1: Green LED every 500ms (HIGH priority)
TaskScheduler_RegisterTask(Task_LED1_Blink, 500, TASK_PRIORITY_HIGH, "LED1_Green", &taskId);
TaskScheduler_StartTask(taskId);  // ID = 0

// Task 2: Blue LED every 1000ms (NORMAL priority)
TaskScheduler_RegisterTask(Task_LED2_Blink, 1000, TASK_PRIORITY_NORMAL, "LED2_Blue", &taskId);
TaskScheduler_StartTask(taskId);  // ID = 1

// Task 3: Red LED every 2000ms (NORMAL priority)
TaskScheduler_RegisterTask(Task_LED3_Blink, 2000, TASK_PRIORITY_NORMAL, "LED3_Red", &taskId);
TaskScheduler_StartTask(taskId);  // ID = 2

// Task 4: UART status every 3000ms (LOW priority)
TaskScheduler_RegisterTask(Task_UARTStatus, 3000, TASK_PRIORITY_LOW, "UART_Status", &taskId);
TaskScheduler_StartTask(taskId);  // ID = 3

// Task 5: System monitor every 5000ms (IDLE priority)
TaskScheduler_RegisterTask(Task_SystemMonitor, 5000, TASK_PRIORITY_IDLE, "System_Monitor", &taskId);
TaskScheduler_StartTask(taskId);  // ID = 4

// Task 6: Power saving every 5000ms (IDLE priority)
TaskScheduler_RegisterTask(Task_PowerSaving, 5000, TASK_PRIORITY_IDLE, "Power_Saving", &taskId);
TaskScheduler_StartTask(taskId);  // ID = 5

Each registration:

  1. Finds the next empty slot
  2. Fills in all the task information
  3. Assigns the next ID (0, 1, 2, 3, 4, 5)
  4. Starts the task
  5. Sends a debug message

Visual: Task Array After All Registrations

Slot | ID | Name           | Function          | Period | Priority | State
-----|----|--------------  |-------------------|--------|----------|-------
  0  | 0  | LED1_Green     | Task_LED1_Blink   | 500ms  | HIGH     | READY
  1  | 1  | LED2_Blue      | Task_LED2_Blink   | 1000ms | NORMAL   | READY
  2  | 2  | LED3_Red       | Task_LED3_Blink   | 2000ms | NORMAL   | READY
  3  | 3  | UART_Status    | Task_UARTStatus   | 3000ms | LOW      | READY
  4  | 4  | System_Monitor | Task_SystemMonitor| 5000ms | IDLE     | READY
  5  | 5  | Power_Saving   | Task_PowerSaving  | 5000ms | IDLE     | READY
  6  | -  | [empty]        | NULL              | -      | -        | STOPPED
  7  | -  | [empty]        | NULL              | -      | -        | STOPPED

Other Functions in task_scheduler.c – When Are They Used?

Let’s map out ALL the functions in task_scheduler.c and understand when each one is used in our program flow.

Functions Used During Initialization (Where We Are Now)

Already used:

  1. TaskScheduler_Init() – Called once to set up empty scheduler
  2. TaskScheduler_RegisterTask() – Called 6 times to register each task
  3. TaskScheduler_StartTask() – Called 6 times to mark tasks as ready

Functions That Will Be Used in the Main Loop (Coming Next)

Not yet called, but will be soon:

  1. TaskScheduler_RunDispatcher() – The heart of the scheduler! // In main loop:while(1) { TaskScheduler_RunDispatcher(); // This runs continuously // ... handle commands ...} This function:
    • Checks which tasks are due to run
    • Selects highest priority ready task
    • Executes it
    • Updates performance metrics

Utility Functions Used by the Dispatcher

Called internally by RunDispatcher:

  1. DetermineWakeupSource() – Static helper function
    • Used after waking from low-power mode
    • Checks what caused the wake-up (RTC or pin)
  2. Performance tracking (inline code in RunDispatcher):
    • Measures execution time
    • Calculates scheduling jitter
    • Updates max values

Management Functions Used During Runtime

Called based on user commands or system events:

  1. TaskScheduler_StopTask(uint8_t taskId) // When user presses 's' in command menu: TaskScheduler_StopTask(taskId); // Stops a running task
  2. TaskScheduler_SuspendTask(uint8_t taskId) // Temporarily pause a task (can be resumed) TaskScheduler_SuspendTask(2); // Suspend the red LED task
  3. TaskScheduler_ResumeTask(uint8_t taskId) // Resume a suspended task TaskScheduler_ResumeTask(2); // Resume the red LED task
  4. TaskScheduler_GetTaskInfo(uint8_t taskId, TaskInfo_t* info) // Get information about a task TaskInfo_t info; TaskScheduler_GetTaskInfo(0, &info); printf("Task %s has run %lu times\n", info.name, info.executionCount);
  5. TaskScheduler_GetStatus(TaskSchedulerStatus_t* status) // When user presses 'd' for scheduler status: TaskSchedulerStatus_t status; TaskScheduler_GetStatus(&status); // Prints all task information

Functions That May Never Be Called

Depending on program flow:

  1. TaskScheduler_ResetStats(uint8_t taskId)
    • Resets performance statistics for a task
    • Only called if user chooses to reset stats
  2. TaskScheduler_UnregisterTask(uint8_t taskId)
    • Removes a task completely from scheduler
    • Our demo doesn’t unregister tasks, but it’s available

Visual: Function Call Timeline

Program Start
    |
    ├─> TaskScheduler_Init() ──────────────── Called once
    |
    ├─> TaskScheduler_RegisterTask() ───┐
    ├─> TaskScheduler_StartTask()       ├──── Called 6 times
    |   (repeated 6 times)              ┘     during init
    |
    ├─> Main Loop Starts
    |   |
    |   ├─> TaskScheduler_RunDispatcher() ─── Called continuously
    |   |   ├─> Checks all tasks
    |   |   ├─> Executes ready tasks
    |   |   └─> Updates metrics
    |   |
    |   ├─> User presses 'd'
    |   |   └─> TaskScheduler_GetStatus() ─── Shows all tasks
    |   |
    |   ├─> User presses 's'
    |   |   └─> TaskScheduler_StopTask() ──── Stops a task
    |   |
    |   └─> (continues forever)
    |
    └─> Program never ends (infinite loop)

Why Have Functions We Don’t Use?

Having these additional functions makes the scheduler:

  1. Flexible – Can adapt to different use cases
  2. Debuggable – Can inspect task states during development
  3. Maintainable – Can add/remove tasks dynamically
  4. Reusable – Same scheduler can work in different projects

Summary: Current Execution State

Right now, we’ve:

  • Initialized the scheduler (empty)
  • Registered 6 tasks
  • Started all 6 tasks (marked as READY)

But we haven’t:

  • Actually executed any tasks yet
  • Used any management functions
  • Entered the main loop

Next: The main loop will start, and TaskScheduler_RunDispatcher() will begin orchestrating task execution based on timing and priorities.

The Complete Task Registration Sequence

Main() registers six tasks in total:

1. Task_LED1_Blink - 500ms period, HIGH priority (Green LED)
2. Task_LED2_Blink - 1000ms period, NORMAL priority (Blue LED)  
3. Task_LED3_Blink - 2000ms period, NORMAL priority (Red LED)
4. Task_UARTStatus - 3000ms period, LOW priority (Status messages)
5. Task_SystemMonitor - 5000ms period, IDLE priority (System info)
6. Task_PowerSaving - 5000ms period, IDLE priority (Power management)

Task array after all registrations:

┌─────────────────────┐
│ ID:0 LED1_Green     │ HIGH priority, 500ms
├─────────────────────┤
│ ID:1 LED2_Blue      │ NORMAL priority, 1000ms
├─────────────────────┤
│ ID:2 LED3_Red       │ NORMAL priority, 2000ms
├─────────────────────┤
│ ID:3 UART_Status    │ LOW priority, 3000ms
├─────────────────────┤
│ ID:4 System_Monitor │ IDLE priority, 5000ms
├─────────────────────┤
│ ID:5 Power_Saving   │ IDLE priority, 5000ms
├─────────────────────┤
│ [empty]             │
├─────────────────────┤
│ [empty]             │
└─────────────────────┘

How the Scheduler Will Work

After initialization and registration, the scheduler is ready to run. In the main loop, TaskScheduler_RunDispatcher() will be called repeatedly:

while(1) {
    /* Run the task dispatcher */
    TaskScheduler_RunDispatcher();
    
    /* Handle user input */
    if (UART_IsDataAvailable()) {
        // Process commands
    }
    
    /* Small delay to prevent CPU overload */
    SysTick_Delay(1);
}

The Dispatcher Algorithm

TaskScheduler_RunDispatcher() implements a priority-based cooperative scheduler:

  1. Check all tasks to see which are due to run
  2. Select highest priority task among those ready
  3. Execute the selected task
  4. Update performance metrics
  5. Return to main loop

Timing Example

Let’s see how tasks execute over time:

Time(ms) | Task Executed      | Next Due Times
---------|-------------------|------------------
0        | -                 | LED1:500, LED2:1000, LED3:2000
500      | LED1 (HIGH)       | LED1:1000, LED2:1000, LED3:2000
1000     | LED1 (HIGH)       | LED1:1500, LED2:2000, LED3:2000
1000     | LED2 (NORMAL)     | LED1:1500, LED2:2000, LED3:2000
1500     | LED1 (HIGH)       | LED1:2000, LED2:2000, LED3:2000
2000     | LED1 (HIGH)       | LED1:2500, LED2:3000, LED3:4000
2000     | LED2 (NORMAL)     | LED1:2500, LED2:3000, LED3:4000
2000     | LED3 (NORMAL)     | LED1:2500, LED2:3000, LED3:4000

Notice how when multiple tasks are ready at the same time, higher priority tasks execute first.

Performance Monitoring

The scheduler tracks two key metrics for each task:

1. Execution Time

How long each task takes to complete:

uint32_t startTime = systick_counter;
task->taskFunc();  // Execute task
uint32_t executionTime = systick_counter - startTime;

if (executionTime > task->maxExecutionTime) {
    task->maxExecutionTime = executionTime;
}

2. Scheduling Jitter

Deviation from the ideal execution time:

uint32_t scheduledTime = task->lastExecuted + task->period_ms;
uint32_t actualTime = systick_counter;
uint32_t jitter = (actualTime > scheduledTime) ? 
                  (actualTime - scheduledTime) : 
                  (scheduledTime - actualTime);

if (jitter > task->maxJitter) {
    task->maxJitter = jitter;
}

These metrics help identify:

  • Tasks taking too long
  • Scheduling accuracy
  • System load issues

Integration with Other Systems

The task scheduler integrates with all previously initialized systems:

SysTick Integration

  • All timing based on systick_counter
  • 1ms resolution for task periods
  • Accurate performance measurements

UART Integration

  • Tasks can output debug messages
  • Scheduler status can be printed
  • Command interface for task control

Power Management Integration

  • Power saving task runs periodically
  • System can sleep between task executions
  • Wake-up resumes normal scheduling

What’s Next in main()

After all tasks are registered and started, main() enters its infinite loop where TaskScheduler_RunDispatcher() orchestrates everything. The system is now fully initialized and operational.


Next: Part 7 – The Main Loop and Task Execution →

This is Part 6 of the STM32 IoT Framework series.

Leave a Comment

Your email address will not be published. Required fields are marked *

2 thoughts on “Part 6: The Ultimate Guide to Effortless MCU Scheduling”

  1. Pingback: Part 5: Finally! Unlock MCU Sleep: Easy Power Secrets - Learn By Building

  2. Pingback: Part 7: Your Simple Guide to the STM32 Main Loop - Learn By Building