Advanced 2: User/Kernel Mode and SVC#
Sample code#
Directory:
docs/os-on-arduino/code/adv2_syscall
uv run pio run -d adv2_syscall -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 — 94 lines
1/**
2 * 応用編 第2章: ユーザー/カーネルモードと SVC
3 *
4 * タスクが Serial や os_sleep() を直接呼ばず、すべて svc 命令を経由して
5 * カーネルに依頼する形にしたデモ。
6 *
7 * user_putchar() などのラッパは svc を発行するだけで、実際に Serial を
8 * 触るのは SVC_Handler の先にいる sys_putchar()。ユーザーコードから
9 * ペリフェラルへの直接アクセスが 1 箇所も無いことがポイント。
10 *
11 * なお本章 2.3 の「非特権モードへ落とす」(CONTROL.nPRIV=1)は、
12 * PendSV でタスクごとに CONTROL を切り替える改造が要る。本書はそこを
13 * 擬似コードで示すにとどめているため、このサンプルも特権スレッドのまま
14 * 動かしている。SVC の仕組み自体は特権/非特権のどちらからでも同じ。
15 */
16
17#include <Arduino.h>
18#include "os_kernel.h"
19#include "os_syscall.h"
20#include "os_syscall_user.h"
21
22// システムコールだけで書いた文字列出力
23static void user_print(const char *s)
24{
25 while (*s) user_putchar(*s++);
26}
27
28static void user_print_int(int v)
29{
30 char buf[12];
31 snprintf(buf, sizeof(buf), "%d", v);
32 user_print(buf);
33}
34
35// ユーザータスク A: 自分の PID を名乗って寝るだけ
36static void task_a(void)
37{
38 while (1) {
39 user_print("[A] pid=");
40 user_print_int(user_getpid());
41 user_print("\r\n");
42 user_sleep(1000);
43 }
44}
45
46// ユーザータスク B: yield を挟みながら数える
47static void task_b(void)
48{
49 int n = 0;
50 while (1) {
51 user_print("[B] count=");
52 user_print_int(n++);
53 user_print("\r\n");
54 user_yield();
55 user_sleep(1500);
56 }
57}
58
59// 未定義の SVC 番号を呼ぶと -1 (ENOSYS 相当) が返ることを確かめる
60static void task_probe(void)
61{
62 user_sleep(2500);
63 int r = (int)SYSCALL0(99);
64 user_print("[probe] svc #99 -> ");
65 user_print_int(r);
66 user_print(" (-1 なら ENOSYS 相当が返っている)\r\n");
67 user_exit(0);
68 while (1) user_sleep(1000); // ここへは戻らない
69}
70
71void setup()
72{
73 Serial.begin(115200);
74 while (!Serial) { }
75
76 Serial.println(F("=== Advanced 2: SVC system calls ==="));
77 Serial.println(F("以降の出力はすべて svc 経由(sys_putchar)で出ています"));
78
79 os_init();
80
81 // SVC の優先度を下げておく(重要)。
82 // リセット直後の SVC は最高優先度 0 のため、SVC_Handler の中で
83 // Serial.write() がバッファ空きを待つと、それを配る UART/USB 割り込み
84 // (優先度はもっと低い)が永遠に走れず、システム全体が止まる。
85 // 「ペリフェラル割り込みより低く、PendSV/SysTick(15) より高く」が正解。
86 NVIC_SetPriority(SVCall_IRQn, 14);
87
88 os_create_task(task_a, "UserA", 512);
89 os_create_task(task_b, "UserB", 512);
90 os_create_task(task_probe, "Probe", 512);
91 os_start();
92}
93
94void loop() { }
include/os_kernel.h — 74 lines
1/**
2 * os_kernel.h - Mini OS Kernel Header
3 * Chapter 6: Memory Protection with HardFault Handler
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
15typedef enum {
16 TASK_READY,
17 TASK_RUNNING,
18 TASK_BLOCKED,
19 TASK_SUSPENDED,
20 TASK_TERMINATED
21} TaskState;
22
23typedef struct {
24 uint32_t *sp;
25 uint32_t *stack_base;
26 uint32_t stack_size;
27 TaskState state;
28 uint8_t id;
29 uint8_t priority;
30 char name[16];
31 void (*entry)(void);
32 uint32_t wake_tick;
33 uint32_t cpu_ticks;
34} TCB;
35
36#define MAX_TASKS 8
37#define DEFAULT_STACK_SIZE 512
38// タスク用スタックをまとめて置く領域。os_create_task() は
39// ここから要求されたぶんだけ切り出す。
40// 既定より大きなスタックを要求するタスク(シェルなど)があるため、
41// MAX_TASKS * DEFAULT_STACK_SIZE では足りない。余裕を持たせる。
42#define STACK_POOL_SIZE (MAX_TASKS * DEFAULT_STACK_SIZE + 2048)
43#define IDLE_STACK_SIZE 256
44#define TIME_SLICE_MS 10
45
46void os_init(void);
47int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size);
48void os_start(void);
49void os_yield(void);
50void os_sleep(uint32_t ms);
51
52void os_suspend_task(int id);
53void os_resume_task(int id);
54void os_terminate_task(int id);
55
56int os_get_task_count(void);
57const char* os_get_task_name(int id);
58TaskState os_get_task_state(int id);
59int os_get_current_task(void);
60int os_get_cpu_usage(int task_id);
61uint32_t os_get_tick(void);
62
63extern TCB g_tasks[MAX_TASKS];
64extern volatile int g_task_count;
65extern volatile int g_current_task;
66extern volatile int g_next_task;
67extern volatile uint32_t g_system_ticks;
68extern const uint32_t g_tcb_size;
69
70#ifdef __cplusplus
71}
72#endif
73
74#endif
include/os_syscall.h — 33 lines
1// os_syscall.h
2//
3// 本書 応用編 第2章「ユーザー/カーネルモードと SVC」のシステムコール番号と、
4// カーネル側の実体。番号は svc 命令の即値としてそのまま埋め込まれる。
5
6#pragma once
7
8#include <stdint.h>
9
10#define SYS_putchar 0
11#define SYS_yield 1
12#define SYS_sleep 2
13#define SYS_exit 3
14#define SYS_getpid 4
15#define SYS_NR 5 // システムコール総数
16
17#ifdef __cplusplus
18extern "C" {
19#endif
20
21// カーネル側の実体(SVC_Handler から呼ばれる)
22void sys_putchar(char c);
23void sys_yield(void);
24void sys_sleep(uint32_t ms);
25void sys_exit(int code);
26int sys_getpid(void);
27
28// SVC ハンドラの C 側ディスパッチャ
29void svc_dispatch_c(uint32_t *sp);
30
31#ifdef __cplusplus
32}
33#endif
include/os_syscall_user.h — 38 lines
1// os_syscall_user.h
2//
3// ユーザー側のラッパ。svc 命令の即値はコンパイル時定数でなければならないため、
4// static inline にして呼び出し元で定数畳み込みされることを前提にしている。
5
6#pragma once
7
8#include <stdint.h>
9#include "os_syscall.h"
10
11static inline uint32_t _svc_call(uint8_t num, uint32_t a0,
12 uint32_t a1, uint32_t a2)
13{
14 register uint32_t r0 __asm("r0") = a0;
15 register uint32_t r1 __asm("r1") = a1;
16 register uint32_t r2 __asm("r2") = a2;
17 uint32_t ret;
18
19 __asm volatile (
20 "svc %[n]"
21 : "=r" (r0)
22 : [n] "i" (num), "r" (r0), "r" (r1), "r" (r2)
23 : "memory"
24 );
25 ret = r0;
26 return ret;
27}
28
29// ★ 引数がインライン即値である必要があるため、番号ごとにマクロ展開
30#define SYSCALL0(n) _svc_call((n), 0, 0, 0)
31#define SYSCALL1(n, a) _svc_call((n), (uint32_t)(a), 0, 0)
32#define SYSCALL2(n, a, b) _svc_call((n), (uint32_t)(a), (uint32_t)(b), 0)
33
34static inline void user_putchar(char c) { SYSCALL1(SYS_putchar, c); }
35static inline void user_yield(void) { SYSCALL0(SYS_yield); }
36static inline void user_sleep(uint32_t m) { SYSCALL1(SYS_sleep, m); }
37static inline void user_exit(int code) { SYSCALL1(SYS_exit, code); }
38static inline int user_getpid(void) { return (int)SYSCALL0(SYS_getpid); }
src/os_kernel.cpp — 231 lines
1/**
2 * os_kernel.cpp - Mini OS Kernel Implementation
3 * Chapter 6: Memory Protection
4 */
5
6#include "os_kernel.h"
7#include <Arduino.h>
8#include <string.h>
9
10TCB g_tasks[MAX_TASKS];
11volatile int g_task_count = 0;
12volatile int g_current_task = -1;
13volatile int g_next_task = -1;
14volatile uint32_t g_system_ticks = 0;
15
16const uint32_t g_tcb_size = sizeof(TCB);
17
18static uint32_t g_stack_pool[STACK_POOL_SIZE / 4];
19static uint32_t g_stack_used; // 切り出し済みのワード数
20
21static void idle_task(void) { while (1) __WFI(); }
22
23// タスク関数が return したときの受け皿。例外フレームの LR スロットは
24// ここを指す (第3章 3.4)。EXC_RETURN を置く場所ではない。
25static void task_exit(void)
26{
27 g_tasks[g_current_task].state = TASK_TERMINATED;
28 os_yield();
29 while (1) { } // ここには到達しない
30}
31
32static uint32_t* init_task_stack(uint32_t *stack_top, void (*entry)(void))
33{
34 uint32_t *sp = stack_top;
35 *(--sp) = 0x01000000;
36 *(--sp) = (uint32_t)entry;
37 *(--sp) = (uint32_t)task_exit; // LR: タスクが return したときの戻り先
38 *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
39 // FPU を使ったタスクは例外フレームが拡張される(FP レジスタ込み)。
40 // どちらのフレームで中断したかは EXC_RETURN の値に現れるため、
41 // タスクごとに EXC_RETURN も保存する。初期値は「PSP・基本フレーム」。
42 *(--sp) = 0xFFFFFFFD;
43 *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
44 *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
45 return sp;
46}
47
48void os_init(void)
49{
50 memset(g_tasks, 0, sizeof(g_tasks));
51 g_task_count = 0;
52 g_current_task = -1;
53 g_next_task = -1;
54 g_system_ticks = 0;
55 os_create_task(idle_task, "idle", IDLE_STACK_SIZE);
56}
57
58int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size)
59{
60 if (g_task_count >= MAX_TASKS) return -1;
61 int id = g_task_count;
62 TCB *task = &g_tasks[id];
63 task->id = id;
64 task->state = TASK_READY;
65 task->priority = id;
66 task->entry = entry;
67 task->wake_tick = 0;
68 task->cpu_ticks = 0;
69 strncpy(task->name, name, sizeof(task->name) - 1);
70 if (stack_size == 0) {
71 stack_size = DEFAULT_STACK_SIZE;
72 }
73 uint32_t words = (stack_size + 3) / 4;
74 if (g_stack_used + words > STACK_POOL_SIZE / 4) {
75 return -1; // プールが尽きた。黙って縮めず、失敗として返す
76 }
77 g_stack_used += words;
78 task->stack_size = stack_size;
79 task->stack_base = &g_stack_pool[g_stack_used]; // スタックは上端から下へ伸びる
80 task->sp = init_task_stack(task->stack_base, entry);
81 g_task_count++;
82 return id;
83}
84
85void os_schedule(void)
86{
87 for (int i = 0; i < g_task_count; i++) {
88 if (g_tasks[i].state == TASK_BLOCKED && g_system_ticks >= g_tasks[i].wake_tick) {
89 g_tasks[i].state = TASK_READY;
90 }
91 }
92
93 int next = g_current_task;
94 do {
95 next = (next + 1) % g_task_count;
96 if (g_tasks[next].state == TASK_READY || g_tasks[next].state == TASK_RUNNING) break;
97 } while (next != g_current_task);
98
99 if (next != g_current_task || g_current_task < 0) {
100 if (g_current_task >= 0 && g_tasks[g_current_task].state == TASK_RUNNING) {
101 g_tasks[g_current_task].state = TASK_READY;
102 }
103 g_tasks[next].state = TASK_RUNNING;
104 g_next_task = next;
105 SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
106 }
107}
108
109void os_yield(void) { os_schedule(); }
110void os_sleep(uint32_t ms) {
111 if (g_current_task > 0) {
112 g_tasks[g_current_task].state = TASK_BLOCKED;
113 g_tasks[g_current_task].wake_tick = g_system_ticks + ms;
114 os_schedule();
115 }
116}
117void os_suspend_task(int id) {
118 if (id > 0 && id < g_task_count) {
119 g_tasks[id].state = TASK_SUSPENDED;
120 if (id == g_current_task) os_schedule();
121 }
122}
123void os_resume_task(int id) {
124 if (id > 0 && id < g_task_count && g_tasks[id].state == TASK_SUSPENDED) {
125 g_tasks[id].state = TASK_READY;
126 }
127}
128void os_terminate_task(int id) {
129 if (id > 0 && id < g_task_count) {
130 g_tasks[id].state = TASK_TERMINATED;
131 if (id == g_current_task) os_schedule();
132 }
133}
134
135uint32_t os_get_tick(void) { return g_system_ticks; }
136int os_get_task_count(void) { return g_task_count; }
137const char* os_get_task_name(int id) { return (id >= 0 && id < g_task_count) ? g_tasks[id].name : ""; }
138TaskState os_get_task_state(int id) { return (id >= 0 && id < g_task_count) ? g_tasks[id].state : TASK_TERMINATED; }
139int os_get_current_task(void) { return g_current_task; }
140
141void os_start(void)
142{
143 NVIC_SetPriority(PendSV_IRQn, 0xFF);
144 SysTick_Config(SystemCoreClock / 1000 * TIME_SLICE_MS);
145 g_current_task = -1;
146 g_next_task = 0;
147 // Do NOT switch CONTROL to PSP here — PSP is not yet initialized.
148 // PendSV returns with EXC_RETURN=0xFFFFFFFD, which automatically
149 // switches thread mode to PSP after loading the first task's context.
150 SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
151 while (1) { }
152}
153
154extern "C" void SysTick_Handler(void) {
155 // 実際に経過した時間を、いま走っていたタスクの取り分として計上する。
156 // os_schedule() 側で数えると os_yield() の回数まで混ざってしまう。
157 if (g_current_task >= 0) {
158 g_tasks[g_current_task].cpu_ticks += TIME_SLICE_MS;
159 }
160 g_system_ticks += TIME_SLICE_MS;
161 os_schedule();
162}
163
164extern "C" __attribute__((naked)) void PendSV_Handler(void)
165{
166 __asm volatile (
167 " MRS R0, PSP \n"
168 " LDR R1, =g_current_task \n"
169 " LDR R2, [R1] \n"
170 " CMP R2, #0 \n"
171 " BLT _skip_save \n"
172 // FPU を使って中断したタスク(EXC_RETURN の bit4 が 0)は、
173 // ハードウェアが積まない S16-S31 もここで退避する
174 " TST LR, #0x10 \n"
175 " IT EQ \n"
176 " VSTMDBEQ R0!, {S16-S31} \n"
177 // R4-R11 と EXC_RETURN(LR) を保存する
178 " STMDB R0!, {R4-R11, LR} \n"
179 " LDR R3, =g_tasks \n"
180 " LDR R4, =g_tcb_size \n"
181 " LDR R4, [R4] \n"
182 " MUL R2, R2, R4 \n"
183 " ADD R3, R3, R2 \n"
184 " STR R0, [R3] \n"
185 "_skip_save: \n"
186 " LDR R1, =g_next_task \n"
187 " LDR R2, [R1] \n"
188 " LDR R1, =g_current_task \n"
189 " STR R2, [R1] \n"
190 " LDR R3, =g_tasks \n"
191 " LDR R4, =g_tcb_size \n"
192 " LDR R4, [R4] \n"
193 " MUL R2, R2, R4 \n"
194 " ADD R3, R3, R2 \n"
195 " LDR R0, [R3] \n"
196 " LDMIA R0!, {R4-R11, LR} \n"
197 // FPU を使って中断していたタスクなら S16-S31 も復元する
198 " TST LR, #0x10 \n"
199 " IT EQ \n"
200 " VLDMIAEQ R0!, {S16-S31} \n"
201 " MSR PSP, R0 \n"
202 " BX LR \n"
203 );
204}
205
206// CPU使用率を取得(パーセント)
207// 直前にこの関数を呼んだ時点からの差分で計算する。
208// ps のようにタスクごとにループで呼ぶため、基準時刻もタスクごとに持つ。
209int os_get_cpu_usage(int task_id)
210{
211 if (task_id < 0 || task_id >= g_task_count) {
212 return -1;
213 }
214
215 static uint32_t last_total[MAX_TASKS] = {0};
216 static uint32_t last_task_ticks[MAX_TASKS] = {0};
217
218 uint32_t total = g_system_ticks;
219 uint32_t delta_total = total - last_total[task_id];
220
221 if (delta_total == 0) {
222 return 0;
223 }
224
225 uint32_t delta_task = g_tasks[task_id].cpu_ticks - last_task_ticks[task_id];
226
227 last_total[task_id] = total;
228 last_task_ticks[task_id] = g_tasks[task_id].cpu_ticks;
229
230 return (delta_task * 100) / delta_total;
231}
src/os_svc.cpp — 95 lines
1// os_svc.cpp
2//
3// 本書 応用編 第2章 2.5「SVC_Handler の実装」。
4//
5// SVC 番号は命令そのものに埋め込まれているので、ハンドラからは
6// 「スタックに積まれた戻り先 PC の 2 バイト手前」を読んで取り出す。
7
8#include <Arduino.h>
9#include "os_syscall.h"
10#include "os_kernel.h"
11
12// ---- カーネル側のシステムコール実体 ----
13//
14// 本文では svc_dispatch_c から呼ぶ形だけを示しているので、中身はここで書く。
15// いずれも「ユーザーが直接触れないもの」をカーネルが代行する形になっている。
16
17void sys_putchar(char c)
18{
19 Serial.write(c);
20}
21
22void sys_yield(void)
23{
24 os_yield();
25}
26
27void sys_sleep(uint32_t ms)
28{
29 os_sleep(ms);
30}
31
32void sys_exit(int code)
33{
34 (void)code;
35 os_terminate_task(os_get_current_task());
36}
37
38int sys_getpid(void)
39{
40 return os_get_current_task();
41}
42
43// ---- ディスパッチャ ----
44
45// 引数 sp は SVC を発行したコードのスタックフレーム先頭を指す
46extern "C" void svc_dispatch_c(uint32_t *sp)
47{
48 // sp[0]=R0, sp[1]=R1, sp[2]=R2, sp[3]=R3
49 // sp[6]=元の PC (SVC の次の命令)
50 uint8_t *pc = (uint8_t *)sp[6];
51 uint8_t svc_num = pc[-2]; // svc 命令の下位バイト = 番号
52
53 uint32_t arg0 = sp[0];
54 uint32_t ret = 0;
55
56 switch (svc_num) {
57 case SYS_putchar:
58 sys_putchar((char)arg0);
59 break;
60 case SYS_yield:
61 sys_yield();
62 break;
63 case SYS_sleep:
64 sys_sleep(arg0); // ms 単位
65 break;
66 case SYS_exit:
67 sys_exit((int)arg0);
68 break;
69 case SYS_getpid:
70 ret = (uint32_t)sys_getpid();
71 break;
72 default:
73 ret = (uint32_t)-1; // ENOSYS 相当
74 break;
75 }
76
77 // 戻り値は R0 (sp[0]) に書き戻す
78 sp[0] = ret;
79}
80
81// ---- 例外ハンドラ本体 ----
82//
83// SVC が特権スレッドから来たか非特権スレッドから来たかで、引数が積まれている
84// スタックが MSP か PSP かが変わる。EXC_RETURN の bit2 で見分ける。
85
86extern "C" __attribute__((naked)) void SVC_Handler(void)
87{
88 __asm volatile (
89 "TST lr, #4 \n" // EXC_RETURN bit2: 0=MSP, 1=PSP
90 "ITE EQ \n"
91 "MRSEQ r0, MSP \n" // bit2==0 なら MSP
92 "MRSNE r0, PSP \n" // bit2==1 なら PSP
93 "B svc_dispatch_c \n" // R0 = スタックポインタ
94 );
95}
On the board#
[A] and [B] keep alternating, each character going out through svc (Supervisor Call, the instruction user code uses to call into the OS). The [probe] lines at the start scrolled by before the monitor opened.#
Pitfalls#
(Collected from reader questions — ask via GitHub Issues.)