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)
- 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

View File

@ -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
}
}

View File

@ -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]

View File

@ -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;

View File

@ -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::*;

View File

@ -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);
}