Code commenting/documentation

This commit is contained in:
Christoffer Martinsson 2025-09-15 23:42:54 +02:00
parent 6cf9bde7e0
commit b98614fe8e
15 changed files with 395 additions and 316 deletions

View File

@ -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 halleffect 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 8way 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 - Halleffect 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× 8way HAT
- Advanced input pipeline
- Digital smoothing for stable axes
- Peraxis calibration (min/center/max) with EEPROM persistence
- Optional exponential response curves (LUT based)
- Throttle hold (capture + remap around center)
- Virtual throttle mode (map rightX to Slider; disable Z)
- 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)
- ![pcb_top](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_top.png) - ![pcb_top](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_top.png)
- ![pcb_bottom](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_bottom.png) - ![pcb_bottom](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_bottom.png)
- 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 toplevel `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 poweron): hold the frontleftlower button
- Infirmware combo: Frontleftlower + Topleftmode + Toprightmode
## 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 Frontleftupper + Topleftmode + Toprightmode.
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 TopleftUP
- M7: press TopleftDOWN
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 Topleftmode button to capture the current throttle value
- Press again at center to clear hold
- Virtual throttle mode: press the Toprightmode button to toggle
- Disables Z axis and maps rightX 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 elf2uf2rs)
- `install.sh`: unified helper to test, build, and flash (local or via SSH)

View File

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

View File

@ -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 peraxis filters
//! - Calibrate + apply deadzones + optional expo via lookup table
//! - Implement throttlehold 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) /// Throttlehold 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 throttlehold to the leftY 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 throttlehold state for leftY 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 throttlehold value for leftY 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 prehold 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,

View File

@ -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 pullups
//! - Columns are configured as pushpull outputs
//! - Debounce is handled perbutton using a simple counter
//! - A tiny intercolumn 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 pullups)
/// * `rows` - An array of references to the row pins. /// - `cols`: array of column pins (pushpull 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 perbutton 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
} }

View File

@ -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 (nonmatrix) 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 ====================
/// Highlevel 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 (nonmatrix) 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 readonly 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: autorelease 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 // Autorelease 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.
} }
} }

View File

@ -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 peraxis 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 unittested 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 peraxis 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
} }
} }

View File

@ -1,24 +1,44 @@
//! Exponential curve processing and lookup tables for joystick axes //! Exponential response curves for joystick axes
//!
//! This module provides a small, allocationfree way to apply an exponential
//! response to 12bit axis values. A lookup table (LUT) is generated once and
//! then used at runtime to map linear input into an exposhaped output without
//! floatingpoint math.
//!
//! Notes
//! - LUT length is `ADC_MAX + 1` (4096 entries for 12bit 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 (12bit 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 12bit 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

View File

@ -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 (12bit).
pub const ADC_MIN: u16 = 0; pub const ADC_MIN: u16 = 0;
/// ADC raw maximum (12bit).
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) => {{

View File

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

View File

@ -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, 8way HAT
//! - EEPROMbacked 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 frontleftlower to enter USB massstorage 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 (fullspeed 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]) {

View File

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

View File

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

View File

@ -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 peraxis calibration and gimbal mode to an
//! external 24xseries EEPROM. The API is closurebased 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 littleendian (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);
} }
} }

View File

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

View File

@ -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 12bit unsigned values to 16bit 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 rightX 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 perbutton 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;
} }