ASTROCAMP 衛星開発ゼミ

事前学習 第1段階 / Week 4

リアルタイム
システム

テーマLED の点滅速度をスイッチで切り替える
目的①リアルタイムシステムの型を学ぶ
目的②シンプルなテーマにも型があることを学ぶ

今日の題材 Raspberry Pi Pico Photo: Laserlicht / Wikimedia Commons / CC BY-SA 4.0

0.1 今回の位置づけ / 第1段階の締めくくり

Week 1〜3 で作れるようになった要素を、1個の CPU の上で回し続ける

Week 1〜3:
マイコンの原理・センサとの通信という要素技術
Week 4:
要素を同時に動かす統合の技術
模擬衛星のシステム構成図
センサ・アクチュエータ・通信のすべてが1つの Pico W にぶら下がる
Week 4 | リアルタイムシステム02
Chapter 1

衛星の仕事は1つではない

姿勢制御・テレメトリ送信・コマンド受信という、周期も性質も違う仕事が1個の CPU で回り続ける

1個の CPU(Raspberry Pi Pico W)
姿勢制御
短い周期で測って動かし続ける
テレメトリ送信
緩い周期で定期的に送る
コマンド受信
不定期。来た瞬間を取りこぼせない
不定
Week 4 | リアルタイムシステム03

1.1 リアルタイムの定義

工学のリアルタイムは、処理が速いことではなく、決めた締め切り(デッドライン)を毎回守ること

速さでも平均でもなく、締め切りを毎回守れるか

処理 A 処理 B
平均の速さ 1 ms(速い) 8 ms(遅い)
最悪のとき まれに 1 秒 常に 8 ms
締め切り 10 ms を毎回守れるか ✕ 破る回がある ◯ 毎回守る
Week 4 | リアルタイムシステム04

1.2 締め切りの重さ

締め切り違反の被害で区別する ── 事故に直結するならハード、品質低下で済むならソフト

ハードリアルタイム

違反1回で意味を失う

エアバッグの展開
数 ms の締め切りを1回でも破れば無意味
模擬衛星の姿勢制御の周期

ソフトリアルタイム

破っても品質が下がるだけ

動画のコマ落ち
不快だが破綻はしない
地上局の表示遅れ
Week 4 | リアルタイムシステム05

1.3 今日のお題

「LED 点滅の速度をスイッチで切り替える」は、衛星と同じ周期タスクとイベントの最小セット

LED を点滅させる
「500 ms ごと」という締め切り付きの周期タスク
 = 姿勢制御・テレメトリと同じ性質
スイッチを判定する
いつ来るか分からないイベント
 = コマンド受信と同じ性質
回路の補足

プルアップ抵抗:スイッチは GND に接続するかどうかを選ぶだけ
接続しないとき H にするため必要(I2C と同じ)

LED+抵抗と、外付けプルアップ抵抗つきスイッチを Raspberry Pi Pico W に配線した回路
LED+抵抗と、外付けプルアップ抵抗つきスイッチの回路
Week 4 | リアルタイムシステム06

1.4 この章のまとめ / ここは覚えて次へ

周期の違う複数の仕事を1個の CPU で締め切り通りに回し続ける

覚える ①
リアルタイム≠動作が早い
リアルタイム=締め切りを毎回守る
覚える ②
処理時間・周期の違う仕事を
マイコン1つで実行する方法を学ぶ
覚える ③
それを学ぶための今日のテーマが
LED 点滅周期のスイッチでの切替
Week 4 | リアルタイムシステム07
Chapter 2

第1段:sleep_ms() で待つ素朴版

sleep_ms(500) で待ちながら LED を反転し、ループの末尾でスイッチを1回だけ読む一本道

// [Code-01] 第1段:素朴版(骨格)while (true) {    gpio_put(LED_PIN, 1);    sleep_ms(interval_ms);        // 点灯のまま待つ    gpio_put(LED_PIN, 0);    sleep_ms(interval_ms);        // 消灯のまま待つ    if (gpio_get(SW_PIN) == 0) {  // 確認は1周に1回        interval_ms = (interval_ms == 500) ? 100 : 500;    }}
「待って、待って、最後に1回読む」

1周のうち2つの sleep_ms() が大半を占め、スイッチ確認は末尾に一度だけ。読みやすく、点滅だけなら完璧に動く。

Week 4 | リアルタイムシステム08
QUIZ

2.1 予想してみよう

素朴版でスイッチを短く押すと、点滅速度が変わるまでに何が起きるか?(あてはまるものをすべて選ぶ)

Aほぼ遅れなし(押した瞬間に変わる)
B最大で約 1 秒 遅れることがある
Cそもそも反応しない
Week 4 | リアルタイムシステム09
WORK

2.2 手を動かす①:Wokwi で試す

Wokwi のシミュレータで素朴版を動かし、ボタンを押した瞬間と点滅速度が変わる瞬間のズレ(遅れ)を観察する

Wokwi シミュレータ:Raspberry Pi Pico W に LED とボタンをつないだ素朴版(camp_week4_01)
Wokwi プロジェクト camp_week4_01LINKwokwi.com/projects/470318028620099585
Week 4 | リアルタイムシステム10

2.4 遅れの原因

sleep_ms(500) は戻るまで CPU を占有し続ける呼び出し(ブロッキング)。その間は他の仕事が進まない

1周 ≈ 1000 ms のタイムライン
スイッチを読める瞬間はここだけ(一瞬)
sleep_ms(500) ── 点灯のまま塞がる
sleep_ms(500) ── 消灯のまま塞がる
切替1周のうち 読みはマイクロ秒の一瞬だけ
✕ 押下への応答 は次に読まれるまで返らない。1周 ≈ 1000 ms のほぼ全部が塞がった時間 ── 衛星なら sleep 中に届いたコマンドへの反応が最大約 1 秒遅れる。
Week 4 | リアルタイムシステム11

2.5 取りこぼしの原因

sleep 中に始まって終わる短い入力は、末尾の 1 回の読みに残らず取りこぼされる

原因は遅れと同じ。1 周に 1 回、しかも「今このピンが押されているか」という現在のレベルしか見ていない。だから押されていた瞬間の長さで結果が分かれる。

押しっぱなし△ 遅れて拾われる
押 ───▶ 読む瞬間もまだ押している
一瞬押す✕ 取りこぼし
押→離
sleep 中に完結
直し方の芽 レベルではなく変化した瞬間(エッジ)を捉えれば取りこぼさない ── その仕組みが第4段の割り込み(5章)。衛星では一瞬のコマンドや異常を取りこぼすのは、遅れよりむしろ恐い。
Week 4 | リアルタイムシステム12

2.6 この章のまとめ / ここは覚えて次へ

素朴版の​​​「遅れ」も​​​「取り​こぼし」も、​​​待っている​​​間 CPU が​​​塞がる​​​ブロッキングと​​​いう​​​一つの​問題から​​​生じる

覚える ①
sleep_ms()は CPU を返さない処理(ブロッキング)
覚える ②
塞がった時間は応答の遅れになり、
その間に始まって終わる短い入力は取りこぼされる
Week 4 | リアルタイムシステム13
Chapter 3

壁の前でやること:先人の型を調べる

「LED 点滅と​​​スイッチ」と​​​いう​​​単純な​​​お題にも、​​​
組み込み開発の​​​確立された​​​定石が​​​既に​​​そろっている

壁にぶつかる
素朴な実装が限界を迎える
先人の型を調べる
AI に聞く・定石を探す
次の段へ進む
定石を実装に落とす
↻ 今日の5段は、このループの繰り返し
Week 4 | リアルタイムシステム14

3.1 第2段:ノンブロッキング化(time_us_64)

現在時刻が​​​締め切りを​​​過ぎたら​​​実行し、​​​
前回の​​​締め切りに​​​周期を​​​足して​​​次を​​​予約する

// [Code-02] 第2段:ノンブロッキング版(骨格)uint64_t next_toggle_us = time_us_64();while (true) {    uint64_t now = time_us_64();    if (now >= next_toggle_us) {         // 締め切りが来たか        led_on = !led_on;        gpio_put(LED_PIN, led_on);        next_toggle_us += interval_ms*1000; // 次を予約    }    if (gpio_get(SW_PIN) == 0) {         // 毎周確認できる        interval_ms = (interval_ms == 500) ? 100 : 500;    }}
① 読む 現在時刻
② 比べる 締め切りと
③ 予約する 前回の締め切りに周期を足す(ずれない)

sleep が1行もない。

⚠ スイッチの if は“レベル”判定。高速ループでは押している間ずっと発火 → 次スライドで修正
Week 4 | リアルタイムシステム15

3.2 レベルではなくエッジ

高速ループで“今押されているか”を見ると、1回の押下の間に何千回も反転する。“押した瞬間”=立下り(1→0)だけを1回拾う

// [Code-03] 立下りエッジを自前で検出bool sw_prev = 1;                  // 前回の読み値(初期は H)while (true) {    // …点滅の締め切り判定は第2段のまま…    bool sw = gpio_get(SW_PIN);    // 今回の読み値    if (sw_prev == 1 && sw == 0)   // H→L の“その瞬間”だけ        interval_ms = (interval_ms == 500) ? 100 : 500;    sw_prev = sw;                  // 次回のために保存}
✕ レベルで判定
gpio_get() == 0
押している間ずっと真 → 毎周反転して制御不能
◯ エッジで判定
prev == 1 && now == 0
変化した1瞬間だけ真 → 1押下=1回だけ反転

ハードに見張らせる割り込みは 5章 で。

Week 4 | リアルタイムシステム16

3.3 いま作った形の名前

高速に回りながら各仕事の締め切りを確認する無限ループ=スーパーループ(協調的マルチタスク)

while (true) ↻
LED 点滅
すぐ CPU を返す
スイッチ確認
すぐ CPU を返す
↺ 高速に周回して順に見回る

各仕事が「すぐ CPU を返す」行儀を守ることで成立(だから協調的)。

組み込みの現場で広く使われる基本形。合宿のリアルタイム設計の出発点。

Week 4 | リアルタイムシステム17

3.4 この章のまとめ / ここは覚えて次へ

まず型を​調べる。​今回は​スーパーループと​いう​型を​学ぶ。

覚える ①
困ったら、まず先人の型を調べる(AI に聞いてよい)
覚える ②
ノンブロッキング
=待たずに経過を確認
覚える ③
その型がスーパーループ
(協調的マルチタスク)
Week 4 | リアルタイムシステム18
Chapter 4

第3段:タスクを足して同じ形に載せる

センサ読みもテレメトリ送信も、点滅と同じ「締め切りが来たかを確認して実行し、次を予約する」形で足せる

点滅
周期:短
締め切りが来たか?→実行→次を予約
センサ読み
周期:短
締め切りが来たか?→実行→次を予約
テレメトリ送信
周期:長
締め切りが来たか?→実行→次を予約

どのカードも同じ形 ── 周期の数字が違うだけ

Week 4 | リアルタイムシステム19

4.1 簡易スケジューラの構造

タスクごとに周期と前回の締め切りを持たせると、タスクの追加は if ブロック1つで済む

while (true) {
    uint64_t now = time_us_64();
    if (now - last_blink >= BLINK_US) { last_blink += BLINK_US; task_blink(); }    if (now - last_sense >= SENSE_US) { last_sense += SENSE_US; task_sense(); }
    if (now - last_telem >= TELEM_US) { last_telem += TELEM_US; task_telem(); }
    task_check_switch();              // イベントの確認は毎周
}
task_blink だけを追う ── 毎周 now − last_blink を測り、それが周期 BLINK_US に届いた瞬間だけ if が true になって実行する
now − last_blink now(時間) BLINK_US = 200 ms(周期・しきい値) last_blink=200=400=600=800 02004006008001000 now=140 last_blink=0 差=140 140 < 200 → if=false(何もしない) now=400 last_blink=200 差=200 200 ≥ 200 → true:task_blink(); last_blink += 200
各タスクは順番に実行 ── CPU は1周のなかで1つずつ処理するので、複数のタスクが同時に走って重複することはない。
Week 4 | リアルタイムシステム20
QUIZ

4.2 考えてみよう

このスーパーループに、1回の実行に 50 ms かかる画像処理タスクを足すと、10 ms 周期のタスクはどうなるか?

Aスケジューラが途中で切り替えるので、影響はほぼ出ない
B画像処理の実行中は走れず、
締め切りを最大で約 50 ms 過ぎる
C画像処理だけ自動的に後回しにされ、
10 ms 周期は保たれる
Week 4 | リアルタイムシステム21

4.3 この章のまとめ / ここは覚えて次へ

タスクは同じ形で増やせるが、1つでも実行の長い処理を挟むと、他のタスクの締め切りをまとめて壊す

覚える ①
タスクは「周期+前回の締め切り+確認」の同じ形に載る
覚える ②
長い処理を1つ挟むと、他の全タスクが締め切りを落とす
Week 4 | リアルタイムシステム22
Chapter 5

第4段:見回り(ポーリング)から割り込みへ

ソフトウェアが一定間隔で「来た?」と見に行くポーリングでは、イベントは確認の隙間で起こり、次の見回りまで気づけない

スイッチ押下(出来事)
確認
確認
確認
確認
確認
ここで気づく
時間 →
応答の遅れ(次の見回りまで)

見回りの​間隔が、​そのまま​応答の​遅れの​最大値に​なる。​イベントを​見逃すことも​ありうる。

Week 4 | リアルタイムシステム23

5.1 ポーリングと割り込みの比較

同じ出来事でも、ポーリングは次の見回りまで遅れる。割り込みは瞬間に処理を中断して ISR へ飛ぶため、遅れが小さく一定になる

ポーリング:ソフトウェアが見に行く── 隙間で起きた出来事は次の見回りまで待つ
出来事(スイッチ押下)
確認
確認
確認
確認
確認
時間 →
遅れ:最大で見回り間隔ぶん
割り込み:ハードウェアが知らせる── 出来事の瞬間に中断して ISR へ飛ぶ
ISR
メインループ
遅れ ≒ 0・一定
ISR = Interrupt Service Routine
日本語で「割り込みサービスルーチン」
時間 →
① 事前に「このピンが変化したら ISR を呼べ」と登録しておく
② 中断と復帰はハードが自動 ── メインループは気づかない
Week 4 | リアルタイムシステム24
WORK

5.2 手を動かす③:スイッチを割り込みで受ける

立下りエッジを​​​割り込み要因と​​​して​​​登録すると、​​​
押下の​​​瞬間に​​​メインの​​​処理を​​​中断して​​​ ISR が​​​呼ばれる

// [Code-04] 第4段:GPIO 割り込み(骨格) volatile uint32_t interval_ms = 500;   // 点滅間隔(ISR と共有) void on_switch(uint gpio, uint32_t events) {    interval_ms = (interval_ms == 500) ? 100 : 500;  // 間隔を切替} // main() で登録:押下=GND への立下りエッジgpio_set_irq_enabled_with_callback(SW_PIN, GPIO_IRQ_EDGE_FALL, true, &on_switch);
共有変数に volatile
volatile uint32_t interval_ms
メインループにとって知らないタイミングで値が変わりうることをコンパイラに示すため
Week 4 | リアルタイムシステム25

5.3 点滅の締め切りもハードに刻ませる:タイマ割り込み

ハードウェアタイマ(に​​​よる​​​タイマ割り込み)は、​​​
メインの​​​処理と​​​無関係に​​​一定間隔で​​​ ISR を​​​呼び続ける

// [Code-05] タイマ割り込みで点滅を刻む bool on_blink_timer(repeating_timer_t *t) {    led_on = !led_on;    gpio_put(LED_PIN, led_on);    return true;   // true で繰り返し継続}add_repeating_timer_ms(-500, on_blink_timer, NULL, &timer);
第1引数は負値で指定
add_repeating_timer_ms(-500, …)
Pico SDK の仕様。負値は「前回の開始時刻」から次を数えるため、ISR の実行時間を差し引いた正確な 500 ms 周期になる(正値は「前回の終了時刻」からで、処理時間の分だけ周期が延びる)
Week 4 | リアルタイムシステム26

5.4 ISR の作法:短く保つ

ISR は状態を更新する数行にとどめ、重い処理はメインループ側で行う

◯ 短い ISR:状態を更新して即座に戻る ── メインループの乱れは最小
割り込み発生
ISR
メインループ
続きを実行
時間 →
停止 ≒ 一瞬
✕ 重い ISR(printf / sleep …):長く居座り、その間すべてが止まる
割り込み発生
ISR(printf / sleep …)
メインループ
停止中
やっと続き
時間 →
この間ずっと停止 ── メインループも他の割り込みも待たされる
Week 4 | リアルタイムシステム27

5.6 この章のまとめ / ここは覚えて次へ

割り込みで応答の遅れは一定・極小になる。ただし ISR は短く保ち、共有変数には volatile を付ける

覚える ①
割り込み=出来事の瞬間にハードウェアが CPU を呼ぶ仕組み
覚える ②
ISR は短く保ち、状態の更新だけにとどめる
覚える ③
ISR とループで共有する変数には volatile を付ける
Week 4 | リアルタイムシステム28
Chapter 6

第5段:定型の仕事をハードウェアに委譲する

CPU がやっていた定型の仕事をハードウェアに設定として渡すと、以降 CPU は関与せずに動き続ける

① ソフトのループ
CPU 関与:大(毎周確認)
② タイマ割り込み
CPU 関与:小(切替時だけ)
③ ハードへ委譲(PWM)
CPU 関与:ゼロ(設定後)
Week 4 | リアルタイムシステム29

6.1 PWM とは:周期とデューティ比

カウンタが 0→wrap を1周する時間が「周期」、その中で H になる割合が「デューティ比」

Count wrap level 0 t T 2T 3T カウンタ 比較レベル level 1 周期 = T(周波数 = 1/T) V 3.3 0 t T 2T 3T GPIO 出力パルス H の区間 = デューティ比 = level ÷ (wrap+1)
カウンタ(count)が比較レベル(level)を超えると出力が H→L に切り替わる ── その幅がデューティ比。
周期(=周波数)

カウンタが 0 → wrap を1周する時間が1周期。周期が短いほど高い周波数で、wrap(と clkdiv)で決まる。

周波数 = クロック ÷ (clkdiv × (wrap+1))
デューティ比

1周期のうち出力が H の割合。比較レベル level で決まり、明るさやサーボ指令の「量」を表す。

デューティ比 = level ÷ (wrap+1)
Week 4 | リアルタイムシステム30

6.2 設定と使いどころ:数行で委譲、ただし点滅には速すぎる

数行の​​​設定で​​​委譲でき、​​​以降​​​ CPU は​​​関与しない。​​​
ただし速すぎて​​​点滅には​​​不向きで、​​​明るさ制御や​​​サーボ指令に​​​使う
。​​​

// [Code-06] 第5段:PWM に任せる(骨格) gpio_set_function(LED_PIN, GPIO_FUNC_PWM);uint slice = pwm_gpio_to_slice_num(LED_PIN);pwm_set_clkdiv(slice, 125.0f);   // 125 MHz → 1 MHzpwm_set_wrap(slice, 999);        // 0〜999 で折り返し → 1 kHzpwm_set_chan_level(slice, ch, 500);  // デューティ比 50%pwm_set_enabled(slice, true);    // 以降 CPU は関与しない
速すぎて点滅には不向き

16 ビットのカウンタと分周の上限で、基本的には高速動作。
1 Hz のゆっくりした点滅は PWM 単体では作れない。

LED の明るさ制御 ── 速い点滅がデューティ比=明るさに融ける
サーボモータの指令 ── パルス幅で角度・量を伝える(模擬衛星:リアクションホイール)
Week 4 | リアルタイムシステム31

6.3 委譲の手段:PWM は入口、主流は DMA・PIO

定型の仕事をハードに任せる手段は複数ある ── PWM は最も簡単な入口で、実務の主流は DMA・PIO

今日扱う
PWM
周期的な H/L 波形を作る専用ペリフェラル。数行の設定だけで委譲できる。使い方は簡単。
主流・難しい
DMA
Direct Memory Access。CPU を介さず、メモリ⇄ペリフェラル間で大量のデータを運ぶ。取り込みの定番。
主流・難しい
PIO
Programmable I/O。RP2040 特有。任意の信号プロトコルをハードで生成・受信できる。自由度が高い。
共通点:いずれも「CPU を介さず、定型の仕事をハードに任せる」委譲。DMA・PIO は強力だが習得が難しいので、今日は概念の紹介までにとどめる。
Week 4 | リアルタイムシステム32

6.4 この章のまとめ / ここは覚えて次へ

定型の仕事はハードへ委譲して CPU を空ける。PWM はその簡単な入口で、実務の主流は DMA・PIO

覚える ①
ハードへの委譲=定型の仕事をハードに任せ、CPU を本業に空ける(今日は PWM で体験)
覚える ②
委譲の主流は DMA・PIO(強力だが難しい)。CPU 時間は配分すべき有限の資源
Week 4 | リアルタイムシステム33
Chapter 8

並行と並列 / 残る2つの選択肢:マルチコア・RTOS の概説

他にも方法がある

マルチコア物理的に複数のコアで真の同時実行(RP2040 は core0 / core1 の2コア)

これまで:並行(1コアを時分割)

1個の CPU を高速に切り替え、同時に見せる。スーパーループ・割り込みはこれ。

マルチコアで可能に:並列(真の同時)

2つのコアが文字どおり同時に走り、仕事を物理的に分担(姿勢制御と通信を別コアへ)。

代償:同じ変数への同時書き込みが起き、排他制御(スピンロック・ミューテックス)が要る。

RTOS締め切りで強制的にタスクを切り替える OS(FreeRTOS が定番)

考え方:仕事をタスクに分け、それぞれに優先度を付ける。OS が実行するタスクを選び、より優先度の高いタスクの出番が来たら、実行中のタスクを途中で強制的に止めて切り替える(プリエンプション)。スーパーループのように各処理の「行儀」に頼らないのが違い。

得意:締め切りの厳しいタスクを優先して確実に間に合わせられる。タスクごとに独立して書けるので、処理が増えても構造が破綻しにくい。

代償:マルチコア同様、タスク間で共有するデータの競合が起きるため排他制御が要る。OS の分だけ学習コストとメモリ・実行時間のオーバーヘッドも増える。

Week 4 | リアルタイムシステム34

まとめ / 今日の解空間 ── 5つの型とその代償

まず解空間と設計の型を調べてから書き始める。知らない方法は選べない。

選択肢
得意
代償
スーパーループ
単純・見通しがよい
ジッタ・優先度に弱い
割り込み
出来事に遅れなく即応答
共有資源の競合
ハードへ委譲
定型仕事を CPU 抜きで継続
DMA・PIO は習得が難しい
マルチコア
真の並列で仕事を分担
排他制御が要る
RTOS
強制切り替えで締め切り厳守
複雑さ・競合が常駐
Week 4 | リアルタイムシステム35

10.1 次回予告:Week 5 宇宙開発とシステムズエンジニアリング

第2段階へ ── 宇宙機の要求と制約から開発の進め方を学ぶ

ミッション要求とサクセスクライテリアの全体像を提示
制約が V 字モデルやモデルベース開発を必然にする理由
模擬衛星のシステム構成図
Week 5 からは、この全体をどう設計・運用するかへ視点が上がる
Week 4 | リアルタイムシステム36
宿題

10.2 チャタリング込みで、スイッチ切り替えを完成させる

今日の​​​お題​「スイッチで​​​点滅速度を​​​切り​替える」を、​​​
Wokwi の​​​「Bounce」を​​​有効に​​​したまま​​​実装する

設定:Wokwi の「Bounce」(チャタリング模擬)を有効(=既定)のまま。1回の押下で接点が数十回開閉する

①スイッチ単体で printf で検証:LED は関係なくスイッチの H/L のみを printf 文で出力し、チャタリングの様子を観測する。
②チャタリング対策の実装:LED の点滅をスイッチ(チャタリングあり・対策あり)で切り替えるプログラムを実装する。
回答(提出):①②のコード(Wokwiのリンク)に加え、
採用した手法/広げた選択肢/最終的な選定理由 の3点を書く
Wokwi 上部のツールバーで「Bounce」にチェックが入った状態。Raspberry Pi Pico W にプッシュボタンと LED を配線
Wokwi の「Bounce」を有効にしたまま(画面上部のチェック)
Week 4 | リアルタイムシステム37