From 2de28f38b9837eed510e59af98edad4f5fb32f14 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Thu, 18 Sep 2025 12:50:45 +0200 Subject: [PATCH] Fixed debounce issue. Added more filtering for HAT switches --- README.md | 2 + rp2040/src/button_matrix.rs | 65 +++++++++++++++++----- rp2040/src/buttons.rs | 105 ++++++++++++++++++++++++------------ rp2040/src/hardware.rs | 4 +- rp2040/src/main.rs | 2 +- rp2040/src/mapping.rs | 5 +- 6 files changed, 134 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2d39c63..3595110 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/rp2040/src/button_matrix.rs b/rp2040/src/button_matrix.rs index 155c732..c2db171 100644 --- a/rp2040/src/button_matrix.rs +++ b/rp2040/src/button_matrix.rs @@ -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>, @@ -192,7 +213,7 @@ use std::rc::Rc; let cols: &'static mut [&'static mut dyn OutputPin; 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 } } diff --git a/rp2040/src/buttons.rs b/rp2040/src/buttons.rs index 9b3b538..0c79683 100644 --- a/rp2040/src/buttons.rs +++ b/rp2040/src/buttons.rs @@ -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] diff --git a/rp2040/src/hardware.rs b/rp2040/src/hardware.rs index 75dd25e..5b6e755 100644 --- a/rp2040/src/hardware.rs +++ b/rp2040/src/hardware.rs @@ -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; diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index b47b86d..fd2a1da 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -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::*; diff --git a/rp2040/src/mapping.rs b/rp2040/src/mapping.rs index 2403708..27fc347 100644 --- a/rp2040/src/mapping.rs +++ b/rp2040/src/mapping.rs @@ -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); }