4.7 KiB
synth-embedded
Real-time audio synthesizer firmware for the Raspberry Pi Pico 2 (RP2350), built on the Embassy async runtime. Outputs 48 kHz stereo I2S audio to a PCM5102A DAC and accepts MIDI input over UART.
Hardware
| GPIO | Signal | Connected to |
|---|---|---|
| 9 | BCK (bit clock) | PCM5102A BCK |
| 10 | LRCK (word select) | PCM5102A LRCK |
| 11 | DATA | PCM5102A DIN |
| 1 | UART0 RX | MIDI IN (optocoupler) |
Any PCM5102A-compatible I2S DAC (e.g., the CJMCU-5102 module) works. Standard MIDI current-loop interface: 5 mA optocoupler on GPIO 1, no UART TX required.
Architecture
Two Embassy tasks run concurrently on core 0:
┌──────────────┐ Mutex<SynthParams> ┌──────────────┐
│ midi_task │ ──────────────────────────▶│ audio_task │
│ (UART RX) │ NoteOn/Off, CC, PB, PC │ (PIO I2S) │
└──────────────┘ └──────────────┘
│ │
GPIO 1 GPIO 9/10/11
31 250 baud 48 kHz I2S
audio_task
- Initialises the
PioI2sOutdriver (PIO0 state machine 0, 24-bit, 48 kHz). - Creates the DSP chain: VCO → SVF → VCA ← ADSR.
- Runs a double-buffered DMA loop — while one 256-frame block transfers to the PIO FIFO, the next block is rendered on the CPU.
Block size: 256 frames × 2 channels × 4 bytes = 2 KB per buffer, 5.33 ms per block. At 150 MHz the Cortex-M33 has roughly 10× headroom for the DSP render.
Sample format: 24-bit signed PCM, left-justified in bits 31..8 of a u32 word, interleaved L/R: [L0, R0, L1, R1, …].
midi_task
Reads raw bytes from UART0 at 31 250 baud (standard MIDI) and feeds them to synth_core::MidiParser. Decoded events update the shared PARAMS mutex:
| Event | Action |
|---|---|
| Note On | Set note, velocity, gate = true |
| Note Off | Clear gate if note matches |
| CC 1 (mod wheel) | Filter resonance |
| CC 7 | Master volume |
| CC 72 | Release time (0–8 s) |
| CC 73 | Attack time (0–4 s) |
| CC 74 | Filter cutoff (80 Hz–18 kHz) |
| CC 75 | Decay time (0–4 s) |
| Pitch Bend | ±2 semitones |
| Program Change | Waveform (0=Sine 1=Saw 2=Square 3=Triangle 4=Pulse) |
Building
Prerequisites
# Rust target for RP2350 Cortex-M33
rustup target add thumbv8m.main-none-eabihf
# probe-rs for flashing
cargo install probe-rs-tools
Build
cargo build -p synth-embedded --release
Flash
Connect a debug probe (Raspberry Pi Debug Probe, J-Link, etc.) and run:
cargo run -p synth-embedded --release
# or equivalently:
probe-rs run --chip RP2350 target/thumbv8m.main-none-eabihf/release/synth-embedded
The runner in .cargo/config.toml calls probe-rs run automatically.
Debug logging
defmt logs are streamed over RTT. View them with:
probe-rs attach --chip RP2350 --log-format '{t} {L} {s}'
The DEFMT_LOG environment variable (set to debug by default in .cargo/config.toml) controls verbosity.
Linker Setup
The crate uses a custom memory.x to describe the RP2350's 2 MB flash and 520 KB SRAM. build.rs copies it to the build output directory so cortex-m-rt's link.x can find it via INCLUDE memory.x.
.cargo/config.toml passes two linker scripts:
-Tdefmt.x— places the defmt log sections-Tlink.x— cortex-m-rt startup, vector table, andINCLUDE memory.x
Dependencies
| Crate | Purpose |
|---|---|
synth-core |
DSP modules (VCO, SVF, ADSR, VCA, MIDI parser) |
embassy-executor |
Async task executor (Cortex-M) |
embassy-rp |
RP2350 HAL + PIO I2S driver |
embassy-sync |
Mutex for shared parameter state |
embassy-time |
Timekeeping |
cortex-m / cortex-m-rt |
ARM Cortex-M runtime |
defmt + defmt-rtt |
Structured logging over RTT |
panic-probe |
Panic messages sent to debug probe |
fixed |
Fixed-point clock divisor math |
libm |
no_std math (powf for pitch bend) |
Customisation
Change the oscillator waveform default — edit SynthParams::new() in src/params.rs.
Change block size — edit BLOCK_FRAMES in src/audio.rs. Larger blocks reduce CPU wake-up overhead; smaller blocks reduce latency.
Add a second voice — instantiate a second (Vco, Adsr, Svf, Vca) tuple in audio_task, mix both outputs before the DMA write.
Change GPIO pins — update the constants in src/main.rs and re-wire accordingly.