Drumboy Pro Wiki
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.
Hardware
Specifications HW
| Subsystem | Part | Interface | Notes |
|---|---|---|---|
| MCU | STM32H723ZGT6 | LQFP-144 | Cortex-M7, 550 MHz (VOS0), 127 of 144 pins used |
| Display driver | NT35510 | 16-bit parallel (GPIO-driven) | 540×960 TFT with its own onboard GRAM, off-board module on a 51-pin FPC |
| Display backlight driver | MP3302DJ-LF-Z | Boost converter (SW node) | Drives the LCD's LED backlight |
| Display connector | FH26W-51S FPC, 0.3 mm pitch | — | 51-pin flex from Drumboy_Main straight to the LCD module |
| Audio codec | SGTL5000XNBA3R2 | I2S3 (audio) + I2C3 (control, addr 0x14) | Headphone / line-out DAC, line-in ADC |
| SDRAM | IS42S16800J-7TL-TR | 16-bit FMC SDRAM | 16 MB (8M×16) at 0xC0000000 |
| Storage | TF-01A microSD socket | SDMMC2, 4-bit bus | FAT filesystem — samples, projects, firmware packages |
| I/O expanders | 6× MCP23017 | I2C3 (3×) + I2C4 (3×), addr 0x20–0x25 | 3× interrupt-driven keypad matrices, 3× output-only LED drivers |
| Encoders | 8× PEC12R-4017F | GPIO quadrature, A-channel on EXTI | Spread across ports A/C/E/F/G |
| Keys | 42× tactile switches | via 3× MCP23017 matrix (X/A/B) | Layer pads (X), left & right button matrices (A/B) |
| LEDs | 42× discrete LEDs | via 3× MCP23017 (C/D/E) + 3× direct GPIO | 12-seg EQ L/EQ R meters, layer indicators, LED0–2 status LEDs on PC1–PC3 |
| MIDI | H11L1 opto-isolator + USART1/USART6 | 31,250 baud, half-duplex single-wire | Isolated in (3.3 V digital domain), direct-drive out |
| Power in | USB-C (TYPE-C-31-M-17) | CC1/CC2 → 5.1 kΩ pulldowns | 0.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.
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:
| Stage | Config | Output | Feeds |
|---|---|---|---|
| HSE | — | 8 MHz | PLL reference |
| PLL1 | M=1, N=68, P=1 | 550 MHz | SYSCLK → Cortex-M7 core (VOS0) |
| PLL2 | M=1, N=30, P=1, R=1 | 240 MHz | FMC, SDMMC2 kernel clock |
| PLL3 | M=1, N=25 (fractional, FRACN=3289), P=2 | ≈101.606 MHz | SPI3/I2S3 kernel clock — dedicated audio PLL, real I2S rate 44.1 kHz |
| AHB (HCLK) | HPRE /2 | 275 MHz | AXI bus, DMA, GPIO |
| APB1–4 | D1/D2/D3 PPRE /2 | 137.5 MHz | USART, I2C, most TIM peripheral clocks |
Boot & debug pins
| Pin | CubeMX signal | Notes |
|---|---|---|
| PA13 (JTMS/SWDIO) | DEBUG_JTMS-SWDIO | SWD data |
| PA14 (JTCK/SWCLK) | DEBUG_JTCK-SWCLK | SWD 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.
Drumboy_Main.kicad_sch.| Bus | Signals | MCU pins |
|---|---|---|
| Data (16-bit) | FMC_D0–D15 | PD14, PD15, PD0, PD1, PE7–PE15, PD8–PD10 |
| Address (mux) | FMC_A0–A11 | PF0–PF5, PF12–PF15, PG0, PG1 |
| Bank select | FMC_BA0, FMC_BA1 | PG4, PG5 |
| Byte lanes | FMC_NBL0, FMC_NBL1 | PE0, PE1 |
| Clock / enable | FMC_SDCLK, FMC_SDCKE0 | PG8, PC5 |
| Chip select | FMC_SDNE0 | PC4 |
| Strobes | FMC_SDNRAS, FMC_SDNCAS, FMC_SDNWE | PF11, PG15, PC0 |
The hardware FMC SDRAM controller times the interface entirely in SDCLK cycles:
| Parameter | Value |
|---|---|
| Column bits | 9 |
| CAS latency | 2 cycles |
| SDCLK period | HCLK / 2 |
| Read burst / pipe delay | Enabled / 2 cycles |
| Load-to-active delay | 2 cycles |
| Exit self-refresh delay | 9 cycles |
| Self-refresh time | 5 cycles |
| Row cycle delay | 8 cycles |
| Write recovery time | 4 cycles |
| RP / RCD delay | 2 / 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.
| Signal | MCU pin(s) | Role |
|---|---|---|
| LCD_D0–D15 | PB0–PB15 | 16-bit parallel data bus to the panel's GRAM (GPIO) |
| LCD_CS | PD2 | Chip select |
| LCD_RD | PD3 | Read strobe |
| LCD_WR | PD4 | Write strobe |
| LCD_RS | PD5 | Register/data select |
| LCD_RESET | PG13 | Panel reset |
| LCD_BL | PG14 | Backlight enable |
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:
| Bus | Signal | MCU pin |
|---|---|---|
I2C3 (control, addr 0x14) | I2C3_SCL | PA8 |
| I2C3_SDA | PC9 | |
| I2S3 (audio) | I2S3_WS | PA4 |
| I2S3_CK | PC10 | |
| I2S3_SDI | PC11 | |
| I2S3_SDO / I2S3_MCK | PC12 / PC7 |
I2S3 runs on its own PLL3-derived kernel clock, in fractional mode: CubeMX reports I2S3.RealAudioFreq at 44.099 kHz.
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:
| Ref | Addr | Bus | Reset / INT nets | Role |
|---|---|---|---|---|
| U1 | 0x25 | I2C3 | MX_X_RESET / MX_X_INT | Layer keypad (input, EXTI) |
| U2 | 0x20 | I2C3 | MX_A_RESET / MX_A_INT | Left keypad (input, EXTI) |
| U3 | 0x21 | I2C3 | MX_B_RESET / MX_B_INT | Right keypad (input, EXTI) |
| U4 | 0x22 | I2C4 | MX_C_RESET | LED driver, output only |
| U5 | 0x23 | I2C4 | MX_D_RESET | LED driver, output only |
| U6 | 0x24 | I2C4 | MX_E_RESET | LED driver, output only |
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:
Drumboy_Main.kicad_sch. Every encoder channel repeats this pull-up + RC debounce pattern.| Encoder | A (EXTI) | B |
|---|---|---|
| 0 | PE2 | PE6 |
| 1 | PE3 | PA5 |
| 2 | PE4 | PA6 |
| 3 | PE5 | PA7 |
| 4 | PG6 | PG2 |
| 5 | PG7 | PG3 |
| 6 | PF8 | PC14 (OSC32_IN) |
| 7 | PF9 | PC15 (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:
| Signal | MCU pin |
|---|---|
| SDMMC2_CK | PD6 |
| SDMMC2_CMD | PD7 |
| SDMMC2_D0–D3 | PG9, PG10, PG11, PG12 |
| SDMMC2_DETECT | PC13 |
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:
| Port | Signal(s) | MCU pin | Notes |
|---|---|---|---|
| MIDI In | MIDI_RX | PA9 (USART1_TX pin, half-duplex) | Through U7 (H11L1 opto-isolator) + D10 (1N4148WT) protection diode, isolated onto the 3.3 V digital domain |
| MIDI Out | MIDI_TX | PC6 (USART6_TX) | Direct drive, 47 Ω current-limit resistors (R58, R62) |
| Sync In | SYNC_IN_PULSE / _GATE | PA15 (EXTI15) / PF6 | GPIO pulse + gate pair |
| Sync Out | SYNC_OUT_PULSE / _GATE | PA10 / PA12 | GPIO, 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.
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.
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:
| Domain | Ferrites | Local rail / ground | Regulator |
|---|---|---|---|
| USB | FB1, FB2 | +5V_USB / GND_USB | — |
| Digital | FB3, FB4 | +5V_DIG / GND_DIG | IC4 (AP2112K-3.3) → +3.3V_DIG |
| LCD | FB5, FB6 | +5V_LCD / GND_LCD | Feeds IC3 boost converter, see below |
| DAC (codec digital) | FB7, FB8 | +5V_DAC / GND_DAC | IC5 (AP2112K-3.3) → +3.3V_DAC |
| DAC analog | FB9 | +3.3V_DAC_ANA / GND_DAC | IC6 (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.
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.
| Ref | Part | Qty | Role |
|---|---|---|---|
| IC2 | STM32H723ZGT6 | 1 | Main MCU, LQFP-144 |
| IC1 | IS42S16800J-7TL-TR | 1 | 16 MB SDRAM, sample memory |
| IC7 | SGTL5000XNBA3R2 | 1 | Audio codec, QFN-33 |
| IC3 | MP3302DJ-LF-Z | 1 | Boost converter — LCD backlight driver, not audio |
| L1 | NRS5024T4R7MMGJ | 1 | 4.7 µH boost inductor for IC3 |
| D9 | MBR0520LT3G | 1 | Schottky rectifier for the IC3 boost circuit |
| IC4, IC5 | AP2112K-3.3TRG1 | 2 | 3.3 V LDOs — digital rail (IC4) and codec-digital rail (IC5) |
| IC6 | AP2112K-1.8TRG1 | 1 | 1.8 V LDO, codec analog rail |
| U1–U6 | MCP23017_ML | 6 | I2C GPIO expanders — 3× keypad (I2C3), 3× LED driver (I2C4) |
| U8–U15 | PEC12R-4017F-N0024 | 8 | Rotary encoders |
| U7 | H11L1 | 1 | MIDI-in opto-isolator |
| D10 | 1N4148WT | 1 | MIDI-in loop protection diode |
| SW1–SW42 | SW_Push (SW_PG1350) | 42 | Tactile key switches |
| D1–D44 (excl. D9/D10) | LED (G/W/R/O/Y) | 42 | Pad backlight + EQ meter LEDs |
| J1 | TYPE-C-31-M-17 | 1 | USB-C power input |
| J2 | FH26W-51S-0.3SHW | 1 | 51-pin FPC to the LCD module |
| J4–J10 | SJ1-3515-SMT-TR | 7 | 3.5 mm TRS jacks |
| MICRO_SD | TF-01A | 1 | microSD socket |
| Y1 | ABM7-8.000MHZ-D2Y-T | 1 | HSE crystal, 8 MHz |
| F1 | MF-PSMF075X-2 | 1 | 0.75 A resettable fuse, USB-C input |
| FB1–FB9 | BLM18AG (102SN1D / 601SN1D) | 9 | Per-domain power isolation |
| R1, R2 | 5.1 kΩ | 2 | USB-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.
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.
| Project | Repository | Responsibility |
|---|---|---|
Drumboy-Pro-Boot-H723 | github.com/Randomwaves-Team/Drumboy-Pro-Boot-H723 | Checks the SD card for a firmware package, validates and programs it, then jumps to the application |
Drumboy-Pro-App-H723 | github.com/Randomwaves-Team/Drumboy-Pro-App-H723 | The sequencer, DSP engine, UI, and all runtime behavior |
Memory Map SW
FLASH_FIRMWARE_ADDRESS = 0x08020000 is a hard boundary neither project may cross.| Region | Address | Size | Purpose (application project) |
|---|---|---|---|
| FLASH (app) | 0x08020000 | 896 KB | Application code + constants |
| RAM_D1 | 0x24000000 | 320 KB | Stack, heap, audio buffers, Controller state |
| RAM_D2 | 0x30000000 | 32 KB | DMA buffers (NOLOAD section) |
| RAM_D3 | 0x38000000 | 16 KB | BDMA-accessible I2C buffers (i2c3_rx/tx, i2c4_rx/tx) |
| DTCMRAM | 0x20000000 | 128 KB | Tightly-coupled data |
| ITCMRAM | 0x00000000 | 64 KB | Tightly-coupled instructions |
| SDRAM | 0xC0000000 | 16 MB | Sample 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
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
| State | Message |
|---|---|
| In progress | UPDATING FIRMWARE |
| Success | FIRMWARE UPDATED |
| Bad header | FIRMWARE ERROR / FORMAT-CHECK |
| Version out of range | FIRMWARE ERROR / VERSION-CHECK |
| Size out of range | FIRMWARE ERROR / SIZE-CHECK |
| Pre-write CRC mismatch | FIRMWARE ERROR / CRC-CHECK |
| Post-write verify mismatch | FIRMWARE 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 peripherals —
Lcd,Dac,Sd,Keyboard,Encoder[8],Led(EQ left, EQ right, layer) - DSP modules —
Lpf[],Eq,Filter[],Effect[],Reverb,Metronome,Lfo[] - Domain data —
Layer[10](instrument + sequencer data per layer),SampleSector[](SD card sample index) - Transport state —
playActive,recordActive,playArmed,recordArmed,playInterval,songInterval - Menu state —
menu,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:
| Constant | Value | What it sizes |
|---|---|---|
| kLayerLibrarySize | 10 | Layers (instruments) |
| kFilterLibrarySize | 2 | Filter 1 / Filter 2 |
| kEffectLibrarySize | 2 | Effect 1 / Effect 2 |
| kSubEffectLibrarySize | 9 | Effect types per slot (Delay…Bitcrusher) |
| kLfoLibrarySize | 10 | Independent LFOs (one pool shared by all layers) |
| kLpfLibrarySize | 2 | Master output anti-aliasing LPFs (L/R) |
| kBankLibrarySize | 5 | Song banks per layer |
| kBeatLibrarySize | 64 | Beats (steps) per bank |
| kBeatFillLibrarySize | 32 | Shared pool of complex-fill slots per bank |
| kBeatMicroLibrarySize | 17 | Sub-hits per allocated fill slot |
| kSampleSectorLibrarySize | 15 | SD sample RAM-index slots |
| kEncoderBankSize | 5 | Global Menu knob banks |
| kEncoderSize | 8 | Physical encoders |
| kFileLibrarySize | 100 | File (song) slots on SD |
| kDrumkitLibrarySize | 100 | Drumkit 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.
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.
| Stage | Buffer | Description |
|---|---|---|
| processAudioReceive | audioReceive[] | Decode I2S input from Line In |
| processAudioMetronome | audioMetronome[] | Mix metronome click |
| processAudioLayer | audioLayer[10][] | Each layer reads its sample from SDRAM |
| processAudioEq | audioEq[] | Global parametric EQ — low shelf, high shelf, 2× peak |
| processAudioFilter | audioFilter[N][] | Per-send multimode filters |
| processAudioEffect | audioEffect[N][] | Per-send effects (delay, chorus, etc.) |
| processAudioReverb | audioReverb_L/R[] | Global stereo reverb |
| processAudioSend | audioSend_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.
| MixerInMode | Injects into |
|---|---|
| MIXER_IN_EQ | Head of the chain, before the global EQ |
| MIXER_IN_FILTER_0 | Filter 1's input bus |
| MIXER_IN_FILTER_1 | Filter 2's input bus |
| MIXER_IN_EFFECT_0 | Effect 1's input bus |
| MIXER_IN_EFFECT_1 | Effect 2's input bus |
| MIXER_IN_REVERB | The reverb's mono input bus |
| MIXER_IN_SEND | Directly 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
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.
| Field | Range | Notes |
|---|---|---|
| Type | OFF / LPF / HPF / BPF / BSF | BSF = band-stop (notch) |
| Slope | 6 or 12 dB/oct | 6 dB/oct is emulated by blending the 12 dB biquad output 50/50 with the undriven input, not a separate first-order filter |
| Frequency | 10 Hz – 20 kHz | 199-entry log-spaced lookup table |
| Resonance (Q) | 0.70 – 5.50 | 100-step lookup table |
| Drive | 0 – 100 | gain = 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.
| Parameter | Range |
|---|---|
| Frequency | 47-step lookup table |
| Q | 8-step lookup table (peak bands only) |
| Gain | 37-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.
| Stage | Sizes (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-delay | up to 4500 (≈102 ms), 20-step table |
| Surround (stereo derivation) delay | up 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.
| Type | Notes |
|---|---|
| Delay | SDRAM ring buffer, 96,000 samples (≈2.18 s) × 2 (one per slot) |
| Chorus | SDRAM ring buffer, 24,000 samples (≈0.54 s) × 2 |
| Flanger | Short modulated delay, params Time/Level/Feedback/Damping/Dry/Wet-shaped |
| Phaser | All-pass stage cascade |
| Compressor | Two curve modes, Light/Heavy |
| Expander | Downward expansion, same envelope-follower shape as the Line-In noise gate |
| Overdrive | Soft-knee style saturation |
| Distortion | Harder clipping than Overdrive |
| Bitcrusher | Resolution / 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
| Field | Values |
|---|---|
| Type | SIN / TRI / SAW / SQR / RND |
| Waveform table | 3200 samples per type, unipolar [0,1], precomputed in Lfo.h |
| Rate | 1/8, 1/6, 1/4, 1/3, 1/2, 1/1, 2/1, 3/1, 4/1, 6/1, 8/1 |
| Phase | 8 steps, 45° increments |
| Depth | 0 – 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:
| Destination | Applied 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.
| Struct | Holds | Notes |
|---|---|---|
| Bank | 64× Beat, 32× BeatFill | One of 5 per layer; tracks the live playhead (playBeatNum, playBeatMicroNum) |
| Beat | 16-bit packed bitfield | active flag, fill type/pattern/time/level, start/end interval |
| BeatFill | up to 17× BeatMicro | Pool-allocated on demand — a scarce shared resource, not 1:1 with beats |
| BeatMicro | active + level (packed byte) + interval | One 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
| Timer | Purpose |
|---|---|
| TIM4 | MIDI TX clock output |
| TIM5 | UI transition animations |
| TIM6 | System preset auto-save debounce |
| TIM7 | Encoder preset auto-save debounce |
| TIM8 | LED EQ meter refresh |
| TIM12 | Up-key long-press detection |
| TIM13 | Down-key long-press detection |
| TIM14 | Generic key long-press |
| TIM15 | Play/sequencer tick (BPM-derived) |
| TIM16 | Text overlay auto-hide |
| TIM17 | SD card hot-plug detection |
| TIM23 | Beat sync pulse output |
| TIM24 | Limit-alert overlay display |
| GPIO source | Handler |
|---|---|
| MX_X_INT | interruptLayerKeypadRead() — MCP23017 layer pads |
| MX_A_INT | interruptLeftKeypadRead() — MCP23017 left buttons |
| MX_B_INT | interruptRightKeypadRead() — MCP23017 right buttons |
| ENCO_0A–7A | interruptEncoderRead(0..7) — 8 rotary encoders |
| BUTTON | interruptFuncButtonRead() — function button |
| SYNC_IN_PULSE | interruptSyncInPulse() — 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 fromHAL_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.
| File | Path | Payload |
|---|---|---|
| System preset | Preset/System.rwp | Mixer + Metronome + Sync settings — one global file, no numbered slots |
| Encoder preset | Preset/Encoder.rwp | 5 knob banks × 8 encoders (global) + a compact per-menu local mapping |
| Project (File) | File/File_NNN.rwf | 14,360 B — 1500 B drumkit-style block + 10×1286 B song blocks (one per layer) |
| Drumkit | Drumkit/Drumkit_NNN.rwd | 1500 B — marker(1) + main(174) + LFO(60) + layer(250) + inst&sample refs(1000), no sequencer data |
| Song sub-block | embedded in a .rwf | 1286 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)
- Download the latest firmware from
randomwaves.io. - Power off the device and remove the SD card.
- Copy the
.binfile to the SD card's root directory. - 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
| Asset | Notes |
|---|---|
| Font_05x07.h | Smallest bitmap font, 4 sizes total (05×07 / 07×09 / 10×14 / 14×18), selected via the LcdFont enum |
| Icon.h | Static indexed-color bitmaps compiled into firmware (e.g. the “no SD card” icon, 78×100px, 32-color palette) |
| System/Image/*.rwi | Larger 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.h — WHITE/BLACK/GRAY_05…GRAY_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:
| Instance | Address offset | Role |
|---|---|---|
| keyboard.keyLayer | 0x25 | Layer pad matrix (read) |
| keyboard.keyLeft | 0x20 | Left 16-key matrix (read) |
| keyboard.keyRight | 0x21 | Right 16-key matrix (read) |
| ledLayer | 0x22 | Layer pad LEDs (write, I2C4 DMA) |
| ledEqLeft | 0x23 | Left EQ/VU meter LEDs (write) |
| ledEqRight | 0x24 | Right 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, SONG | FILE, 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
| Key | Opens |
|---|---|
| F | File menu |
| D | Drumkit menu |
| M | Mixer menu |
| RHY | Rhythm menu |
| METRO | Metronome menu |
| SYNC | Sync menu |
| EQ | Parametric EQ menu |
| FIL | Filter 1/2 menu |
| EFF | Effect 1/2 menu |
| REV | Reverb menu |
| L + Layer | LFO menu (held) |
| I + Layer | Layer Instrument menu (cycles 3 pages) |
| S + Layer | Layer Song menu |
| M/F/P + Layer | Instant 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
- Install STM32CubeIDE and open the
Drumboy-Pro-App-H723(and/or-Boot-H723) project folder. - Wire
SWDIO,GNDandSWCLKfrom the board to an ST-Link V2, then plug the ST-Link into your computer over USB. - Build and flash for target
STM32H723ZGTX— use the provided debug launch config (Drumboy-Pro-App-H723 Debug.launch/...-Boot-H723 Debug.launch).
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
floatinternally; int32↔float conversion happens only inprocessAudioReceive/processAudioSend. ALWAYSINLINEforces 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.