Improved latency

This commit is contained in:
Christoffer Martinsson 2025-09-18 10:50:07 +02:00
parent a165558875
commit fa841fda65
4 changed files with 66 additions and 69 deletions

View File

@ -68,23 +68,31 @@ Config Layer (holding CONFIG button)
## Features
- Ergonomic design (low profile)
- Halleffect 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× 8way HAT
- Advanced input pipeline
- Digital smoothing for stable axes
- Peraxis 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 rightX 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/)

View File

@ -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

View File

@ -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;

View File

@ -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(),
)