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
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
@ -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)
- Halleffect gimbals (FrSky M7/M10)
- USB HID joystick device
- 7 axes: X, Y, Z, Rx, Ry, Rz, Slider
- 32 buttons
- 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
@ -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 toplevel `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 poweron): hold the frontleftlower button
- Infirmware combo: Frontleftlower + Topleftmode + Toprightmode
## 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 Frontleftupper + Topleftmode + Toprightmode.
3. Move both gimbals through full travel (all corners).
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"
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

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,
//! ADC reading, calibration, and gimbal mode compensation.
//! Responsibilities
//! - 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::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)
/// Throttlehold 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 throttlehold to the leftY 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 throttlehold state for leftY 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 throttlehold value for leftY 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 prehold 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,

View File

@ -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 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 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<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> {
/// 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 pullups)
/// - `cols`: array of column pins (pushpull outputs)
/// - `debounce`: number of consecutive scans a change must persist before it is accepted
pub fn new(
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
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.
/// 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 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) {
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
}

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,
//! 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 (nonmatrix) buttons
pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2;
// ==================== BUTTON STRUCT ====================
@ -33,6 +38,7 @@ pub struct Button {
}
// ==================== SPECIAL ACTIONS ====================
/// Highlevel 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<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS>) {
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
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)
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<bool> {
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 readonly 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: autorelease 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
// Autorelease 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.
}
}
}

View File

@ -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 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::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<F>(
&mut self,
axes: &[GimbalAxis; NBR_OF_GIMBAL_AXIS],
@ -133,10 +143,9 @@ impl CalibrationManager {
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.
/// Uses factory defaults if EEPROM read fails.
/// Updates the provided axes with loaded values; retains defaults on error.
pub fn load_axis_calibration<F>(
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<F>(read_fn: &mut F) -> u8
where
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(
&self,
axes: &mut [GimbalAxis; NBR_OF_GIMBAL_AXIS],
@ -529,4 +537,3 @@ mod tests {
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 core::cmp::PartialOrd;
use libm::powf;
/// Precomputed exponential lookup table (12bit 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 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] {
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<T: PartialOrd>(value: T, out_min: T, out_max: T) -> T {
if value < out_min {
out_min

View File

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

View File

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

View File

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

View File

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

View File

@ -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<P, SM, I>
where
I: AnyPin<Function = P::PinFunction>,
@ -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);
}
}

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;
// ==================== 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<u8, ()>,
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<u8, ()>
) -> Result<u8, StorageError> {
@ -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 littleendian (low then high byte) format.
fn read_u16_with_closure(
read_byte_fn: &mut dyn FnMut(u32) -> Result<u8, ()>,
low_addr: u32,
@ -216,4 +216,4 @@ mod tests {
assert_eq!(max, 4000);
assert_eq!(center, 2050);
}
}
}

View File

@ -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<B: UsbBus> 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 (littleendian i16).
data[0] = report.x as u8;
data[1] = (report.x >> 8) 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.
//! 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 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).
/// 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 rightX 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 perbutton USB change flags for the next iteration
for key in matrix_keys.iter_mut() {
key.usb_changed = false;
}