Fixed debounce issue
This commit is contained in:
parent
f890fcd815
commit
33531719d6
@ -36,12 +36,18 @@
|
|||||||
- Extreme low profile (only one pcb).
|
- Extreme low profile (only one pcb).
|
||||||
- Cost efficient solution with one pcb and one 3D printed cover.
|
- 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.
|
- 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:
|
- Status indication driven by the joystick-style heartbeat model:
|
||||||
- Heartbeat green while waiting for USB enumeration.
|
- Heartbeat green while waiting for USB enumeration.
|
||||||
- Solid green during normal operation; automatic heartbeat idle animation after 5 s inactivity.
|
- Solid green during normal operation; automatic heartbeat idle animation after 5 s inactivity.
|
||||||
- Blue solid / flashing when sticky lock is armed / latched.
|
- Blue solid / flashing when sticky lock is armed / latched.
|
||||||
- Red solid on firmware error, red flashing for Caps Lock.
|
- 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
|
## Build environment rp2040 Zero
|
||||||
|
|||||||
@ -16,6 +16,9 @@ pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
|
|||||||
press_threshold: u8,
|
press_threshold: u8,
|
||||||
release_threshold: u8,
|
release_threshold: u8,
|
||||||
debounce_counter: [u8; N],
|
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> {
|
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,
|
press_threshold,
|
||||||
release_threshold,
|
release_threshold,
|
||||||
debounce_counter: [0; N],
|
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) {
|
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);
|
||||||
@ -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 {
|
if self.debounce_counter[button_index] >= threshold {
|
||||||
|
// 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.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;
|
self.debounce_counter[button_index] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +172,7 @@ mod tests {
|
|||||||
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, 3);
|
let matrix = ButtonMatrix::new(rows, cols, 5, 5);
|
||||||
|
|
||||||
(matrix, row_state, col_state)
|
(matrix, row_state, col_state)
|
||||||
}
|
}
|
||||||
@ -176,19 +191,33 @@ mod tests {
|
|||||||
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 5 scans to register press
|
||||||
|
for _ in 0..4 {
|
||||||
|
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
||||||
matrix.process_column(0);
|
matrix.process_column(0);
|
||||||
states = matrix.buttons_pressed();
|
states = matrix.buttons_pressed();
|
||||||
assert!(states[0]);
|
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]); // Now pressed
|
||||||
|
|
||||||
row_state.set(false);
|
row_state.set(false);
|
||||||
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);
|
matrix.process_column(0);
|
||||||
states = matrix.buttons_pressed();
|
states = matrix.buttons_pressed();
|
||||||
assert!(states[0]);
|
assert!(states[0]); // Still pressed
|
||||||
matrix.process_column(0);
|
}
|
||||||
|
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
||||||
|
matrix.process_column(0); // 5th scan
|
||||||
states = matrix.buttons_pressed();
|
states = matrix.buttons_pressed();
|
||||||
assert!(!states[0]);
|
assert!(!states[0]); // Now released
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,10 @@
|
|||||||
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
|
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
|
||||||
|
|
||||||
/// Debounce scans required before a key state toggles.
|
/// Debounce scans required before a key state toggles.
|
||||||
pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 2;
|
/// Increased from 2/3 to 5/5 to prevent double characters from key bounce.
|
||||||
pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 3;
|
/// 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.
|
/// Initial scan iterations before trusting key state.
|
||||||
pub const INITIAL_SCAN_PASSES: usize = 50;
|
pub const INITIAL_SCAN_PASSES: usize = 50;
|
||||||
|
|||||||
@ -89,6 +89,7 @@ impl KeyboardState {
|
|||||||
&self,
|
&self,
|
||||||
usb_initialized: bool,
|
usb_initialized: bool,
|
||||||
usb_active: bool,
|
usb_active: bool,
|
||||||
|
usb_suspended: bool,
|
||||||
idle_mode: bool,
|
idle_mode: bool,
|
||||||
) -> StatusSummary {
|
) -> StatusSummary {
|
||||||
StatusSummary::new(
|
StatusSummary::new(
|
||||||
@ -97,6 +98,7 @@ impl KeyboardState {
|
|||||||
matches!(self.sticky_state, StickyState::Latched),
|
matches!(self.sticky_state, StickyState::Latched),
|
||||||
usb_initialized,
|
usb_initialized,
|
||||||
usb_active,
|
usb_active,
|
||||||
|
usb_suspended,
|
||||||
idle_mode,
|
idle_mode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -220,7 +222,7 @@ mod tests {
|
|||||||
state.update_caps_lock(true);
|
state.update_caps_lock(true);
|
||||||
state.mark_started();
|
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.caps_lock_active);
|
||||||
assert!(summary.usb_active);
|
assert!(summary.usb_active);
|
||||||
assert!(summary.usb_initialized);
|
assert!(summary.usb_initialized);
|
||||||
|
|||||||
@ -25,6 +25,7 @@ use rp2040_hal::{
|
|||||||
};
|
};
|
||||||
use usb_device::class_prelude::*;
|
use usb_device::class_prelude::*;
|
||||||
use usb_device::prelude::*;
|
use usb_device::prelude::*;
|
||||||
|
use usb_device::device::UsbDeviceState;
|
||||||
use usbd_human_interface_device::prelude::*;
|
use usbd_human_interface_device::prelude::*;
|
||||||
|
|
||||||
#[unsafe(link_section = ".boot2")]
|
#[unsafe(link_section = ".boot2")]
|
||||||
@ -116,6 +117,8 @@ fn main() -> ! {
|
|||||||
status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
||||||
let mut status_time_ms: u32 = 0;
|
let mut status_time_ms: u32 = 0;
|
||||||
let mut usb_initialized = false;
|
let mut usb_initialized = false;
|
||||||
|
let mut usb_suspended = false;
|
||||||
|
let mut wake_on_input = false;
|
||||||
let mut last_activity_ms: u32 = 0;
|
let mut last_activity_ms: u32 = 0;
|
||||||
|
|
||||||
button_matrix.init_pins();
|
button_matrix.init_pins();
|
||||||
@ -161,18 +164,43 @@ fn main() -> ! {
|
|||||||
let idle_mode = usb_initialized && idle_elapsed >= timers::IDLE_TIMEOUT_MS;
|
let idle_mode = usb_initialized && idle_elapsed >= timers::IDLE_TIMEOUT_MS;
|
||||||
let usb_active = usb_initialized && !idle_mode;
|
let usb_active = usb_initialized && !idle_mode;
|
||||||
status_led.apply_summary(
|
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,
|
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);
|
button_matrix.scan_matrix(&mut delay);
|
||||||
let pressed_keys = button_matrix.buttons_pressed();
|
let pressed_keys = button_matrix.buttons_pressed();
|
||||||
|
|
||||||
|
// Check for input activity
|
||||||
if pressed_keys.iter().any(|pressed| *pressed) {
|
if pressed_keys.iter().any(|pressed| *pressed) {
|
||||||
last_activity_ms = status_time_ms;
|
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);
|
let keyboard_report = keyboard_state.process_scan(pressed_keys);
|
||||||
|
|
||||||
|
// Only transmit USB reports when not suspended
|
||||||
|
if !usb_suspended {
|
||||||
match keyboard.device().write_report(keyboard_report) {
|
match keyboard.device().write_report(keyboard_report) {
|
||||||
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
|
Err(UsbHidError::WouldBlock) | Err(UsbHidError::Duplicate) => {}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@ -183,6 +211,7 @@ fn main() -> ! {
|
|||||||
usb_initialized = false;
|
usb_initialized = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
match keyboard.tick() {
|
match keyboard.tick() {
|
||||||
Err(UsbHidError::WouldBlock) | Ok(_) => {}
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,12 +29,14 @@ pub enum StatusMode {
|
|||||||
Error = 9,
|
Error = 9,
|
||||||
Bootloader = 10,
|
Bootloader = 10,
|
||||||
Power = 11,
|
Power = 11,
|
||||||
|
Suspended = 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct SystemState {
|
pub struct SystemState {
|
||||||
pub usb_active: bool,
|
pub usb_active: bool,
|
||||||
pub usb_initialized: bool,
|
pub usb_initialized: bool,
|
||||||
|
pub usb_suspended: bool,
|
||||||
pub idle_mode: bool,
|
pub idle_mode: bool,
|
||||||
pub calibration_active: bool,
|
pub calibration_active: bool,
|
||||||
pub throttle_hold_enable: bool,
|
pub throttle_hold_enable: bool,
|
||||||
@ -51,6 +53,7 @@ pub struct StatusSummary {
|
|||||||
pub sticky_latched: bool,
|
pub sticky_latched: bool,
|
||||||
pub usb_initialized: bool,
|
pub usb_initialized: bool,
|
||||||
pub usb_active: bool,
|
pub usb_active: bool,
|
||||||
|
pub usb_suspended: bool,
|
||||||
pub idle_mode: bool,
|
pub idle_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +64,7 @@ impl StatusSummary {
|
|||||||
sticky_latched: bool,
|
sticky_latched: bool,
|
||||||
usb_initialized: bool,
|
usb_initialized: bool,
|
||||||
usb_active: bool,
|
usb_active: bool,
|
||||||
|
usb_suspended: bool,
|
||||||
idle_mode: bool,
|
idle_mode: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -69,6 +73,7 @@ impl StatusSummary {
|
|||||||
sticky_latched,
|
sticky_latched,
|
||||||
usb_initialized,
|
usb_initialized,
|
||||||
usb_active,
|
usb_active,
|
||||||
|
usb_suspended,
|
||||||
idle_mode,
|
idle_mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,6 +82,7 @@ impl StatusSummary {
|
|||||||
SystemState {
|
SystemState {
|
||||||
usb_active: self.usb_active,
|
usb_active: self.usb_active,
|
||||||
usb_initialized: self.usb_initialized,
|
usb_initialized: self.usb_initialized,
|
||||||
|
usb_suspended: self.usb_suspended,
|
||||||
idle_mode: self.idle_mode,
|
idle_mode: self.idle_mode,
|
||||||
calibration_active: false,
|
calibration_active: false,
|
||||||
throttle_hold_enable: false,
|
throttle_hold_enable: false,
|
||||||
@ -222,6 +228,10 @@ const fn descriptor_for(mode: StatusMode, base_mode: StatusMode) -> ModeDescript
|
|||||||
period_ms: HEARTBEAT_POWER_MS,
|
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 {
|
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
|
StatusMode::Warning
|
||||||
} else if system_state.sticky_latched {
|
} else if system_state.sticky_latched {
|
||||||
StatusMode::ActivityFlash
|
StatusMode::ActivityFlash
|
||||||
@ -407,4 +419,15 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(determine_base_mode(state), StatusMode::NormalFlash);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user