Updated debounce and flash script

This commit is contained in:
Christoffer Martinsson 2025-11-08 19:29:11 +01:00
parent 8d7ec43515
commit 9c64a137a7
5 changed files with 44 additions and 20 deletions

View File

@ -86,6 +86,7 @@ impl Board {
hardware::MATRIX_DEBOUNCE_SCANS_PRESS, hardware::MATRIX_DEBOUNCE_SCANS_PRESS,
hardware::MATRIX_DEBOUNCE_SCANS_RELEASE, hardware::MATRIX_DEBOUNCE_SCANS_RELEASE,
hardware::MIN_PRESS_SPACING_SCANS, hardware::MIN_PRESS_SPACING_SCANS,
hardware::RELEASE_GRACE_PERIOD_SCANS,
); );
button_matrix.init_pins(); button_matrix.init_pins();

View File

@ -63,8 +63,10 @@ pub struct ButtonMatrix<P, const ROWS: usize, const COLS: usize, const KEYS: usi
release_threshold: u8, release_threshold: u8,
debounce_counter: [u8; KEYS], debounce_counter: [u8; KEYS],
last_press_scan: [u32; KEYS], last_press_scan: [u32; KEYS],
last_release_scan: [u32; KEYS],
scan_counter: u32, scan_counter: u32,
min_press_gap_scans: u32, min_press_gap_scans: u32,
release_grace_period_scans: u32,
} }
impl<P, const ROWS: usize, const COLS: usize, const KEYS: usize> ButtonMatrix<P, ROWS, COLS, KEYS> impl<P, const ROWS: usize, const COLS: usize, const KEYS: usize> ButtonMatrix<P, ROWS, COLS, KEYS>
@ -76,6 +78,7 @@ where
press_threshold: u8, press_threshold: u8,
release_threshold: u8, release_threshold: u8,
min_press_gap_scans: u32, min_press_gap_scans: u32,
release_grace_period_scans: u32,
) -> Self { ) -> Self {
debug_assert_eq!(KEYS, ROWS * COLS); debug_assert_eq!(KEYS, ROWS * COLS);
Self { Self {
@ -85,8 +88,10 @@ where
release_threshold, release_threshold,
debounce_counter: [0; KEYS], debounce_counter: [0; KEYS],
last_press_scan: [0; KEYS], last_press_scan: [0; KEYS],
last_release_scan: [0; KEYS],
scan_counter: 0, scan_counter: 0,
min_press_gap_scans, min_press_gap_scans,
release_grace_period_scans,
} }
} }
@ -133,7 +138,11 @@ where
if self.debounce_counter[button_index] >= threshold { if self.debounce_counter[button_index] >= threshold {
self.pressed[button_index] = match current_state { self.pressed[button_index] = match current_state {
true => self.should_register_press(button_index), true => self.should_register_press(button_index),
false => false, false => {
// Track release events for grace period
self.last_release_scan[button_index] = self.scan_counter;
false
}
}; };
self.debounce_counter[button_index] = 0; self.debounce_counter[button_index] = 0;
} }
@ -145,9 +154,13 @@ where
} }
fn should_register_press(&mut self, button_index: usize) -> bool { fn should_register_press(&mut self, button_index: usize) -> bool {
let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]); let press_elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[button_index]);
let can_register = self.last_press_scan[button_index] == 0 let release_elapsed = self.scan_counter.wrapping_sub(self.last_release_scan[button_index]);
|| elapsed >= self.min_press_gap_scans;
let can_register = (self.last_press_scan[button_index] == 0
|| press_elapsed >= self.min_press_gap_scans)
&& (self.last_release_scan[button_index] == 0
|| release_elapsed >= self.release_grace_period_scans);
if can_register { if can_register {
self.last_press_scan[button_index] = self.scan_counter; self.last_press_scan[button_index] = self.scan_counter;
@ -211,7 +224,7 @@ mod tests {
let row_state = Rc::new(Cell::new(false)); let row_state = Rc::new(Cell::new(false));
let column_state = Rc::new(Cell::new(false)); let column_state = Rc::new(Cell::new(false));
let pins = MockMatrixPins::new(row_state.clone(), column_state.clone()); let pins = MockMatrixPins::new(row_state.clone(), column_state.clone());
let matrix = ButtonMatrix::new(pins, 5, 5, 200); let matrix = ButtonMatrix::new(pins, 5, 5, 200, 100);
(matrix, row_state, column_state) (matrix, row_state, column_state)
} }

View File

@ -7,13 +7,17 @@ use rp2040_hal::gpio::Pins;
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.
/// Increased from 2/3 to 5/5 to prevent double characters from key bounce. /// Increased from 5/5 to 8/8 to prevent double characters from key bounce.
/// At 250μs scan rate: 5 scans = 1.25ms debounce time. /// At 250μs scan rate: 8 scans = 2ms debounce time.
pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 5; pub const MATRIX_DEBOUNCE_SCANS_PRESS: u8 = 8;
pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 5; pub const MATRIX_DEBOUNCE_SCANS_RELEASE: u8 = 8;
/// Minimum scans between two press events for the same key (50ms at 250μs scan cadence). /// Minimum scans between two press events for the same key (75ms at 250μs scan cadence).
pub const MIN_PRESS_SPACING_SCANS: u32 = 200; pub const MIN_PRESS_SPACING_SCANS: u32 = 300;
/// Grace period after key release before allowing new press (37.5ms at 250μs scan cadence).
/// This prevents phantom presses from release bounce.
pub const RELEASE_GRACE_PERIOD_SCANS: u32 = 150;
/// 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;

View File

@ -354,7 +354,7 @@ mod tests {
#[test] #[test]
fn idle_mode_backdates_start_time() { fn idle_mode_backdates_start_time() {
// Idle mode should start mid-breath so the LED resumes smoothly. // Idle mode should start mid-breath so the LED resumes smoothly.
let now = 10_000; let now: u32 = 10_000;
let expected = now.saturating_sub(BREATH_PERIOD_MS / 2); let expected = now.saturating_sub(BREATH_PERIOD_MS / 2);
assert_eq!(mode_start_time(StatusMode::Idle, now), expected); assert_eq!(mode_start_time(StatusMode::Idle, now), expected);
assert_eq!(mode_start_time(StatusMode::Active, now), now); assert_eq!(mode_start_time(StatusMode::Active, now), now);

View File

@ -41,9 +41,13 @@ def candidate_paths(explicit: str, user: str) -> list[Path]:
if not root.exists() or not root.is_dir(): if not root.exists() or not root.is_dir():
continue continue
# Many systems mount the UF2 volume directly as a child of the root directory. # Many systems mount the UF2 volume directly as a child of the root directory.
for child in root.iterdir(): try:
if child.is_dir(): for child in root.iterdir():
paths.append(child) if child.is_dir():
paths.append(child)
except PermissionError:
# Skip directories we can't read
continue
return paths return paths
@ -53,13 +57,11 @@ def choose_mount(explicit: str, user: str) -> Path | None:
# For an explicit mount we only care whether it exists. # For an explicit mount we only care whether it exists.
path = Path(explicit) path = Path(explicit)
return path if path.exists() and path.is_dir() else None return path if path.exists() and path.is_dir() else None
# Prefer candidates containing INFO_UF2.TXT, fall back to first existing directory. # Only accept candidates containing INFO_UF2.TXT (real RP2040 bootloader mounts)
info_candidates = [path for path in candidates if (path / INFO_FILE).exists()] info_candidates = [path for path in candidates if (path / INFO_FILE).exists()]
if info_candidates: if info_candidates:
return info_candidates[0] return info_candidates[0]
for path in candidates: # Don't fall back to random directories - only accept real RP2040 mounts
if path.exists() and path.is_dir():
return path
return None return None
@ -104,9 +106,13 @@ def main() -> int:
) )
else: else:
print( print(
"Unable to detect RP2040 UF2 mount. Pass one via mount=/path", "Unable to detect RP2040 UF2 mount. Make sure device is in bootloader mode:",
file=sys.stderr, file=sys.stderr,
) )
print("1. Press reset/boot button on rp2040 Zero board", file=sys.stderr)
print("2. Press upper left corner key when connecting USB", file=sys.stderr)
print("3. Press Fn+Fn+Shift+Shift+Ctrl chord", file=sys.stderr)
print("Then try again or pass explicit mount via mount=/run/media/$USER/RPI-RP2", file=sys.stderr)
return 1 return 1