Fixed debounce issue. Added more filtering for HAT switches
This commit is contained in:
parent
a629a3e94d
commit
2de28f38b9
@ -96,6 +96,8 @@ Config Layer (holding CONFIG button)
|
||||
|
||||
- USB interrupt endpoint configured for 1 ms poll interval (1 kHz reports)
|
||||
- Input scan, smoothing, processing, and mapping now execute back-to-back
|
||||
- Enhanced button debounce: 15-scan threshold (3ms) with anti-bounce protection to prevent double presses
|
||||
- Smart HAT switch filtering: disables all HAT buttons when multiple directions are detected to prevent spurious inputs
|
||||
- First activity after idle forces immediate USB packet without waiting for the next tick
|
||||
- Existing idle timeout preserved (5 s) to avoid unnecessary host wake-ups
|
||||
|
||||
|
||||
@ -37,6 +37,9 @@ pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
|
||||
pressed: [bool; N],
|
||||
debounce: u8,
|
||||
debounce_counter: [u8; N],
|
||||
// Anti-bounce protection: minimum time between same-button 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> {
|
||||
@ -57,6 +60,8 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
||||
pressed: [false; N],
|
||||
debounce,
|
||||
debounce_counter: [0; N],
|
||||
last_press_scan: [0; N],
|
||||
scan_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +83,7 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
||||
/// Arguments
|
||||
/// - `delay`: short delay implementation used to let signals settle between columns
|
||||
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);
|
||||
@ -103,7 +109,22 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
||||
|
||||
self.debounce_counter[button_index] += 1;
|
||||
if self.debounce_counter[button_index] >= self.debounce {
|
||||
self.pressed[button_index] = current_state;
|
||||
// Anti-bounce protection for press events: minimum 25 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 >= 25 {
|
||||
// 5ms at 200μ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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,17 +133,17 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
||||
///
|
||||
/// For small `N` this copy is cheap. If needed, the API could be extended to
|
||||
/// return a reference in the future.
|
||||
pub fn buttons_pressed(&mut self) -> [bool; N] {
|
||||
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;
|
||||
use super::*;
|
||||
use core::cell::Cell;
|
||||
use embedded_hal::digital::ErrorType;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct MockInputPin {
|
||||
state: Rc<Cell<bool>>,
|
||||
@ -192,7 +213,7 @@ use std::rc::Rc;
|
||||
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
|
||||
Box::leak(Box::new([col_pin]));
|
||||
|
||||
let matrix = ButtonMatrix::new(rows, cols, 2);
|
||||
let matrix = ButtonMatrix::new(rows, cols, 15);
|
||||
|
||||
(matrix, row_state, col_state)
|
||||
}
|
||||
@ -210,16 +231,34 @@ use std::rc::Rc;
|
||||
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);
|
||||
matrix.process_column(0);
|
||||
matrix.process_column(0);
|
||||
// Need 15 scans to register press
|
||||
for _ in 0..14 {
|
||||
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); // 15th scan
|
||||
states = matrix.buttons_pressed();
|
||||
assert!(states[0]);
|
||||
assert!(states[0]); // Now pressed
|
||||
|
||||
row_state.set(false);
|
||||
matrix.process_column(0);
|
||||
matrix.process_column(0);
|
||||
// Need 15 scans to register release
|
||||
for _ in 0..14 {
|
||||
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); // 15th scan
|
||||
states = matrix.buttons_pressed();
|
||||
assert!(!states[0]);
|
||||
assert!(!states[0]); // Now released
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,41 +98,41 @@ impl ButtonManager {
|
||||
|
||||
/// Filter HAT switches so only a single direction (or center) can be active.
|
||||
pub fn filter_hat_switches(&mut self) {
|
||||
// Filter left hat switch buttons
|
||||
for i in BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT {
|
||||
if (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT)
|
||||
.filter(|&j| j != i)
|
||||
.any(|j| self.buttons[j].pressed)
|
||||
{
|
||||
self.buttons[i].pressed = false;
|
||||
}
|
||||
}
|
||||
const LEFT_HAT_DIRECTIONS: [usize; 4] = [
|
||||
BUTTON_TOP_LEFT_HAT_UP,
|
||||
BUTTON_TOP_LEFT_HAT_RIGHT,
|
||||
BUTTON_TOP_LEFT_HAT_DOWN,
|
||||
BUTTON_TOP_LEFT_HAT_LEFT,
|
||||
];
|
||||
|
||||
// Fix button state for center hat press on left hat
|
||||
if self.buttons[BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT]
|
||||
const RIGHT_HAT_DIRECTIONS: [usize; 4] = [
|
||||
BUTTON_TOP_RIGHT_HAT_UP,
|
||||
BUTTON_TOP_RIGHT_HAT_RIGHT,
|
||||
BUTTON_TOP_RIGHT_HAT_DOWN,
|
||||
BUTTON_TOP_RIGHT_HAT_LEFT,
|
||||
];
|
||||
|
||||
self.reconcile_hat(&LEFT_HAT_DIRECTIONS, BUTTON_TOP_LEFT_HAT);
|
||||
self.reconcile_hat(&RIGHT_HAT_DIRECTIONS, BUTTON_TOP_RIGHT_HAT);
|
||||
}
|
||||
|
||||
fn reconcile_hat(&mut self, directions: &[usize; 4], center: usize) {
|
||||
let pressed_count = directions
|
||||
.iter()
|
||||
.any(|b| b.pressed)
|
||||
{
|
||||
self.buttons[BUTTON_TOP_LEFT_HAT].pressed = false;
|
||||
.filter(|&&index| self.buttons[index].pressed)
|
||||
.count();
|
||||
|
||||
if pressed_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter right hat switch buttons
|
||||
for i in BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT {
|
||||
if (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT)
|
||||
.filter(|&j| j != i)
|
||||
.any(|j| self.buttons[j].pressed)
|
||||
{
|
||||
self.buttons[i].pressed = false;
|
||||
self.buttons[center].pressed = false;
|
||||
|
||||
if pressed_count >= 2 {
|
||||
for &index in directions.iter() {
|
||||
self.buttons[index].pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix button state for center hat press on right hat
|
||||
if self.buttons[BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT]
|
||||
.iter()
|
||||
.any(|b| b.pressed)
|
||||
{
|
||||
self.buttons[BUTTON_TOP_RIGHT_HAT].pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update press types (short/long) and USB change flags; returns whether USB should be updated.
|
||||
@ -410,11 +410,12 @@ mod tests {
|
||||
|
||||
manager.filter_hat_switches();
|
||||
|
||||
// Only one should remain (implementation filters out conflicting ones)
|
||||
// Multiple directions cancel the hat output completely
|
||||
let pressed_count = (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT)
|
||||
.filter(|&i| manager.buttons[i].pressed)
|
||||
.count();
|
||||
assert!(pressed_count <= 1);
|
||||
assert_eq!(pressed_count, 0);
|
||||
assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -427,11 +428,12 @@ mod tests {
|
||||
|
||||
manager.filter_hat_switches();
|
||||
|
||||
// Only one should remain (implementation filters out conflicting ones)
|
||||
// Multiple directions cancel the hat output completely
|
||||
let pressed_count = (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT)
|
||||
.filter(|&i| manager.buttons[i].pressed)
|
||||
.count();
|
||||
assert!(pressed_count <= 1);
|
||||
assert_eq!(pressed_count, 0);
|
||||
assert!(!manager.buttons[BUTTON_TOP_RIGHT_HAT].pressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -446,6 +448,43 @@ mod tests {
|
||||
|
||||
// Center button should be disabled when directional is pressed
|
||||
assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
|
||||
// But single direction should remain active
|
||||
assert!(manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hat_switch_single_direction_allowed() {
|
||||
let mut manager = ButtonManager::new();
|
||||
|
||||
// Press only one directional button on left hat
|
||||
manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed = true;
|
||||
|
||||
manager.filter_hat_switches();
|
||||
|
||||
// Single direction should remain active
|
||||
assert!(manager.buttons[BUTTON_TOP_LEFT_HAT_UP].pressed);
|
||||
|
||||
// Test same for right hat
|
||||
let mut manager = ButtonManager::new();
|
||||
manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed = true;
|
||||
|
||||
manager.filter_hat_switches();
|
||||
|
||||
// Single direction should remain active
|
||||
assert!(manager.buttons[BUTTON_TOP_RIGHT_HAT_DOWN].pressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hat_center_button_works_alone() {
|
||||
let mut manager = ButtonManager::new();
|
||||
|
||||
// Press only center button (no directions)
|
||||
manager.buttons[BUTTON_TOP_LEFT_HAT].pressed = true;
|
||||
|
||||
manager.filter_hat_switches();
|
||||
|
||||
// Center button should remain active when no directions are pressed
|
||||
assert!(manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -27,7 +27,9 @@ pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
|
||||
/// Number of physical gimbal axes.
|
||||
pub const NBR_OF_GIMBAL_AXIS: usize = 4;
|
||||
/// Debounce threshold (in scans) for the matrix.
|
||||
pub const DEBOUNCE: u8 = 10;
|
||||
/// Increased from 10 to 15 scans to prevent double button presses from bounce.
|
||||
/// At 200μs scan rate: 15 scans = 3ms debounce time.
|
||||
pub const DEBOUNCE: u8 = 15;
|
||||
/// Bytes reserved in EEPROM for calibration data + gimbal mode.
|
||||
pub const EEPROM_DATA_LENGTH: usize = 25;
|
||||
|
||||
|
||||
@ -62,8 +62,8 @@ use rp2040_hal::{
|
||||
};
|
||||
use status::{StatusLed, StatusMode, SystemState};
|
||||
use usb_device::class_prelude::*;
|
||||
use usb_device::prelude::*;
|
||||
use usb_device::device::UsbDeviceState;
|
||||
use usb_device::prelude::*;
|
||||
use usb_joystick_device::JoystickConfig;
|
||||
use usb_report::get_joystick_report;
|
||||
use usbd_human_interface_device::prelude::*;
|
||||
|
||||
@ -196,7 +196,10 @@ mod tests {
|
||||
configure_button_mappings(&mut buttons);
|
||||
|
||||
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_UP].usb_button, USB_HAT_UP);
|
||||
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_RIGHT].usb_button, USB_HAT_RIGHT);
|
||||
assert_eq!(
|
||||
buttons[BUTTON_TOP_RIGHT_HAT_RIGHT].usb_button,
|
||||
USB_HAT_RIGHT
|
||||
);
|
||||
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_DOWN].usb_button, USB_HAT_DOWN);
|
||||
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_LEFT].usb_button, USB_HAT_LEFT);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user