Updated debounce and flash script
This commit is contained in:
parent
8d7ec43515
commit
9c64a137a7
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user