//! Button matrix scanner for CMDR Joystick. //! //! Mirrors the refactor performed for the keyboard firmware: the matrix owns //! concrete pins, exposes a small `MatrixPinAccess` trait, and keeps the //! debouncing + minimum press spacing behaviour identical to the original //! joystick implementation. use cortex_m::delay::Delay; use embedded_hal::digital::{InputPin, OutputPin}; use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp}; /// Abstraction over the matrix pins so the scanner can work with either the /// concrete RP2040 pins or test doubles. 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 backed by RP2040 GPIO using dynamic pin IDs. type RowPin = Pin; type ColPin = Pin; 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 all columns high so rows can be strobed one at a time. for column in self.cols.iter_mut() { let _ = column.set_high(); } } fn set_column_low(&mut self, column: usize) { // Pull the active column low before scanning its rows. let _ = self.cols[column].set_low(); } fn set_column_high(&mut self, column: usize) { // Release the column after scanning so other columns remain idle. let _ = self.cols[column].set_high(); } fn read_row(&mut self, row: usize) -> bool { // Treat any low level as a pressed switch, defaulting to false on IO errors. self.rows[row].is_low().unwrap_or(false) } } /// Row/column scanned button matrix driver with debounce counters and minimum /// spacing between subsequent presses of the same key. pub struct ButtonMatrix where P: MatrixPinAccess, { pins: P, pressed: [bool; BUTTONS], debounce_threshold: u8, debounce_counter: [u8; BUTTONS], last_press_scan: [u32; BUTTONS], min_press_gap_scans: u32, scan_counter: u32, } impl ButtonMatrix where P: MatrixPinAccess, { pub fn new(pins: P, debounce_threshold: u8, min_press_gap_scans: u32) -> Self { debug_assert_eq!(BUTTONS, ROWS * COLS); Self { pins, pressed: [false; BUTTONS], debounce_threshold, debounce_counter: [0; BUTTONS], last_press_scan: [0; BUTTONS], min_press_gap_scans, scan_counter: 0, } } 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 fn buttons_pressed(&self) -> [bool; BUTTONS] { self.pressed } fn process_column(&mut self, column: usize) { // Drive a single column scan to update button press history. for row in 0..ROWS { let index = column + (row * COLS); let current_state = self.pins.read_row(row); if current_state == self.pressed[index] { self.debounce_counter[index] = 0; continue; } self.debounce_counter[index] = self.debounce_counter[index].saturating_add(1); if self.debounce_counter[index] < self.debounce_threshold { continue; } self.debounce_counter[index] = 0; if current_state { if self.should_register_press(index) { self.pressed[index] = true; } } else { self.pressed[index] = false; } } } fn should_register_press(&mut self, index: usize) -> bool { // Decide if a press should register given debounce timing. let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[index]); let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans; if can_register { self.last_press_scan[index] = self.scan_counter; } can_register } #[cfg(all(test, feature = "std"))] pub(crate) fn process_column_for_test(&mut self, column: usize) { self.process_column(column); } #[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 MockPins { row_state: Rc>, column_state: Rc>, } impl MockPins { fn new(row_state: Rc>, column_state: Rc>) -> Self { // Build a button matrix scanner with default state tracking arrays. Self { row_state, column_state, } } } impl MatrixPinAccess<1, 1> for MockPins { fn init_columns(&mut self) { // Simulate the hardware by driving the single column high by default. self.column_state.set(true); } fn set_column_low(&mut self, _column: usize) { // Drop the mock column low to emulate scanning behaviour. self.column_state.set(false); } fn set_column_high(&mut self, _column: usize) { // Release the mock column back to the idle high state. self.column_state.set(true); } fn read_row(&mut self, _row: usize) -> bool { // Return the mocked row state so tests can control pressed/unpressed. self.row_state.get() } } fn fixture() -> ( ButtonMatrix, Rc>, Rc>, ) { let row = Rc::new(Cell::new(false)); let column = Rc::new(Cell::new(true)); let pins = MockPins::new(row.clone(), column.clone()); let matrix = ButtonMatrix::new(pins, 2, 3); (matrix, row, column) } #[test] fn debounce_requires_consecutive_scans() { // Debounce logic should require two consecutive pressed scans before registering. let (mut matrix, row, _column) = fixture(); matrix.set_scan_counter(1); row.set(true); matrix.bump_scan_counter(); matrix.process_column_for_test(0); assert!(!matrix.buttons_pressed()[0]); matrix.bump_scan_counter(); matrix.process_column_for_test(0); assert!(matrix.buttons_pressed()[0]); } }