cmdr-keyboard/rp2040/src/button_matrix.rs

224 lines
7.0 KiB
Rust

//! Button matrix scanner for CMDR Keyboard 42
//!
//! Ported from the joystick firmware for consistent behaviour and structure.
//! Scans a row/column matrix and produces a debounced boolean state for each
//! button using per-button counters and small inter-column delays.
use core::convert::Infallible;
use cortex_m::delay::Delay;
use embedded_hal::digital::{InputPin, OutputPin};
/// Row/column scanned button matrix driver with debounce counters.
pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
pressed: [bool; N],
press_threshold: u8,
release_threshold: u8,
debounce_counter: [u8; N],
// Additional protection: minimum time between same-key presses
last_press_scan: [u32; N],
scan_counter: u32,
}
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
pub fn new(
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
press_threshold: u8,
release_threshold: u8,
) -> Self {
Self {
rows,
cols,
pressed: [false; N],
press_threshold,
release_threshold,
debounce_counter: [0; N],
last_press_scan: [0; N],
scan_counter: 0,
}
}
pub fn init_pins(&mut self) {
for col in self.cols.iter_mut() {
col.set_high().unwrap();
}
}
pub fn scan_matrix(&mut self, delay: &mut Delay) {
self.scan_counter = self.scan_counter.wrapping_add(1);
for col_index in 0..self.cols.len() {
self.cols[col_index].set_low().unwrap();
delay.delay_us(1);
self.process_column(col_index);
self.cols[col_index].set_high().unwrap();
delay.delay_us(1);
}
}
fn process_column(&mut self, col_index: usize) {
for row_index in 0..self.rows.len() {
let button_index: usize = col_index + (row_index * C);
let current_state = self.rows[row_index].is_low().unwrap();
if current_state == self.pressed[button_index] {
self.debounce_counter[button_index] = 0;
continue;
}
self.debounce_counter[button_index] =
self.debounce_counter[button_index].saturating_add(1);
let threshold = if current_state {
self.press_threshold
} else {
self.release_threshold
};
if self.debounce_counter[button_index] >= threshold {
// Additional protection for press events: minimum 20 scans (5ms) between presses
if current_state { // Pressing
let scans_since_last = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]);
if scans_since_last >= 20 { // 5ms at 250μs scan rate
self.pressed[button_index] = current_state;
self.last_press_scan[button_index] = self.scan_counter;
}
} else { // Releasing
self.pressed[button_index] = current_state;
}
self.debounce_counter[button_index] = 0;
}
}
}
pub fn buttons_pressed(&mut self) -> [bool; N] {
self.pressed
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use core::cell::Cell;
use embedded_hal::digital::ErrorType;
use std::rc::Rc;
struct MockInputPin {
state: Rc<Cell<bool>>,
}
impl MockInputPin {
fn new(state: Rc<Cell<bool>>) -> Self {
Self { state }
}
}
impl ErrorType for MockInputPin {
type Error = Infallible;
}
impl InputPin for MockInputPin {
fn is_high(&mut self) -> Result<bool, Self::Error> {
Ok(!self.state.get())
}
fn is_low(&mut self) -> Result<bool, Self::Error> {
Ok(self.state.get())
}
}
struct MockOutputPin {
state: Rc<Cell<bool>>,
}
impl MockOutputPin {
fn new(state: Rc<Cell<bool>>) -> Self {
Self { state }
}
}
impl ErrorType for MockOutputPin {
type Error = Infallible;
}
impl OutputPin for MockOutputPin {
fn set_high(&mut self) -> Result<(), Self::Error> {
self.state.set(true);
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
self.state.set(false);
Ok(())
}
}
fn matrix_fixture() -> (
ButtonMatrix<'static, 1, 1, 1>,
Rc<Cell<bool>>,
Rc<Cell<bool>>,
) {
let row_state = Rc::new(Cell::new(false));
let col_state = Rc::new(Cell::new(false));
let row_pin: &'static mut dyn InputPin<Error = Infallible> =
Box::leak(Box::new(MockInputPin::new(row_state.clone())));
let col_pin: &'static mut dyn OutputPin<Error = Infallible> =
Box::leak(Box::new(MockOutputPin::new(col_state.clone())));
let rows: &'static mut [&'static mut dyn InputPin<Error = Infallible>; 1] =
Box::leak(Box::new([row_pin]));
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
Box::leak(Box::new([col_pin]));
let matrix = ButtonMatrix::new(rows, cols, 5, 5);
(matrix, row_state, col_state)
}
#[test]
fn init_sets_columns_high() {
let (mut matrix, _row_state, col_state) = matrix_fixture();
assert!(!col_state.get());
matrix.init_pins();
assert!(col_state.get());
}
#[test]
fn debounce_respects_threshold() {
let (mut matrix, row_state, _col_state) = matrix_fixture();
let mut states = matrix.buttons_pressed();
assert!(!states[0]);
// Set scan counter to start with enough history
matrix.scan_counter = 100;
row_state.set(true);
// Need 5 scans to register press
for _ in 0..4 {
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]); // Still not pressed
}
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.process_column(0); // 5th scan
states = matrix.buttons_pressed();
assert!(states[0]); // Now pressed
row_state.set(false);
// Need 5 scans to register release
for _ in 0..4 {
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(states[0]); // Still pressed
}
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
matrix.process_column(0); // 5th scan
states = matrix.buttons_pressed();
assert!(!states[0]); // Now released
}
}