# 第7章: LEDマトリクス可視化 UNO R4 WiFi 内蔵の LED マトリクスに OS の内部状態を表示します。 :::{admonition} この章で分かること: `top` やタスクマネージャの CPU % は、どう数えている? :class: tip Linux の `top` や Windows のタスクマネージャには、プログラムごとの CPU 使用率が出ます。 この章の OS も、同じような値を自分で数えて、LED マトリクスとシェルの `ps` に出します。 数え方は、タイマ割り込みのたびに「そのとき走っていたタスク」に時間を足していく、 というものです。Linux も基本は同じ考え方です。 くわしくは {ref}`コラム「top と htop」 ` にまとめました。 ::: ## LED マトリクスの配線 96 個の LED は、11 本の線(ROW0〜ROW10)の**チャーリープレクシング**で つながっています。2 本の線の組ごとに向きが逆の LED が 2 個ついていて、 片方を HIGH・もう片方を LOW にすると 1 個だけが光ります。下の回路図の右上にある表は、 回路図上の LED 番号とマトリクス上の位置の対応です。 付属の `Arduino_LED_Matrix` ライブラリは、10 kHz のタイマ割り込み 1 回ごとに LED を 1 個ずつ順に点け替えています(96 個で 1 周するので、1 画面あたり 約 104 Hz)。 ([`Arduino_LED_Matrix.h` L182](https://github.com/arduino/ArduinoCore-renesas/blob/1.5.1/libraries/Arduino_LED_Matrix/src/Arduino_LED_Matrix.h#L182)、 [L346–L349](https://github.com/arduino/ArduinoCore-renesas/blob/1.5.1/libraries/Arduino_LED_Matrix/src/Arduino_LED_Matrix.h#L346-L349)、 ArduinoCore-renesas 1.5.1) ```{figure} ../_static/uno_r4_wifi_led_matrix_schematic.png :name: fig-ch07-led-matrix-schematic :width: 100% LED マトリクスの回路図。 出典: Arduino, [ABX00087 Schematics](https://docs.arduino.cc/resources/schematics/ABX00087-schematics.pdf) 2 ページ目「LED MATRIX」(Silvio Navaretti)。 [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) ``` ## サンプルコード - ディレクトリ: `docs/os-on-arduino/code/07_led_matrix` - ビルドと書き込み: ```bash uv run pio run -d 07_led_matrix -t upload uv run pio device monitor -b 115200 ``` コードを見る: [docs/os-on-arduino/code/07_led_matrix](https://github.com/iory/learning-os-from-arduino/tree/main/docs/os-on-arduino/code/07_led_matrix) ## ソースコード この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。 :::{dropdown} `src/main.cpp` — 72 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/src/main.cpp) ```{literalinclude} ../../../../code/07_led_matrix/src/main.cpp :language: cpp :linenos: ``` ::: :::{dropdown} `include/os_display.h` — 18 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/include/os_display.h) ```{literalinclude} ../../../../code/07_led_matrix/include/os_display.h :language: cpp :linenos: ``` ::: :::{dropdown} `include/os_fault.h` — 46 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/include/os_fault.h) ```{literalinclude} ../../../../code/07_led_matrix/include/os_fault.h :language: cpp :linenos: ``` ::: :::{dropdown} `include/os_kernel.h` — 75 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/include/os_kernel.h) ```{literalinclude} ../../../../code/07_led_matrix/include/os_kernel.h :language: cpp :linenos: ``` ::: :::{dropdown} `include/shell.h` — 23 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/include/shell.h) ```{literalinclude} ../../../../code/07_led_matrix/include/shell.h :language: cpp :linenos: ``` ::: :::{dropdown} `src/os_display.cpp` — 162 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/src/os_display.cpp) ```{literalinclude} ../../../../code/07_led_matrix/src/os_display.cpp :language: cpp :linenos: ``` ::: :::{dropdown} `src/os_fault.cpp` — 170 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/src/os_fault.cpp) ```{literalinclude} ../../../../code/07_led_matrix/src/os_fault.cpp :language: cpp :linenos: ``` ::: :::{dropdown} `src/os_kernel.cpp` — 241 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/src/os_kernel.cpp) ```{literalinclude} ../../../../code/07_led_matrix/src/os_kernel.cpp :language: cpp :linenos: ``` ::: :::{dropdown} `src/shell.cpp` — 190 行 :icon: code [GitHub で開く](https://github.com/iory/learning-os-from-arduino/blob/main/docs/os-on-arduino/code/07_led_matrix/src/shell.cpp) ```{literalinclude} ../../../../code/07_led_matrix/src/shell.cpp :language: cpp :linenos: ``` ::: ## 実機での出力 Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。 ```text ps ID NAME STATE CPU% -- ---- ----- ---- 0 idle READY 50% 1 LED BLOCKED 0% 2 Light BLOCKED 0% 3 Heavy READY 49% 4 Shell RUNNING 0% 5 Display READY 0% > ``` Heavy は眠らないのに 49% しかありません。本書のスケジューラは idle(ID 0)も普通の タスクと同じ順番で回すので、ほかのタスクが眠っている間は idle と Heavy が交互に走り、 CPU を半分ずつ分け合うためです。 実機のマトリクスはこう動きます。 ```{figure} ../_static/ch07_kill.gif :name: fig-ch07-board :width: 80% 実機の LED マトリクス。上半分が直近 1.2 秒の CPU 負荷で、0.1 秒ごとに左から順に 1 列ずつ書き換わります。下半分の右端 2 列はタスク 0〜3 の状態です。`kill 3` で `Heavy` を止めるとグラフが下がり、`exec 3` でまた上がります。(UNO R4 WiFi を Web カメラで撮影) ``` ### シミュレータで見る 実機が無くても、同じファームウェアを {doc}`シミュレータ <../getting-started/simulator>` で動かすとこう見えます。 ```{figure} ../_static/sim_ch07_kill.gif :name: fig-ch07-sim :width: 80% マトリクスの上半分が直近 1.2 秒の CPU 負荷(0.1 秒ごとに左から順に 1 列ずつ書き換わる)、下半分の右端 2 列がタスク 0〜3 の状態です。`kill 3` で重い計算の `Heavy` を止めるとグラフが消えていき、`exec 3` でまた上がります。(シミュレータで撮影) ``` ## 改良版: `07_led_matrix_v2`(書籍未掲載) 本書のコードの表示は、上半分の負荷の履歴が時間の順に並んでいないことと、 下半分がほとんど光らないことから、何を表しているのか読み取りにくいところが あります。そこで表示を Linux の `htop`(CPU の負荷を横棒で、プロセスごとの CPU 使用率を一覧で出すツール。下のコラム参照)のような「**いまの値**」の横棒に した改良版を [`code/07_led_matrix_v2`](https://github.com/iory/build-your-own-arduino-rtos/tree/main/code/07_led_matrix_v2) に置きました。カーネルは本書と同じで、 変えたのは表示・シェル・タスクの中身だけです。 書籍のリポジトリ(learning-os-from-arduino)には入っていないので、このサイトの リポジトリから取ってきて書き込みます。 ```bash 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 ``` | 行 | 中身(どれも 12 個で 100%、0.5 秒ごとに更新) | |---|---| | 0–1 | CPU 全体の負荷(idle 以外) | | 2 | 空き | | 3–7 | LED / Light / Heavy / Shell / Display の CPU 使用率 | シェルに足した `top` と打つと、マトリクスと同じ数字が 0.5 秒ごとに端末へ 描き直されます(もう一度 `top` で止まる。表示中も `kill 3` などを打てます)。 Heavy 以外に Light も CPU を使うようにしてあるので、行ごとの棒を足すと CPU 全体の棒になるのが見えます。 ```{raw} html ``` 実機(UNO R4 WiFi)の `top` の値は次のとおりです。 | 操作 | CPU 全体 | 目に見えること | |---|---|---| | 起動直後 | 約 60% | Light(16%)と Heavy(40%)の行に棒 | | `kill 3` | 約 24% | Heavy の棒が消え、全体の棒が縮む | | `kill 2` | 約 4% | 残るのは `top` を描いている Shell だけ | | `exec 2` / `exec 3` | 元に戻る | | ```{note} Heavy は眠らずに回り続けても 40〜50% しか取れません。本書のスケジューラは idle も普通のタスクと同じ順番で回すので、idle と CPU を分け合うためです。 ``` (ch07-top-column)= ~~~{admonition} コラム: `top` と `htop` :class: tip Linux には、動いているプロセスごとの CPU 使用率を一覧する `top`(procps-ng)と、 それを見やすくした `htop` があります。v2 のマトリクスとシェルの `top` は、 この 2 つを真似たものです。 **見ているもの。** `htop` の画面上部の CPU メーターは、CPU 時間を種類 (nice・user・system など)ごとに色分けして積み上げた横棒です ([htop(1) の METERS](https://github.com/htop-dev/htop/blob/1daf6d9312d13f6edb4593340b9c3f65a7eadad4/htop.1.in))。その下の一覧の `%CPU` は、 「前回画面を描いてから今回までの経過時間のうち、そのプロセスが使った割合」です ([top(1) の %CPU](https://man7.org/linux/man-pages/man1/top.1.html))。v2 では行 0–1 が CPU メーター、行 3–7 が一覧に あたります。 **更新の間隔。** `htop` は既定で 1.5 秒ごとに描き直します(`-d` は 0.1 秒単位で、 既定値は 15。[Settings.h](https://github.com/htop-dev/htop/blob/1daf6d9312d13f6edb4593340b9c3f65a7eadad4/Settings.h#L21))。procps-ng の `top` も既定は 1.5 秒で、 元の設定でビルドした場合は 3 秒です([top.h](https://gitlab.com/procps-ng/procps/-/blob/55b7d01d2c6006362b1d221a41594c40b6d77640/src/top/top.h#L124-129))。v2 は 0.5 秒です。 **CPU 時間の数え方。** Linux は基本的に、タイマ割り込み(tick)のたびに 「そのとき走っていたタスク」へ 1 tick ぶんの CPU 時間を足していきます ([`account_process_tick()`](https://github.com/torvalds/linux/blob/f0100363d8c374bd8e9ea7c9ba02744f0b802ca4/kernel/sched/cputime.c#L683-L712))。その合計が `/proc/stat` ([proc_stat(5)](https://man7.org/linux/man-pages/man5/proc_stat.5.html))などに出ていて、`top` は 2 回読んだ差分から 割合を出します。本書の OS も、SysTick 割り込みのたびに走っていたタスクへ 10 ms を足していて(`os_kernel.cpp` の `SysTick_Handler`)、考え方は同じです。 v2 はこの値を 0.5 秒ごとに 2 回読んで差を取っています。 tick で数える方式は、tick と tick の間に終わってしまう短い仕事を取りこぼします。 LED タスクが 0% に見えるのはこのためです(Linux でも、設定によっては tick 以外の 方法で数えます)。 **止める。** `top` では `k`、`htop` では `F9` か `k` で、選んだプロセスにシグナルを 送って止めます([top(1)](https://man7.org/linux/man-pages/man1/top.1.html)、[htop(1)](https://github.com/htop-dev/htop/blob/1daf6d9312d13f6edb4593340b9c3f65a7eadad4/htop.1.in))。本書のシェルの `kill N` はタスクを一時停止(suspend)するだけなので、`exec N` で再開できます。 ~~~ (ch07-break)= ## 壊してみる ここでは、上の改良版 `07_led_matrix_v2` を使います。カーネルは本書のコードと同じで、 マトリクスの横棒と `top` で CPU% の変化が目で見えるからです。 **予想してから**答えを開いてください。1 つめはコードを変えずに、 ブラウザ版の改良版で そのまま試せます。2 つめはコードを変えてビルドし、`firmware.elf` を {ref}`ブラウザ版にドロップ ` します。v2 はこのサイトの リポジトリにあるので、上の「改良版」の手順で clone した `code/` の中で作業します。 ```bash uv run pio run -d 07_led_matrix_v2 # できた 07_led_matrix_v2/.pio/build/uno_r4_wifi/firmware.elf をブラウザ版にドロップ # 元に戻すとき: git checkout -- 07_led_matrix_v2 ``` :::{note} 答えに載せた出力は、ブラウザ版と同じ QEMU で実際に動かして取ったものです。 実機ではまだ確かめていません。ブラウザ版は重い計算が実機より遅いので、 CPU% の値は少し違って見えることがあります。 ::: ### 1. シェルが自分を止める `kill 0` は `Error: Cannot kill idle task (ID 0)` で断られます。 では、`kill 4` はどうでしょう。ID 4 は、いまコマンドを受け付けているシェル自身です。 **予想:** `kill 4` のあと、`exec 4` で元に戻せるでしょうか。 :::{dropdown} 答え **戻せません。** `kill 4` を打つと、いつもの `Task 4 (Shell) suspended.` すら出ずに、 それきり `ps` も `exec 4` も `top` も、何を打っても返事がありません。 ```text Mini OS Shell Type 'help' for commands. > kill 4 ``` `os_suspend_task()` は、止めるのが自分自身なら、その場で `os_schedule()` を呼んで ほかのタスクに切り替えます。シェルは `suspended.` を表示する前に止まり、二度と 順番が回ってきません。`exec 4` を打っても、それを読むはずのシェルが止まっているので 届きません。 戻すにはリセットします(ブラウザ版ならページを読み込み直します)。 `kill 0` と同じように、`kill` が自分自身の ID を断るようにすれば防げます。 ::: ### 2. idle を順番から外す `ps` を打つと、眠らずに回り続ける Heavy の CPU% が 41% しかありません (QEMU で idle 41%・Light 16%・Heavy 41%)。本書のスケジューラが、idle も普通の タスクと同じ順番で回していて、Heavy と idle が CPU を分け合うからです。 そこで、`src/os_kernel.cpp` の `os_schedule()` を変えて、idle はほかに走れるタスクが 無いときだけ選ぶようにします。 ```diff 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; ``` **予想:** マトリクスの Heavy の行と、いちばん上の全体の棒はどうなるでしょう。 :::{dropdown} 答え **全体の棒が 12 個いっぱいになり、Heavy の棒が倍近くに伸びます。** `ps` では Heavy が 79%、idle が 0% です(ブラウザ版では Heavy 76%)。 ```text > 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% ``` 全体の棒は「idle 以外」の割合なので、idle が 0% なら 100% です。 ほかのタスクが眠っている間、これまでは idle と Heavy が交互に走っていましたが、 いまは Heavy だけが走ります。Light が眠っている間の時間も Heavy が使うので、 Light の 20% を除いた残りが、ほぼ全部 Heavy に回ります。 ブラウザ版で `top` を出したままにすると、Heavy は 50% ほどに下がります。 遅いエミュレータの上で表を描き直すのに、Shell が 30% ほど使うからです。 Heavy の値を比べるときは、`top` を止めて `ps` で見てください。 続けて `kill 3` で Heavy を止め、数秒待って `ps` を打つと、idle 74%・Light 25% に なりました。走れるタスクが無い時間だけ、idle が走っています。 LED が 0% なのは、仕事を 10 ms(1 tick)より短い時間で終えて眠るので、 tick のたびに数える方式では取りこぼすためです({ref}`コラム「top と htop」 `)。 ::: ## つまずきやすいポイント (読者からの質問に応じて随時追記します。質問は [GitHub Issues](https://github.com/iory/build-your-own-arduino-rtos/issues) へどうぞ)