Project 13: Mini Operating System - Cooperative Multitasking

Build a tiny cooperative scheduler with a task manager UI on the LCD.

Quick Reference

Attribute Value
Difficulty Level 5: Master
Time Estimate 1-2 months
Main Programming Language C (Alternatives: Rust)
Alternative Programming Languages Rust
Coolness Level Level 5: Pure Magic
Business Potential 1. The “Resume Gold” Level
Prerequisites Projects 5 and 10 (multicore + bare metal)
Key Topics Task scheduling, context switching, stack management

1. Learning Objectives

By completing this project, you will:

  1. Implement cooperative task scheduling with yield points.
  2. Create task control blocks with stack pointers.
  3. Switch contexts safely between tasks.
  4. Display a task manager UI with CPU usage per task.
  5. Measure scheduler overhead and latency.

2. All Theory Needed (Per-Concept Breakdown)

2.1 Cooperative Scheduling and Task Control Blocks

Fundamentals

Cooperative scheduling means tasks voluntarily yield control to the scheduler. Each task runs until it calls yield(), which saves its context and switches to another task. The scheduler manages a list of task control blocks (TCBs), each containing the task’s stack pointer, state, and metadata. Cooperative scheduling is simpler than preemptive scheduling because you avoid interrupts, but it requires tasks to behave well and yield frequently.

Deep Dive into the concept

A TCB is a small struct that describes a task: stack base, stack pointer, state (ready, running, blocked), and optional stats like CPU time. When the scheduler switches tasks, it saves the current task’s stack pointer and restores the next task’s stack pointer. In cooperative systems, context switches occur at explicit yield points, making the system deterministic and easier to debug. However, a single misbehaving task that never yields will block the entire system. You must design tasks with short time slices and regular yield points. A common pattern is to write tasks as state machines that return control often.

Scheduling policies can be simple round-robin or priority-based. For this project, round-robin is sufficient. You iterate through the list of tasks and pick the next READY task. You can also implement a simple sleep mechanism by tracking a wake-up time and skipping tasks until they are due. CPU usage per task can be measured by counting how many ticks each task runs or by measuring cycles between yield calls.

How this fits on projects

Scheduling is central in Section 3.2 and Section 5.10 Phase 2. It builds on Project 5 (multicore synchronization) and Project 10 (bare-metal startup). Also used in: Project 5, Project 10.

Definitions & key terms

  • Cooperative -> Tasks yield voluntarily.
  • TCB -> Task Control Block.
  • Yield -> Voluntary context switch.
  • Round-robin -> Scheduler cycles through tasks in order.

Mental model diagram (ASCII)

Task A -> yield -> Task B -> yield -> Task C -> yield -> Task A

How it works (step-by-step)

  1. Scheduler selects next READY task.
  2. Save current task SP.
  3. Restore next task SP.
  4. Resume next task.
  5. Task calls yield when done.

Failure modes:

  • Task never yields -> system hangs.
  • Incorrect SP restore -> crash.

Minimal concrete example

typedef struct { uint32_t *sp; uint8_t state; } tcb_t;

void yield(void) {
  save_context(&tasks[cur]);
  cur = (cur + 1) % task_count;
  restore_context(&tasks[cur]);
}

Common misconceptions

  • “Cooperative is simpler, so no bugs.” -> Stack and context bugs still occur.
  • “Tasks can run forever.” -> They must yield regularly.

Check-your-understanding questions

  1. Why must tasks yield in a cooperative scheduler?
  2. What is stored in a TCB?
  3. How does round-robin scheduling work?

Check-your-understanding answers

  1. The scheduler only switches at yield points.
  2. Stack pointer, state, and metadata.
  3. It cycles through tasks in a fixed order.

Real-world applications

  • Simple embedded OS kernels
  • Cooperative task loops in IoT devices

Where you’ll apply it

  • This project: Section 3.2, Section 5.10 Phase 2
  • Also used in: Project 5

References

  • “Operating Systems: Three Easy Pieces” (scheduling)

Key insights

Cooperative scheduling is easy to implement but requires disciplined tasks.

Summary

A cooperative scheduler is a deterministic way to run multiple tasks on a MCU.

Homework/Exercises to practice the concept

  1. Implement a round-robin scheduler with 3 tasks.
  2. Add a sleep() function to skip tasks temporarily.
  3. Measure how often each task runs.

Solutions to the homework/exercises

  1. Cycle through array of TCBs and call yield.
  2. Store wake-up tick in TCB.
  3. Count yields per task.

2.2 Context Switching and Stack Management

Fundamentals

A context switch saves the CPU registers and stack pointer of the current task and restores those of the next task. Each task needs its own stack. Stack size must be chosen carefully: too small and you get overflow; too large and you waste RAM. In cooperative systems, context switches are explicit, but they still require correct save/restore code.

Deep Dive into the concept

On ARM Cortex-M33, the CPU automatically saves some registers on exception entry, but in a cooperative scheduler you may implement context switching in C or assembly without exceptions. This means you must save callee-saved registers and the stack pointer. A typical context switch stores R4-R11 and SP. On RISC-V, you must save s0-s11 and SP. The scheduler then loads the next task’s SP and restores registers. You must also initialize each task’s stack with an initial context so that when it is first scheduled, it begins at its entry function. This is done by pushing a fake stack frame with the entry address and initial register values.

Stack management requires monitoring for overflow. You can place a guard pattern at the end of the stack and check whether it is overwritten. You can also measure maximum stack depth by scanning for the guard pattern. For this project, include a stack usage display in the task manager UI. This helps you tune stack sizes and teaches you how stack usage varies per task.

How this fits on projects

Context switching is central to Section 3.2 and Section 5.10 Phase 2. It builds on Project 10 (startup) and Project 5 (multicore knowledge). Also used in: Project 10, Project 5.

Definitions & key terms

  • Context switch -> Save/restore CPU state between tasks.
  • Stack frame -> Saved registers and return address.
  • Guard pattern -> Known value used to detect overflow.
  • Stack pointer (SP) -> Points to current stack top.

Mental model diagram (ASCII)

Task A stack: [guard][...used...][SP]
Task B stack: [guard][...used...][SP]

How it works (step-by-step)

  1. Save registers and SP to current task’s TCB.
  2. Load SP for next task.
  3. Restore registers and resume next task.
  4. Task runs until it yields.

Failure modes:

  • Wrong register save -> crashes.
  • Stack overflow -> silent corruption.

Minimal concrete example

void init_task(tcb_t *t, void (*entry)(void), uint8_t *stack, size_t size) {
  uint32_t *sp = (uint32_t*)(stack + size);
  *(--sp) = (uint32_t)entry; // initial PC
  t->sp = sp;
}

Common misconceptions

  • “Stack size is easy to guess.” -> It often isn’t.
  • “Cooperative means no context switch.” -> It still switches contexts.

Check-your-understanding questions

  1. What registers must be saved in a context switch?
  2. How do you detect stack overflow?
  3. Why initialize a fake stack frame?

Check-your-understanding answers

  1. Callee-saved registers and SP (plus PC if needed).
  2. Use guard pattern and check for corruption.
  3. So the task starts at the entry function on first switch.

Real-world applications

  • RTOS kernels
  • Cooperative multitasking in low-power devices

Where you’ll apply it

  • This project: Section 3.2, Section 5.10 Phase 2
  • Also used in: Project 10

References

  • ARM Cortex-M33 architecture docs
  • “Operating Systems: Three Easy Pieces” (context switching)

Key insights

Context switching is stack manipulation plus disciplined register saves.

Summary

Correct stack setup and context save/restore are the heart of a tiny OS.

Homework/Exercises to practice the concept

  1. Create two tasks and switch between them manually.
  2. Add a stack guard and display max usage.
  3. Trigger a stack overflow and detect it.

Solutions to the homework/exercises

  1. Alternate calling yield from each task.
  2. Fill stack with 0xDEADBEEF and scan.
  3. Overflow will overwrite guard pattern.

3. Project Specification

3.1 What You Will Build

A cooperative multitasking mini-OS with a task manager UI on the LCD. The system runs multiple tasks (render, input, telemetry) and shows CPU usage and stack usage for each.

3.2 Functional Requirements

  1. Task creation with dedicated stacks.
  2. Cooperative scheduler (round-robin).
  3. Yield function for tasks.
  4. Task manager UI with stats.

3.3 Non-Functional Requirements

  • Performance: scheduler overhead <5% CPU.
  • Reliability: no crashes over 1 hour.
  • Usability: clear task names and stats.

3.4 Example Usage / Output

Task Manager
1) Render  35%  Stack: 60%
2) Input   5%   Stack: 20%
3) Stats   10%  Stack: 15%

3.5 Data Formats / Schemas / Protocols

  • TCB struct { sp, state, cpu_ticks, stack_base }

3.6 Edge Cases

  • Task never yields
  • Stack overflow
  • Scheduler with no READY tasks

3.7 Real World Outcome

The LCD shows a live task manager with per-task CPU and stack usage. Tasks run smoothly and the system remains responsive.

3.7.1 How to Run (Copy/Paste)

cd LEARN_RP2350_LCD_DEEP_DIVE/mini_os
mkdir -p build
cd build
cmake ..
make -j4
cp mini_os.uf2 /Volumes/RP2350

3.7.2 Golden Path Demo (Deterministic)

  • Three tasks run: render, input, stats.
  • CPU usage adds up to ~100%.
  • Stack usage numbers remain stable.

3.7.3 Failure Demo (Deterministic)

  • Comment out yield in one task.
  • Expected: system freezes.
  • Fix: reinstate yield points.

4. Solution Architecture

4.1 High-Level Design

[Scheduler] -> [Task A]
           -> [Task B]
           -> [Task C]
[UI Renderer] -> LCD

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Scheduler | Select next task | Round-robin | | Context switch | Save/restore regs | ASM or inline C | | Task manager UI | Display stats | LCD update at 10 Hz |

4.3 Data Structures (No Full Code)

typedef struct { uint32_t *sp; uint32_t ticks; uint8_t state; } tcb_t;

4.4 Algorithm Overview

Key Algorithm: Scheduler Tick

  1. Find next READY task.
  2. Save current context.
  3. Restore next context.
  4. Continue execution.

Complexity Analysis:

  • Time: O(num_tasks)
  • Space: O(num_tasks)

5. Implementation Guide

5.1 Development Environment Setup

# Use pico-sdk or bare-metal toolchain

5.2 Project Structure

mini_os/
- src/
  - scheduler.c
  - context_switch.S
  - tasks.c
  - main.c

5.3 The Core Question You’re Answering

“How do I design a tiny OS that schedules tasks on a microcontroller?”

5.4 Concepts You Must Understand First

  1. Task control blocks
  2. Context switching
  3. Cooperative scheduling

5.5 Questions to Guide Your Design

  1. How will tasks yield control?
  2. How many tasks can fit in RAM?
  3. How will you track CPU usage per task?

5.6 Thinking Exercise

Estimate stack usage for three tasks with nested calls.

5.7 The Interview Questions They’ll Ask

  1. What is a context switch?
  2. Cooperative vs preemptive scheduling?
  3. How do you prevent stack overflow?

5.8 Hints in Layers

  • Hint 1: Start with two tasks: idle and display.
  • Hint 2: Add yield points after each update.
  • Hint 3: Add stack guard patterns.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Scheduling | “Operating Systems: Three Easy Pieces” | Ch. 7-9 | | Low-level C | “Effective C” | Ch. 5 |

5.10 Implementation Phases

Phase 1: Scheduler Core (1-2 weeks)

Goals: Switch between two tasks. Tasks: Implement TCB + yield. Checkpoint: LED blink tasks alternate.

Phase 2: Task Manager UI (1-2 weeks)

Goals: Display task stats. Tasks: Compute CPU usage per task. Checkpoint: UI updates at 10 Hz.

Phase 3: Robustness (2-3 weeks)

Goals: Stack guards and error handling. Tasks: Add overflow detection, idle task. Checkpoint: System runs 1 hour without crash.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Scheduler | Round-robin vs priority | Round-robin | Simpler, deterministic | | Context switch | ASM vs C | ASM | Precise control |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | TCB logic | task state transitions | | Integration Tests | Context switch | alternating tasks | | Stress Tests | Long run | 1 hour continuous |

6.2 Critical Test Cases

  1. Yield test: tasks switch reliably.
  2. Stack guard: overflow detected.
  3. No ready task: idle task runs.

6.3 Test Data

Task count: 3, stack size: 1 KB each

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |———|———|———-| | Missing yield | system freezes | add yield points | | Stack overflow | random crashes | add guard pattern | | Wrong register save | hard faults | verify context switch code |

7.2 Debugging Strategies

  • Toggle GPIO on context switch to verify timing.
  • Display stack usage on UI.

7.3 Performance Traps

  • Too many tasks with large stacks waste RAM.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a sleep() API.

8.2 Intermediate Extensions

  • Add priority scheduling.

8.3 Advanced Extensions

  • Add preemptive scheduling with timer interrupts.

9. Real-World Connections

9.1 Industry Applications

  • Tiny RTOS designs in IoT devices
  • FreeRTOS (compare design)

9.3 Interview Relevance

  • OS design and context switching are advanced interview topics.

10. Resources

10.1 Essential Reading

  • “Operating Systems: Three Easy Pieces”
  • ARM Cortex-M33 docs

10.2 Video Resources

  • OS scheduling tutorials

10.3 Tools & Documentation

  • GDB for debugging context switches

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain cooperative scheduling.
  • I can save/restore a task context.

11.2 Implementation

  • Scheduler runs multiple tasks reliably.
  • Task manager UI shows correct stats.

11.3 Growth

  • I can explain OS design trade-offs in an interview.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Two tasks switch via yield.

Full Completion:

  • Task manager UI with stats.

Excellence (Going Above & Beyond):

  • Add preemptive scheduling and priority classes.