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
|
||||
//! 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
|
||||
//! Mirrors the refactor performed for the keyboard firmware: the matrix owns
|
||||
//! concrete pins, exposes a small `MatrixPinAccess` trait, and keeps the
|
||||
//! debouncing + minimum press spacing behaviour identical to the original
|
||||
//! joystick implementation.
|
||||
|
||||
use core::convert::Infallible;
|
||||
use cortex_m::delay::Delay;
|
||||
use embedded_hal::digital::{InputPin, OutputPin};
|
||||
use rp2040_hal::gpio::{DynPinId, FunctionSioInput, FunctionSioOutput, Pin, PullNone, PullUp};
|
||||
|
||||
/// Button matrix driver
|
||||
///
|
||||
/// Generics
|
||||
/// - `R`: number of rows
|
||||
/// - `C`: number of columns
|
||||
/// - `N`: total number of buttons (usually `R * C`)
|
||||
///
|
||||
/// Example
|
||||
/// ```ignore
|
||||
/// // 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],
|
||||
cols: &'a mut [&'a mut dyn OutputPin<Error = Infallible>; C],
|
||||
pressed: [bool; N],
|
||||
debounce: u8,
|
||||
debounce_counter: [u8; N],
|
||||
// Anti-bounce protection: minimum time between same-button presses
|
||||
last_press_scan: [u32; N],
|
||||
/// Abstraction over the matrix pins so the scanner can work with either the
|
||||
/// concrete RP2040 pins or test doubles.
|
||||
pub trait MatrixPinAccess<const ROWS: usize, const COLS: usize> {
|
||||
fn init_columns(&mut self);
|
||||
fn set_column_low(&mut self, column: usize);
|
||||
fn set_column_high(&mut self, column: usize);
|
||||
fn read_row(&mut self, row: usize) -> bool;
|
||||
}
|
||||
|
||||
/// Concrete matrix pins backed by RP2040 GPIO using dynamic pin IDs.
|
||||
type RowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
|
||||
type ColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
|
||||
|
||||
pub struct MatrixPins<const ROWS: usize, const COLS: usize> {
|
||||
rows: [RowPin; ROWS],
|
||||
cols: [ColPin; COLS],
|
||||
}
|
||||
|
||||
impl<const ROWS: usize, const COLS: usize> MatrixPins<ROWS, COLS> {
|
||||
pub fn new(rows: [RowPin; ROWS], cols: [ColPin; COLS]) -> Self {
|
||||
Self { rows, cols }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const ROWS: usize, const COLS: usize> MatrixPinAccess<ROWS, COLS> for MatrixPins<ROWS, COLS> {
|
||||
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,
|
||||
}
|
||||
|
||||
impl<'a, const R: usize, const C: usize, const N: usize> ButtonMatrix<'a, R, C, N> {
|
||||
/// Creates a new button matrix.
|
||||
///
|
||||
/// 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],
|
||||
debounce: u8,
|
||||
) -> Self {
|
||||
impl<P, const ROWS: usize, const COLS: usize, const BUTTONS: usize>
|
||||
ButtonMatrix<P, ROWS, COLS, BUTTONS>
|
||||
where
|
||||
P: MatrixPinAccess<ROWS, COLS>,
|
||||
{
|
||||
pub fn new(pins: P, debounce_threshold: u8, min_press_gap_scans: u32) -> Self {
|
||||
debug_assert_eq!(BUTTONS, ROWS * COLS);
|
||||
Self {
|
||||
rows,
|
||||
cols,
|
||||
pressed: [false; N],
|
||||
debounce,
|
||||
debounce_counter: [0; N],
|
||||
last_press_scan: [0; N],
|
||||
pins,
|
||||
pressed: [false; BUTTONS],
|
||||
debounce_threshold,
|
||||
debounce_counter: [0; BUTTONS],
|
||||
last_press_scan: [0; BUTTONS],
|
||||
min_press_gap_scans,
|
||||
scan_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
self.pins.init_columns();
|
||||
}
|
||||
|
||||
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) {
|
||||
self.scan_counter = self.scan_counter.wrapping_add(1);
|
||||
for col_index in 0..self.cols.len() {
|
||||
self.cols[col_index].set_low().unwrap();
|
||||
for column in 0..COLS {
|
||||
self.pins.set_column_low(column);
|
||||
delay.delay_us(1);
|
||||
self.process_column(col_index);
|
||||
self.cols[col_index].set_high().unwrap();
|
||||
self.process_column(column);
|
||||
self.pins.set_column_high(column);
|
||||
delay.delay_us(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a single column: drive low, sample rows, update debounce state, then release high.
|
||||
///
|
||||
/// 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();
|
||||
pub fn buttons_pressed(&self) -> [bool; BUTTONS] {
|
||||
self.pressed
|
||||
}
|
||||
|
||||
if current_state == self.pressed[button_index] {
|
||||
self.debounce_counter[button_index] = 0;
|
||||
fn process_column(&mut self, column: usize) {
|
||||
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;
|
||||
}
|
||||
|
||||
self.debounce_counter[button_index] += 1;
|
||||
if self.debounce_counter[button_index] >= self.debounce {
|
||||
// Anti-bounce protection for press events: minimum 25 scans (5ms) between presses
|
||||
if current_state {
|
||||
// Pressing
|
||||
let scans_since_last = self
|
||||
.scan_counter
|
||||
.wrapping_sub(self.last_press_scan[button_index]);
|
||||
if scans_since_last >= 25 {
|
||||
// 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[index] = self.debounce_counter[index].saturating_add(1);
|
||||
if self.debounce_counter[index] < self.debounce_threshold {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.debounce_counter[index] = 0;
|
||||
if current_state {
|
||||
if self.should_register_press(index) {
|
||||
self.pressed[index] = true;
|
||||
}
|
||||
self.debounce_counter[button_index] = 0;
|
||||
} else {
|
||||
self.pressed[index] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn should_register_press(&mut self, index: usize) -> bool {
|
||||
let elapsed = self.scan_counter.wrapping_sub(self.last_press_scan[index]);
|
||||
let can_register = self.last_press_scan[index] == 0 || elapsed >= self.min_press_gap_scans;
|
||||
|
||||
if can_register {
|
||||
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 {
|
||||
use super::*;
|
||||
use core::cell::Cell;
|
||||
use embedded_hal::digital::ErrorType;
|
||||
use std::rc::Rc;
|
||||
|
||||
struct MockInputPin {
|
||||
state: Rc<Cell<bool>>,
|
||||
#[derive(Clone)]
|
||||
struct MockPins {
|
||||
row_state: Rc<Cell<bool>>,
|
||||
column_state: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl MockInputPin {
|
||||
fn new(state: Rc<Cell<bool>>) -> Self {
|
||||
Self { state }
|
||||
impl MockPins {
|
||||
fn new(row_state: Rc<Cell<bool>>, column_state: Rc<Cell<bool>>) -> Self {
|
||||
Self {
|
||||
row_state,
|
||||
column_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorType for MockInputPin {
|
||||
type Error = Infallible;
|
||||
}
|
||||
|
||||
impl InputPin for MockInputPin {
|
||||
fn is_high(&mut self) -> Result<bool, Self::Error> {
|
||||
Ok(!self.state.get())
|
||||
impl MatrixPinAccess<1, 1> for MockPins {
|
||||
fn init_columns(&mut self) {
|
||||
self.column_state.set(true);
|
||||
}
|
||||
|
||||
fn is_low(&mut self) -> Result<bool, Self::Error> {
|
||||
Ok(self.state.get())
|
||||
fn set_column_low(&mut self, _column: usize) {
|
||||
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 {
|
||||
state: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
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>,
|
||||
fn fixture() -> (
|
||||
ButtonMatrix<MockPins, 1, 1, 1>,
|
||||
Rc<Cell<bool>>,
|
||||
Rc<Cell<bool>>,
|
||||
) {
|
||||
let row_state = Rc::new(Cell::new(false));
|
||||
let col_state = Rc::new(Cell::new(false));
|
||||
|
||||
let row_pin: &'static mut dyn InputPin<Error = Infallible> =
|
||||
Box::leak(Box::new(MockInputPin::new(row_state.clone())));
|
||||
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)
|
||||
let row = Rc::new(Cell::new(false));
|
||||
let column = Rc::new(Cell::new(true));
|
||||
let pins = MockPins::new(row.clone(), column.clone());
|
||||
let matrix = ButtonMatrix::new(pins, 2, 3);
|
||||
(matrix, row, column)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_pins_sets_columns_high() {
|
||||
let (mut matrix, _row_state, col_state) = matrix_fixture();
|
||||
assert!(!col_state.get());
|
||||
matrix.init_pins();
|
||||
assert!(col_state.get());
|
||||
}
|
||||
fn debounce_requires_consecutive_scans() {
|
||||
let (mut matrix, row, _column) = fixture();
|
||||
matrix.set_scan_counter(1);
|
||||
|
||||
#[test]
|
||||
fn process_column_obeys_debounce() {
|
||||
let (mut matrix, row_state, _col_state) = matrix_fixture();
|
||||
let mut states = matrix.buttons_pressed();
|
||||
assert!(!states[0]);
|
||||
row.set(true);
|
||||
matrix.bump_scan_counter();
|
||||
matrix.process_column_for_test(0);
|
||||
assert!(!matrix.buttons_pressed()[0]);
|
||||
|
||||
// Set scan counter to start with enough history
|
||||
matrix.scan_counter = 100;
|
||||
|
||||
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
|
||||
matrix.bump_scan_counter();
|
||||
matrix.process_column_for_test(0);
|
||||
assert!(matrix.buttons_pressed()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
//! - Evaluate special combinations (bootloader, calibration, etc.)
|
||||
//! - 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::mapping::*;
|
||||
use embedded_hal::digital::InputPin;
|
||||
@ -17,6 +17,13 @@ use rp2040_hal::timer::Timer;
|
||||
// Total buttons including the two extra (non‑matrix) buttons
|
||||
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 ====================
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
@ -75,10 +82,7 @@ impl ButtonManager {
|
||||
}
|
||||
|
||||
/// Update button states from the button matrix snapshot.
|
||||
pub fn update_from_matrix(
|
||||
&mut self,
|
||||
matrix: &mut ButtonMatrix<BUTTON_ROWS, BUTTON_COLS, NUMBER_OF_BUTTONS>,
|
||||
) {
|
||||
pub fn update_from_matrix(&mut self, matrix: &mut JoystickButtonMatrix) {
|
||||
for (index, key) in matrix.buttons_pressed().iter().enumerate() {
|
||||
self.buttons[index].pressed = *key;
|
||||
}
|
||||
|
||||
@ -1,105 +1,39 @@
|
||||
//! 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.
|
||||
//! Mirrors the structure introduced for the CMDR Keyboard firmware so that
|
||||
//! 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).
|
||||
pub const XTAL_FREQ_HZ: u32 = 12_000_000u32;
|
||||
/// USB Vendor ID.
|
||||
pub const XTAL_FREQ_HZ: u32 = 12_000_000;
|
||||
|
||||
/// USB Vendor ID/Product ID.
|
||||
pub const USB_VID: u16 = 0x1209;
|
||||
/// USB Product ID.
|
||||
pub const USB_PID: u16 = 0x0002;
|
||||
|
||||
// ==================== JOYSTICK CONSTANTS ====================
|
||||
/// Button matrix geometry (rows).
|
||||
/// Button matrix geometry (rows/cols) and count.
|
||||
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).
|
||||
|
||||
/// ADC characteristics.
|
||||
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.
|
||||
/// Increased from 10 to 15 scans to prevent double button presses from bounce.
|
||||
/// At 200μs scan rate: 15 scans = 3ms debounce time.
|
||||
pub const DEBOUNCE: u8 = 15;
|
||||
/// Bytes reserved in EEPROM for calibration data + gimbal mode.
|
||||
|
||||
/// Debounce thresholds.
|
||||
pub const MATRIX_DEBOUNCE_SCANS: u8 = 15;
|
||||
pub const MIN_PRESS_SPACING_SCANS: u32 = 25; // ~5ms @ 200µs cadence
|
||||
|
||||
/// EEPROM storage length (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;
|
||||
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.
|
||||
pub mod usb {
|
||||
pub const MANUFACTURER: &str = "CMtec";
|
||||
@ -107,86 +41,155 @@ pub mod usb {
|
||||
pub const SERIAL_NUMBER: &str = "0001";
|
||||
}
|
||||
|
||||
// ==================== PIN ACCESS MACROS ====================
|
||||
|
||||
/// 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) => {{
|
||||
const _: u8 = $crate::hardware::pins::LEFT_EXTRA_BUTTON_PIN;
|
||||
$pins.gpio1
|
||||
}};
|
||||
($pins:expr, right_extra_button) => {{
|
||||
const _: u8 = $crate::hardware::pins::RIGHT_EXTRA_BUTTON_PIN;
|
||||
$pins.gpio0
|
||||
}};
|
||||
($pins:expr, button_row_0) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_0;
|
||||
$pins.gpio6
|
||||
}};
|
||||
($pins:expr, button_row_1) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_1;
|
||||
$pins.gpio8
|
||||
}};
|
||||
($pins:expr, button_row_2) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_2;
|
||||
$pins.gpio4
|
||||
}};
|
||||
($pins:expr, button_row_3) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_3;
|
||||
$pins.gpio7
|
||||
}};
|
||||
($pins:expr, button_row_4) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_ROW_PIN_4;
|
||||
$pins.gpio5
|
||||
}};
|
||||
($pins:expr, button_col_0) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_0;
|
||||
$pins.gpio9
|
||||
}};
|
||||
($pins:expr, button_col_1) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_1;
|
||||
$pins.gpio10
|
||||
}};
|
||||
($pins:expr, button_col_2) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_2;
|
||||
$pins.gpio11
|
||||
}};
|
||||
($pins:expr, button_col_3) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_3;
|
||||
$pins.gpio12
|
||||
}};
|
||||
($pins:expr, button_col_4) => {{
|
||||
const _: u8 = $crate::hardware::pins::BUTTON_COL_PIN_4;
|
||||
$pins.gpio13
|
||||
}};
|
||||
($pins:expr, adc_left_x) => {{
|
||||
const _: u8 = $crate::hardware::pins::ADC_LEFT_X_PIN;
|
||||
$pins.gpio29
|
||||
}};
|
||||
($pins:expr, adc_left_y) => {{
|
||||
const _: u8 = $crate::hardware::pins::ADC_LEFT_Y_PIN;
|
||||
$pins.gpio28
|
||||
}};
|
||||
($pins:expr, adc_right_x) => {{
|
||||
const _: u8 = $crate::hardware::pins::ADC_RIGHT_X_PIN;
|
||||
$pins.gpio27
|
||||
}};
|
||||
($pins:expr, adc_right_y) => {{
|
||||
const _: u8 = $crate::hardware::pins::ADC_RIGHT_Y_PIN;
|
||||
$pins.gpio26
|
||||
}};
|
||||
($pins:expr, status_led) => {{
|
||||
const _: u8 = $crate::hardware::pins::STATUS_LED_PIN;
|
||||
$pins.gpio16
|
||||
}};
|
||||
($pins:expr, i2c_sda) => {{
|
||||
const _: u8 = $crate::hardware::pins::I2C_SDA_PIN;
|
||||
$pins.gpio14
|
||||
}};
|
||||
($pins:expr, i2c_scl) => {{
|
||||
const _: u8 = $crate::hardware::pins::I2C_SCL_PIN;
|
||||
$pins.gpio15
|
||||
}};
|
||||
/// Timing cadences.
|
||||
pub mod timers {
|
||||
pub const STATUS_LED_INTERVAL_MS: u32 = 40;
|
||||
pub const SCAN_INTERVAL_US: u32 = 200;
|
||||
pub const USB_UPDATE_INTERVAL_MS: u32 = 1;
|
||||
pub const USB_ACTIVITY_TIMEOUT_MS: u32 = 5_000;
|
||||
}
|
||||
|
||||
/// I2C helpers.
|
||||
pub mod i2c {
|
||||
use eeprom24x::SlaveAddr;
|
||||
use fugit::{Rate, RateExtU32};
|
||||
|
||||
pub const FREQUENCY_HZ: u32 = 400_000;
|
||||
pub const SYSTEM_CLOCK_HZ: u32 = 125_000_000;
|
||||
|
||||
pub fn frequency() -> Rate<u32, 1, 1> {
|
||||
FREQUENCY_HZ.Hz()
|
||||
}
|
||||
|
||||
pub fn system_clock() -> Rate<u32, 1, 1> {
|
||||
SYSTEM_CLOCK_HZ.Hz()
|
||||
}
|
||||
|
||||
pub const EEPROM_ADDRESS: SlaveAddr = SlaveAddr::Alternative(false, false, false);
|
||||
}
|
||||
|
||||
/// Raw GPIO constants retained for documentation/reference.
|
||||
pub mod pins {
|
||||
pub const LEFT_EXTRA_BUTTON: u8 = 1;
|
||||
pub const RIGHT_EXTRA_BUTTON: u8 = 0;
|
||||
pub const BUTTON_ROW_0: u8 = 6;
|
||||
pub const BUTTON_ROW_1: u8 = 8;
|
||||
pub const BUTTON_ROW_2: u8 = 4;
|
||||
pub const BUTTON_ROW_3: u8 = 7;
|
||||
pub const BUTTON_ROW_4: u8 = 5;
|
||||
pub const BUTTON_COL_0: u8 = 9;
|
||||
pub const BUTTON_COL_1: u8 = 10;
|
||||
pub const BUTTON_COL_2: u8 = 11;
|
||||
pub const BUTTON_COL_3: u8 = 12;
|
||||
pub const BUTTON_COL_4: u8 = 13;
|
||||
pub const ADC_LEFT_X: u8 = 29;
|
||||
pub const ADC_LEFT_Y: u8 = 28;
|
||||
pub const ADC_RIGHT_X: u8 = 27;
|
||||
pub const ADC_RIGHT_Y: u8 = 26;
|
||||
pub const STATUS_LED: u8 = 16;
|
||||
pub const I2C_SDA: u8 = 14;
|
||||
pub const I2C_SCL: u8 = 15;
|
||||
}
|
||||
|
||||
/// Matrix row pins (dynamic to simplify scanning code).
|
||||
pub type MatrixRowPin = Pin<DynPinId, FunctionSioInput, PullUp>;
|
||||
/// Matrix column pins (dynamic push-pull outputs).
|
||||
pub type MatrixColPin = Pin<DynPinId, FunctionSioOutput, PullNone>;
|
||||
/// Extra buttons (pull-up inputs).
|
||||
pub type ExtraButtonPin = Pin<DynPinId, FunctionSioInput, PullUp>;
|
||||
/// Status LED pin configured for PIO output.
|
||||
pub type StatusLedPin = Pin<gpio::bank0::Gpio16, FunctionPio0, PullNone>;
|
||||
/// I2C SDA/SCL pins after reconfiguration.
|
||||
pub type I2cSdaPin = Pin<gpio::bank0::Gpio14, FunctionI2C, PullUp>;
|
||||
pub type I2cSclPin = Pin<gpio::bank0::Gpio15, FunctionI2C, PullUp>;
|
||||
|
||||
/// Analog axis input pins (remain as SIO inputs until wrapped by `AdcPin`).
|
||||
pub struct AxisInputs {
|
||||
pub left_x: Pin<gpio::bank0::Gpio29, FunctionSioInput, PullNone>,
|
||||
pub left_y: Pin<gpio::bank0::Gpio28, FunctionSioInput, PullNone>,
|
||||
pub right_x: Pin<gpio::bank0::Gpio27, FunctionSioInput, PullNone>,
|
||||
pub right_y: Pin<gpio::bank0::Gpio26, FunctionSioInput, PullNone>,
|
||||
}
|
||||
|
||||
/// Bundle returned by `split_board_pins`.
|
||||
pub struct BoardPins {
|
||||
pub matrix_rows: [MatrixRowPin; BUTTON_ROWS],
|
||||
pub matrix_cols: [MatrixColPin; BUTTON_COLS],
|
||||
pub left_extra_button: ExtraButtonPin,
|
||||
pub right_extra_button: ExtraButtonPin,
|
||||
pub axis_inputs: AxisInputs,
|
||||
pub status_led: StatusLedPin,
|
||||
pub i2c_sda: I2cSdaPin,
|
||||
pub i2c_scl: I2cSclPin,
|
||||
}
|
||||
|
||||
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
|
||||
//! 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).
|
||||
pub mod axis;
|
||||
/// 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.
|
||||
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.
|
||||
pub use axis::{AxisManager, GimbalAxis, VirtualAxis};
|
||||
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_main]
|
||||
|
||||
mod axis;
|
||||
mod button_matrix;
|
||||
mod buttons;
|
||||
mod calibration;
|
||||
mod expo;
|
||||
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 cmdr_joystick_25::buttons::SpecialAction;
|
||||
use cmdr_joystick_25::hardware::{self, timers};
|
||||
use cmdr_joystick_25::status::StatusMode;
|
||||
use cmdr_joystick_25::usb_joystick_device::JoystickConfig;
|
||||
use cmdr_joystick_25::{bootloader, Board, BoardParts, JoystickState};
|
||||
use embedded_hal_0_2::adc::OneShot;
|
||||
use embedded_hal_0_2::timer::CountDown;
|
||||
use fugit::ExtU32;
|
||||
use hardware::timers;
|
||||
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 panic_halt as _;
|
||||
use usb_device::prelude::*;
|
||||
use usb_joystick_device::JoystickConfig;
|
||||
use usb_report::get_joystick_report;
|
||||
use usbd_human_interface_device::prelude::*;
|
||||
use usbd_human_interface_device::prelude::{UsbHidClassBuilder, UsbHidError};
|
||||
|
||||
#[panic_handler]
|
||||
fn panic(_info: &PanicInfo) -> ! {
|
||||
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]
|
||||
#[unsafe(link_section = ".boot2")]
|
||||
#[unsafe(no_mangle)]
|
||||
#[used]
|
||||
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]
|
||||
fn main() -> ! {
|
||||
// Hardware initialization and peripheral setup for joystick operation
|
||||
|
||||
// Acquire exclusive access to RP2040 peripherals
|
||||
let mut pac = pac::Peripherals::take().unwrap();
|
||||
|
||||
// Initialize watchdog timer (required for clock configuration)
|
||||
let mut watchdog = Watchdog::new(pac.WATCHDOG);
|
||||
|
||||
// Configure system clocks and phase-locked loops for stable operation
|
||||
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 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 BoardParts {
|
||||
mut button_matrix,
|
||||
mut status_led,
|
||||
mut delay,
|
||||
timer,
|
||||
mut adc,
|
||||
mut axis_pins,
|
||||
mut left_extra_button,
|
||||
mut right_extra_button,
|
||||
mut eeprom,
|
||||
usb_bus,
|
||||
} = Board::new().into_parts();
|
||||
|
||||
let mut usb_hid_joystick = UsbHidClassBuilder::new()
|
||||
.add_device(JoystickConfig::default())
|
||||
.build(&usb_bus);
|
||||
.build(usb_bus);
|
||||
|
||||
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()
|
||||
.manufacturer(hardware::usb::MANUFACTURER)
|
||||
.product(hardware::usb::PRODUCT)
|
||||
@ -275,63 +46,45 @@ fn main() -> ! {
|
||||
.unwrap()
|
||||
.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
|
||||
let mut read_fn = |addr: u32| eeprom.read_byte(addr).map_err(|_| ());
|
||||
CalibrationManager::load_axis_calibration(&mut axis_manager.axes, &mut read_fn);
|
||||
let gimbal_mode = CalibrationManager::load_gimbal_mode(&mut read_fn);
|
||||
axis_manager.set_gimbal_mode(gimbal_mode);
|
||||
calibration_manager.set_gimbal_mode(gimbal_mode);
|
||||
button_matrix.prime(&mut delay, 10);
|
||||
let initial_pressed = button_matrix.buttons_pressed();
|
||||
if bootloader::startup_requested(&initial_pressed) {
|
||||
bootloader::enter(&mut status_led);
|
||||
}
|
||||
|
||||
{
|
||||
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 {
|
||||
// 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_initialized {
|
||||
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;
|
||||
state.usb_state().on_poll();
|
||||
}
|
||||
|
||||
// Check USB device state for suspend/resume handling
|
||||
let usb_state = usb_dev.state();
|
||||
let was_suspended = usb_suspended;
|
||||
usb_suspended = usb_state == UsbDeviceState::Suspend;
|
||||
state.usb_state().on_suspend_change(usb_state);
|
||||
|
||||
// Handle USB resume transition
|
||||
if was_suspended && !usb_suspended {
|
||||
// Device was suspended and is now resumed
|
||||
usb_activity = true;
|
||||
idle_mode = false;
|
||||
usb_activity_timeout_count = 0;
|
||||
usb_send_pending = true;
|
||||
wake_on_input = false;
|
||||
if status_tick.wait().is_ok() {
|
||||
status_time_ms = status_time_ms.saturating_add(timers::STATUS_LED_INTERVAL_MS);
|
||||
status_led.update_from_system_state(state.system_state(), status_time_ms);
|
||||
}
|
||||
|
||||
// Handle USB suspend transition
|
||||
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)
|
||||
let should_scan = if state.usb_state().suspended {
|
||||
static mut SUSPENDED_SCAN_COUNTER: u8 = 0;
|
||||
unsafe {
|
||||
SUSPENDED_SCAN_COUNTER = (SUSPENDED_SCAN_COUNTER + 1) % 10;
|
||||
@ -341,219 +94,83 @@ fn main() -> ! {
|
||||
true
|
||||
};
|
||||
|
||||
if scan_count_down.wait().is_ok() && should_scan {
|
||||
// ## 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
|
||||
if should_scan && scan_tick.wait().is_ok() {
|
||||
button_matrix.scan_matrix(&mut delay);
|
||||
|
||||
// Read raw 12-bit ADC values from all 4 gimbal potentiometers
|
||||
let mut raw_values = [
|
||||
adc.read(&mut adc_pin_left_x).unwrap(),
|
||||
adc.read(&mut adc_pin_left_y).unwrap(),
|
||||
adc.read(&mut adc_pin_right_x).unwrap(),
|
||||
adc.read(&mut adc_pin_right_y).unwrap(),
|
||||
adc.read(&mut axis_pins.left_x).unwrap(),
|
||||
adc.read(&mut axis_pins.left_y).unwrap(),
|
||||
adc.read(&mut axis_pins.right_x).unwrap(),
|
||||
adc.read(&mut axis_pins.right_y).unwrap(),
|
||||
];
|
||||
state.tick_smoothers(&mut raw_values);
|
||||
|
||||
// Apply hardware-specific axis compensation (M10/M7 differences)
|
||||
axis_manager.apply_gimbal_compensation(&mut raw_values);
|
||||
|
||||
// Apply digital smoothing filters to reduce ADC noise and jitter
|
||||
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(),
|
||||
state.update_button_states(
|
||||
&mut button_matrix,
|
||||
&mut left_extra_button,
|
||||
&mut right_extra_button,
|
||||
);
|
||||
match action {
|
||||
SpecialAction::Bootloader => {
|
||||
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,
|
||||
);
|
||||
}
|
||||
SpecialAction::StartCalibration => {
|
||||
for (index, item) in axis_manager.axes.iter_mut().enumerate() {
|
||||
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
|
||||
|
||||
let action = state.check_special_action();
|
||||
if matches!(action, SpecialAction::Bootloader) {
|
||||
if !state.usb_state().suspended {
|
||||
let clear_report = JoystickState::empty_report();
|
||||
for _ in 0..3 {
|
||||
match usb_hid_joystick.device().write_report(&clear_report) {
|
||||
Ok(_) => break,
|
||||
Err(UsbHidError::WouldBlock) => {
|
||||
let _ = usb_hid_joystick.tick();
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
SpecialAction::CalibrationSetModeM7 => {
|
||||
// Set gimbal mode to M7 and reset calibration
|
||||
if calibration_manager.set_gimbal_mode_m7(&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::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 => {}
|
||||
bootloader::enter(&mut status_led);
|
||||
} else if !matches!(action, SpecialAction::None) {
|
||||
let mut write_page =
|
||||
|page: u32, data: &[u8]| eeprom.write_page(page, data).map_err(|_| ());
|
||||
state.handle_special_action(action, &mut write_page);
|
||||
}
|
||||
|
||||
// Always update calibration for dynamic min/max tracking when active
|
||||
calibration_manager.update_dynamic_calibration(&mut axis_manager.axes, &smoother);
|
||||
state.update_calibration_tracking();
|
||||
|
||||
// Process gimbal axes through calibration, expo curves, and scaling
|
||||
if axis_manager.process_axis_values(&smoother, &expo_lut) {
|
||||
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 state.process_axes() {
|
||||
state.usb_state().handle_input_activity();
|
||||
}
|
||||
|
||||
// Update virtual axes based on front button states
|
||||
if axis_manager.update_virtual_axes(button_manager.buttons(), vt_enable) {
|
||||
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 state.update_virtual_axes() {
|
||||
state.usb_state().handle_input_activity();
|
||||
}
|
||||
|
||||
// Process button logic (press types, timing, USB mapping)
|
||||
if button_manager.process_button_logic_with_timer(&timer) {
|
||||
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 state.finalize_button_logic(&timer) {
|
||||
state.usb_state().handle_input_activity();
|
||||
}
|
||||
}
|
||||
|
||||
if status_led_count_down.wait().is_ok() {
|
||||
// ## Status LED Updates (100Hz)
|
||||
//
|
||||
// Update status LED to reflect current system state:
|
||||
// - Green: Normal operation with USB connection
|
||||
// - 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,
|
||||
);
|
||||
let usb_tick_elapsed = usb_tick.wait().is_ok();
|
||||
if usb_tick_elapsed {
|
||||
state
|
||||
.usb_state()
|
||||
.advance_idle_timer(timers::USB_UPDATE_INTERVAL_MS);
|
||||
}
|
||||
// ## 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
|
||||
let usb_tick = usb_update_count_down.wait().is_ok();
|
||||
if usb_activity && (usb_tick || usb_send_pending) && !usb_suspended {
|
||||
let mut send_report = || {
|
||||
let virtual_ry_value = axis_manager.get_virtual_ry_value(&expo_lut_virtual);
|
||||
let virtual_rz_value = axis_manager.get_virtual_rz_value(&expo_lut_virtual);
|
||||
match usb_hid_joystick.device().write_report(&get_joystick_report(
|
||||
button_manager.buttons_mut(),
|
||||
&mut axis_manager.axes,
|
||||
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);
|
||||
}
|
||||
if state.usb_state().activity
|
||||
&& (usb_tick_elapsed || state.usb_state().send_pending)
|
||||
&& !state.usb_state().suspended
|
||||
{
|
||||
let report = state.build_report();
|
||||
match usb_hid_joystick.device().write_report(&report) {
|
||||
Err(UsbHidError::WouldBlock) => {}
|
||||
Ok(_) => {
|
||||
state.usb_state().acknowledge_report();
|
||||
}
|
||||
};
|
||||
|
||||
if usb_tick {
|
||||
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();
|
||||
Err(error) => {
|
||||
status_led.update(StatusMode::Error);
|
||||
panic!("Failed to write joystick report: {:?}", error);
|
||||
}
|
||||
} else {
|
||||
send_report();
|
||||
}
|
||||
} else if usb_tick && usb_active && !usb_suspended {
|
||||
// Only update idle mode for non-suspended devices
|
||||
idle_mode = true;
|
||||
} else if usb_tick_elapsed && state.usb_state().active && !state.usb_state().suspended {
|
||||
state.usb_state().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