cmdr-joystick/rp2040/src/button_matrix.rs

245 lines
7.6 KiB
Rust

//! 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<const ROWS: usize, const COLS: usize> {
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<DynPinId, FunctionSioInput, PullUp>;
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
pub struct MatrixPins<const ROWS: usize, const COLS: usize> {
rows: [RowPin; ROWS],
cols: [ColPin; COLS],
}
impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self {
Self { rows, cols }
}
}
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
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<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
where
P: MatrixPinAccess<ROWS, COLS>,
{
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<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
ButtonMatrix<P, ROWS, COLS, BUTTONS>
where
P: MatrixPinAccess<ROWS, COLS>,
{
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<Cell<bool>>,
column_state: Rc<Cell<bool>>,
}
impl MockPins {
fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> 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<MockPins, 1, 1, 1>,
Rc<Cell<bool>>,
Rc<Cell<bool>>,
) {
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]);
}
}