Fixed debounce issue. Added more filtering for HAT switches

This commit is contained in:
Christoffer Martinsson 2025-09-18 12:50:45 +02:00
parent a629a3e94d
commit 2de28f38b9
6 changed files with 134 additions and 49 deletions

View File

@ -96,6 +96,8 @@ Config Layer (holding CONFIG button)
- USB interrupt endpoint configured for 1 ms poll interval (1 kHz reports) - USB interrupt endpoint configured for 1 ms poll interval (1 kHz reports)
- Input scan, smoothing, processing, and mapping now execute back-to-back - 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 - 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 - Existing idle timeout preserved (5 s) to avoid unnecessary host wake-ups

View File

@ -37,6 +37,9 @@ pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
pressed: [bool; N], pressed: [bool; N],
debounce: u8, debounce: u8,
debounce_counter: [u8; N], 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> { 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], pressed: [false; N],
debounce, debounce,
debounce_counter: [0; N], 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 /// Arguments
/// - `delay`: short delay implementation used to let signals settle between columns /// - `delay`: short delay implementation used to let signals settle between columns
pub fn scan_matrix(&mut self, delay: &mut Delay) { 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() { for col_index in 0..self.cols.len() {
self.cols[col_index].set_low().unwrap(); self.cols[col_index].set_low().unwrap();
delay.delay_us(1); 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; self.debounce_counter[button_index] += 1;
if self.debounce_counter[button_index] >= self.debounce { 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 /// For small `N` this copy is cheap. If needed, the API could be extended to
/// return a reference in the future. /// return a reference in the future.
pub fn buttons_pressed(&mut self) -> [bool; N] { pub fn buttons_pressed(&mut self) -> [bool; N] {
self.pressed self.pressed
} }
} }
#[cfg(all(test, feature = "std"))] #[cfg(all(test, feature = "std"))]
mod tests { mod tests {
use super::*; use super::*;
use core::cell::Cell; use core::cell::Cell;
use embedded_hal::digital::ErrorType; use embedded_hal::digital::ErrorType;
use std::rc::Rc; use std::rc::Rc;
struct MockInputPin { struct MockInputPin {
state: Rc<Cell<bool>>, state: Rc<Cell<bool>>,
@ -192,7 +213,7 @@ use std::rc::Rc;
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] = let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
Box::leak(Box::new([col_pin])); 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) (matrix, row_state, col_state)
} }
@ -210,16 +231,34 @@ use std::rc::Rc;
let (mut matrix, row_state, _col_state) = matrix_fixture(); let (mut matrix, row_state, _col_state) = matrix_fixture();
let mut states = matrix.buttons_pressed(); let mut states = matrix.buttons_pressed();
assert!(!states[0]); assert!(!states[0]);
// Set scan counter to start with enough history
matrix.scan_counter = 100;
row_state.set(true); row_state.set(true);
matrix.process_column(0); // Need 15 scans to register press
matrix.process_column(0); 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(); states = matrix.buttons_pressed();
assert!(states[0]); assert!(states[0]); // Now pressed
row_state.set(false); row_state.set(false);
matrix.process_column(0); // Need 15 scans to register release
matrix.process_column(0); 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(); states = matrix.buttons_pressed();
assert!(!states[0]); assert!(!states[0]); // Now released
} }
} }

View File

@ -98,41 +98,41 @@ impl ButtonManager {
/// Filter HAT switches so only a single direction (or center) can be active. /// Filter HAT switches so only a single direction (or center) can be active.
pub fn filter_hat_switches(&mut self) { pub fn filter_hat_switches(&mut self) {
// Filter left hat switch buttons const LEFT_HAT_DIRECTIONS: [usize; 4] = [
for i in BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT { BUTTON_TOP_LEFT_HAT_UP,
if (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT) BUTTON_TOP_LEFT_HAT_RIGHT,
.filter(|&j| j != i) BUTTON_TOP_LEFT_HAT_DOWN,
.any(|j| self.buttons[j].pressed) BUTTON_TOP_LEFT_HAT_LEFT,
{ ];
self.buttons[i].pressed = false;
}
}
// Fix button state for center hat press on left hat const RIGHT_HAT_DIRECTIONS: [usize; 4] = [
if self.buttons[BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT] 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() .iter()
.any(|b| b.pressed) .filter(|&&index| self.buttons[index].pressed)
{ .count();
self.buttons[BUTTON_TOP_LEFT_HAT].pressed = false;
if pressed_count == 0 {
return;
} }
// Filter right hat switch buttons self.buttons[center].pressed = false;
for i in BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT {
if (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT) if pressed_count >= 2 {
.filter(|&j| j != i) for &index in directions.iter() {
.any(|j| self.buttons[j].pressed) self.buttons[index].pressed = false;
{
self.buttons[i].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. /// 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(); 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) let pressed_count = (BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT)
.filter(|&i| manager.buttons[i].pressed) .filter(|&i| manager.buttons[i].pressed)
.count(); .count();
assert!(pressed_count <= 1); assert_eq!(pressed_count, 0);
assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].pressed);
} }
#[test] #[test]
@ -427,11 +428,12 @@ mod tests {
manager.filter_hat_switches(); 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) let pressed_count = (BUTTON_TOP_RIGHT_HAT_UP..=BUTTON_TOP_RIGHT_HAT_LEFT)
.filter(|&i| manager.buttons[i].pressed) .filter(|&i| manager.buttons[i].pressed)
.count(); .count();
assert!(pressed_count <= 1); assert_eq!(pressed_count, 0);
assert!(!manager.buttons[BUTTON_TOP_RIGHT_HAT].pressed);
} }
#[test] #[test]
@ -446,6 +448,43 @@ mod tests {
// Center button should be disabled when directional is pressed // Center button should be disabled when directional is pressed
assert!(!manager.buttons[BUTTON_TOP_LEFT_HAT].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] #[test]

View File

@ -27,7 +27,9 @@ pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
/// Number of physical gimbal axes. /// Number of physical gimbal axes.
pub const NBR_OF_GIMBAL_AXIS: usize = 4; pub const NBR_OF_GIMBAL_AXIS: usize = 4;
/// Debounce threshold (in scans) for the matrix. /// 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. /// Bytes reserved in EEPROM for calibration data + gimbal mode.
pub const EEPROM_DATA_LENGTH: usize = 25; pub const EEPROM_DATA_LENGTH: usize = 25;

View File

@ -62,8 +62,8 @@ use rp2040_hal::{
}; };
use status::{StatusLed, StatusMode, SystemState}; use status::{StatusLed, StatusMode, SystemState};
use usb_device::class_prelude::*; use usb_device::class_prelude::*;
use usb_device::prelude::*;
use usb_device::device::UsbDeviceState; use usb_device::device::UsbDeviceState;
use usb_device::prelude::*;
use usb_joystick_device::JoystickConfig; use usb_joystick_device::JoystickConfig;
use usb_report::get_joystick_report; use usb_report::get_joystick_report;
use usbd_human_interface_device::prelude::*; use usbd_human_interface_device::prelude::*;

View File

@ -196,7 +196,10 @@ mod tests {
configure_button_mappings(&mut buttons); 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_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_DOWN].usb_button, USB_HAT_DOWN);
assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_LEFT].usb_button, USB_HAT_LEFT); assert_eq!(buttons[BUTTON_TOP_RIGHT_HAT_LEFT].usb_button, USB_HAT_LEFT);
} }