Chapter 3: Implementing Context Switching#
Sample code#
Directory:
docs/os-on-arduino/code/03_context_switch
uv run pio run -d 03_context_switch -t upload
uv run pio device monitor -b 115200
Source code#
The sketch for this chapter. Click a file name to expand. The code is mirrored automatically from the manuscript repository, so what you see here is always current.
src/main.cpp — 176 lines
1/**
2 * Chapter 3: Context Switch Demo
3 *
4 * Demonstrates manual context switching using os_yield().
5 * Three tasks run concurrently, each yielding control manually.
6 */
7
8#include <Arduino.h>
9#include <stddef.h>
10#include "os_kernel.h"
11#include "Arduino_LED_Matrix.h"
12
13// Arduino UNO R4 WiFi は基板上に "L" と印字された LED を持っています。
14// 追加部品なしで動かせるよう、これを主 LED として使います。
15#define LED1_PIN LED_BUILTIN
16
17// 2個目の LED も外付けせず、基板内蔵の 12×8 LED マトリクスで代用します。
18// この章では左半分を LED2 として丸ごと点灯・消灯します。
19// マトリクス制御の仕組みは第7章で学びます。ここではライブラリに任せます。
20static ArduinoLEDMatrix matrix;
21static uint8_t matrix_frame[8][12];
22
23static void matrix_led(int led, bool on)
24{
25 int col_start = (led == 3) ? 6 : 0; // 3 なら右半分、それ以外は左半分
26 for (int row = 0; row < 8; row++) {
27 for (int col = col_start; col < col_start + 6; col++) {
28 matrix_frame[row][col] = on ? 1 : 0;
29 }
30 }
31 matrix.renderBitmap(matrix_frame, 8, 12);
32}
33
34// Task 1: Blink LED1
35void task1(void)
36{
37 Serial.println("Task1 started");
38 Serial.flush();
39 pinMode(LED1_PIN, OUTPUT);
40 int count = 0;
41 while (1) {
42 digitalWrite(LED1_PIN, HIGH);
43 for (volatile int i = 0; i < 100000; i++); // Simple delay
44 digitalWrite(LED1_PIN, LOW);
45 for (volatile int i = 0; i < 100000; i++);
46
47 Serial.print("Task1 yield #");
48 Serial.println(count++);
49 Serial.flush();
50 os_yield(); // Explicitly yield CPU
51 }
52}
53
54// Task 2: Blink LED2 (matrix left half) at different speed
55void task2(void)
56{
57 Serial.println("Task2 started");
58 Serial.flush();
59 int count = 0;
60 while (1) {
61 matrix_led(2, true);
62 for (volatile int i = 0; i < 50000; i++);
63 matrix_led(2, false);
64 for (volatile int i = 0; i < 50000; i++);
65
66 Serial.print("Task2 yield #");
67 Serial.println(count++);
68 Serial.flush();
69 os_yield();
70 }
71}
72
73// Task 3: Serial output
74void task3(void)
75{
76 int count = 0;
77 while (1) {
78 Serial.print("Task3 count: ");
79 Serial.println(count++);
80 Serial.flush();
81
82 for (volatile int i = 0; i < 200000; i++);
83 Serial.println("Task3 yielding...");
84 Serial.flush();
85 os_yield();
86 Serial.println("Task3 resumed");
87 Serial.flush();
88 }
89}
90
91void setup()
92{
93 Serial.begin(115200);
94 delay(2000); // Wait for serial connection
95
96 // Blink built-in LED to show we're alive
97 pinMode(LED_BUILTIN, OUTPUT);
98 for (int i = 0; i < 3; i++) {
99 digitalWrite(LED_BUILTIN, HIGH);
100 delay(100);
101 digitalWrite(LED_BUILTIN, LOW);
102 delay(100);
103 }
104
105 // LEDマトリクスを初期化(LED2 の代用)
106 matrix.begin();
107
108 Serial.println();
109 Serial.println("=== Context Switch Demo ===");
110 Serial.println();
111 Serial.println("This demonstrates cooperative context switching.");
112 Serial.println("Each task must call os_yield() to switch to another task.");
113 Serial.println();
114
115 // Initialize OS
116 Serial.println("Initializing OS...");
117 Serial.flush();
118 os_init();
119 Serial.println("OS initialized.");
120 Serial.flush();
121
122 // Debug: print TCB size and offsets
123 Serial.print("TCB size: ");
124 Serial.println(sizeof(TCB));
125 Serial.print(" sp offset: ");
126 Serial.println(offsetof(TCB, sp));
127 Serial.print(" stack_base offset: ");
128 Serial.println(offsetof(TCB, stack_base));
129 Serial.print(" state offset: ");
130 Serial.println(offsetof(TCB, state));
131 Serial.print(" name offset: ");
132 Serial.println(offsetof(TCB, name));
133 Serial.print(" entry offset: ");
134 Serial.println(offsetof(TCB, entry));
135 Serial.flush();
136
137 // Create tasks
138 Serial.println("Creating tasks...");
139 Serial.flush();
140 os_create_task(task1, "LED1", 512);
141 os_create_task(task2, "LED2", 512);
142 os_create_task(task3, "Serial", 1024);
143
144 Serial.println("Tasks created:");
145 Serial.println(" - idle (ID 0)");
146 Serial.println(" - LED1 (ID 1)");
147 Serial.println(" - LED2 (ID 2)");
148 Serial.println(" - Serial (ID 3)");
149 Serial.println();
150 Serial.print("Task count: ");
151 Serial.println(g_task_count);
152 Serial.println();
153 // Debug: print task stack pointers
154 Serial.println("Task stack pointers:");
155 for (int i = 0; i < g_task_count; i++) {
156 Serial.print(" Task ");
157 Serial.print(i);
158 Serial.print(" sp: 0x");
159 Serial.println((uint32_t)g_tasks[i].sp, HEX);
160 }
161 Serial.flush();
162
163 Serial.print("sizeof(TCB) = ");
164 Serial.println(sizeof(TCB));
165 Serial.println("Starting OS...");
166 Serial.flush();
167 delay(100);
168
169 // Start OS (never returns)
170 os_start();
171}
172
173void loop()
174{
175 // Never reached
176}
include/os_kernel.h — 66 lines
1/**
2 * os_kernel.h - Mini OS Kernel Header
3 * Chapter 3: Context Switch Implementation
4 */
5
6#ifndef OS_KERNEL_H
7#define OS_KERNEL_H
8
9#include <stdint.h>
10
11#ifdef __cplusplus
12extern "C" {
13#endif
14
15// Task states
16typedef enum {
17 TASK_READY, // Ready to run
18 TASK_RUNNING, // Currently running
19 TASK_BLOCKED, // Blocked (waiting for I/O, etc.)
20 TASK_SUSPENDED, // Suspended (by kill command)
21 TASK_TERMINATED // Terminated
22} TaskState;
23
24// Task Control Block (TCB)
25typedef struct {
26 uint32_t *sp; // Current stack pointer
27 uint32_t *stack_base; // Stack base (high address)
28 uint32_t stack_size; // Stack size in bytes
29 TaskState state; // Task state
30 uint8_t id; // Task ID
31 uint8_t priority; // Priority (0 = highest)
32 char name[16]; // Task name
33 void (*entry)(void); // Entry point
34} TCB;
35
36// System configuration
37#define MAX_TASKS 8
38#define DEFAULT_STACK_SIZE 512
39// タスク用スタックをまとめて置く領域。os_create_task() は
40// ここから要求されたぶんだけ切り出す。
41// 既定より大きなスタックを要求するタスク(シェルなど)があるため、
42// MAX_TASKS * DEFAULT_STACK_SIZE では足りない。余裕を持たせる。
43#define STACK_POOL_SIZE (MAX_TASKS * DEFAULT_STACK_SIZE + 2048)
44#define IDLE_STACK_SIZE 256
45
46// Kernel API
47void os_init(void);
48int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size);
49void os_start(void);
50void os_yield(void);
51
52// Internal functions
53void os_schedule(void);
54
55// Global variables (for assembly access)
56extern TCB g_tasks[MAX_TASKS];
57extern volatile int g_task_count;
58extern volatile int g_current_task;
59extern volatile int g_next_task;
60extern const uint32_t g_tcb_size;
61
62#ifdef __cplusplus
63}
64#endif
65
66#endif // OS_KERNEL_H
src/os_kernel.cpp — 200 lines
1/**
2 * os_kernel.cpp - Mini OS Kernel Implementation
3 * Chapter 3: Context Switch Implementation
4 */
5
6#include "os_kernel.h"
7#include <Arduino.h>
8#include <string.h>
9
10// Global variables
11TCB g_tasks[MAX_TASKS];
12volatile int g_task_count = 0;
13volatile int g_current_task = -1;
14volatile int g_next_task = -1;
15
16// TCB size for PendSV assembly (compiler may use -fshort-enums, changing sizeof)
17const uint32_t g_tcb_size = sizeof(TCB);
18
19// Stack areas (statically allocated)
20// All tasks use DEFAULT_STACK_SIZE for simplicity
21static uint32_t g_stack_pool[STACK_POOL_SIZE / 4];
22static uint32_t g_stack_used; // 切り出し済みのワード数
23
24// Idle task (does nothing, waits for interrupt)
25static void idle_task(void)
26{
27 while (1) {
28 __WFI(); // Wait For Interrupt: sleep CPU until interrupt
29 }
30}
31
32// Task exit handler - called if a task returns from its entry function
33static void task_exit(void)
34{
35 // Mark current task as terminated and yield
36 g_tasks[g_current_task].state = TASK_TERMINATED;
37 os_yield();
38 while (1) { } // Should never reach here
39}
40
41// Initialize stack for a task so it can "return" from context switch
42static uint32_t* init_task_stack(uint32_t *stack_top, void (*entry)(void))
43{
44 // Stack grows from high to low address
45 uint32_t *sp = stack_top;
46
47 // Hardware auto-restores these on exception return (in order: R0,R1,R2,R3,R12,LR,PC,xPSR)
48 // Note: Stack is filled from high to low, so we push in reverse order
49 *(--sp) = 0x01000000; // xPSR: Thumb bit set (bit 24)
50 *(--sp) = (uint32_t)entry; // PC: entry point
51 *(--sp) = (uint32_t)task_exit; // LR: return address if task exits
52 *(--sp) = 0; // R12
53 *(--sp) = 0; // R3
54 *(--sp) = 0; // R2
55 *(--sp) = 0; // R1
56 *(--sp) = 0; // R0
57
58 // Software saves/restores these (R4-R11)
59 *(--sp) = 0; // R11
60 *(--sp) = 0; // R10
61 *(--sp) = 0; // R9
62 *(--sp) = 0; // R8
63 *(--sp) = 0; // R7
64 *(--sp) = 0; // R6
65 *(--sp) = 0; // R5
66 *(--sp) = 0; // R4
67
68 return sp;
69}
70
71void os_init(void)
72{
73 // Initialize task array
74 memset(g_tasks, 0, sizeof(g_tasks));
75 g_task_count = 0;
76 g_current_task = -1;
77 g_next_task = -1;
78
79 // Create idle task (ID=0)
80 os_create_task(idle_task, "idle", IDLE_STACK_SIZE);
81}
82
83int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size)
84{
85 if (g_task_count >= MAX_TASKS) {
86 return -1; // Task limit reached
87 }
88
89 int id = g_task_count;
90 TCB *task = &g_tasks[id];
91
92 // Initialize TCB
93 task->id = id;
94 task->state = TASK_READY;
95 task->priority = id; // Simple: use ID as priority
96 task->entry = entry;
97 strncpy(task->name, name, sizeof(task->name) - 1);
98
99 // Set up stack (all tasks use DEFAULT_STACK_SIZE)
100 if (stack_size == 0) {
101 stack_size = DEFAULT_STACK_SIZE;
102 }
103 uint32_t words = (stack_size + 3) / 4;
104 if (g_stack_used + words > STACK_POOL_SIZE / 4) {
105 return -1; // プールが尽きた。黙って縮めず、失敗として返す
106 }
107 g_stack_used += words;
108 task->stack_size = stack_size;
109 task->stack_base = &g_stack_pool[g_stack_used]; // スタックは上端から下へ伸びる // High address (end of array)
110 task->sp = init_task_stack(task->stack_base, entry);
111
112 g_task_count++;
113 return id;
114}
115
116void os_schedule(void)
117{
118 // Simple round-robin scheduling (skip idle task 0 for now)
119 int next = g_current_task;
120
121 do {
122 next = (next + 1) % g_task_count;
123 // Skip idle task (task 0) for debugging
124 if (next == 0 && g_task_count > 1) {
125 next = 1;
126 }
127 if (g_tasks[next].state == TASK_READY ||
128 g_tasks[next].state == TASK_RUNNING) {
129 break;
130 }
131 } while (next != g_current_task);
132
133 if (next != g_current_task) {
134 if (g_current_task >= 0) {
135 g_tasks[g_current_task].state = TASK_READY;
136 }
137 g_tasks[next].state = TASK_RUNNING;
138 g_next_task = next;
139
140 // Trigger PendSV
141 SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
142 }
143}
144
145void os_yield(void)
146{
147 os_schedule();
148}
149
150void os_start(void)
151{
152 // Set PendSV to lowest priority (so it runs after other interrupts)
153 NVIC_SetPriority(PendSV_IRQn, 0xFF);
154
155 // g_current_task = -1 means no task is running yet.
156 // PendSV will skip saving context and just load the first task.
157 g_current_task = -1;
158 g_next_task = (g_task_count > 1) ? 1 : 0;
159
160 // Trigger PendSV to load the first task's context
161 SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
162
163 // Wait here - PendSV will switch to the first task
164 while (1) { }
165}
166
167// PendSV Handler - Context Switch
168extern "C" __attribute__((naked)) void PendSV_Handler(void)
169{
170 __asm volatile (
171 " MRS R0, PSP \n"
172 " LDR R1, =g_current_task \n"
173 " LDR R2, [R1] \n"
174 " CMP R2, #0 \n"
175 " BLT _skip_save \n"
176 // Save R4-R11 only when there is a valid current task
177 " STMDB R0!, {R4-R11} \n"
178 " LDR R3, =g_tasks \n"
179 " LDR R4, =g_tcb_size \n"
180 " LDR R4, [R4] \n"
181 " MUL R2, R2, R4 \n"
182 " ADD R3, R3, R2 \n"
183 " STR R0, [R3] \n"
184 "_skip_save: \n"
185 " LDR R1, =g_next_task \n"
186 " LDR R2, [R1] \n"
187 " LDR R1, =g_current_task \n"
188 " STR R2, [R1] \n"
189 " LDR R3, =g_tasks \n"
190 " LDR R4, =g_tcb_size \n"
191 " LDR R4, [R4] \n"
192 " MUL R2, R2, R4 \n"
193 " ADD R3, R3, R2 \n"
194 " LDR R0, [R3] \n"
195 " LDMIA R0!, {R4-R11} \n"
196 " MSR PSP, R0 \n"
197 " LDR LR, =0xFFFFFFFD \n"
198 " BX LR \n"
199 );
200}
On the board#
Task1 (registered as LED1) → Task2 (LED2) → Task3 (Serial) keep handing over to each other. They go round about 8 times a second, so the output scrolls quickly.#
When each of the three tasks has the CPU and when the LEDs are lit, over two rounds measured
on the board. The tasks hand over with os_yield() and one round takes about 117 ms. The L LED
is lit for only about 22 ms of that and the left half of the matrix for about 12 ms, which is
why a video or a simulator GIF does not show them as blinking. (Measured by recording
micros() timestamps inside each task; the extra measuring code is not part of the book’s sample.)#
Pitfalls#
(Collected from reader questions — ask via GitHub Issues.)