Updated USB suspend implementaion

This commit is contained in:
Christoffer Martinsson 2025-09-20 20:42:06 +02:00
parent 9be79dd057
commit fc95f84eb0
3 changed files with 135 additions and 90 deletions

View File

@ -1,7 +1,7 @@
[package] [package]
name = "cmdr-keyboard" name = "cmdr-keyboard"
version = "0.2.0" version = "0.2.0"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
cortex-m = "0.7.2" cortex-m = "0.7.2"

View File

@ -1,9 +1,91 @@
//! Keyboard state management and HID report generation. //! Keyboard state management and HID report generation.
use crate::hardware;
use crate::{NUMBER_OF_KEYS, KeyMatrix, KeyReport}; use crate::{NUMBER_OF_KEYS, KeyMatrix, KeyReport};
use crate::layout; use crate::layout;
use crate::status::StatusSummary; use crate::status::StatusSummary;
use usbd_human_interface_device::page::Keyboard; use usbd_human_interface_device::page::Keyboard;
use usb_device::device::UsbDeviceState;
/// Tracks USB lifecycle state (suspend/idle/activity) for the keyboard HID device.
pub struct UsbState {
pub initialized: bool,
pub active: bool,
pub suspended: bool,
pub wake_on_input: bool,
pub idle_mode: bool,
activity: bool,
activity_elapsed_ms: u32,
}
impl UsbState {
pub const fn new() -> Self {
Self {
initialized: false,
active: false,
suspended: false,
wake_on_input: false,
idle_mode: false,
activity: false,
activity_elapsed_ms: 0,
}
}
pub fn on_poll(&mut self) {
if !self.initialized {
self.initialized = true;
}
if !self.active {
self.mark_activity();
}
self.active = true;
}
pub fn mark_activity(&mut self) {
self.activity = true;
self.activity_elapsed_ms = 0;
self.idle_mode = false;
}
pub fn handle_input_activity(&mut self) {
self.mark_activity();
if self.suspended && self.wake_on_input {
self.wake_on_input = false;
}
}
pub fn on_suspend_change(&mut self, state: UsbDeviceState) {
let was_suspended = self.suspended;
self.suspended = state == UsbDeviceState::Suspend;
match (was_suspended, self.suspended) {
(true, false) => {
self.mark_activity();
self.wake_on_input = false;
}
(false, true) => {
self.idle_mode = true;
self.activity = false;
self.wake_on_input = true;
}
_ => {}
}
}
pub fn advance_idle_timer(&mut self, interval_ms: u32) {
if !self.activity {
return;
}
self.activity_elapsed_ms = self.activity_elapsed_ms.saturating_add(interval_ms);
if self.activity_elapsed_ms >= hardware::timers::IDLE_TIMEOUT_MS {
self.activity = false;
self.activity_elapsed_ms = 0;
self.idle_mode = true;
}
}
pub fn acknowledge_report(&mut self) {}
}
/// Captures per-key state transitions and the function layer active when it was pressed. /// Captures per-key state transitions and the function layer active when it was pressed.
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
@ -38,6 +120,7 @@ pub struct KeyboardState {
sticky_key: Keyboard, sticky_key: Keyboard,
caps_lock_active: bool, caps_lock_active: bool,
started: bool, started: bool,
usb: UsbState,
} }
impl KeyboardState { impl KeyboardState {
@ -49,6 +132,7 @@ impl KeyboardState {
sticky_key: Keyboard::NoEventIndicated, sticky_key: Keyboard::NoEventIndicated,
caps_lock_active: false, caps_lock_active: false,
started: false, started: false,
usb: UsbState::new(),
} }
} }
@ -79,11 +163,18 @@ impl KeyboardState {
pub fn mark_started(&mut self) { pub fn mark_started(&mut self) {
// Note that the HID interface has successfully exchanged reports. // Note that the HID interface has successfully exchanged reports.
self.started = true; self.started = true;
self.usb.mark_activity();
self.usb.active = true;
self.usb.initialized = true;
} }
pub fn mark_stopped(&mut self) { pub fn mark_stopped(&mut self) {
// Reset the flag when USB communication fails. // Reset the flag when USB communication fails.
self.started = false; self.started = false;
self.usb.active = false;
self.usb.initialized = false;
self.usb.activity = false;
self.usb.idle_mode = false;
} }
pub fn started(&self) -> bool { pub fn started(&self) -> bool {
@ -107,22 +198,17 @@ impl KeyboardState {
}; };
} }
pub fn status_summary( pub fn status_summary(&self) -> StatusSummary {
&self,
usb_initialized: bool,
usb_active: bool,
usb_suspended: bool,
idle_mode: bool,
) -> StatusSummary {
// Produce a condensed summary consumed by the status LED driver. // Produce a condensed summary consumed by the status LED driver.
let usb_active = self.usb.active && !self.usb.idle_mode;
StatusSummary::new( StatusSummary::new(
self.caps_lock_active, self.caps_lock_active,
matches!(self.sticky_state, StickyState::Armed), matches!(self.sticky_state, StickyState::Armed),
matches!(self.sticky_state, StickyState::Latched), matches!(self.sticky_state, StickyState::Latched),
usb_initialized, self.usb.initialized,
usb_active, usb_active,
usb_suspended, self.usb.suspended,
idle_mode, self.usb.idle_mode,
) )
} }
@ -187,6 +273,14 @@ impl KeyboardState {
active_fn_keys.min(2) active_fn_keys.min(2)
} }
pub fn usb_state(&mut self) -> &mut UsbState {
&mut self.usb
}
pub fn usb(&self) -> &UsbState {
&self.usb
}
} }
impl Default for KeyboardState { impl Default for KeyboardState {
@ -246,8 +340,7 @@ mod tests {
let mut state = KeyboardState::new(); let mut state = KeyboardState::new();
state.update_caps_lock(true); state.update_caps_lock(true);
state.mark_started(); state.mark_started();
let summary = state.status_summary();
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);

View File

@ -4,6 +4,7 @@
//! Email: cm@cmtec.se //! Email: cm@cmtec.se
//! License: Please refer to LICENSE in root directory //! License: Please refer to LICENSE in root directory
//! Firmware entry orchestrating the CMDR Keyboard runtime loop.
#![no_std] #![no_std]
#![no_main] #![no_main]
@ -13,45 +14,18 @@ use embedded_hal_0_2::timer::CountDown;
use fugit::ExtU32; use fugit::ExtU32;
use panic_halt as _; use panic_halt as _;
use usb_device::UsbError; use usb_device::UsbError;
use usb_device::device::UsbDeviceState;
use usb_device::prelude::*; use usb_device::prelude::*;
use usbd_human_interface_device::device::keyboard::NKROBootKeyboardConfig; use usbd_human_interface_device::device::keyboard::NKROBootKeyboardConfig;
use usbd_human_interface_device::page::Keyboard; use usbd_human_interface_device::page::Keyboard;
use usbd_human_interface_device::prelude::UsbHidError; use usbd_human_interface_device::prelude::UsbHidError;
use usbd_human_interface_device::prelude::*; use usbd_human_interface_device::prelude::*;
// The boot2 image must live in the dedicated ROM section, which requires these attributes. // Embed the boot2 image for the W25Q080 flash; required for RP2040 to boot from external flash.
#[unsafe(link_section = ".boot2")] #[link_section = ".boot2"]
#[unsafe(no_mangle)] #[no_mangle]
#[used] #[used]
pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080; pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
fn handle_usb_state_changes(
usb_dev: &UsbDevice<rp2040_hal::usb::UsbBus>,
usb_suspended: &mut bool,
wake_on_input: &mut bool,
last_activity_ms: &mut u32,
status_time_ms: u32,
) {
// Track suspend/resume transitions and refresh idle timers when USB wakes.
let current_suspended = usb_dev.state() == UsbDeviceState::Suspend;
let was_suspended = *usb_suspended;
match (was_suspended, current_suspended) {
(true, false) => {
*last_activity_ms = status_time_ms;
*wake_on_input = false;
}
(false, true) => {
*wake_on_input = true;
}
_ => {}
}
*usb_suspended = current_suspended;
}
#[rp2040_hal::entry] #[rp2040_hal::entry]
fn main() -> ! { fn main() -> ! {
// Bring up the board peripherals and split them into reusable parts. // Bring up the board peripherals and split them into reusable parts.
@ -94,38 +68,30 @@ fn main() -> ! {
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis()); status_tick.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 suspended_scan_counter: u8 = 0;
let mut usb_suspended = false;
let mut wake_on_input = false;
let mut last_activity_ms: u32 = 0;
let mut suspended_scan_divider: u8 = 0;
loop { loop {
if status_tick.wait().is_ok() { if status_tick.wait().is_ok() {
// Update the status LED summary on its cadence. // Update the status LED summary on its cadence.
status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS); status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS);
let idle_elapsed = status_time_ms.saturating_sub(last_activity_ms); {
let idle_mode = usb_initialized && idle_elapsed >= timers::IDLE_TIMEOUT_MS; keyboard_state
let usb_active = usb_initialized && !idle_mode; .usb_state()
status_led.apply_summary( .advance_idle_timer(timers::STATUS_LED_INTERVAL_MS);
keyboard_state.status_summary( }
usb_initialized, let summary = keyboard_state.status_summary();
usb_active, status_led.apply_summary(summary, status_time_ms);
usb_suspended,
idle_mode,
),
status_time_ms,
);
} }
// When suspended, thin out scans to reduce power but keep responsiveness. let should_scan = {
const SUSPENDED_SCAN_PERIOD: u8 = 20; const SUSPENDED_SCAN_PERIOD: u8 = 20;
let should_scan = if !usb_suspended { if keyboard_state.usb().suspended {
suspended_scan_divider = 0; suspended_scan_counter = (suspended_scan_counter + 1) % SUSPENDED_SCAN_PERIOD;
true suspended_scan_counter == 0
} else { } else {
suspended_scan_divider = (suspended_scan_divider + 1) % SUSPENDED_SCAN_PERIOD; suspended_scan_counter = 0;
suspended_scan_divider == 0 true
}
}; };
if usb_tick.wait().is_ok() && should_scan { if usb_tick.wait().is_ok() && should_scan {
@ -134,7 +100,7 @@ fn main() -> ! {
let pressed_keys = button_matrix.buttons_pressed(); let pressed_keys = button_matrix.buttons_pressed();
if bootloader::chord_requested(&pressed_keys) { if bootloader::chord_requested(&pressed_keys) {
if !usb_suspended { if !keyboard_state.usb().suspended {
for _ in 0..3 { for _ in 0..3 {
let clear_report: KeyReport = [Keyboard::NoEventIndicated; hardware::NUMBER_OF_KEYS]; let clear_report: KeyReport = [Keyboard::NoEventIndicated; hardware::NUMBER_OF_KEYS];
match keyboard.device().write_report(clear_report) { match keyboard.device().write_report(clear_report) {
@ -152,24 +118,18 @@ fn main() -> ! {
} }
if pressed_keys.iter().any(|pressed| *pressed) { if pressed_keys.iter().any(|pressed| *pressed) {
last_activity_ms = status_time_ms; keyboard_state.usb_state().handle_input_activity();
if wake_on_input && usb_suspended {
wake_on_input = false;
}
} }
let keyboard_report = keyboard_state.process_scan(pressed_keys); let keyboard_report = keyboard_state.process_scan(pressed_keys);
if !usb_suspended { if !keyboard_state.usb().suspended {
// Try to send the generated report to the host. // Try to send the generated report to the host.
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(_) => {}
usb_initialized = true;
}
Err(_) => { Err(_) => {
keyboard_state.mark_stopped(); keyboard_state.mark_stopped();
usb_initialized = false;
} }
} }
} }
@ -178,34 +138,26 @@ fn main() -> ! {
Err(UsbHidError::WouldBlock) | Ok(_) => {} Err(UsbHidError::WouldBlock) | Ok(_) => {}
Err(_) => { Err(_) => {
keyboard_state.mark_stopped(); keyboard_state.mark_stopped();
usb_initialized = false;
} }
} }
} }
if usb_dev.poll(&mut [&mut keyboard]) { if usb_dev.poll(&mut [&mut keyboard]) {
keyboard_state.usb_state().on_poll();
// Consume OUT reports (e.g., LED indicators) and track host activity. // Consume OUT reports (e.g., LED indicators) and track host activity.
match keyboard.device().read_report() { match keyboard.device().read_report() {
Err(UsbError::WouldBlock) => {} Err(UsbError::WouldBlock) => {}
Err(_) => { Err(_) => {
keyboard_state.mark_stopped(); keyboard_state.mark_stopped();
usb_initialized = false;
} }
Ok(leds) => { Ok(leds) => {
keyboard_state.update_caps_lock(leds.caps_lock); keyboard_state.update_caps_lock(leds.caps_lock);
keyboard_state.mark_started(); keyboard_state.mark_started();
usb_initialized = true;
last_activity_ms = status_time_ms;
} }
} }
} }
keyboard_state
handle_usb_state_changes( .usb_state()
&usb_dev, .on_suspend_change(usb_dev.state());
&mut usb_suspended,
&mut wake_on_input,
&mut last_activity_ms,
status_time_ms,
);
} }
} }