Debouncing
As I mentioned in the last section, hardware can be a little… special. This is definitely the case for the buttons on the MB2, and really for almost any pushbutton or switch in almost any system. If you are seeing several interrupts for a single keypress, it is probably the result of what is known as switch "bouncing". This is literally what the name implies: as the electrical contacts of the switch come together, they may bounce apart and then recontact several times rather quickly before establishing a solid connection. Unfortunately, our microprocessor is very fast by mechanical standards: each one of these bounces makes a new interrupt.
To "debounce" the switch, you need to not process button press interrupts for a short time after you receive one. 50-100ms is typically a good debounce interval. Debounce timing seems hard: you definitely don't want to spin in an interrupt handler, and yet it would be hard to deal with this in the main program.
The solution comes through another form of hardware concurrency: the TIMER peripheral we have used
a bunch already. You can set the timer when a "good" button interrupt is received, and not respond
to further interrupts for that button until the timer peripheral has counted enough time off. The
timers in nrf-hal come configured with a 32-bit count value and a "tick rate" of 1 MHz: a million
ticks per second. For a 100ms debounce, just let the timer count off 100,000 ticks. Anytime the
button interrupt handler sees that the timer is running, it can just do nothing.
The implementation of all this can be seen in the next example (examples/count-debounce.rs). When
you run the example you should see one count per button press.
#![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); 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; } } }
NOTE The buttons on the MB2 are a little fiddly: it's pretty easy to push one down enough to feel a "click" but not enough to actually make contact with the switch. I recommend using a fingernail to press the button when testing.