Embedded C++ firmware for custom audio effects/synthesizers on the
GuitarML FunBox guitar pedal, which uses
an Electro-Smith Daisy Seed board (STM32H750 Cortex-M7). Cross-compiled with
arm-none-eabi-gcc, flashed via DFU. There are no unit tests -- verification
is done by flashing to hardware.
Each experiment lives in its own subdirectory (e.g. basesynth/, template/)
with its own Makefile. Shared headers live in include/. Two git submodules
provide the hardware abstraction (libDaisy) and DSP library (DaisySP).
nix-shell # enters shell with gcc-arm-embedded, make, dfu-util, bear
nix-shell --run "make" # one-shot build of everythingThe nix shellHook runs bear -- make automatically to generate
compile_commands.json for clangd LSP support.
# Build libraries first (inits submodules, applies funbox.patch to libDaisy)
make libs
# Build everything (libs + all experiments)
make
# Build a single experiment
make basesynth
make template
# Build from within an experiment directory (after libs are built)
cd basesynth && makeBuild artifacts go to <experiment>/build/ (.bin, .elf, .hex, .map).
make clean # removes all build/ directories (libs + experiments)
cd basesynth && make clean # clean a single experimentRequires the Daisy Seed to be in DFU mode and correct host udev rules.
dfu-util must be available on the host (nix-shell alone may not work).
# From within an experiment directory:
make program-dfu # flash via USB DFU (main firmware)
make program-boot # flash the bootloadermake openocd # start OpenOCD server
make program # flash via OpenOCD (BOOT_NONE app type only)
make debug # attach GDB to running OpenOCD sessionThere is no test suite. This is bare-metal embedded firmware -- validation is done by flashing to the physical Daisy Seed hardware and testing manually.
Makefile # Top-level orchestrator (libs, experiments, clean)
shell.nix # Nix dev environment
funbox.patch # Patch applied to libDaisy for FunBox pin mappings
include/
funbox.h # Hardware abstraction (knobs, switches, LEDs enums)
expressionHandler.h # Expression pedal handler (FunBox v3)
basesynth/ # FM synthesizer experiment
Makefile # Sets TARGET, CPP_SOURCES, includes libDaisy/core/Makefile
main.cpp # Entry point, hardware init, audio callback
basesynth.h/cpp # Tap tempo + program logic
synth.h/cpp # Polyphonic FM synth (Voice/Synth classes)
note.h # Note frequencies, intervals, and chord utilities
template/ # Skeleton for new experiments
Makefile
template.cpp
libDaisy/ # Git submodule (Electro-Smith HAL)
core/Makefile # Core build system included by all experiments
DaisySP/ # Git submodule (Electro-Smith DSP library)
- Copy
template/to a new directory (e.g.myeffect/) - Edit the new
Makefile: setTARGET = myeffectand listCPP_SOURCES - Add
C_INCLUDES += -I../includeto accessfunbox.h - Add a target to the top-level
Makefilefollowing thebasesynthpattern - Use
template.cppas the starting point -- it has the full boilerplate for controls, switches, audio callback, and main loop
- Based on LLVM style
- 2-space indentation, 120-column limit
- Allman brace style (braces on new lines for all block types)
- Format with:
clang-format -i <file>
- Classes:
PascalCase(BaseSynth,Voice,Synth,ExpressionHandler) - Methods:
snake_casefor project classes (init,process,note_on,tap_tempo,set_ratio);PascalCasefor callbacks and functions that interface with the Daisy API (AudioCallback,UpdateButtons,UpdateSwitches) - Member variables:
snake_case, accessed viathis->explicitly (this->freq,this->sample_rate,this->mod_ratio) - Local variables:
snake_case(sample_rate,mod_freq,total_diff) - Constants/macros:
UPPER_SNAKE_CASE(#define MAX_VOICES 8,TAP_TEMPO_SAMPLES,TEMPO_DISPLAY_DURATION) - Namespaces: lowercase (
funbox,daisy,daisysp) - Enum values:
UPPER_SNAKE_CASEwithin classes (FOOTSWITCH_1,KNOB_1,LED_1) - Global variables: short lowercase names for hardware state
(
hw,bypass,led1,led2,param1..param6)
- Project headers first (
#include "basesynth.h") - Then Daisy/DaisySP headers (
#include "daisy_petal.h",#include "daisysp.h") - Then shared project headers (
#include "funbox.h") - Use
#pragma oncefor header guards (project headers use this pattern;expressionHandler.hadditionally has traditional#ifndefguards)
- C++ standard:
gnu++14(set inlibDaisy/core/Makefile) - Exceptions disabled (
-fno-exceptions), RTTI disabled (-fno-rtti) - No STL containers or dynamic allocation -- this is bare-metal embedded code
- Float literals always use
fsuffix (0.5f,1.0f,440.0f) - Inline trivial methods in the header with
inlinekeyword
- Audio callback: a
static void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size)function registered viahw.StartAudio(). Process controls first, then iterate samples. - Bypass pattern: global
bool bypasstoggled by footswitch; when true, pass input directly to output. - Hardware access: global
DaisyPetal hwinstance. UseFunbox::enum values fromfunbox.hfor knob/switch/LED indices. - Namespaces: always
using namespace daisy;,using namespace daisysp;,using namespace funbox;at file scope in.cppfiles. - Member access: use explicit
this->for member variable access in class method implementations. - DSP classes: follow
init(float sample_rate)/process()/note_on()/note_off()pattern matching DaisySP conventions. - Note/chord utilities (
note.h): useconstexprfunctions for note-to-frequency conversion (sharp,flat,octave,interval,note) andinlinefunctions returning aChordstruct for chord construction (chord_major,chord_minor,chord_seventh, etc.).constexprnote functions use recursive expansion (notpow) for C++14 compatibility. Chord functions usechord_from_intervals(root, intervals, count)as a shared builder. TheChordstruct holds up toMAX_CHORD_NOTES(8) frequencies in a fixed-size array with acountfield -- no allocation. - Compile-time vs runtime: prefer
inline constexprfor pure arithmetic on note frequencies -- the compiler evaluates these at compile time when arguments are constants. Use plaininlinefor functions that populate structs via loops (C++14constexprrestrictions).
There is no exception handling or error reporting infrastructure. This is
bare-metal firmware with no OS. Functions return default/safe values on edge
cases (e.g. TAP_TEMPO_DEFAULT when no valid tempo data exists). The main
loop runs indefinitely with System::Delay(10).
- Do not use dynamic memory allocation (
new,malloc) -- all objects are statically allocated - Do not use C++ exceptions or RTTI (disabled by compiler flags)
- Do not use STL containers that allocate (e.g.
std::vector,std::string) - Do not modify files inside
libDaisy/orDaisySP/directly -- changes to libDaisy go throughfunbox.patch - Do not add files to
.gitignorepatterns*/build/or.cache-- these are already ignored