From fa841fda6590ebb7f44ef7939390f6f75d60c9d6 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Thu, 18 Sep 2025 10:50:07 +0200 Subject: [PATCH] Improved latency --- README.md | 14 +++- rp2040/src/hardware.rs | 5 +- rp2040/src/main.rs | 114 ++++++++++++++---------------- rp2040/src/usb_joystick_device.rs | 2 +- 4 files changed, 66 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 55941c0..7d300d1 100644 --- a/README.md +++ b/README.md @@ -68,23 +68,31 @@ Config Layer (holding CONFIG button) ## Features - Ergonomic design (low profile) -- Hall‑effect gimbals (FrSky M7/M10) +- Hall-effect gimbals (FrSky M7/M10) - USB HID joystick device - 7 axes: X, Y, Z, Rx, Ry, Rz, Slider - 32 buttons - 1× 8‑way HAT - Advanced input pipeline - Digital smoothing for stable axes - - Per‑axis calibration (min/center/max) with EEPROM persistence + - Per-axis calibration (min/center/max) with EEPROM persistence - Optional exponential response curves (LUT based) - Throttle hold (capture + remap around center) - - Virtual throttle mode (map right‑X to Slider; disable Z) + - Virtual throttle mode (map right-X to Slider; disable Z) + - 1 ms USB HID poll rate with immediate post-scan processing for minimal latency - Status LED (WS2812 via PIO) for mode/health indication - Power-on heartbeat (green) before USB enumeration - Activity colors: green (active), blue (virtual throttle/calibration), orange (holds) - Warning/error tones (red) and bootloader purple - Idle heartbeat flashes at half speed once inputs settle +## Low-latency firmware path + +- USB interrupt endpoint configured for 1 ms poll interval (1 kHz reports) +- Input scan, smoothing, processing, and mapping now execute back-to-back +- First activity after idle forces immediate USB packet without waiting for the next tick +- Existing idle timeout preserved (5 s) to avoid unnecessary host wake-ups + ## Hardware - 2x FrSky M7 or M10 gimbals [M7 datasheet](https://www.frsky-rc.com/product/m7/) diff --git a/rp2040/src/hardware.rs b/rp2040/src/hardware.rs index 6451938..75dd25e 100644 --- a/rp2040/src/hardware.rs +++ b/rp2040/src/hardware.rs @@ -90,11 +90,8 @@ pub mod timers { /// Button matrix scan interval (µs). pub const SCAN_INTERVAL_US: u32 = 200; - /// Data processing interval (µs) for axis/button logic. - pub const DATA_PROCESS_INTERVAL_US: u32 = 1200; - /// USB HID report interval (ms). - pub const USB_UPDATE_INTERVAL_MS: u32 = 10; + pub const USB_UPDATE_INTERVAL_MS: u32 = 1; /// USB activity timeout (ms) - stop sending reports after this period of inactivity. pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000; // 5 seconds diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index 4d81399..10df3ee 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -224,15 +224,13 @@ fn main() -> ! { let mut scan_count_down = timer.count_down(); scan_count_down.start(timers::SCAN_INTERVAL_US.micros()); - let mut data_process_count_down = timer.count_down(); - data_process_count_down.start(timers::DATA_PROCESS_INTERVAL_US.micros()); - let mut usb_update_count_down = timer.count_down(); usb_update_count_down.start(timers::USB_UPDATE_INTERVAL_MS.millis()); let mut usb_activity: bool = false; let mut usb_active: bool = false; let mut usb_initialized: bool = false; + let mut usb_send_pending: bool = false; let mut vt_enable: bool = false; let mut idle_mode: bool = false; let mut usb_activity_timeout_count: u32 = 0; @@ -295,12 +293,13 @@ fn main() -> ! { usb_activity = true; // Force initial report idle_mode = false; usb_activity_timeout_count = 0; + usb_send_pending = true; } usb_active = true; } if scan_count_down.wait().is_ok() { - // ## High-Frequency Input Sampling (1kHz) + // ## High-Frequency Input Sampling (~5 kHz) // // Sample all inputs at high frequency for responsive control: // - Button matrix scanning with debouncing @@ -324,42 +323,9 @@ fn main() -> ! { // Apply digital smoothing filters to reduce ADC noise and jitter axis_manager.update_smoothers(&mut smoother, &raw_values); - // Note: Filtered values are processed in the data processing phase - // through calculate_axis_value() with expo curves and calibration - } - - if status_led_count_down.wait().is_ok() { - // ## Status LED Updates (100Hz) + // ## Immediate Data Processing (formerly 1000 Hz) // - // Update status LED to reflect current system state: - // - Green: Normal operation with USB connection - // - Blue: Calibration mode active - // - Yellow: Throttle hold or Virtual Throttle enabled - // - Red: Error state or disconnected - // - Purple: Bootloader mode - - let system_state = SystemState { - usb_active, - usb_initialized, - idle_mode, - calibration_active: calibration_manager.is_active(), - throttle_hold_enable: axis_manager.throttle_hold_enable, - vt_enable, - }; - status_led.update_from_system_state( - system_state, - (timer.get_counter().ticks() / 1000) as u32, - ); - } - - if data_process_count_down.wait().is_ok() { - // ## Medium-Frequency Data Processing (100Hz) - // - // Process all input data and handle complex logic: - // - Button state management and special combinations - // - Axis processing with expo curves and calibration - // - Calibration system updates and mode selection - // - Virtual axis control and throttle hold processing + // Process all input data right after sampling for minimal latency. // Update button states from matrix scan and extra buttons button_manager.update_from_matrix(&mut button_matrix); @@ -426,19 +392,12 @@ fn main() -> ! { // Always update calibration for dynamic min/max tracking when active calibration_manager.update_dynamic_calibration(&mut axis_manager.axes, &smoother); - // ### Axis Processing Pipeline - // - // Complete axis processing chain: - // 1. Apply exponential curves for enhanced feel - // 2. Handle throttle hold functionality - // 3. Update virtual axes (RY/RZ) from button input - // 4. Track axis movement for USB activity detection - // Process gimbal axes through calibration, expo curves, and scaling if axis_manager.process_axis_values(&smoother, &expo_lut) { usb_activity = true; usb_activity_timeout_count = 0; // Reset timeout on real input activity idle_mode = false; + usb_send_pending = true; } // Update virtual axes based on front button states @@ -446,6 +405,7 @@ fn main() -> ! { usb_activity = true; usb_activity_timeout_count = 0; // Reset timeout on real input activity idle_mode = false; + usb_send_pending = true; } // Process button logic (press types, timing, USB mapping) @@ -453,10 +413,34 @@ fn main() -> ! { usb_activity = true; usb_activity_timeout_count = 0; // Reset timeout on real input activity idle_mode = false; + usb_send_pending = true; } } - // ## USB HID Report Transmission (20Hz) + if status_led_count_down.wait().is_ok() { + // ## Status LED Updates (100Hz) + // + // Update status LED to reflect current system state: + // - Green: Normal operation with USB connection + // - Blue: Calibration mode active + // - Yellow: Throttle hold or Virtual Throttle enabled + // - Red: Error state or disconnected + // - Purple: Bootloader mode + + let system_state = SystemState { + usb_active, + usb_initialized, + idle_mode, + calibration_active: calibration_manager.is_active(), + throttle_hold_enable: axis_manager.throttle_hold_enable, + vt_enable, + }; + status_led.update_from_system_state( + system_state, + (timer.get_counter().ticks() / 1000) as u32, + ); + } + // ## USB HID Report Transmission (up to 1 kHz) // // Transmit USB HID reports only when there is input activity. // This power-management approach prevents the computer from staying @@ -470,18 +454,10 @@ fn main() -> ! { // Only transmit USB reports when input activity is detected let usb_tick = usb_update_count_down.wait().is_ok(); - - if usb_tick && usb_activity { - // Check if we've exceeded the activity timeout - usb_activity_timeout_count += timers::USB_UPDATE_INTERVAL_MS; - if usb_activity_timeout_count >= timers::USB_ACTIVITY_TIMEOUT_MS { - usb_activity = false; // Stop sending reports after timeout - usb_activity_timeout_count = 0; - idle_mode = true; - } else { + if usb_activity && (usb_tick || usb_send_pending) { + let mut send_report = || { let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual); let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual); - match usb_hid_joystick.device().write_report(&get_joystick_report( button_manager.buttons_mut(), &mut axis_manager.axes, @@ -490,12 +466,28 @@ fn main() -> ! { &vt_enable, )) { Err(UsbHidError::WouldBlock) => {} - Ok(_) => {} + Ok(_) => { + usb_send_pending = false; + } Err(e) => { status_led.update(StatusMode::Error); - core::panic!("Failed to write joystick report: {:?}", e) + core::panic!("Failed to write joystick report: {:?}", e); } - }; + } + }; + + if usb_tick { + usb_activity_timeout_count += timers::USB_UPDATE_INTERVAL_MS; + if usb_activity_timeout_count >= timers::USB_ACTIVITY_TIMEOUT_MS { + usb_activity = false; + usb_activity_timeout_count = 0; + idle_mode = true; + usb_send_pending = false; + } else { + send_report(); + } + } else { + send_report(); } } else if usb_tick && usb_active { idle_mode = true; diff --git a/rp2040/src/usb_joystick_device.rs b/rp2040/src/usb_joystick_device.rs index 51aa5b7..c5e5a68 100644 --- a/rp2040/src/usb_joystick_device.rs +++ b/rp2040/src/usb_joystick_device.rs @@ -185,7 +185,7 @@ impl Default for JoystickConfig<'_> { unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR)) .boot_device(InterfaceProtocol::None) .description("Joystick") - .in_endpoint(10.millis())) + .in_endpoint(1.millis())) .without_out_endpoint() .build(), )