Fixed debounce issue

This commit is contained in:
Christoffer Martinsson 2025-09-18 12:15:55 +02:00
parent f890fcd815
commit 33531719d6
6 changed files with 137 additions and 28 deletions

View File

@ -36,12 +36,18 @@
- Extreme low profile (only one pcb).
- Cost efficient solution with one pcb and one 3D printed cover.
- Function layers provide three active maps (primary + two Fn layers) with sticky-lock support.
- High-speed key scanning: 250µs cadence with 2-scan press / 3-scan release debounce for sub-millisecond latency and immediate USB reporting.
- High-speed key scanning: 250µs cadence with enhanced 5-scan debounce (1.25ms) and anti-bounce protection to prevent double characters.
- Status indication driven by the joystick-style heartbeat model:
- Heartbeat green while waiting for USB enumeration.
- Solid green during normal operation; automatic heartbeat idle animation after 5s inactivity.
- Blue solid / flashing when sticky lock is armed / latched.
- Red solid on firmware error, red flashing for Caps Lock.
- LED turns off during USB suspend for power savings.
- Power management for USB suspend/resume:
- Automatic power saving when USB host suspends device.
- Reduced key scanning frequency (20x slower) during suspend.
- Wake-on-input detection for any key press.
- Immediate resume response when keys are pressed.
## Build environment rp2040 Zero

View File

@ -16,6 +16,9 @@ pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
press_threshold: u8,
release_threshold: u8,
debounce_counter: [u8; N],
// Additional protection: minimum time between same-key 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> {
@ -32,6 +35,8 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
press_threshold,
release_threshold,
debounce_counter: [0; N],
last_press_scan: [0; N],
scan_counter: 0,
}
}
@ -42,6 +47,7 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
}
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);
@ -71,7 +77,16 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
};
if self.debounce_counter[button_index] >= threshold {
self.pressed[button_index] = current_state;
// Additional protection for press events: minimum 20 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 >= 20 { // 5ms at 250μ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;
}
}
@ -157,7 +172,7 @@ mod tests {
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
Box::leak(Box::new([col_pin]));
let matrix = ButtonMatrix::new(rows, cols, 2, 3);
let matrix = ButtonMatrix::new(rows, cols, 5, 5);
(matrix, row_state, col_state)
}
@ -176,19 +191,33 @@ mod tests {
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 5 scans to register press
for _ in 0..4 {
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); // 5th 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 5 scans to register release
for _ in 0..4 {
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); // 5th scan
states = matrix.buttons_pressed();
assert!(states[0]);
matrix.process_column(0);
states = matrix.buttons_pressed();
assert!(!states[0]);
assert!(!states[0]); // Now released
}
}

View File

@ -4,8 +4,10 @@
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
/// Debounce scans required before a key state toggles.
pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 2;
pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 3;
/// Increased from 2/3 to 5/5 to prevent double characters from key bounce.
/// At 250μs scan rate: 5 scans = 1.25ms debounce time.
pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 5;
pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 5;
/// Initial scan iterations before trusting key state.
pub const INITIAL_SCAN_PASSES: usize = 50;

View File

@ -89,6 +89,7 @@ impl KeyboardState {
&self,
usb_initialized: bool,
usb_active: bool,
usb_suspended: bool,
idle_mode: bool,
) -> StatusSummary {
StatusSummary::new(
@ -97,6 +98,7 @@ impl KeyboardState {
matches!(self.sticky_state, StickyState::Latched),
usb_initialized,
usb_active,
usb_suspended,
idle_mode,
)
}
@ -220,7 +222,7 @@ mod tests {
state.update_caps_lock(true);
state.mark_started();
let summary = state.status_summary(true, true, false);
let summary = state.status_summary(true, true, false, false);
assert!(summary.caps_lock_active);
assert!(summary.usb_active);
assert!(summary.usb_initialized);

View File

@ -25,6 +25,7 @@ use rp2040_hal::{
};
use usb_device::class_prelude::*;
use usb_device::prelude::*;
use usb_device::device::UsbDeviceState;
use usbd_human_interface_device::prelude::*;
#[unsafe(link_section = ".boot2")]
@ -116,6 +117,8 @@ fn main() -> ! {
status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis());
let mut status_time_ms: u32 = 0;
let mut usb_initialized = false;
let mut usb_suspended = false;
let mut wake_on_input = false;
let mut last_activity_ms: u32 = 0;
button_matrix.init_pins();
@ -161,28 +164,54 @@ fn main() -> ! {
let idle_mode = usb_initialized && idle_elapsed >= timers::IDLE_TIMEOUT_MS;
let usb_active = usb_initialized && !idle_mode;
status_led.apply_summary(
keyboard_state.status_summary(usb_initialized, usb_active, idle_mode),
keyboard_state.status_summary(usb_initialized, usb_active, usb_suspended, idle_mode),
status_time_ms,
);
}
if usb_tick_count_down.wait().is_ok() {
// Skip high-frequency scanning when suspended to save power
// Only scan periodically to detect wake-up inputs
let should_scan = if usb_suspended {
// When suspended, reduce scan frequency by factor of 20 (every ~5ms instead of 250μs)
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
unsafe {
SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 20;
SUSPENDED_SCAN_COUNTER == 0
}
} else {
true
};
if usb_tick_count_down.wait().is_ok() && should_scan {
button_matrix.scan_matrix(&mut delay);
let pressed_keys = button_matrix.buttons_pressed();
// Check for input activity
if pressed_keys.iter().any(|pressed| *pressed) {
last_activity_ms = status_time_ms;
// Wake from USB suspend if input detected
if wake_on_input && usb_suspended {
// TODO: Implement remote wakeup if supported by host
wake_on_input = false;
}
}
let keyboard_report = keyboard_state.process_scan(pressed_keys);
match keyboard.device().write_report(keyboard_report) {
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
Ok(_) => {
usb_initialized = true;
}
Err(_) => {
keyboard_state.mark_stopped();
usb_initialized = false;
}
};
// Only transmit USB reports when not suspended
if !usb_suspended {
match keyboard.device().write_report(keyboard_report) {
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
Ok(_) => {
usb_initialized = true;
}
Err(_) => {
keyboard_state.mark_stopped();
usb_initialized = false;
}
};
}
match keyboard.tick() {
Err(UsbHidError::WouldBlock) | Ok(_) => {}
@ -208,5 +237,23 @@ fn main() -> ! {
}
}
}
// Check USB device state for suspend/resume handling
let usb_state = usb_dev.state();
let was_suspended = usb_suspended;
usb_suspended = usb_state == UsbDeviceState::Suspend;
// Handle USB resume transition
if was_suspended && !usb_suspended {
// Device was suspended and is now resumed
last_activity_ms = status_time_ms;
wake_on_input = false;
}
// Handle USB suspend transition
if !was_suspended && usb_suspended {
// Device has just been suspended - enter power saving mode
wake_on_input = true;
}
}
}

View File

@ -29,12 +29,14 @@ pub enum StatusMode {
Error = 9,
Bootloader = 10,
Power = 11,
Suspended = 12,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemState {
pub usb_active: bool,
pub usb_initialized: bool,
pub usb_suspended: bool,
pub idle_mode: bool,
pub calibration_active: bool,
pub throttle_hold_enable: bool,
@ -51,6 +53,7 @@ pub struct StatusSummary {
pub sticky_latched: bool,
pub usb_initialized: bool,
pub usb_active: bool,
pub usb_suspended: bool,
pub idle_mode: bool,
}
@ -61,6 +64,7 @@ impl StatusSummary {
sticky_latched: bool,
usb_initialized: bool,
usb_active: bool,
usb_suspended: bool,
idle_mode: bool,
) -> Self {
Self {
@ -69,6 +73,7 @@ impl StatusSummary {
sticky_latched,
usb_initialized,
usb_active,
usb_suspended,
idle_mode,
}
}
@ -77,6 +82,7 @@ impl StatusSummary {
SystemState {
usb_active: self.usb_active,
usb_initialized: self.usb_initialized,
usb_suspended: self.usb_suspended,
idle_mode: self.idle_mode,
calibration_active: false,
throttle_hold_enable: false,
@ -222,6 +228,10 @@ const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescript
period_ms: HEARTBEAT_POWER_MS,
},
},
StatusMode::Suspended => ModeDescriptor {
color: COLOR_OFF,
effect: LedEffect::Solid,
},
}
}
@ -235,7 +245,9 @@ fn scale_color(base: RGB8, brightness: u8) -> RGB8 {
}
fn determine_base_mode(system_state: SystemState) -> StatusMode {
if system_state.caps_lock_active {
if system_state.usb_suspended {
StatusMode::Suspended
} else if system_state.caps_lock_active {
StatusMode::Warning
} else if system_state.sticky_latched {
StatusMode::ActivityFlash
@ -407,4 +419,15 @@ mod tests {
};
assert_eq!(determine_base_mode(state), StatusMode::NormalFlash);
}
#[test]
fn usb_suspend_takes_priority() {
let state = SystemState {
usb_suspended: true,
caps_lock_active: true, // Even with caps lock active
sticky_latched: true, // Even with sticky latched
..SystemState::default()
};
assert_eq!(determine_base_mode(state), StatusMode::Suspended);
}
}