デバウンス
前のセクションで述べたように、ハードウェアは少し……特殊なことがあります。これは MB2 の ボタンでまさに当てはまり、実際にはほぼあらゆるシステムのほぼあらゆる押しボタンやスイッチでも同様です。1 回のキー押下で複数の割り込みが発生しているなら、その原因はおそらくスイッチの 「バウンス」として知られている現象です。これは文字どおり名前のとおりの現象です。スイッチの 電気接点が接触するとき、しっかり接続が確立されるまでの間に、いったん離れて再び接触することを 短時間のうちに何度か繰り返すことがあります。残念ながら、私たちのマイクロプロセッサは機械的な 基準では 非常に 高速です。このバウンスの 1 回 1 回が新しい割り込みを発生させます。
スイッチを「デバウンス」するには、ボタン押下の割り込みを 1 回受け取ったあと、短時間はそれを 処理しないようにする必要があります。通常は 50〜100ms が適切なデバウンス間隔です。デバウンスの タイミング処理は難しそうです。割り込みハンドラの中でビジーウェイトするのは絶対に避けたいですし、 かといってメインプログラムでこれを扱うのも簡単ではありません。
この解決策は、別の形のハードウェア並行性、つまりこれまでも何度も使ってきた TIMER
ペリフェラルにあります。「有効な」ボタン割り込みを受け取ったときにタイマーを設定し、そのボタンに
ついてはタイマーペリフェラルが十分な時間を数え終えるまで、それ以降の割り込みには応答しないように
できます。nrf-hal のタイマーは、32 ビットのカウント値と 1 MHz の「ティックレート」
(1 秒あたり 100 万ティック)で設定されています。100ms のデバウンスなら、タイマーに
100,000 ティック数えさせるだけです。ボタンの割り込みハンドラは、タイマーが動作中であることを
確認したら、何もしなければよいのです。
これらすべての実装は次の例(examples/count-debounce.rs)で確認できます。この例を実行すると、
ボタンを 1 回押すごとに 1 回だけカウントされるはずです。
#![no_main]
#![no_std]
use core::sync::atomic::{
AtomicUsize,
Ordering::{AcqRel, Acquire},
};
use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section_lock_mut::LockMut;
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
use microbit::{
hal::{
self, gpiote,
pac::{self, interrupt},
},
Board,
};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
static GPIOTE_PERIPHERAL: LockMut<gpiote::Gpiote> = LockMut::new();
static DEBOUNCE_TIMER: LockMut<hal::Timer<pac::TIMER0>> = LockMut::new();
// 100ms at 1MHz count rate.
const DEBOUNCE_TIME: u32 = 100 * 1_000_000 / 1000;
#[interrupt]
fn GPIOTE() {
DEBOUNCE_TIMER.with_lock(|debounce_timer| {
if debounce_timer.read() == 0 {
let _ = COUNTER.fetch_add(1, AcqRel);
debounce_timer.start(DEBOUNCE_TIME);
}
});
GPIOTE_PERIPHERAL.with_lock(|gpiote| {
gpiote.channel0().reset_events();
});
}
#[entry]
fn main() -> ! {
rtt_init_print!();
let board = Board::take().unwrap();
let button_a = board.buttons.button_a.into_floating_input();
// Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO
// wire goes low).
let gpiote = gpiote::Gpiote::new(board.GPIOTE);
let channel = gpiote.channel0();
channel
.input_pin(&button_a.degrade())
.hi_to_lo()
.enable_interrupt();
channel.reset_events();
GPIOTE_PERIPHERAL.init(gpiote);
// Set up the debounce timer.
let mut debounce_timer = hal::Timer::new(board.TIMER0);
debounce_timer.disable_interrupt();
debounce_timer.reset_event();
DEBOUNCE_TIMER.init(debounce_timer);
// Set up the NVIC to handle interrupts.
unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
pac::NVIC::unpend(pac::Interrupt::GPIOTE);
// Because we're not disabling GPIOTE interrupts even during the debounce timer countdown,
// we can get extra button interrupts even during the debounce interval.
// It would be reasonable to disable button interrupts when the debounce timer is started
// and re-enable them when it expires, but this would require a debounce timer interrupt handler.
// To make a simple "fix" for this that doesn't hurt the readability,
// we introduce the cur_count "guard" variable.
let mut cur_count = 0;
loop {
// "wait for interrupt": CPU goes to sleep until an interrupt.
asm::wfi();
let count = COUNTER.load(Acquire);
if count > cur_count {
rprintln!("ouch {}", count);
cur_count = count;
}
}
}
注 MB2 のボタンは少し扱いにくく、「カチッ」とした感触がある程度まで押しても、実際には スイッチが接触していないことがよくあります。テストするときは、爪でボタンを押すことをおすすめします。