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)
- 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).
- 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_MCLASSplus explicitUC_CPU_ARM_CORTEX_M4. Other mode/model combinations misdecode Thumb-2 VFP.
- 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.
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 (mfr00 20 6B, version05 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.
- Identity request
- 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.
- 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_ratebefore 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.
- Amp CV (0..32767):
Wire format (both directions), 8 bytes:
AA m0 m1 m2 m3 m4 m5 ckwithck = (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).
- 0x01 / 0x02: ui-command set/clear;
- 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.
- 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.