297 lines
8.8 KiB
Rust
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]);
|
|
}
|
|
}
|