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
|
## Features
|
||||||
|
|
||||||
- Ergonomic design (low profile)
|
- Ergonomic design (low profile)
|
||||||
- Hall‑effect gimbals (FrSky M7/M10)
|
- Hall-effect gimbals (FrSky M7/M10)
|
||||||
- USB HID joystick device
|
- USB HID joystick device
|
||||||
- 7 axes: X, Y, Z, Rx, Ry, Rz, Slider
|
- 7 axes: X, Y, Z, Rx, Ry, Rz, Slider
|
||||||
- 32 buttons
|
- 32 buttons
|
||||||
- 1× 8‑way HAT
|
- 1× 8‑way HAT
|
||||||
- Advanced input pipeline
|
- Advanced input pipeline
|
||||||
- Digital smoothing for stable axes
|
- 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)
|
- Optional exponential response curves (LUT based)
|
||||||
- Throttle hold (capture + remap around center)
|
- 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
|
- Status LED (WS2812 via PIO) for mode/health indication
|
||||||
- Power-on heartbeat (green) before USB enumeration
|
- Power-on heartbeat (green) before USB enumeration
|
||||||
- Activity colors: green (active), blue (virtual throttle/calibration), orange (holds)
|
- Activity colors: green (active), blue (virtual throttle/calibration), orange (holds)
|
||||||
- Warning/error tones (red) and bootloader purple
|
- Warning/error tones (red) and bootloader purple
|
||||||
- Idle heartbeat flashes at half speed once inputs settle
|
- 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
|
## Hardware
|
||||||
|
|
||||||
- 2x FrSky M7 or M10 gimbals [M7 datasheet](https://www.frsky-rc.com/product/m7/)
|
- 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).
|
/// Button matrix scan interval (µs).
|
||||||
pub const SCAN_INTERVAL_US: u32 = 200;
|
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).
|
/// 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.
|
/// USB activity timeout (ms) - stop sending reports after this period of inactivity.
|
||||||
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000; // 5 seconds
|
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();
|
let mut scan_count_down = timer.count_down();
|
||||||
scan_count_down.start(timers::SCAN_INTERVAL_US.micros());
|
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();
|
let mut usb_update_count_down = timer.count_down();
|
||||||
usb_update_count_down.start(timers::USB_UPDATE_INTERVAL_MS.millis());
|
usb_update_count_down.start(timers::USB_UPDATE_INTERVAL_MS.millis());
|
||||||
|
|
||||||
let mut usb_activity: bool = false;
|
let mut usb_activity: bool = false;
|
||||||
let mut usb_active: bool = false;
|
let mut usb_active: bool = false;
|
||||||
let mut usb_initialized: bool = false;
|
let mut usb_initialized: bool = false;
|
||||||
|
let mut usb_send_pending: bool = false;
|
||||||
let mut vt_enable: bool = false;
|
let mut vt_enable: bool = false;
|
||||||
let mut idle_mode: bool = false;
|
let mut idle_mode: bool = false;
|
||||||
let mut usb_activity_timeout_count: u32 = 0;
|
let mut usb_activity_timeout_count: u32 = 0;
|
||||||
@ -295,12 +293,13 @@ fn main() -> ! {
|
|||||||
usb_activity = true; // Force initial report
|
usb_activity = true; // Force initial report
|
||||||
idle_mode = false;
|
idle_mode = false;
|
||||||
usb_activity_timeout_count = 0;
|
usb_activity_timeout_count = 0;
|
||||||
|
usb_send_pending = true;
|
||||||
}
|
}
|
||||||
usb_active = true;
|
usb_active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if scan_count_down.wait().is_ok() {
|
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:
|
// Sample all inputs at high frequency for responsive control:
|
||||||
// - Button matrix scanning with debouncing
|
// - Button matrix scanning with debouncing
|
||||||
@ -324,42 +323,9 @@ fn main() -> ! {
|
|||||||
// Apply digital smoothing filters to reduce ADC noise and jitter
|
// Apply digital smoothing filters to reduce ADC noise and jitter
|
||||||
axis_manager.update_smoothers(&mut smoother, &raw_values);
|
axis_manager.update_smoothers(&mut smoother, &raw_values);
|
||||||
|
|
||||||
// Note: Filtered values are processed in the data processing phase
|
// ## Immediate Data Processing (formerly 1000 Hz)
|
||||||
// through calculate_axis_value() with expo curves and calibration
|
|
||||||
}
|
|
||||||
|
|
||||||
if status_led_count_down.wait().is_ok() {
|
|
||||||
// ## Status LED Updates (100Hz)
|
|
||||||
//
|
//
|
||||||
// Update status LED to reflect current system state:
|
// Process all input data right after sampling for minimal latency.
|
||||||
// - 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
|
|
||||||
|
|
||||||
// Update button states from matrix scan and extra buttons
|
// Update button states from matrix scan and extra buttons
|
||||||
button_manager.update_from_matrix(&mut button_matrix);
|
button_manager.update_from_matrix(&mut button_matrix);
|
||||||
@ -426,19 +392,12 @@ fn main() -> ! {
|
|||||||
// Always update calibration for dynamic min/max tracking when active
|
// Always update calibration for dynamic min/max tracking when active
|
||||||
calibration_manager.update_dynamic_calibration(&mut axis_manager.axes, &smoother);
|
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
|
// Process gimbal axes through calibration, expo curves, and scaling
|
||||||
if axis_manager.process_axis_values(&smoother, &expo_lut) {
|
if axis_manager.process_axis_values(&smoother, &expo_lut) {
|
||||||
usb_activity = true;
|
usb_activity = true;
|
||||||
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
||||||
idle_mode = false;
|
idle_mode = false;
|
||||||
|
usb_send_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update virtual axes based on front button states
|
// Update virtual axes based on front button states
|
||||||
@ -446,6 +405,7 @@ fn main() -> ! {
|
|||||||
usb_activity = true;
|
usb_activity = true;
|
||||||
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
||||||
idle_mode = false;
|
idle_mode = false;
|
||||||
|
usb_send_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process button logic (press types, timing, USB mapping)
|
// Process button logic (press types, timing, USB mapping)
|
||||||
@ -453,10 +413,34 @@ fn main() -> ! {
|
|||||||
usb_activity = true;
|
usb_activity = true;
|
||||||
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
||||||
idle_mode = false;
|
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.
|
// Transmit USB HID reports only when there is input activity.
|
||||||
// This power-management approach prevents the computer from staying
|
// This power-management approach prevents the computer from staying
|
||||||
@ -470,18 +454,10 @@ fn main() -> ! {
|
|||||||
|
|
||||||
// Only transmit USB reports when input activity is detected
|
// Only transmit USB reports when input activity is detected
|
||||||
let usb_tick = usb_update_count_down.wait().is_ok();
|
let usb_tick = usb_update_count_down.wait().is_ok();
|
||||||
|
if usb_activity && (usb_tick || usb_send_pending) {
|
||||||
if usb_tick && usb_activity {
|
let mut send_report = || {
|
||||||
// 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 {
|
|
||||||
let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual);
|
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);
|
let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual);
|
||||||
|
|
||||||
match usb_hid_joystick.device().write_report(&get_joystick_report(
|
match usb_hid_joystick.device().write_report(&get_joystick_report(
|
||||||
button_manager.buttons_mut(),
|
button_manager.buttons_mut(),
|
||||||
&mut axis_manager.axes,
|
&mut axis_manager.axes,
|
||||||
@ -490,12 +466,28 @@ fn main() -> ! {
|
|||||||
&vt_enable,
|
&vt_enable,
|
||||||
)) {
|
)) {
|
||||||
Err(UsbHidError::WouldBlock) => {}
|
Err(UsbHidError::WouldBlock) => {}
|
||||||
Ok(_) => {}
|
Ok(_) => {
|
||||||
|
usb_send_pending = false;
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
status_led.update(StatusMode::Error);
|
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 {
|
} else if usb_tick && usb_active {
|
||||||
idle_mode = true;
|
idle_mode = true;
|
||||||
|
|||||||
@ -185,7 +185,7 @@ impl Default for JoystickConfig<'_> {
|
|||||||
unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR))
|
unwrap!(unwrap!(InterfaceBuilder::new(JOYSTICK_DESCRIPTOR))
|
||||||
.boot_device(InterfaceProtocol::None)
|
.boot_device(InterfaceProtocol::None)
|
||||||
.description("Joystick")
|
.description("Joystick")
|
||||||
.in_endpoint(10.millis()))
|
.in_endpoint(1.millis()))
|
||||||
.without_out_endpoint()
|
.without_out_endpoint()
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user