From 33531719d603ff5b8844f8271f1fcb55dfe77f06 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Thu, 18 Sep 2025 12:15:55 +0200 Subject: [PATCH] Fixed debounce issue --- README.md | 8 ++++- rp2040/src/button_matrix.rs | 51 ++++++++++++++++++++------ rp2040/src/hardware.rs | 6 ++-- rp2040/src/keyboard.rs | 4 ++- rp2040/src/main.rs | 71 ++++++++++++++++++++++++++++++------- rp2040/src/status.rs | 25 ++++++++++++- 6 files changed, 137 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index e168f02..03b83bf 100644 --- a/README.md +++ b/README.md @@ -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 5 s 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 diff --git a/rp2040/src/button_matrix.rs b/rp2040/src/button_matrix.rs index 2fbacf0..32575e2 100644 --- a/rp2040/src/button_matrix.rs +++ b/rp2040/src/button_matrix.rs @@ -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; 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 } } diff --git a/rp2040/src/hardware.rs b/rp2040/src/hardware.rs index 6f10799..fdd57bc 100644 --- a/rp2040/src/hardware.rs +++ b/rp2040/src/hardware.rs @@ -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; diff --git a/rp2040/src/keyboard.rs b/rp2040/src/keyboard.rs index 9314864..50ff539 100644 --- a/rp2040/src/keyboard.rs +++ b/rp2040/src/keyboard.rs @@ -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); diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index 6804107..ea44f75 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -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; + } } } diff --git a/rp2040/src/status.rs b/rp2040/src/status.rs index a76f43e..b1b2168 100644 --- a/rp2040/src/status.rs +++ b/rp2040/src/status.rs @@ -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); + } }