//! Button matrix scanner for CMDR Keyboard. //! //! The scanner owns a concrete set of matrix pins and produces a debounced //! boolean state for each key. use cortex_m::delay::Delay; use embedded_hal::digital::{InputPin, OutputPin}; use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp}; /// Abstraction over the physical row/column pins backing the button matrix. pub trait MatrixPinAccess { fn init_columns(&mut self); fn set_column_low(&mut self, column: usize); fn set_column_high(&mut self, column: usize); fn read_row(&mut self, row: usize) -> bool; } /// Concrete matrix pins built from RP2040 dynamic pins. type RowPin = Pin; type ColPin = Pin; /// Strongly typed bundle of row and column pins used during matrix scanning. pub struct MatrixPins { rows: [RowPin; ROWS], cols: [ColPin; COLS], } impl MatrixPins { pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self { Self { rows, cols } } } impl MatrixPinAccess for MatrixPins { fn init_columns(&mut self) { // Default every column high so rows can be strobed individually. for column in self.cols.iter_mut() { column.set_high().ok(); } } fn set_column_low(&mut self, column: usize) { // Pull the active column low before sampling its rows. self.cols[column].set_low().ok(); } fn set_column_high(&mut self, column: usize) { // Release the column after sampling to avoid ghosting. self.cols[column].set_high().ok(); } fn read_row(&mut self, row: usize) -> bool { // Treat a low level as a pressed switch; default to false on IO errors. self.rows[row].is_low().unwrap_or(false) } } /// Row/column scanned button matrix driver with debounce counters. pub struct ButtonMatrix { pins: P, pressed: [bool; KEYS], press_threshold: u8, release_threshold: u8, debounce_counter: [u8; KEYS], last_press_scan: [u32; KEYS], scan_counter: u32, min_press_gap_scans: u32, } impl ButtonMatrix where P: MatrixPinAccess, { pub fn new( pins: P, press_threshold: u8, release_threshold: u8, min_press_gap_scans: u32, ) -> Self { debug_assert_eq!(KEYS, ROWS * COLS); Self { pins, pressed: [false; KEYS], press_threshold, release_threshold, debounce_counter: [0; KEYS], last_press_scan: [0; KEYS], scan_counter: 0, min_press_gap_scans, } } pub fn init_pins(&mut self) { self.pins.init_columns(); } pub fn prime(&mut self, delay: &mut Delay, passes: usize) { for _ in 0..passes { self.scan_matrix(delay); } } pub fn scan_matrix(&mut self, delay: &mut Delay) { self.scan_counter = self.scan_counter.wrapping_add(1); for column in 0..COLS { self.pins.set_column_low(column); delay.delay_us(1); self.process_column(column); self.pins.set_column_high(column); delay.delay_us(1); } } pub(crate) fn process_column(&mut self, column: usize) { for row in 0..ROWS { let button_index = column + (row * COLS); let current_state = self.pins.read_row(row); 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 { self.pressed[button_index] = match current_state { true => self.should_register_press(button_index), false => false, }; self.debounce_counter[button_index] = 0; } } } pub fn buttons_pressed(&self) -> [bool; KEYS] { self.pressed } fn should_register_press(&mut self, button_index: usize) -> bool { let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]); let can_register = self.last_press_scan[button_index] == 0 || elapsed >= self.min_press_gap_scans; if can_register { self.last_press_scan[button_index] = self.scan_counter; } can_register } #[cfg(all(test, feature = "std"))] pub(crate) fn set_scan_counter(&mut self, value: u32) { self.scan_counter = value; } #[cfg(all(test, feature = "std"))] pub(crate) fn bump_scan_counter(&mut self) { self.scan_counter = self.scan_counter.wrapping_add(1); } } #[cfg(all(test, feature = "std"))] mod tests { use super::*; use core::cell::Cell; use std::rc::Rc; #[derive(Clone)] struct MockMatrixPins { row: Rc>, column: Rc>, } impl MockMatrixPins { fn new(row: Rc>, column: Rc>) -> Self { Self { row, column } } } impl MatrixPinAccess<1, 1> for MockMatrixPins { fn init_columns(&mut self) { self.column.set(true); } fn set_column_low(&mut self, _column: usize) { self.column.set(false); } fn set_column_high(&mut self, _column: usize) { self.column.set(true); } fn read_row(&mut self, _row: usize) -> bool { self.row.get() } } fn fixture() -> ( ButtonMatrix, Rc>, Rc>, ) { // Provide a matrix instance backed by mock row/column signals for testing. let row_state = Rc::new(Cell::new(false)); let column_state = Rc::new(Cell::new(false)); let pins = MockMatrixPins::new(row_state.clone(), column_state.clone()); let matrix = ButtonMatrix::new(pins, 5, 5, 200); (matrix, row_state, column_state) } #[test] fn init_sets_columns_high() { // Initialisation should drive the column line to its idle high level. let (mut matrix, _row, column) = fixture(); assert!(!column.get()); matrix.init_pins(); assert!(column.get()); } #[test] fn debounce_respects_threshold() { // Debounce counters must reach the threshold before toggling key state. let (mut matrix, row, _column) = fixture(); let mut states = matrix.buttons_pressed(); assert!(!states[0]); matrix.set_scan_counter(100); row.set(true); for _ in 0..4 { matrix.bump_scan_counter(); matrix.process_column(0); states = matrix.buttons_pressed(); assert!(!states[0]); } matrix.bump_scan_counter(); matrix.process_column(0); states = matrix.buttons_pressed(); assert!(states[0]); row.set(false); for _ in 0..4 { matrix.bump_scan_counter(); matrix.process_column(0); states = matrix.buttons_pressed(); assert!(states[0]); } matrix.bump_scan_counter(); matrix.process_column(0); states = matrix.buttons_pressed(); assert!(!states[0]); } #[test] fn min_press_gap_blocks_fast_retrigger() { // Verify that a second press faster than the configured gap is ignored until enough scans pass. let (mut matrix, row, _column) = fixture(); matrix.set_scan_counter(1); row.set(true); for _ in 0..5 { matrix.bump_scan_counter(); matrix.process_column(0); } assert!(matrix.buttons_pressed()[0]); row.set(false); for _ in 0..5 { matrix.bump_scan_counter(); matrix.process_column(0); } assert!(!matrix.buttons_pressed()[0]); row.set(true); for _ in 0..5 { matrix.bump_scan_counter(); matrix.process_column(0); } assert!(!matrix.buttons_pressed()[0]); for _ in 0..200 { matrix.bump_scan_counter(); } for _ in 0..5 { matrix.bump_scan_counter(); matrix.process_column(0); } assert!(matrix.buttons_pressed()[0]); } }