ポーリングは、実のところイマイチです
そういえば、方向指示器は普通点滅しますよね? ボタンが押されたときに方向指示器の LED を点滅させるには、プログラムをどう拡張すればよいでしょうか。 Hello World プログラムで LED を点滅させる方法はすでに知っています。LED を点灯し、しばらく待ってから消灯します。 しかし、ボタンの押下も確認しながら、これをメインループの中でどう実現すればよいのでしょうか? たとえば、次のようなことを試せます。
#![allow(unused)]
fn main() {
loop {
if button_a.is_low().unwrap() {
// 左矢印を点滅
display.show(&LEFT_ARROW);
timer.delay_ms(500_u32);
display.show(&BLANK);
timer.delay_ms(500_u32);
} else if button_b.is_low().unwrap() {
// 右矢印を点滅
display.show(&RIGHT_ARROW);
timer.delay_ms(500_u32);
display.show(&BLANK);
timer.delay_ms(500_u32);
} else {
display.show(&BLANK);
}
timer.delay_ms(10_u32);
}
}
問題がわかりますか? ここでは同時に 2 つのことをしようとしています。
- ボタンの押下を確認する
- LED を点滅させる
しかし、プロセッサは一度に 1 つのことしかできません。 点滅のための遅延中にボタンを押すと、遅延が終わってループが再開されるまで、プロセッサは応答できません。 その結果、ほとんど応答しないプログラムになってしまいます(実際に試して、ボタンの反応がどれだけ遅いか見てみてください)。
もっと「賢い」プログラムなら、点滅の遅延が動いている間、プロセッサは実際には何もしていないことを理解しています。 遅延が終わるのを待っている間にも、プログラムは別のことを十分に行えます。つまり、ボタンの押下を確認できるのです。
スーパーループ
組み込みシステムにおける スーパーループ という用語は、順番にいくつものことを行うメイン制御ループを指します。 これは、これまで使ってきた単純な制御フローを自然に拡張したものです。 一見すると複数のことが同時に起きているように見えるロジックを扱うには、イベントに対して十分に応答できるよう、プログラムの構造をもう少し賢く組み立てる必要があります。
方向指示器のプログラムのように、ボタンが押されている間は LED を点滅させ、ボタンが離されたら素早く点滅を止めたい場合、プログラムのさまざまな状態を表す「状態機械」を作れます。 ボタンについては 3 つの状態があります。
- どのボタンも押されていない
- ボタン A が押されている
- ボタン B が押されている
表示についても 3 つの状態があります。
- どの LED も点灯していない
- 表示がアクティブな点滅状態にある(LED が点灯している)
- 表示が非アクティブな点滅状態にある(LED は消灯しており、点滅周期が終わったら再び点灯するのを待っている)
応答性を確保する必要があるため、これらの異なる状態を組み合わせなければなりません。 プログラムのすべての状態を完全に表すと、次のようになります。
- どのボタンも押されていない
- ボタン A が押されており、アクティブな点滅状態にある(左矢印が表示に出ている)
- ボタン A が押されており、非アクティブな点滅状態にある(表示には何も出ていない)
- ボタン B が押されており、アクティブな点滅状態にある(右矢印が表示に出ている)
- ボタン B が押されており、非アクティブな点滅状態にある(表示には何も出ていない)
いずれかのボタンが最初に押されて、状態 (1) から状態 (2) または (4) に遷移したとき、ボタンが押された瞬間からカウントアップするタイマーカウンタを初期化します。 タイマーがあるしきい値(たとえば 0.5 秒)に達し、かつボタンがまだ押されたままであれば、それぞれ状態 (3) または (5) に遷移し、タイマーカウンタを再初期化します。 その後、タイマーが再びあるしきい値に達したら、それぞれ状態 (2) または (4) に戻ります。 状態 (2)、(3)、(4)、(5) のいずれかにいる間に、ボタンがもう押されていないことがわかった場合は、状態 (1) に戻ります。
メインのスーパーループ制御フローでは、ボタンを繰り返しポーリングし、現在のタイマーカウンタ(あれば)をしきい値と比較し、上記の条件のいずれかを満たしたら状態を変更します。
このスーパーループはデモとして実装してあります(examples/blink-held.rs)。ただし、状態機械は、ボタン A が押されている間だけ LED を点滅させるように単純化しています。
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::{InputPin, OutputPin};
use microbit::hal::timer::Timer;
use microbit::{hal::gpio, Board};
use panic_rtt_target as _;
use rtt_target::rtt_init_print;
const ON_TICKS: u16 = 25;
const OFF_TICKS: u16 = 75;
#[derive(Clone, Copy)]
enum Light {
Lit(u16),
Unlit(u16),
}
impl Light {
fn flip(self) -> Self {
match self {
Light::Lit(_) => Light::Unlit(OFF_TICKS),
Light::Unlit(_) => Light::Lit(ON_TICKS),
}
}
fn tick_down(self) -> Self {
match self {
Light::Lit(ticks) => Light::Lit(ticks.max(1) - 1),
Light::Unlit(ticks) => Light::Unlit(ticks.max(1) - 1),
}
}
}
#[derive(Clone, Copy)]
enum Indicator {
Off,
Blinking(Light),
}
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = Board::take().unwrap();
let mut timer = Timer::new(board.TIMER0);
// Configure buttons
let mut button_a = board.buttons.button_a;
// Configure LED (top-left LED at row1, col1)
let mut row1 = board
.display_pins
.row1
.into_push_pull_output(gpio::Level::Low);
let _col1 = board
.display_pins
.col1
.into_push_pull_output(gpio::Level::Low);
let mut state = Indicator::Off;
loop {
let button_pressed = button_a.is_low().unwrap();
match (button_pressed, state) {
// Turn indicator off when no button.
(false, _) => {
row1.set_low().unwrap();
state = Indicator::Off;
}
//
(true, Indicator::Off) => {
row1.set_high().unwrap();
state = Indicator::Blinking(Light::Lit(ON_TICKS));
}
(true, Indicator::Blinking(light)) => match light {
Light::Lit(0) | Light::Unlit(0) => {
let light = light.flip();
match light {
Light::Lit(_) => row1.set_high().unwrap(),
Light::Unlit(_) => row1.set_low().unwrap(),
}
state = Indicator::Blinking(light);
}
Light::Lit(_) | Light::Unlit(_) => {
state = Indicator::Blinking(light.tick_down());
}
},
}
timer.delay_ms(10_u32);
}
}
これはまだ少し複雑です。10ms のループ遅延でも ボタンの変化を捉えるには十分すぎるほどです。
スーパーループは機能しますし、組み込みシステムでもよく使われますが、プログラマはイベントに対する高い応答性を維持できるよう注意しなければなりません。 このスーパーループのプログラムが、前の単純なポーリングの例とどう違うかに注目してください。 上に示したスーパーループにおける各状態遷移のステップは、かなり短い時間で終わるべきです(たとえば、プロセッサを長時間ブロックしてイベントを見逃す可能性のある遅延は、もはやありません)。 すべての状態遷移が高速で、比較的ブロッキングしないスーパーループへと単純なポーリングプログラムを変換するのは、必ずしも簡単ではありません。そのような場合には、同時に実行されるさまざまなイベントを処理するための別の手法に頼る必要があります。
並行性
複数のことを同時に行うことを 並行 プログラミングと呼びます。 並行性はプログラミングのさまざまな場面に現れますが、特に組み込みシステムでは重要です。 高い応答性を維持しながら周辺機器とやり取りするシステムを実装するための手法は数多くあります(たとえば、割り込み処理、協調的マルチタスク、イベントキューなど)。 これらのいくつかは後の章で見ていきます。
組み込みの文脈における並行性のよい入門が here にあるので、 先に進む前に読んでおくとよいでしょう。
ひとまず、button_a.is_low() や display_pins.row1.set_high() を呼び出したときに何が起きているのかを、もう少し深く見ていきましょう。