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)
|
- 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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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::*;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user