事前学習 第1段階 / Week 4
Photo: Laserlicht / Wikimedia Commons / CC BY-SA 4.0
0.1 今回の位置づけ / 第1段階の締めくくり
衛星の仕事は1つではない
1.1 リアルタイムの定義
速さでも平均でもなく、締め切りを毎回守れるか
| 処理 A | 処理 B | |
|---|---|---|
| 平均の速さ | 1 ms(速い) | 8 ms(遅い) |
| 最悪のとき | まれに 1 秒 | 常に 8 ms |
| 締め切り 10 ms を毎回守れるか | ✕ 破る回がある | ◯ 毎回守る |
1.2 締め切りの重さ
違反1回で意味を失う
破っても品質が下がるだけ
1.3 今日のお題
プルアップ抵抗:スイッチは GND に接続するかどうかを選ぶだけ
接続しないとき H にするため必要(I2C と同じ)
1.4 この章のまとめ / ここは覚えて次へ
第1段:sleep_ms() で待つ素朴版
// [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周のうち2つの sleep_ms() が大半を占め、スイッチ確認は末尾に一度だけ。読みやすく、点滅だけなら完璧に動く。
2.1 予想してみよう
2.2 手を動かす①:Wokwi で試す
2.4 遅れの原因
2.5 取りこぼしの原因
原因は遅れと同じ。1 周に 1 回、しかも「今このピンが押されているか」という現在のレベルしか見ていない。だから押されていた瞬間の長さで結果が分かれる。
2.6 この章のまとめ / ここは覚えて次へ
壁の前でやること:先人の型を調べる
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行もない。
3.2 レベルではなくエッジ
// [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; // 次回のために保存}
ハードに見張らせる割り込みは 5章 で。
3.3 いま作った形の名前
各仕事が「すぐ CPU を返す」行儀を守ることで成立(だから協調的)。
組み込みの現場で広く使われる基本形。合宿のリアルタイム設計の出発点。
3.4 この章のまとめ / ここは覚えて次へ
第3段:タスクを足して同じ形に載せる
どのカードも同じ形 ── 周期の数字が違うだけ
4.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(); // イベントの確認は毎周 }
4.3 この章のまとめ / ここは覚えて次へ
第4段:見回り(ポーリング)から割り込みへ
5.1 ポーリングと割り込みの比較
5.2 手を動かす③:スイッチを割り込みで受ける
// [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);
5.3 点滅の締め切りもハードに刻ませる:タイマ割り込み
// [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);
5.4 ISR の作法:短く保つ
5.6 この章のまとめ / ここは覚えて次へ
第5段:定型の仕事をハードウェアに委譲する
6.1 PWM とは:周期とデューティ比
カウンタが 0 → wrap を1周する時間が1周期。周期が短いほど高い周波数で、wrap(と clkdiv)で決まる。
1周期のうち出力が H の割合。比較レベル level で決まり、明るさやサーボ指令の「量」を表す。
6.2 設定と使いどころ:数行で委譲、ただし点滅には速すぎる
// [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 単体では作れない。
6.3 委譲の手段:PWM は入口、主流は DMA・PIO
6.4 この章のまとめ / ここは覚えて次へ
並行と並列 / 残る2つの選択肢:マルチコア・RTOS の概説
1個の CPU を高速に切り替え、同時に見せる。スーパーループ・割り込みはこれ。
2つのコアが文字どおり同時に走り、仕事を物理的に分担(姿勢制御と通信を別コアへ)。
代償:同じ変数への同時書き込みが起き、排他制御(スピンロック・ミューテックス)が要る。
考え方:仕事をタスクに分け、それぞれに優先度を付ける。OS が実行するタスクを選び、より優先度の高いタスクの出番が来たら、実行中のタスクを途中で強制的に止めて切り替える(プリエンプション)。スーパーループのように各処理の「行儀」に頼らないのが違い。
得意:締め切りの厳しいタスクを優先して確実に間に合わせられる。タスクごとに独立して書けるので、処理が増えても構造が破綻しにくい。
代償:マルチコア同様、タスク間で共有するデータの競合が起きるため排他制御が要る。OS の分だけ学習コストとメモリ・実行時間のオーバーヘッドも増える。
まとめ / 今日の解空間 ── 5つの型とその代償
10.1 次回予告:Week 5 宇宙開発とシステムズエンジニアリング
10.2 チャタリング込みで、スイッチ切り替えを完成させる
設定:Wokwi の「Bounce」(チャタリング模擬)を有効(=既定)のまま。1回の押下で接点が数十回開閉する