Skip to content

Instantly share code, notes, and snippets.

@lomereiter
Created September 5, 2026 17:10
Show Gist options
  • Select an option

  • Save lomereiter/9b8f986f69c2fbbf2cbb5dded1a93a4d to your computer and use it in GitHub Desktop.

Select an option

Save lomereiter/9b8f986f69c2fbbf2cbb5dded1a93a4d to your computer and use it in GitHub Desktop.
MicroFreak firmware reverse engineering

I/O addresses for emulation

Everything here is pinned to two firmware images:

  • Main MCU: nanowave_main__fw5_0_0_2084__2023_05_30.bin (STM32F446, Cortex-M4F)
  • OLED chip: nanowave_iochip_oled__fw1_0_0_1905__2023_05_29.bin (Cortex-M0-class)

Firmware images

  • Both files carry a 64-byte header; the ARM payload starts at file offset 0x40.
  • The header is skipped by the bootloader; load the payload, not the file start.
  • Main payload lives at 0x0800C000 because a custom DFU bootloader (not part of the file) occupies 0x08000000–0x0800BFFF.
  • The OLED payload loads at 0x08000000 (no bootloader hole).

Main MCU — memory and boot

  • RAM: 0x20000000–0x2001FFFF (128 KB). QSPI flash window: 0x90000000.
  • Initial stack pointer (vector table word 0 at 0x0800C000): 0x20020000.
  • Reset entry (word 1): 0x0805F848 (Thumb bit set; start emulation at 0x0805F849). It runs the real startup, including .data copy and C library init.
  • Some calibration data is copied at boot from the bootloader hole. You might need to set some of it to non-zero values (by trial-and-error).
  • Enable the FPU before any float code: write 0x00F00000 to CPACR (0xE000ED88).
  • Unicorn requirements: UC_MODE_THUMB | UC_MODE_MCLASS plus explicit UC_CPU_ARM_CORTEX_M4. Other mode/model combinations misdecode Thumb-2 VFP.

Main MCU — the main loop (the universal injection point)

  • The foreground loop's per-iteration work starts in a function entered at 0x0803A750. Running one call from there to its natural return is the cleanest unit of firmware progress; stop/insert all I/O between iterations.
  • HAL tick storage: 0x2001E1D8 (advance it to make timeouts and blinkers run).
  • A USB polling gate at 0x200009D4 must be zeroed after every loop iteration; if it latches non-zero, the USB-MIDI TX path stops being serviced.
  • Peripheral-base shortcuts: USART2 0x40004400, TIM13 0x40001C00, DAC 0x40007400.

Main MCU — MIDI input

DIN (USART1, 31250 baud, PA9/PA10, IRQ 37):

  • The RX interrupt (entry 0x08058214) only moves bytes into a ring; parsing happens in the main loop.
  • Byte-ring wrapper at 0x2001E08C:
    • +0x0C: TX busy flag
    • +0x10: write index, +0x14: read index (mod 256)
    • +0x18: 256-byte data area (index mask 0xFF)
  • To inject: append bytes at the write index and advance it; the firmware advances the read index.
  • The drain step (called from the loop, 0x08047C6C) pops bytes into the parser at 0x080480A8. Parser state at 0x2001DC5C:
    • +0xB8 parser state, +0xB9 running status, +0xBA/+0xBB data bytes
    • Handles channel voice messages with running status, system common/realtime, and SysEx (F0…F7) with payload dispatch.
  • Useful SysEx on this path (Arturia ID 00 20 6B):
    • Identity request F0 7E 7F 06 01 F7 → identity reply (mfr 00 20 6B, version 05 00 00 24).
    • Command frames F0 00 20 6B 07 [opcode] [payload] F7: opcode 0x40 = raw 16-bit parameter write (id = group<<8 | index), 0x41 = parameter value query (reply is 7-bit packed), 0x42/0x43 = config-byte write/read.
  • USB-MIDI: the same parser output feeds both paths; the USB side's sole transmit exit is the call at 0x0801D1B8:
    • r1 == 0x81 selects a MIDI transfer (r1 == 0 is an audio SPI transfer through the same code — filter it out), context at 0x2001EBF0.
    • r2/r3 point at USB-MIDI event packets: 4-byte packets (CIN + up to 3 MIDI bytes) that must be decoded back to plain MIDI bytes.

Main MCU — audio output

  • Render context (render globals, double buffers, voice state) at 0x20004830.
  • The render entry point is 0x08038DF9 (call with r0 = context, r1 = buffer-half index). It renders one 32-sample block.
  • DAC output buffer: context + 0x16E72 (= 0x2001B6A2):
    • s16 stereo interleaved, L = R.
    • Double-buffered in 128-byte halves: half 0 at +0x16E72, half 1 at +0x16EF2.
    • The render writes the half selected by r1; read the other half.
  • Per-iteration audio clock service: write TIM13's counter (0x40001C24) with position * 50000 / sample_rate before calling the render, or time-dependent code (LFOs, envelopes) freezes.
  • The digital stream is an ungated drone: the amp envelope is an analog control voltage, not in the signal path. To gate host-side:
    • Amp CV (0..32767): *(u16*)0x200004D4 — 0 idle, ~32767 note held.
    • Master volume: *(u16*)0x20000008.
    • The CV routes physically end in TIM3 PWM compare registers (0x40000400 block) and the on-chip DAC (0x40007408); they do execute in emulation.
    • Envelope state: voice base 0x2001B17C, +0x1A0 → +0x00 state (0 idle, 1 attack, 2 decay, 3 sustain, 4 release), +0x64 output value.

Main ↔ OLED iochip link (USART2, 460800 baud, PA2/PA3, IRQ 38)

Wire format (both directions), 8 bytes:

  • AA m0 m1 m2 m3 m4 m5 ck with ck = (0xAA + m0 + … + m5) & 0xFF.
  • The logical message is the six bytes m0..m5; dispatch keys on m0 & 0x0F.
  • Event family (m0 low nibble 0): m1 = event code, payload in m2..m5. Key codes:
    • 0x01 / 0x02: ui-command set/clear; id16 = (m2 & 0x1F) << 8 | m3, m5 = mask (mask&3 selects increment/decrement direction).
    • 0x03 / 0x04: parameter set/query; id16 as above, val16 = m4 << 8 | m5.
    • 0x05 / 0x06 / 0x07: note on / off / control variants.
    • 0x0F: modulation-matrix value; val16 is scale-shifted (≥8 → <<4).
  • A config byte at 0x200007A8 + 0x1E gates event routing: when non-zero, only event 0x03 with id 0x704 (master volume) is delivered.

Main side, receive (frames from the OLED chip — mostly absent in practice):

  • Byte-ring wrapper at 0x2001E004:
    • +0x0C: TX busy, +0x10: write index, +0x14: read index, +0x18: 64-byte data area (mask 0x3F).
  • Drained once per loop iteration by the routine at 0x0802D530: frame synchronize/validate (0x0807F678), then dispatch (0x080219D4, dispatch object 0x200005A0, its vtable at 0x08073500), then event routing (0x08021794).
  • To inject frames: append wire bytes to the ring and advance the write index; the firmware does the rest on its next iteration.

Main side, transmit (display frames to the OLED chip):

  • Bytes leave via DMA, so writes to the UART data register are invisible to a memory hook. The reliable capture point is the frame-enqueue routine at 0x08021414 (r0 = message words packed LE in r1/r2) — hook it, take the six-byte message, and wire-frame it yourself.

OLED iochip

  • RAM: 0x20000000–0x20007FFF (32 KB). Boot entry 0x080000B8 (skips SystemInit), initial stack 0x200020F0, tick counter at 0x2000002C.
  • Receiving display frames:
    • RX dispatch loop at 0x08005F82; it consumes bytes and runs the main loop tail. Return to the main loop happens at 0x0800AB6C.
    • Dispatch object 0x20000EF0; its +0x38 field holds a pointer to the RX queue object (a vtable with "read byte" and "available" entries).
    • Easiest injection: point +0x38 at a small RAM-backed fake queue and run the dispatch loop with an instruction cap until the queue drains (~10–50k instructions per frame).
    • It ignores all frame families not addressed to it — replaying the whole shared wire is safe.
  • Rendering output:
    • SSD1306 framebuffer at 0x20000068, 1024 bytes — 128 columns × 8 pages, page format (byte = 8 vertical pixels). This is the display output; read it instead of emulating I2C.
    • It is pushed to the panel over I2C1 (0x40005400, address 0x3C).
    • Text buffer at 0x2000049C (length at 0x20000498); active font pointer at 0x20000494 — boot selects the sans-serif font at 0x0800E964.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment