第3章: コンテキストスイッチの実装#

タスクの実行状態を保存・復元する仕組みを PendSV 例外の上に実装します。

サンプルコード#

  • ディレクトリ: 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

コードを見る: docs/os-on-arduino/code/03_context_switch

ソースコード#

この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。

src/main.cpp — 176 行

GitHub で開く

  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 行

GitHub で開く

 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 行

GitHub で開く

  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}

実機での出力#

Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。

os_yield() で 3 つのタスクが切り替わります。

../_images/ch03_yield.gif

Task1(登録名 LED1)→ Task2(LED2)→ Task3(Serial)と譲り合いながら回り続けるところ。 1 秒あたり 8 周ほどしているので、かなりの速さで流れます#

../_images/ch03_timing.svg

3 つのタスクが CPU を使っている時間と、LED が点いている時間(実機で計測した 2 周ぶん)。 タスクは os_yield() で順に CPU を譲り合い、1 周は約 117 ms です。そのうち L LED が 点いているのは約 22 ms、マトリクス左半分は約 12 ms だけなので、動画やシミュレータの GIF では点滅には見えません。(各タスクの中で micros() の時刻を記録して計測。計測用に 書き足したコードは本書のサンプルには含まれません)#

=== Context Switch Demo ===

This demonstrates cooperative context switching.
Each task must call os_yield() to switch to another task.

Initializing OS...
OS initialized.
TCB size: 36
  sp offset: 0
  stack_base offset: 4
  state offset: 12
  name offset: 15
  entry offset: 32
Creating tasks...
Tasks created:
  - idle (ID 0)
  - LED1 (ID 1)
  - LED2 (ID 2)
  - Serial (ID 3)

Task count: 4

Task stack pointers:
  Task 0 sp: 0x20000260
...

TCB(Task Control Block:タスクごとの管理情報)の大きさとオフセットを表示しているのは、 PendSV のアセンブリが TCB の配列を TCB size バイトずつ進んでたどり、各 TCB の 先頭(オフセット 0)にある sp を読み書きするからです。コンパイラの設定で構造体の 大きさや並びが変わっても、アセンブリの前提とずれていないかをここで確かめられます。

つまずきやすいポイント#

(読者からの質問に応じて随時追記します。質問は GitHub Issues へどうぞ)