Chapter 7: LED Matrix Visualization#

What this chapter answers: how do top and Task Manager count CPU %?

Linux’s top and Windows Task Manager show CPU usage per program. The OS in this chapter counts a similar number itself and shows it on the LED matrix and in the shell’s ps.

It counts by adding time, on every timer interrupt, to whichever task was running at that moment. Linux works on the same basic idea. The details are in the column “top and htop”.

How the LED matrix is wired#

The 96 LEDs are charlieplexed on 11 lines (ROW0–ROW10). Each pair of lines carries two LEDs in opposite directions, so driving one line HIGH and the other LOW lights exactly one LED. The table at the top right of the schematic below maps the LED numbers in the schematic to positions on the matrix.

The bundled Arduino_LED_Matrix library lights one LED per tick of a 10 kHz timer interrupt, stepping through all 96 in turn (about 104 Hz per full frame). (Arduino_LED_Matrix.h L182, L346–L349, ArduinoCore-renesas 1.5.1)

../_images/uno_r4_wifi_led_matrix_schematic.png

LED matrix schematic. Source: Arduino, ABX00087 Schematics, page 2 “LED MATRIX” (Silvio Navaretti). CC BY-SA 4.0#

Sample code#

  • Directory: docs/os-on-arduino/code/07_led_matrix

uv run pio run -d 07_led_matrix -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 — 72 lines

Open on GitHub

 1// 06_led_matrix.ino
 2// LEDマトリクス可視化のデモ
 3
 4#include "os_kernel.h"
 5#include "os_display.h"
 6#include "shell.h"
 7
 8// Arduino UNO R4 WiFi は基板上に "L" と印字された LED を持っています。
 9// 追加部品なしで動かせるよう、これを主 LED として使います。
10#define LED_PIN LED_BUILTIN
11
12// タスク1: LED点滅(軽い)
13void task_led(void)
14{
15    pinMode(LED_PIN, OUTPUT);
16    while (1) {
17        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
18        os_sleep(200);
19    }
20}
21
22// タスク2: 軽い計算
23void task_light(void)
24{
25    while (1) {
26        volatile int sum = 0;
27        for (int i = 0; i < 10000; i++) {
28            sum += i;
29        }
30        os_sleep(50);
31    }
32}
33
34// タスク3: 重い計算(CPU負荷高い)
35void task_heavy(void)
36{
37    while (1) {
38        volatile long sum = 0;
39        for (long i = 0; i < 500000; i++) {
40            sum += i;
41        }
42        // os_sleep なし:CPUをずっと使う
43    }
44}
45
46void setup()
47{
48    Serial.begin(115200);
49    while (!Serial) { }
50
51    Serial.println("=== LED Matrix Visualization Demo ===");
52    Serial.println("Watch the LED matrix to see OS activity!\n");
53
54    os_init();
55
56    os_create_task(task_led, "LED", 512);
57    os_create_task(task_light, "Light", 512);
58    os_create_task(task_heavy, "Heavy", 512);
59    os_create_task(shell_task, "Shell", 1024);
60    os_create_task(display_task, "Display", 512);
61
62    Serial.println("Legend:");
63    Serial.println("  Top half: CPU load graph (left=old, right=new)");
64    Serial.println("  Bottom half: Task execution (1 row per task)");
65    Serial.println("  Right indicators: RUNNING=11, READY=10, BLOCKED=01\n");
66
67    Serial.println("Try 'kill 3' to stop Heavy task and watch CPU load drop!\n");
68
69    os_start();
70}
71
72void loop() { }
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/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_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 — 190 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 <Arduino.h>
 10#include <string.h>
 11
 12#define CMD_BUF_SIZE 64
 13
 14static char cmd_buf[CMD_BUF_SIZE];
 15static int cmd_pos = 0;
 16
 17static const char* state_to_str(TaskState state)
 18{
 19    switch (state) {
 20        case TASK_READY:      return "READY";
 21        case TASK_RUNNING:    return "RUNNING";
 22        case TASK_BLOCKED:    return "BLOCKED";
 23        case TASK_SUSPENDED:  return "SUSPEND";
 24        case TASK_TERMINATED: return "TERM";
 25        default:              return "?";
 26    }
 27}
 28
 29static void cmd_help(void)
 30{
 31    Serial.println("Available commands:");
 32    Serial.println("  ps      - List all tasks");
 33    Serial.println("  kill N  - Suspend task N");
 34    Serial.println("  exec N  - Resume task N");
 35    Serial.println("  info    - System information");
 36    Serial.println("  reboot  - Reboot system");
 37    Serial.println("  help    - Show this help");
 38}
 39
 40static void cmd_ps(void)
 41{
 42    Serial.println();
 43    Serial.println("ID  NAME           STATE      CPU%");
 44    Serial.println("--  ----           -----      ----");
 45
 46    for (int i = 0; i < os_get_task_count(); i++) {
 47        Serial.print(i);
 48        Serial.print("   ");
 49
 50        const char *name = os_get_task_name(i);
 51        Serial.print(name);
 52        for (int j = strlen(name); j < 15; j++) Serial.print(" ");
 53
 54        Serial.print(state_to_str(os_get_task_state(i)));
 55        for (int j = strlen(state_to_str(os_get_task_state(i))); j < 10; j++) Serial.print(" ");
 56
 57        Serial.print(os_get_cpu_usage(i));
 58        Serial.println("%");
 59    }
 60    Serial.println();
 61}
 62
 63static void cmd_kill(int id)
 64{
 65    if (id <= 0) {
 66        Serial.println("Error: Cannot kill idle task (ID 0)");
 67        return;
 68    }
 69    if (id >= os_get_task_count()) {
 70        Serial.println("Error: Invalid task ID");
 71        return;
 72    }
 73
 74    os_suspend_task(id);
 75    Serial.print("Task ");
 76    Serial.print(id);
 77    Serial.print(" (");
 78    Serial.print(os_get_task_name(id));
 79    Serial.println(") suspended.");
 80}
 81
 82static void cmd_exec(int id)
 83{
 84    if (id <= 0 || id >= os_get_task_count()) {
 85        Serial.println("Error: Invalid task ID");
 86        return;
 87    }
 88
 89    os_resume_task(id);
 90    Serial.print("Task ");
 91    Serial.print(id);
 92    Serial.print(" (");
 93    Serial.print(os_get_task_name(id));
 94    Serial.println(") resumed.");
 95}
 96
 97static void cmd_info(void)
 98{
 99    Serial.println();
100    Serial.println("=== System Information ===");
101    Serial.print("System Ticks:  ");
102    Serial.println(os_get_tick());
103    Serial.print("Uptime:        ");
104    Serial.print(os_get_tick() / 1000);
105    Serial.println(" s");
106    Serial.print("Task Count:    ");
107    Serial.println(os_get_task_count());
108    Serial.print("Time Slice:    ");
109    Serial.print(TIME_SLICE_MS);
110    Serial.println(" ms");
111    Serial.println();
112}
113
114static void cmd_reboot(void)
115{
116    Serial.println("Rebooting...");
117    delay(100);
118    NVIC_SystemReset();
119}
120
121void shell_process_command(char *cmd)
122{
123    // Trim whitespace
124    while (*cmd == ' ') cmd++;
125    char *end = cmd + strlen(cmd) - 1;
126    while (end > cmd && *end == ' ') *end-- = '\0';
127
128    if (strlen(cmd) == 0) return;
129
130    // Parse command
131    if (strcmp(cmd, "help") == 0 || strcmp(cmd, "?") == 0) {
132        cmd_help();
133    } else if (strcmp(cmd, "ps") == 0) {
134        cmd_ps();
135    } else if (strncmp(cmd, "kill ", 5) == 0) {
136        int id = atoi(cmd + 5);
137        cmd_kill(id);
138    } else if (strncmp(cmd, "exec ", 5) == 0) {
139        int id = atoi(cmd + 5);
140        cmd_exec(id);
141    } else if (strcmp(cmd, "info") == 0) {
142        cmd_info();
143    } else if (strcmp(cmd, "reboot") == 0) {
144        cmd_reboot();
145    } else {
146        Serial.print("Unknown command: ");
147        Serial.println(cmd);
148        Serial.println("Type 'help' for available commands.");
149    }
150}
151
152void shell_task(void)
153{
154    Serial.println();
155    Serial.println("Mini OS Shell");
156    Serial.println("Type 'help' for commands.");
157
158    while (1) {
159        // 保留中のフォルト情報をタスク文脈で表示する (第6章 6.3)
160        fault_print_pending();
161
162        Serial.print("> ");
163
164        cmd_pos = 0;
165        memset(cmd_buf, 0, CMD_BUF_SIZE);
166
167        while (1) {
168            if (Serial.available()) {
169                char ch = Serial.read();
170
171                if (ch == '\r' || ch == '\n') {
172                    Serial.println();
173                    break;
174                } else if (ch == '\b' || ch == 127) {
175                    if (cmd_pos > 0) {
176                        cmd_pos--;
177                        cmd_buf[cmd_pos] = '\0';
178                        Serial.print("\b \b");
179                    }
180                } else if (cmd_pos < CMD_BUF_SIZE - 1) {
181                    cmd_buf[cmd_pos++] = ch;
182                    Serial.print(ch);
183                }
184            }
185            os_sleep(10);
186        }
187
188        shell_process_command(cmd_buf);
189    }
190}

On the board#

../_images/ch07_kill.gif

The LED matrix on a real UNO R4 WiFi. The top half is the CPU load over the last 1.2 s, rewritten one column at a time from the left every 0.1 s; the rightmost two columns of the bottom half show the state of tasks 0–3. kill 3 stops Heavy and the graph drops; exec 3 brings it back. (Filmed with a webcam.)#

In the simulator#

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

../_images/sim_ch07_kill.gif

The top half of the matrix is the CPU load over the last 1.2 s (rewritten one column at a time from the left every 0.1 s), and the rightmost two columns of the bottom half show the state of tasks 0–3. kill 3 stops the busy Heavy task and the graph drains away; exec 3 brings it back. (Recorded in the simulator.)#

Improved version: 07_led_matrix_v2 (not in the book)#

In the book’s code the load history in the top half is not drawn in time order and the bottom half barely lights up, so the display is hard to read. code/07_led_matrix_v2 redraws it as bars of the current value, like Linux’s htop (a tool that shows CPU load as bars and lists each process’s CPU usage; see the column below). The kernel is the same as the book’s; only the display, the shell and the tasks change.

It is not in the book’s repository (learning-os-from-arduino), so get it from this site’s repository and flash it:

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 07_led_matrix_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 / Light / Heavy / Shell / Display

Type top in the shell and the same numbers are redrawn in the terminal every 0.5 s (type top again to stop; you can still type kill 3 and so on). Light now uses CPU too, so you can see the per-task bars add up to the total.

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 3

~24%

Heavy’s bar disappears and the total shrinks

kill 2

~4%

Only Shell, drawing top, is left

exec 2 / exec 3

Back to normal

Note

Heavy never sleeps, yet it only gets 40–50%. The book’s scheduler runs idle in turn like any other task, so Heavy shares the CPU with idle.

Column: top and htop

On Linux, top (procps-ng) lists the CPU usage of each running process, and htop is a friendlier take on it. The v2 matrix and its top command imitate the two.

What they show. htop’s CPU meter at the top of the screen is a bar that stacks kinds of CPU time (nice, user, system, …) in different colors (htop(1), METERS). In the list below it, %CPU is the process’s share of the elapsed time since the last screen update (top(1), %CPU). In v2, rows 0–1 are the CPU meter and rows 3–7 are the list.

How often they redraw. htop redraws every 1.5 s by default (-d is in tenths of a second and defaults to 15; Settings.h). procps-ng top also defaults to 1.5 s, or 3 s when built with its original defaults (top.h). v2 uses 0.5 s.

How CPU time is counted. Linux basically adds one tick of CPU time to whichever task was running at each timer interrupt (account_process_tick()). The totals appear in /proc/stat (proc_stat(5)) and elsewhere, and top computes percentages from the difference between two reads. The book’s OS does the same: on every SysTick interrupt it adds 10 ms to the task that was running (SysTick_Handler in os_kernel.cpp). v2 reads those counters every 0.5 s and takes the difference.

Counting by ticks misses short jobs that finish between two ticks. That is why the LED task shows 0% (Linux can count by other means, depending on its configuration).

Stopping a process. Press k in top, or F9 or k in htop, to send a signal to the selected process (top(1), htop(1)). The book’s shell kill N only suspends the task, so exec N resumes it.

Break it#

These experiments use the improved 07_led_matrix_v2 above. Its kernel is the same as the book’s, and the matrix bars and top make the change in CPU % visible.

Predict before you open each answer. The first one needs no code change: try it directly in the improved version in the browser. For the second, change the code, build it, and drop firmware.elf into the browser version. v2 lives in this site’s repository, so work inside the code/ you cloned with the steps in the improved-version section above.

uv run pio run -d 07_led_matrix_v2
# drop 07_led_matrix_v2/.pio/build/uno_r4_wifi/firmware.elf into the browser version
# to undo: git checkout -- 07_led_matrix_v2

Note

The outputs in the answers were captured by actually running the code in QEMU, the same emulator the browser version uses. They have not been checked on the real board yet. The browser version runs heavy computation slower than the board, so the CPU % values may look slightly different there.

1. The shell stops itself#

kill 0 is refused with Error: Cannot kill idle task (ID 0). What about kill 4? ID 4 is the shell itself, the one reading your commands.

Predict: after kill 4, can you bring it back with exec 4?

Answer

No. After kill 4, not even the usual Task 4 (Shell) suspended. appears, and from then on nothing you type (ps, exec 4, top) gets a reply.

Mini OS Shell
Type 'help' for commands.
> kill 4

When the task to suspend is the caller itself, os_suspend_task() calls os_schedule() on the spot and switches to another task. The shell stops before printing suspended. and never gets a turn again. exec 4 never arrives, because the shell that would read it is the one that is stopped.

To recover, reset the board (in the browser version, reload the page). Making kill refuse its own ID, as it already does for kill 0, prevents this.

2. Take idle out of the rotation#

Type ps and Heavy, which runs without ever sleeping, gets only 41% of the CPU (in QEMU: idle 41%, Light 16%, Heavy 41%). The book’s scheduler puts idle in the same rotation as every other task, so Heavy and idle share the CPU.

So change os_schedule() in src/os_kernel.cpp to pick idle only when no other task can run.

     int next = g_current_task;
     do {
         next = (next + 1) % g_task_count;
-        if (g_tasks[next].state == TASK_READY || g_tasks[next].state == TASK_RUNNING) break;
+        if (next != 0 && (g_tasks[next].state == TASK_READY || g_tasks[next].state == TASK_RUNNING)) break;
     } while (next != g_current_task);
+    if (g_tasks[next].state != TASK_READY && g_tasks[next].state != TASK_RUNNING) next = 0;

Predict: what happens to Heavy’s row on the matrix, and to the total bar at the top?

Answer

The total bar fills all 12 dots, and Heavy’s bar nearly doubles. ps shows Heavy at 79% and idle at 0% (76% for Heavy in the browser version).

> ps

ID  NAME           STATE      CPU%
--  ----           -----      ----
0   idle           READY     0%
1   LED            BLOCKED   0%
2   Light          BLOCKED   20%
3   Heavy          READY     79%
4   Shell          RUNNING   0%
5   Display        BLOCKED   0%

The total bar is the share of everything except idle, so idle at 0% means 100%. While the other tasks sleep, idle and Heavy used to take turns; now only Heavy runs. Heavy also gets the time Light spends asleep, so apart from Light’s 20%, almost everything goes to Heavy.

If you leave top running in the browser version, Heavy drops to about 50%, because redrawing the table on the slow emulator costs the Shell about 30%. To compare Heavy’s numbers, stop top and use ps.

After kill 3 stops Heavy and a few seconds pass, ps showed idle 74% and Light 25%. Idle now runs only when no other task can.

LED shows 0% because it finishes its work in less than 10 ms (one tick) and goes back to sleep, which counting on every tick misses (the column “top and htop”).

Pitfalls#

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