Chapter 8: A Tiny Interpreter#

Sample code#

  • Directory: docs/os-on-arduino/code/08_interpreter

uv run pio run -d 08_interpreter -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 — 53 lines

Open on GitHub

 1// 07_interpreter.ino
 2// 簡易インタプリタのデモ
 3
 4#include "os_kernel.h"
 5#include "shell.h"
 6#include "interpreter.h"
 7#include "os_display.h"
 8
 9// Arduino UNO R4 WiFi は基板上に "L" と印字された LED を持っています。
10// 追加部品なしで動かせるよう、これを主 LED として使います。
11#define LED_PIN LED_BUILTIN
12
13void task_led(void)
14{
15    pinMode(LED_PIN, OUTPUT);
16    while (1) {
17        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
18        os_sleep(500);
19    }
20}
21
22void setup()
23{
24    Serial.begin(115200);
25    while (!Serial) { }
26
27    Serial.println("=== Motion Interpreter Demo ===");
28    Serial.println("Type 'run' to enter script mode.");
29    Serial.println("Type 'run <command>' for one-line execution.\n");
30
31    Serial.println("Available commands:");
32    Serial.println("  FORWARD <dist>      - Move forward");
33    Serial.println("  BACKWARD <dist>      - Move backward");
34    Serial.println("  TURN <angle>     - Turn");
35    Serial.println("  SERVO <ch> <deg>  - Set servo angle");
36    Serial.println("  LED <pin> <0/1> - Control LED");
37    Serial.println("  DELAY <ms>        - Delay");
38    Serial.println("  PRINT <text>      - Print message");
39    Serial.println("  LOOP <n>        - Start loop");
40    Serial.println("  END             - End loop");
41    Serial.println();
42
43    interp_init();
44    os_init();
45
46    os_create_task(task_led, "LED", 512);
47    os_create_task(shell_task, "Shell", 1024);
48    os_create_task(display_task, "Display", 512);
49
50    os_start();
51}
52
53void loop() { }
include/interpreter.h — 36 lines

Open on GitHub

 1// interpreter.h
 2
 3#ifndef INTERPRETER_H
 4#define INTERPRETER_H
 5
 6#include <stdint.h>
 7
 8#define MAX_SCRIPT_LINES 32
 9#define MAX_LINE_LENGTH 32
10
11// 実行結果
12typedef enum {
13    EXEC_OK,
14    EXEC_ERROR,
15    EXEC_END
16} ExecResult;
17
18// インタプリタ初期化
19void interp_init(void);
20
21// スクリプトをロード(複数行を改行で区切って渡す)
22int interp_load(const char *script);
23
24// 1行実行
25ExecResult interp_step(void);
26
27// スクリプト全体を実行
28int interp_run(void);
29
30// 実行中かどうか
31bool interp_is_running(void);
32
33// インタプリタタスク
34void interpreter_task(void);
35
36#endif // INTERPRETER_H
include/os_display.h — 18 lines

Open on GitHub

 1// os_display.h
 2
 3#ifndef OS_DISPLAY_H
 4#define OS_DISPLAY_H
 5
 6#include "os_kernel.h"
 7#include "Arduino_LED_Matrix.h"
 8
 9// 表示を初期化
10void display_init(void);
11
12// 表示を更新(定期的に呼ぶ)
13void display_update(void);
14
15// 表示タスク
16void display_task(void);
17
18#endif // OS_DISPLAY_H
include/os_fault.h — 46 lines

Open on GitHub

 1/**
 2 * os_fault.h - Fault Handler Header
 3 * Chapter 6: Memory Protection
 4 */
 5
 6#ifndef OS_FAULT_H
 7#define OS_FAULT_H
 8
 9#include <stdint.h>
10
11#ifdef __cplusplus
12extern "C" {
13#endif
14
15// Fault information structure
16typedef struct {
17    uint32_t r0;
18    uint32_t r1;
19    uint32_t r2;
20    uint32_t r3;
21    uint32_t r12;
22    uint32_t lr;
23    uint32_t pc;
24    uint32_t xpsr;
25} FaultFrame;
26
27// Initialize fault handling
28void fault_init(void);
29
30// Get fault reason string
31const char* fault_get_reason(void);
32
33// Check if a fault occurred
34int fault_occurred(void);
35
36// Clear fault flag
37void fault_clear(void);
38
39// Print pending fault info (call from task context, not from ISR)
40void fault_print_pending(void);
41
42#ifdef __cplusplus
43}
44#endif
45
46#endif
include/os_kernel.h — 75 lines

Open on GitHub

 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_task_cpu_ticks(int task_id);
62uint32_t os_get_tick(void);
63
64extern TCB g_tasks[MAX_TASKS];
65extern volatile int g_task_count;
66extern volatile int g_current_task;
67extern volatile int g_next_task;
68extern volatile uint32_t g_system_ticks;
69extern const uint32_t g_tcb_size;
70
71#ifdef __cplusplus
72}
73#endif
74
75#endif
include/servo_pwm.h — 61 lines

Open on GitHub

 1/**
 2 * Simple PWM-based Servo Control for Arduino Uno R4 WiFi
 3 *
 4 * Since the standard Servo library is not available in PlatformIO for Renesas,
 5 * this provides a simple software PWM implementation.
 6 */
 7
 8#ifndef SERVO_PWM_H
 9#define SERVO_PWM_H
10
11#include <Arduino.h>
12
13class ServoPWM {
14private:
15    int pin;
16    int angle;
17    bool attached;
18
19    void writeMicroseconds(int us) {
20        if (!attached) return;
21        // Standard servo pulse: 500-2500us for 0-180 degrees
22        // Period: 20ms (50Hz)
23        digitalWrite(pin, HIGH);
24        delayMicroseconds(us);
25        digitalWrite(pin, LOW);
26    }
27
28public:
29    ServoPWM() : pin(-1), angle(90), attached(false) {}
30
31    void attach(int p) {
32        pin = p;
33        pinMode(pin, OUTPUT);
34        attached = true;
35    }
36
37    void detach() {
38        attached = false;
39    }
40
41    void write(int ang) {
42        if (!attached) return;
43        angle = constrain(ang, 0, 180);
44        // Map angle (0-180) to pulse width (500-2500us)
45        int us = map(angle, 0, 180, 500, 2500);
46        writeMicroseconds(us);
47    }
48
49    int read() {
50        return angle;
51    }
52
53    // Call this in loop() to maintain servo position
54    void refresh() {
55        if (!attached) return;
56        int us = map(angle, 0, 180, 500, 2500);
57        writeMicroseconds(us);
58    }
59};
60
61#endif // SERVO_PWM_H
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/interpreter.cpp — 288 lines

Open on GitHub

  1// interpreter.cpp
  2
  3#include "interpreter.h"
  4#include "os_kernel.h"
  5#include <Arduino.h>
  6#include <string.h>
  7#include <stdlib.h>
  8#include <Servo.h>
  9
 10// スクリプト格納
 11static char g_script[MAX_SCRIPT_LINES][MAX_LINE_LENGTH];
 12static int g_script_lines = 0;
 13static int g_current_line = 0;
 14static bool g_running = false;
 15
 16// ループスタック
 17#define MAX_LOOP_DEPTH 4
 18static int g_loop_stack[MAX_LOOP_DEPTH];
 19static int g_loop_count[MAX_LOOP_DEPTH];
 20static int g_loop_depth = 0;
 21
 22// ハードウェア
 23static Servo g_servos[2];
 24static bool g_servo_attached[2] = {false, false};
 25
 26// サーボピン
 27#define SERVO_PIN_0 9
 28#define SERVO_PIN_1 10
 29
 30// モーター制御(ダミー実装)
 31static void motor_forward(int distance)
 32{
 33    Serial.print("[Motor] Forward ");
 34    Serial.print(distance);
 35    Serial.println(" units");
 36    // 実際のモーター制御はここに実装
 37    os_sleep(distance * 10);  // 仮の待機
 38}
 39
 40static void motor_backward(int distance)
 41{
 42    Serial.print("[Motor] Backward ");
 43    Serial.print(distance);
 44    Serial.println(" units");
 45    os_sleep(distance * 10);
 46}
 47
 48static void motor_turn(int angle)
 49{
 50    Serial.print("[Motor] Turn ");
 51    Serial.print(angle);
 52    Serial.println(" degrees");
 53    os_sleep(abs(angle) * 10);
 54}
 55
 56// サーボ制御
 57static void servo_set(int channel, int angle)
 58{
 59    if (channel < 0 || channel > 1) {
 60        Serial.println("[Error] Invalid servo channel");
 61        return;
 62    }
 63
 64    // 必要に応じてアタッチ
 65    if (!g_servo_attached[channel]) {
 66        int pin = (channel == 0) ? SERVO_PIN_0 : SERVO_PIN_1;
 67        g_servos[channel].attach(pin);
 68        g_servo_attached[channel] = true;
 69    }
 70
 71    // 角度を制限
 72    if (angle < 0) angle = 0;
 73    if (angle > 180) angle = 180;
 74
 75    g_servos[channel].write(angle);
 76
 77    Serial.print("[Servo] Ch");
 78    Serial.print(channel);
 79    Serial.print(" = ");
 80    Serial.print(angle);
 81    Serial.println(" deg");
 82}
 83
 84// LED制御
 85static void led_set(int pin, int state)
 86{
 87    pinMode(pin, OUTPUT);
 88    digitalWrite(pin, state ? HIGH : LOW);
 89
 90    Serial.print("[LED] Pin ");
 91    Serial.print(pin);
 92    Serial.print(" = ");
 93    Serial.println(state ? "ON" : "OFF");
 94}
 95
 96// 空白をスキップ
 97static const char* skip_spaces(const char *cursor)
 98{
 99    while (*cursor == ' ' || *cursor == '\t') cursor++;
100    return cursor;
101}
102
103// 数値を解析
104static int parse_int(const char **cursor_ptr)
105{
106    const char *cursor = skip_spaces(*cursor_ptr);
107    int sign = 1;
108    int value = 0;
109
110    if (*cursor == '-') {
111        sign = -1;
112        cursor++;
113    }
114
115    while (*cursor >= '0' && *cursor <= '9') {
116        value = value * 10 + (*cursor - '0');
117        cursor++;
118    }
119
120    *cursor_ptr = cursor;
121    return sign * value;
122}
123
124// 1行を実行
125static ExecResult execute_line(const char *line)
126{
127    const char *cursor = skip_spaces(line);
128
129    // 空行やコメント
130    if (*cursor == '\0' || *cursor == '#') {
131        return EXEC_OK;
132    }
133
134    // コマンドを取得
135    char cmd[16] = {0};
136    int i = 0;
137    while (*cursor && *cursor != ' ' && *cursor != '\t' && i < 15) {
138        cmd[i++] = *cursor++;
139    }
140
141    // コマンド実行
142    if (strcmp(cmd, "FORWARD") == 0) {
143        int dist = parse_int(&cursor);
144        motor_forward(dist);
145    }
146    else if (strcmp(cmd, "BACKWARD") == 0) {
147        int dist = parse_int(&cursor);
148        motor_backward(dist);
149    }
150    else if (strcmp(cmd, "TURN") == 0) {
151        int angle = parse_int(&cursor);
152        motor_turn(angle);
153    }
154    else if (strcmp(cmd, "SERVO") == 0) {
155        int ch = parse_int(&cursor);
156        int angle = parse_int(&cursor);
157        servo_set(ch, angle);
158    }
159    else if (strcmp(cmd, "LED") == 0) {
160        int pin = parse_int(&cursor);
161        int state = parse_int(&cursor);
162        led_set(pin, state);
163    }
164    else if (strcmp(cmd, "DELAY") == 0) {
165        int ms = parse_int(&cursor);
166        Serial.print("[Delay] ");
167        Serial.print(ms);
168        Serial.println(" ms");
169        os_sleep(ms);
170    }
171    else if (strcmp(cmd, "PRINT") == 0) {
172        cursor = skip_spaces(cursor);
173        Serial.print("[Print] ");
174        Serial.println(cursor);
175    }
176    else if (strcmp(cmd, "LOOP") == 0) {
177        if (g_loop_depth >= MAX_LOOP_DEPTH) {
178            Serial.println("[Error] Loop too deep");
179            return EXEC_ERROR;
180        }
181        int count = parse_int(&cursor);
182        g_loop_stack[g_loop_depth] = g_current_line;
183        g_loop_count[g_loop_depth] = count;
184        g_loop_depth++;
185    }
186    else if (strcmp(cmd, "END") == 0) {
187        if (g_loop_depth <= 0) {
188            Serial.println("[Error] END without LOOP");
189            return EXEC_ERROR;
190        }
191        g_loop_count[g_loop_depth - 1]--;
192        if (g_loop_count[g_loop_depth - 1] > 0) {
193            // ループ継続
194            g_current_line = g_loop_stack[g_loop_depth - 1];
195        } else {
196            // ループ終了
197            g_loop_depth--;
198        }
199    }
200    else {
201        Serial.print("[Error] Unknown command: ");
202        Serial.println(cmd);
203        return EXEC_ERROR;
204    }
205
206    return EXEC_OK;
207}
208
209void interp_init(void)
210{
211    g_script_lines = 0;
212    g_current_line = 0;
213    g_running = false;
214    g_loop_depth = 0;
215}
216
217int interp_load(const char *script)
218{
219    interp_init();
220
221    const char *cursor = script;
222    int line = 0;
223
224    while (*cursor && line < MAX_SCRIPT_LINES) {
225        int i = 0;
226        while (*cursor && *cursor != '\n' && i < MAX_LINE_LENGTH - 1) {
227            g_script[line][i++] = *cursor++;
228        }
229        g_script[line][i] = '\0';
230
231        // 空行でなければカウント
232        if (i > 0) {
233            line++;
234        }
235
236        if (*cursor == '\n') cursor++;
237    }
238
239    g_script_lines = line;
240    g_current_line = 0;
241
242    Serial.print("[Interpreter] Loaded ");
243    Serial.print(g_script_lines);
244    Serial.println(" lines");
245
246    return g_script_lines;
247}
248
249ExecResult interp_step(void)
250{
251    if (g_current_line >= g_script_lines) {
252        g_running = false;
253        return EXEC_END;
254    }
255
256    g_running = true;
257    ExecResult result = execute_line(g_script[g_current_line]);
258    g_current_line++;
259
260    return result;
261}
262
263int interp_run(void)
264{
265    Serial.println("\n[Interpreter] Starting script...");
266
267    int executed = 0;
268    while (g_current_line < g_script_lines) {
269        ExecResult result = interp_step();
270        if (result == EXEC_ERROR) {
271            Serial.println("[Interpreter] Script error, aborting");
272            return -1;
273        }
274        executed++;
275    }
276
277    Serial.print("[Interpreter] Script completed: ");
278    Serial.print(executed);
279    Serial.println(" commands executed");
280
281    g_running = false;
282    return executed;
283}
284
285bool interp_is_running(void)
286{
287    return g_running;
288}
src/os_display.cpp — 162 lines

Open on GitHub

  1// os_display.cpp
  2
  3#include "os_display.h"
  4#include <Arduino.h>
  5
  6// LEDマトリクスオブジェクト
  7static ArduinoLEDMatrix matrix;
  8
  9// フレームバッファ(8行×12列)
 10static uint8_t frame[8][12];
 11
 12// CPU負荷履歴(12サンプル)
 13static uint8_t cpu_history[12];
 14static int history_index = 0;
 15
 16// タスク実行履歴(4タスク × 8サンプル)
 17#define TASK_HISTORY_LEN 8
 18static int task_history[MAX_TASKS][TASK_HISTORY_LEN];
 19static int task_history_index = 0;
 20
 21void display_init(void)
 22{
 23    matrix.begin();
 24
 25    // フレームバッファをクリア
 26    memset(frame, 0, sizeof(frame));
 27    memset(cpu_history, 0, sizeof(cpu_history));
 28    memset(task_history, -1, sizeof(task_history));
 29}
 30
 31// CPU負荷をパーセントから高さ(0-4)に変換
 32static int cpu_to_height(int cpu_percent)
 33{
 34    if (cpu_percent >= 80) return 4;
 35    if (cpu_percent >= 60) return 3;
 36    if (cpu_percent >= 40) return 2;
 37    if (cpu_percent >= 20) return 1;
 38    return 0;
 39}
 40
 41// フレームバッファにCPU負荷グラフを描画
 42static void draw_cpu_graph(void)
 43{
 44    // 上半分(行0-3)にCPU負荷を描画
 45    for (int col = 0; col < 12; col++) {
 46        int height = cpu_to_height(cpu_history[col]);
 47
 48        for (int row = 0; row < 4; row++) {
 49            // 下から上に向かって描画(行3が底、行0が頂上)
 50            frame[3 - row][col] = (row < height) ? 1 : 0;
 51        }
 52    }
 53}
 54
 55// フレームバッファにタスク実行状態を描画
 56static void draw_task_status(void)
 57{
 58    int task_count = os_get_task_count();
 59
 60    // 最大4タスク表示(行4-7)
 61    int display_tasks = (task_count > 4) ? 4 : task_count;
 62
 63    for (int t = 0; t < display_tasks; t++) {
 64        int row = 4 + t;
 65
 66        // 履歴を描画
 67        for (int i = 0; i < TASK_HISTORY_LEN; i++) {
 68            int hist_idx = (task_history_index + i + 1) % TASK_HISTORY_LEN;
 69            int col = i;
 70
 71            if (task_history[t][hist_idx] == t) {
 72                // このタスクが実行中だった
 73                frame[row][col] = 1;
 74            } else {
 75                frame[row][col] = 0;
 76            }
 77        }
 78
 79        // 現在の状態を示すインジケータ(右端)
 80        TaskState state = os_get_task_state(t);
 81        switch (state) {
 82            case TASK_RUNNING:
 83                frame[row][10] = 1;
 84                frame[row][11] = 1;
 85                break;
 86            case TASK_READY:
 87                frame[row][10] = 1;
 88                frame[row][11] = 0;
 89                break;
 90            case TASK_BLOCKED:
 91                frame[row][10] = 0;
 92                frame[row][11] = 1;
 93                break;
 94            default:
 95                frame[row][10] = 0;
 96                frame[row][11] = 0;
 97                break;
 98        }
 99    }
100}
101
102// 区切り線を描画
103static void draw_separator(void)
104{
105    // CPU負荷領域とタスク状態領域の間に区切り線
106    // 列8, 9を区切りとして使用
107    for (int row = 0; row < 8; row++) {
108        frame[row][8] = 0;
109        frame[row][9] = (row == 3) ? 1 : 0;  // 行3だけ点灯
110    }
111}
112
113void display_update(void)
114{
115    // CPU 使用率を更新(アイドル以外の割合)。
116    // os_get_cpu_usage() は呼ぶたびに基準時刻を更新するので、ここから呼ぶと
117    // シェルの ps の計測窓を壊してしまう。生カウンタから自前で差分を取る。
118    static uint32_t last_total = 0;
119    static uint32_t last_idle  = 0;
120
121    uint32_t total = os_get_tick();
122    uint32_t idle  = os_get_task_cpu_ticks(0);
123    uint32_t d_total = total - last_total;
124    uint32_t d_idle  = idle  - last_idle;
125    last_total = total;
126    last_idle  = idle;
127
128    int total_cpu = (d_total == 0) ? 0
129                  : (int)(100u - (d_idle * 100u) / d_total);
130
131    // 履歴に追加
132    cpu_history[history_index] = total_cpu;
133    history_index = (history_index + 1) % 12;
134
135    // 現在実行中のタスクを記録
136    int current = os_get_current_task();
137    if (current >= 0 && current < MAX_TASKS) {
138        task_history[current][task_history_index] = current;
139    }
140    task_history_index = (task_history_index + 1) % TASK_HISTORY_LEN;
141
142    // フレームバッファをクリア
143    memset(frame, 0, sizeof(frame));
144
145    // 各要素を描画
146    draw_cpu_graph();
147    draw_task_status();
148
149    // マトリクスに表示
150    matrix.renderBitmap(frame, 8, 12);
151}
152
153// 表示更新タスク
154void display_task(void)
155{
156    display_init();
157
158    while (1) {
159        display_update();
160        os_sleep(100);  // 100msごとに更新
161    }
162}
src/os_fault.cpp — 170 lines

Open on GitHub

  1/**
  2 * os_fault.cpp - Fault Handler Implementation
  3 * Chapter 6: Memory Protection
  4 */
  5
  6#include "os_fault.h"
  7#include "os_kernel.h"
  8#include <Arduino.h>
  9
 10static volatile int g_fault_occurred = 0;
 11static volatile const char* g_fault_reason = "";
 12static volatile int g_faulted_task = -1;
 13
 14// Forward declarations for vector table override
 15extern "C" __attribute__((naked)) void HardFault_Handler(void);
 16extern "C" void PendSV_Handler(void);
 17extern "C" void SysTick_Handler(void);
 18
 19void fault_init(void)
 20{
 21    // Enable divide by zero trap
 22    SCB->CCR |= SCB_CCR_DIV_0_TRP_Msk;
 23
 24    // Do NOT enable separate UsageFault/BusFault/MemFault handlers.
 25    // By leaving them disabled, all faults escalate to HardFault,
 26    // which we handle via the VTOR override above.
 27
 28    // The Arduino framework copies the vector table to RAM (VTOR points to RAM).
 29    // We must overwrite the HardFault entry in the RAM vector table directly.
 30    // Also override PendSV and SysTick entries for the OS scheduler.
 31    uint32_t *vtor = (uint32_t *)SCB->VTOR;
 32    vtor[3] = (uint32_t)HardFault_Handler;   // Vector #3 = HardFault
 33    vtor[14] = (uint32_t)PendSV_Handler;     // Vector #14 = PendSV
 34    vtor[15] = (uint32_t)SysTick_Handler;    // Vector #15 = SysTick
 35
 36    g_fault_occurred = 0;
 37    g_fault_reason = "";
 38}
 39
 40const char* fault_get_reason(void)
 41{
 42    return (const char*)g_fault_reason;
 43}
 44
 45int fault_occurred(void)
 46{
 47    return g_fault_occurred;
 48}
 49
 50void fault_clear(void)
 51{
 52    g_fault_occurred = 0;
 53    g_fault_reason = "";
 54}
 55
 56static void analyze_fault(void)
 57{
 58    uint32_t cfsr = SCB->CFSR;
 59
 60    // Check Usage Fault Status Register (UFSR)
 61    if (cfsr & SCB_CFSR_DIVBYZERO_Msk) {
 62        g_fault_reason = "Divide by zero";
 63    } else if (cfsr & SCB_CFSR_UNALIGNED_Msk) {
 64        g_fault_reason = "Unaligned access";
 65    } else if (cfsr & SCB_CFSR_UNDEFINSTR_Msk) {
 66        g_fault_reason = "Undefined instruction";
 67    } else if (cfsr & SCB_CFSR_INVSTATE_Msk) {
 68        g_fault_reason = "Invalid state (Thumb bit)";
 69    } else if (cfsr & SCB_CFSR_INVPC_Msk) {
 70        g_fault_reason = "Invalid PC";
 71    }
 72    // Check Bus Fault Status Register (BFSR)
 73    else if (cfsr & SCB_CFSR_PRECISERR_Msk) {
 74        g_fault_reason = "Precise data bus error";
 75    } else if (cfsr & SCB_CFSR_IMPRECISERR_Msk) {
 76        g_fault_reason = "Imprecise data bus error";
 77    } else if (cfsr & SCB_CFSR_IBUSERR_Msk) {
 78        g_fault_reason = "Instruction bus error";
 79    }
 80    // Check Memory Management Fault Status Register (MMFSR)
 81    else if (cfsr & SCB_CFSR_DACCVIOL_Msk) {
 82        g_fault_reason = "Data access violation";
 83    } else if (cfsr & SCB_CFSR_IACCVIOL_Msk) {
 84        g_fault_reason = "Instruction access violation";
 85    }
 86    else {
 87        g_fault_reason = "Unknown fault";
 88    }
 89
 90    // Clear fault flags
 91    SCB->CFSR = cfsr;
 92}
 93
 94// Safe landing pad for terminated tasks after fault recovery
 95extern "C" void _fault_task_sink(void)
 96{
 97    while (1) {
 98        __WFI();
 99    }
100}
101
102// Flag to defer fault message printing to a safe context
103static volatile int g_fault_pending_print = 0;
104static volatile uint32_t g_fault_pc = 0;
105
106extern "C" void HardFault_Handler_C(FaultFrame *frame)
107{
108    g_fault_occurred = 1;
109    g_faulted_task = g_current_task;
110    g_fault_pc = frame->pc;
111
112    analyze_fault();
113
114    // Terminate the faulted task and redirect PC to safe sink
115    if (g_faulted_task > 0) {
116        os_terminate_task(g_faulted_task);
117
118        // Rewrite the stacked PC to a safe infinite loop so that
119        // returning from the fault doesn't re-execute the faulting
120        // instruction. The SysTick scheduler will switch away from
121        // this terminated task on the next tick.
122        frame->pc = (uint32_t)_fault_task_sink;
123        frame->xpsr = 0x01000000;  // Thumb bit set
124
125        // Defer printing to next safe context
126        g_fault_pending_print = 1;
127        return;
128    }
129
130    // If idle task or no task crashed, halt
131    while (1) { }
132}
133
134// Call this from a task (e.g., shell) to print deferred fault info
135void fault_print_pending(void)
136{
137    if (!g_fault_pending_print) return;
138    g_fault_pending_print = 0;
139
140    Serial.println();
141    Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
142    Serial.println("!!!       FAULT DETECTED      !!!");
143    Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
144    Serial.println();
145    Serial.print("Task ");
146    Serial.print(g_faulted_task);
147    Serial.print(" (");
148    Serial.print(os_get_task_name(g_faulted_task));
149    Serial.println(") crashed!");
150    Serial.print("Reason: ");
151    Serial.println((const char*)g_fault_reason);
152    Serial.print("PC at fault: 0x");
153    Serial.println(g_fault_pc, HEX);
154    Serial.println("Task terminated. Other tasks continue.");
155    Serial.println();
156}
157
158extern "C" __attribute__((naked)) void HardFault_Handler(void)
159{
160    __asm volatile (
161        "   TST     LR, #4              \n"  // Check which stack was used
162        "   ITE     EQ                  \n"
163        "   MRSEQ   R0, MSP             \n"  // MSP if LR bit 2 is 0
164        "   MRSNE   R0, PSP             \n"  // PSP if LR bit 2 is 1
165        "   PUSH    {LR}                \n"  // Save EXC_RETURN
166        "   BL      HardFault_Handler_C \n"  // Call C handler (preserves LR on stack)
167        "   POP     {LR}                \n"  // Restore EXC_RETURN
168        "   BX      LR                  \n"  // Return from exception properly
169    );
170}
src/os_kernel.cpp — 241 lines

Open on GitHub

  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 のようにタスクごとにループで呼ぶため、基準時刻もタスクごとに持つ。
209// CPU 使用カウンタの生値。os_get_cpu_usage() と違って状態を持たないので、
210// 何度呼んでも他の呼び手の計測窓を壊さない (第4章 4.7)。
211uint32_t os_get_task_cpu_ticks(int task_id)
212{
213    if (task_id < 0 || task_id >= g_task_count) {
214        return 0;
215    }
216    return g_tasks[task_id].cpu_ticks;
217}
218
219int os_get_cpu_usage(int task_id)
220{
221    if (task_id < 0 || task_id >= g_task_count) {
222        return -1;
223    }
224
225    static uint32_t last_total[MAX_TASKS] = {0};
226    static uint32_t last_task_ticks[MAX_TASKS] = {0};
227
228    uint32_t total = g_system_ticks;
229    uint32_t delta_total = total - last_total[task_id];
230
231    if (delta_total == 0) {
232        return 0;
233    }
234
235    uint32_t delta_task = g_tasks[task_id].cpu_ticks - last_task_ticks[task_id];
236
237    last_total[task_id] = total;
238    last_task_ticks[task_id] = g_tasks[task_id].cpu_ticks;
239
240    return (delta_task * 100) / delta_total;
241}
src/shell.cpp — 237 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 "os_fault.h"
  9#include "interpreter.h"
 10#include <Arduino.h>
 11#include <string.h>
 12
 13#define CMD_BUF_SIZE 64
 14
 15static char cmd_buf[CMD_BUF_SIZE];
 16static int cmd_pos = 0;
 17
 18static void cmd_run(const char *arg);
 19
 20static const char* state_to_str(TaskState state)
 21{
 22    switch (state) {
 23        case TASK_READY:      return "READY";
 24        case TASK_RUNNING:    return "RUNNING";
 25        case TASK_BLOCKED:    return "BLOCKED";
 26        case TASK_SUSPENDED:  return "SUSPEND";
 27        case TASK_TERMINATED: return "TERM";
 28        default:              return "?";
 29    }
 30}
 31
 32static void cmd_help(void)
 33{
 34    Serial.println("Available commands:");
 35    Serial.println("  ps      - List all tasks");
 36    Serial.println("  kill N  - Suspend task N");
 37    Serial.println("  exec N  - Resume task N");
 38    Serial.println("  info    - System information");
 39    Serial.println("  reboot  - Reboot system");
 40    Serial.println("  help    - Show this help");
 41}
 42
 43static void cmd_ps(void)
 44{
 45    Serial.println();
 46    Serial.println("ID  NAME           STATE      CPU%");
 47    Serial.println("--  ----           -----      ----");
 48
 49    for (int i = 0; i < os_get_task_count(); i++) {
 50        Serial.print(i);
 51        Serial.print("   ");
 52
 53        const char *name = os_get_task_name(i);
 54        Serial.print(name);
 55        for (int j = strlen(name); j < 15; j++) Serial.print(" ");
 56
 57        Serial.print(state_to_str(os_get_task_state(i)));
 58        for (int j = strlen(state_to_str(os_get_task_state(i))); j < 10; j++) Serial.print(" ");
 59
 60        Serial.print(os_get_cpu_usage(i));
 61        Serial.println("%");
 62    }
 63    Serial.println();
 64}
 65
 66static void cmd_kill(int id)
 67{
 68    if (id <= 0) {
 69        Serial.println("Error: Cannot kill idle task (ID 0)");
 70        return;
 71    }
 72    if (id >= os_get_task_count()) {
 73        Serial.println("Error: Invalid task ID");
 74        return;
 75    }
 76
 77    os_suspend_task(id);
 78    Serial.print("Task ");
 79    Serial.print(id);
 80    Serial.print(" (");
 81    Serial.print(os_get_task_name(id));
 82    Serial.println(") suspended.");
 83}
 84
 85static void cmd_exec(int id)
 86{
 87    if (id <= 0 || id >= os_get_task_count()) {
 88        Serial.println("Error: Invalid task ID");
 89        return;
 90    }
 91
 92    os_resume_task(id);
 93    Serial.print("Task ");
 94    Serial.print(id);
 95    Serial.print(" (");
 96    Serial.print(os_get_task_name(id));
 97    Serial.println(") resumed.");
 98}
 99
100static void cmd_info(void)
101{
102    Serial.println();
103    Serial.println("=== System Information ===");
104    Serial.print("System Ticks:  ");
105    Serial.println(os_get_tick());
106    Serial.print("Uptime:        ");
107    Serial.print(os_get_tick() / 1000);
108    Serial.println(" s");
109    Serial.print("Task Count:    ");
110    Serial.println(os_get_task_count());
111    Serial.print("Time Slice:    ");
112    Serial.print(TIME_SLICE_MS);
113    Serial.println(" ms");
114    Serial.println();
115}
116
117static void cmd_reboot(void)
118{
119    Serial.println("Rebooting...");
120    delay(100);
121    NVIC_SystemReset();
122}
123
124void shell_process_command(char *cmd)
125{
126    // Trim whitespace
127    while (*cmd == ' ') cmd++;
128    char *end = cmd + strlen(cmd) - 1;
129    while (end > cmd && *end == ' ') *end-- = '\0';
130
131    if (strlen(cmd) == 0) return;
132
133    // Parse command
134    if (strcmp(cmd, "help") == 0 || strcmp(cmd, "?") == 0) {
135        cmd_help();
136    } else if (strcmp(cmd, "ps") == 0) {
137        cmd_ps();
138    } else if (strncmp(cmd, "kill ", 5) == 0) {
139        int id = atoi(cmd + 5);
140        cmd_kill(id);
141    } else if (strncmp(cmd, "exec ", 5) == 0) {
142        int id = atoi(cmd + 5);
143        cmd_exec(id);
144    } else if (strncmp(cmd, "run", 3) == 0) {
145        const char *arg = cmd + 3;
146        while (*arg == ' ') arg++;
147        cmd_run(arg);
148    } else if (strcmp(cmd, "info") == 0) {
149        cmd_info();
150    } else if (strcmp(cmd, "reboot") == 0) {
151        cmd_reboot();
152    } else {
153        Serial.print("Unknown command: ");
154        Serial.println(cmd);
155        Serial.println("Type 'help' for available commands.");
156    }
157}
158
159// シリアルから 1 行読み取る(本書 8.4「シェルとの統合」)
160static void read_line(char *buf, int size)
161{
162    int pos = 0;
163    memset(buf, 0, size);
164
165    while (1) {
166        if (Serial.available()) {
167            char ch = Serial.read();
168
169            if (ch == '\r' || ch == '\n') {
170                Serial.println();
171                return;
172            } else if (ch == '\b' || ch == 127) {
173                if (pos > 0) {
174                    pos--;
175                    buf[pos] = '\0';
176                    Serial.print("\b \b");
177                }
178            } else if (pos < size - 1) {
179                buf[pos++] = ch;
180                Serial.print(ch);
181            }
182        }
183        os_sleep(10);
184    }
185}
186
187// run コマンド: 引数があれば 1 行スクリプト、無ければ複数行の入力モード
188static void cmd_run(const char *arg)
189{
190    static char script[MAX_SCRIPT_LINES * MAX_LINE_LENGTH];
191    static char line[MAX_LINE_LENGTH];
192
193    if (arg && *arg) {
194        interp_load(arg);
195        interp_run();
196        return;
197    }
198
199    Serial.println("Enter script (empty line to execute):");
200    script[0] = '\0';
201
202    while (1) {
203        Serial.print(". ");
204        read_line(line, MAX_LINE_LENGTH);
205        if (line[0] == '\0') break;
206
207        if (strlen(script) + strlen(line) + 2 >= sizeof(script)) {
208            Serial.println("Error: script too long");
209            return;
210        }
211        strcat(script, line);
212        strcat(script, "\n");
213    }
214
215    if (script[0] != '\0') {
216        interp_load(script);
217        interp_run();
218    }
219}
220
221void shell_task(void)
222{
223    Serial.println();
224    Serial.println("Mini OS Shell");
225    Serial.println("Type 'help' for commands.");
226
227    interp_init();
228
229    while (1) {
230        // 保留中のフォルト情報をタスク文脈で表示する (第6章 6.3)
231        fault_print_pending();
232
233        Serial.print("> ");
234        read_line(cmd_buf, CMD_BUF_SIZE);
235        shell_process_command(cmd_buf);
236    }
237}

On the board#

../_images/ch08_interpreter.gif

Typing commands into the interpreter: PRINT reaches [Print] and FORWARD reaches [Motor].#

In the simulator#

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

../_images/sim_ch08_led.gif

The LED task keeps inverting L every 500 ms, so it is stopped with kill 1 first; then the interpreter runs run LED 13 1 (on) and run LED 13 0 (off). Without stopping the LED task, its next inversion would overwrite the command. (Recorded in the simulator.)#

Improved version: 08_interpreter_v2 (not in the book)#

Chapter 8 uses the same LED matrix display as chapter 7, and it is just as hard to read (see the improved version in Chapter 7: LED Matrix Visualization). code/08_interpreter_v2 adds the display and the shell top command of chapter 7’s 07_led_matrix_v2. The interpreter and the kernel are the same as the book’s.

It is not in the book’s repository, so get it from this site’s repository and flash it (if you already cloned it for chapter 7’s improved version, only the upload line is needed, in build-your-own-arduino-rtos/code):

git clone https://github.com/iory/build-your-own-arduino-rtos.git
cd build-your-own-arduino-rtos/code
uv sync
uv run pio run -d 08_interpreter_v2 -t upload
uv run pio device monitor -b 115200

Rows

Content (12 dots = 100%, updated every 0.5 s)

0–1

Total CPU load (everything except idle)

2

Blank

3–7

CPU% of LED / Shell / Display / Light / Heavy

Chapter 8’s own tasks (LED, Shell, Display) barely use the CPU: the LED task and the interpreter’s FORWARD / TURN / DELAY all wait with os_sleep(), which hands the CPU to other tasks. To make the bars move, v2 adds chapter 7’s Light (uses 20 ms of CPU, then sleeps 60 ms) and Heavy (computes without sleeping) after the book’s three tasks. LED, Shell and Display keep their task numbers, so kill 1 → run LED 13 1 works as in the book.

Values read from top on a real UNO R4 WiFi:

Action

Total CPU

What you see

After boot

~60%

Bars on Light (16%) and Heavy (40%)

kill 5

~24%

Heavy’s bar disappears and the total shrinks

kill 4

~4%

Only Shell, drawing top, is left

exec 4 / exec 5

Back to normal

Note

While a script runs with run, the top screen stops updating: it is redrawn while the shell waits for input, so it resumes when the script ends. The matrix keeps updating because the display task draws it.

Pitfalls#

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