Refactored code
This commit is contained in:
parent
adc69a7f40
commit
ce714ad71d
30
Justfile
Normal file
30
Justfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
set export := true
|
||||||
|
|
||||||
|
default: check
|
||||||
|
|
||||||
|
check:
|
||||||
|
cd rp2040 && cargo check --target thumbv6m-none-eabi
|
||||||
|
|
||||||
|
test:
|
||||||
|
cd rp2040 && cargo test --lib --target x86_64-unknown-linux-gnu --features std
|
||||||
|
|
||||||
|
build-uf2:
|
||||||
|
cd rp2040 && cargo build --release --target thumbv6m-none-eabi
|
||||||
|
cd rp2040 && cargo objcopy --release --target thumbv6m-none-eabi -- -O binary target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin
|
||||||
|
cd rp2040 && python3 uf2conv.py target/thumbv6m-none-eabi/release/cmdr-joystick-25.bin --base 0x10000000 --family 0xe48bff56 --convert --output target/firmware.uf2
|
||||||
|
|
||||||
|
clean:
|
||||||
|
cargo clean --manifest-path rp2040/Cargo.toml
|
||||||
|
|
||||||
|
flash mount="" timeout="10":
|
||||||
|
@just build-uf2
|
||||||
|
MOUNT="{{mount}}" python3 tools/copy_uf2.py --source rp2040/target/firmware.uf2 --timeout {{timeout}}
|
||||||
|
|
||||||
|
flash-ssh target mount="/Volumes/RPI-RP2" key="" port="22":
|
||||||
|
@just build-uf2
|
||||||
|
target="{{target}}"
|
||||||
|
mount="{{mount}}"
|
||||||
|
key_arg=""
|
||||||
|
if [ -n "{{key}}" ]; then key_arg="-i {{key}}"; fi
|
||||||
|
ssh $key_arg -p {{port}} "$target" "mkdir -p \"$mount\""
|
||||||
|
scp $key_arg -P {{port}} rp2040/target/firmware.uf2 "$target:$mount/"
|
||||||
195
rp2040/src/board.rs
Normal file
195
rp2040/src/board.rs
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
use crate::button_matrix::{ButtonMatrix, MatrixPins};
|
||||||
|
use crate::hardware::{self, BoardPins};
|
||||||
|
use crate::status::StatusLed;
|
||||||
|
use cortex_m::delay::Delay;
|
||||||
|
use cortex_m::interrupt;
|
||||||
|
use eeprom24x::{addr_size, page_size, unique_serial, Eeprom24x};
|
||||||
|
use rp2040_hal::adc::{Adc, AdcPin};
|
||||||
|
use rp2040_hal::clocks::Clock;
|
||||||
|
use rp2040_hal::gpio::{self, Pin, PullNone};
|
||||||
|
use rp2040_hal::i2c::I2C;
|
||||||
|
use rp2040_hal::pac;
|
||||||
|
use rp2040_hal::pio::PIOExt;
|
||||||
|
use rp2040_hal::sio::Sio;
|
||||||
|
use rp2040_hal::timer::Timer;
|
||||||
|
use rp2040_hal::watchdog::Watchdog;
|
||||||
|
use rp2040_hal::{clocks::init_clocks_and_plls, gpio::FunctionSioInput};
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
use usb_device::class_prelude::UsbBusAllocator;
|
||||||
|
|
||||||
|
pub type JoystickMatrix = ButtonMatrix<
|
||||||
|
MatrixPins<{ hardware::BUTTON_ROWS }, { hardware::BUTTON_COLS }>,
|
||||||
|
{ hardware::BUTTON_ROWS },
|
||||||
|
{ hardware::BUTTON_COLS },
|
||||||
|
{ hardware::NUMBER_OF_BUTTONS },
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub type JoystickStatusLed = StatusLed<pac::PIO0, rp2040_hal::pio::SM0, hardware::StatusLedPin>;
|
||||||
|
|
||||||
|
type BoardI2c = I2C<pac::I2C1, (hardware::I2cSdaPin, hardware::I2cSclPin)>;
|
||||||
|
type BoardEeprom = Eeprom24x<BoardI2c, page_size::B32, addr_size::TwoBytes, unique_serial::No>;
|
||||||
|
|
||||||
|
pub struct AxisAnalogPins {
|
||||||
|
pub left_x: AdcPin<Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>>,
|
||||||
|
pub left_y: AdcPin<Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>>,
|
||||||
|
pub right_x: AdcPin<Pin<gpio::bank0::Gpio27, FunctionSioInput, PullNone>>,
|
||||||
|
pub right_y: AdcPin<Pin<gpio::bank0::Gpio26, FunctionSioInput, PullNone>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxisAnalogPins {
|
||||||
|
fn new(inputs: hardware::AxisInputs) -> Self {
|
||||||
|
let left_x = AdcPin::new(inputs.left_x).unwrap();
|
||||||
|
let left_y = AdcPin::new(inputs.left_y).unwrap();
|
||||||
|
let right_x = AdcPin::new(inputs.right_x).unwrap();
|
||||||
|
let right_y = AdcPin::new(inputs.right_y).unwrap();
|
||||||
|
Self {
|
||||||
|
left_x,
|
||||||
|
left_y,
|
||||||
|
right_x,
|
||||||
|
right_y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Board {
|
||||||
|
button_matrix: JoystickMatrix,
|
||||||
|
status_led: JoystickStatusLed,
|
||||||
|
delay: Delay,
|
||||||
|
timer: Timer,
|
||||||
|
adc: Adc,
|
||||||
|
axis_pins: AxisAnalogPins,
|
||||||
|
left_extra_button: hardware::ExtraButtonPin,
|
||||||
|
right_extra_button: hardware::ExtraButtonPin,
|
||||||
|
eeprom: BoardEeprom,
|
||||||
|
usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BoardParts {
|
||||||
|
pub button_matrix: JoystickMatrix,
|
||||||
|
pub status_led: JoystickStatusLed,
|
||||||
|
pub delay: Delay,
|
||||||
|
pub timer: Timer,
|
||||||
|
pub adc: Adc,
|
||||||
|
pub axis_pins: AxisAnalogPins,
|
||||||
|
pub left_extra_button: hardware::ExtraButtonPin,
|
||||||
|
pub right_extra_button: hardware::ExtraButtonPin,
|
||||||
|
pub eeprom: BoardEeprom,
|
||||||
|
pub usb_bus: &'static UsbBusAllocator<rp2040_hal::usb::UsbBus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut pac = pac::Peripherals::take().unwrap();
|
||||||
|
let core = pac::CorePeripherals::take().unwrap();
|
||||||
|
|
||||||
|
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
||||||
|
let clocks = init_clocks_and_plls(
|
||||||
|
hardware::XTAL_FREQ_HZ,
|
||||||
|
pac.XOSC,
|
||||||
|
pac.CLOCKS,
|
||||||
|
pac.PLL_SYS,
|
||||||
|
pac.PLL_USB,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
&mut watchdog,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let sio = Sio::new(pac.SIO);
|
||||||
|
let raw_pins = gpio::Pins::new(
|
||||||
|
pac.IO_BANK0,
|
||||||
|
pac.PADS_BANK0,
|
||||||
|
sio.gpio_bank0,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
);
|
||||||
|
let pins = BoardPins::new(raw_pins);
|
||||||
|
|
||||||
|
let matrix_pins = MatrixPins::new(pins.matrix_rows, pins.matrix_cols);
|
||||||
|
let mut button_matrix = ButtonMatrix::new(
|
||||||
|
matrix_pins,
|
||||||
|
hardware::MATRIX_DEBOUNCE_SCANS,
|
||||||
|
hardware::MIN_PRESS_SPACING_SCANS,
|
||||||
|
);
|
||||||
|
button_matrix.init_pins();
|
||||||
|
|
||||||
|
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
|
||||||
|
let status_led = StatusLed::new(
|
||||||
|
pins.status_led,
|
||||||
|
&mut pio,
|
||||||
|
sm0,
|
||||||
|
clocks.peripheral_clock.freq(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
|
||||||
|
let delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
||||||
|
|
||||||
|
let i2c = I2C::i2c1(
|
||||||
|
pac.I2C1,
|
||||||
|
pins.i2c_sda,
|
||||||
|
pins.i2c_scl,
|
||||||
|
hardware::i2c::frequency(),
|
||||||
|
&mut pac.RESETS,
|
||||||
|
hardware::i2c::system_clock(),
|
||||||
|
);
|
||||||
|
let eeprom = Eeprom24x::new_24x32(i2c, hardware::i2c::EEPROM_ADDRESS);
|
||||||
|
|
||||||
|
let adc = Adc::new(pac.ADC, &mut pac.RESETS);
|
||||||
|
let axis_pins = AxisAnalogPins::new(pins.axis_inputs);
|
||||||
|
|
||||||
|
let usb_bus = usb_allocator(
|
||||||
|
pac.USBCTRL_REGS,
|
||||||
|
pac.USBCTRL_DPRAM,
|
||||||
|
clocks.usb_clock,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
button_matrix,
|
||||||
|
status_led,
|
||||||
|
delay,
|
||||||
|
timer,
|
||||||
|
adc,
|
||||||
|
axis_pins,
|
||||||
|
left_extra_button: pins.left_extra_button,
|
||||||
|
right_extra_button: pins.right_extra_button,
|
||||||
|
eeprom,
|
||||||
|
usb_bus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_parts(self) -> BoardParts {
|
||||||
|
BoardParts {
|
||||||
|
button_matrix: self.button_matrix,
|
||||||
|
status_led: self.status_led,
|
||||||
|
delay: self.delay,
|
||||||
|
timer: self.timer,
|
||||||
|
adc: self.adc,
|
||||||
|
axis_pins: self.axis_pins,
|
||||||
|
left_extra_button: self.left_extra_button,
|
||||||
|
right_extra_button: self.right_extra_button,
|
||||||
|
eeprom: self.eeprom,
|
||||||
|
usb_bus: self.usb_bus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usb_allocator(
|
||||||
|
usbctrl_regs: pac::USBCTRL_REGS,
|
||||||
|
usbctrl_dpram: pac::USBCTRL_DPRAM,
|
||||||
|
usb_clock: rp2040_hal::clocks::UsbClock,
|
||||||
|
resets: &mut pac::RESETS,
|
||||||
|
) -> &'static UsbBusAllocator<rp2040_hal::usb::UsbBus> {
|
||||||
|
static USB_BUS: StaticCell<UsbBusAllocator<rp2040_hal::usb::UsbBus>> = StaticCell::new();
|
||||||
|
|
||||||
|
interrupt::free(|_| {
|
||||||
|
USB_BUS.init_with(|| {
|
||||||
|
UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
|
||||||
|
usbctrl_regs,
|
||||||
|
usbctrl_dpram,
|
||||||
|
usb_clock,
|
||||||
|
true,
|
||||||
|
resets,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
30
rp2040/src/bootloader.rs
Normal file
30
rp2040/src/bootloader.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//! Bootloader helpers shared between power-on checks and runtime button chords.
|
||||||
|
|
||||||
|
use crate::status::{StatusLed, StatusMode};
|
||||||
|
use cortex_m::asm;
|
||||||
|
use rp2040_hal::gpio::AnyPin;
|
||||||
|
use rp2040_hal::pio::{PIOExt, StateMachineIndex};
|
||||||
|
|
||||||
|
/// Returns `true` when the power-on matrix snapshot requests bootloader entry.
|
||||||
|
///
|
||||||
|
/// The original firmware required the front-left-lower button to be held during
|
||||||
|
/// power-up to jump straight into ROM boot.
|
||||||
|
pub fn startup_requested(buttons: &[bool; crate::hardware::NUMBER_OF_BUTTONS]) -> bool {
|
||||||
|
buttons[crate::mapping::BUTTON_FRONT_LEFT_LOWER]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts the RP2040 into the ROM bootloader after updating the status LED.
|
||||||
|
pub fn enter<P, SM, I>(status_led: &mut StatusLed<P, SM, I>) -> !
|
||||||
|
where
|
||||||
|
P: PIOExt,
|
||||||
|
SM: StateMachineIndex,
|
||||||
|
I: AnyPin<Function = P::PinFunction>,
|
||||||
|
{
|
||||||
|
status_led.update(StatusMode::Bootloader);
|
||||||
|
let gpio_activity_pin_mask: u32 = 0;
|
||||||
|
let disable_interface_mask: u32 = 0;
|
||||||
|
rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask);
|
||||||
|
loop {
|
||||||
|
asm::nop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,140 +1,166 @@
|
|||||||
//! Button matrix scanner for CMDR Joystick 25
|
//! Button matrix scanner for CMDR Joystick 25.
|
||||||
//!
|
//!
|
||||||
//! Scans a row/column matrix and produces a debounced boolean state for each
|
//! Mirrors the refactor performed for the keyboard firmware: the matrix owns
|
||||||
//! button. Designed for small matrices on microcontrollers where timing is
|
//! concrete pins, exposes a small `MatrixPinAccess` trait, and keeps the
|
||||||
//! deterministic and GPIO is plentiful.
|
//! debouncing + minimum press spacing behaviour identical to the original
|
||||||
//!
|
//! joystick implementation.
|
||||||
//! - 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;
|
use cortex_m::delay::Delay;
|
||||||
use embedded_hal::digital::{InputPin, OutputPin};
|
use embedded_hal::digital::{InputPin, OutputPin};
|
||||||
|
use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};
|
||||||
|
|
||||||
/// Button matrix driver
|
/// Abstraction over the matrix pins so the scanner can work with either the
|
||||||
///
|
/// concrete RP2040 pins or test doubles.
|
||||||
/// Generics
|
pub trait MatrixPinAccess<const ROWS: usize, const COLS: usize> {
|
||||||
/// - `R`: number of rows
|
fn init_columns(&mut self);
|
||||||
/// - `C`: number of columns
|
fn set_column_low(&mut self, column: usize);
|
||||||
/// - `N`: total number of buttons (usually `R * C`)
|
fn set_column_high(&mut self, column: usize);
|
||||||
///
|
fn read_row(&mut self, row: usize) -> bool;
|
||||||
/// Example
|
}
|
||||||
/// ```ignore
|
|
||||||
/// // 4 rows, 6 columns, 24 buttons, 5-scan debounce
|
/// Concrete matrix pins backed by RP2040 GPIO using dynamic pin IDs.
|
||||||
/// let mut matrix: ButtonMatrix<4, 6, 24> = ButtonMatrix::new(row_pins, col_pins, 5);
|
type RowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
|
||||||
/// matrix.init_pins();
|
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
|
||||||
/// loop {
|
|
||||||
/// matrix.scan_matrix(&mut delay);
|
pub struct MatrixPins<const ROWS: usize, const COLS: usize> {
|
||||||
/// let states = matrix.buttons_pressed();
|
rows: [RowPin; ROWS],
|
||||||
/// // use `states`
|
cols: [ColPin; COLS],
|
||||||
/// }
|
}
|
||||||
/// ```
|
|
||||||
pub struct ButtonMatrix<'a, const R: usize, const C: usize, const N: usize> {
|
impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
|
||||||
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self {
|
||||||
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
|
Self { rows, cols }
|
||||||
pressed: [bool; N],
|
}
|
||||||
debounce: u8,
|
}
|
||||||
debounce_counter: [u8; N],
|
|
||||||
// Anti-bounce protection: minimum time between same-button presses
|
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
|
||||||
last_press_scan: [u32; N],
|
fn init_columns(&mut self) {
|
||||||
|
for column in self.cols.iter_mut() {
|
||||||
|
let _ = column.set_high();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_column_low(&mut self, column: usize) {
|
||||||
|
let _ = self.cols[column].set_low();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_column_high(&mut self, column: usize) {
|
||||||
|
let _ = self.cols[column].set_high();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_row(&mut self, row: usize) -> bool {
|
||||||
|
self.rows[row].is_low().unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Row/column scanned button matrix driver with debounce counters and minimum
|
||||||
|
/// spacing between subsequent presses of the same key.
|
||||||
|
pub struct ButtonMatrix<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
|
||||||
|
where
|
||||||
|
P: MatrixPinAccess<ROWS, COLS>,
|
||||||
|
{
|
||||||
|
pins: P,
|
||||||
|
pressed: [bool; BUTTONS],
|
||||||
|
debounce_threshold: u8,
|
||||||
|
debounce_counter: [u8; BUTTONS],
|
||||||
|
last_press_scan: [u32; BUTTONS],
|
||||||
|
min_press_gap_scans: u32,
|
||||||
scan_counter: u32,
|
scan_counter: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
|
impl<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
|
||||||
/// Creates a new button matrix.
|
ButtonMatrix<P, ROWS, COLS, BUTTONS>
|
||||||
///
|
where
|
||||||
/// Arguments
|
P: MatrixPinAccess<ROWS, COLS>,
|
||||||
/// - `rows`: array of row pins (inputs with pull‑ups)
|
{
|
||||||
/// - `cols`: array of column pins (push‑pull outputs)
|
pub fn new(pins: P, debounce_threshold: u8, min_press_gap_scans: u32) -> Self {
|
||||||
/// - `debounce`: number of consecutive scans a change must persist before it is accepted
|
debug_assert_eq!(BUTTONS, ROWS * COLS);
|
||||||
pub fn new(
|
|
||||||
rows: &'a mut [&'a mut dyn InputPin<Error = Infallible>; R],
|
|
||||||
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
|
|
||||||
debounce: u8,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
rows,
|
pins,
|
||||||
cols,
|
pressed: [false; BUTTONS],
|
||||||
pressed: [false; N],
|
debounce_threshold,
|
||||||
debounce,
|
debounce_counter: [0; BUTTONS],
|
||||||
debounce_counter: [0; N],
|
last_press_scan: [0; BUTTONS],
|
||||||
last_press_scan: [0; N],
|
min_press_gap_scans,
|
||||||
scan_counter: 0,
|
scan_counter: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the matrix GPIOs (set all columns high).
|
|
||||||
///
|
|
||||||
/// Call once before the first scan.
|
|
||||||
pub fn init_pins(&mut self) {
|
pub fn init_pins(&mut self) {
|
||||||
for col in self.cols.iter_mut() {
|
self.pins.init_columns();
|
||||||
col.set_high().unwrap();
|
}
|
||||||
|
|
||||||
|
pub fn prime(&mut self, delay: &mut Delay, passes: usize) {
|
||||||
|
for _ in 0..passes {
|
||||||
|
self.scan_matrix(delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scan the matrix and update each button's debounced state.
|
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
///
|
|
||||||
/// Arguments
|
|
||||||
/// - `delay`: short delay implementation used to let signals settle between columns
|
|
||||||
pub fn scan_matrix(&mut self, delay: &mut Delay) {
|
pub fn scan_matrix(&mut self, delay: &mut Delay) {
|
||||||
self.scan_counter = self.scan_counter.wrapping_add(1);
|
self.scan_counter = self.scan_counter.wrapping_add(1);
|
||||||
for col_index in 0..self.cols.len() {
|
for column in 0..COLS {
|
||||||
self.cols[col_index].set_low().unwrap();
|
self.pins.set_column_low(column);
|
||||||
delay.delay_us(1);
|
delay.delay_us(1);
|
||||||
self.process_column(col_index);
|
self.process_column(column);
|
||||||
self.cols[col_index].set_high().unwrap();
|
self.pins.set_column_high(column);
|
||||||
delay.delay_us(1);
|
delay.delay_us(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a single column: drive low, sample rows, update debounce state, then release high.
|
pub fn buttons_pressed(&self) -> [bool; BUTTONS] {
|
||||||
///
|
self.pressed
|
||||||
/// 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);
|
|
||||||
let current_state = self.rows[row_index].is_low().unwrap();
|
|
||||||
|
|
||||||
if current_state == self.pressed[button_index] {
|
fn process_column(&mut self, column: usize) {
|
||||||
self.debounce_counter[button_index] = 0;
|
for row in 0..ROWS {
|
||||||
|
let index = column + (row * COLS);
|
||||||
|
let current_state = self.pins.read_row(row);
|
||||||
|
|
||||||
|
if current_state == self.pressed[index] {
|
||||||
|
self.debounce_counter[index] = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.debounce_counter[button_index] += 1;
|
self.debounce_counter[index] = self.debounce_counter[index].saturating_add(1);
|
||||||
if self.debounce_counter[button_index] >= self.debounce {
|
if self.debounce_counter[index] < self.debounce_threshold {
|
||||||
// Anti-bounce protection for press events: minimum 25 scans (5ms) between presses
|
continue;
|
||||||
if current_state {
|
}
|
||||||
// Pressing
|
|
||||||
let scans_since_last = self
|
self.debounce_counter[index] = 0;
|
||||||
.scan_counter
|
if current_state {
|
||||||
.wrapping_sub(self.last_press_scan[button_index]);
|
if self.should_register_press(index) {
|
||||||
if scans_since_last >= 25 {
|
self.pressed[index] = true;
|
||||||
// 5ms at 200μs scan rate
|
|
||||||
self.pressed[button_index] = current_state;
|
|
||||||
self.last_press_scan[button_index] = self.scan_counter;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Releasing
|
|
||||||
self.pressed[button_index] = current_state;
|
|
||||||
}
|
}
|
||||||
self.debounce_counter[button_index] = 0;
|
} else {
|
||||||
|
self.pressed[index] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a copy of the debounced pressed state for all buttons.
|
fn should_register_press(&mut self, index: usize) -> bool {
|
||||||
///
|
let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[index]);
|
||||||
/// For small `N` this copy is cheap. If needed, the API could be extended to
|
let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans;
|
||||||
/// return a reference in the future.
|
|
||||||
pub fn buttons_pressed(&mut self) -> [bool; N] {
|
if can_register {
|
||||||
self.pressed
|
self.last_press_scan[index] = self.scan_counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
can_register
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "std"))]
|
||||||
|
pub(crate) fn process_column_for_test(&mut self, column: usize) {
|
||||||
|
self.process_column(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "std"))]
|
||||||
|
pub(crate) fn set_scan_counter(&mut self, value: u32) {
|
||||||
|
self.scan_counter = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "std"))]
|
||||||
|
pub(crate) fn bump_scan_counter(&mut self) {
|
||||||
|
self.scan_counter = self.scan_counter.wrapping_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,123 +168,65 @@ impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C,
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use core::cell::Cell;
|
use core::cell::Cell;
|
||||||
use embedded_hal::digital::ErrorType;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
struct MockInputPin {
|
#[derive(Clone)]
|
||||||
state: Rc<Cell<bool>>,
|
struct MockPins {
|
||||||
|
row_state: Rc<Cell<bool>>,
|
||||||
|
column_state: Rc<Cell<bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockInputPin {
|
impl MockPins {
|
||||||
fn new(state: Rc<Cell<bool>>) -> Self {
|
fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> Self {
|
||||||
Self { state }
|
Self {
|
||||||
|
row_state,
|
||||||
|
column_state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorType for MockInputPin {
|
impl MatrixPinAccess<1, 1> for MockPins {
|
||||||
type Error = Infallible;
|
fn init_columns(&mut self) {
|
||||||
}
|
self.column_state.set(true);
|
||||||
|
|
||||||
impl InputPin for MockInputPin {
|
|
||||||
fn is_high(&mut self) -> Result<bool, Self::Error> {
|
|
||||||
Ok(!self.state.get())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_low(&mut self) -> Result<bool, Self::Error> {
|
fn set_column_low(&mut self, _column: usize) {
|
||||||
Ok(self.state.get())
|
self.column_state.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_column_high(&mut self, _column: usize) {
|
||||||
|
self.column_state.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_row(&mut self, _row: usize) -> bool {
|
||||||
|
self.row_state.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MockOutputPin {
|
fn fixture() -> (
|
||||||
state: Rc<Cell<bool>>,
|
ButtonMatrix<MockPins, 1, 1, 1>,
|
||||||
}
|
|
||||||
|
|
||||||
impl MockOutputPin {
|
|
||||||
fn new(state: Rc<Cell<bool>>) -> Self {
|
|
||||||
Self { state }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorType for MockOutputPin {
|
|
||||||
type Error = Infallible;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputPin for MockOutputPin {
|
|
||||||
fn set_high(&mut self) -> Result<(), Self::Error> {
|
|
||||||
self.state.set(true);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_low(&mut self) -> Result<(), Self::Error> {
|
|
||||||
self.state.set(false);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn matrix_fixture() -> (
|
|
||||||
ButtonMatrix<'static, 1, 1, 1>,
|
|
||||||
Rc<Cell<bool>>,
|
Rc<Cell<bool>>,
|
||||||
Rc<Cell<bool>>,
|
Rc<Cell<bool>>,
|
||||||
) {
|
) {
|
||||||
let row_state = Rc::new(Cell::new(false));
|
let row = Rc::new(Cell::new(false));
|
||||||
let col_state = Rc::new(Cell::new(false));
|
let column = Rc::new(Cell::new(true));
|
||||||
|
let pins = MockPins::new(row.clone(), column.clone());
|
||||||
let row_pin: &'static mut dyn InputPin<Error = Infallible> =
|
let matrix = ButtonMatrix::new(pins, 2, 3);
|
||||||
Box::leak(Box::new(MockInputPin::new(row_state.clone())));
|
(matrix, row, column)
|
||||||
let col_pin: &'static mut dyn OutputPin<Error = Infallible> =
|
|
||||||
Box::leak(Box::new(MockOutputPin::new(col_state.clone())));
|
|
||||||
|
|
||||||
let rows: &'static mut [&'static mut dyn InputPin<Error = Infallible>; 1] =
|
|
||||||
Box::leak(Box::new([row_pin]));
|
|
||||||
let cols: &'static mut [&'static mut dyn OutputPin<Error = Infallible>; 1] =
|
|
||||||
Box::leak(Box::new([col_pin]));
|
|
||||||
|
|
||||||
let matrix = ButtonMatrix::new(rows, cols, 15);
|
|
||||||
|
|
||||||
(matrix, row_state, col_state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn init_pins_sets_columns_high() {
|
fn debounce_requires_consecutive_scans() {
|
||||||
let (mut matrix, _row_state, col_state) = matrix_fixture();
|
let (mut matrix, row, _column) = fixture();
|
||||||
assert!(!col_state.get());
|
matrix.set_scan_counter(1);
|
||||||
matrix.init_pins();
|
|
||||||
assert!(col_state.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
row.set(true);
|
||||||
fn process_column_obeys_debounce() {
|
matrix.bump_scan_counter();
|
||||||
let (mut matrix, row_state, _col_state) = matrix_fixture();
|
matrix.process_column_for_test(0);
|
||||||
let mut states = matrix.buttons_pressed();
|
assert!(!matrix.buttons_pressed()[0]);
|
||||||
assert!(!states[0]);
|
|
||||||
|
|
||||||
// Set scan counter to start with enough history
|
matrix.bump_scan_counter();
|
||||||
matrix.scan_counter = 100;
|
matrix.process_column_for_test(0);
|
||||||
|
assert!(matrix.buttons_pressed()[0]);
|
||||||
row_state.set(true);
|
|
||||||
// Need 15 scans to register press
|
|
||||||
for _ in 0..14 {
|
|
||||||
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
||||||
matrix.process_column(0);
|
|
||||||
states = matrix.buttons_pressed();
|
|
||||||
assert!(!states[0]); // Still not pressed
|
|
||||||
}
|
|
||||||
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
||||||
matrix.process_column(0); // 15th scan
|
|
||||||
states = matrix.buttons_pressed();
|
|
||||||
assert!(states[0]); // Now pressed
|
|
||||||
|
|
||||||
row_state.set(false);
|
|
||||||
// Need 15 scans to register release
|
|
||||||
for _ in 0..14 {
|
|
||||||
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
||||||
matrix.process_column(0);
|
|
||||||
states = matrix.buttons_pressed();
|
|
||||||
assert!(states[0]); // Still pressed
|
|
||||||
}
|
|
||||||
matrix.scan_counter = matrix.scan_counter.wrapping_add(1);
|
|
||||||
matrix.process_column(0); // 15th scan
|
|
||||||
states = matrix.buttons_pressed();
|
|
||||||
assert!(!states[0]); // Now released
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
//! - Evaluate special combinations (bootloader, calibration, etc.)
|
//! - Evaluate special combinations (bootloader, calibration, etc.)
|
||||||
//! - Expose a compact state consumed by USB report generation
|
//! - Expose a compact state consumed by USB report generation
|
||||||
|
|
||||||
use crate::button_matrix::ButtonMatrix;
|
use crate::button_matrix::{ButtonMatrix, MatrixPins};
|
||||||
use crate::hardware::{AXIS_CENTER, BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS};
|
use crate::hardware::{AXIS_CENTER, BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS};
|
||||||
use crate::mapping::*;
|
use crate::mapping::*;
|
||||||
use embedded_hal::digital::InputPin;
|
use embedded_hal::digital::InputPin;
|
||||||
@ -17,6 +17,13 @@ use rp2040_hal::timer::Timer;
|
|||||||
// Total buttons including the two extra (non‑matrix) buttons
|
// Total buttons including the two extra (non‑matrix) buttons
|
||||||
pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2;
|
pub const TOTAL_BUTTONS: usize = NUMBER_OF_BUTTONS + 2;
|
||||||
|
|
||||||
|
pub type JoystickButtonMatrix = ButtonMatrix<
|
||||||
|
MatrixPins<{ BUTTON_ROWS }, { BUTTON_COLS }>,
|
||||||
|
{ BUTTON_ROWS },
|
||||||
|
{ BUTTON_COLS },
|
||||||
|
{ NUMBER_OF_BUTTONS },
|
||||||
|
>;
|
||||||
|
|
||||||
// ==================== BUTTON STRUCT ====================
|
// ==================== BUTTON STRUCT ====================
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
@ -75,10 +82,7 @@ impl ButtonManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update button states from the button matrix snapshot.
|
/// Update button states from the button matrix snapshot.
|
||||||
pub fn update_from_matrix(
|
pub fn update_from_matrix(&mut self, matrix: &mut JoystickButtonMatrix) {
|
||||||
&mut self,
|
|
||||||
matrix: &mut ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS>,
|
|
||||||
) {
|
|
||||||
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
|
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
|
||||||
self.buttons[index].pressed = *key;
|
self.buttons[index].pressed = *key;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,105 +1,39 @@
|
|||||||
//! Hardware configuration for CMDR Joystick 25 (RP2040)
|
//! Hardware configuration for CMDR Joystick 25 (RP2040)
|
||||||
//!
|
//!
|
||||||
//! Centralizes board constants, GPIO mappings, timing cadences and helper
|
//! Mirrors the structure introduced for the CMDR Keyboard firmware so that
|
||||||
//! macros to keep hardware details out of business logic.
|
//! bring-up, pin management, and timing constants follow the same layout.
|
||||||
|
|
||||||
|
use rp2040_hal::gpio::Pins;
|
||||||
|
use rp2040_hal::gpio::{
|
||||||
|
self, DynPinId, FunctionI2C, FunctionPio0, FunctionSioInput, FunctionSioOutput, Pin, PullNone,
|
||||||
|
PullUp,
|
||||||
|
};
|
||||||
|
|
||||||
// ==================== CRYSTAL AND USB CONSTANTS ====================
|
|
||||||
/// External crystal frequency (Hz).
|
/// External crystal frequency (Hz).
|
||||||
pub const XTAL_FREQ_HZ: u32 = 12_000_000u32;
|
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
|
||||||
/// USB Vendor ID.
|
|
||||||
|
/// USB Vendor ID/Product ID.
|
||||||
pub const USB_VID: u16 = 0x1209;
|
pub const USB_VID: u16 = 0x1209;
|
||||||
/// USB Product ID.
|
|
||||||
pub const USB_PID: u16 = 0x0002;
|
pub const USB_PID: u16 = 0x0002;
|
||||||
|
|
||||||
// ==================== JOYSTICK CONSTANTS ====================
|
/// Button matrix geometry (rows/cols) and count.
|
||||||
/// Button matrix geometry (rows).
|
|
||||||
pub const BUTTON_ROWS: usize = 5;
|
pub const BUTTON_ROWS: usize = 5;
|
||||||
/// Button matrix geometry (columns).
|
|
||||||
pub const BUTTON_COLS: usize = 5;
|
pub const BUTTON_COLS: usize = 5;
|
||||||
/// Total number of matrix buttons.
|
|
||||||
pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS;
|
pub const NUMBER_OF_BUTTONS: usize = BUTTON_ROWS * BUTTON_COLS;
|
||||||
/// ADC raw minimum (12‑bit).
|
|
||||||
|
/// ADC characteristics.
|
||||||
pub const ADC_MIN: u16 = 0;
|
pub const ADC_MIN: u16 = 0;
|
||||||
/// ADC raw maximum (12‑bit).
|
|
||||||
pub const ADC_MAX: u16 = 4095;
|
pub const ADC_MAX: u16 = 4095;
|
||||||
/// Logical axis center.
|
|
||||||
pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
|
pub const AXIS_CENTER: u16 = (ADC_MIN + ADC_MAX) / 2;
|
||||||
/// Number of physical gimbal axes.
|
|
||||||
pub const NBR_OF_GIMBAL_AXIS: usize = 4;
|
pub const NBR_OF_GIMBAL_AXIS: usize = 4;
|
||||||
/// Debounce threshold (in scans) for the matrix.
|
|
||||||
/// Increased from 10 to 15 scans to prevent double button presses from bounce.
|
/// Debounce thresholds.
|
||||||
/// At 200μs scan rate: 15 scans = 3ms debounce time.
|
pub const MATRIX_DEBOUNCE_SCANS: u8 = 15;
|
||||||
pub const DEBOUNCE: u8 = 15;
|
pub const MIN_PRESS_SPACING_SCANS: u32 = 25; // ~5ms @ 200µs cadence
|
||||||
/// Bytes reserved in EEPROM for calibration data + gimbal mode.
|
|
||||||
|
/// EEPROM storage length (calibration data + gimbal mode).
|
||||||
pub const EEPROM_DATA_LENGTH: usize = 25;
|
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;
|
|
||||||
pub const RIGHT_EXTRA_BUTTON_PIN: u8 = 0;
|
|
||||||
|
|
||||||
/// Button matrix row pins
|
|
||||||
pub const BUTTON_ROW_PIN_0: u8 = 6;
|
|
||||||
pub const BUTTON_ROW_PIN_1: u8 = 8;
|
|
||||||
pub const BUTTON_ROW_PIN_2: u8 = 4;
|
|
||||||
pub const BUTTON_ROW_PIN_3: u8 = 7;
|
|
||||||
pub const BUTTON_ROW_PIN_4: u8 = 5;
|
|
||||||
|
|
||||||
/// Button matrix column pins
|
|
||||||
pub const BUTTON_COL_PIN_0: u8 = 9;
|
|
||||||
pub const BUTTON_COL_PIN_1: u8 = 10;
|
|
||||||
pub const BUTTON_COL_PIN_2: u8 = 11;
|
|
||||||
pub const BUTTON_COL_PIN_3: u8 = 12;
|
|
||||||
pub const BUTTON_COL_PIN_4: u8 = 13;
|
|
||||||
|
|
||||||
/// ADC pins for gimbal axes
|
|
||||||
pub const ADC_LEFT_X_PIN: u8 = 29;
|
|
||||||
pub const ADC_LEFT_Y_PIN: u8 = 28;
|
|
||||||
pub const ADC_RIGHT_X_PIN: u8 = 27;
|
|
||||||
pub const ADC_RIGHT_Y_PIN: u8 = 26;
|
|
||||||
|
|
||||||
/// Status LED pin
|
|
||||||
pub const STATUS_LED_PIN: u8 = 16;
|
|
||||||
|
|
||||||
/// I2C pins for EEPROM
|
|
||||||
pub const I2C_SDA_PIN: u8 = 14;
|
|
||||||
pub const I2C_SCL_PIN: u8 = 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== I2C CONFIGURATION ====================
|
|
||||||
/// I2C frequency and system clock helpers for the EEPROM bus.
|
|
||||||
pub mod i2c {
|
|
||||||
use fugit::{Rate, RateExtU32};
|
|
||||||
|
|
||||||
pub const I2C_FREQUENCY_HZ: u32 = 400_000;
|
|
||||||
pub fn i2c_frequency() -> Rate<u32, 1, 1> {
|
|
||||||
I2C_FREQUENCY_HZ.Hz()
|
|
||||||
}
|
|
||||||
pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000;
|
|
||||||
pub fn system_clock() -> Rate<u32, 1, 1> {
|
|
||||||
SYSTEM_CLOCK_HZ.Hz()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== TIMER INTERVALS ====================
|
|
||||||
/// Cadences for periodic firmware tasks.
|
|
||||||
pub mod timers {
|
|
||||||
/// Status LED update interval (ms).
|
|
||||||
pub const STATUS_LED_INTERVAL_MS: u32 = 40;
|
|
||||||
|
|
||||||
/// Button matrix scan interval (µs).
|
|
||||||
pub const SCAN_INTERVAL_US: u32 = 200;
|
|
||||||
|
|
||||||
/// USB HID report interval (ms).
|
|
||||||
pub const USB_UPDATE_INTERVAL_MS: u32 = 1;
|
|
||||||
|
|
||||||
/// USB activity timeout (ms) - stop sending reports after this period of inactivity.
|
|
||||||
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000; // 5 seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== USB DEVICE CONFIGURATION ====================
|
|
||||||
/// USB string descriptors.
|
/// USB string descriptors.
|
||||||
pub mod usb {
|
pub mod usb {
|
||||||
pub const MANUFACTURER: &str = "CMtec";
|
pub const MANUFACTURER: &str = "CMtec";
|
||||||
@ -107,86 +41,155 @@ pub mod usb {
|
|||||||
pub const SERIAL_NUMBER: &str = "0001";
|
pub const SERIAL_NUMBER: &str = "0001";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== PIN ACCESS MACROS ====================
|
/// Timing cadences.
|
||||||
|
pub mod timers {
|
||||||
/// Macro to access typed GPIO pins using board constants.
|
pub const STATUS_LED_INTERVAL_MS: u32 = 40;
|
||||||
/// Avoids scattering raw GPIO numbers; each arm references the constant it maps.
|
pub const SCAN_INTERVAL_US: u32 = 200;
|
||||||
#[macro_export]
|
pub const USB_UPDATE_INTERVAL_MS: u32 = 1;
|
||||||
macro_rules! get_pin {
|
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000;
|
||||||
($pins:expr, left_extra_button) => {{
|
}
|
||||||
const _: u8 = $crate::hardware::pins::LEFT_EXTRA_BUTTON_PIN;
|
|
||||||
$pins.gpio1
|
/// I2C helpers.
|
||||||
}};
|
pub mod i2c {
|
||||||
($pins:expr, right_extra_button) => {{
|
use eeprom24x::SlaveAddr;
|
||||||
const _: u8 = $crate::hardware::pins::RIGHT_EXTRA_BUTTON_PIN;
|
use fugit::{Rate, RateExtU32};
|
||||||
$pins.gpio0
|
|
||||||
}};
|
pub const FREQUENCY_HZ: u32 = 400_000;
|
||||||
($pins:expr, button_row_0) => {{
|
pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000;
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_0;
|
|
||||||
$pins.gpio6
|
pub fn frequency() -> Rate<u32, 1, 1> {
|
||||||
}};
|
FREQUENCY_HZ.Hz()
|
||||||
($pins:expr, button_row_1) => {{
|
}
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_1;
|
|
||||||
$pins.gpio8
|
pub fn system_clock() -> Rate<u32, 1, 1> {
|
||||||
}};
|
SYSTEM_CLOCK_HZ.Hz()
|
||||||
($pins:expr, button_row_2) => {{
|
}
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_2;
|
|
||||||
$pins.gpio4
|
pub const EEPROM_ADDRESS: SlaveAddr = SlaveAddr::Alternative(false, false, false);
|
||||||
}};
|
}
|
||||||
($pins:expr, button_row_3) => {{
|
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_3;
|
/// Raw GPIO constants retained for documentation/reference.
|
||||||
$pins.gpio7
|
pub mod pins {
|
||||||
}};
|
pub const LEFT_EXTRA_BUTTON: u8 = 1;
|
||||||
($pins:expr, button_row_4) => {{
|
pub const RIGHT_EXTRA_BUTTON: u8 = 0;
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_4;
|
pub const BUTTON_ROW_0: u8 = 6;
|
||||||
$pins.gpio5
|
pub const BUTTON_ROW_1: u8 = 8;
|
||||||
}};
|
pub const BUTTON_ROW_2: u8 = 4;
|
||||||
($pins:expr, button_col_0) => {{
|
pub const BUTTON_ROW_3: u8 = 7;
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_0;
|
pub const BUTTON_ROW_4: u8 = 5;
|
||||||
$pins.gpio9
|
pub const BUTTON_COL_0: u8 = 9;
|
||||||
}};
|
pub const BUTTON_COL_1: u8 = 10;
|
||||||
($pins:expr, button_col_1) => {{
|
pub const BUTTON_COL_2: u8 = 11;
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_1;
|
pub const BUTTON_COL_3: u8 = 12;
|
||||||
$pins.gpio10
|
pub const BUTTON_COL_4: u8 = 13;
|
||||||
}};
|
pub const ADC_LEFT_X: u8 = 29;
|
||||||
($pins:expr, button_col_2) => {{
|
pub const ADC_LEFT_Y: u8 = 28;
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_2;
|
pub const ADC_RIGHT_X: u8 = 27;
|
||||||
$pins.gpio11
|
pub const ADC_RIGHT_Y: u8 = 26;
|
||||||
}};
|
pub const STATUS_LED: u8 = 16;
|
||||||
($pins:expr, button_col_3) => {{
|
pub const I2C_SDA: u8 = 14;
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_3;
|
pub const I2C_SCL: u8 = 15;
|
||||||
$pins.gpio12
|
}
|
||||||
}};
|
|
||||||
($pins:expr, button_col_4) => {{
|
/// Matrix row pins (dynamic to simplify scanning code).
|
||||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_4;
|
pub type MatrixRowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
|
||||||
$pins.gpio13
|
/// Matrix column pins (dynamic push-pull outputs).
|
||||||
}};
|
pub type MatrixColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
|
||||||
($pins:expr, adc_left_x) => {{
|
/// Extra buttons (pull-up inputs).
|
||||||
const _: u8 = $crate::hardware::pins::ADC_LEFT_X_PIN;
|
pub type ExtraButtonPin = Pin<DynPinId, FunctionSioInput, PullUp>;
|
||||||
$pins.gpio29
|
/// Status LED pin configured for PIO output.
|
||||||
}};
|
pub type StatusLedPin = Pin<gpio::bank0::Gpio16, FunctionPio0, PullNone>;
|
||||||
($pins:expr, adc_left_y) => {{
|
/// I2C SDA/SCL pins after reconfiguration.
|
||||||
const _: u8 = $crate::hardware::pins::ADC_LEFT_Y_PIN;
|
pub type I2cSdaPin = Pin<gpio::bank0::Gpio14, FunctionI2C, PullUp>;
|
||||||
$pins.gpio28
|
pub type I2cSclPin = Pin<gpio::bank0::Gpio15, FunctionI2C, PullUp>;
|
||||||
}};
|
|
||||||
($pins:expr, adc_right_x) => {{
|
/// Analog axis input pins (remain as SIO inputs until wrapped by `AdcPin`).
|
||||||
const _: u8 = $crate::hardware::pins::ADC_RIGHT_X_PIN;
|
pub struct AxisInputs {
|
||||||
$pins.gpio27
|
pub left_x: Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>,
|
||||||
}};
|
pub left_y: Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>,
|
||||||
($pins:expr, adc_right_y) => {{
|
pub right_x: Pin<gpio::bank0::Gpio27, FunctionSioInput, PullNone>,
|
||||||
const _: u8 = $crate::hardware::pins::ADC_RIGHT_Y_PIN;
|
pub right_y: Pin<gpio::bank0::Gpio26, FunctionSioInput, PullNone>,
|
||||||
$pins.gpio26
|
}
|
||||||
}};
|
|
||||||
($pins:expr, status_led) => {{
|
/// Bundle returned by `split_board_pins`.
|
||||||
const _: u8 = $crate::hardware::pins::STATUS_LED_PIN;
|
pub struct BoardPins {
|
||||||
$pins.gpio16
|
pub matrix_rows: [MatrixRowPin; BUTTON_ROWS],
|
||||||
}};
|
pub matrix_cols: [MatrixColPin; BUTTON_COLS],
|
||||||
($pins:expr, i2c_sda) => {{
|
pub left_extra_button: ExtraButtonPin,
|
||||||
const _: u8 = $crate::hardware::pins::I2C_SDA_PIN;
|
pub right_extra_button: ExtraButtonPin,
|
||||||
$pins.gpio14
|
pub axis_inputs: AxisInputs,
|
||||||
}};
|
pub status_led: StatusLedPin,
|
||||||
($pins:expr, i2c_scl) => {{
|
pub i2c_sda: I2cSdaPin,
|
||||||
const _: u8 = $crate::hardware::pins::I2C_SCL_PIN;
|
pub i2c_scl: I2cSclPin,
|
||||||
$pins.gpio15
|
}
|
||||||
}};
|
|
||||||
|
impl BoardPins {
|
||||||
|
pub fn new(pins: Pins) -> Self {
|
||||||
|
let row0 = pins.gpio6.into_pull_up_input().into_dyn_pin();
|
||||||
|
let row1 = pins.gpio8.into_pull_up_input().into_dyn_pin();
|
||||||
|
let row2 = pins.gpio4.into_pull_up_input().into_dyn_pin();
|
||||||
|
let row3 = pins.gpio7.into_pull_up_input().into_dyn_pin();
|
||||||
|
let row4 = pins.gpio5.into_pull_up_input().into_dyn_pin();
|
||||||
|
|
||||||
|
let col0 = pins
|
||||||
|
.gpio9
|
||||||
|
.into_push_pull_output()
|
||||||
|
.into_pull_type::<PullNone>()
|
||||||
|
.into_dyn_pin();
|
||||||
|
let col1 = pins
|
||||||
|
.gpio10
|
||||||
|
.into_push_pull_output()
|
||||||
|
.into_pull_type::<PullNone>()
|
||||||
|
.into_dyn_pin();
|
||||||
|
let col2 = pins
|
||||||
|
.gpio11
|
||||||
|
.into_push_pull_output()
|
||||||
|
.into_pull_type::<PullNone>()
|
||||||
|
.into_dyn_pin();
|
||||||
|
let col3 = pins
|
||||||
|
.gpio12
|
||||||
|
.into_push_pull_output()
|
||||||
|
.into_pull_type::<PullNone>()
|
||||||
|
.into_dyn_pin();
|
||||||
|
let col4 = pins
|
||||||
|
.gpio13
|
||||||
|
.into_push_pull_output()
|
||||||
|
.into_pull_type::<PullNone>()
|
||||||
|
.into_dyn_pin();
|
||||||
|
|
||||||
|
let left_extra = pins.gpio1.into_pull_up_input().into_dyn_pin();
|
||||||
|
let right_extra = pins.gpio0.into_pull_up_input().into_dyn_pin();
|
||||||
|
|
||||||
|
let axis_inputs = AxisInputs {
|
||||||
|
left_x: pins.gpio29.into_floating_input(),
|
||||||
|
left_y: pins.gpio28.into_floating_input(),
|
||||||
|
right_x: pins.gpio27.into_floating_input(),
|
||||||
|
right_y: pins.gpio26.into_floating_input(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_led = pins
|
||||||
|
.gpio16
|
||||||
|
.into_function::<FunctionPio0>()
|
||||||
|
.into_pull_type::<PullNone>();
|
||||||
|
|
||||||
|
let i2c_sda = pins
|
||||||
|
.gpio14
|
||||||
|
.into_function::<FunctionI2C>()
|
||||||
|
.into_pull_type::<PullUp>();
|
||||||
|
let i2c_scl = pins
|
||||||
|
.gpio15
|
||||||
|
.into_function::<FunctionI2C>()
|
||||||
|
.into_pull_type::<PullUp>();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
matrix_rows: [row0, row1, row2, row3, row4],
|
||||||
|
matrix_cols: [col0, col1, col2, col3, col4],
|
||||||
|
left_extra_button: left_extra,
|
||||||
|
right_extra_button: right_extra,
|
||||||
|
axis_inputs,
|
||||||
|
status_led,
|
||||||
|
i2c_sda,
|
||||||
|
i2c_scl,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
309
rp2040/src/joystick.rs
Normal file
309
rp2040/src/joystick.rs
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
//! Runtime state for the CMDR Joystick 25.
|
||||||
|
//!
|
||||||
|
//! This mirrors the `KeyboardState` abstraction from the keyboard refactor and
|
||||||
|
//! concentrates axis/button/calibration logic alongside USB bookkeeping.
|
||||||
|
|
||||||
|
use crate::axis::AxisManager;
|
||||||
|
use crate::buttons::{ButtonManager, SpecialAction};
|
||||||
|
use crate::calibration::CalibrationManager;
|
||||||
|
use crate::expo::ExpoLUT;
|
||||||
|
use crate::hardware;
|
||||||
|
use crate::status::SystemState;
|
||||||
|
use crate::usb_joystick_device::JoystickReport;
|
||||||
|
use crate::usb_report::get_joystick_report;
|
||||||
|
use core::fmt::Debug;
|
||||||
|
use dyn_smooth::DynamicSmootherEcoI32;
|
||||||
|
use embedded_hal::digital::InputPin;
|
||||||
|
use rp2040_hal::timer::Timer;
|
||||||
|
use usb_device::device::UsbDeviceState;
|
||||||
|
|
||||||
|
pub struct UsbState {
|
||||||
|
pub initialized: bool,
|
||||||
|
pub active: bool,
|
||||||
|
pub suspended: bool,
|
||||||
|
pub send_pending: bool,
|
||||||
|
pub wake_on_input: bool,
|
||||||
|
pub idle_mode: bool,
|
||||||
|
pub activity: bool,
|
||||||
|
activity_elapsed_ms: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsbState {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
initialized: false,
|
||||||
|
active: false,
|
||||||
|
suspended: false,
|
||||||
|
send_pending: false,
|
||||||
|
wake_on_input: false,
|
||||||
|
idle_mode: false,
|
||||||
|
activity: false,
|
||||||
|
activity_elapsed_ms: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_poll(&mut self) {
|
||||||
|
if !self.initialized {
|
||||||
|
self.initialized = true;
|
||||||
|
}
|
||||||
|
if !self.active {
|
||||||
|
self.mark_activity();
|
||||||
|
}
|
||||||
|
self.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_activity(&mut self) {
|
||||||
|
self.activity = true;
|
||||||
|
self.activity_elapsed_ms = 0;
|
||||||
|
self.idle_mode = false;
|
||||||
|
self.send_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input_activity(&mut self) {
|
||||||
|
self.mark_activity();
|
||||||
|
if self.suspended && self.wake_on_input {
|
||||||
|
self.wake_on_input = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_suspend_change(&mut self, state: UsbDeviceState) {
|
||||||
|
let was_suspended = self.suspended;
|
||||||
|
self.suspended = state == UsbDeviceState::Suspend;
|
||||||
|
|
||||||
|
match (was_suspended, self.suspended) {
|
||||||
|
(true, false) => {
|
||||||
|
self.mark_activity();
|
||||||
|
self.wake_on_input = false;
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
self.idle_mode = true;
|
||||||
|
self.activity = false;
|
||||||
|
self.send_pending = false;
|
||||||
|
self.wake_on_input = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_idle_timer(&mut self, interval_ms: u32) {
|
||||||
|
if !self.activity {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.activity_elapsed_ms = self.activity_elapsed_ms.saturating_add(interval_ms);
|
||||||
|
if self.activity_elapsed_ms >= hardware::timers::USB_ACTIVITY_TIMEOUT_MS {
|
||||||
|
self.activity = false;
|
||||||
|
self.activity_elapsed_ms = 0;
|
||||||
|
self.idle_mode = true;
|
||||||
|
self.send_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn acknowledge_report(&mut self) {
|
||||||
|
self.send_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JoystickState {
|
||||||
|
axis_manager: AxisManager,
|
||||||
|
button_manager: ButtonManager,
|
||||||
|
calibration_manager: CalibrationManager,
|
||||||
|
smoother: [DynamicSmootherEcoI32; hardware::NBR_OF_GIMBAL_AXIS],
|
||||||
|
expo_primary: ExpoLUT,
|
||||||
|
expo_virtual: ExpoLUT,
|
||||||
|
vt_enable: bool,
|
||||||
|
usb: UsbState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JoystickState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
axis_manager: AxisManager::new(),
|
||||||
|
button_manager: ButtonManager::new(),
|
||||||
|
calibration_manager: CalibrationManager::new(),
|
||||||
|
smoother: AxisManager::create_smoothers(),
|
||||||
|
expo_primary: ExpoLUT::new(0.3),
|
||||||
|
expo_virtual: ExpoLUT::new(0.6),
|
||||||
|
vt_enable: false,
|
||||||
|
usb: UsbState::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_calibration<R>(&mut self, mut read_fn: R)
|
||||||
|
where
|
||||||
|
R: FnMut(u32) -> Result<u8, ()>,
|
||||||
|
{
|
||||||
|
CalibrationManager::load_axis_calibration(&mut self.axis_manager.axes, &mut read_fn);
|
||||||
|
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
|
||||||
|
self.axis_manager.set_gimbal_mode(gimbal_mode);
|
||||||
|
self.calibration_manager.set_gimbal_mode(gimbal_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_button_states<L, R>(
|
||||||
|
&mut self,
|
||||||
|
matrix: &mut crate::board::JoystickMatrix,
|
||||||
|
left_button: &mut L,
|
||||||
|
right_button: &mut R,
|
||||||
|
) where
|
||||||
|
L: InputPin,
|
||||||
|
R: InputPin,
|
||||||
|
L::Error: Debug,
|
||||||
|
R::Error: Debug,
|
||||||
|
{
|
||||||
|
self.button_manager.update_from_matrix(matrix);
|
||||||
|
self.button_manager
|
||||||
|
.update_extra_buttons(left_button, right_button);
|
||||||
|
self.button_manager.filter_hat_switches();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finalize_button_logic(&mut self, timer: &Timer) -> bool {
|
||||||
|
self.button_manager.process_button_logic_with_timer(timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_special_action(&self) -> SpecialAction {
|
||||||
|
self.button_manager.check_special_combinations(
|
||||||
|
self.axis_manager.get_value_before_hold(),
|
||||||
|
self.calibration_manager.is_active(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_special_action<W>(&mut self, action: SpecialAction, mut write_page: W)
|
||||||
|
where
|
||||||
|
W: FnMut(u32, &[u8]) -> Result<(), ()>,
|
||||||
|
{
|
||||||
|
match action {
|
||||||
|
SpecialAction::Bootloader => {}
|
||||||
|
SpecialAction::StartCalibration => {
|
||||||
|
for (index, axis) in self.axis_manager.axes.iter_mut().enumerate() {
|
||||||
|
let centered = self.smoother[index].value() as u16;
|
||||||
|
axis.center = centered;
|
||||||
|
axis.min = centered;
|
||||||
|
axis.max = centered;
|
||||||
|
}
|
||||||
|
self.axis_manager.clear_throttle_hold();
|
||||||
|
self.calibration_manager.start_calibration();
|
||||||
|
}
|
||||||
|
SpecialAction::CancelCalibration => {
|
||||||
|
self.calibration_manager.stop_calibration();
|
||||||
|
}
|
||||||
|
SpecialAction::ThrottleHold(value) => {
|
||||||
|
self.axis_manager.set_throttle_hold(value);
|
||||||
|
}
|
||||||
|
SpecialAction::VirtualThrottleToggle => {
|
||||||
|
self.vt_enable = !self.vt_enable;
|
||||||
|
}
|
||||||
|
SpecialAction::CalibrationSetModeM10 => {
|
||||||
|
if self
|
||||||
|
.calibration_manager
|
||||||
|
.set_gimbal_mode_m10(&mut self.axis_manager.axes, &self.smoother)
|
||||||
|
{
|
||||||
|
self.axis_manager
|
||||||
|
.set_gimbal_mode(self.calibration_manager.get_gimbal_mode());
|
||||||
|
self.axis_manager.clear_throttle_hold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialAction::CalibrationSetModeM7 => {
|
||||||
|
if self
|
||||||
|
.calibration_manager
|
||||||
|
.set_gimbal_mode_m7(&mut self.axis_manager.axes, &self.smoother)
|
||||||
|
{
|
||||||
|
self.axis_manager
|
||||||
|
.set_gimbal_mode(self.calibration_manager.get_gimbal_mode());
|
||||||
|
self.axis_manager.clear_throttle_hold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialAction::CalibrationSave => {
|
||||||
|
self.calibration_manager
|
||||||
|
.save_calibration(&self.axis_manager.axes, &mut |page, data| {
|
||||||
|
write_page(page, data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
SpecialAction::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_calibration_tracking(&mut self) {
|
||||||
|
self.calibration_manager
|
||||||
|
.update_dynamic_calibration(&mut self.axis_manager.axes, &self.smoother);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick_smoothers(&mut self, raw: &mut [u16; hardware::NBR_OF_GIMBAL_AXIS]) {
|
||||||
|
self.axis_manager.apply_gimbal_compensation(raw);
|
||||||
|
self.axis_manager.update_smoothers(&mut self.smoother, raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_axes(&mut self) -> bool {
|
||||||
|
self.axis_manager
|
||||||
|
.process_axis_values(&self.smoother, &self.expo_primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_virtual_axes(&mut self) -> bool {
|
||||||
|
self.axis_manager
|
||||||
|
.update_virtual_axes(self.button_manager.buttons(), self.vt_enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vt_enable(&self) -> bool {
|
||||||
|
self.vt_enable
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usb_state(&mut self) -> &mut UsbState {
|
||||||
|
&mut self.usb
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn axis_manager(&mut self) -> &mut AxisManager {
|
||||||
|
&mut self.axis_manager
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_manager(&mut self) -> &mut ButtonManager {
|
||||||
|
&mut self.button_manager
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expo_virtual(&self) -> &ExpoLUT {
|
||||||
|
&self.expo_virtual
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn smoother(&self) -> &[DynamicSmootherEcoI32; hardware::NBR_OF_GIMBAL_AXIS] {
|
||||||
|
&self.smoother
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calibration_manager(&self) -> &CalibrationManager {
|
||||||
|
&self.calibration_manager
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_state(&self) -> SystemState {
|
||||||
|
SystemState {
|
||||||
|
usb_active: self.usb.active,
|
||||||
|
usb_initialized: self.usb.initialized,
|
||||||
|
usb_suspended: self.usb.suspended,
|
||||||
|
idle_mode: self.usb.idle_mode,
|
||||||
|
calibration_active: self.calibration_manager.is_active(),
|
||||||
|
throttle_hold_enable: self.axis_manager.throttle_hold_enable,
|
||||||
|
vt_enable: self.vt_enable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_report(&mut self) -> JoystickReport {
|
||||||
|
let virtual_ry = self.axis_manager.get_virtual_ry_value(&self.expo_virtual);
|
||||||
|
let virtual_rz = self.axis_manager.get_virtual_rz_value(&self.expo_virtual);
|
||||||
|
get_joystick_report(
|
||||||
|
self.button_manager.buttons_mut(),
|
||||||
|
&mut self.axis_manager.axes,
|
||||||
|
virtual_ry,
|
||||||
|
virtual_rz,
|
||||||
|
&self.vt_enable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_report() -> JoystickReport {
|
||||||
|
JoystickReport {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
z: 0,
|
||||||
|
rx: 0,
|
||||||
|
ry: 0,
|
||||||
|
rz: 0,
|
||||||
|
slider: 0,
|
||||||
|
hat: 8,
|
||||||
|
buttons: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,13 @@
|
|||||||
//! firmware: axis processing, button handling, calibration and storage, USB
|
//! firmware: axis processing, button handling, calibration and storage, USB
|
||||||
//! HID reporting, and hardware/status abstractions.
|
//! HID reporting, and hardware/status abstractions.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
pub mod board;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
pub mod bootloader;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
pub mod joystick;
|
||||||
|
|
||||||
/// Axis processing for gimbal and virtual axes (smoothing, expo, holds).
|
/// Axis processing for gimbal and virtual axes (smoothing, expo, holds).
|
||||||
pub mod axis;
|
pub mod axis;
|
||||||
/// Row/column scanned button matrix driver with debouncing.
|
/// Row/column scanned button matrix driver with debouncing.
|
||||||
@ -29,6 +36,11 @@ pub mod usb_joystick_device;
|
|||||||
/// Convert runtime state into USB HID joystick reports.
|
/// Convert runtime state into USB HID joystick reports.
|
||||||
pub mod usb_report;
|
pub mod usb_report;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
pub use board::{AxisAnalogPins, Board, BoardParts, JoystickMatrix, JoystickStatusLed};
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
pub use joystick::{JoystickState, UsbState};
|
||||||
|
|
||||||
/// Re-exports for convenient access in `main` and downstream consumers.
|
/// Re-exports for convenient access in `main` and downstream consumers.
|
||||||
pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
|
pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
|
||||||
pub use calibration::CalibrationManager;
|
pub use calibration::CalibrationManager;
|
||||||
|
|||||||
@ -1,273 +1,44 @@
|
|||||||
//! CMDR Joystick 25 – RP2040 main firmware
|
|
||||||
//!
|
|
||||||
//! 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
|
|
||||||
//!
|
|
||||||
//! 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
|
|
||||||
//!
|
|
||||||
//! Modes: Normal, Calibration, Throttle Hold, Virtual Throttle, Bootloader
|
|
||||||
//!
|
|
||||||
//! Timing: scan 200 µs, process 1200 µs, USB 10 ms, LED 250 ms
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
mod axis;
|
use cmdr_joystick_25::buttons::SpecialAction;
|
||||||
mod button_matrix;
|
use cmdr_joystick_25::hardware::{self, timers};
|
||||||
mod buttons;
|
use cmdr_joystick_25::status::StatusMode;
|
||||||
mod calibration;
|
use cmdr_joystick_25::usb_joystick_device::JoystickConfig;
|
||||||
mod expo;
|
use cmdr_joystick_25::{bootloader, Board, BoardParts, JoystickState};
|
||||||
mod hardware;
|
|
||||||
mod mapping;
|
|
||||||
mod status;
|
|
||||||
mod storage;
|
|
||||||
mod usb_joystick_device;
|
|
||||||
mod usb_report;
|
|
||||||
|
|
||||||
use axis::AxisManager;
|
|
||||||
use button_matrix::ButtonMatrix;
|
|
||||||
use buttons::{ButtonManager, SpecialAction};
|
|
||||||
use calibration::CalibrationManager;
|
|
||||||
use core::convert::Infallible;
|
|
||||||
use core::panic::PanicInfo;
|
|
||||||
use cortex_m::delay::Delay;
|
|
||||||
use eeprom24x::{Eeprom24x, SlaveAddr};
|
|
||||||
use embedded_hal::digital::{InputPin, OutputPin};
|
|
||||||
use embedded_hal_0_2::adc::OneShot;
|
use embedded_hal_0_2::adc::OneShot;
|
||||||
use embedded_hal_0_2::timer::CountDown;
|
use embedded_hal_0_2::timer::CountDown;
|
||||||
use fugit::ExtU32;
|
use fugit::ExtU32;
|
||||||
use hardware::timers;
|
use panic_halt as _;
|
||||||
use mapping::*;
|
|
||||||
use rp2040_hal::{
|
|
||||||
adc::Adc,
|
|
||||||
adc::AdcPin,
|
|
||||||
clocks::{init_clocks_and_plls, Clock},
|
|
||||||
gpio::Pins,
|
|
||||||
i2c::I2C,
|
|
||||||
pac,
|
|
||||||
pio::PIOExt,
|
|
||||||
timer::Timer,
|
|
||||||
watchdog::Watchdog,
|
|
||||||
Sio,
|
|
||||||
};
|
|
||||||
use status::{StatusLed, StatusMode, SystemState};
|
|
||||||
use usb_device::class_prelude::*;
|
|
||||||
use usb_device::device::UsbDeviceState;
|
|
||||||
use usb_device::prelude::*;
|
use usb_device::prelude::*;
|
||||||
use usb_joystick_device::JoystickConfig;
|
use usbd_human_interface_device::prelude::{UsbHidClassBuilder, UsbHidError};
|
||||||
use usb_report::get_joystick_report;
|
|
||||||
use usbd_human_interface_device::prelude::*;
|
|
||||||
|
|
||||||
#[panic_handler]
|
#[unsafe(link_section = ".boot2")]
|
||||||
fn panic(_info: &PanicInfo) -> ! {
|
#[unsafe(no_mangle)]
|
||||||
loop {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Boot loader configuration for RP2040 ROM.
|
|
||||||
///
|
|
||||||
/// The linker places this boot block at the start of our program image to help the ROM
|
|
||||||
/// bootloader initialize our code. This specific boot loader supports W25Q080 flash memory.
|
|
||||||
#[link_section = ".boot2"]
|
|
||||||
#[no_mangle]
|
|
||||||
#[used]
|
#[used]
|
||||||
pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
|
pub static BOOT2_FIRMWARE: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
|
||||||
|
|
||||||
use expo::ExpoLUT;
|
|
||||||
|
|
||||||
/// Hardware configuration imports from the hardware abstraction layer.
|
|
||||||
use hardware::{ADC_MAX, ADC_MIN};
|
|
||||||
use hardware::{BUTTON_COLS, BUTTON_ROWS, NUMBER_OF_BUTTONS};
|
|
||||||
|
|
||||||
/// Additional hardware constants for button debouncing.
|
|
||||||
use hardware::DEBOUNCE;
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
#[rp2040_hal::entry]
|
#[rp2040_hal::entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
// Hardware initialization and peripheral setup for joystick operation
|
let BoardParts {
|
||||||
|
mut button_matrix,
|
||||||
// Acquire exclusive access to RP2040 peripherals
|
mut status_led,
|
||||||
let mut pac = pac::Peripherals::take().unwrap();
|
mut delay,
|
||||||
|
timer,
|
||||||
// Initialize watchdog timer (required for clock configuration)
|
mut adc,
|
||||||
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
mut axis_pins,
|
||||||
|
mut left_extra_button,
|
||||||
// Configure system clocks and phase-locked loops for stable operation
|
mut right_extra_button,
|
||||||
let clocks = init_clocks_and_plls(
|
mut eeprom,
|
||||||
hardware::XTAL_FREQ_HZ,
|
usb_bus,
|
||||||
pac.XOSC,
|
} = Board::new().into_parts();
|
||||||
pac.CLOCKS,
|
|
||||||
pac.PLL_SYS,
|
|
||||||
pac.PLL_USB,
|
|
||||||
&mut pac.RESETS,
|
|
||||||
&mut watchdog,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let core = pac::CorePeripherals::take().unwrap();
|
|
||||||
|
|
||||||
// Initialize SIO (Single-cycle I/O) for high-performance GPIO operations
|
|
||||||
let sio = Sio::new(pac.SIO);
|
|
||||||
|
|
||||||
// Configure GPIO pins to their default operational state
|
|
||||||
let pins = Pins::new(
|
|
||||||
pac.IO_BANK0,
|
|
||||||
pac.PADS_BANK0,
|
|
||||||
sio.gpio_bank0,
|
|
||||||
&mut pac.RESETS,
|
|
||||||
);
|
|
||||||
|
|
||||||
let i2c = I2C::i2c1(
|
|
||||||
pac.I2C1,
|
|
||||||
get_pin!(pins, i2c_sda).reconfigure(), // sda
|
|
||||||
get_pin!(pins, i2c_scl).reconfigure(), // scl
|
|
||||||
hardware::i2c::i2c_frequency(),
|
|
||||||
&mut pac.RESETS,
|
|
||||||
hardware::i2c::system_clock(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let i2c_address = SlaveAddr::Alternative(false, false, false);
|
|
||||||
let mut eeprom = Eeprom24x::new_24x32(i2c, i2c_address);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Configure ADC input pins for 4-axis gimbal (Left X/Y, Right X/Y)
|
|
||||||
let mut adc_pin_left_x = AdcPin::new(get_pin!(pins, adc_left_x).into_floating_input()).unwrap();
|
|
||||||
let mut adc_pin_left_y = AdcPin::new(get_pin!(pins, adc_left_y).into_floating_input()).unwrap();
|
|
||||||
let mut adc_pin_right_x =
|
|
||||||
AdcPin::new(get_pin!(pins, adc_right_x).into_floating_input()).unwrap();
|
|
||||||
let mut adc_pin_right_y =
|
|
||||||
AdcPin::new(get_pin!(pins, adc_right_y).into_floating_input()).unwrap();
|
|
||||||
|
|
||||||
// # Button Matrix Configuration\n //\n // Configure the 5x5 button matrix using row/column scanning technique.\n // Rows are configured as pull-up inputs, columns as push-pull outputs.\n // This allows scanning 25 buttons with only 10 GPIO pins.\n\n // Configure button matrix row pins (inputs with pull-up resistors)
|
|
||||||
let button_matrix_row_pins: &mut [&mut dyn InputPin<Error = Infallible>; BUTTON_ROWS] = &mut [
|
|
||||||
&mut get_pin!(pins, button_row_0).into_pull_up_input(),
|
|
||||||
&mut get_pin!(pins, button_row_1).into_pull_up_input(),
|
|
||||||
&mut get_pin!(pins, button_row_2).into_pull_up_input(),
|
|
||||||
&mut get_pin!(pins, button_row_3).into_pull_up_input(),
|
|
||||||
&mut get_pin!(pins, button_row_4).into_pull_up_input(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Configure button matrix column pins (push-pull outputs for scanning)
|
|
||||||
let button_matrix_col_pins: &mut [&mut dyn OutputPin<Error = Infallible>; BUTTON_COLS] = &mut [
|
|
||||||
&mut get_pin!(pins, button_col_0).into_push_pull_output(),
|
|
||||||
&mut get_pin!(pins, button_col_1).into_push_pull_output(),
|
|
||||||
&mut get_pin!(pins, button_col_2).into_push_pull_output(),
|
|
||||||
&mut get_pin!(pins, button_col_3).into_push_pull_output(),
|
|
||||||
&mut get_pin!(pins, button_col_4).into_push_pull_output(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initialize button matrix scanner with debouncing
|
|
||||||
let mut button_matrix: ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS> =
|
|
||||||
ButtonMatrix::new(button_matrix_row_pins, button_matrix_col_pins, DEBOUNCE);
|
|
||||||
|
|
||||||
// Configure matrix pins for scanning operation
|
|
||||||
button_matrix.init_pins();
|
|
||||||
|
|
||||||
// Configure additional buttons outside the matrix (total: 27 buttons)
|
|
||||||
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: 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);
|
|
||||||
let mut status_led = StatusLed::new(
|
|
||||||
get_pin!(pins, status_led).into_function(),
|
|
||||||
&mut pio,
|
|
||||||
sm0,
|
|
||||||
clocks.peripheral_clock.freq(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initial LED state (red) indicates system initialization
|
|
||||||
status_led.update(StatusMode::Error);
|
|
||||||
|
|
||||||
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// Multiple scans ensure debounce algorithm captures stable button states
|
|
||||||
button_matrix.scan_matrix(&mut delay);
|
|
||||||
}
|
|
||||||
if button_matrix.buttons_pressed()[BUTTON_FRONT_LEFT_LOWER] {
|
|
||||||
status_led.update(StatusMode::Bootloader);
|
|
||||||
let gpio_activity_pin_mask: u32 = 0;
|
|
||||||
let disable_interface_mask: u32 = 0;
|
|
||||||
rp2040_hal::rom_data::reset_to_usb_boot(gpio_activity_pin_mask, disable_interface_mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer configuration: cadence for LED updates, scans, processing and USB
|
|
||||||
|
|
||||||
// Initialize hardware timer peripheral
|
|
||||||
let timer = Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
|
|
||||||
|
|
||||||
let mut status_led_count_down = timer.count_down();
|
|
||||||
status_led_count_down.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
|
||||||
|
|
||||||
// Removed unused millisecond countdown timer
|
|
||||||
|
|
||||||
let mut scan_count_down = timer.count_down();
|
|
||||||
scan_count_down.start(timers::SCAN_INTERVAL_US.micros());
|
|
||||||
|
|
||||||
let mut usb_update_count_down = timer.count_down();
|
|
||||||
usb_update_count_down.start(timers::USB_UPDATE_INTERVAL_MS.millis());
|
|
||||||
|
|
||||||
let mut usb_activity: bool = false;
|
|
||||||
let mut usb_active: bool = false;
|
|
||||||
let mut usb_initialized: bool = false;
|
|
||||||
let mut usb_suspended: bool = false;
|
|
||||||
let mut usb_send_pending: bool = false;
|
|
||||||
let mut vt_enable: bool = false;
|
|
||||||
let mut idle_mode: bool = false;
|
|
||||||
let mut usb_activity_timeout_count: u32 = 0;
|
|
||||||
let mut wake_on_input: bool = false;
|
|
||||||
|
|
||||||
let mut axis_manager = AxisManager::new();
|
|
||||||
let mut button_manager = ButtonManager::new();
|
|
||||||
let mut calibration_manager = CalibrationManager::new();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
let expo_lut_virtual = ExpoLUT::new(0.6);
|
|
||||||
|
|
||||||
// Initialize digital smoothing filters for each gimbal axis
|
|
||||||
let mut smoother = AxisManager::create_smoothers();
|
|
||||||
|
|
||||||
// USB HID configuration (full‑speed joystick class)
|
|
||||||
|
|
||||||
// Initialize USB bus allocator for RP2040
|
|
||||||
let usb_bus = UsbBusAllocator::new(rp2040_hal::usb::UsbBus::new(
|
|
||||||
pac.USBCTRL_REGS,
|
|
||||||
pac.USBCTRL_DPRAM,
|
|
||||||
clocks.usb_clock,
|
|
||||||
true,
|
|
||||||
&mut pac.RESETS,
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut usb_hid_joystick = UsbHidClassBuilder::new()
|
let mut usb_hid_joystick = UsbHidClassBuilder::new()
|
||||||
.add_device(JoystickConfig::default())
|
.add_device(JoystickConfig::default())
|
||||||
.build(&usb_bus);
|
.build(usb_bus);
|
||||||
|
|
||||||
let mut usb_dev =
|
let mut usb_dev =
|
||||||
UsbDeviceBuilder::new(&usb_bus, UsbVidPid(hardware::USB_VID, hardware::USB_PID))
|
UsbDeviceBuilder::new(usb_bus, UsbVidPid(hardware::USB_VID, hardware::USB_PID))
|
||||||
.strings(&[StringDescriptors::default()
|
.strings(&[StringDescriptors::default()
|
||||||
.manufacturer(hardware::usb::MANUFACTURER)
|
.manufacturer(hardware::usb::MANUFACTURER)
|
||||||
.product(hardware::usb::PRODUCT)
|
.product(hardware::usb::PRODUCT)
|
||||||
@ -275,63 +46,45 @@ fn main() -> ! {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Calibration data initialization: load axis calibration and gimbal mode from EEPROM
|
let mut state = JoystickState::new();
|
||||||
|
status_led.update(StatusMode::Error);
|
||||||
|
|
||||||
// Load calibration data from EEPROM using CalibrationManager
|
button_matrix.prime(&mut delay, 10);
|
||||||
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
|
let initial_pressed = button_matrix.buttons_pressed();
|
||||||
CalibrationManager::load_axis_calibration(&mut axis_manager.axes, &mut read_fn);
|
if bootloader::startup_requested(&initial_pressed) {
|
||||||
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
|
bootloader::enter(&mut status_led);
|
||||||
axis_manager.set_gimbal_mode(gimbal_mode);
|
}
|
||||||
calibration_manager.set_gimbal_mode(gimbal_mode);
|
|
||||||
|
{
|
||||||
|
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
|
||||||
|
state.load_calibration(&mut read_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut scan_tick = timer.count_down();
|
||||||
|
scan_tick.start(timers::SCAN_INTERVAL_US.micros());
|
||||||
|
|
||||||
|
let mut status_tick = timer.count_down();
|
||||||
|
status_tick.start(timers::STATUS_LED_INTERVAL_MS.millis());
|
||||||
|
|
||||||
|
let mut usb_tick = timer.count_down();
|
||||||
|
usb_tick.start(timers::USB_UPDATE_INTERVAL_MS.millis());
|
||||||
|
|
||||||
|
let mut status_time_ms: u32 = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// 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]) {
|
if usb_dev.poll(&mut [&mut usb_hid_joystick]) {
|
||||||
if !usb_initialized {
|
state.usb_state().on_poll();
|
||||||
usb_initialized = true;
|
|
||||||
}
|
|
||||||
if !usb_active {
|
|
||||||
usb_activity = true; // Force initial report
|
|
||||||
idle_mode = false;
|
|
||||||
usb_activity_timeout_count = 0;
|
|
||||||
usb_send_pending = true;
|
|
||||||
}
|
|
||||||
usb_active = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check USB device state for suspend/resume handling
|
|
||||||
let usb_state = usb_dev.state();
|
let usb_state = usb_dev.state();
|
||||||
let was_suspended = usb_suspended;
|
state.usb_state().on_suspend_change(usb_state);
|
||||||
usb_suspended = usb_state == UsbDeviceState::Suspend;
|
|
||||||
|
|
||||||
// Handle USB resume transition
|
if status_tick.wait().is_ok() {
|
||||||
if was_suspended && !usb_suspended {
|
status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS);
|
||||||
// Device was suspended and is now resumed
|
status_led.update_from_system_state(state.system_state(), status_time_ms);
|
||||||
usb_activity = true;
|
|
||||||
idle_mode = false;
|
|
||||||
usb_activity_timeout_count = 0;
|
|
||||||
usb_send_pending = true;
|
|
||||||
wake_on_input = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle USB suspend transition
|
let should_scan = if state.usb_state().suspended {
|
||||||
if !was_suspended && usb_suspended {
|
|
||||||
// Device has just been suspended - enter power saving mode
|
|
||||||
idle_mode = true;
|
|
||||||
usb_activity = false;
|
|
||||||
usb_send_pending = false;
|
|
||||||
wake_on_input = true;
|
|
||||||
|
|
||||||
// Reduce LED update frequency to save power when suspended
|
|
||||||
// LED will be off anyway (Suspended mode), so slow updates are fine
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip high-frequency scanning when suspended to save power
|
|
||||||
// Only scan periodically to detect wake-up inputs
|
|
||||||
let should_scan = if usb_suspended {
|
|
||||||
// When suspended, reduce scan frequency by factor of 10 (every ~2ms instead of 200μs)
|
|
||||||
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
|
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
|
||||||
unsafe {
|
unsafe {
|
||||||
SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 10;
|
SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 10;
|
||||||
@ -341,219 +94,83 @@ fn main() -> ! {
|
|||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
if scan_count_down.wait().is_ok() && should_scan {
|
if should_scan && scan_tick.wait().is_ok() {
|
||||||
// ## High-Frequency Input Sampling (~5 kHz)
|
|
||||||
//
|
|
||||||
// Sample all inputs at high frequency for responsive control:
|
|
||||||
// - Button matrix scanning with debouncing
|
|
||||||
// - ADC reading from all 4 gimbal axes
|
|
||||||
// - Digital filtering for noise reduction
|
|
||||||
|
|
||||||
// Scan 5x5 button matrix for input changes
|
|
||||||
button_matrix.scan_matrix(&mut delay);
|
button_matrix.scan_matrix(&mut delay);
|
||||||
|
|
||||||
// Read raw 12-bit ADC values from all 4 gimbal potentiometers
|
|
||||||
let mut raw_values = [
|
let mut raw_values = [
|
||||||
adc.read(&mut adc_pin_left_x).unwrap(),
|
adc.read(&mut axis_pins.left_x).unwrap(),
|
||||||
adc.read(&mut adc_pin_left_y).unwrap(),
|
adc.read(&mut axis_pins.left_y).unwrap(),
|
||||||
adc.read(&mut adc_pin_right_x).unwrap(),
|
adc.read(&mut axis_pins.right_x).unwrap(),
|
||||||
adc.read(&mut adc_pin_right_y).unwrap(),
|
adc.read(&mut axis_pins.right_y).unwrap(),
|
||||||
];
|
];
|
||||||
|
state.tick_smoothers(&mut raw_values);
|
||||||
|
|
||||||
// Apply hardware-specific axis compensation (M10/M7 differences)
|
state.update_button_states(
|
||||||
axis_manager.apply_gimbal_compensation(&mut raw_values);
|
&mut button_matrix,
|
||||||
|
&mut left_extra_button,
|
||||||
// Apply digital smoothing filters to reduce ADC noise and jitter
|
&mut right_extra_button,
|
||||||
axis_manager.update_smoothers(&mut smoother, &raw_values);
|
|
||||||
|
|
||||||
// ## Immediate Data Processing (formerly 1000 Hz)
|
|
||||||
//
|
|
||||||
// Process all input data right after sampling for minimal latency.
|
|
||||||
|
|
||||||
// Update button states from matrix scan and extra buttons
|
|
||||||
button_manager.update_from_matrix(&mut button_matrix);
|
|
||||||
button_manager.update_extra_buttons(&mut left_extra_button, &mut right_extra_button);
|
|
||||||
button_manager.filter_hat_switches();
|
|
||||||
|
|
||||||
// Process special button combinations for system control
|
|
||||||
let action = button_manager.check_special_combinations(
|
|
||||||
axis_manager.get_value_before_hold(),
|
|
||||||
calibration_manager.is_active(),
|
|
||||||
);
|
);
|
||||||
match action {
|
|
||||||
SpecialAction::Bootloader => {
|
let action = state.check_special_action();
|
||||||
status_led.update(StatusMode::Bootloader);
|
if matches!(action, SpecialAction::Bootloader) {
|
||||||
let gpio_activity_pin_mask: u32 = 0;
|
if !state.usb_state().suspended {
|
||||||
let disable_interface_mask: u32 = 0;
|
let clear_report = JoystickState::empty_report();
|
||||||
rp2040_hal::rom_data::reset_to_usb_boot(
|
for _ in 0..3 {
|
||||||
gpio_activity_pin_mask,
|
match usb_hid_joystick.device().write_report(&clear_report) {
|
||||||
disable_interface_mask,
|
Ok(_) => break,
|
||||||
);
|
Err(UsbHidError::WouldBlock) => {
|
||||||
}
|
let _ = usb_hid_joystick.tick();
|
||||||
SpecialAction::StartCalibration => {
|
}
|
||||||
for (index, item) in axis_manager.axes.iter_mut().enumerate() {
|
Err(_) => break,
|
||||||
item.center = smoother[index].value() as u16;
|
}
|
||||||
item.min = item.center;
|
|
||||||
item.max = item.center;
|
|
||||||
}
|
|
||||||
axis_manager.clear_throttle_hold(); // Clear throttle hold when cancelling calibration
|
|
||||||
calibration_manager.start_calibration();
|
|
||||||
}
|
|
||||||
SpecialAction::CancelCalibration => {
|
|
||||||
calibration_manager.stop_calibration();
|
|
||||||
}
|
|
||||||
SpecialAction::ThrottleHold(hold_value) => {
|
|
||||||
axis_manager.set_throttle_hold(hold_value);
|
|
||||||
}
|
|
||||||
SpecialAction::VirtualThrottleToggle => {
|
|
||||||
vt_enable = !vt_enable;
|
|
||||||
}
|
|
||||||
SpecialAction::CalibrationSetModeM10 => {
|
|
||||||
// Set gimbal mode to M10 and reset calibration
|
|
||||||
if calibration_manager.set_gimbal_mode_m10(&mut axis_manager.axes, &smoother) {
|
|
||||||
axis_manager.set_gimbal_mode(calibration_manager.get_gimbal_mode());
|
|
||||||
axis_manager.clear_throttle_hold(); // Clear holds after mode change
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpecialAction::CalibrationSetModeM7 => {
|
bootloader::enter(&mut status_led);
|
||||||
// Set gimbal mode to M7 and reset calibration
|
} else if !matches!(action, SpecialAction::None) {
|
||||||
if calibration_manager.set_gimbal_mode_m7(&mut axis_manager.axes, &smoother) {
|
let mut write_page =
|
||||||
axis_manager.set_gimbal_mode(calibration_manager.get_gimbal_mode());
|
|page: u32, data: &[u8]| eeprom.write_page(page, data).map_err(|_| ());
|
||||||
axis_manager.clear_throttle_hold(); // Clear holds after mode change
|
state.handle_special_action(action, &mut write_page);
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialAction::CalibrationSave => {
|
|
||||||
// Save calibration data and end calibration mode
|
|
||||||
calibration_manager
|
|
||||||
.save_calibration(&axis_manager.axes, &mut |page: u32, data: &[u8]| {
|
|
||||||
eeprom.write_page(page, data).map_err(|_| ())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
SpecialAction::None => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always update calibration for dynamic min/max tracking when active
|
state.update_calibration_tracking();
|
||||||
calibration_manager.update_dynamic_calibration(&mut axis_manager.axes, &smoother);
|
|
||||||
|
|
||||||
// Process gimbal axes through calibration, expo curves, and scaling
|
if state.process_axes() {
|
||||||
if axis_manager.process_axis_values(&smoother, &expo_lut) {
|
state.usb_state().handle_input_activity();
|
||||||
usb_activity = true;
|
|
||||||
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
|
||||||
idle_mode = false;
|
|
||||||
usb_send_pending = true;
|
|
||||||
|
|
||||||
// Wake from USB suspend if input detected
|
|
||||||
if wake_on_input && usb_suspended {
|
|
||||||
// TODO: Implement remote wakeup if supported by host
|
|
||||||
wake_on_input = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update virtual axes based on front button states
|
if state.update_virtual_axes() {
|
||||||
if axis_manager.update_virtual_axes(button_manager.buttons(), vt_enable) {
|
state.usb_state().handle_input_activity();
|
||||||
usb_activity = true;
|
|
||||||
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
|
||||||
idle_mode = false;
|
|
||||||
usb_send_pending = true;
|
|
||||||
|
|
||||||
// Wake from USB suspend if input detected
|
|
||||||
if wake_on_input && usb_suspended {
|
|
||||||
// TODO: Implement remote wakeup if supported by host
|
|
||||||
wake_on_input = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process button logic (press types, timing, USB mapping)
|
if state.finalize_button_logic(&timer) {
|
||||||
if button_manager.process_button_logic_with_timer(&timer) {
|
state.usb_state().handle_input_activity();
|
||||||
usb_activity = true;
|
|
||||||
usb_activity_timeout_count = 0; // Reset timeout on real input activity
|
|
||||||
idle_mode = false;
|
|
||||||
usb_send_pending = true;
|
|
||||||
|
|
||||||
// Wake from USB suspend if input detected
|
|
||||||
if wake_on_input && usb_suspended {
|
|
||||||
// TODO: Implement remote wakeup if supported by host
|
|
||||||
wake_on_input = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if status_led_count_down.wait().is_ok() {
|
let usb_tick_elapsed = usb_tick.wait().is_ok();
|
||||||
// ## Status LED Updates (100Hz)
|
if usb_tick_elapsed {
|
||||||
//
|
state
|
||||||
// Update status LED to reflect current system state:
|
.usb_state()
|
||||||
// - Green: Normal operation with USB connection
|
.advance_idle_timer(timers::USB_UPDATE_INTERVAL_MS);
|
||||||
// - Blue: Calibration mode active
|
|
||||||
// - Yellow: Throttle hold or Virtual Throttle enabled
|
|
||||||
// - Red: Error state or disconnected
|
|
||||||
// - Purple: Bootloader mode
|
|
||||||
|
|
||||||
let system_state = SystemState {
|
|
||||||
usb_active,
|
|
||||||
usb_initialized,
|
|
||||||
usb_suspended,
|
|
||||||
idle_mode,
|
|
||||||
calibration_active: calibration_manager.is_active(),
|
|
||||||
throttle_hold_enable: axis_manager.throttle_hold_enable,
|
|
||||||
vt_enable,
|
|
||||||
};
|
|
||||||
status_led.update_from_system_state(
|
|
||||||
system_state,
|
|
||||||
(timer.get_counter().ticks() / 1000) as u32,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// ## USB HID Report Transmission (up to 1 kHz)
|
|
||||||
//
|
|
||||||
// Transmit USB HID reports only when there is input activity.
|
|
||||||
// This power-management approach prevents the computer from staying
|
|
||||||
// awake unnecessarily while maintaining responsive control.
|
|
||||||
//
|
|
||||||
// The report includes:
|
|
||||||
// - All 7 analog axes with proper scaling
|
|
||||||
// - 32-button bitmask with USB mapping
|
|
||||||
// - 8-direction HAT switch state
|
|
||||||
// - Virtual throttle mode handling
|
|
||||||
|
|
||||||
// Only transmit USB reports when input activity is detected and not suspended
|
if state.usb_state().activity
|
||||||
let usb_tick = usb_update_count_down.wait().is_ok();
|
&& (usb_tick_elapsed || state.usb_state().send_pending)
|
||||||
if usb_activity && (usb_tick || usb_send_pending) && !usb_suspended {
|
&& !state.usb_state().suspended
|
||||||
let mut send_report = || {
|
{
|
||||||
let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual);
|
let report = state.build_report();
|
||||||
let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual);
|
match usb_hid_joystick.device().write_report(&report) {
|
||||||
match usb_hid_joystick.device().write_report(&get_joystick_report(
|
Err(UsbHidError::WouldBlock) => {}
|
||||||
button_manager.buttons_mut(),
|
Ok(_) => {
|
||||||
&mut axis_manager.axes,
|
state.usb_state().acknowledge_report();
|
||||||
virtual_ry_value,
|
|
||||||
virtual_rz_value,
|
|
||||||
&vt_enable,
|
|
||||||
)) {
|
|
||||||
Err(UsbHidError::WouldBlock) => {}
|
|
||||||
Ok(_) => {
|
|
||||||
usb_send_pending = false;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
status_led.update(StatusMode::Error);
|
|
||||||
core::panic!("Failed to write joystick report: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
Err(error) => {
|
||||||
|
status_led.update(StatusMode::Error);
|
||||||
if usb_tick {
|
panic!("Failed to write joystick report: {:?}", error);
|
||||||
usb_activity_timeout_count += timers::USB_UPDATE_INTERVAL_MS;
|
|
||||||
if usb_activity_timeout_count >= timers::USB_ACTIVITY_TIMEOUT_MS {
|
|
||||||
usb_activity = false;
|
|
||||||
usb_activity_timeout_count = 0;
|
|
||||||
idle_mode = true;
|
|
||||||
usb_send_pending = false;
|
|
||||||
} else {
|
|
||||||
send_report();
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
send_report();
|
|
||||||
}
|
}
|
||||||
} else if usb_tick && usb_active && !usb_suspended {
|
} else if usb_tick_elapsed && state.usb_state().active && !state.usb_state().suspended {
|
||||||
// Only update idle mode for non-suspended devices
|
state.usb_state().idle_mode = true;
|
||||||
idle_mode = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
tools/copy_uf2.py
Executable file
110
tools/copy_uf2.py
Executable file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Copy a UF2 artifact to a detected RP2040 mass-storage mount."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
INFO_FILE = "INFO_UF2.TXT"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--source", type=Path, required=True, help="Path to the UF2 file to copy")
|
||||||
|
parser.add_argument("--timeout", type=float, default=10.0, help="Seconds to wait for the mount")
|
||||||
|
parser.add_argument(
|
||||||
|
"--mount",
|
||||||
|
type=str,
|
||||||
|
default=os.environ.get("MOUNT", ""),
|
||||||
|
help="Explicit mount point (default: auto-detect)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def candidate_paths(explicit: str, user: str) -> list[Path]:
|
||||||
|
paths: list[Path] = []
|
||||||
|
if explicit:
|
||||||
|
paths.append(Path(explicit))
|
||||||
|
roots = [
|
||||||
|
Path("/Volumes"),
|
||||||
|
Path("/media"),
|
||||||
|
Path(f"/media/{user}"),
|
||||||
|
Path("/run/media"),
|
||||||
|
Path(f"/run/media/{user}"),
|
||||||
|
]
|
||||||
|
for root in roots:
|
||||||
|
if not root.exists() or not root.is_dir():
|
||||||
|
continue
|
||||||
|
for child in root.iterdir():
|
||||||
|
if child.is_dir():
|
||||||
|
paths.append(child)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def choose_mount(explicit: str, user: str) -> Path | None:
|
||||||
|
candidates = candidate_paths(explicit, user)
|
||||||
|
if explicit:
|
||||||
|
path = Path(explicit)
|
||||||
|
return path if path.exists() and path.is_dir() else None
|
||||||
|
info_candidates = [path for path in candidates if (path / INFO_FILE).exists()]
|
||||||
|
if info_candidates:
|
||||||
|
return info_candidates[0]
|
||||||
|
for path in candidates:
|
||||||
|
if path.exists() and path.is_dir():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
source = args.source
|
||||||
|
if not source.exists():
|
||||||
|
print(f"UF2 source file not found: {source}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
explicit_mount = args.mount.strip()
|
||||||
|
user = os.environ.get("USER", "")
|
||||||
|
deadline = time.time() + float(args.timeout)
|
||||||
|
|
||||||
|
while time.time() <= deadline:
|
||||||
|
mount = choose_mount(explicit_mount, user)
|
||||||
|
if mount is not None:
|
||||||
|
if not mount.exists() or not mount.is_dir():
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
destination = mount / source.name
|
||||||
|
try:
|
||||||
|
shutil.copy2(source, destination)
|
||||||
|
try:
|
||||||
|
if hasattr(os, "sync"):
|
||||||
|
os.sync()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
time.sleep(0.5)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(f"Failed to copy UF2 to {destination}: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"Copied {source} to {destination}")
|
||||||
|
return 0
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if explicit_mount:
|
||||||
|
print(
|
||||||
|
f"Mount point '{explicit_mount}' not found within {args.timeout} seconds",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Unable to detect RP2040 UF2 mount. Pass one via mount=/path",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
sys.exit(main())
|
||||||
Loading…
x
Reference in New Issue
Block a user