Drumboy Pro Wiki
RANDOMWAVES · OPEN-SOURCE GROOVEBOX

Drumboy Pro Wiki

GitHub Pages
MCU
STM32H723ZGT6
Core clock
550 MHz
Display
540×960
SDRAM
16 MB
Layers
10
Encoders
8
Keys
42
Flash
1024 KB

System Overview

The MCU sits at the center of six subsystems: the NT35510 display, driven over a GPIO parallel bus into its own onboard GRAM; 16 MB of external SDRAM on the FMC bus for sample streaming; the SGTL5000 audio codec, over I2S3 (audio) and I2C3 (control); a microSD card over SDMMC2 for storage; the physical control surface, fanned out over I2C behind six MCP23017 expanders; and external connectivity — MIDI, Sync and USB-C power — over USART and GPIO.

STM32H723ZGT6 Cortex-M7 · 550 MHz VOS0 · FPU NT35510 LCD 540×960 · 16-bit parallel SDRAM 16 MB IS42S16800J · 16-bit 0xC0000000 SGTL5000 Codec + MP3302 Class-D amp Line In/Out · Headphone I2S3 audio · I2C3 control microSD (TF-01A) FAT filesystem samples, projects, firmware packages Control Surface 6× MCP23017 · 42 keys 42 LEDs · 8 rotary encoders I2C + GPIO EXTI Connectivity MIDI In/Out · Sync In/Out USB-C power (3.3V / 1.8V) USART1/6 · GPIO
Fig. 1 — System block diagram, STM32H723 (brass outline) at the center.
PART ONE

Hardware

Specifications HW

SubsystemPartInterfaceNotes
MCUSTM32H723ZGT6LQFP-144Cortex-M7, 550 MHz (VOS0), 127 of 144 pins used
Display driverNT3551016-bit parallel (GPIO-driven)540×960 TFT with its own onboard GRAM, off-board module on a 51-pin FPC
Display backlight driverMP3302DJ-LF-ZBoost converter (SW node)Drives the LCD's LED backlight
Display connectorFH26W-51S FPC, 0.3 mm pitch51-pin flex from Drumboy_Main straight to the LCD module
Audio codecSGTL5000XNBA3R2I2S3 (audio) + I2C3 (control, addr 0x14)Headphone / line-out DAC, line-in ADC
SDRAMIS42S16800J-7TL-TR16-bit FMC SDRAM16 MB (8M×16) at 0xC0000000
StorageTF-01A microSD socketSDMMC2, 4-bit busFAT filesystem — samples, projects, firmware packages
I/O expanders6× MCP23017I2C3 (3×) + I2C4 (3×), addr 0x20–0x253× interrupt-driven keypad matrices, 3× output-only LED drivers
Encoders8× PEC12R-4017FGPIO quadrature, A-channel on EXTISpread across ports A/C/E/F/G
Keys42× tactile switchesvia 3× MCP23017 matrix (X/A/B)Layer pads (X), left & right button matrices (A/B)
LEDs42× discrete LEDsvia 3× MCP23017 (C/D/E) + 3× direct GPIO12-seg EQ L/EQ R meters, layer indicators, LED0–2 status LEDs on PC1–PC3
MIDIH11L1 opto-isolator + USART1/USART631,250 baud, half-duplex single-wireIsolated in (3.3 V digital domain), direct-drive out
Power inUSB-C (TYPE-C-31-M-17)CC1/CC2 → 5.1 kΩ pulldowns0.75 A poly-fuse, then 9-way ferrite-isolated domain split

CPU & Clock Tree HW

IC2 is the STM32H723ZGT6 (Arm Cortex-M7, LQFP-144) — the single MCU driving every subsystem on Drumboy_Main: SDRAM over FMC, the display and control surface over GPIO, the audio codec over I2S3/I2C3, and MIDI/Sync over two USARTs.

KiCad schematic crop of IC2 (STM32H723ZGT6), full 144-pin LQFP pinout with every net label, plus the adjacent decoupling capacitor bank, VCAP filter caps, HSE crystal (Y1), NRST/BOOT0 reset circuits, and I2C3/I2C4 pull-ups.
IC2 and its support circuitry, from Drumboy_Main.kicad_sch.

It's clocked from an 8 MHz HSE crystal (Y1, ABM7-8.000MHz-D2Y-T). CubeMX's RCC.* block gives the exact multiplier chain for all three PLLs:

StageConfigOutputFeeds
HSE8 MHzPLL reference
PLL1M=1, N=68, P=1550 MHzSYSCLK → Cortex-M7 core (VOS0)
PLL2M=1, N=30, P=1, R=1240 MHzFMC, SDMMC2 kernel clock
PLL3M=1, N=25 (fractional, FRACN=3289), P=2≈101.606 MHzSPI3/I2S3 kernel clock — dedicated audio PLL, real I2S rate 44.1 kHz
AHB (HCLK)HPRE /2275 MHzAXI bus, DMA, GPIO
APB1–4D1/D2/D3 PPRE /2137.5 MHzUSART, I2C, most TIM peripheral clocks

Boot & debug pins

PinCubeMX signalNotes
PA13 (JTMS/SWDIO)DEBUG_JTMS-SWDIOSWD data
PA14 (JTCK/SWCLK)DEBUG_JTCK-SWCLKSWD clock

SDRAM HW

IC1 is an IS42S16800J-7TL-TR (128 Mbit, 8M×16, SOP-54), providing 16 MB of external sample memory at 0xC0000000, streamed by the ten instrument layers during playback.

KiCad schematic crop of IC1 (IS42S16800J-7TL-TR), full 54-pin SDRAM pinout with its decoupling capacitor bank.
IC1, from Drumboy_Main.kicad_sch.
BusSignalsMCU pins
Data (16-bit)FMC_D0–D15PD14, PD15, PD0, PD1, PE7–PE15, PD8–PD10
Address (mux)FMC_A0–A11PF0–PF5, PF12–PF15, PG0, PG1
Bank selectFMC_BA0, FMC_BA1PG4, PG5
Byte lanesFMC_NBL0, FMC_NBL1PE0, PE1
Clock / enableFMC_SDCLK, FMC_SDCKE0PG8, PC5
Chip selectFMC_SDNE0PC4
StrobesFMC_SDNRAS, FMC_SDNCAS, FMC_SDNWEPF11, PG15, PC0

The hardware FMC SDRAM controller times the interface entirely in SDCLK cycles:

ParameterValue
Column bits9
CAS latency2 cycles
SDCLK periodHCLK / 2
Read burst / pipe delayEnabled / 2 cycles
Load-to-active delay2 cycles
Exit self-refresh delay9 cycles
Self-refresh time5 cycles
Row cycle delay8 cycles
Write recovery time4 cycles
RP / RCD delay2 / 2 cycles

Display (LCD) HW

The NT35510 panel carries its own onboard GRAM (graphics RAM) and refreshes the screen from it independently. The STM32 writes commands and pixel data into that GRAM over a 16-bit parallel bus: LCD_D0–D15 map to plain GPIO_Output pins on Port B (PB0–PB15), toggled directly by the firmware's Lcd driver.

SignalMCU pin(s)Role
LCD_D0–D15PB0–PB1516-bit parallel data bus to the panel's GRAM (GPIO)
LCD_CSPD2Chip select
LCD_RDPD3Read strobe
LCD_WRPD4Write strobe
LCD_RSPD5Register/data select
LCD_RESETPG13Panel reset
LCD_BLPG14Backlight enable
KiCad schematic crop of J2 (the 51-pin FPC to the display), the LCD_IM0-IM2 strapping resistors, the LCD_PWM/BL_EN divider network, and the IC3 backlight boost converter with L1 and D9.
J2 connector and backlight boost circuit, from Drumboy_Main.kicad_sch.

LCD_IM0–IM2, the NT35510's interface-mode select, are hardwired via 0 Ω links (R33→+3.3V_DIG, R34/R35→GND_DIG) on Drumboy_Main itself, fixing the panel to 16-bit 8080 parallel mode at power-up. Brightness is switched through LCD_BL.

Backlight power comes from a dedicated boost converter. The panel reaches the board over the 51-pin, 0.3 mm-pitch Hirose FH26W FPC (J2).

Audio Codec HW

IC7, an SGTL5000XNBA3R2 in QFN-33, is the entire analog audio path — headphone amp, line-out DAC and line-in ADC all live inside this one chip. It's controlled over I2C3 and streams PCM over I2S3:

BusSignalMCU pin
I2C3 (control, addr 0x14)I2C3_SCLPA8
I2C3_SDAPC9
I2S3 (audio)I2S3_WSPA4
I2S3_CKPC10
I2S3_SDIPC11
I2S3_SDO / I2S3_MCKPC12 / PC7

I2S3 runs on its own PLL3-derived kernel clock, in fractional mode: CubeMX reports I2S3.RealAudioFreq at 44.099 kHz.

KiCad schematic crop of IC7 (SGTL5000XNBA3R2), its LDO regulators IC5/IC6, DAC rail decoupling, and the headphone/line-in/line-out jacks J8-J10.
IC7 and the DAC power/audio-jack circuitry, from Drumboy_Main.kicad_sch.

The codec's supply is electrically separated from the rest of the board — +3.3V_DAC (IC5) and +1.8V_DAC (IC6) are their own LDO outputs, riding on a ferrite-isolated +5V_DAC rail with its own GND_DAC net, distinct from the digital +3.3V_DIG/GND_DIG domain that powers the MCU and LCD.

Control Surface & I/O Expansion HW

The 42 keys and 42 LEDs run behind six MCP23017 16-bit I2C GPIO expanders (U1–U6), split across two I2C buses:

RefAddrBusReset / INT netsRole
U10x25I2C3MX_X_RESET / MX_X_INTLayer keypad (input, EXTI)
U20x20I2C3MX_A_RESET / MX_A_INTLeft keypad (input, EXTI)
U30x21I2C3MX_B_RESET / MX_B_INTRight keypad (input, EXTI)
U40x22I2C4MX_C_RESETLED driver, output only
U50x23I2C4MX_D_RESETLED driver, output only
U60x24I2C4MX_E_RESETLED driver, output only
KiCad schematic crop of U1 (MCP23017, layer keypad expander at I2C address 0x25): GPA0-7 and GPB0-1 wired to SW_X_00-09, with its INT/RESET pull-ups and decoupling.
U1, layer keypad (addr 0x25).
KiCad schematic crop of U2 (MCP23017, left keypad expander at I2C address 0x20): GPA0-7 and GPB0-7 wired to SW_A_00-15, with its INT/RESET pull-ups and decoupling.
U2, left keypad (addr 0x20).
KiCad schematic crop of U3 (MCP23017, right keypad expander at I2C address 0x21): GPA0-7 and GPB0-7 wired to SW_B_00-15, with its INT/RESET pull-ups and decoupling.
U3, right keypad (addr 0x21).

The three input expanders (U1–U3), from Drumboy_Main.kicad_sch — identical circuit, different address strap and net names.

Only the three input expanders (X/A/B) have their INT line wired to an EXTI-capable MCU pin — matching the firmware's GPIO EXTI table (MX_X_INT→PD11, MX_A_INT→PA0, MX_B_INT→PA1). The three LED-driver expanders (C/D/E) are pure outputs and don't need one. I2C3 is shared between the codec and the three input expanders; I2C4 is dedicated to the three LED-driver expanders. Between them, the schematic's net list shows a 12-segment left EQ meter (LED_EQ_A_00–11), a 12-segment right EQ meter (LED_EQ_B_00–11) and 10 layer/pad indicators (LED_KEY_00–09) — plus three simple status LEDs (LED0–LED2) wired directly to PC1–PC3, outside the I2C matrix entirely.

Encoders

The 8 rotary encoders (U8–U15, PEC12R-4017F-N0024) run directly on GPIO, each quadrature A-channel on its own EXTI line:

KiCad schematic crop of encoder 0 (U8, PEC12R-4017F-N0024): channel A and B each go through a 10k pull-up plus a 10k/0.01uF RC filter before reaching ENCO_0A/ENCO_0B.
Fig. — Encoder 0 (U8), from Drumboy_Main.kicad_sch. Every encoder channel repeats this pull-up + RC debounce pattern.
EncoderA (EXTI)B
0PE2PE6
1PE3PA5
2PE4PA6
3PE5PA7
4PG6PG2
5PG7PG3
6PF8PC14 (OSC32_IN)
7PF9PC15 (OSC32_OUT)

Encoders 6 and 7 borrow the pins normally reserved for a 32.768 kHz RTC crystal (PC14-OSC32_IN / PC15-OSC32_OUT) — there's no RTC crystal populated on this board, so CubeMX frees the pair up as plain GPIO.

Storage (microSD) HW

The TF-01A microSD socket runs on SDMMC2's native 4-bit bus, with a dedicated hot-plug detect line feeding the firmware's card-present polling:

SignalMCU pin
SDMMC2_CKPD6
SDMMC2_CMDPD7
SDMMC2_D0–D3PG9, PG10, PG11, PG12
SDMMC2_DETECTPC13

CubeMX sets SDMMC2.ClockDiv = 32 against the 240 MHz PLL2-derived kernel clock — the same PLL2 output shared with SDRAM.

MIDI & Sync Circuits HW

MIDI In is opto-isolated; MIDI Out and both Sync ports are direct-drive GPIO/UART:

PortSignal(s)MCU pinNotes
MIDI InMIDI_RXPA9 (USART1_TX pin, half-duplex)Through U7 (H11L1 opto-isolator) + D10 (1N4148WT) protection diode, isolated onto the 3.3 V digital domain
MIDI OutMIDI_TXPC6 (USART6_TX)Direct drive, 47 Ω current-limit resistors (R58, R62)
Sync InSYNC_IN_PULSE / _GATEPA15 (EXTI15) / PF6GPIO pulse + gate pair
Sync OutSYNC_OUT_PULSE / _GATEPA10 / PA12GPIO, pulse timed by TIM23

Both MIDI UARTs run at the fixed MIDI baud rate, configured in CubeMX as half-duplex single-wire mode: USART1.BaudRate = USART6.BaudRate = 31250.

KiCad schematic crop of J4-J7 (Sync In, Sync Out, MIDI In, MIDI Out jacks), U7 (H11L1 opto-isolator) with D10 protection diode, and the current-limit/pull resistors R48, R51, R53, R54, R58, R62.
MIDI/Sync jacks and the opto-isolator circuit, from Drumboy_Main.kicad_sch.

I/O & Connectivity HW

Nine physical interfaces reach the outside world: USB-C for power, a microSD slot for storage, and seven 3.5 mm (1/8") TRS jacks — all seven use the same SJ1-3515-SMT-TR footprint (J4–J10). A separate SWD header (not shown below) is used for programming.

USB-C power microSD storage Line In 3.5mm TRS Line Out 3.5mm TRS Headphone 3.5mm TRS MIDI In TRS-MIDI MIDI Out TRS-MIDI Sync In 3.5mm Sync Out 3.5mm 7 × SJ1-3515 3.5mm jacks · USB-C & microSD at left · headphone jack shares the codec's cleanest path
Fig. 2 — Physical edge connectors, left to right as populated on Drumboy_Main: power/storage, then audio, then MIDI/Sync. Port mode behavior (Note/Sync/Sync+Note, Mode A/B/C) is documented in the User Manual's Sync menu, not repeated here.

Power System HW

USB-C (J1) is the only power input. CC1/CC2 are pulled through 5.1 kΩ resistors (R1, R2) — a fixed default-current sink, no PD negotiation chip. A 0.75 A resettable poly-fuse (F1) sits in series on +5V_USB before it becomes the board's common +5V_BUS.

Domain isolation

From +5V_BUS, nine ferrite beads (FB1–FB9) split the supply into five separate islands, each with its own local ground net — a star topology, not a shared plane:

DomainFerritesLocal rail / groundRegulator
USBFB1, FB2+5V_USB / GND_USB
DigitalFB3, FB4+5V_DIG / GND_DIGIC4 (AP2112K-3.3) → +3.3V_DIG
LCDFB5, FB6+5V_LCD / GND_LCDFeeds IC3 boost converter, see below
DAC (codec digital)FB7, FB8+5V_DAC / GND_DACIC5 (AP2112K-3.3) → +3.3V_DAC
DAC analogFB9+3.3V_DAC_ANA / GND_DACIC6 (AP2112K-1.8) → +1.8V_DAC

The five local grounds reconnect to the common GND only through a handful of 0 Ω links (R33, R34, R35, R50, R51, R52, R54 in the BOM) — a deliberate single-point star-ground scheme, on top of the per-domain ferrite filtering above.

KiCad schematic crop of IC4 (AP2112K-3.3TRG1), the digital 3.3V LDO regulator: +5V_DIG in, +3.3V_DIG out, with its enable pull-up and input/output decoupling caps.
IC4, the digital 3.3 V regulator, from Drumboy_Main.kicad_sch.

LCD backlight supply

The display backlight runs off its own switching supply, independent of the linear rails above: IC3 (MP3302DJ-LF-Z) as a boost converter, with L1 (4.7 µH, NRS5024T4R7MMGJ) as the switching inductor and D9 (MBR0520LT3G Schottky) as the rectifier — a textbook boost topology producing BL_VCC for the backlight LED string, enabled and dimmed from the MCU via LCD_BL (PG14).

ChargePad accessory

ChargePad is a detachable USB-C battery dongle — its own charge-management PCB, not part of the Drumboy_Main KiCad project, so it isn't covered by the net-level detail above. See the User Manual for its LED indicators and charge behavior.

Key Components / BOM HW

Full BOM is generated from KiCad at production/bom.csv. The table below groups the parts that define the platform's capability — passives (0.1 µF/4.7 µF decoupling, 10 k⁠ Ω pull networks, etc.) are omitted for brevity.

RefPartQtyRole
IC2STM32H723ZGT61Main MCU, LQFP-144
IC1IS42S16800J-7TL-TR116 MB SDRAM, sample memory
IC7SGTL5000XNBA3R21Audio codec, QFN-33
IC3MP3302DJ-LF-Z1Boost converter — LCD backlight driver, not audio
L1NRS5024T4R7MMGJ14.7 µH boost inductor for IC3
D9MBR0520LT3G1Schottky rectifier for the IC3 boost circuit
IC4, IC5AP2112K-3.3TRG123.3 V LDOs — digital rail (IC4) and codec-digital rail (IC5)
IC6AP2112K-1.8TRG111.8 V LDO, codec analog rail
U1–U6MCP23017_ML6I2C GPIO expanders — 3× keypad (I2C3), 3× LED driver (I2C4)
U8–U15PEC12R-4017F-N00248Rotary encoders
U7H11L11MIDI-in opto-isolator
D101N4148WT1MIDI-in loop protection diode
SW1–SW42SW_Push (SW_PG1350)42Tactile key switches
D1–D44 (excl. D9/D10)LED (G/W/R/O/Y)42Pad backlight + EQ meter LEDs
J1TYPE-C-31-M-171USB-C power input
J2FH26W-51S-0.3SHW151-pin FPC to the LCD module
J4–J10SJ1-3515-SMT-TR73.5 mm TRS jacks
MICRO_SDTF-01A1microSD socket
Y1ABM7-8.000MHZ-D2Y-T1HSE crystal, 8 MHz
F1MF-PSMF075X-210.75 A resettable fuse, USB-C input
FB1–FB9BLM18AG (102SN1D / 601SN1D)9Per-domain power isolation
R1, R25.1 kΩ2USB-C CC1/CC2 pulldowns

Programming / Debug Header HW

Firmware is flashed and debugged over SWD. Wire an ST-Link V2 to the board's SWDIO (PA13), SWCLK (PA14) and GND pins, then flash from STM32CubeIDE.

PART TWO

Software

Two STM32CubeIDE projects: Drumboy-Pro-Boot-H723 (bootloader) and Drumboy-Pro-App-H723 (application), written in C/C++ against the STM32 HAL.

Firmware Architecture SW

Firmware is deliberately split into two independently-built projects that share one flash device. The bootloader owns sector 0 and is responsible only for validating and installing updates; the application owns the rest of flash and is where the actual instrument runs. This split means a bad application build can never brick the update path itself.

ProjectRepositoryResponsibility
Drumboy-Pro-Boot-H723github.com/Randomwaves-Team/Drumboy-Pro-Boot-H723Checks the SD card for a firmware package, validates and programs it, then jumps to the application
Drumboy-Pro-App-H723github.com/Randomwaves-Team/Drumboy-Pro-App-H723The sequencer, DSP engine, UI, and all runtime behavior

Memory Map SW

Bootloader — Sector 0 0x08000000 – 0x0801FFFF · 128 KB Application — Sectors 1–7 0x08020000 – 0x080FFFFF · 896 KB VTOR relocated here at startup SCB->VTOR = APPLICATION_ADDRESS jumps to reset handler at +4
Fig. 3 — Flash split between the two projects. FLASH_FIRMWARE_ADDRESS = 0x08020000 is a hard boundary neither project may cross.
RegionAddressSizePurpose (application project)
FLASH (app)0x08020000896 KBApplication code + constants
RAM_D10x24000000320 KBStack, heap, audio buffers, Controller state
RAM_D20x3000000032 KBDMA buffers (NOLOAD section)
RAM_D30x3800000016 KBBDMA-accessible I2C buffers (i2c3_rx/tx, i2c4_rx/tx)
DTCMRAM0x20000000128 KBTightly-coupled data
ITCMRAM0x0000000064 KBTightly-coupled instructions
SDRAM0xC000000016 MBSample audio data, read at runtime

Bootloader SW

Controller::initialize() in the bootloader runs three steps in order: turn on the status LED, run updateFirmware() if an SD card is present, then startApplication().

Firmware update flow

SD detect find *.bin Validate header magic + version + size Stage to SDRAM 10 KB chunks CRC-32 check staged payload Erase + write flash, 256-bit words CRC-32 verify re-read from flash Success move to System/Firmware/ Any failure delete the file
Fig. 4 — updateFirmware(). CRC is checked twice: once against the staged SDRAM payload before any erase, and again by re-reading flash after the write completes.

Firmware binary format

[0..23]  Header: "RW_DRUMBOY_PRO_FIRMWARE " (24 bytes)
[24..25] versionMajor (uint16_t)
[26..27] versionMinor (uint16_t)
[28..31] firmwareSize (uint32_t)  — payload byte count, must be > 0
[32..35] crc            (uint32_t)  — CRC-32 (zlib/ISO-3309) of the payload
[36...]  Firmware payload

Total file size must equal firmwareSize + 36. Range checks reject major ≥ 10, minor ≥ 100, or a size outside 0 < size < 917504 bytes before anything is written to flash — a zero-size payload is explicitly rejected so it can't erase the application sector without writing anything back.

Application jump

SysMemBootJump = (void (*)(void))(*((uint32_t *)(0x08020000 + 4)));
__disable_irq();
SysTick->CTRL = 0;
__set_MSP(*(uint32_t *)0x08020000);
SysMemBootJump();

LCD status messages

StateMessage
In progressUPDATING FIRMWARE
SuccessFIRMWARE UPDATED
Bad headerFIRMWARE ERROR / FORMAT-CHECK
Version out of rangeFIRMWARE ERROR / VERSION-CHECK
Size out of rangeFIRMWARE ERROR / SIZE-CHECK
Pre-write CRC mismatchFIRMWARE ERROR / CRC-CHECK
Post-write verify mismatchFIRMWARE ERROR / VERIFY-CHECK

Flash programming happens in 256-bit (8×32-bit word) chunks — the minimum granularity the H72x/H73x flash controller supports.

Application SW

Startup sequence

__disable_irq();
SCB->VTOR = APPLICATION_ADDRESS;   // relocate vector table
SCB_InvalidateICache();
SCB_InvalidateDCache();
HAL_Init();
SCB->CPACR |= ((3UL << 20) | (3UL << 22));      // enable FPU
__set_FPSCR(__get_FPSCR() | (1 << 24) | (1 << 25)); // FTZ + DN
__enable_irq();
SystemClock_Config();
PeriphCommonClock_Config();
System_Initialize();
Sdram_Initialize();
controller.initialize();
while (true) controller.update();

Project structure

Core/
  Src/main.cpp                      — startup, clock/SDRAM init, all ISR callbacks
  Library/Controller/
    Controller.cpp / Controller.h   — central state machine (~1000+ line header)
  Library/Global/
    Global.h                        — shared constants, types, math helpers
    Lfo.h                           — LFO data types and waveforms
    Metronome.h                     — metronome timing + sample playback
    MCP23017.h                      — I2C I/O expander driver
  Library/Lcd/                      — NT35510 driver + bitmap fonts + icons
  Library/Dac/
    Dac.cpp / Dac.h                 — SGTL5000 register-level driver
    SGTL5000.h                      — register address definitions
FATFS/                              — SD card FAT layer
STM32H723ZGTX_FLASH.ld              — linker script (FLASH @ 0x08020000)

The Controller class

Controller is the single global instance and owns every subsystem:

  • Hardware peripheralsLcd, Dac, Sd, Keyboard, Encoder[8], Led (EQ left, EQ right, layer)
  • DSP modulesLpf[], Eq, Filter[], Effect[], Reverb, Metronome, Lfo[]
  • Domain dataLayer[10] (instrument + sequencer data per layer), SampleSector[] (SD card sample index)
  • Transport stateplayActive, recordActive, playArmed, recordArmed, playInterval, songInterval
  • Menu statemenu, preMenu, menuTab, selectedLayerNum, selectedBeatNum

Lifecycle is just two calls: initialize() runs the boot sequence (SD init, LCD logo, preset load); update() is the main-loop body — keyboard_action → encoder_action → led_action → lcd_update.

Subsystem & library sizes

Every fixed-size pool the Controller owns is a compile-time constant in Global.h. Menu logic, preset serialization and DSP loops all index against these rather than magic numbers:

ConstantValueWhat it sizes
kLayerLibrarySize10Layers (instruments)
kFilterLibrarySize2Filter 1 / Filter 2
kEffectLibrarySize2Effect 1 / Effect 2
kSubEffectLibrarySize9Effect types per slot (Delay…Bitcrusher)
kLfoLibrarySize10Independent LFOs (one pool shared by all layers)
kLpfLibrarySize2Master output anti-aliasing LPFs (L/R)
kBankLibrarySize5Song banks per layer
kBeatLibrarySize64Beats (steps) per bank
kBeatFillLibrarySize32Shared pool of complex-fill slots per bank
kBeatMicroLibrarySize17Sub-hits per allocated fill slot
kSampleSectorLibrarySize15SD sample RAM-index slots
kEncoderBankSize5Global Menu knob banks
kEncoderSize8Physical encoders
kFileLibrarySize100File (song) slots on SD
kDrumkitLibrarySize100Drumkit slots on SD

Filter, Effect, Reverb, Eq, Lpf, Layer and Lfo are not defined in Controller.h itself — the class only holds instances of them; the struct definitions live in Global.h alongside every other shared type and constant.

Audio Engine SW

The 10 internal sample layers and the external Line In are summed and run through one serial DSP chain: parametric EQ, two multimode filters, two send effects, then a stereo reverb, before splitting to Line Out and Headphone. Each of the 10 layers is independently modulated by the 10-slot global LFO matrix — the same signal path Randomwaves calls the device's "Sonic Architecture" in the user manual.

10× Layer Line In 10× LFO matrix mod Param EQ Filter 1 + 2 Effect 1 + 2 Reverb stereo Output Line Out / HP per-layer routing bits choose EQ / Filter 1+2 / Effect 1+2 / Reverb send — see Layer Inst menu
Fig. 5 — The DSP serial chain (processAudioEq → processAudioFilter → processAudioEffect → processAudioReverb → processAudioSend), matching the pipeline stages below.

I2S DMA pipeline

I2S3 runs ping-pong DMA at 32 samples × 2 channels × 2 halves = 128 words per buffer. The half-transfer callback processes samples 0–31; the complete-transfer callback processes samples 32–63.

StageBufferDescription
processAudioReceiveaudioReceive[]Decode I2S input from Line In
processAudioMetronomeaudioMetronome[]Mix metronome click
processAudioLayeraudioLayer[10][]Each layer reads its sample from SDRAM
processAudioEqaudioEq[]Global parametric EQ — low shelf, high shelf, 2× peak
processAudioFilteraudioFilter[N][]Per-send multimode filters
processAudioEffectaudioEffect[N][]Per-send effects (delay, chorus, etc.)
processAudioReverbaudioReverb_L/R[]Global stereo reverb
processAudioSendaudioSend_L/R[]Master output → I2S TX buffer

Signal routing — not a single strict chain

Fig. 5 shows the chain each source ultimately flows through, but every source — Line In, the metronome click, and each of the 10 layers — carries its own MixerInMode and can be summed in at any stage, not just at the head of the chain. A layer can skip the EQ and inject straight into Effect 2, for instance.

MixerInModeInjects into
MIXER_IN_EQHead of the chain, before the global EQ
MIXER_IN_FILTER_0Filter 1's input bus
MIXER_IN_FILTER_1Filter 2's input bus
MIXER_IN_EFFECT_0Effect 1's input bus
MIXER_IN_EFFECT_1Effect 2's input bus
MIXER_IN_REVERBThe reverb's mono input bus
MIXER_IN_SENDDirectly to master, after the reverb

Within the chain itself, Filter 1 and Filter 2 are cascaded (Filter 1's output feeds Filter 2's bus, on top of anything routed directly into MIXER_IN_FILTER_1), and Effect 1 → Effect 2 the same way — so the two filter slots and the two effect slots are always in series with each other, never parallel. This per-layer routing is set from the Layer Instrument menu (eq/filter/effect/reverb engage flags plus mixerInMode).

Clock configuration

HSE (8 MHz) → PLL1: PLLM=1, PLLN=68, PLLP=1 → SYSCLK = 550 MHz (VOS0)
PLL2: PLL2M=1, PLL2N=30, PLL2R=1 → FMC, SDMMC clocks
PLL3: PLL3M=1, PLL3N=25 (fractional, FRACN=3289), PLL3P=2 → SPI3/I2S3 clock
AHB divider = 2 → HCLK = 275 MHz
Audio clock, fixed I2S3 used to share PLL2 (240 MHz) with FMC and SDMMC2, and 8 MHz HSE has no integer division path to 44.1 kHz through it — the real I2S sample rate came out at 44 642.86 Hz (+1.23%, +21 cents). I2S3 now runs on its own PLL3 kernel clock in fractional mode, landing at 44.099 kHz — about 1 Hz off nominal. One consequence of the switch: playback pitch shifted slightly for any material rendered under the old clock.

DSP Modules SW

The four processing stages in the audio chain — Filter, EQ, Reverb, Effect — are each their own struct in Global.h, instantiated by the Controller (two Filters, one EQ, one Reverb, two Effects) and driven per-block from processAudioBlock(). All of them are hand-rolled biquad/delay-line DSP against the STM32 FPU — there's no CMSIS-DSP or external DSP library in the chain.

Filter (2 instances — “FIL-1” / “FIL-2”)

A single biquad per instance, coefficients derived with the standard RBJ Audio EQ Cookbook formulas (cosw0/sinw0/alpha, via the fast sine/cosine lookup tables rather than libm). Drive saturation (softKneeClip) is applied to the input before the biquad, not after.

FieldRangeNotes
TypeOFF / LPF / HPF / BPF / BSFBSF = band-stop (notch)
Slope6 or 12 dB/oct6 dB/oct is emulated by blending the 12 dB biquad output 50/50 with the undriven input, not a separate first-order filter
Frequency10 Hz – 20 kHz199-entry log-spaced lookup table
Resonance (Q)0.70 – 5.50100-step lookup table
Drive0 – 100gain = 1 + drive×0.04 (1.0–5.0×) into softKneeClip()

Parametric EQ (1 global instance)

Six biquads in series, same RBJ cookbook math as the Filter module: Low Shelf → High Shelf → 4× Peak. Each biquad is Direct-Form-I with an anti-denormal offset (1e-18) added every sample to keep the FPU out of subnormal-flush stalls during silence.

ParameterRange
Frequency47-step lookup table
Q8-step lookup table (peak bands only)
Gain37-step lookup table, index 24 = unity (0 dB)

Reverb (1 global instance)

A Freeverb/Schroeder–Moorer-style design — the classic tuning constants are unmistakable in the comb and all-pass sizes below. Pre-delay and a stereo “surround” delay line sit around the core: input is mono-summed, and the R channel is derived by tapping the L output through the surround delay rather than running two independent reverb tanks.

StageSizes (samples)
Comb filters ×8 (parallel, damped feedback)1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617
All-pass filters ×4 (series, feedback 0.35)225, 556, 441, 341
Pre-delayup to 4500 (≈102 ms), 20-step table
Surround (stereo derivation) delayup to 900 (≈20 ms), 21-step table

Comb feedback is derived from the Size and Decay parameters: combFeedback = 0.70 + size×0.15 + decay×0.13; Damping sets the one-pole low-pass coefficient inside each comb's feedback path.

Effects (2 instances, 9 selectable types each)

Each Effect slot is a union of nine distinct DSP engines — only the selected type runs. Every type shares a generic 6-parameter block (A–F, with per-type min/max/label/units), so the menu UI is data-driven rather than hard-coded per effect.

TypeNotes
DelaySDRAM ring buffer, 96,000 samples (≈2.18 s) × 2 (one per slot)
ChorusSDRAM ring buffer, 24,000 samples (≈0.54 s) × 2
FlangerShort modulated delay, params Time/Level/Feedback/Damping/Dry/Wet-shaped
PhaserAll-pass stage cascade
CompressorTwo curve modes, Light/Heavy
ExpanderDownward expansion, same envelope-follower shape as the Line-In noise gate
OverdriveSoft-knee style saturation
DistortionHarder clipping than Overdrive
BitcrusherResolution / SampleRate / Threshold / Color params

Delay and Chorus are the only two types that need dedicated sample memory, which is why they're the only ones with named SDRAM buffers (RAM_DELAY_0/1, RAM_CHORUS_0/1) — every other type runs on stack/register state only.

LFO & Modulation SW

The 10-slot LFO pool is not an audio-rate modulation matrix — it's sample-and-hold at note-trigger time. Nothing is modulated continuously mid-note; a value is read from the LFO's waveform table once, when a layer's beat fires, and held until the next trigger.

Waveforms & timing

FieldValues
TypeSIN / TRI / SAW / SQR / RND
Waveform table3200 samples per type, unipolar [0,1], precomputed in Lfo.h
Rate1/8, 1/6, 1/4, 1/3, 1/2, 1/1, 2/1, 3/1, 4/1, 6/1, 8/1
Phase8 steps, 45° increments
Depth0 – 100

Rate is a musical division of one measure, not Hz: sampleInc = (3200 × rateMultiplier) / barInterval, recalculated whenever tempo or bar length changes. The LFO's read position (sampleCounter) advances once per sequencer tick under interruptPlay() (TIM15) and is reset to 0 at every measure boundary — LFOs are tempo-locked and can't drift out of phase over a long song.

Modulation destinations

Each Layer has 8 independent, optional LFO assignments — any of the 10 pooled LFOs can feed any number of these, on any layer:

DestinationApplied range
lfoLevel±50
lfoPitch±100
lfoStart±100
lfoEnd±100
lfoTiming±100
lfoProbability±100
lfoResolution±24
lfoDirection±4-step lookup

At trigger time the firmware samples the assigned LFO's waveform table at the current sampleCounter, centers it (subtract 0.5), scales by Depth and by the destination's range above, then adds it to the layer's base parameter and clamps to that parameter's normal range. Because this only happens on trigger, a slow LFO can land a different value on every hit of a busy layer, while a layer that plays once per bar effectively samples the LFO once per bar.

Sequencer & Rhythm Data SW

Each Layer owns 5 independent song Banks; each Bank holds up to 64 Beats (one per 1/64th-resolution step slot) plus a shared pool of 32 complex-fill slots that any of those 64 beats can borrow from.

StructHoldsNotes
Bank64× Beat, 32× BeatFillOne of 5 per layer; tracks the live playhead (playBeatNum, playBeatMicroNum)
Beat16-bit packed bitfieldactive flag, fill type/pattern/time/level, start/end interval
BeatFillup to 17× BeatMicroPool-allocated on demand — a scarce shared resource, not 1:1 with beats
BeatMicroactive + level (packed byte) + intervalOne programmatically-generated sub-hit inside a fill

Beat bitfield layout

bit 15     active
bits 14-11 fillType    (0–15)
bits 10-8  fillPattern (0–7)
bits 7-4   fillTime    (0–15)
bits 3-0   fillLevel   (0–15)
— plus separate fillTarget, startInterval, endInterval fields

A plain beat (fillType == 0) just plays once at startInterval. A complex fill claims a free BeatFill slot from the bank's shared pool of 32 and generates its BeatMicro sub-hits algorithmically from a template: 3 timing alignments (Standard / Left-aligned / Right-aligned) × 5 level curves (Flat, Step, Hi–Lo, Lo–Hi, Random).

Step-advance algorithm

Controller::layerSong_calculatePlayBeatNum() runs once per bank per playhead update: it scans beatLibrary[0..lastActiveBeatNum] for the next beat (or, if the active beat has a complex fill, its next BeatMicro) whose start interval is ≥ the current play position, wrapping back to beat 0 if none is found. The result feeds layer_playBeat(), which triggers the sample and — per LFO & Modulation — samples any assigned LFOs at that moment.

Timers & Interrupts SW

TimerPurpose
TIM4MIDI TX clock output
TIM5UI transition animations
TIM6System preset auto-save debounce
TIM7Encoder preset auto-save debounce
TIM8LED EQ meter refresh
TIM12Up-key long-press detection
TIM13Down-key long-press detection
TIM14Generic key long-press
TIM15Play/sequencer tick (BPM-derived)
TIM16Text overlay auto-hide
TIM17SD card hot-plug detection
TIM23Beat sync pulse output
TIM24Limit-alert overlay display
GPIO sourceHandler
MX_X_INTinterruptLayerKeypadRead() — MCP23017 layer pads
MX_A_INTinterruptLeftKeypadRead() — MCP23017 left buttons
MX_B_INTinterruptRightKeypadRead() — MCP23017 right buttons
ENCO_0A–7AinterruptEncoderRead(0..7) — 8 rotary encoders
BUTTONinterruptFuncButtonRead() — function button
SYNC_IN_PULSEinterruptSyncInPulse() — external sync

MIDI & Sync SW

  • MIDI RX — USART1, DMA, one byte at a time (midiRxData). HAL_UART_RxCpltCallback → receiveMidiCommand(), which re-arms the DMA.
  • MIDI TX — USART6, DMA. enqueueMidiTx() queues bytes; serviceMidiTx() runs from HAL_UART_TxCpltCallback.
  • Clock output — driven by the TIM4 interrupt → interruptMidiTxClock().
  • MIDI channel — 1–16, held-key select in the Sync menu.
  • Sync PPQN — 1–24, held-key select in the Sync menu.

Multiple Drumboy units (or third-party gear) can be chained master→slave using either MIDI or Sync, per the mode tables in I/O & Connectivity.

SD Card & Storage SW

SD Card/System/
  Preset/    — System.rwp, Encoder.rwp (global device state, autosaved)
  File/      — File_001.rwf … File_100.rwf (100 song slots)
  Drumkit/   — Drumkit_001.rwd … Drumkit_100.rwd (100 kit slots)
  Data/      — Inst.lib + one *.ins per instrument (Kick, Snare, Clap, …)
  Image/     — Icon_*.rwi, Image_*.rwi (indexed-palette UI bitmaps)
  Firmware/  — archived update packages, written by the bootloader

Files and drumkits are separate save slots: a File stores Layer Song, Layer Instrument, LFO, Rhythm, EQ, Filter, Effect and Reverb settings together (a full song); a Drumkit stores the same instrument/sound-shaping settings without the song arrangement, so kits can be swapped independently of songs. Each has a 100-slot library on the SD card, browsed from the File (F) and Drumkit (D) menus.

On-disk formats

Every preset/project file opens with a fixed 26-byte ASCII reference string (a per-format magic value, e.g. kFileRef, kDrumkitRef) at offset 0, then payload from offset 25; File and Drumkit files close with a trailing "EOF" marker.

FilePathPayload
System presetPreset/System.rwpMixer + Metronome + Sync settings — one global file, no numbered slots
Encoder presetPreset/Encoder.rwp5 knob banks × 8 encoders (global) + a compact per-menu local mapping
Project (File)File/File_NNN.rwf14,360 B — 1500 B drumkit-style block + 10×1286 B song blocks (one per layer)
DrumkitDrumkit/Drumkit_NNN.rwd1500 B — marker(1) + main(174) + LFO(60) + layer(250) + inst&sample refs(1000), no sequencer data
Song sub-blockembedded in a .rwf1286 B — marker(1) + 5×257 B bank blocks, each 64 beats × 4 B

Autosave — debounced write-behind

System and Encoder presets don't save on every change; they use a one-shot debounce timer (TIM6 for System, TIM7 for Encoder) that any relevant setter restarts. If the user keeps adjusting, the timer keeps getting pushed back and nothing is written; once it's left alone long enough to expire, interruptSystemPreset()/interruptEncoderPreset() just raise a dirty flag. The main loop's saveSystemPreset()/saveEncoderPreset() check that flag, briefly disable keyboard input, serialize the live struct to SD at a fixed offset, then clear the flag — a small write-behind cache that keeps routine knob-nudging from wearing out the card.

End-user firmware update (via SD card)

  1. Download the latest firmware from randomwaves.io.
  2. Power off the device and remove the SD card.
  3. Copy the .bin file to the SD card's root directory.
  4. Re-insert the card and power on — the bootloader detects and installs it automatically.

Display & Graphics SW

The NT35510 is driven over a bit-banged, 8080-style parallel bus (LCD_CS/RS/WR/RD/RESET/BL GPIOs, NT35510.h defines the full MIPI-DCS-style command set). Every draw call programs a GRAM window and streams pixels into it: Lcd::setAddressWindow() writes the column/row bounds (commands 0x2A/0x2B, high+low byte each) then issues LCD_WRITE_MEMORY_START (0x2C) before the pixel stream.

Drawing primitives

Lcd exposes pixel/line/rectangle/circle/triangle-fill primitives, indexed-bitmap blits (drawRGB16Image / drawRGB24Image, with alpha variants), fadeRGB16/24Image for the animated menu-transition crossfades, and bitmap text/number drawing.

Fonts & icons

AssetNotes
Font_05x07.hSmallest bitmap font, 4 sizes total (05×07 / 07×09 / 10×14 / 14×18), selected via the LcdFont enum
Icon.hStatic indexed-color bitmaps compiled into firmware (e.g. the “no SD card” icon, 78×100px, 32-color palette)
System/Image/*.rwiLarger UI bitmaps (logo, menu banners, layer-strip art) loaded from SD at runtime into named SDRAM slots (RAM_IMAGE_*/RAM_ICON_*), same indexed-palette scheme as the compiled-in icons

Color is RGB565 (RGB16Color) with a large named palette in Global.hWHITE/BLACK/GRAY_05GRAY_95 in 5% steps, plus CYAN for the selection highlight and MAGENTA for the selected beat/step.

Input System SW

The 42 keys, 42 LEDs and 8 encoders all hang off six MCP23017 I2C expanders. MCP23017.h is just the register map (IODIRA/B, GPINTEN, DEFVAL, INTCON, GPPU, INTF, INTCAP, GPIOA/B…) — the six instances and their roles are wired up in Controller.cpp:

InstanceAddress offsetRole
keyboard.keyLayer0x25Layer pad matrix (read)
keyboard.keyLeft0x20Left 16-key matrix (read)
keyboard.keyRight0x21Right 16-key matrix (read)
ledLayer0x22Layer pad LEDs (write, I2C4 DMA)
ledEqLeft0x23Left EQ/VU meter LEDs (write)
ledEqRight0x24Right EQ/VU meter LEDs (write)

Layer pad buttons and layer pad LEDs are physically co-located but electrically separate — reads go through keyLayer (0x25), writes through ledLayer (0x22).

Key layout

Left keypad (16)Right keypad (16)
RESET, PLAYSTOP, REC, ADD, UP, CLEAR, LEFT, CENTER, RIGHT, COPY, DOWN, PASTE, MUTE, FILL, STYLE, SONGFILE, DRUMKIT, MIXER, RHYTHM, METRONOME, IO, FILTER‑0, FILTER‑1, EQ, EFFECT‑0, EFFECT‑1, REVERB, LFO, LAYER_INST, LAYER_SONG, KNOB

A layer pad's meaning is entirely context-sensitive: with no modifier held it fires layer_playBeat() (and writes a sequencer step if recording); held together with MUTE/FILL/STYLE it toggles that state instead; held with LFO/LAYER_INST/LAYER_SONG it opens that layer's menu; held with the bank or knob-bank keys it selects a song bank or encoder bank.

LED priority

led_action() resolves what each LED shows in strict priority order, highest first: (1) layer-selection highlight (bank/encoder-bank picker) overrides everything, (2) real-time beat-playback flash (one bit per layer, set/cleared on the beat start/end flags), (3) the stereo EQ/VU meter, peak-hold with linear decay through a 12-step non-linear dB ladder, alternating which channel (L/R) it services each call to spread the I2C DMA load. Every LED write is non-blocking, gated on the I2C peripheral being idle.

Menu System SW

The UI splits into two operational areas: a fully customizable Global Menu for frequently-used functions, and fixed-parameter Dedicated Menus (Rhythm, Metronome, Sync, EQ, Filter, Effect, Reverb, LFO, Layer Inst, Layer Song, File, Drumkit, Mixer) for precise sound shaping.

Knob control

8 assignable knobs behave differently per context. In dedicated menus their function is fixed. In the Global Menu, each of 5 knob banks (40 mappable functions total) is user-defined — hold Select and turn a knob to map it, hold Knob Bank + a Layer key to switch banks. Holding F while turning a knob doubles the change rate (Fast Mode); holding S halves it (Slow Mode).

Menu key reference

KeyOpens
FFile menu
DDrumkit menu
MMixer menu
RHYRhythm menu
METROMetronome menu
SYNCSync menu
EQParametric EQ menu
FILFilter 1/2 menu
EFFEffect 1/2 menu
REVReverb menu
L + LayerLFO menu (held)
I + LayerLayer Instrument menu (cycles 3 pages)
S + LayerLayer Song menu
M/F/P + LayerInstant Mute / Fill / PlayMode toggle for that layer

Per-layer signal routing is fixed order: Layer → Eq → Filter 1 → Filter 2 → Effect 1 → Effect 2 → Reverb → Line Out, with each stage independently enabled per layer from the Layer Instrument menu. Full parameter ranges for every menu (Rhythm, Mixer, EQ, Filter, Effect, Reverb, LFO, etc.) are documented in the printed User Manual.

Build, Flash & Debug SW

  1. Install STM32CubeIDE and open the Drumboy-Pro-App-H723 (and/or -Boot-H723) project folder.
  2. Wire SWDIO, GND and SWCLK from the board to an ST-Link V2, then plug the ST-Link into your computer over USB.
  3. Build and flash for target STM32H723ZGTX — use the provided debug launch config (Drumboy-Pro-App-H723 Debug.launch / ...-Boot-H723 Debug.launch).
Flash origin matters The application must build with FLASH origin at 0x08020000 (896 KB, sectors 1–7) — never overlap the bootloader's sector 0. The linker scripts (STM32H723ZGTX_FLASH.ld) in each project already encode this; if you fork the project, keep it that way or the bootloader's jump target breaks.

Coding patterns worth knowing before you touch the DSP or ISR code

  • Audio buffers are float internally; int32↔float conversion happens only in processAudioReceive / processAudioSend.
  • ALWAYSINLINE forces inlining of hot audio helpers (softClip, softKneeClip, amplitudeTodB, etc.).
  • D3 RAM (.RAM_D3) holds BDMA-accessible I2C buffers — declare those with __attribute__((section(".RAM_D3"))).
  • FTZ is enabled at startup — don't rely on denormal float behavior in DSP code.
  • Every encoder action calls an xxx_set*() method that updates the data model and redraws the relevant LCD region in the same call.