Improved latency
This commit is contained in:
parent
a165558875
commit
fa841fda65
14
README.md
14
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/)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user