cmdr-keyboard/rp2040/src/button_matrix.rs

297 lines
8.8 KiB
Rust

//! Button matrix scanner for CMDR Keyboard.
//!
//! The scanner owns a concrete set of matrix pins and produces a debounced
//! boolean state for each key.
use cortex_m::delay::Delay;
use embedded_hal::digital::{InputPin, OutputPin};
use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};
/// Abstraction over the physical row/column pins backing the button matrix.
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 built from RP2040 dynamic pins.
type RowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
/// Strongly typed bundle of row and column pins used during matrix scanning.
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 every column high so rows can be strobed individually.
for column in self.cols.iter_mut() {
column.set_high().ok();
}
}
fn set_column_low(&mut self, column: usize) {
// Pull the active column low before sampling its rows.
self.cols[column].set_low().ok();
}
fn set_column_high(&mut self, column: usize) {
// Release the column after sampling to avoid ghosting.
self.cols[column].set_high().ok();
}
fn read_row(&mut self, row: usize) -> bool {
// Treat a low level as a pressed switch; default to false on IO errors.
self.rows[row].is_low().unwrap_or(false)
}
}
/// Row/column scanned button matrix driver with debounce counters.
pub struct ButtonMatrix<P, const ROWS: usize, const COLS: usize, const KEYS: usize> {
pins: P,
pressed: [bool; KEYS],
press_threshold: u8,
release_threshold: u8,
debounce_counter: [u8; KEYS],
last_press_scan: [u32; KEYS],
scan_counter: u32,
min_press_gap_scans: u32,
}
impl<P, const ROWS: usize, const COLS: usize, const KEYS: usize> ButtonMatrix<P, ROWS, COLS, KEYS>
where
P: MatrixPinAccess<ROWS, COLS>,
{
pub fn new(
pins: P,
press_threshold: u8,
release_threshold: u8,
min_press_gap_scans: u32,
) -> Self {
debug_assert_eq!(KEYS, ROWS * COLS);
Self {
pins,
pressed: [false; KEYS],
press_threshold,
release_threshold,
debounce_counter: [0; KEYS],
last_press_scan: [0; KEYS],
scan_counter: 0,
min_press_gap_scans,
}
}
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(crate) fn process_column(&mut self, column: usize) {
for row in 0..ROWS {
let button_index = column + (row * COLS);
let current_state = self.pins.read_row(row);
if current_state == self.pressed[button_index] {
self.debounce_counter[button_index] = 0;
continue;
}
self.debounce_counter[button_index] =
self.debounce_counter[button_index].saturating_add(1);
let threshold = if current_state {
self.press_threshold
} else {
self.release_threshold
};
if self.debounce_counter[button_index] >= threshold {
self.pressed[button_index] = match current_state {
true => self.should_register_press(button_index),
false => false,
};
self.debounce_counter[button_index] = 0;
}
}
}
pub fn buttons_pressed(&self) -> [bool; KEYS] {
self.pressed
}
fn should_register_press(&mut self, button_index: usize) -> bool {
let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]);
let can_register = self.last_press_scan[button_index] == 0
|| elapsed >= self.min_press_gap_scans;
if can_register {
self.last_press_scan[button_index] = self.scan_counter;
}
can_register
}
#[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 MockMatrixPins {
row: Rc<Cell<bool>>,
column: Rc<Cell<bool>>,
}
impl MockMatrixPins {
fn new(row: Rc<Cell<bool>>, column: Rc<Cell<bool>>) -> Self {
Self { row, column }
}
}
impl MatrixPinAccess<1, 1> for MockMatrixPins {
fn init_columns(&mut self) {
self.column.set(true);
}
fn set_column_low(&mut self, _column: usize) {
self.column.set(false);
}
fn set_column_high(&mut self, _column: usize) {
self.column.set(true);
}
fn read_row(&mut self, _row: usize) -> bool {
self.row.get()
}
}
fn fixture() -> (
ButtonMatrix<MockMatrixPins, 1, 1, 1>,
Rc<Cell<bool>>,
Rc<Cell<bool>>,
) {
// Provide a matrix instance backed by mock row/column signals for testing.
let row_state = Rc::new(Cell::new(false));
let column_state = Rc::new(Cell::new(false));
let pins = MockMatrixPins::new(row_state.clone(), column_state.clone());
let matrix = ButtonMatrix::new(pins, 5, 5, 200);
(matrix, row_state, column_state)
}
#[test]
fn init_sets_columns_high() {
// Initialisation should drive the column line to its idle high level.
let (mut matrix, _row, column) = fixture();
assert!(!column.get());
matrix.init_pins();
assert!(column.get());
}
#[test]
fn debounce_respects_threshold() {
// Debounce counters must reach the threshold before toggling key state.
let (mut matrix, row, _column) = fixture();
let mut states = matrix.buttons_pressed();
assert!(!states[0]);
matrix.set_scan_counter(100);
row.set(true);
for _ in 0..4 {
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]);
}
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(states[0]);
row.set(false);
for _ in 0..4 {
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(states[0]);
}
matrix.bump_scan_counter();
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]);
}
#[test]
fn min_press_gap_blocks_fast_retrigger() {
// Verify that a second press faster than the configured gap is ignored until enough scans pass.
let (mut matrix, row, _column) = fixture();
matrix.set_scan_counter(1);
row.set(true);
for _ in 0..5 {
matrix.bump_scan_counter();
matrix.process_column(0);
}
assert!(matrix.buttons_pressed()[0]);
row.set(false);
for _ in 0..5 {
matrix.bump_scan_counter();
matrix.process_column(0);
}
assert!(!matrix.buttons_pressed()[0]);
row.set(true);
for _ in 0..5 {
matrix.bump_scan_counter();
matrix.process_column(0);
}
assert!(!matrix.buttons_pressed()[0]);
for _ in 0..200 {
matrix.bump_scan_counter();
}
for _ in 0..5 {
matrix.bump_scan_counter();
matrix.process_column(0);
}
assert!(matrix.buttons_pressed()[0]);
}
}