cmdr-joystick/rp2040/src/button_matrix.rs

226 lines
6.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 pullups
//! - Columns are configured as pushpull outputs
//! - Debounce is handled perbutton using a simple counter
//! - A tiny intercolumn 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 pullups)
/// - `cols`: array of column pins (pushpull 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 perbutton 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]);
}
}