Chapter 5: An Interactive Shell#

Sample code#

  • Directory: docs/os-on-arduino/code/05_shell

uv run pio run -d 05_shell -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 — 96 lines

Open on GitHub

 1/**
 2 * Chapter 5: Interactive Shell Demo
 3 *
 4 * Features:
 5 * - ps: List all tasks with state and CPU usage
 6 * - kill N: Suspend task N
 7 * - exec N: Resume suspended task N
 8 * - info: Show system information
 9 * - reboot: Reboot the system
10 */
11
12#include <Arduino.h>
13#include "os_kernel.h"
14#include "shell.h"
15#include "Arduino_LED_Matrix.h"
16
17// Arduino UNO R4 WiFi は基板上に "L" と印字された LED を持っています。
18// 追加部品なしで動かせるよう、これを主 LED として使います。
19#define LED1_PIN LED_BUILTIN
20
21// 2個目の LED も外付けせず、基板内蔵の 12×8 LED マトリクスで代用します。
22// 左半分を LED2 として丸ごと点灯・消灯します。
23// マトリクス制御の仕組みは第7章で学びます。ここではライブラリに任せます。
24static ArduinoLEDMatrix matrix;
25static uint8_t matrix_frame[8][12];
26
27static void matrix_led(int led, bool on)
28{
29    int col_start = (led == 3) ? 6 : 0;
30    for (int row = 0; row < 8; row++) {
31        for (int col = col_start; col < col_start + 6; col++) {
32            matrix_frame[row][col] = on ? 1 : 0;
33        }
34    }
35    matrix.renderBitmap(matrix_frame, 8, 12);
36}
37
38void task_led1(void)
39{
40    pinMode(LED1_PIN, OUTPUT);
41    while (1) {
42        digitalWrite(LED1_PIN, HIGH);
43        os_sleep(200);
44        digitalWrite(LED1_PIN, LOW);
45        os_sleep(200);
46    }
47}
48
49void task_led2(void)
50{
51    while (1) {
52        matrix_led(2, true);
53        os_sleep(500);
54        matrix_led(2, false);
55        os_sleep(500);
56    }
57}
58
59void task_heartbeat(void)
60{
61    int count = 0;
62    while (1) {
63        // Silent heartbeat - just keeps running
64        count++;
65        os_sleep(1000);
66    }
67}
68
69void setup()
70{
71    Serial.begin(115200);
72    while (!Serial) { }
73
74    Serial.println("=== Interactive Shell Demo ===");
75    Serial.println();
76
77    // LEDマトリクスを初期化(LED2 の代用)
78    matrix.begin();
79
80    os_init();
81
82    os_create_task(task_led1, "LED1", 512);
83    os_create_task(task_led2, "LED2", 512);
84    os_create_task(task_heartbeat, "Heartbeat", 256);
85    os_create_task(shell_task, "Shell", 1024);
86
87    Serial.println("Tasks created. Starting OS...");
88    Serial.println();
89
90    os_start();
91}
92
93void loop()
94{
95    // Never reached
96}
include/os_kernel.h — 79 lines

Open on GitHub

 1/**
 2 * os_kernel.h - Mini OS Kernel Header
 3 * Chapter 5: Interactive Shell
 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 1024
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
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);
51void os_sleep(uint32_t ms);
52
53// Task control API (for shell)
54void os_suspend_task(int id);
55void os_resume_task(int id);
56void os_terminate_task(int id);
57
58// Task info API
59int os_get_task_count(void);
60const char* os_get_task_name(int id);
61TaskState os_get_task_state(int id);
62int os_get_current_task(void);
63int os_get_cpu_usage(int task_id);
64uint32_t os_get_tick(void);
65uint32_t os_get_cpu_ticks(int id);
66uint32_t os_get_stack_used(int id);
67
68extern TCB g_tasks[MAX_TASKS];
69extern volatile int g_task_count;
70extern volatile int g_current_task;
71extern volatile int g_next_task;
72extern volatile uint32_t g_system_ticks;
73extern const uint32_t g_tcb_size;
74
75#ifdef __cplusplus
76}
77#endif
78
79#endif
include/shell.h — 23 lines

Open on GitHub

 1/**
 2 * shell.h - Interactive Shell Header
 3 * Chapter 5: Interactive Shell
 4 */
 5
 6#ifndef SHELL_H
 7#define SHELL_H
 8
 9#ifdef __cplusplus
10extern "C" {
11#endif
12
13// Shell task entry point
14void shell_task(void);
15
16// Process a single command
17void shell_process_command(char *cmd);
18
19#ifdef __cplusplus
20}
21#endif
22
23#endif
src/os_kernel.cpp — 281 lines

Open on GitHub

  1/**
  2 * os_kernel.cpp - Mini OS Kernel Implementation
  3 * Chapter 5: Interactive Shell
  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
 16// TCB size for PendSV assembly (compiler may use -fshort-enums, changing sizeof)
 17const uint32_t g_tcb_size = sizeof(TCB);
 18
 19static uint32_t g_stack_pool[STACK_POOL_SIZE / 4];
 20static uint32_t g_stack_used;   // 切り出し済みのワード数
 21
 22static void idle_task(void)
 23{
 24    while (1) {
 25        __WFI();
 26    }
 27}
 28
 29// タスク関数が return したときの受け皿。例外フレームの LR スロットは
 30// ここを指す (第3章 3.4)。EXC_RETURN を置く場所ではない。
 31static void task_exit(void)
 32{
 33    g_tasks[g_current_task].state = TASK_TERMINATED;
 34    os_yield();
 35    while (1) { }  // ここには到達しない
 36}
 37
 38static uint32_t* init_task_stack(uint32_t *stack_top, void (*entry)(void))
 39{
 40    uint32_t *sp = stack_top;
 41
 42    *(--sp) = 0x01000000;
 43    *(--sp) = (uint32_t)entry;
 44    *(--sp) = (uint32_t)task_exit;  // LR: タスクが return したときの戻り先
 45    *(--sp) = 0;
 46    *(--sp) = 0;
 47    *(--sp) = 0;
 48    *(--sp) = 0;
 49    *(--sp) = 0;
 50
 51    *(--sp) = 0;
 52    *(--sp) = 0;
 53    *(--sp) = 0;
 54    *(--sp) = 0;
 55    *(--sp) = 0;
 56    *(--sp) = 0;
 57    *(--sp) = 0;
 58    *(--sp) = 0;
 59
 60    return sp;
 61}
 62
 63void os_init(void)
 64{
 65    memset(g_tasks, 0, sizeof(g_tasks));
 66    g_task_count = 0;
 67    g_current_task = -1;
 68    g_next_task = -1;
 69    g_system_ticks = 0;
 70
 71    os_create_task(idle_task, "idle", IDLE_STACK_SIZE);
 72}
 73
 74int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size)
 75{
 76    if (g_task_count >= MAX_TASKS) return -1;
 77
 78    int id = g_task_count;
 79    TCB *task = &g_tasks[id];
 80
 81    task->id = id;
 82    task->state = TASK_READY;
 83    task->priority = id;
 84    task->entry = entry;
 85    task->wake_tick = 0;
 86    task->cpu_ticks = 0;
 87    strncpy(task->name, name, sizeof(task->name) - 1);
 88
 89    if (stack_size == 0) {
 90        stack_size = DEFAULT_STACK_SIZE;
 91    }
 92    uint32_t words = (stack_size + 3) / 4;
 93    if (g_stack_used + words > STACK_POOL_SIZE / 4) {
 94        return -1;          // プールが尽きた。黙って縮めず、失敗として返す
 95    }
 96    g_stack_used += words;
 97    task->stack_size = stack_size;
 98    task->stack_base = &g_stack_pool[g_stack_used];   // スタックは上端から下へ伸びる
 99    task->sp = init_task_stack(task->stack_base, entry);
100
101    g_task_count++;
102    return id;
103}
104
105void os_schedule(void)
106{
107    for (int i = 0; i < g_task_count; i++) {
108        if (g_tasks[i].state == TASK_BLOCKED) {
109            if (g_system_ticks >= g_tasks[i].wake_tick) {
110                g_tasks[i].state = TASK_READY;
111            }
112        }
113    }
114
115    int next = g_current_task;
116    do {
117        next = (next + 1) % g_task_count;
118        if (g_tasks[next].state == TASK_READY ||
119            g_tasks[next].state == TASK_RUNNING) {
120            break;
121        }
122    } while (next != g_current_task);
123
124    if (next != g_current_task || g_current_task < 0) {
125        if (g_current_task >= 0) {
126            if (g_tasks[g_current_task].state == TASK_RUNNING) {
127                g_tasks[g_current_task].state = TASK_READY;
128            }
129        }
130        g_tasks[next].state = TASK_RUNNING;
131        g_next_task = next;
132        SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
133    }
134}
135
136void os_yield(void) { os_schedule(); }
137
138void os_sleep(uint32_t ms)
139{
140    if (g_current_task > 0) {
141        g_tasks[g_current_task].state = TASK_BLOCKED;
142        g_tasks[g_current_task].wake_tick = g_system_ticks + ms;
143        os_schedule();
144    }
145}
146
147void os_suspend_task(int id)
148{
149    if (id > 0 && id < g_task_count) {
150        g_tasks[id].state = TASK_SUSPENDED;
151        if (id == g_current_task) {
152            os_schedule();
153        }
154    }
155}
156
157void os_resume_task(int id)
158{
159    if (id > 0 && id < g_task_count) {
160        if (g_tasks[id].state == TASK_SUSPENDED) {
161            g_tasks[id].state = TASK_READY;
162        }
163    }
164}
165
166void os_terminate_task(int id)
167{
168    if (id > 0 && id < g_task_count) {
169        g_tasks[id].state = TASK_TERMINATED;
170        if (id == g_current_task) {
171            os_schedule();
172        }
173    }
174}
175
176uint32_t os_get_tick(void) { return g_system_ticks; }
177int os_get_task_count(void) { return g_task_count; }
178const char* os_get_task_name(int id) {
179    if (id < 0 || id >= g_task_count) return "";
180    return g_tasks[id].name;
181}
182TaskState os_get_task_state(int id) {
183    if (id < 0 || id >= g_task_count) return TASK_TERMINATED;
184    return g_tasks[id].state;
185}
186int os_get_current_task(void) { return g_current_task; }
187uint32_t os_get_cpu_ticks(int id) {
188    if (id < 0 || id >= g_task_count) return 0;
189    return g_tasks[id].cpu_ticks;
190}
191uint32_t os_get_stack_used(int id) {
192    if (id < 0 || id >= g_task_count) return 0;
193    uint32_t *base = g_tasks[id].stack_base - (g_tasks[id].stack_size / 4);
194    uint32_t *sp = g_tasks[id].sp;
195    return (g_tasks[id].stack_base - sp) * 4;
196}
197
198void os_start(void)
199{
200    NVIC_SetPriority(PendSV_IRQn, 0xFF);
201    SysTick_Config(SystemCoreClock / 1000 * TIME_SLICE_MS);
202
203    g_current_task = -1;
204    g_next_task = 0;
205
206    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
207
208    while (1) { }
209}
210
211extern "C" void SysTick_Handler(void)
212{
213    // 実際に経過した時間を、いま走っていたタスクの取り分として計上する。
214    // os_schedule() 側で数えると os_yield() の回数まで混ざってしまう。
215    if (g_current_task >= 0) {
216        g_tasks[g_current_task].cpu_ticks += TIME_SLICE_MS;
217    }
218    g_system_ticks += TIME_SLICE_MS;
219    os_schedule();
220}
221
222extern "C" __attribute__((naked)) void PendSV_Handler(void)
223{
224    __asm volatile (
225        "   MRS     R0, PSP              \n"
226        "   LDR     R1, =g_current_task  \n"
227        "   LDR     R2, [R1]             \n"
228        "   CMP     R2, #0               \n"
229        "   BLT     _skip_save           \n"
230        // Save R4-R11 only when there is a valid current task
231        "   STMDB   R0!, {R4-R11}        \n"
232        "   LDR     R3, =g_tasks         \n"
233        "   LDR     R4, =g_tcb_size      \n"
234        "   LDR     R4, [R4]             \n"
235        "   MUL     R2, R2, R4           \n"
236        "   ADD     R3, R3, R2           \n"
237        "   STR     R0, [R3]             \n"
238        "_skip_save:                     \n"
239        "   LDR     R1, =g_next_task     \n"
240        "   LDR     R2, [R1]             \n"
241        "   LDR     R1, =g_current_task  \n"
242        "   STR     R2, [R1]             \n"
243        "   LDR     R3, =g_tasks         \n"
244        "   LDR     R4, =g_tcb_size      \n"
245        "   LDR     R4, [R4]             \n"
246        "   MUL     R2, R2, R4           \n"
247        "   ADD     R3, R3, R2           \n"
248        "   LDR     R0, [R3]             \n"
249        "   LDMIA   R0!, {R4-R11}        \n"
250        "   MSR     PSP, R0              \n"
251        "   LDR     LR, =0xFFFFFFFD      \n"
252        "   BX      LR                   \n"
253    );
254}
255
256// CPU使用率を取得(パーセント)
257// 直前にこの関数を呼んだ時点からの差分で計算する。
258// ps のようにタスクごとにループで呼ぶため、基準時刻もタスクごとに持つ。
259int os_get_cpu_usage(int task_id)
260{
261    if (task_id < 0 || task_id >= g_task_count) {
262        return -1;
263    }
264
265    static uint32_t last_total[MAX_TASKS] = {0};
266    static uint32_t last_task_ticks[MAX_TASKS] = {0};
267
268    uint32_t total = g_system_ticks;
269    uint32_t delta_total = total - last_total[task_id];
270
271    if (delta_total == 0) {
272        return 0;
273    }
274
275    uint32_t delta_task = g_tasks[task_id].cpu_ticks - last_task_ticks[task_id];
276
277    last_total[task_id] = total;
278    last_task_ticks[task_id] = g_tasks[task_id].cpu_ticks;
279
280    return (delta_task * 100) / delta_total;
281}
src/shell.cpp — 186 lines

Open on GitHub

  1/**
  2 * shell.cpp - Interactive Shell Implementation
  3 * Chapter 5: Interactive Shell
  4 */
  5
  6#include "shell.h"
  7#include "os_kernel.h"
  8#include <Arduino.h>
  9#include <string.h>
 10
 11#define CMD_BUF_SIZE 64
 12
 13static char cmd_buf[CMD_BUF_SIZE];
 14static int cmd_pos = 0;
 15
 16static const char* state_to_str(TaskState state)
 17{
 18    switch (state) {
 19        case TASK_READY:      return "READY";
 20        case TASK_RUNNING:    return "RUNNING";
 21        case TASK_BLOCKED:    return "BLOCKED";
 22        case TASK_SUSPENDED:  return "SUSPEND";
 23        case TASK_TERMINATED: return "TERM";
 24        default:              return "?";
 25    }
 26}
 27
 28static void cmd_help(void)
 29{
 30    Serial.println("Available commands:");
 31    Serial.println("  ps      - List all tasks");
 32    Serial.println("  kill N  - Suspend task N");
 33    Serial.println("  exec N  - Resume task N");
 34    Serial.println("  info    - System information");
 35    Serial.println("  reboot  - Reboot system");
 36    Serial.println("  help    - Show this help");
 37}
 38
 39static void cmd_ps(void)
 40{
 41    Serial.println();
 42    Serial.println("ID  NAME           STATE      CPU%");
 43    Serial.println("--  ----           -----      ----");
 44
 45    for (int i = 0; i < os_get_task_count(); i++) {
 46        Serial.print(i);
 47        Serial.print("   ");
 48
 49        const char *name = os_get_task_name(i);
 50        Serial.print(name);
 51        for (int j = strlen(name); j < 15; j++) Serial.print(" ");
 52
 53        Serial.print(state_to_str(os_get_task_state(i)));
 54        for (int j = strlen(state_to_str(os_get_task_state(i))); j < 10; j++) Serial.print(" ");
 55
 56        Serial.print(os_get_cpu_usage(i));
 57        Serial.println("%");
 58    }
 59    Serial.println();
 60}
 61
 62static void cmd_kill(int id)
 63{
 64    if (id <= 0) {
 65        Serial.println("Error: Cannot kill idle task (ID 0)");
 66        return;
 67    }
 68    if (id >= os_get_task_count()) {
 69        Serial.println("Error: Invalid task ID");
 70        return;
 71    }
 72
 73    os_suspend_task(id);
 74    Serial.print("Task ");
 75    Serial.print(id);
 76    Serial.print(" (");
 77    Serial.print(os_get_task_name(id));
 78    Serial.println(") suspended.");
 79}
 80
 81static void cmd_exec(int id)
 82{
 83    if (id <= 0 || id >= os_get_task_count()) {
 84        Serial.println("Error: Invalid task ID");
 85        return;
 86    }
 87
 88    os_resume_task(id);
 89    Serial.print("Task ");
 90    Serial.print(id);
 91    Serial.print(" (");
 92    Serial.print(os_get_task_name(id));
 93    Serial.println(") resumed.");
 94}
 95
 96static void cmd_info(void)
 97{
 98    Serial.println();
 99    Serial.println("=== System Information ===");
100    Serial.print("System Ticks:  ");
101    Serial.println(os_get_tick());
102    Serial.print("Uptime:        ");
103    Serial.print(os_get_tick() / 1000);
104    Serial.println(" s");
105    Serial.print("Task Count:    ");
106    Serial.println(os_get_task_count());
107    Serial.print("Time Slice:    ");
108    Serial.print(TIME_SLICE_MS);
109    Serial.println(" ms");
110    Serial.println();
111}
112
113static void cmd_reboot(void)
114{
115    Serial.println("Rebooting...");
116    delay(100);
117    NVIC_SystemReset();
118}
119
120void shell_process_command(char *cmd)
121{
122    // Trim whitespace
123    while (*cmd == ' ') cmd++;
124    char *end = cmd + strlen(cmd) - 1;
125    while (end > cmd && *end == ' ') *end-- = '\0';
126
127    if (strlen(cmd) == 0) return;
128
129    // Parse command
130    if (strcmp(cmd, "help") == 0 || strcmp(cmd, "?") == 0) {
131        cmd_help();
132    } else if (strcmp(cmd, "ps") == 0) {
133        cmd_ps();
134    } else if (strncmp(cmd, "kill ", 5) == 0) {
135        int id = atoi(cmd + 5);
136        cmd_kill(id);
137    } else if (strncmp(cmd, "exec ", 5) == 0) {
138        int id = atoi(cmd + 5);
139        cmd_exec(id);
140    } else if (strcmp(cmd, "info") == 0) {
141        cmd_info();
142    } else if (strcmp(cmd, "reboot") == 0) {
143        cmd_reboot();
144    } else {
145        Serial.print("Unknown command: ");
146        Serial.println(cmd);
147        Serial.println("Type 'help' for available commands.");
148    }
149}
150
151void shell_task(void)
152{
153    Serial.println();
154    Serial.println("Mini OS Shell");
155    Serial.println("Type 'help' for commands.");
156
157    while (1) {
158        Serial.print("> ");
159
160        cmd_pos = 0;
161        memset(cmd_buf, 0, CMD_BUF_SIZE);
162
163        while (1) {
164            if (Serial.available()) {
165                char ch = Serial.read();
166
167                if (ch == '\r' || ch == '\n') {
168                    Serial.println();
169                    break;
170                } else if (ch == '\b' || ch == 127) {
171                    if (cmd_pos > 0) {
172                        cmd_pos--;
173                        cmd_buf[cmd_pos] = '\0';
174                        Serial.print("\b \b");
175                    }
176                } else if (cmd_pos < CMD_BUF_SIZE - 1) {
177                    cmd_buf[cmd_pos++] = ch;
178                    Serial.print(ch);
179                }
180            }
181            os_sleep(10);
182        }
183
184        shell_process_command(cmd_buf);
185    }
186}

On the board#

kill 1 stops task LED1 and the L LED stops blinking (it stays as it was); exec 1 brings it back. The left half of the matrix (LED2) keeps blinking every 0.5 s throughout. (Filmed with a webcam at 30 fps.)

In the simulator#

Without the board, the same firmware running in the simulator looks like this.

../_images/sim_ch05_kill.gif

With L (LED1) and the left half of the matrix (LED2) blinking, kill 1 from the shell stops only L, and exec 1 starts it again. The left half never stops. (Recorded in the simulator.)#

Pitfalls#

(Collected from reader questions — ask via GitHub Issues.)