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]
name = "cmdr-keyboard"
version = "0.2.0"
edition = "2024"
edition = "2021"
[dependencies]
cortex-m = "0.7.2"

View File

@ -1,9 +1,91 @@
//! Keyboard state management and HID report generation.
use crate::hardware;
use crate::{NUMBER_OF_KEYS, KeyMatrix, KeyReport};
use crate::layout;
use crate::status::StatusSummary;
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.
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
@ -38,6 +120,7 @@ pub struct KeyboardState {
sticky_key: Keyboard,
caps_lock_active: bool,
started: bool,
usb: UsbState,
}
impl KeyboardState {
@ -49,6 +132,7 @@ impl KeyboardState {
sticky_key: Keyboard::NoEventIndicated,
caps_lock_active: false,
started: false,
usb: UsbState::new(),
}
}
@ -79,11 +163,18 @@ impl KeyboardState {
pub fn mark_started(&mut self) {
// Note that the HID interface has successfully exchanged reports.
self.started = true;
self.usb.mark_activity();
self.usb.active = true;
self.usb.initialized = true;
}
pub fn mark_stopped(&mut self) {
// Reset the flag when USB communication fails.
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 {
@ -107,22 +198,17 @@ impl KeyboardState {
};
}
pub fn status_summary(
&self,
usb_initialized: bool,
usb_active: bool,
usb_suspended: bool,
idle_mode: bool,
) -> StatusSummary {
pub fn status_summary(&self) -> StatusSummary {
// Produce a condensed summary consumed by the status LED driver.
let usb_active = self.usb.active && !self.usb.idle_mode;
StatusSummary::new(
self.caps_lock_active,
matches!(self.sticky_state, StickyState::Armed),
matches!(self.sticky_state, StickyState::Latched),
usb_initialized,
self.usb.initialized,
usb_active,
usb_suspended,
idle_mode,
self.usb.suspended,
self.usb.idle_mode,
)
}
@ -187,6 +273,14 @@ impl KeyboardState {
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 {
@ -246,8 +340,7 @@ mod tests {
let mut state = KeyboardState::new();
state.update_caps_lock(true);
state.mark_started();
let summary = state.status_summary(true, true, false, false);
let summary = state.status_summary();
assert!(summary.caps_lock_active);
assert!(summary.usb_active);
assert!(summary.usb_initialized);

View File

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