//! Button matrix scanner for CMDR Joystick 25 //! //! Scans a row/column matrix and produces a debounced boolean state for each //! button. Designed for small matrices on microcontrollers where timing is //! deterministic and GPIO is plentiful. //! //! - Rows are configured as inputs with pull‑ups //! - Columns are configured as push‑pull outputs //! - Debounce is handled per‑button using a simple counter //! - A tiny inter‑column delay is inserted to allow signals to settle use core::convert::Infallible; use cortex_m::delay::Delay; use embedded_hal::digital::{InputPin, OutputPin}; /// Button matrix driver /// /// Generics /// - `R`: number of rows /// - `C`: number of columns /// - `N`: total number of buttons (usually `R * C`) /// /// Example /// ```ignore /// // 4 rows, 6 columns, 24 buttons, 5-scan debounce /// let mut matrix: ButtonMatrix<4, 6, 24> = ButtonMatrix::new(row_pins, col_pins, 5); /// matrix.init_pins(); /// loop { /// matrix.scan_matrix(&mut delay); /// let states = matrix.buttons_pressed(); /// // use `states` /// } /// ``` pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> { rows: &'a mut [&'a mut dyn InputPin; R], cols: &'a mut [&'a mut dyn OutputPin; C], pressed: [bool; N], debounce: u8, debounce_counter: [u8; N], } impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> { /// Creates a new button matrix. /// /// Arguments /// - `rows`: array of row pins (inputs with pull‑ups) /// - `cols`: array of column pins (push‑pull outputs) /// - `debounce`: number of consecutive scans a change must persist before it is accepted pub fn new( rows: &'a mut [&'a mut dyn InputPin; R], cols: &'a mut [&'a mut dyn OutputPin; C], debounce: u8, ) -> Self { Self { rows, cols, pressed: [false; N], debounce, debounce_counter: [0; N], } } /// Initialize the matrix GPIOs (set all columns high). /// /// Call once before the first scan. pub fn init_pins(&mut self) { for col in self.cols.iter_mut() { col.set_high().unwrap(); } } /// Scan the matrix and update each button's debounced state. /// /// Call at a fixed cadence. The simple debounce uses a per‑button counter: only /// when a changed level is observed for `debounce` consecutive scans is the /// new state committed. /// /// Arguments /// - `delay`: short delay implementation used to let signals settle between columns pub fn scan_matrix(&mut self, delay: &mut Delay) { 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); } } /// Process a single column: drive low, sample rows, update debounce state, then release high. /// /// Arguments /// - `col_index`: index of the column being scanned 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] += 1; if self.debounce_counter[button_index] >= self.debounce { self.pressed[button_index] = current_state; } } } /// Return a copy of the debounced pressed state for all buttons. /// /// For small `N` this copy is cheap. If needed, the API could be extended to /// return a reference in the future. 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>, } impl MockInputPin { fn new(state: Rc>) -> Self { Self { state } } } impl ErrorType for MockInputPin { type Error = Infallible; } impl InputPin for MockInputPin { fn is_high(&mut self) -> Result { Ok(!self.state.get()) } fn is_low(&mut self) -> Result { Ok(self.state.get()) } } struct MockOutputPin { state: Rc>, } impl MockOutputPin { fn new(state: Rc>) -> 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>, Rc>, ) { let row_state = Rc::new(Cell::new(false)); let col_state = Rc::new(Cell::new(false)); let row_pin: &'static mut dyn InputPin = Box::leak(Box::new(MockInputPin::new(row_state.clone()))); let col_pin: &'static mut dyn OutputPin = Box::leak(Box::new(MockOutputPin::new(col_state.clone()))); let rows: &'static mut [&'static mut dyn InputPin; 1] = Box::leak(Box::new([row_pin])); let cols: &'static mut [&'static mut dyn OutputPin; 1] = Box::leak(Box::new([col_pin])); let matrix = ButtonMatrix::new(rows, cols, 2); (matrix, row_state, col_state) } #[test] fn init_pins_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 process_column_obeys_debounce() { let (mut matrix, row_state, _col_state) = matrix_fixture(); let mut states = matrix.buttons_pressed(); assert!(!states[0]); row_state.set(true); matrix.process_column(0); matrix.process_column(0); states = matrix.buttons_pressed(); assert!(states[0]); row_state.set(false); matrix.process_column(0); matrix.process_column(0); states = matrix.buttons_pressed(); assert!(!states[0]); } }