Code commenting/documentation
This commit is contained in:
parent
6cf9bde7e0
commit
b98614fe8e
92
README.md
92
README.md
@ -1,6 +1,8 @@
|
||||
# CMDR Joystick 25
|
||||
|
||||
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)
|
||||
|
||||
- 
|
||||
- 
|
||||
- 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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<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 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<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 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
|
||||
}
|
||||
|
||||
@ -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<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 (non‑matrix) 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 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<F>(
|
||||
&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<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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<T: PartialOrd>(value: T, out_min: T, out_max: T) -> T {
|
||||
if value < out_min {
|
||||
out_min
|
||||
|
||||
@ -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) => {{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<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 little‑endian (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (little‑endian i16).
|
||||
data[0] = report.x as u8;
|
||||
data[1] = (report.x >> 8) as u8;
|
||||
data[2] = report.y as u8;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
//! USB HID Report Generation for CMDR Joystick 25
|
||||
//! USB HID report generation for CMDR Joystick 25
|
||||
//!
|
||||
//! Handles the conversion of axis values and button states into USB HID joystick reports.
|
||||
//! 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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user