Code commenting/documentation
This commit is contained in:
parent
6cf9bde7e0
commit
b98614fe8e
92
README.md
92
README.md
@ -1,6 +1,8 @@
|
|||||||
# CMDR Joystick 25
|
# CMDR Joystick 25
|
||||||
|
|
||||||
USB Joystick with 2 hall effect gimbals, 2 hat switches and 19 buttons.
|
USB HID joystick firmware + hardware: 2 hall‑effect gimbals, 2 physical hat
|
||||||
|
switches, and a 5x5 button matrix (plus 2 extra buttons). The firmware exposes
|
||||||
|
7 HID axes (X, Y, Z, Rx, Ry, Rz, Slider), 32 buttons and one 8‑way HAT.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ USB HID joystick map :
|
|||||||
| | H1L | B11 | H1R | | H2L | B12 | H2R | |
|
| | H1L | B11 | H1R | | H2L | B12 | H2R | |
|
||||||
| | H1D | | H2D | |
|
| | H1D | | H2D | |
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
|
|
||||||
USB HID joystick map (Fn):
|
USB HID joystick map (Fn):
|
||||||
---------------------------------------------------------------
|
---------------------------------------------------------------
|
||||||
| Fn L| B21 U| B27 U| | B32 | | B8 U| B3 U| B4 L|
|
| Fn L| B21 U| B27 U| | B32 | | B8 U| B3 U| B4 L|
|
||||||
@ -66,11 +68,18 @@ Config Layer (holding CONFIG button)
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Ergonomic design (low profile)
|
- Ergonomic design (low profile)
|
||||||
- Hall effect gimbals
|
- Hall‑effect gimbals (FrSky M7/M10)
|
||||||
- Supports USB HID joystick
|
- USB HID joystick device
|
||||||
- 4x axis
|
- 7 axes: X, Y, Z, Rx, Ry, Rz, Slider
|
||||||
- 4x hat switches (4x virtual, 2x hardware)
|
- 32 buttons
|
||||||
- 32x buttons (32x virtual, 19x hardware)
|
- 1× 8‑way HAT
|
||||||
|
- Advanced input pipeline
|
||||||
|
- Digital smoothing for stable axes
|
||||||
|
- 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)
|
||||||
|
- Status LED (WS2812 via PIO) for mode/health indication
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
@ -83,7 +92,6 @@ Config Layer (holding CONFIG button)
|
|||||||
- 1x Top plate (3D printed)
|
- 1x Top plate (3D printed)
|
||||||
- 2x Hat swith top (3D printed) [stl](/mCAD/Hat_Castle_Short_scale_99_99_130.stl)
|
- 2x Hat swith top (3D printed) [stl](/mCAD/Hat_Castle_Short_scale_99_99_130.stl)
|
||||||
- 1x Custom PCB (CMDR Joystick 25 rev A)
|
- 1x Custom PCB (CMDR Joystick 25 rev A)
|
||||||
|
|
||||||
- 
|
- 
|
||||||
- 
|
- 
|
||||||
- Gerber files: [zip](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_gerber.zip)
|
- Gerber files: [zip](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_gerber.zip)
|
||||||
@ -95,14 +103,32 @@ Config Layer (holding CONFIG button)
|
|||||||
- N-Fet 2N7002: [pdf](https://www.mouser.se/datasheet/2/408/T2N7002AK_datasheet_en_20150401-1916411.pdf)
|
- N-Fet 2N7002: [pdf](https://www.mouser.se/datasheet/2/408/T2N7002AK_datasheet_en_20150401-1916411.pdf)
|
||||||
- Small signal diod 1N4148W: [pdf](https://www.diodes.com/assets/Datasheets/BAV16W_1N4148W.pdf)
|
- Small signal diod 1N4148W: [pdf](https://www.diodes.com/assets/Datasheets/BAV16W_1N4148W.pdf)
|
||||||
|
|
||||||
## Software Build environment
|
## Build, test, and flash (install.sh)
|
||||||
|
|
||||||
##### Rust
|
Use the top‑level `install.sh` to run tests, build, and flash. It handles
|
||||||
|
prerequisites, target setup, UF2 conversion, and optional SSH deployment.
|
||||||
|
|
||||||
- Cargo (rust embedded)
|
Common commands
|
||||||
- Flashing via Cargo
|
|
||||||
- Press and hold boot button on rp2040zero board while perform a reset
|
```
|
||||||
- Press and hold CONF and press BOOT button.
|
# Run comprehensive tests (host + embedded checks + clippy + release build)
|
||||||
|
./install.sh test
|
||||||
|
|
||||||
|
# Build and flash locally (copies UF2 to RPI-RP2 mass storage)
|
||||||
|
./install.sh flash --local
|
||||||
|
|
||||||
|
# Build and transfer UF2 via SSH to a remote machine
|
||||||
|
./install.sh flash --ssh --target user@host --mount /Volumes/RPI-RP2
|
||||||
|
|
||||||
|
# Clean build artifacts and temporary files
|
||||||
|
./install.sh clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Entering bootloader
|
||||||
|
|
||||||
|
- Hardware: hold BOOTSEL while plugging USB
|
||||||
|
- Firmware shortcut (at power‑on): hold the front‑left‑lower button
|
||||||
|
- In‑firmware combo: Front‑left‑lower + Top‑left‑mode + Top‑right‑mode
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
@ -111,6 +137,38 @@ Config Layer (holding CONFIG button)
|
|||||||
## Calibration
|
## Calibration
|
||||||
|
|
||||||
1. Center both gimbals.
|
1. Center both gimbals.
|
||||||
2. Press and hold CONF button and press CAL botton. Status led will start blinking green.
|
2. Start calibration (LED will flash): press Front‑left‑upper + Top‑left‑mode + Top‑right‑mode.
|
||||||
3. Move both gimbals to all corners.
|
3. Move both gimbals through full travel (all corners).
|
||||||
4. Press right hat switch to save calibration data to eeprom.
|
4. Optional: select gimbal mode while calibrating
|
||||||
|
- M10: press Top‑left‑UP
|
||||||
|
- M7: press Top‑left‑DOWN
|
||||||
|
5. Save calibration to EEPROM: press the right hat center button.
|
||||||
|
6. Exit calibration: repeat step 2 combination (toggles off).
|
||||||
|
|
||||||
|
Notes
|
||||||
|
|
||||||
|
- During calibration, min/max are tracked from the smoothed ADC values.
|
||||||
|
- On mode change (M10/M7) centers are reset from the current position.
|
||||||
|
|
||||||
|
## Runtime controls
|
||||||
|
|
||||||
|
- Throttle hold: press the Top‑left‑mode button to capture the current throttle value
|
||||||
|
- Press again at center to clear hold
|
||||||
|
- Virtual throttle mode: press the Top‑right‑mode button to toggle
|
||||||
|
- Disables Z axis and maps right‑X to the Slider (symmetric around center)
|
||||||
|
|
||||||
|
## Development and testing
|
||||||
|
|
||||||
|
Run unit tests on host (uses `std` for test modules):
|
||||||
|
|
||||||
|
```
|
||||||
|
cd rp2040
|
||||||
|
cargo test --features std
|
||||||
|
```
|
||||||
|
|
||||||
|
Repo structure
|
||||||
|
|
||||||
|
- `rp2040/src/*.rs`: firmware modules (axis, buttons, matrix, calibration, storage, HID)
|
||||||
|
- `rp2040/memory.x`: linker script
|
||||||
|
- `rp2040/uf2conv.py`: UF2 converter (alternative to elf2uf2‑rs)
|
||||||
|
- `install.sh`: unified helper to test, build, and flash (local or via SSH)
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
name = "cmdr-joystick-25"
|
name = "cmdr-joystick-25"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
# Firmware crate for the CMDR Joystick 25 (RP2040)
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# rp2040_hal dependencies copied from v0.11
|
# Core embedded + RP2040 HAL stack (aligned with rp2040-hal v0.11)
|
||||||
cortex-m = "0.7.2"
|
cortex-m = "0.7.2"
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
cortex-m-rtic = "1.1.4"
|
cortex-m-rtic = "1.1.4"
|
||||||
@ -31,20 +32,20 @@ rp2040-boot2 = "0.3.0"
|
|||||||
rp2040-hal = {version = "0.11.0", features = ["critical-section-impl", "rt", "defmt"]}
|
rp2040-hal = {version = "0.11.0", features = ["critical-section-impl", "rt", "defmt"]}
|
||||||
static_cell = "2.1.0"
|
static_cell = "2.1.0"
|
||||||
|
|
||||||
# USB hid dependencies
|
# USB HID stack
|
||||||
usbd-human-interface-device = {version = "0.5.1"}
|
usbd-human-interface-device = {version = "0.5.1"}
|
||||||
usb-device = "0.3"
|
usb-device = "0.3"
|
||||||
packed_struct = { version = "0.10", default-features = false }
|
packed_struct = { version = "0.10", default-features = false }
|
||||||
heapless = "0.8"
|
heapless = "0.8"
|
||||||
|
|
||||||
# EEPROM dependencies
|
# External EEPROM (24x) driver
|
||||||
eeprom24x = "0.7.2"
|
eeprom24x = "0.7.2"
|
||||||
|
|
||||||
# ws2812-pio dependencies
|
# WS2812 LED (PIO) + color helpers
|
||||||
ws2812-pio = "0.9.0"
|
ws2812-pio = "0.9.0"
|
||||||
smart-leds = "0.4.0"
|
smart-leds = "0.4.0"
|
||||||
|
|
||||||
# Analog filter dependencies
|
# Analog filtering and math
|
||||||
dyn-smooth = "0.2.0"
|
dyn-smooth = "0.2.0"
|
||||||
libm = "0.2.7"
|
libm = "0.2.7"
|
||||||
|
|
||||||
@ -70,5 +71,5 @@ bench = false
|
|||||||
test = false
|
test = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = [] # firmware runs in `no_std` by default
|
||||||
std = []
|
std = [] # enable when running unit tests on host
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
//! Axis management and processing for CMDR Joystick 25
|
//! Axis processing for CMDR Joystick 25
|
||||||
//!
|
//!
|
||||||
//! Handles gimbal axis processing, virtual axis management, throttle hold system,
|
//! Responsibilities
|
||||||
//! ADC reading, calibration, and gimbal mode compensation.
|
//! - Apply gimbal mode compensation (M10/M7) to raw ADC readings
|
||||||
|
//! - Feed smoothed samples into per‑axis filters
|
||||||
|
//! - Calibrate + apply deadzones + optional expo via lookup table
|
||||||
|
//! - Implement throttle‑hold remapping on the throttle axis
|
||||||
|
//! - Maintain virtual axes updated by button presses
|
||||||
|
//! - Detect activity to throttle USB traffic
|
||||||
|
//!
|
||||||
|
//! Data flow
|
||||||
|
//! raw ADC → gimbal compensation → smoothing → `GimbalAxis::process_value`
|
||||||
|
//! → throttle hold → activity detection → USB report conversion
|
||||||
|
|
||||||
use crate::buttons::{Button, TOTAL_BUTTONS};
|
use crate::buttons::{Button, TOTAL_BUTTONS};
|
||||||
use crate::expo::{constrain, ExpoLUT};
|
use crate::expo::{constrain, ExpoLUT};
|
||||||
@ -21,11 +30,8 @@ pub const GIMBAL_AXIS_RIGHT_Y: usize = 3;
|
|||||||
pub const GIMBAL_MODE_M10: u8 = 0;
|
pub const GIMBAL_MODE_M10: u8 = 0;
|
||||||
pub const GIMBAL_MODE_M7: u8 = 1;
|
pub const GIMBAL_MODE_M7: u8 = 1;
|
||||||
|
|
||||||
/// Digital signal processing configuration for analog smoothing filters.
|
/// Digital smoothing parameters for `DynamicSmootherEcoI32` filters.
|
||||||
///
|
/// Reduce ADC noise and jitter for stable axis values.
|
||||||
/// These parameters control the DynamicSmootherEcoI32 filters used to reduce noise
|
|
||||||
/// and jitter from the ADC readings. The smoothing helps provide stable axis values
|
|
||||||
/// and improves the overall control feel.
|
|
||||||
pub const BASE_FREQ: i32 = 2 << I32_FRAC_BITS;
|
pub const BASE_FREQ: i32 = 2 << I32_FRAC_BITS;
|
||||||
pub const SAMPLE_FREQ: i32 = 1000 << I32_FRAC_BITS;
|
pub const SAMPLE_FREQ: i32 = 1000 << I32_FRAC_BITS;
|
||||||
pub const SENSITIVITY: i32 = (0.01 * ((1 << I32_FRAC_BITS) as f32)) as i32;
|
pub const SENSITIVITY: i32 = (0.01 * ((1 << I32_FRAC_BITS) as f32)) as i32;
|
||||||
@ -64,26 +70,26 @@ impl Default for GimbalAxis {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GimbalAxis {
|
impl GimbalAxis {
|
||||||
/// Create a new GimbalAxis with default settings
|
/// Create a new GimbalAxis with default settings.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new GimbalAxis with calibration data
|
/// Create a new GimbalAxis with calibration data.
|
||||||
pub fn with_calibration(min: u16, max: u16, center: u16) -> Self {
|
pub fn with_calibration(min: u16, max: u16, center: u16) -> Self {
|
||||||
let mut axis = Self::new();
|
let mut axis = Self::new();
|
||||||
axis.set_calibration(min, max, center);
|
axis.set_calibration(min, max, center);
|
||||||
axis
|
axis
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply calibration data to the axis
|
/// Apply calibration data to the axis.
|
||||||
pub fn set_calibration(&mut self, min: u16, max: u16, center: u16) {
|
pub fn set_calibration(&mut self, min: u16, max: u16, center: u16) {
|
||||||
self.min = min;
|
self.min = min;
|
||||||
self.max = max;
|
self.max = max;
|
||||||
self.center = center;
|
self.center = center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process filtered ADC value through calibration and expo curve
|
/// Process filtered ADC value through calibration, deadzone and optional expo.
|
||||||
pub fn process_value(&mut self, filtered_value: i32, expo_lut: &ExpoLUT) {
|
pub fn process_value(&mut self, filtered_value: i32, expo_lut: &ExpoLUT) {
|
||||||
// Convert filtered value to u16 range
|
// Convert filtered value to u16 range
|
||||||
let raw_value = constrain(filtered_value, ADC_MIN as i32, ADC_MAX as i32) as u16;
|
let raw_value = constrain(filtered_value, ADC_MIN as i32, ADC_MAX as i32) as u16;
|
||||||
@ -101,20 +107,20 @@ impl GimbalAxis {
|
|||||||
self.value_before_hold = self.value;
|
self.value_before_hold = self.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set axis hold at current value
|
/// Set/arm a hold value for the axis (used by throttle hold).
|
||||||
pub fn set_hold(&mut self, value: u16) {
|
pub fn set_hold(&mut self, value: u16) {
|
||||||
self.hold = value;
|
self.hold = value;
|
||||||
self.hold_pending = true;
|
self.hold_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for axis activity (value changed since last check)
|
/// Check if the axis value changed since the last probe (activity flag).
|
||||||
pub fn check_activity(&mut self) -> bool {
|
pub fn check_activity(&mut self) -> bool {
|
||||||
let activity = self.value != self.previous_value;
|
let activity = self.value != self.previous_value;
|
||||||
self.previous_value = self.value;
|
self.previous_value = self.value;
|
||||||
activity
|
activity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process throttle hold with complex remapping logic (specialized for throttle axis)
|
/// Throttle‑hold remapping for throttle axis (center → hold; below/above remapped).
|
||||||
pub fn process_throttle_hold(&mut self, throttle_hold_enable: bool) {
|
pub fn process_throttle_hold(&mut self, throttle_hold_enable: bool) {
|
||||||
if throttle_hold_enable && self.value < AXIS_CENTER && !self.hold_pending {
|
if throttle_hold_enable && self.value < AXIS_CENTER && !self.hold_pending {
|
||||||
self.value = remap(self.value, ADC_MIN, AXIS_CENTER, ADC_MIN, self.hold);
|
self.value = remap(self.value, ADC_MIN, AXIS_CENTER, ADC_MIN, self.hold);
|
||||||
@ -152,8 +158,8 @@ impl VirtualAxis {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update virtual axis based on button inputs
|
/// Update virtual axis based on button inputs.
|
||||||
/// Returns true if USB activity should be signaled
|
/// Returns true if USB activity should be signaled.
|
||||||
pub fn update(&mut self, up_pressed: bool, down_pressed: bool, _vt_enable: bool) -> bool {
|
pub fn update(&mut self, up_pressed: bool, down_pressed: bool, _vt_enable: bool) -> bool {
|
||||||
let mut activity = false;
|
let mut activity = false;
|
||||||
|
|
||||||
@ -219,7 +225,7 @@ impl AxisManager {
|
|||||||
self.gimbal_mode = mode;
|
self.gimbal_mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply gimbal mode compensation to raw ADC values
|
/// Apply gimbal mode compensation to raw ADC values.
|
||||||
pub fn apply_gimbal_compensation(&self, raw_values: &mut [u16; 4]) {
|
pub fn apply_gimbal_compensation(&self, raw_values: &mut [u16; 4]) {
|
||||||
if self.gimbal_mode == GIMBAL_MODE_M10 {
|
if self.gimbal_mode == GIMBAL_MODE_M10 {
|
||||||
// Invert X1 and Y2 axis (M10 gimbals)
|
// Invert X1 and Y2 axis (M10 gimbals)
|
||||||
@ -232,7 +238,7 @@ impl AxisManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process filtering by integrating with smoother array
|
/// Tick smoothing filters with latest raw samples.
|
||||||
pub fn update_smoothers(
|
pub fn update_smoothers(
|
||||||
&self,
|
&self,
|
||||||
smoother: &mut [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
|
smoother: &mut [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -244,7 +250,7 @@ impl AxisManager {
|
|||||||
smoother[GIMBAL_AXIS_RIGHT_Y].tick(raw_values[GIMBAL_AXIS_RIGHT_Y] as i32);
|
smoother[GIMBAL_AXIS_RIGHT_Y].tick(raw_values[GIMBAL_AXIS_RIGHT_Y] as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process axis values with calibration, deadzone, and expo
|
/// Update all axes from smoothers (calibration, deadzone, expo) and apply holds.
|
||||||
pub fn process_axis_values(
|
pub fn process_axis_values(
|
||||||
&mut self,
|
&mut self,
|
||||||
smoother: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
|
smoother: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -257,20 +263,20 @@ impl AxisManager {
|
|||||||
self.check_activity()
|
self.check_activity()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process throttle hold value with complex remapping logic
|
/// Apply throttle‑hold to the left‑Y axis and derive enable flag.
|
||||||
pub fn process_throttle_hold(&mut self) {
|
pub fn process_throttle_hold(&mut self) {
|
||||||
self.throttle_hold_enable = self.axes[GIMBAL_AXIS_LEFT_Y].hold != AXIS_CENTER;
|
self.throttle_hold_enable = self.axes[GIMBAL_AXIS_LEFT_Y].hold != AXIS_CENTER;
|
||||||
self.axes[GIMBAL_AXIS_LEFT_Y].process_throttle_hold(self.throttle_hold_enable);
|
self.axes[GIMBAL_AXIS_LEFT_Y].process_throttle_hold(self.throttle_hold_enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear throttle hold for left Y axis (reset to initial state)
|
/// Clear throttle‑hold state for left‑Y axis (reset to initial state).
|
||||||
pub fn clear_throttle_hold(&mut self) {
|
pub fn clear_throttle_hold(&mut self) {
|
||||||
self.axes[GIMBAL_AXIS_LEFT_Y].hold = AXIS_CENTER;
|
self.axes[GIMBAL_AXIS_LEFT_Y].hold = AXIS_CENTER;
|
||||||
self.axes[GIMBAL_AXIS_LEFT_Y].hold_pending = false;
|
self.axes[GIMBAL_AXIS_LEFT_Y].hold_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update virtual axes based on button inputs
|
/// Update virtual axes based on button inputs.
|
||||||
/// Returns true if USB activity should be signaled
|
/// Returns true if USB activity should be signaled.
|
||||||
pub fn update_virtual_axes(
|
pub fn update_virtual_axes(
|
||||||
&mut self,
|
&mut self,
|
||||||
buttons: &[Button; TOTAL_BUTTONS],
|
buttons: &[Button; TOTAL_BUTTONS],
|
||||||
@ -301,7 +307,7 @@ impl AxisManager {
|
|||||||
activity
|
activity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for axis activity (movement from previous value)
|
/// Check for axis activity (movement from previous value).
|
||||||
pub fn check_activity(&mut self) -> bool {
|
pub fn check_activity(&mut self) -> bool {
|
||||||
let mut activity = false;
|
let mut activity = false;
|
||||||
|
|
||||||
@ -314,18 +320,18 @@ impl AxisManager {
|
|||||||
activity
|
activity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set throttle hold value for left Y axis (original behavior)
|
/// Set throttle‑hold value for left‑Y axis (original behavior).
|
||||||
pub fn set_throttle_hold(&mut self, hold_value: u16) {
|
pub fn set_throttle_hold(&mut self, hold_value: u16) {
|
||||||
self.axes[GIMBAL_AXIS_LEFT_Y].set_hold(hold_value);
|
self.axes[GIMBAL_AXIS_LEFT_Y].set_hold(hold_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get unprocessed value for special button combinations (original logic)
|
/// Get pre‑hold value for special button combinations (original logic).
|
||||||
pub fn get_value_before_hold(&self) -> u16 {
|
pub fn get_value_before_hold(&self) -> u16 {
|
||||||
// Original code captured axis.value BEFORE throttle hold was applied
|
// Original code captured axis.value BEFORE throttle hold was applied
|
||||||
self.axes[GIMBAL_AXIS_LEFT_Y].value_before_hold
|
self.axes[GIMBAL_AXIS_LEFT_Y].value_before_hold
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get virtual RY axis value for joystick report
|
/// Get virtual RY axis value for joystick report.
|
||||||
pub fn get_virtual_ry_value(&self, expo_lut: &ExpoLUT) -> u16 {
|
pub fn get_virtual_ry_value(&self, expo_lut: &ExpoLUT) -> u16 {
|
||||||
calculate_axis_value(
|
calculate_axis_value(
|
||||||
self.virtual_ry.value,
|
self.virtual_ry.value,
|
||||||
@ -338,7 +344,7 @@ impl AxisManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get virtual RZ axis value for joystick report
|
/// Get virtual RZ axis value for joystick report.
|
||||||
pub fn get_virtual_rz_value(&self, expo_lut: &ExpoLUT) -> u16 {
|
pub fn get_virtual_rz_value(&self, expo_lut: &ExpoLUT) -> u16 {
|
||||||
calculate_axis_value(
|
calculate_axis_value(
|
||||||
self.virtual_rz.value,
|
self.virtual_rz.value,
|
||||||
@ -351,10 +357,8 @@ impl AxisManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize digital smoothing filters for each gimbal axis
|
/// Initialize digital smoothing filters for each gimbal axis.
|
||||||
///
|
/// Creates an array of `DynamicSmootherEcoI32` filters configured with shared DSP parameters.
|
||||||
/// Creates an array of DynamicSmootherEcoI32 filters configured with appropriate
|
|
||||||
/// DSP parameters for noise reduction and jitter elimination from ADC readings.
|
|
||||||
pub fn create_smoothers() -> [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS] {
|
pub fn create_smoothers() -> [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS] {
|
||||||
[
|
[
|
||||||
DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
|
DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY),
|
||||||
@ -367,7 +371,7 @@ impl AxisManager {
|
|||||||
|
|
||||||
// ==================== AXIS PROCESSING FUNCTIONS ====================
|
// ==================== AXIS PROCESSING FUNCTIONS ====================
|
||||||
|
|
||||||
/// Remapping values from one range to another
|
/// Remap a value from one range to another using integer math and clamping.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `value` - Value to remap
|
/// * `value` - Value to remap
|
||||||
@ -385,10 +389,10 @@ pub fn remap(value: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -
|
|||||||
) as u16
|
) as u16
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate calibrated axis value with deadzone and expo curve application
|
/// Calculate calibrated axis value with deadzone and optional expo.
|
||||||
///
|
///
|
||||||
/// Processes raw axis input through calibration, deadzone filtering, and optional
|
/// Process raw input through [min, center, max] calibration, apply deadzones,
|
||||||
/// exponential curve application for smooth joystick response.
|
/// then apply expo via LUT if enabled for smooth response.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `value` - Raw axis value to process
|
/// * `value` - Raw axis value to process
|
||||||
@ -400,7 +404,7 @@ pub fn remap(value: u16, in_min: u16, in_max: u16, out_min: u16, out_max: u16) -
|
|||||||
/// * `expo_lut` - Exponential curve lookup table for smooth response
|
/// * `expo_lut` - Exponential curve lookup table for smooth response
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// Processed axis value with calibration, deadzone, and expo applied
|
/// Processed axis value after calibration, deadzone, and optional expo
|
||||||
pub fn calculate_axis_value(
|
pub fn calculate_axis_value(
|
||||||
value: u16,
|
value: u16,
|
||||||
min: u16,
|
min: u16,
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
//! Project: CMtec CMDR joystick 24
|
//! Button matrix scanner for CMDR Joystick 25
|
||||||
//! Date: 2025-03-09
|
//!
|
||||||
//! Author: Christoffer Martinsson
|
//! Scans a row/column matrix and produces a debounced boolean state for each
|
||||||
//! Email: cm@cmtec.se
|
//! button. Designed for small matrices on microcontrollers where timing is
|
||||||
//! License: Please refer to LICENSE in root directory
|
//! deterministic and GPIO is plentiful.
|
||||||
|
//!
|
||||||
|
//! - Rows are configured as inputs with pull‑ups
|
||||||
|
//! - Columns are configured as push‑pull outputs
|
||||||
|
//! - Debounce is handled per‑button using a simple counter
|
||||||
|
//! - A tiny inter‑column delay is inserted to allow signals to settle
|
||||||
|
|
||||||
use core::convert::Infallible;
|
use core::convert::Infallible;
|
||||||
use cortex_m::delay::Delay;
|
use cortex_m::delay::Delay;
|
||||||
@ -10,9 +15,21 @@ use embedded_hal::digital::{InputPin, OutputPin};
|
|||||||
|
|
||||||
/// Button matrix driver
|
/// Button matrix driver
|
||||||
///
|
///
|
||||||
/// # Example
|
/// Generics
|
||||||
|
/// - `R`: number of rows
|
||||||
|
/// - `C`: number of columns
|
||||||
|
/// - `N`: total number of buttons (usually `R * C`)
|
||||||
|
///
|
||||||
|
/// Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let button_matrix: ButtonMatrix<4, 6, 48> = ButtonMatrix::new(row_pins, col_pins, 5);
|
/// // 4 rows, 6 columns, 24 buttons, 5-scan debounce
|
||||||
|
/// let mut matrix: ButtonMatrix<4, 6, 24> = ButtonMatrix::new(row_pins, col_pins, 5);
|
||||||
|
/// matrix.init_pins();
|
||||||
|
/// loop {
|
||||||
|
/// matrix.scan_matrix(&mut delay);
|
||||||
|
/// let states = matrix.buttons_pressed();
|
||||||
|
/// // use `states`
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
|
pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
|
||||||
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
||||||
@ -25,11 +42,10 @@ pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
|
|||||||
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
|
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
|
||||||
/// Creates a new button matrix.
|
/// Creates a new button matrix.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// Arguments
|
||||||
///
|
/// - `rows`: array of row pins (inputs with pull‑ups)
|
||||||
/// * `rows` - An array of references to the row pins.
|
/// - `cols`: array of column pins (push‑pull outputs)
|
||||||
/// * `cols` - An array of references to the column pins.
|
/// - `debounce`: number of consecutive scans a change must persist before it is accepted
|
||||||
/// * `debounce` - The debounce time in number of scans.
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
||||||
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
|
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
|
||||||
@ -44,21 +60,23 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the button matrix.
|
/// Initialize the matrix GPIOs (set all columns high).
|
||||||
/// This should be called once before scanning the matrix.
|
///
|
||||||
|
/// Call once before the first scan.
|
||||||
pub fn init_pins(&mut self) {
|
pub fn init_pins(&mut self) {
|
||||||
for col in self.cols.iter_mut() {
|
for col in self.cols.iter_mut() {
|
||||||
col.set_high().unwrap();
|
col.set_high().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans the button matrix and updates the pressed state of each button.
|
/// Scan the matrix and update each button's debounced state.
|
||||||
/// This should be called at regular intervals.
|
|
||||||
/// Allow at least 5 times the delay compared to the needed button latency.
|
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// Call at a fixed cadence. The simple debounce uses a per‑button counter: only
|
||||||
|
/// when a changed level is observed for `debounce` consecutive scans is the
|
||||||
|
/// new state committed.
|
||||||
///
|
///
|
||||||
/// * `delay` - A mutable reference to a delay object.
|
/// Arguments
|
||||||
|
/// - `delay`: short delay implementation used to let signals settle between columns
|
||||||
pub fn scan_matrix(&mut self, delay: &mut Delay) {
|
pub fn scan_matrix(&mut self, delay: &mut Delay) {
|
||||||
for col_index in 0..self.cols.len() {
|
for col_index in 0..self.cols.len() {
|
||||||
self.cols[col_index].set_low().unwrap();
|
self.cols[col_index].set_low().unwrap();
|
||||||
@ -69,11 +87,10 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a column of the button matrix.
|
/// Process a single column: drive low, sample rows, update debounce state, then release high.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// Arguments
|
||||||
///
|
/// - `col_index`: index of the column being scanned
|
||||||
/// * `col_index` - The index of the column to process.
|
|
||||||
fn process_column(&mut self, col_index: usize) {
|
fn process_column(&mut self, col_index: usize) {
|
||||||
for row_index in 0..self.rows.len() {
|
for row_index in 0..self.rows.len() {
|
||||||
let button_index: usize = col_index + (row_index * C);
|
let button_index: usize = col_index + (row_index * C);
|
||||||
@ -91,7 +108,10 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an array of booleans indicating whether each button is pressed.
|
/// Return a copy of the debounced pressed state for all buttons.
|
||||||
|
///
|
||||||
|
/// For small `N` this copy is cheap. If needed, the API could be extended to
|
||||||
|
/// return a reference in the future.
|
||||||
pub fn buttons_pressed(&mut self) -> [bool; N] {
|
pub fn buttons_pressed(&mut self) -> [bool; N] {
|
||||||
self.pressed
|
self.pressed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
//! Button management and processing for CMDR Joystick 25
|
//! Button processing for CMDR Joystick 25
|
||||||
//!
|
//!
|
||||||
//! Handles button state tracking, press type detection, HAT switch filtering,
|
//! Responsibilities
|
||||||
//! and special button combination processing.
|
//! - Integrate button matrix results and extra pins
|
||||||
|
//! - Track pressed/previous states and USB change flags
|
||||||
|
//! - Detect short/long presses with minimal hold timing
|
||||||
|
//! - Filter HAT switches so only one direction remains active
|
||||||
|
//! - Evaluate special combinations (bootloader, calibration, etc.)
|
||||||
|
//! - Expose a compact state consumed by USB report generation
|
||||||
|
|
||||||
use crate::mapping::*;
|
use crate::mapping::*;
|
||||||
use crate::button_matrix::ButtonMatrix;
|
use crate::button_matrix::ButtonMatrix;
|
||||||
@ -9,7 +14,7 @@ use crate::hardware::{NUMBER_OF_BUTTONS, AXIS_CENTER, BUTTON_ROWS, BUTTON_COLS};
|
|||||||
use embedded_hal::digital::InputPin;
|
use embedded_hal::digital::InputPin;
|
||||||
use rp2040_hal::timer::Timer;
|
use rp2040_hal::timer::Timer;
|
||||||
|
|
||||||
// Total buttons including extra buttons
|
// Total buttons including the two extra (non‑matrix) buttons
|
||||||
pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2;
|
pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2;
|
||||||
|
|
||||||
// ==================== BUTTON STRUCT ====================
|
// ==================== BUTTON STRUCT ====================
|
||||||
@ -33,6 +38,7 @@ pub struct Button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== SPECIAL ACTIONS ====================
|
// ==================== SPECIAL ACTIONS ====================
|
||||||
|
/// High‑level actions triggered by dedicated button combinations.
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum SpecialAction {
|
pub enum SpecialAction {
|
||||||
@ -48,6 +54,7 @@ pub enum SpecialAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== BUTTON MANAGER ====================
|
// ==================== BUTTON MANAGER ====================
|
||||||
|
/// Aggregates and processes all buttons, exposing a stable API to the rest of the firmware.
|
||||||
|
|
||||||
pub struct ButtonManager {
|
pub struct ButtonManager {
|
||||||
pub buttons: [Button; TOTAL_BUTTONS],
|
pub buttons: [Button; TOTAL_BUTTONS],
|
||||||
@ -69,14 +76,14 @@ impl ButtonManager {
|
|||||||
Self { buttons }
|
Self { buttons }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update button states from button matrix scan
|
/// Update button states from the button matrix snapshot.
|
||||||
pub fn update_from_matrix(&mut self, matrix: &mut ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS>) {
|
pub fn update_from_matrix(&mut self, matrix: &mut ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS>) {
|
||||||
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
|
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
|
||||||
self.buttons[index].pressed = *key;
|
self.buttons[index].pressed = *key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update extra button states from hardware pins
|
/// Update extra (non‑matrix) button states from hardware pins.
|
||||||
pub fn update_extra_buttons<L, R>(&mut self, left_pin: &mut L, right_pin: &mut R)
|
pub fn update_extra_buttons<L, R>(&mut self, left_pin: &mut L, right_pin: &mut R)
|
||||||
where
|
where
|
||||||
L: InputPin,
|
L: InputPin,
|
||||||
@ -88,7 +95,7 @@ impl ButtonManager {
|
|||||||
self.buttons[BUTTON_FRONT_RIGHT_EXTRA].pressed = right_pin.is_low().unwrap_or(false);
|
self.buttons[BUTTON_FRONT_RIGHT_EXTRA].pressed = right_pin.is_low().unwrap_or(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter HAT switch buttons - prevents multiple directional buttons from being pressed simultaneously
|
/// Filter HAT switches so only a single direction (or center) can be active.
|
||||||
pub fn filter_hat_switches(&mut self) {
|
pub fn filter_hat_switches(&mut self) {
|
||||||
// Filter left hat switch buttons
|
// Filter left hat switch buttons
|
||||||
for i in BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT {
|
for i in BUTTON_TOP_LEFT_HAT_UP..=BUTTON_TOP_LEFT_HAT_LEFT {
|
||||||
@ -127,7 +134,7 @@ impl ButtonManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process button press types (short/long press detection) and update USB states
|
/// Update press types (short/long) and USB change flags; returns whether USB should be updated.
|
||||||
pub fn process_button_logic(&mut self, current_time: u32) -> bool {
|
pub fn process_button_logic(&mut self, current_time: u32) -> bool {
|
||||||
let mut usb_activity = false;
|
let mut usb_activity = false;
|
||||||
|
|
||||||
@ -148,16 +155,13 @@ impl ButtonManager {
|
|||||||
usb_activity
|
usb_activity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process button timing logic with integrated timer access
|
/// Convenience wrapper that derives `current_time` from the hardware timer.
|
||||||
///
|
|
||||||
/// This method handles timer access internally, providing better encapsulation
|
|
||||||
/// for button timing operations and removing timer dependency from main.rs.
|
|
||||||
pub fn process_button_logic_with_timer(&mut self, timer: &Timer) -> bool {
|
pub fn process_button_logic_with_timer(&mut self, timer: &Timer) -> bool {
|
||||||
let current_time = (timer.get_counter().ticks() / 1000) as u32;
|
let current_time = (timer.get_counter().ticks() / 1000) as u32;
|
||||||
self.process_button_logic(current_time)
|
self.process_button_logic(current_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for special button combinations (bootloader, calibration, etc.)
|
/// Check for special button combinations (bootloader, calibration state/mode, VT, etc.).
|
||||||
pub fn check_special_combinations(&self, unprocessed_axis_value: u16, calibration_active: bool) -> SpecialAction {
|
pub fn check_special_combinations(&self, unprocessed_axis_value: u16, calibration_active: bool) -> SpecialAction {
|
||||||
// Secondary way to enter bootloader
|
// Secondary way to enter bootloader
|
||||||
if self.buttons[BUTTON_FRONT_LEFT_LOWER].pressed
|
if self.buttons[BUTTON_FRONT_LEFT_LOWER].pressed
|
||||||
@ -221,7 +225,7 @@ impl ButtonManager {
|
|||||||
SpecialAction::None
|
SpecialAction::None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get button press event (true on press, false on release, None if no change)
|
/// Get a change event for a button: Some(true)=press, Some(false)=release, None=no change.
|
||||||
fn get_button_press_event(&self, button_index: usize) -> Option<bool> {
|
fn get_button_press_event(&self, button_index: usize) -> Option<bool> {
|
||||||
let button = &self.buttons[button_index];
|
let button = &self.buttons[button_index];
|
||||||
if button.pressed != button.previous_pressed {
|
if button.pressed != button.previous_pressed {
|
||||||
@ -231,12 +235,12 @@ impl ButtonManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mutable reference to buttons array for external access
|
/// Get mutable access to the internal buttons array.
|
||||||
pub fn buttons_mut(&mut self) -> &mut [Button; TOTAL_BUTTONS] {
|
pub fn buttons_mut(&mut self) -> &mut [Button; TOTAL_BUTTONS] {
|
||||||
&mut self.buttons
|
&mut self.buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get immutable reference to buttons array for external access
|
/// Get read‑only access to the internal buttons array.
|
||||||
pub fn buttons(&self) -> &[Button; TOTAL_BUTTONS] {
|
pub fn buttons(&self) -> &[Button; TOTAL_BUTTONS] {
|
||||||
&self.buttons
|
&self.buttons
|
||||||
}
|
}
|
||||||
@ -244,7 +248,13 @@ impl ButtonManager {
|
|||||||
|
|
||||||
// ==================== BUTTON PRESS TYPE DETECTION ====================
|
// ==================== BUTTON PRESS TYPE DETECTION ====================
|
||||||
|
|
||||||
/// Update button press type (short/long press detection)
|
/// Update button press type and manage USB press lifecycle.
|
||||||
|
///
|
||||||
|
/// Behavior
|
||||||
|
/// - On physical press: record start time and defer decision
|
||||||
|
/// - Long press: if enabled and held >= threshold, activate `usb_button_long`
|
||||||
|
/// - Short press: on release (and if long not activated), activate `usb_button`
|
||||||
|
/// - USB press lifetime: auto‑release after a minimal hold so the host sees a pulse
|
||||||
fn update_button_press_type(button: &mut Button, current_time: u32) {
|
fn update_button_press_type(button: &mut Button, current_time: u32) {
|
||||||
const LONG_PRESS_THRESHOLD: u32 = 200;
|
const LONG_PRESS_THRESHOLD: u32 = 200;
|
||||||
|
|
||||||
@ -285,7 +295,7 @@ fn update_button_press_type(button: &mut Button, current_time: u32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-release for short press after minimum hold time
|
// Auto‑release generated USB press after minimum hold time
|
||||||
const USB_MIN_HOLD_MS: u32 = 50;
|
const USB_MIN_HOLD_MS: u32 = 50;
|
||||||
if button.usb_press_active
|
if button.usb_press_active
|
||||||
&& (!button.pressed
|
&& (!button.pressed
|
||||||
@ -488,4 +498,4 @@ mod tests {
|
|||||||
// which isn't available in the test environment, but the method compilation
|
// which isn't available in the test environment, but the method compilation
|
||||||
// is verified through the cargo check above.
|
// is verified through the cargo check above.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
//! Calibration management for CMDR Joystick 25
|
//! Calibration management for CMDR Joystick 25
|
||||||
//!
|
//!
|
||||||
//! Handles axis calibration, gimbal mode selection, and calibration data persistence.
|
//! Responsibilities
|
||||||
//! Provides focused methods for different calibration operations with clean separation of concerns.
|
//! - Manage the calibration lifecycle (start/active/stop)
|
||||||
|
//! - Track min/max dynamically from smoothed ADC samples
|
||||||
|
//! - Select gimbal mode (M10/M7) during calibration and reset centers
|
||||||
|
//! - Persist per‑axis calibration and gimbal mode to EEPROM
|
||||||
|
//! - Load persisted settings at boot with resilient defaults
|
||||||
|
//!
|
||||||
|
//! The manager is intentionally stateful but small. It isolates storage access
|
||||||
|
//! behind simple `read_fn`/`write_fn` closures so the code can be unit‑tested on
|
||||||
|
//! a host without hardware.
|
||||||
|
|
||||||
use crate::axis::{GIMBAL_MODE_M7, GIMBAL_MODE_M10, GimbalAxis};
|
use crate::axis::{GIMBAL_MODE_M7, GIMBAL_MODE_M10, GimbalAxis};
|
||||||
use crate::hardware::NBR_OF_GIMBAL_AXIS;
|
use crate::hardware::NBR_OF_GIMBAL_AXIS;
|
||||||
@ -10,13 +18,14 @@ use dyn_smooth::DynamicSmootherEcoI32;
|
|||||||
|
|
||||||
// ==================== CALIBRATION MANAGER ====================
|
// ==================== CALIBRATION MANAGER ====================
|
||||||
|
|
||||||
|
/// Orchestrates runtime calibration and storage interactions.
|
||||||
pub struct CalibrationManager {
|
pub struct CalibrationManager {
|
||||||
active: bool,
|
active: bool,
|
||||||
gimbal_mode: u8,
|
gimbal_mode: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CalibrationManager {
|
impl CalibrationManager {
|
||||||
/// Create a new CalibrationManager with default settings
|
/// Create a new CalibrationManager with default settings.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
active: false,
|
active: false,
|
||||||
@ -24,33 +33,34 @@ impl CalibrationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if calibration mode is currently active
|
/// Check if calibration mode is currently active.
|
||||||
pub fn is_active(&self) -> bool {
|
pub fn is_active(&self) -> bool {
|
||||||
self.active
|
self.active
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start calibration mode
|
/// Start calibration mode.
|
||||||
pub fn start_calibration(&mut self) {
|
pub fn start_calibration(&mut self) {
|
||||||
self.active = true;
|
self.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop calibration mode
|
/// Stop calibration mode.
|
||||||
pub fn stop_calibration(&mut self) {
|
pub fn stop_calibration(&mut self) {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current gimbal mode
|
/// Get current gimbal mode.
|
||||||
pub fn get_gimbal_mode(&self) -> u8 {
|
pub fn get_gimbal_mode(&self) -> u8 {
|
||||||
self.gimbal_mode
|
self.gimbal_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set gimbal mode
|
/// Set gimbal mode.
|
||||||
pub fn set_gimbal_mode(&mut self, mode: u8) {
|
pub fn set_gimbal_mode(&mut self, mode: u8) {
|
||||||
self.gimbal_mode = mode;
|
self.gimbal_mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update dynamic calibration - continuous min/max tracking during calibration
|
/// Update dynamic calibration (continuous min/max tracking) while active.
|
||||||
/// Only active during calibration mode
|
///
|
||||||
|
/// Only has effect when calibration is active.
|
||||||
pub fn update_dynamic_calibration(
|
pub fn update_dynamic_calibration(
|
||||||
&self,
|
&self,
|
||||||
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -70,8 +80,8 @@ impl CalibrationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set gimbal mode to M10 and reset axis calibration to current center
|
/// Set gimbal mode to M10 and reset each axis (min/max/center) to the current smoothed center.
|
||||||
/// Returns true if mode was changed (for use during calibration)
|
/// Returns true if mode was changed (only while active).
|
||||||
pub fn set_gimbal_mode_m10(
|
pub fn set_gimbal_mode_m10(
|
||||||
&mut self,
|
&mut self,
|
||||||
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -86,8 +96,8 @@ impl CalibrationManager {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set gimbal mode to M7 and reset axis calibration to current center
|
/// Set gimbal mode to M7 and reset each axis (min/max/center) to the current smoothed center.
|
||||||
/// Returns true if mode was changed (for use during calibration)
|
/// Returns true if mode was changed (only while active).
|
||||||
pub fn set_gimbal_mode_m7(
|
pub fn set_gimbal_mode_m7(
|
||||||
&mut self,
|
&mut self,
|
||||||
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -102,8 +112,8 @@ impl CalibrationManager {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save calibration data to storage and end calibration mode
|
/// Save calibration data to EEPROM and end calibration mode.
|
||||||
/// Returns true if calibration was saved and ended
|
/// Returns true if data was written and calibration ended.
|
||||||
pub fn save_calibration<F>(
|
pub fn save_calibration<F>(
|
||||||
&mut self,
|
&mut self,
|
||||||
axes: &[GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &[GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -133,10 +143,9 @@ impl CalibrationManager {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load axis calibration data from EEPROM storage
|
/// Load per‑axis calibration data from EEPROM.
|
||||||
///
|
///
|
||||||
/// Updates the provided axes array with calibration values loaded from storage.
|
/// Updates the provided axes with loaded values; retains defaults on error.
|
||||||
/// Uses factory defaults if EEPROM read fails.
|
|
||||||
pub fn load_axis_calibration<F>(
|
pub fn load_axis_calibration<F>(
|
||||||
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
read_fn: &mut F,
|
read_fn: &mut F,
|
||||||
@ -156,9 +165,8 @@ impl CalibrationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load gimbal mode from EEPROM storage
|
/// Load gimbal mode from EEPROM.
|
||||||
///
|
/// Returns the stored mode or M10 default if read fails.
|
||||||
/// Returns the stored gimbal mode or M10 default if read fails.
|
|
||||||
pub fn load_gimbal_mode<F>(read_fn: &mut F) -> u8
|
pub fn load_gimbal_mode<F>(read_fn: &mut F) -> u8
|
||||||
where
|
where
|
||||||
F: FnMut(u32) -> Result<u8, ()>,
|
F: FnMut(u32) -> Result<u8, ()>,
|
||||||
@ -167,7 +175,7 @@ impl CalibrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Reset axis calibration values to current center position
|
/// Reset each axis calibration to its current smoothed center.
|
||||||
fn reset_axis_calibration(
|
fn reset_axis_calibration(
|
||||||
&self,
|
&self,
|
||||||
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
|
||||||
@ -529,4 +537,3 @@ mod tests {
|
|||||||
assert!(!manager.is_active()); // Should remain inactive
|
assert!(!manager.is_active()); // Should remain inactive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,44 @@
|
|||||||
//! Exponential curve processing and lookup tables for joystick axes
|
//! Exponential response curves for joystick axes
|
||||||
|
//!
|
||||||
|
//! This module provides a small, allocation‑free way to apply an exponential
|
||||||
|
//! response to 12‑bit axis values. A lookup table (LUT) is generated once and
|
||||||
|
//! then used at runtime to map linear input into an expo‑shaped output without
|
||||||
|
//! floating‑point math.
|
||||||
|
//!
|
||||||
|
//! Notes
|
||||||
|
//! - LUT length is `ADC_MAX + 1` (4096 entries for 12‑bit ADC)
|
||||||
|
//! - `ExpoLUT::new(factor)` builds the table up front; higher factors increase
|
||||||
|
//! curve intensity near the ends and soften around center
|
||||||
|
//! - `apply_expo_curve` performs a single array lookup with a boundary guard
|
||||||
|
|
||||||
use crate::{ADC_MAX, ADC_MIN};
|
use crate::{ADC_MAX, ADC_MIN};
|
||||||
use core::cmp::PartialOrd;
|
use core::cmp::PartialOrd;
|
||||||
use libm::powf;
|
use libm::powf;
|
||||||
|
|
||||||
|
/// Precomputed exponential lookup table (12‑bit domain).
|
||||||
pub struct ExpoLUT {
|
pub struct ExpoLUT {
|
||||||
lut: [u16; 4096],
|
lut: [u16; 4096],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExpoLUT {
|
impl ExpoLUT {
|
||||||
|
/// Build a new lookup table for the provided expo factor.
|
||||||
|
///
|
||||||
|
/// Recommended range for `factor` is 0.0 (linear) to 1.0 (strong expo).
|
||||||
pub fn new(factor: f32) -> Self {
|
pub fn new(factor: f32) -> Self {
|
||||||
let lut = generate_expo_lut(factor);
|
let lut = generate_expo_lut(factor);
|
||||||
Self { lut }
|
Self { lut }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply the precomputed expo mapping to a single axis value.
|
||||||
pub fn apply(&self, value: u16) -> u16 {
|
pub fn apply(&self, value: u16) -> u16 {
|
||||||
apply_expo_curve(value, &self.lut)
|
apply_expo_curve(value, &self.lut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a 12‑bit LUT for an exponential response curve.
|
||||||
|
///
|
||||||
|
/// - `expo` in [0.0, 1.0] recommended; values outside that range may exaggerate results.
|
||||||
|
/// - Output is clamped to `ADC_MIN..=ADC_MAX`.
|
||||||
pub fn generate_expo_lut(expo: f32) -> [u16; ADC_MAX as usize + 1] {
|
pub fn generate_expo_lut(expo: f32) -> [u16; ADC_MAX as usize + 1] {
|
||||||
let mut lut: [u16; ADC_MAX as usize + 1] = [0; ADC_MAX as usize + 1];
|
let mut lut: [u16; ADC_MAX as usize + 1] = [0; ADC_MAX as usize + 1];
|
||||||
for i in 0..ADC_MAX + 1 {
|
for i in 0..ADC_MAX + 1 {
|
||||||
@ -31,6 +51,9 @@ pub fn generate_expo_lut(expo: f32) -> [u16; ADC_MAX as usize + 1] {
|
|||||||
lut
|
lut
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply an expo LUT to an input value.
|
||||||
|
///
|
||||||
|
/// If the value exceeds the table length, returns `ADC_MAX` as a guard.
|
||||||
pub fn apply_expo_curve(value: u16, expo_lut: &[u16]) -> u16 {
|
pub fn apply_expo_curve(value: u16, expo_lut: &[u16]) -> u16 {
|
||||||
if value >= expo_lut.len() as u16 {
|
if value >= expo_lut.len() as u16 {
|
||||||
return ADC_MAX;
|
return ADC_MAX;
|
||||||
@ -38,6 +61,7 @@ pub fn apply_expo_curve(value: u16, expo_lut: &[u16]) -> u16 {
|
|||||||
expo_lut[value as usize]
|
expo_lut[value as usize]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clamp `value` into the inclusive range `[out_min, out_max]`.
|
||||||
pub fn constrain<T: PartialOrd>(value: T, out_min: T, out_max: T) -> T {
|
pub fn constrain<T: PartialOrd>(value: T, out_min: T, out_max: T) -> T {
|
||||||
if value < out_min {
|
if value < out_min {
|
||||||
out_min
|
out_min
|
||||||
|
|||||||
@ -1,30 +1,38 @@
|
|||||||
//! Project: CMtec CMDR joystick 25
|
//! Hardware configuration for CMDR Joystick 25 (RP2040)
|
||||||
//! Date: 2023-08-01
|
//!
|
||||||
//! Author: Christoffer Martinsson
|
//! Centralizes board constants, GPIO mappings, timing cadences and helper
|
||||||
//! Email: cm@cmtec.se
|
//! macros to keep hardware details out of business logic.
|
||||||
//! License: Please refer to LICENSE in root directory
|
|
||||||
|
|
||||||
//! Hardware configuration constants and pin definitions for CMDR Joystick 25
|
|
||||||
|
|
||||||
// ==================== CRYSTAL AND USB CONSTANTS ====================
|
// ==================== CRYSTAL AND USB CONSTANTS ====================
|
||||||
|
/// External crystal frequency (Hz).
|
||||||
pub const XTAL_FREQ_HZ: u32 = 12_000_000u32;
|
pub const XTAL_FREQ_HZ: u32 = 12_000_000u32;
|
||||||
|
/// USB Vendor ID.
|
||||||
pub const USB_VID: u16 = 0x1209;
|
pub const USB_VID: u16 = 0x1209;
|
||||||
|
/// USB Product ID.
|
||||||
pub const USB_PID: u16 = 0x0002;
|
pub const USB_PID: u16 = 0x0002;
|
||||||
|
|
||||||
// ==================== JOYSTICK CONSTANTS ====================
|
// ==================== JOYSTICK CONSTANTS ====================
|
||||||
|
/// Button matrix geometry (rows).
|
||||||
pub const BUTTON_ROWS: usize = 5;
|
pub const BUTTON_ROWS: usize = 5;
|
||||||
|
/// Button matrix geometry (columns).
|
||||||
pub const BUTTON_COLS: usize = 5;
|
pub const BUTTON_COLS: usize = 5;
|
||||||
|
/// Total number of matrix buttons.
|
||||||
pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS;
|
pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS;
|
||||||
|
/// ADC raw minimum (12‑bit).
|
||||||
pub const ADC_MIN: u16 = 0;
|
pub const ADC_MIN: u16 = 0;
|
||||||
|
/// ADC raw maximum (12‑bit).
|
||||||
pub const ADC_MAX: u16 = 4095;
|
pub const ADC_MAX: u16 = 4095;
|
||||||
|
/// Logical axis center.
|
||||||
pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
|
pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
|
||||||
|
/// Number of physical gimbal axes.
|
||||||
pub const NBR_OF_GIMBAL_AXIS: usize = 4;
|
pub const NBR_OF_GIMBAL_AXIS: usize = 4;
|
||||||
|
/// Debounce threshold (in scans) for the matrix.
|
||||||
pub const DEBOUNCE: u8 = 10;
|
pub const DEBOUNCE: u8 = 10;
|
||||||
|
/// Bytes reserved in EEPROM for calibration data + gimbal mode.
|
||||||
pub const EEPROM_DATA_LENGTH: usize = 25;
|
pub const EEPROM_DATA_LENGTH: usize = 25;
|
||||||
|
|
||||||
// ==================== GPIO PIN DEFINITIONS ====================
|
// ==================== GPIO PIN DEFINITIONS ====================
|
||||||
|
/// Logical mapping between board functions and GPIO numbers.
|
||||||
pub mod pins {
|
pub mod pins {
|
||||||
/// Extra buttons (TX/RX pins)
|
/// Extra buttons (TX/RX pins)
|
||||||
pub const LEFT_EXTRA_BUTTON_PIN: u8 = 1;
|
pub const LEFT_EXTRA_BUTTON_PIN: u8 = 1;
|
||||||
@ -59,7 +67,7 @@ pub mod pins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== I2C CONFIGURATION ====================
|
// ==================== I2C CONFIGURATION ====================
|
||||||
|
/// I2C frequency and system clock helpers for the EEPROM bus.
|
||||||
pub mod i2c {
|
pub mod i2c {
|
||||||
use fugit::{Rate, RateExtU32};
|
use fugit::{Rate, RateExtU32};
|
||||||
|
|
||||||
@ -74,23 +82,23 @@ pub mod i2c {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== TIMER INTERVALS ====================
|
// ==================== TIMER INTERVALS ====================
|
||||||
|
/// Cadences for periodic firmware tasks.
|
||||||
pub mod timers {
|
pub mod timers {
|
||||||
/// Status LED update interval (250ms)
|
/// Status LED update interval (ms).
|
||||||
pub const STATUS_LED_INTERVAL_MS: u32 = 250;
|
pub const STATUS_LED_INTERVAL_MS: u32 = 250;
|
||||||
|
|
||||||
/// Scan timer interval (200us)
|
/// Button matrix scan interval (µs).
|
||||||
pub const SCAN_INTERVAL_US: u32 = 200;
|
pub const SCAN_INTERVAL_US: u32 = 200;
|
||||||
|
|
||||||
/// Data processing interval (1200us)
|
/// Data processing interval (µs) for axis/button logic.
|
||||||
pub const DATA_PROCESS_INTERVAL_US: u32 = 1200;
|
pub const DATA_PROCESS_INTERVAL_US: u32 = 1200;
|
||||||
|
|
||||||
/// USB update interval (10ms)
|
/// USB HID report interval (ms).
|
||||||
pub const USB_UPDATE_INTERVAL_MS: u32 = 10;
|
pub const USB_UPDATE_INTERVAL_MS: u32 = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== USB DEVICE CONFIGURATION ====================
|
// ==================== USB DEVICE CONFIGURATION ====================
|
||||||
|
/// USB string descriptors.
|
||||||
pub mod usb {
|
pub mod usb {
|
||||||
pub const MANUFACTURER: &str = "CMtec";
|
pub const MANUFACTURER: &str = "CMtec";
|
||||||
pub const PRODUCT: &str = "CMDR Joystick 25";
|
pub const PRODUCT: &str = "CMDR Joystick 25";
|
||||||
@ -99,8 +107,8 @@ pub mod usb {
|
|||||||
|
|
||||||
// ==================== PIN ACCESS MACROS ====================
|
// ==================== PIN ACCESS MACROS ====================
|
||||||
|
|
||||||
/// Macro to access GPIO pins using hardware constants
|
/// Macro to access typed GPIO pins using board constants.
|
||||||
/// This eliminates hardcoded pin numbers in main.rs and ensures constants are used
|
/// Avoids scattering raw GPIO numbers; each arm references the constant it maps.
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! get_pin {
|
macro_rules! get_pin {
|
||||||
($pins:expr, left_extra_button) => {{
|
($pins:expr, left_extra_button) => {{
|
||||||
|
|||||||
@ -1,25 +1,42 @@
|
|||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
pub mod axis; // include axis management module
|
//! CMDR Joystick 25 firmware library for RP2040.
|
||||||
pub mod mapping; // include button mapping module for buttons dependency
|
//!
|
||||||
pub mod button_matrix; // include button matrix module for buttons dependency
|
//! This crate provides the reusable building blocks that power the main
|
||||||
pub mod buttons; // include button management module
|
//! firmware: axis processing, button handling, calibration and storage, USB
|
||||||
pub mod calibration; // include calibration management module
|
//! HID reporting, and hardware/status abstractions.
|
||||||
pub mod expo; // include your module
|
|
||||||
pub mod hardware; // include hardware module for storage dependency
|
|
||||||
pub mod status; // include status LED module
|
|
||||||
pub mod storage; // include storage module
|
|
||||||
pub mod usb_joystick_device; // include USB joystick device module
|
|
||||||
pub mod usb_report; // include USB HID report generation module
|
|
||||||
|
|
||||||
// re-export useful items if you want
|
/// Axis processing for gimbal and virtual axes (smoothing, expo, holds).
|
||||||
|
pub mod axis;
|
||||||
|
/// Button-to-USB mapping tables and HAT constants.
|
||||||
|
pub mod mapping;
|
||||||
|
/// Row/column scanned button matrix driver with debouncing.
|
||||||
|
pub mod button_matrix;
|
||||||
|
/// Button state machine (short/long press, timing, special actions).
|
||||||
|
pub mod buttons;
|
||||||
|
/// Calibration workflow and persistence orchestration.
|
||||||
|
pub mod calibration;
|
||||||
|
/// Exponential response curves and helpers.
|
||||||
|
pub mod expo;
|
||||||
|
/// Hardware constants, pin definitions, timing, and helper macros.
|
||||||
|
pub mod hardware;
|
||||||
|
/// WS2812 status LED driver and status model.
|
||||||
|
pub mod status;
|
||||||
|
/// EEPROM storage layout and read/write helpers.
|
||||||
|
pub mod storage;
|
||||||
|
/// USB HID joystick descriptor and writer.
|
||||||
|
pub mod usb_joystick_device;
|
||||||
|
/// Convert runtime state into USB HID joystick reports.
|
||||||
|
pub mod usb_report;
|
||||||
|
|
||||||
|
/// Re-exports for convenient access in `main` and downstream consumers.
|
||||||
pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
|
pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
|
||||||
pub use calibration::CalibrationManager;
|
pub use calibration::CalibrationManager;
|
||||||
pub use expo::{ExpoLUT, apply_expo_curve, constrain, generate_expo_lut};
|
pub use expo::{ExpoLUT, apply_expo_curve, constrain, generate_expo_lut};
|
||||||
pub use storage::{read_axis_calibration, read_gimbal_mode, write_calibration_data, StorageError};
|
pub use storage::{read_axis_calibration, read_gimbal_mode, write_calibration_data, StorageError};
|
||||||
pub use usb_report::{get_joystick_report, axis_12bit_to_i16};
|
pub use usb_report::{get_joystick_report, axis_12bit_to_i16};
|
||||||
|
|
||||||
// put common constants here too
|
/// Common ADC range constants used across modules.
|
||||||
pub const ADC_MIN: u16 = 0;
|
pub const ADC_MIN: u16 = 0;
|
||||||
pub const ADC_MAX: u16 = 4095;
|
pub const ADC_MAX: u16 = 4095;
|
||||||
pub const AXIS_CENTER: u16 = ADC_MAX / 2;
|
pub const AXIS_CENTER: u16 = ADC_MAX / 2;
|
||||||
|
|||||||
@ -1,54 +1,24 @@
|
|||||||
//! # CMtec CMDR Joystick 25 - Main Firmware
|
//! CMDR Joystick 25 – RP2040 main firmware
|
||||||
//!
|
//!
|
||||||
//! **Project:** CMtec CMDR joystick 25
|
//! Overview
|
||||||
//! **Date:** 2023-08-01
|
//! - 4 gimbal axes (LX, LY, RX, RY) with smoothing, calibration and expo
|
||||||
//! **Author:** Christoffer Martinsson
|
//! - 2 virtual axes (RY/RZ) driven by buttons with direction compensation
|
||||||
//! **Email:** cm@cmtec.se
|
//! - 5x5 button matrix + 2 extra buttons, with debounce and short/long press
|
||||||
//! **License:** Please refer to LICENSE in root directory
|
//! - USB HID joystick: 7 axes, 32 buttons, 8‑way HAT
|
||||||
|
//! - EEPROM‑backed calibration and gimbal mode (M10/M7)
|
||||||
|
//! - WS2812 status LED for state indication
|
||||||
//!
|
//!
|
||||||
//! ## Overview
|
//! Modules
|
||||||
|
//! - hardware.rs: pins, clocks, timers, helpers
|
||||||
|
//! - axis.rs: gimbal/virtual axis processing and throttle hold
|
||||||
|
//! - button_matrix.rs + buttons.rs: scanning, debouncing, press types, special actions
|
||||||
|
//! - calibration.rs + storage.rs: runtime calibration and persistence
|
||||||
|
//! - usb_report.rs + usb_joystick_device.rs: HID descriptor and report generation
|
||||||
|
//! - status.rs: WS2812 driver and status model
|
||||||
//!
|
//!
|
||||||
//! This is the main firmware entry point for the CMtec CMDR Joystick 25, a professional-grade
|
//! Modes: Normal, Calibration, Throttle Hold, Virtual Throttle, Bootloader
|
||||||
//! USB HID joystick controller built on the Raspberry Pi RP2040 microcontroller. The firmware
|
|
||||||
//! provides advanced features including:
|
|
||||||
//!
|
//!
|
||||||
//! - **4-axis gimbal control** (Left X/Y, Right X/Y) with M10/M7 hardware support
|
//! Timing: scan 200 µs, process 1200 µs, USB 10 ms, LED 250 ms
|
||||||
//! - **Virtual axis control** (RY/RZ) via front panel buttons with direction compensation
|
|
||||||
//! - **Advanced calibration system** with real-time min/max tracking and EEPROM persistence
|
|
||||||
//! - **Exponential response curves** for enhanced control feel and precision
|
|
||||||
//! - **Throttle hold functionality** with configurable hold values
|
|
||||||
//! - **Professional button matrix** (5x5 grid + 2 extra buttons)
|
|
||||||
//! - **USB HID compliance** with 7-axis, 32-button, 8-direction HAT switch support
|
|
||||||
//! - **Status LED system** with multiple operational modes
|
|
||||||
//!
|
|
||||||
//! ## Architecture
|
|
||||||
//!
|
|
||||||
//! The firmware follows a modular design with clear separation of concerns:
|
|
||||||
//!
|
|
||||||
//! - **Hardware abstraction** (`hardware.rs`) - Pin definitions and hardware constants
|
|
||||||
//! - **Axis management** (`axis.rs`) - Gimbal and virtual axis processing with filtering
|
|
||||||
//! - **Button processing** (`buttons.rs`) - Matrix scanning, debouncing, and special combinations
|
|
||||||
//! - **Calibration system** (`calibration.rs`) - Real-time calibration and gimbal mode selection
|
|
||||||
//! - **USB reporting** (`usb_report.rs`) - HID report generation and axis conversion
|
|
||||||
//! - **Data persistence** (`storage.rs`) - EEPROM calibration data management
|
|
||||||
//! - **Status indication** (`status.rs`) - LED control and system state visualization
|
|
||||||
//! - **Signal processing** (`expo.rs`) - Exponential curve lookup tables
|
|
||||||
//!
|
|
||||||
//! ## Operational Modes
|
|
||||||
//!
|
|
||||||
//! - **Normal Operation** - Standard joystick functionality with all features active
|
|
||||||
//! - **Calibration Mode** - Real-time axis calibration with min/max tracking
|
|
||||||
//! - **Throttle Hold** - Maintains throttle position for hands-free operation
|
|
||||||
//! - **Virtual Throttle** - Button-controlled axis remapping for alternative control schemes
|
|
||||||
//! - **Bootloader Entry** - Safe firmware update mode via USB mass storage
|
|
||||||
//!
|
|
||||||
//! ## Hardware Support
|
|
||||||
//!
|
|
||||||
//! - **M10 Gimbal** - Standard configuration with proper axis mapping
|
|
||||||
//! - **M7 Gimbal** - Hardware variant with axis inversion compensation
|
|
||||||
//! - **USB HID** - Full-speed USB 2.0 with professional descriptor compliance
|
|
||||||
//! - **EEPROM Storage** - Persistent calibration data with error handling
|
|
||||||
//! - **WS2812 Status LED** - Advanced status indication with multiple modes
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
@ -123,10 +93,7 @@ use hardware::DEBOUNCE;
|
|||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
#[rp2040_hal::entry]
|
#[rp2040_hal::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
// # Hardware Initialization Phase
|
// Hardware initialization and peripheral setup for joystick operation
|
||||||
//
|
|
||||||
// Initialize all RP2040 peripheral singleton objects and configure the hardware
|
|
||||||
// subsystems for joystick operation.
|
|
||||||
|
|
||||||
// Acquire exclusive access to RP2040 peripherals
|
// Acquire exclusive access to RP2040 peripherals
|
||||||
let mut pac = pac::Peripherals::take().unwrap();
|
let mut pac = pac::Peripherals::take().unwrap();
|
||||||
@ -172,11 +139,7 @@ fn main() -> ! {
|
|||||||
let i2c_address = SlaveAddr::Alternative(false, false, false);
|
let i2c_address = SlaveAddr::Alternative(false, false, false);
|
||||||
let mut eeprom = Eeprom24x::new_24x32(i2c, i2c_address);
|
let mut eeprom = Eeprom24x::new_24x32(i2c, i2c_address);
|
||||||
|
|
||||||
// # ADC Configuration
|
// ADC configuration: prepare 12-bit ADC channels for all four gimbal axes
|
||||||
//
|
|
||||||
// Configure the 12-bit ADC for reading analog values from the gimbal potentiometers.
|
|
||||||
// The ADC provides 0-4095 raw values that are later processed through filtering,
|
|
||||||
// calibration, and exponential curve lookup.
|
|
||||||
|
|
||||||
// Initialize 12-bit ADC with 4 channels for gimbal axes
|
// Initialize 12-bit ADC with 4 channels for gimbal axes
|
||||||
let mut adc = Adc::new(pac.ADC, &mut pac.RESETS);
|
let mut adc = Adc::new(pac.ADC, &mut pac.RESETS);
|
||||||
@ -218,11 +181,7 @@ fn main() -> ! {
|
|||||||
let mut left_extra_button = get_pin!(pins, left_extra_button).into_pull_up_input();
|
let mut left_extra_button = get_pin!(pins, left_extra_button).into_pull_up_input();
|
||||||
let mut right_extra_button = get_pin!(pins, right_extra_button).into_pull_up_input();
|
let mut right_extra_button = get_pin!(pins, right_extra_button).into_pull_up_input();
|
||||||
|
|
||||||
// # Status LED Initialization
|
// Status LED initialization: WS2812 via PIO for runtime status indication
|
||||||
//
|
|
||||||
// Configure WS2812 RGB LED using PIO state machine for status indication.
|
|
||||||
// The LED provides visual feedback for system state, calibration mode,
|
|
||||||
// errors, and operational status.
|
|
||||||
|
|
||||||
// Initialize WS2812 status LED using PIO state machine
|
// Initialize WS2812 status LED using PIO state machine
|
||||||
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
|
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
|
||||||
@ -238,11 +197,7 @@ fn main() -> ! {
|
|||||||
|
|
||||||
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
||||||
|
|
||||||
// # Bootloader Entry Check
|
// Bootloader entry check: early matrix scan; hold front‑left‑lower to enter USB mass‑storage bootloader
|
||||||
//
|
|
||||||
// Perform initial button scan to check for bootloader entry condition.
|
|
||||||
// Holding the front-left-lower button during power-on enters USB mass storage
|
|
||||||
// bootloader mode for firmware updates.
|
|
||||||
|
|
||||||
// Scan button matrix multiple times to ensure stable debounced readings
|
// Scan button matrix multiple times to ensure stable debounced readings
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
@ -256,14 +211,7 @@ fn main() -> ! {
|
|||||||
rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask);
|
rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
// # Timer Configuration
|
// Timer configuration: cadence for LED updates, scans, processing and USB
|
||||||
//
|
|
||||||
// Configure multiple countdown timers for different system operations:
|
|
||||||
// - Status LED updates (visual feedback)
|
|
||||||
// - Button matrix scanning (input processing)
|
|
||||||
// - Data processing (axis and button logic)
|
|
||||||
// - USB report transmission (HID communication)
|
|
||||||
// - Millisecond timing (general purpose)
|
|
||||||
|
|
||||||
// Initialize hardware timer peripheral
|
// Initialize hardware timer peripheral
|
||||||
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
|
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
|
||||||
@ -290,11 +238,7 @@ fn main() -> ! {
|
|||||||
let mut button_manager = ButtonManager::new();
|
let mut button_manager = ButtonManager::new();
|
||||||
let mut calibration_manager = CalibrationManager::new();
|
let mut calibration_manager = CalibrationManager::new();
|
||||||
|
|
||||||
// # Signal Processing Initialization
|
// Signal processing: expo LUTs and smoothing filters
|
||||||
//
|
|
||||||
// Initialize exponential curve lookup tables and digital smoothing filters.
|
|
||||||
// The expo curves provide non-linear response characteristics for enhanced
|
|
||||||
// control feel, while smoothing filters reduce ADC noise and jitter.
|
|
||||||
|
|
||||||
// Create exponential curve lookup tables (avoids floating-point math in real-time)
|
// Create exponential curve lookup tables (avoids floating-point math in real-time)
|
||||||
let expo_lut = ExpoLUT::new(0.3);
|
let expo_lut = ExpoLUT::new(0.3);
|
||||||
@ -303,14 +247,7 @@ fn main() -> ! {
|
|||||||
// Initialize digital smoothing filters for each gimbal axis
|
// Initialize digital smoothing filters for each gimbal axis
|
||||||
let mut smoother = AxisManager::create_smoothers();
|
let mut smoother = AxisManager::create_smoothers();
|
||||||
|
|
||||||
// # USB HID Configuration
|
// USB HID configuration (full‑speed joystick class)
|
||||||
//
|
|
||||||
// Configure USB Human Interface Device (HID) for joystick functionality.
|
|
||||||
// The device presents as a standard USB joystick with:
|
|
||||||
// - 7 analog axes (X, Y, Z, RX, RY, RZ, Slider)
|
|
||||||
// - 32 digital buttons
|
|
||||||
// - 8-direction HAT switch
|
|
||||||
// - Full-speed USB 2.0 compliance
|
|
||||||
|
|
||||||
// Initialize USB bus allocator for RP2040
|
// Initialize USB bus allocator for RP2040
|
||||||
let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
|
let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
|
||||||
@ -334,11 +271,7 @@ fn main() -> ! {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// # Calibration Data Initialization
|
// Calibration data initialization: load axis calibration and gimbal mode from EEPROM
|
||||||
//
|
|
||||||
// Load previously saved calibration data from EEPROM storage.
|
|
||||||
// Each axis has individual min/max/center values for accurate scaling.
|
|
||||||
// Gimbal mode (M10/M7) is also restored from storage.
|
|
||||||
|
|
||||||
// Load calibration data from EEPROM using CalibrationManager
|
// Load calibration data from EEPROM using CalibrationManager
|
||||||
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
|
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
|
||||||
@ -348,13 +281,7 @@ fn main() -> ! {
|
|||||||
calibration_manager.set_gimbal_mode(gimbal_mode);
|
calibration_manager.set_gimbal_mode(gimbal_mode);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// # Main Control Loop
|
// Main control loop: poll USB, scan inputs, process data, send reports
|
||||||
//
|
|
||||||
// The main control loop handles multiple time-sliced operations:
|
|
||||||
// 1. USB HID polling and device state management
|
|
||||||
// 2. High-frequency button matrix scanning and ADC reading
|
|
||||||
// 3. Medium-frequency data processing and axis calculations
|
|
||||||
// 4. Low-frequency USB report transmission and status updates
|
|
||||||
|
|
||||||
// Handle USB device polling and maintain connection state
|
// Handle USB device polling and maintain connection state
|
||||||
if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
|
if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
// TODO rename to mapping.rs
|
//! Button-to-USB mapping for CMDR Joystick 25
|
||||||
//! Button configuration and USB mapping for CMDR Joystick 25
|
//!
|
||||||
|
//! This module defines the hardware button indices (matrix layout and extras)
|
||||||
|
//! and their mapping to USB joystick button numbers and HAT directions.
|
||||||
|
//! The mapping is consumed by the button processing and USB report layers.
|
||||||
|
//!
|
||||||
|
//! Notes
|
||||||
|
//! - USB buttons are 1-based (USB_BUTTON_1..=USB_BUTTON_32)
|
||||||
|
//! - HAT is represented by four symbolic constants: up/right/down/left
|
||||||
|
//! - Some buttons enable a “long press” variant via `usb_button_long`
|
||||||
|
|
||||||
// HW Button index map:
|
// HW Button index map:
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
@ -86,7 +94,12 @@ pub const USB_HAT_LEFT: usize = 36;
|
|||||||
|
|
||||||
use crate::buttons::Button;
|
use crate::buttons::Button;
|
||||||
|
|
||||||
/// Configure USB button mappings for all buttons
|
/// Configure USB button mappings for all buttons.
|
||||||
|
///
|
||||||
|
/// Populates per-button fields used by the button state machine:
|
||||||
|
/// - `usb_button`: regular (short-press) USB button assignment
|
||||||
|
/// - `usb_button_long`: long-press USB button assignment (if enabled)
|
||||||
|
/// - `enable_long_press` and `enable_long_hold`: behavior flags for timing
|
||||||
pub fn configure_button_mappings(buttons: &mut [Button]) {
|
pub fn configure_button_mappings(buttons: &mut [Button]) {
|
||||||
buttons[BUTTON_FRONT_LEFT_LOWER].usb_button = USB_BUTTON_29;
|
buttons[BUTTON_FRONT_LEFT_LOWER].usb_button = USB_BUTTON_29;
|
||||||
buttons[BUTTON_FRONT_LEFT_UPPER].usb_button = USB_BUTTON_28;
|
buttons[BUTTON_FRONT_LEFT_UPPER].usb_button = USB_BUTTON_28;
|
||||||
@ -151,4 +164,3 @@ pub fn configure_button_mappings(buttons: &mut [Button]) {
|
|||||||
buttons[BUTTON_FRONT_LEFT_EXTRA].usb_button = USB_BUTTON_30;
|
buttons[BUTTON_FRONT_LEFT_EXTRA].usb_button = USB_BUTTON_30;
|
||||||
buttons[BUTTON_FRONT_RIGHT_EXTRA].usb_button = USB_BUTTON_31;
|
buttons[BUTTON_FRONT_RIGHT_EXTRA].usb_button = USB_BUTTON_31;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
//! Project: CMtec CMDR joystick 25
|
//! WS2812 status LED driver for CMDR Joystick 25
|
||||||
//! Date: 2025-09-13
|
//!
|
||||||
//! Author: Christoffer Martinsson
|
//! Provides a small helper around `ws2812_pio` to display firmware state on a
|
||||||
//! Email: cm@cmtec.se
|
//! single WS2812 RGB LED using RP2040 PIO. The driver offers simple modes
|
||||||
//! License: Please refer to LICENSE in root directory
|
//! (solid/flash) and a `SystemState` facade used by main.
|
||||||
|
|
||||||
use rp2040_hal::{
|
use rp2040_hal::{
|
||||||
gpio::AnyPin,
|
gpio::AnyPin,
|
||||||
@ -11,7 +11,7 @@ use rp2040_hal::{
|
|||||||
use smart_leds::{RGB8, SmartLedsWrite};
|
use smart_leds::{RGB8, SmartLedsWrite};
|
||||||
use ws2812_pio::Ws2812Direct;
|
use ws2812_pio::Ws2812Direct;
|
||||||
|
|
||||||
/// Status LED modes with clear semantic meaning
|
/// Status LED modes with clear semantics.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
pub enum StatusMode {
|
pub enum StatusMode {
|
||||||
@ -27,7 +27,7 @@ pub enum StatusMode {
|
|||||||
Bootloader = 9,
|
Bootloader = 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// System state for LED status indication
|
/// Aggregate system state for LED status indication.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct SystemState {
|
pub struct SystemState {
|
||||||
pub usb_active: bool,
|
pub usb_active: bool,
|
||||||
@ -36,7 +36,7 @@ pub struct SystemState {
|
|||||||
pub vt_enable: bool,
|
pub vt_enable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Color definitions for different status modes
|
/// Color definitions for different status modes.
|
||||||
const LED_COLORS: [RGB8; 10] = [
|
const LED_COLORS: [RGB8; 10] = [
|
||||||
RGB8 { r: 0, g: 0, b: 0 }, // Off
|
RGB8 { r: 0, g: 0, b: 0 }, // Off
|
||||||
RGB8 { r: 10, g: 7, b: 0 }, // Normal (Green)
|
RGB8 { r: 10, g: 7, b: 0 }, // Normal (Green)
|
||||||
@ -50,7 +50,7 @@ const LED_COLORS: [RGB8; 10] = [
|
|||||||
RGB8 { r: 0, g: 10, b: 10 }, // Bootloader (Purple)
|
RGB8 { r: 0, g: 10, b: 10 }, // Bootloader (Purple)
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Improved Status LED driver with self-contained state management
|
/// Status LED driver with self-contained state management.
|
||||||
pub struct StatusLed<P, SM, I>
|
pub struct StatusLed<P, SM, I>
|
||||||
where
|
where
|
||||||
I: AnyPin<Function = P::PinFunction>,
|
I: AnyPin<Function = P::PinFunction>,
|
||||||
@ -69,7 +69,7 @@ where
|
|||||||
P: PIOExt,
|
P: PIOExt,
|
||||||
SM: StateMachineIndex,
|
SM: StateMachineIndex,
|
||||||
{
|
{
|
||||||
/// Creates a new StatusLed instance
|
/// Create a new StatusLed instance.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `pin` - PIO pin for WS2812 LED
|
/// * `pin` - PIO pin for WS2812 LED
|
||||||
@ -91,12 +91,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update LED based on system state - main interface for main.rs
|
/// Update LED based on system state and current time (ms).
|
||||||
///
|
|
||||||
/// This replaces the old update_status_led() function from main.rs
|
|
||||||
/// # Arguments
|
|
||||||
/// * `system_state` - Current system state
|
|
||||||
/// * `current_time` - Current time in milliseconds for flash timing
|
|
||||||
pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) {
|
pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) {
|
||||||
let desired_mode = if system_state.calibration_active {
|
let desired_mode = if system_state.calibration_active {
|
||||||
StatusMode::ActivityFlash
|
StatusMode::ActivityFlash
|
||||||
@ -115,10 +110,7 @@ where
|
|||||||
self.set_mode(desired_mode, current_time);
|
self.set_mode(desired_mode, current_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set LED mode directly
|
/// Set LED mode directly (explicit override).
|
||||||
/// # Arguments
|
|
||||||
/// * `mode` - Desired status mode
|
|
||||||
/// * `current_time` - Current time in milliseconds
|
|
||||||
pub fn set_mode(&mut self, mode: StatusMode, current_time: u32) {
|
pub fn set_mode(&mut self, mode: StatusMode, current_time: u32) {
|
||||||
// Force update if mode changed
|
// Force update if mode changed
|
||||||
let force_update = mode != self.current_mode;
|
let force_update = mode != self.current_mode;
|
||||||
@ -127,10 +119,7 @@ where
|
|||||||
self.update_display(current_time, force_update);
|
self.update_display(current_time, force_update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update LED display based on current mode and timing
|
/// Periodic update for flashing behavior at roughly ~500 ms intervals.
|
||||||
/// Called periodically to handle flashing
|
|
||||||
/// # Arguments
|
|
||||||
/// * `current_time` - Current time in milliseconds
|
|
||||||
pub fn update_display(&mut self, current_time: u32, force_update: bool) {
|
pub fn update_display(&mut self, current_time: u32, force_update: bool) {
|
||||||
let should_update = force_update || self.should_flash_now(current_time);
|
let should_update = force_update || self.should_flash_now(current_time);
|
||||||
|
|
||||||
@ -163,13 +152,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get current status mode
|
/// Get current status mode.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn get_mode(&self) -> StatusMode {
|
pub fn get_mode(&self) -> StatusMode {
|
||||||
self.current_mode
|
self.current_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if it's time to update flashing LED
|
/// Check if it's time to update the flashing LED state.
|
||||||
fn should_flash_now(&self, current_time: u32) -> bool {
|
fn should_flash_now(&self, current_time: u32) -> bool {
|
||||||
match self.last_update_time {
|
match self.last_update_time {
|
||||||
None => true, // First update
|
None => true, // First update
|
||||||
@ -188,7 +177,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write color to LED
|
/// Write a single color to the LED.
|
||||||
fn write_color(&mut self, color: RGB8) {
|
fn write_color(&mut self, color: RGB8) {
|
||||||
let _ = self.ws2812_direct.write([color].iter().copied());
|
let _ = self.ws2812_direct.write([color].iter().copied());
|
||||||
}
|
}
|
||||||
@ -200,11 +189,9 @@ where
|
|||||||
P: PIOExt,
|
P: PIOExt,
|
||||||
SM: StateMachineIndex,
|
SM: StateMachineIndex,
|
||||||
{
|
{
|
||||||
/// Legacy interface for compatibility - direct mode update
|
/// Legacy interface for compatibility – direct mode update.
|
||||||
/// This maintains compatibility with existing direct update calls
|
|
||||||
pub fn update(&mut self, mode: StatusMode) {
|
pub fn update(&mut self, mode: StatusMode) {
|
||||||
// Use a dummy time for immediate updates
|
// Use a dummy time for immediate updates
|
||||||
self.set_mode(mode, 0);
|
self.set_mode(mode, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
//! Storage operations for CMDR Joystick 25
|
//! EEPROM storage for CMDR Joystick 25
|
||||||
//!
|
//!
|
||||||
//! Handles EEPROM operations for calibration data and configuration storage.
|
//! Provides helpers to read/write per‑axis calibration and gimbal mode to an
|
||||||
|
//! external 24x‑series EEPROM. The API is closure‑based to allow testing on
|
||||||
|
//! a host without hardware access.
|
||||||
|
|
||||||
use crate::hardware::EEPROM_DATA_LENGTH;
|
use crate::hardware::EEPROM_DATA_LENGTH;
|
||||||
|
|
||||||
// ==================== EEPROM DATA LAYOUT ====================
|
// ==================== EEPROM DATA LAYOUT ====================
|
||||||
|
/// EEPROM address for gimbal mode storage (original layout uses address 25).
|
||||||
|
|
||||||
/// EEPROM address for gimbal mode storage (original read from address 25)
|
|
||||||
pub const GIMBAL_MODE_OFFSET: u32 = EEPROM_DATA_LENGTH as u32; // Address 25
|
pub const GIMBAL_MODE_OFFSET: u32 = EEPROM_DATA_LENGTH as u32; // Address 25
|
||||||
|
|
||||||
// Original format uses: base = axis_index * 6, addresses = base+1,2,3,4,5,6
|
// Original format uses: base = axis_index * 6, addresses = base+1,2,3,4,5,6
|
||||||
|
|
||||||
// ==================== ERROR TYPES ====================
|
// ==================== ERROR TYPES ====================
|
||||||
|
/// Errors returned by storage helpers.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum StorageError {
|
pub enum StorageError {
|
||||||
ReadError,
|
ReadError,
|
||||||
@ -24,8 +24,8 @@ pub enum StorageError {
|
|||||||
|
|
||||||
// ==================== CORE FUNCTIONS ====================
|
// ==================== CORE FUNCTIONS ====================
|
||||||
|
|
||||||
/// Read calibration data for a single axis from EEPROM
|
/// Read calibration data for a single axis from EEPROM.
|
||||||
/// Returns (min, max, center) values as u16
|
/// Returns (min, max, center) values as u16.
|
||||||
pub fn read_axis_calibration(
|
pub fn read_axis_calibration(
|
||||||
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
|
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
|
||||||
axis_index: usize
|
axis_index: usize
|
||||||
@ -40,7 +40,7 @@ pub fn read_axis_calibration(
|
|||||||
Ok((min, max, center))
|
Ok((min, max, center))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read gimbal mode from EEPROM
|
/// Read gimbal mode from EEPROM.
|
||||||
pub fn read_gimbal_mode(
|
pub fn read_gimbal_mode(
|
||||||
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>
|
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>
|
||||||
) -> Result<u8, StorageError> {
|
) -> Result<u8, StorageError> {
|
||||||
@ -48,7 +48,7 @@ pub fn read_gimbal_mode(
|
|||||||
.map_err(|_| StorageError::ReadError)
|
.map_err(|_| StorageError::ReadError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write all calibration data and gimbal mode to EEPROM
|
/// Write all calibration data and gimbal mode to EEPROM.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn write_calibration_data(
|
pub fn write_calibration_data(
|
||||||
write_page_fn: &mut dyn FnMut(u32, &[u8]) -> Result<(), ()>,
|
write_page_fn: &mut dyn FnMut(u32, &[u8]) -> Result<(), ()>,
|
||||||
@ -80,7 +80,7 @@ pub fn write_calibration_data(
|
|||||||
// ==================== HELPER FUNCTIONS ====================
|
// ==================== HELPER FUNCTIONS ====================
|
||||||
|
|
||||||
|
|
||||||
/// Read a u16 value from EEPROM in big-endian format (matching original)
|
/// Read a u16 value from EEPROM in little‑endian (low then high byte) format.
|
||||||
fn read_u16_with_closure(
|
fn read_u16_with_closure(
|
||||||
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
|
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
|
||||||
low_addr: u32,
|
low_addr: u32,
|
||||||
@ -216,4 +216,4 @@ mod tests {
|
|||||||
assert_eq!(max, 4000);
|
assert_eq!(max, 4000);
|
||||||
assert_eq!(center, 2050);
|
assert_eq!(center, 2050);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,7 @@ pub const JOYSTICK_DESCRIPTOR: &[u8] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
|
||||||
|
/// HID report payload matching `JOYSTICK_DESCRIPTOR`.
|
||||||
pub struct JoystickReport {
|
pub struct JoystickReport {
|
||||||
pub x: i16, // 16bit
|
pub x: i16, // 16bit
|
||||||
pub y: i16, // 16bit
|
pub y: i16, // 16bit
|
||||||
@ -128,10 +129,11 @@ pub struct Joystick<'a, B: UsbBus> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<B: UsbBus> Joystick<'_, B> {
|
impl<B: UsbBus> Joystick<'_, B> {
|
||||||
|
/// Serialize and send a joystick report matching the HID descriptor.
|
||||||
pub fn write_report(&mut self, report: &JoystickReport) -> Result<(), UsbHidError> {
|
pub fn write_report(&mut self, report: &JoystickReport) -> Result<(), UsbHidError> {
|
||||||
let mut data: [u8; 19] = [0; 19];
|
let mut data: [u8; 19] = [0; 19];
|
||||||
|
|
||||||
// Did not make the packed struct work, so doing it manually
|
// Pack fields manually to match the descriptor layout (little‑endian i16).
|
||||||
data[0] = report.x as u8;
|
data[0] = report.x as u8;
|
||||||
data[1] = (report.x >> 8) as u8;
|
data[1] = (report.x >> 8) as u8;
|
||||||
data[2] = report.y as u8;
|
data[2] = report.y as u8;
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
//! USB HID Report Generation for CMDR Joystick 25
|
//! USB HID report generation for CMDR Joystick 25
|
||||||
//!
|
//!
|
||||||
//! Handles the conversion of axis values and button states into USB HID joystick reports.
|
//! Converts processed axis values and button states into a `JoystickReport`
|
||||||
//! Provides functionality for virtual axis control, HAT switch processing, and button mapping.
|
//! that matches the HID descriptor defined in `usb_joystick_device.rs`.
|
||||||
|
//! Also contains support for virtual throttle mode and HAT directions.
|
||||||
|
|
||||||
use crate::axis::{GimbalAxis, remap, GIMBAL_AXIS_LEFT_X, GIMBAL_AXIS_LEFT_Y, GIMBAL_AXIS_RIGHT_X, GIMBAL_AXIS_RIGHT_Y};
|
use crate::axis::{GimbalAxis, remap, GIMBAL_AXIS_LEFT_X, GIMBAL_AXIS_LEFT_Y, GIMBAL_AXIS_RIGHT_X, GIMBAL_AXIS_RIGHT_Y};
|
||||||
use crate::buttons::{Button, TOTAL_BUTTONS};
|
use crate::buttons::{Button, TOTAL_BUTTONS};
|
||||||
@ -11,7 +12,7 @@ use crate::usb_joystick_device::JoystickReport;
|
|||||||
|
|
||||||
// ==================== USB REPORT GENERATION ====================
|
// ==================== USB REPORT GENERATION ====================
|
||||||
|
|
||||||
/// Convert 12bit unsigned values to 16bit signed for USB HID joystick reports
|
/// Convert 12‑bit unsigned values to 16‑bit signed for USB HID joystick reports.
|
||||||
///
|
///
|
||||||
/// Maps 12-bit ADC values (0-4095) to 16-bit signed USB HID values (-32768 to 32767).
|
/// Maps 12-bit ADC values (0-4095) to 16-bit signed USB HID values (-32768 to 32767).
|
||||||
/// This is specifically for USB joystick axis reporting.
|
/// This is specifically for USB joystick axis reporting.
|
||||||
@ -34,7 +35,7 @@ pub fn axis_12bit_to_i16(val: u16) -> i16 {
|
|||||||
scaled as i16
|
scaled as i16
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a complete USB HID joystick report from current system state
|
/// Generate a complete USB HID joystick report from the current system state.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `matrix_keys` - Array of button states from the button matrix
|
/// * `matrix_keys` - Array of button states from the button matrix
|
||||||
@ -62,8 +63,9 @@ pub fn get_joystick_report(
|
|||||||
let mut slider: i16 = axis_12bit_to_i16(ADC_MIN);
|
let mut slider: i16 = axis_12bit_to_i16(ADC_MIN);
|
||||||
let mut hat: u8 = 8; // Hat center position
|
let mut hat: u8 = 8; // Hat center position
|
||||||
|
|
||||||
// Virtual axis control: Disables z axis and uses right gimbal X axis to control
|
// Virtual throttle mode:
|
||||||
// the slider axis. Values from center stick to max or min will be recalculated to min to max.
|
// - Disable Z axis
|
||||||
|
// - Map right‑X to Slider with symmetric behavior around center
|
||||||
if *vt_enable {
|
if *vt_enable {
|
||||||
if axis[GIMBAL_AXIS_RIGHT_X].value >= AXIS_CENTER {
|
if axis[GIMBAL_AXIS_RIGHT_X].value >= AXIS_CENTER {
|
||||||
slider = axis_12bit_to_i16(remap(
|
slider = axis_12bit_to_i16(remap(
|
||||||
@ -88,7 +90,7 @@ pub fn get_joystick_report(
|
|||||||
z = 0;
|
z = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process button states and build USB button bitmask
|
// Process buttons and build the USB button bitmask + HAT value
|
||||||
let mut buttons: u32 = 0;
|
let mut buttons: u32 = 0;
|
||||||
for key in matrix_keys.iter_mut() {
|
for key in matrix_keys.iter_mut() {
|
||||||
if key.enable_long_press {
|
if key.enable_long_press {
|
||||||
@ -110,7 +112,7 @@ pub fn get_joystick_report(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset USB changed flags for next iteration
|
// Reset per‑button USB change flags for the next iteration
|
||||||
for key in matrix_keys.iter_mut() {
|
for key in matrix_keys.iter_mut() {
|
||||||
key.usb_changed = false;
|
key.usb_changed = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user