Updated USB suspend implementaion
This commit is contained in:
parent
9be79dd057
commit
fc95f84eb0
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "cmdr-keyboard"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cortex-m = "0.7.2"
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user