An embedded firmware implementation for the RP2040 that transforms analog joystick inputs into USB HID joystick or keyboard reports. Built with Rust using Hexagonal Architecture and Domain-Driven Design principles.
This project implements two USB HID products that read analog inputs from two ADC channels (X and Y axes):
- Joystick: Reports standard HID joystick data (X/Y axes -32768 to 32767)
- Keyboard: Maps joystick direction to arrow keys (Up/Down/Left/Right)
The firmware is designed with clean architecture principles, ensuring testability and maintainability through clear separation of concerns.
The project follows Hexagonal Architecture (Ports & Adapters) with Domain-Driven Design:
-
Domain Layer (
domain/): Core business logic and domain entitiesJoystick: Domain entity representing the joystick abstractionJoystickData: Value object containing X/Y coordinates- Ports: Interfaces defining contracts (
AnalogicReader,AnalogicConverter)
-
Application Layer (
application/): Orchestrates domain logicJoystickReadUseCase: Encapsulates the business logic for reading and writing joystick reportHealthService: Encapsulates health monitor system
-
Infrastructure Layer (
infrastructure/): Hardware adapters and framework integrationcommon/: Shared adapters (ADC, USB, channel, signal, health monitor)joystick/: HID joystick report writer and bootstrapkeyboard/: HID keyboard report writer and bootstrap- Embassy framework integration for async embedded operations
- Raspberry Pi Pico (RP2040)
- Analog joystick module connected to:
- X-axis: GPIO 26 (ADC0)
- Y-axis: GPIO 27 (ADC1)
- USB connection for HID communication
- Rust toolchain (stable or nightly)
thumbv6m-none-eabitarget:rustup target add thumbv6m-none-eabiflip-linklinker:cargo install flip-linkpicotoolfor flashing (orprobe-rsfor debugging)
The project uses custom cargo configuration in .cargo/config.toml:
# Build joystick firmware
cargo build-joystick
# Build keyboard firmware
cargo build-keyboard
# Or build all
cargo build --releaseThe default runner is configured to use picotool:
# Joystick
cargo run-joystick
# Keyboard
cargo run-keyboardThis will:
- Build the firmware
- Flash it to the connected RP2040 (must be in boot mode)
- Execute the firmware
Enable USB CDC-ACM logging for debugging:
cargo run-joystick-dev # Joystick with USB logging
cargo run-keyboard-dev # Keyboard with USB loggingThis enables:
- USB CDC-ACM serial logging
- Debug log level output over USB
To use probe-rs instead of picotool, uncomment the corresponding line in .cargo/config.toml:
runner = "probe-rs run --chip RP2040 --protocol swd"The domain and application layers include unit tests that run on the host:
# Test domain layer
cargo test-domain
# Test application layer
cargo test-applicationThese aliases are configured in .cargo/config.toml to run tests with the native target.
# Build image
docker build -t psp-rp2040-ci .
# Run image with volume
docker run -it --rm \
-v $(pwd):/app \
-w /app \
psp-rp2040-ci
# Run tests
cargo test-domain
cargo test-application
# Build firmware
cargo build --releaseThis docker image and tests are configured in .github/workflows/ci.yml to run tests on push and build native target.
psp-rp2040/
├── domain/ # Core domain logic and ports
│ └── src/
│ ├── entity/ # Joystick and Health entities
│ ├── port/ # Outbound port interfaces
│ ├── vo/ # Joystick and Health reading intervals
│ ├── error.rs # Centralized result and error
│ ├── event.rs # Domain events
│ └── lib.rs # Modules
├── application/ # Use cases
│ └── src/
│ ├── port/ # Inbound port interfaces
│ ├── use_case/ # Business use cases
│ ├── service/ # Business services
│ └── lib.rs # Modules
├── infrastructure/ # Hardware adapters and dependencies
│ ├── common/ # Shared components (channel, signal, config, hardware)
│ ├── joystick/ # Joystick HID bootstrap and report writer
│ └── keyboard/ # Keyboard HID bootstrap and report writer
├── psp-rp2040-joystick/ # Binary crate (joystick composition root)
│ ├── src/main.rs # Entry point
│ └── Embed.toml # probe-rs configuration
├── psp-rp2040-keyboard/ # Binary crate (keyboard composition root)
│ ├── src/main.rs # Entry point
│ └── Embed.toml # probe-rs configuration
└── .cargo/
└── config.toml # Configuration and env variables
- USB HID Joystick: Standard HID joystick reports with X/Y axes (-32768 to 32767)
- USB HID Keyboard: Maps joystick direction to arrow keys (Up/Down/Left/Right)
- Analog Input Processing: Reads 12-bit ADC values and scales to 16-bit signed integers
- Async Architecture: Built on Embassy executor for efficient async/await support
- Testable Design: Domain and application layers are fully testable without hardware dependencies
- Target:
thumbv6m-none-eabi(ARM Cortex-M0+) - Framework: Embassy (async embedded Rust)
- USB Stack:
embassy-usbwithusbd-hidfor HID reports - ADC: RP2040 ADC with interrupt-driven async reads
- Polling Rate: 10ms (100 Hz) for joystick updates
Dual-licensed under MIT OR Apache-2.0.