224 lines
7.0 KiB
Rust
224 lines
7.0 KiB
Rust
//! Button matrix scanner for CMDR Keyboard 42
|
|
//!
|
|
//! Ported from the joystick firmware for consistent behaviour and structure.
|
|
//! Scans a row/column matrix and produces a debounced boolean state for each
|
|
//! button using per-button counters and small inter-column delays.
|
|
|
|
use core::convert::Infallible;
|
|
use cortex_m::delay::Delay;
|
|
use embedded_hal::digital::{InputPin, OutputPin};
|
|
|
|
/// Row/column scanned button matrix driver with debounce counters.
|
|
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],
|
|
press_threshold: u8,
|
|
release_threshold: u8,
|
|
debounce_counter: [u8; N],
|
|
// Additional protection: minimum time between same-key presses
|
|
last_press_scan: [u32; N],
|
|
scan_counter: u32,
|
|
}
|
|
|
|
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
|
|
pub fn new(
|
|
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
|
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
|
|
press_threshold: u8,
|
|
release_threshold: u8,
|
|
) -> Self {
|
|
Self {
|
|
rows,
|
|
cols,
|
|
pressed: [false; N],
|
|
press_threshold,
|
|
release_threshold,
|
|
debounce_counter: [0; N],
|
|
last_press_scan: [0; N],
|
|
scan_counter: 0,
|
|
}
|
|
}
|
|
|
|
pub fn init_pins(&mut self) {
|
|
for col in self.cols.iter_mut() {
|
|
col.set_high().unwrap();
|
|
}
|
|
}
|
|
|
|
pub fn scan_matrix(&mut self, delay: &mut Delay) {
|
|
self.scan_counter = self.scan_counter.wrapping_add(1);
|
|
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);
|
|
}
|
|
}
|
|
|
|
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] =
|
|
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 {
|
|
// Additional protection for press events: minimum 20 scans (5ms) between presses
|
|
if current_state { // Pressing
|
|
let scans_since_last = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]);
|
|
if scans_since_last >= 20 { // 5ms at 250μs scan rate
|
|
self.pressed[button_index] = current_state;
|
|
self.last_press_scan[button_index] = self.scan_counter;
|
|
}
|
|
} else { // Releasing
|
|
self.pressed[button_index] = current_state;
|
|
}
|
|
self.debounce_counter[button_index] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
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, 5, 5);
|
|
|
|
(matrix, row_state, col_state)
|
|
}
|
|
|
|
#[test]
|
|
fn init_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 debounce_respects_threshold() {
|
|
let (mut matrix, row_state, _col_state) = matrix_fixture();
|
|
let mut states = matrix.buttons_pressed();
|
|
assert!(!states[0]);
|
|
|
|
// Set scan counter to start with enough history
|
|
matrix.scan_counter = 100;
|
|
|
|
row_state.set(true);
|
|
// Need 5 scans to register press
|
|
for _ in 0..4 {
|
|
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
matrix.process_column(0);
|
|
states = matrix.buttons_pressed();
|
|
assert!(!states[0]); // Still not pressed
|
|
}
|
|
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
matrix.process_column(0); // 5th scan
|
|
states = matrix.buttons_pressed();
|
|
assert!(states[0]); // Now pressed
|
|
|
|
row_state.set(false);
|
|
// Need 5 scans to register release
|
|
for _ in 0..4 {
|
|
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
matrix.process_column(0);
|
|
states = matrix.buttons_pressed();
|
|
assert!(states[0]); // Still pressed
|
|
}
|
|
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
matrix.process_column(0); // 5th scan
|
|
states = matrix.buttons_pressed();
|
|
assert!(!states[0]); // Now released
|
|
}
|
|
}
|