226 lines
6.8 KiB
Rust
226 lines
6.8 KiB
Rust
//! 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<Error = Infallible>; R],
|
||
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; 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<Error = Infallible>; R],
|
||
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; 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<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, 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]);
|
||
}
|
||
}
|