Updated USB suspend implementaion
This commit is contained in:
parent
9be79dd057
commit
fc95f84eb0
@ -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"
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user