Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Устранение дребезга

Как я упоминал в предыдущем разделе, аппаратура может быть немного… особенной. Это точно относится к кнопкам MB2, да и вообще почти к любой кнопке или переключателю почти в любой системе. Если вы видите несколько прерываний для одного нажатия, скорее всего, это результат так называемого «дребезга» контактов (switch bouncing). Название говорит само за себя: когда электрические контакты переключателя смыкаются, они могут отскакивать, снова и снова замыкаясь за очень короткое время, прежде чем установится надёжное соединение. К сожалению, наш микропроцессор очень быстр по механическим меркам: каждый такой отскок порождает новое прерывание.

Чтобы «устранить дребезг» (debounce) переключателя, нужно не обрабатывать прерывания от нажатия кнопки в течение короткого времени после того, как получили одно. Обычно хорошим интервалом подавления дребезга считаются 50–100 мс. Подобрать время подавления дребезга непросто: в обработчике прерывания точно не стоит крутиться в ожидании, а разбираться с этим в основной программе было бы трудно.

Решение приходит через ещё одну форму аппаратной конкурентности: через TIMER, которым мы уже много раз пользовались. Можно запустить таймер, когда получено «хорошее» прерывание от кнопки, и не реагировать на дальнейшие прерывания от этой кнопки, пока периферия таймера не отсчитает достаточно времени. Таймеры в nrf-hal настроены с 32-битным счётчиком и «частотой тиков» 1 МГц: миллион тиков в секунду. Для подавления дребезга в 100 мс достаточно, чтобы таймер отсчитал 100 000 тиков. Всякий раз, когда обработчик прерывания кнопки видит, что таймер запущен, он может просто ничего не делать.

Реализация всего этого показана в следующем примере (examples/count-debounce.rs). При запуске примера вы должны увидеть по одному счёту на каждое нажатие кнопки.

#![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();

// 100 мс при частоте счёта 1 МГц.
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();

    // Настраиваем GPIOTE так, чтобы он генерировал прерывание при нажатии Button A (GPIO
    // линия переходит в низкий уровень).
    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);

    // Настраиваем таймер подавления дребезга.
    let mut debounce_timer = hal::Timer::new(board.TIMER0);
    debounce_timer.disable_interrupt();
    debounce_timer.reset_event();
    DEBOUNCE_TIMER.init(debounce_timer);

    // Настраиваем NVIC для обработки прерываний.
    unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) };
    pac::NVIC::unpend(pac::Interrupt::GPIOTE);

    // Поскольку мы не отключаем прерывания GPIOTE даже во время отсчёта таймера подавления дребезга,
    // дополнительные прерывания от кнопки могут приходить и внутри интервала подавления.
    // Разумно было бы отключать прерывания кнопки, когда запущен таймер подавления дребезга,
    // и включать их снова по его истечении, но тогда понадобился бы обработчик прерывания таймера.
    // Чтобы простое «исправление» не ухудшало читаемость,
    //  вводим переменную-«охранник» cur_count.
    let mut cur_count = 0;
    loop {
        // «wait for interrupt»: процессор засыпает до прихода прерывания.
        asm::wfi();
        let count = COUNTER.load(Acquire);
        if count > cur_count {
            rprintln!("ouch {}", count);
            cur_count = count;
        }
    }
}

ПРИМЕЧАНИЕ. Кнопки MB2 немного капризны: очень легко нажать на кнопку достаточно, чтобы почувствовать «щелчок», но недостаточно, чтобы контакт действительно замкнулся. При тестировании рекомендую нажимать кнопку ногтем.