Build system for compiling Pure Data (Pd) patches into firmware for Raspberry Pi RP2350 via hvcc.
When I started the project in late 2023, I tried using RP2040 chips and was convinced that I was the first person to try compiling Pure Data on an ARM chip. This valuable experiment showed that the lack of a floating-point unit (FPU) practically rules out patches with more than four to six oscillators, and that it made no sense to develop the project further. I didn’t know at the time that the talented Daisy team had already spent years developing their platform — I only found out while working on the RP2040 firmware. If you don't enjoy tinkering, Daisy boards will likely suit you much better, as they offer an excellent out-of-the-box experience. However, if you enjoy getting your hands dirty with software and hardware tools, this is the place for you.
Later, the RP2350 was released, and I decided to try again. Before I knew it, I was maintaining a fairly complex project for compiling Pure Data patches. Creating working firmware from Pure Data patches does require some persistence, but I decided to publish it so that anyone can give it a try. Together with a community of like-minded individuals, I believe that we can make the firmware and build process highly simple and clear. Raspberry Pi chips are widely used and there are plenty of solid boards with optional PSRAM, which is strongly recommended for certain applications. With this project, you can build your own synthesizers based on Pure Data patches using a cheap and widely available chip.
I test on a board that you can replicate on a breadboard:
- RP2350-Zero (Waveshare)
- PCM5102 module (I2S DAC)
- I2C 128×64 OLED display
- Three buttons for UI control
- Four potentiometers (all ADC channels on RP2350; RP2350B has eight)
- MPR121 for feeding events into the patch
- 3.5 mm TRS jacks for MIDI in/out (support in progress)
A 240 MHz profile is available; the chip can also be overclocked to 600 MHz, which makes the whole endeavour with these parts very attractive. So far, no other overclocking profiles have been implemented or tested.
This project compiles Pure Data patches into firmware for the RP2350 microcontroller, which features FPU and DSP capabilities suitable for real-time audio processing generated from Pd patches.
The firmware is highly configurable: build-time options and config.h / config_local.h let you adapt it to almost any scenario (which components you have or omit). A minimal setup is any RP2350 board plus any I2S DAC; the PCM510x family (e.g. PCM5102) works very well.
PlugData is recommended for authoring patches: it ships with Heavy (Hv) nodes, which are the objects supported by hvcc and used to generate the firmware DSP. Turn on Compile Mode in PlugData so the editor clearly marks objects that are not supported for compilation; that way you avoid building only to find unsupported nodes at compile time. The Heavy/hvcc backend has limitations and does not support every Pd object; for unsupported vanilla objects see the hvcc documentation.
- Python 3.9+
- Python dependencies —
python -m pip install -r requirements.txtinstalls pinnedhvcc,ninja, andcmakeversions; with the project venv you do not need to install CMake or Ninja separately. - hvcc source of truth — build script runs
hvccfrom the active environment (PATH, recommended from projectvenv).third_party/hvccis kept as vendored source/tests reference, not as the executable used by the build script. - Raspberry Pi Pico SDK (for RP2350) - included as git submodule
- CMake (pinned to
3.28.3inrequirements.txt; if you use system CMake instead, minimum3.13+) - Host C/C++ compiler (needed for Pico SDK host tools like
pioasm; e.g. LLVM Clang or Visual Studio Build Tools) - ARM GCC toolchain (arm-none-eabi-gcc)
- picotool (optional, for manual flashing) - included as git submodule
Support tiers:
- Supported: Windows (actively used/tested by maintainer)
- Best effort: Linux, macOS (community testing welcome)
- Untested: other platforms/configurations
After cloning the repository, initialize git submodules:
git submodule update --init --recursiveInstall Python dependencies:
python -m pip install -r requirements.txtDependency pin policy:
requirements.txtis the reproducible baseline for public builds.- Version bumps should be intentional and accompanied by matching README/TECH updates.
Recommended for all platforms: use a repository-local virtual environment to keep tool versions isolated and predictable (cmake, ninja, hvcc):
python -m venv venv
.\venv\Scripts\Activate.ps1
python -m pip install -r requirements.txtpython -m venv venv
source venv/bin/activate
python -m pip install -r requirements.txtIf your machine has multiple global Python/CMake/Ninja installs, prefer launching builds from the activated venv shell and keep IDE/toolchain paths explicit in workspace settings. CMake and Ninja from pip are used for the build; only ARM GCC and a host C/C++ compiler must be installed separately (see below).
The build needs arm-none-eabi-gcc and arm-none-eabi-g++ on PATH or under PICO_TOOLCHAIN_PATH (the toolchain root; both the build script and CMake look in PICO_TOOLCHAIN_PATH/bin). Set PICO_TOOLCHAIN_PATH in the same environment where you run the build (e.g. in your terminal or in the shell that runs the script). On Windows, install the GNU Arm Embedded Toolchain, then add its bin directory to PATH or set PICO_TOOLCHAIN_PATH to the toolchain root. On Linux, install the gcc-arm-none-eabi package (Debian/Ubuntu) or unpack the official tarball and set PATH or PICO_TOOLCHAIN_PATH. On macOS, use Homebrew (arm-none-eabi-gcc) or the official package and put bin on PATH or set PICO_TOOLCHAIN_PATH.
The Pico SDK builds host tools (e.g. pioasm) during configure. The script prefers gcc/g++; if missing, CMake can use clang++ or MSVC. On Windows, install Visual Studio Build Tools with "Desktop development with C++" or full Visual Studio, and run builds from a Developer Command Prompt (or ensure cl is on PATH); or install LLVM/Clang. On Linux, install build-essential. On macOS, install Xcode Command Line Tools (xcode-select --install) or full Xcode. Missing ARM toolchain (arm-none-eabi-gcc/g++) fails Preflight. Missing host C++ compiler is reported as a warning, and CMake may still fail later if it cannot select a usable host toolchain.
Build a firmware from a Pure Data patch:
python scripts/build_firmware.py pd-patches/your_patch.pdThe firmware UF2 file will be generated at:
build/<patch_name>/firmware-build/rp2350_puredata_firmware.elf.uf2
Defaults (when no flags are provided):
ENABLE_WS2812=ON(opt-out)ENABLE_OLED=OFFENABLE_MPR121=OFFENABLE_USB_MIDI=OFFENABLE_I2C_DMA=OFF
CLI flags (single argument per feature):
# Enable optional features
python scripts/build_firmware.py pd-patches/your_patch.pd --oled --mpr121 --usb-midi --i2c-dma
# Disable WS2812 (ON by default)
python scripts/build_firmware.py pd-patches/your_patch.pd --no-ws2812
# Force 240 MHz OC profile (explicit override from build script)
python scripts/build_firmware.py pd-patches/your_patch.pd --overclockedYou can always pass explicit CMake defines:
python scripts/build_firmware.py pd-patches/your_patch.pd -D ENABLE_OLED=ON -D ENABLE_WS2812=OFFClock behavior:
- If
FW_SYS_CLOCK_PROFILEis defined incore/src/config.h/core/src/config_local.h, firmware uses it. --overclockedexplicitly overrides this at build time toFW_SYS_CLOCK_PROFILE_OC_240MHZ.- With
--overclocked, script also defaultsPICO_FLASH_SPI_CLKDIV=4unless you pass your own-D PICO_FLASH_SPI_CLKDIV=....
The build script supports multiple verbosity levels:
--log-level normal(default) - brief steps, warnings/errors visible--log-level verboseor-v- real-time build progress streaming--log-level debugor-d- full commands and paths--log-level quietor-q- errors only
--log-level controls console verbosity only.
Full build output is always captured to files:
build/<patch>/build_firmware.log- latest run, overwritten each buildbuild/<patch>/logs/build_firmware_<timestamp>.log- per-run archive
I2S pins (DIN, BCK, LCK) are configured in:
core/src/config.h- default pinscore/src/config_local.h- local overrides (copyconfig_local.h.examplefor your board)
Default pins (on typical RP2040/RP2350A builds, GPIO 26–29 remain free for on-chip ADC):
- DIN (data): GPIO 5
- LCK (word clock/LRCLK): GPIO 6
- BCK (bit clock): GPIO 7
Default peripherals do not conflict: OLED uses GPIO 2/3, WS2812 uses GPIO 16 (see config.h). See config_local.h.example for overrides.
I2C is initialized through a central bus layer on core0. Configure pins and baud rate in:
core/src/config.h- defaultscore/src/config_local.h- local overrides (copyconfig_local.h.example)
Use I2C_BUS0_* / I2C_BUS1_* to define buses, and OLED_I2C_BUS_ID / MPR121_I2C_BUS_ID to select which bus each device uses. Legacy I2C_BUS_* / OLED_I2C_* macros still map to bus 0.
Flash the firmware manually using picotool:
picotool load build/<patch_name>/firmware-build/rp2350_puredata_firmware.elf.uf2
picotool rebootOr copy the .uf2 file to the RP2350's mass storage device (if mounted).
The contract between Pd patches and firmware is defined in core/src/patch_api.c and documented here.
Use standard Pd MIDI objects in your patch: [notein], [ctlin], [bendin], [pgmin], [touchin], [polytouchin]. The firmware sends USB MIDI into hvcc receivers with canonical argument order:
| Pd object | hvcc receiver | Message format (order) |
|---|---|---|
[notein] |
__hv_notein |
(pitch, velocity, channel0) |
[ctlin] |
__hv_ctlin |
(value, cc, channel0) |
[bendin] |
__hv_bendin |
(bend14, channel0) |
[pgmin] |
__hv_pgmin |
(program, channel0) |
[touchin] |
__hv_touchin |
(pressure, channel0) |
[polytouchin] |
__hv_polytouchin |
(pressure, note, channel0) |
channel0 is 0..15. Reference: third_party/hvcc/tests/src/test_midi.cpp.
hv_setSendHook() is called in exactly one place: patch_api_init(ctx). No driver (ws2812, future I2C/encoders/display) must ever call hv_setSendHook(). This avoids "last one wins" when the project grows.
- Inputs (firmware → patch): Use Daisy-style names for potentiometers:
knob1,knob2,knob3,knob4(e.g.[r knob1 @hv_param 0 1 0]). Potentiometers are disabled by default; enable incore/src/config_local.hby settingPOTS_BACKENDtoPOTS_BACKEND_ADC. Configuration is by ADC channel (first channelPOTS_ADC_FIRST_CHANNEL, countPOTS_COUNT); physical pins are determined by the SDK and vary by chip (e.g. RP2040/RP2350A: channels 0–3 on GPIO 26–29; RP2350B: channels 0–7 on GPIO 40–47). Values are sent only when they change (poll intervalPOTS_POLL_MS, deadbandPOTS_EPS, and 1-pole ADC smoothing). If POTS_BACKEND is NONE or POTS_COUNT is 0,knob*receivers receive no values (they stay silent). - Other scalar inputs (e.g. buttons): use
hw_*names as needed; firmware pushes by hash. - Two classes of inputs:
- Scalar state (knob, pot, button state) → use @hv_param in the patch; firmware pushes by hash.
- Event/packet ("button pressed", "I2C packet", "encoder +N") → use ordinary receivers/messages (not necessarily @hv_param) if it is not a UI parameter.
- Commands from patch (patch → firmware): Use cmd_* or fw_* as names in
[s ...](patch names only; C code may use any wrapper names). The command table (name → format → handler) is inpatch_api.c.
Official API for LED control from the patch:
- set_led_color
(r g b)— three floats (0–1 or 0–255). Sets all LEDs.
This is the only supported LED send command in firmware.
The firmware uses strict multicore separation:
- Core1 (audio core): Runs Heavy DSP processing (
hv_process*()), owns the Heavy context, handles audio buffer production. No blocking I/O, no USB tasks, noprintf. - Core0 (I/O core): Handles initialization, USB/MIDI, peripherals (WS2812 LEDs), and drains control queues.
Communication between cores uses:
ctrl_queue(core0 → core1): MIDI/control events (pushed viapatch_api_push_*/crosscore_bus_ctrl_try_push_*)led_mailbox(core1 → core0): latest LED color published by patch send hook; core0 consumes latest state and appliesws2812_set_all(...)
For complete architecture details, strict multicore rules, failure modes, and validation guidance, see TECH.md.
pd-patches/- Pure Data patch source filescore/- Core firmware project (I2S audio, Heavy integration, multicore)scripts/- Build automation scriptsbuild/- Build output directory (created automatically)sdk/- SDK submodules (pico-sdk, pico-extras)third_party/- Third-party dependencies (heavylib, hvcc sources/tests, u8g2, pico-mpr121, multibutton, picotool)
This project is licensed under the MIT License. See LICENSE.