245 lines
7.6 KiB
Rust
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]);
|
|
}
|
|
}
|