# synth-embedded Real-time audio synthesizer firmware for the **Raspberry Pi Pico 2** (RP2350), built on the [Embassy](https://embassy.dev) 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 ┌──────────────┐ │ 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` 1. Initialises the `PioI2sOut` driver (PIO0 state machine 0, 24-bit, 48 kHz). 2. Creates the DSP chain: **VCO → SVF → VCA ← ADSR**. 3. 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 ```bash # Rust target for RP2350 Cortex-M33 rustup target add thumbv8m.main-none-eabihf # probe-rs for flashing cargo install probe-rs-tools ``` ### Build ```bash cargo build -p synth-embedded --release ``` ### Flash Connect a debug probe (Raspberry Pi Debug Probe, J-Link, etc.) and run: ```bash 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: ```bash 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, and `INCLUDE 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.