This document explains how the v3 firmware fits together — useful when adding a new HAL subsystem, a new DSP module, or trying to understand why the audio task wakes when it does.
┌────────────────────────────────────────────────────────────────┐
│ Application │ examples/, your code
│ Hardware::init() + Audio::set_callback() │
├────────────────────────────────────────────────────────────────┤
│ picoadk::dsp::* │ include/picoadk/dsp/
│ modules · sample_player · pd · math · Real (Q16/float) │
├────────────────────────────────────────────────────────────────┤
│ picoadk::* HAL │ include/picoadk/hal/
│ Audio · Midi · Controls · Storage · Psram · Display · gfx │
│ Encoders · Buttons · Mux · Usb · UsbHost · VoiceLink · Tasks │
├────────────────────────────────────────────────────────────────┤
│ Pico SDK 2.2 · pico-extras · TinyUSB │ lib/pico-sdk/
│ FreeRTOS-Kernel (RPi SMP fork) │ lib/FreeRTOS-Kernel/
├────────────────────────────────────────────────────────────────┤
│ Hardware │ RP2040 / RP2350
└────────────────────────────────────────────────────────────────┘
Application code never reaches into the SDK directly — it talks to the picoadk:: HAL. That's what lets one source tree run on both boards.
┌────────────────┐
I2S TX pin ◀──────── PIO ◀──┤ DMA out chain │◀── int32 ring buffers
I2S BCK/WS ◀──────── PIO │ A → B → A → B │
└────────────────┘
▲
│ deinterleave
│ (audio_task)
│
┌──────────────────┐ │ float planar
│ user audio_cb() │ ───────────────►│ ┌──────────────┐
└──────────────────┘ │ │ g_planar_in │
│ │ g_planar_out │
I2S RX pin ────────► PIO ──────────┤ └──────────────┘
▼
┌────────────────┐
│ DMA in chain │
│ A → B → A → B │
└────────────────┘
Two PIO state machines (output, input) run independently when the user picks Direction::Duplex. Two DMA channels per direction chase each other forever via chain_to. On every chain hop the IRQ fires, flips a g_active_* index, and notifies the audio task. The audio task does the int32↔float conversion and runs the user callback off-IRQ — so DSP work is never racing PIO refills.
Latency = block_size / sample_rate per buffer. With block_size = 32 at 48 kHz that's ~0.67 ms.
priority task what it does
──────── ──── ────────────
MAX-1 audio (built-in) off-IRQ user audio callback
MAX-1 MIDI tud_task() + UART byte pump + Midi::process()
MAX-2 USBHOST (opt.) tuh_task()
IDLE+2 MUX background ADC mux scan
IDLE+1 WDOG watchdog tickle
IDLE idle hook vPortIdleTask
All tasks coexist on both cores (SMP). xTaskCreatePinnedToCore() (the ESP-IDF-style alias) lets you pin time-critical tasks to one core — keep audio on core 0, push USB / MIDI / display refresh to core 1.
RP2040 (v1) RP2350 (v2)
───────────── ─────────────
Flash 4 MB QSPI Flash 4 MB QSPI
SRAM 264 KB SRAM 520 KB
PSRAM 8 MB QSPI
├─ .psram_data (KEEP'd by linker)
├─ .psram_bss (KEEP'd by linker)
└─ .psram_dynamic (TLSF heap)
On v2, anything tagged with PICOADK_PSRAM_BSS / PICOADK_PSRAM_DATA lands in the linker-reserved sections at boot. The remainder is fed to the in-tree TLSF allocator (src/third_party/tlsf/); Psram::alloc and placement-new (PSRAM) use it.
DSP modules that own large buffers (DelayLine::configure_auto, FdnReverb::configure, MemorySampleSource) try PSRAM first and fall back to the system heap if PSRAM isn't available — the same code runs on both boards.
┌──────────┐
USB device ── ▶│ │
UART (DIN) ── ▶ │ Midi:: │── ▶ user::on_note_on / on_cc / on_pb / …
USB host ── ▶ │ dispatch │── ▶ midi::Handler (NRPN/RPN aggregation)
│ │── ▶ user::on_raw (catch-all for sysex etc.)
└──────────┘
Each transport runs in Midi::process() (called from a MIDI task). Inbound messages from any source land in the same callback set; MidiMessage::source reports which transport delivered it.
Master Slave 1 Slave 2
───────────────── ───────────────── ─────────────────
audio cb audio cb audio cb
│ ▲ ▲
│ note_on(ch,n,v) → allocator picks slot
│ if slot==0: local
▼ else: send_frame ───┐
UART TX ──────► UART RX ─────────────┴──► dispatch into slave's
UART TX ──────────► Slave 2 (chain pass-through)
▲
└── enumeration on boot reports
voices_local + board_serial
back to master
The control path is UART; the audio path stays local on every board. Frame protocol is documented at the top of src/hal/voicelink.cpp. CRC-8/SMBus protects every frame.
| File | Role |
|---|---|
boards/picoadk_v{1,2}.h |
Pin map + feature flags. Selected by PICOADK_BOARD. |
boards/picoadk_board.h |
Selector — includes the right per-board header. |
boards/datanoisetv_rp{2040,2350}_dsp.h |
Pico-SDK board headers (defaults for stdio/SPI/I²C). |
boards/picoadk_v2_psram.ld |
v2 linker fragment defining .psram_data/.psram_bss/__psram_dynamic_*. |
cmake/picoadk/board.cmake |
Board selector + PICO_PLATFORM + PICO_BOARD. |
cmake/picoadk/freertos.cmake |
Picks RPi RP2040 or RP2350_ARM_NTZ SMP port. |
cmake/picoadk/vult.cmake |
picoadk_add_vult_dsp() codegen helper. |
cmake/picoadk/sdio.cmake |
FetchContent for rabbitholecomputing/SDIO_RP2350. |
cmake/picoadk/sdfat.cmake |
FetchContent for greiman/SdFat. |
cmake/picoadk/libpd.cmake |
FetchContent for libpd/libpd. |
include/picoadk/picoadk.h |
Umbrella header. |
include/picoadk/hal/*.h |
Public HAL surface — one per subsystem. |
include/picoadk/dsp/*.h |
DSP types and modules. |
include/vultin.h |
Vult runtime ABI (Q16 helpers + adc128_read_vult). |
src/hal/*.cpp |
HAL implementations. |
src/hal/pio/*.pio |
I2S, TDM, SDIO, encoder PIO programs. |
src/midi/*.cpp |
MIDI dispatcher + transports + handlers + Launchpad. |
src/usb/*.{c,cpp} |
TinyUSB descriptors + composite-device glue. |
src/dsp/*.cpp |
DSP primitives + sampler + libpd bridge. |
src/third_party/tlsf/ |
In-tree TLSF allocator (used by Psram). |
examples/<n>_<name>/main.cpp |
Build with -DPICOADK_APP=examples/<n>_<name>. |