From b98614fe8e4430a1c1809bae2c053a522c099d63 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Mon, 15 Sep 2025 23:42:54 +0200 Subject: [PATCH] Code commenting/documentation --- README.md | 92 ++++++++++++++++++---- rp2040/Cargo.toml | 15 ++-- rp2040/src/axis.rs | 80 ++++++++++--------- rp2040/src/button_matrix.rs | 68 ++++++++++------ rp2040/src/buttons.rs | 48 +++++++----- rp2040/src/calibration.rs | 55 +++++++------ rp2040/src/expo.rs | 26 ++++++- rp2040/src/hardware.rs | 44 ++++++----- rp2040/src/lib.rs | 43 ++++++---- rp2040/src/main.rs | 125 +++++++----------------------- rp2040/src/mapping.rs | 20 ++++- rp2040/src/status.rs | 47 ++++------- rp2040/src/storage.rs | 24 +++--- rp2040/src/usb_joystick_device.rs | 4 +- rp2040/src/usb_report.rs | 20 ++--- 15 files changed, 395 insertions(+), 316 deletions(-) diff --git a/README.md b/README.md index 2f8c856..5375aa0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # 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 @@ -32,7 +34,7 @@ USB HID joystick map : | | H1L | B11 | H1R | | H2L | B12 | H2R | | | | H1D | | H2D | | --------------------------------------------------------------- - + USB HID joystick map (Fn): --------------------------------------------------------------- | Fn L| B21 U| B27 U| | B32 | | B8 U| B3 U| B4 L| @@ -66,11 +68,18 @@ Config Layer (holding CONFIG button) ## Features - Ergonomic design (low profile) -- Hall effect gimbals -- Supports USB HID joystick - - 4x axis - - 4x hat switches (4x virtual, 2x hardware) - - 32x buttons (32x virtual, 19x hardware) +- Hall‑effect gimbals (FrSky M7/M10) +- USB HID joystick device + - 7 axes: X, Y, Z, Rx, Ry, Rz, Slider + - 32 buttons + - 1× 8‑way HAT +- Advanced input pipeline + - Digital smoothing for stable axes + - Per‑axis calibration (min/center/max) with EEPROM persistence + - 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 @@ -83,7 +92,6 @@ Config Layer (holding CONFIG button) - 1x Top plate (3D printed) - 2x Hat swith top (3D printed) [stl](/mCAD/Hat_Castle_Short_scale_99_99_130.stl) - 1x Custom PCB (CMDR Joystick 25 rev A) - - ![pcb_top](/eCAD/cmdr-joystick/cmdr-joystick_rev_a_board_top.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) @@ -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) - 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) -- Flashing via Cargo - - Press and hold boot button on rp2040zero board while perform a reset - - Press and hold CONF and press BOOT button. +Common commands + +``` +# 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 @@ -111,6 +137,38 @@ Config Layer (holding CONFIG button) ## Calibration 1. Center both gimbals. -2. Press and hold CONF button and press CAL botton. Status led will start blinking green. -3. Move both gimbals to all corners. -4. Press right hat switch to save calibration data to eeprom. +2. Start calibration (LED will flash): press Front‑left‑upper + Top‑left‑mode + Top‑right‑mode. +3. Move both gimbals through full travel (all corners). +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) diff --git a/rp2040/Cargo.toml b/rp2040/Cargo.toml index 52e7133..7f852f2 100644 --- a/rp2040/Cargo.toml +++ b/rp2040/Cargo.toml @@ -2,9 +2,10 @@ name = "cmdr-joystick-25" version = "0.2.0" edition = "2021" +# Firmware crate for the CMDR Joystick 25 (RP2040) [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-rt = "0.7" 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"]} static_cell = "2.1.0" -# USB hid dependencies +# USB HID stack usbd-human-interface-device = {version = "0.5.1"} usb-device = "0.3" packed_struct = { version = "0.10", default-features = false } heapless = "0.8" -# EEPROM dependencies +# External EEPROM (24x) driver eeprom24x = "0.7.2" -# ws2812-pio dependencies +# WS2812 LED (PIO) + color helpers ws2812-pio = "0.9.0" smart-leds = "0.4.0" -# Analog filter dependencies +# Analog filtering and math dyn-smooth = "0.2.0" libm = "0.2.7" @@ -70,5 +71,5 @@ bench = false test = false [features] -default = [] -std = [] +default = [] # firmware runs in `no_std` by default +std = [] # enable when running unit tests on host diff --git a/rp2040/src/axis.rs b/rp2040/src/axis.rs index 7493d91..77c1363 100644 --- a/rp2040/src/axis.rs +++ b/rp2040/src/axis.rs @@ -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, -//! ADC reading, calibration, and gimbal mode compensation. +//! Responsibilities +//! - 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::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_M7: u8 = 1; -/// Digital signal processing configuration for analog smoothing filters. -/// -/// 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. +/// Digital smoothing parameters for `DynamicSmootherEcoI32` filters. +/// Reduce ADC noise and jitter for stable axis values. pub const BASE_FREQ: i32 = 2 << 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; @@ -64,26 +70,26 @@ impl Default for GimbalAxis { } impl GimbalAxis { - /// Create a new GimbalAxis with default settings + /// Create a new GimbalAxis with default settings. pub fn new() -> Self { 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 { let mut axis = Self::new(); axis.set_calibration(min, max, center); 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) { self.min = min; self.max = max; 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) { // Convert filtered value to u16 range 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; } - /// 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) { self.hold = value; 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 { let activity = self.value != self.previous_value; self.previous_value = self.value; 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) { if throttle_hold_enable && self.value < AXIS_CENTER && !self.hold_pending { 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 - /// Returns true if USB activity should be signaled + /// Update virtual axis based on button inputs. + /// Returns true if USB activity should be signaled. pub fn update(&mut self, up_pressed: bool, down_pressed: bool, _vt_enable: bool) -> bool { let mut activity = false; @@ -219,7 +225,7 @@ impl AxisManager { 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]) { if self.gimbal_mode == GIMBAL_MODE_M10 { // 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( &self, 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); } - /// Process axis values with calibration, deadzone, and expo + /// Update all axes from smoothers (calibration, deadzone, expo) and apply holds. pub fn process_axis_values( &mut self, smoother: &[DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS], @@ -257,20 +263,20 @@ impl AxisManager { 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) { 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); } - /// 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) { self.axes[GIMBAL_AXIS_LEFT_Y].hold = AXIS_CENTER; self.axes[GIMBAL_AXIS_LEFT_Y].hold_pending = false; } - /// Update virtual axes based on button inputs - /// Returns true if USB activity should be signaled + /// Update virtual axes based on button inputs. + /// Returns true if USB activity should be signaled. pub fn update_virtual_axes( &mut self, buttons: &[Button; TOTAL_BUTTONS], @@ -301,7 +307,7 @@ impl AxisManager { activity } - /// Check for axis activity (movement from previous value) + /// Check for axis activity (movement from previous value). pub fn check_activity(&mut self) -> bool { let mut activity = false; @@ -314,18 +320,18 @@ impl AxisManager { 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) { 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 { // Original code captured axis.value BEFORE throttle hold was applied 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 { calculate_axis_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 { calculate_axis_value( self.virtual_rz.value, @@ -351,10 +357,8 @@ impl AxisManager { ) } - /// Initialize digital smoothing filters for each gimbal axis - /// - /// Creates an array of DynamicSmootherEcoI32 filters configured with appropriate - /// DSP parameters for noise reduction and jitter elimination from ADC readings. + /// Initialize digital smoothing filters for each gimbal axis. + /// Creates an array of `DynamicSmootherEcoI32` filters configured with shared DSP parameters. pub fn create_smoothers() -> [DynamicSmootherEcoI32; NBR_OF_GIMBAL_AXIS] { [ DynamicSmootherEcoI32::new(BASE_FREQ, SAMPLE_FREQ, SENSITIVITY), @@ -367,7 +371,7 @@ impl AxisManager { // ==================== AXIS PROCESSING FUNCTIONS ==================== -/// Remapping values from one range to another +/// Remap a value from one range to another using integer math and clamping. /// /// # Arguments /// * `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 } -/// 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 -/// exponential curve application for smooth joystick response. +/// Process raw input through [min, center, max] calibration, apply deadzones, +/// then apply expo via LUT if enabled for smooth response. /// /// # Arguments /// * `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 /// /// # Returns -/// Processed axis value with calibration, deadzone, and expo applied +/// Processed axis value after calibration, deadzone, and optional expo pub fn calculate_axis_value( value: u16, min: u16, diff --git a/rp2040/src/button_matrix.rs b/rp2040/src/button_matrix.rs index 53d1e96..ab22a23 100644 --- a/rp2040/src/button_matrix.rs +++ b/rp2040/src/button_matrix.rs @@ -1,8 +1,13 @@ -//! Project: CMtec CMDR joystick 24 -//! Date: 2025-03-09 -//! Author: Christoffer Martinsson -//! Email: cm@cmtec.se -//! License: Please refer to LICENSE in root directory +//! Button matrix scanner for CMDR Joystick 25 +//! +//! Scans a row/column matrix and produces a debounced boolean state for each +//! button. Designed for small matrices on microcontrollers where timing is +//! 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 cortex_m::delay::Delay; @@ -10,9 +15,21 @@ use embedded_hal::digital::{InputPin, OutputPin}; /// Button matrix driver /// -/// # Example +/// Generics +/// - `R`: number of rows +/// - `C`: number of columns +/// - `N`: total number of buttons (usually `R * C`) +/// +/// Example /// ```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> { rows: &'a mut [&'a mut dyn InputPin; 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> { /// Creates a new button matrix. /// - /// # Arguments - /// - /// * `rows` - An array of references to the row pins. - /// * `cols` - An array of references to the column pins. - /// * `debounce` - The debounce time in number of scans. + /// Arguments + /// - `rows`: array of row pins (inputs with pull‑ups) + /// - `cols`: array of column pins (push‑pull outputs) + /// - `debounce`: number of consecutive scans a change must persist before it is accepted pub fn new( rows: &'a mut [&'a mut dyn InputPin; R], cols: &'a mut [&'a mut dyn OutputPin; C], @@ -44,21 +60,23 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, } } - /// Initializes the button matrix. - /// This should be called once before scanning the matrix. + /// Initialize the matrix GPIOs (set all columns high). + /// + /// Call once before the first scan. pub fn init_pins(&mut self) { for col in self.cols.iter_mut() { col.set_high().unwrap(); } } - /// Scans the button matrix and updates the pressed state of each button. - /// This should be called at regular intervals. - /// Allow at least 5 times the delay compared to the needed button latency. + /// Scan the matrix and update each button's debounced state. /// - /// # 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) { for col_index in 0..self.cols.len() { 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 - /// - /// * `col_index` - The index of the column to process. + /// Arguments + /// - `col_index`: index of the column being scanned fn process_column(&mut self, col_index: usize) { for row_index in 0..self.rows.len() { 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] { self.pressed } diff --git a/rp2040/src/buttons.rs b/rp2040/src/buttons.rs index 95f5c49..c9776ec 100644 --- a/rp2040/src/buttons.rs +++ b/rp2040/src/buttons.rs @@ -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, -//! and special button combination processing. +//! Responsibilities +//! - 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::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 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; // ==================== BUTTON STRUCT ==================== @@ -33,6 +38,7 @@ pub struct Button { } // ==================== SPECIAL ACTIONS ==================== +/// High‑level actions triggered by dedicated button combinations. #[derive(Debug, PartialEq)] pub enum SpecialAction { @@ -48,6 +54,7 @@ pub enum SpecialAction { } // ==================== BUTTON MANAGER ==================== +/// Aggregates and processes all buttons, exposing a stable API to the rest of the firmware. pub struct ButtonManager { pub buttons: [Button; TOTAL_BUTTONS], @@ -69,14 +76,14 @@ impl ButtonManager { 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) { for (index, key) in matrix.buttons_pressed().iter().enumerate() { 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(&mut self, left_pin: &mut L, right_pin: &mut R) where L: InputPin, @@ -88,7 +95,7 @@ impl ButtonManager { 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) { // Filter left hat switch buttons 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 { let mut usb_activity = false; @@ -148,16 +155,13 @@ impl ButtonManager { usb_activity } - /// Process button timing logic with integrated timer access - /// - /// This method handles timer access internally, providing better encapsulation - /// for button timing operations and removing timer dependency from main.rs. + /// Convenience wrapper that derives `current_time` from the hardware timer. pub fn process_button_logic_with_timer(&mut self, timer: &Timer) -> bool { let current_time = (timer.get_counter().ticks() / 1000) as u32; 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 { // Secondary way to enter bootloader if self.buttons[BUTTON_FRONT_LEFT_LOWER].pressed @@ -221,7 +225,7 @@ impl ButtonManager { 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 { let button = &self.buttons[button_index]; 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] { &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] { &self.buttons } @@ -244,7 +248,13 @@ impl ButtonManager { // ==================== 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) { 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; if button.usb_press_active && (!button.pressed @@ -488,4 +498,4 @@ mod tests { // which isn't available in the test environment, but the method compilation // is verified through the cargo check above. } -} \ No newline at end of file +} diff --git a/rp2040/src/calibration.rs b/rp2040/src/calibration.rs index 77018c5..29aa610 100644 --- a/rp2040/src/calibration.rs +++ b/rp2040/src/calibration.rs @@ -1,7 +1,15 @@ //! Calibration management for CMDR Joystick 25 //! -//! Handles axis calibration, gimbal mode selection, and calibration data persistence. -//! Provides focused methods for different calibration operations with clean separation of concerns. +//! Responsibilities +//! - 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::hardware::NBR_OF_GIMBAL_AXIS; @@ -10,13 +18,14 @@ use dyn_smooth::DynamicSmootherEcoI32; // ==================== CALIBRATION MANAGER ==================== +/// Orchestrates runtime calibration and storage interactions. pub struct CalibrationManager { active: bool, gimbal_mode: u8, } impl CalibrationManager { - /// Create a new CalibrationManager with default settings + /// Create a new CalibrationManager with default settings. pub fn new() -> Self { Self { 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 { self.active } - /// Start calibration mode + /// Start calibration mode. pub fn start_calibration(&mut self) { self.active = true; } - /// Stop calibration mode + /// Stop calibration mode. pub fn stop_calibration(&mut self) { self.active = false; } - /// Get current gimbal mode + /// Get current gimbal mode. pub fn get_gimbal_mode(&self) -> u8 { self.gimbal_mode } - /// Set gimbal mode + /// Set gimbal mode. pub fn set_gimbal_mode(&mut self, mode: u8) { self.gimbal_mode = mode; } - /// Update dynamic calibration - continuous min/max tracking during calibration - /// Only active during calibration mode + /// Update dynamic calibration (continuous min/max tracking) while active. + /// + /// Only has effect when calibration is active. pub fn update_dynamic_calibration( &self, 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 - /// Returns true if mode was changed (for use during calibration) + /// Set gimbal mode to M10 and reset each axis (min/max/center) to the current smoothed center. + /// Returns true if mode was changed (only while active). pub fn set_gimbal_mode_m10( &mut self, axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], @@ -86,8 +96,8 @@ impl CalibrationManager { true } - /// Set gimbal mode to M7 and reset axis calibration to current center - /// Returns true if mode was changed (for use during calibration) + /// Set gimbal mode to M7 and reset each axis (min/max/center) to the current smoothed center. + /// Returns true if mode was changed (only while active). pub fn set_gimbal_mode_m7( &mut self, axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], @@ -102,8 +112,8 @@ impl CalibrationManager { true } - /// Save calibration data to storage and end calibration mode - /// Returns true if calibration was saved and ended + /// Save calibration data to EEPROM and end calibration mode. + /// Returns true if data was written and calibration ended. pub fn save_calibration( &mut self, axes: &[GimbalAxis; NBR_OF_GIMBAL_AXIS], @@ -133,10 +143,9 @@ impl CalibrationManager { 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. - /// Uses factory defaults if EEPROM read fails. + /// Updates the provided axes with loaded values; retains defaults on error. pub fn load_axis_calibration( axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], read_fn: &mut F, @@ -156,9 +165,8 @@ impl CalibrationManager { } } - /// Load gimbal mode from EEPROM storage - /// - /// Returns the stored gimbal mode or M10 default if read fails. + /// Load gimbal mode from EEPROM. + /// Returns the stored mode or M10 default if read fails. pub fn load_gimbal_mode(read_fn: &mut F) -> u8 where F: FnMut(u32) -> Result, @@ -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( &self, axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS], @@ -529,4 +537,3 @@ mod tests { assert!(!manager.is_active()); // Should remain inactive } } - diff --git a/rp2040/src/expo.rs b/rp2040/src/expo.rs index f96ac46..41ee73e 100644 --- a/rp2040/src/expo.rs +++ b/rp2040/src/expo.rs @@ -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 core::cmp::PartialOrd; use libm::powf; +/// Precomputed exponential lookup table (12‑bit domain). pub struct ExpoLUT { lut: [u16; 4096], } 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 { let lut = generate_expo_lut(factor); Self { lut } } + /// Apply the precomputed expo mapping to a single axis value. pub fn apply(&self, value: u16) -> u16 { 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] { let mut lut: [u16; ADC_MAX as usize + 1] = [0; ADC_MAX as usize + 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 } +/// 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 { if value >= expo_lut.len() as u16 { return ADC_MAX; @@ -38,6 +61,7 @@ pub fn apply_expo_curve(value: u16, expo_lut: &[u16]) -> u16 { expo_lut[value as usize] } +/// Clamp `value` into the inclusive range `[out_min, out_max]`. pub fn constrain(value: T, out_min: T, out_max: T) -> T { if value < out_min { out_min diff --git a/rp2040/src/hardware.rs b/rp2040/src/hardware.rs index 97f3c20..5c112c6 100644 --- a/rp2040/src/hardware.rs +++ b/rp2040/src/hardware.rs @@ -1,30 +1,38 @@ -//! Project: CMtec CMDR joystick 25 -//! Date: 2023-08-01 -//! Author: Christoffer Martinsson -//! Email: cm@cmtec.se -//! License: Please refer to LICENSE in root directory - -//! Hardware configuration constants and pin definitions for CMDR Joystick 25 +//! Hardware configuration for CMDR Joystick 25 (RP2040) +//! +//! Centralizes board constants, GPIO mappings, timing cadences and helper +//! macros to keep hardware details out of business logic. // ==================== CRYSTAL AND USB CONSTANTS ==================== - +/// External crystal frequency (Hz). pub const XTAL_FREQ_HZ: u32 = 12_000_000u32; +/// USB Vendor ID. pub const USB_VID: u16 = 0x1209; +/// USB Product ID. pub const USB_PID: u16 = 0x0002; // ==================== JOYSTICK CONSTANTS ==================== - +/// Button matrix geometry (rows). pub const BUTTON_ROWS: usize = 5; +/// Button matrix geometry (columns). pub const BUTTON_COLS: usize = 5; +/// Total number of matrix buttons. pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS; +/// ADC raw minimum (12‑bit). pub const ADC_MIN: u16 = 0; +/// ADC raw maximum (12‑bit). pub const ADC_MAX: u16 = 4095; +/// Logical axis center. pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2; +/// Number of physical gimbal axes. pub const NBR_OF_GIMBAL_AXIS: usize = 4; +/// Debounce threshold (in scans) for the matrix. pub const DEBOUNCE: u8 = 10; +/// Bytes reserved in EEPROM for calibration data + gimbal mode. pub const EEPROM_DATA_LENGTH: usize = 25; // ==================== GPIO PIN DEFINITIONS ==================== +/// Logical mapping between board functions and GPIO numbers. pub mod pins { /// Extra buttons (TX/RX pins) pub const LEFT_EXTRA_BUTTON_PIN: u8 = 1; @@ -59,7 +67,7 @@ pub mod pins { } // ==================== I2C CONFIGURATION ==================== - +/// I2C frequency and system clock helpers for the EEPROM bus. pub mod i2c { use fugit::{Rate, RateExtU32}; @@ -74,23 +82,23 @@ pub mod i2c { } // ==================== TIMER INTERVALS ==================== - +/// Cadences for periodic firmware tasks. pub mod timers { - /// Status LED update interval (250ms) + /// Status LED update interval (ms). pub const STATUS_LED_INTERVAL_MS: u32 = 250; - /// Scan timer interval (200us) + /// Button matrix scan interval (µs). 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; - /// USB update interval (10ms) + /// USB HID report interval (ms). pub const USB_UPDATE_INTERVAL_MS: u32 = 10; } // ==================== USB DEVICE CONFIGURATION ==================== - +/// USB string descriptors. pub mod usb { pub const MANUFACTURER: &str = "CMtec"; pub const PRODUCT: &str = "CMDR Joystick 25"; @@ -99,8 +107,8 @@ pub mod usb { // ==================== PIN ACCESS MACROS ==================== -/// Macro to access GPIO pins using hardware constants -/// This eliminates hardcoded pin numbers in main.rs and ensures constants are used +/// Macro to access typed GPIO pins using board constants. +/// Avoids scattering raw GPIO numbers; each arm references the constant it maps. #[macro_export] macro_rules! get_pin { ($pins:expr, left_extra_button) => {{ diff --git a/rp2040/src/lib.rs b/rp2040/src/lib.rs index 36a1a8d..39b9ee3 100644 --- a/rp2040/src/lib.rs +++ b/rp2040/src/lib.rs @@ -1,25 +1,42 @@ #![cfg_attr(not(feature = "std"), no_std)] -pub mod axis; // include axis management module -pub mod mapping; // include button mapping module for buttons dependency -pub mod button_matrix; // include button matrix module for buttons dependency -pub mod buttons; // include button management module -pub mod calibration; // include calibration management module -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 +//! CMDR Joystick 25 firmware library for RP2040. +//! +//! This crate provides the reusable building blocks that power the main +//! firmware: axis processing, button handling, calibration and storage, USB +//! HID reporting, and hardware/status abstractions. -// 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 calibration::CalibrationManager; 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 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_MAX: u16 = 4095; pub const AXIS_CENTER: u16 = ADC_MAX / 2; diff --git a/rp2040/src/main.rs b/rp2040/src/main.rs index 8d796f5..89fc742 100644 --- a/rp2040/src/main.rs +++ b/rp2040/src/main.rs @@ -1,54 +1,24 @@ -//! # CMtec CMDR Joystick 25 - Main Firmware +//! CMDR Joystick 25 – RP2040 main firmware //! -//! **Project:** CMtec CMDR joystick 25 -//! **Date:** 2023-08-01 -//! **Author:** Christoffer Martinsson -//! **Email:** cm@cmtec.se -//! **License:** Please refer to LICENSE in root directory +//! Overview +//! - 4 gimbal axes (LX, LY, RX, RY) with smoothing, calibration and expo +//! - 2 virtual axes (RY/RZ) driven by buttons with direction compensation +//! - 5x5 button matrix + 2 extra buttons, with debounce and short/long press +//! - 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 -//! USB HID joystick controller built on the Raspberry Pi RP2040 microcontroller. The firmware -//! provides advanced features including: +//! Modes: Normal, Calibration, Throttle Hold, Virtual Throttle, Bootloader //! -//! - **4-axis gimbal control** (Left X/Y, Right X/Y) with M10/M7 hardware support -//! - **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 +//! Timing: scan 200 µs, process 1200 µs, USB 10 ms, LED 250 ms #![no_std] #![no_main] @@ -123,10 +93,7 @@ use hardware::DEBOUNCE; #[cfg(not(test))] #[rp2040_hal::entry] fn main() -> ! { - // # Hardware Initialization Phase - // - // Initialize all RP2040 peripheral singleton objects and configure the hardware - // subsystems for joystick operation. + // Hardware initialization and peripheral setup for joystick operation // Acquire exclusive access to RP2040 peripherals let mut pac = pac::Peripherals::take().unwrap(); @@ -172,11 +139,7 @@ fn main() -> ! { let i2c_address = SlaveAddr::Alternative(false, false, false); let mut eeprom = Eeprom24x::new_24x32(i2c, i2c_address); - // # ADC Configuration - // - // 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. + // ADC configuration: prepare 12-bit ADC channels for all four gimbal axes // Initialize 12-bit ADC with 4 channels for gimbal axes 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 right_extra_button = get_pin!(pins, right_extra_button).into_pull_up_input(); - // # Status LED Initialization - // - // 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. + // Status LED initialization: WS2812 via PIO for runtime status indication // Initialize WS2812 status LED using PIO state machine 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()); - // # Bootloader Entry Check - // - // 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. + // Bootloader entry check: early matrix scan; hold front‑left‑lower to enter USB mass‑storage bootloader // Scan button matrix multiple times to ensure stable debounced readings 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); } - // # Timer Configuration - // - // 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) + // Timer configuration: cadence for LED updates, scans, processing and USB // Initialize hardware timer peripheral let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks); @@ -290,11 +238,7 @@ fn main() -> ! { let mut button_manager = ButtonManager::new(); let mut calibration_manager = CalibrationManager::new(); - // # Signal Processing Initialization - // - // 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. + // Signal processing: expo LUTs and smoothing filters // Create exponential curve lookup tables (avoids floating-point math in real-time) let expo_lut = ExpoLUT::new(0.3); @@ -303,14 +247,7 @@ fn main() -> ! { // Initialize digital smoothing filters for each gimbal axis let mut smoother = AxisManager::create_smoothers(); - // # USB HID Configuration - // - // 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 + // USB HID configuration (full‑speed joystick class) // Initialize USB bus allocator for RP2040 let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new( @@ -334,11 +271,7 @@ fn main() -> ! { .unwrap() .build(); - // # Calibration Data Initialization - // - // 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. + // Calibration data initialization: load axis calibration and gimbal mode from EEPROM // Load calibration data from EEPROM using CalibrationManager 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); loop { - // # Main Control Loop - // - // 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 + // Main control loop: poll USB, scan inputs, process data, send reports // Handle USB device polling and maintain connection state if usb_dev.poll(&mut [&mut usb_hid_joystick]) { diff --git a/rp2040/src/mapping.rs b/rp2040/src/mapping.rs index 241ab38..a934c95 100644 --- a/rp2040/src/mapping.rs +++ b/rp2040/src/mapping.rs @@ -1,5 +1,13 @@ -// TODO rename to mapping.rs -//! Button configuration and USB mapping for CMDR Joystick 25 +//! Button-to-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: // --------------------------------------------------------------- @@ -86,7 +94,12 @@ pub const USB_HAT_LEFT: usize = 36; 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]) { buttons[BUTTON_FRONT_LEFT_LOWER].usb_button = USB_BUTTON_29; 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_RIGHT_EXTRA].usb_button = USB_BUTTON_31; } - diff --git a/rp2040/src/status.rs b/rp2040/src/status.rs index 7b76806..f54d92a 100644 --- a/rp2040/src/status.rs +++ b/rp2040/src/status.rs @@ -1,8 +1,8 @@ -//! Project: CMtec CMDR joystick 25 -//! Date: 2025-09-13 -//! Author: Christoffer Martinsson -//! Email: cm@cmtec.se -//! License: Please refer to LICENSE in root directory +//! WS2812 status LED driver for CMDR Joystick 25 +//! +//! Provides a small helper around `ws2812_pio` to display firmware state on a +//! single WS2812 RGB LED using RP2040 PIO. The driver offers simple modes +//! (solid/flash) and a `SystemState` facade used by main. use rp2040_hal::{ gpio::AnyPin, @@ -11,7 +11,7 @@ use rp2040_hal::{ use smart_leds::{RGB8, SmartLedsWrite}; use ws2812_pio::Ws2812Direct; -/// Status LED modes with clear semantic meaning +/// Status LED modes with clear semantics. #[allow(dead_code)] #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum StatusMode { @@ -27,7 +27,7 @@ pub enum StatusMode { Bootloader = 9, } -/// System state for LED status indication +/// Aggregate system state for LED status indication. #[derive(Clone, Copy)] pub struct SystemState { pub usb_active: bool, @@ -36,7 +36,7 @@ pub struct SystemState { pub vt_enable: bool, } -/// Color definitions for different status modes +/// Color definitions for different status modes. const LED_COLORS: [RGB8; 10] = [ RGB8 { r: 0, g: 0, b: 0 }, // Off 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) ]; -/// Improved Status LED driver with self-contained state management +/// Status LED driver with self-contained state management. pub struct StatusLed where I: AnyPin, @@ -69,7 +69,7 @@ where P: PIOExt, SM: StateMachineIndex, { - /// Creates a new StatusLed instance + /// Create a new StatusLed instance. /// /// # Arguments /// * `pin` - PIO pin for WS2812 LED @@ -91,12 +91,7 @@ where } } - /// Update LED based on system state - main interface for main.rs - /// - /// 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 + /// Update LED based on system state and current time (ms). pub fn update_from_system_state(&mut self, system_state: SystemState, current_time: u32) { let desired_mode = if system_state.calibration_active { StatusMode::ActivityFlash @@ -115,10 +110,7 @@ where self.set_mode(desired_mode, current_time); } - /// Set LED mode directly - /// # Arguments - /// * `mode` - Desired status mode - /// * `current_time` - Current time in milliseconds + /// Set LED mode directly (explicit override). pub fn set_mode(&mut self, mode: StatusMode, current_time: u32) { // Force update if mode changed let force_update = mode != self.current_mode; @@ -127,10 +119,7 @@ where self.update_display(current_time, force_update); } - /// Update LED display based on current mode and timing - /// Called periodically to handle flashing - /// # Arguments - /// * `current_time` - Current time in milliseconds + /// Periodic update for flashing behavior at roughly ~500 ms intervals. pub fn update_display(&mut self, current_time: u32, force_update: bool) { 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)] pub fn get_mode(&self) -> StatusMode { 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 { match self.last_update_time { 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) { let _ = self.ws2812_direct.write([color].iter().copied()); } @@ -200,11 +189,9 @@ where P: PIOExt, SM: StateMachineIndex, { - /// Legacy interface for compatibility - direct mode update - /// This maintains compatibility with existing direct update calls + /// Legacy interface for compatibility – direct mode update. pub fn update(&mut self, mode: StatusMode) { // Use a dummy time for immediate updates self.set_mode(mode, 0); } } - diff --git a/rp2040/src/storage.rs b/rp2040/src/storage.rs index 9ab8f72..21dad8f 100644 --- a/rp2040/src/storage.rs +++ b/rp2040/src/storage.rs @@ -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; // ==================== EEPROM DATA LAYOUT ==================== - - -/// EEPROM address for gimbal mode storage (original read from address 25) +/// EEPROM address for gimbal mode storage (original layout uses 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 // ==================== ERROR TYPES ==================== - +/// Errors returned by storage helpers. #[derive(Debug)] pub enum StorageError { ReadError, @@ -24,8 +24,8 @@ pub enum StorageError { // ==================== CORE FUNCTIONS ==================== -/// Read calibration data for a single axis from EEPROM -/// Returns (min, max, center) values as u16 +/// Read calibration data for a single axis from EEPROM. +/// Returns (min, max, center) values as u16. pub fn read_axis_calibration( read_byte_fn: &mut dyn FnMut(u32) -> Result, axis_index: usize @@ -40,7 +40,7 @@ pub fn read_axis_calibration( Ok((min, max, center)) } -/// Read gimbal mode from EEPROM +/// Read gimbal mode from EEPROM. pub fn read_gimbal_mode( read_byte_fn: &mut dyn FnMut(u32) -> Result ) -> Result { @@ -48,7 +48,7 @@ pub fn read_gimbal_mode( .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)] pub fn write_calibration_data( write_page_fn: &mut dyn FnMut(u32, &[u8]) -> Result<(), ()>, @@ -80,7 +80,7 @@ pub fn write_calibration_data( // ==================== 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( read_byte_fn: &mut dyn FnMut(u32) -> Result, low_addr: u32, @@ -216,4 +216,4 @@ mod tests { assert_eq!(max, 4000); assert_eq!(center, 2050); } -} \ No newline at end of file +} diff --git a/rp2040/src/usb_joystick_device.rs b/rp2040/src/usb_joystick_device.rs index 88c65cc..bb7a7f2 100644 --- a/rp2040/src/usb_joystick_device.rs +++ b/rp2040/src/usb_joystick_device.rs @@ -111,6 +111,7 @@ pub const JOYSTICK_DESCRIPTOR: &[u8] = &[ ]; #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] +/// HID report payload matching `JOYSTICK_DESCRIPTOR`. pub struct JoystickReport { pub x: i16, // 16bit pub y: i16, // 16bit @@ -128,10 +129,11 @@ pub struct Joystick<'a, B: UsbBus> { } impl Joystick<'_, B> { + /// Serialize and send a joystick report matching the HID descriptor. pub fn write_report(&mut self, report: &JoystickReport) -> Result<(), UsbHidError> { 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[1] = (report.x >> 8) as u8; data[2] = report.y as u8; diff --git a/rp2040/src/usb_report.rs b/rp2040/src/usb_report.rs index b6f14e2..e0f5c47 100644 --- a/rp2040/src/usb_report.rs +++ b/rp2040/src/usb_report.rs @@ -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. -//! Provides functionality for virtual axis control, HAT switch processing, and button mapping. +//! Converts processed axis values and button states into a `JoystickReport` +//! 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::buttons::{Button, TOTAL_BUTTONS}; @@ -11,7 +12,7 @@ use crate::usb_joystick_device::JoystickReport; // ==================== 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). /// This is specifically for USB joystick axis reporting. @@ -34,7 +35,7 @@ pub fn axis_12bit_to_i16(val: u16) -> 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 /// * `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 hat: u8 = 8; // Hat center position - // Virtual axis control: Disables z axis and uses right gimbal X axis to control - // the slider axis. Values from center stick to max or min will be recalculated to min to max. + // Virtual throttle mode: + // - Disable Z axis + // - Map right‑X to Slider with symmetric behavior around center if *vt_enable { if axis[GIMBAL_AXIS_RIGHT_X].value >= AXIS_CENTER { slider = axis_12bit_to_i16(remap( @@ -88,7 +90,7 @@ pub fn get_joystick_report( 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; for key in matrix_keys.iter_mut() { 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() { key.usb_changed = false; }