Firmware

keystep37-mod-1-public

Hype23/100

Arturia KeyStep 37 firmware 1.1.6 reverse-engineering notes (derived, not vendor material)

keystep37-mod-1-public is custom firmware for the Arturia KeyStep 37, by @jim-bitbot. Open source under MIT, 3 stars on GitHub.

forArturia KeyStep 37·by@jim-bitbot

Unclaimed. Is this yours? Sign in with GitHub as @jim-bitbot to claim it.

Downloads
0
Stars
3
Forks
0
Commits
15
Last commit
Get the source

No release files yet. Build from source.

MIT· jim-bitbot/keystep37-mod-1-public·downloads come from the creator’s GitHub releases

12
Commits
0
Releases
12 commits and 0 releases in the last year
Activity · last 12 months

KeyStep 37 — firmware 1.1.6 notes

Derived reverse-engineering notes for the stock Arturia KeyStep 37 application firmware 1.1.6.579. Not Arturia’s firmware, not a replacement image, and not something you flash from this repo. The notes and scripts here are MIT; Arturia’s firmware is not.

Vendor .led files, the stripped flash extract, official manuals, and update captures are not published. They stay local (see .gitignore). Everything here is addresses, dumps, narratives, and tools that operate on a copy you already have.

If you came here for a custom firmware: there isn’t one.

Start here

FileWhat it is
firmware-re/notes/address-catalog.mdNamed flash/RAM addresses. This is the map.
firmware-re/notes/stock-shift-map.mdEvery panel gesture and its Test-20 control ID.
firmware-re/notes/scans/tickets.mdTicket list A–BA and what each one asked.
firmware-re/notes/flash-map.mdMCU, app range, unused fill.
firmware-re/notes/led-header.mdHow a .led is framed (Huaxin segments + checksums).
docs/ARTURIA-FIRMWARE-RE-GUIDE.mdHow the notes were built (wire constant → cmp → callers).

Catalog tags:

  • P — live protocol or occupancy ID matches an immediate in code
  • S — structure from disassembly or Unicorn
  • H — hypothesis (including claims not re-traced here)
  • X — ruled out

A ticket is a scan dump plus a model write-up. Scans live in firmware-re/notes/scans/. Narratives and proposed catalog rows live in firmware-re/notes/model/. The catalog is the accepted subset.

Tickets A–AS are modeled and copied into the catalog. Scans AT–BA are on disk and not modeled yet.

firmware-re/notes/findings-2026-09-20.md is a chronological lab log. Mid-file “not yet” paragraphs are stale; prefer the catalog. model/synthesis.md still has a “Top open leads (post Round-2)” list; treat the sections below as current.

Coverage

App Thumb 0x08004000–0x0801F400 is about 109 KB. recreate.py names roughly 85 function starts. The catalog has about 270 unique flash addresses and 75 RAM addresses; about 30 rows are P, about 150 are S. A C++ STM32 app this size usually has high-hundreds of functions, so by count this is perhaps 10–15%. That is an order-of-magnitude guess, not a measurement.

Function count undersells the hot path. Mapped end to end: boot and the ctor sweep, the main loop and IRQs, the three input buses, the tick object and tempo, the swing table, the 8-way mode TBH and its builders, play_time_step, the step layout (8 voices × 2 bytes), the rest/tie emission gate, the CC21 mapping, and the full panel occupancy. For “clone a mode and change what it emits,” that is most of what matters.

As a map for adding one arp mode: perhaps two thirds of the way there. The remaining gaps are a reliable step-gating model and a free gesture to switch the mode. As a complete reading of the firmware: a small fraction, and deliberately so.

Deliberately out of scope

The bootloader (0x08000000–0x08003FFF), the USB stack body, and the ST HAL were left alone for safety. That is about a fifth of the image unexamined by design. It caps what “full” can mean.

Still open

  • The trigger that commits a RAM slot to flash
  • Which GPIOD pin on 0x20004f00 is the sync jack and which is a DIP (Arturia’s docs say the rear DIP selects clock source and needs a reboot; the firmware object we have is the jack path)
  • USB vs DIN identity on 0x20001d60 (DIN slot is confirmed; USB is a separate site)
  • The family selector in param_field_dispatch
  • About a dozen ctor objects that exist but have no role
  • Case 5 of seq_step_store
  • The hold-length-clear gesture (Shift+Oct−+Oct+)
  • Named handlers for Shift secondaries — every Shift+key combo is taken, so a spare gesture for a new mode has to come from these
  • Scans AT–BA: on disk, not modeled

The Shift-handler gap matters most if the goal is a new mode toggle.

The Euclidean hear-tests are a caution on the tick model. The same flashed e0b image gave a steady 1,2,2, then ~90% every-step, then 3-in-8. Hearing Walk vs Pattern while looking at the wrong TBH case (panel CC21 is 1-based; internal is minus one) explains some of that. Results that change on one image also suggest a second entry into play_time_step (EXTI0 → 0x08011ff0) that is not pinned down. Get that reproducible before trusting the tick model for a chord generator.

Related work

These are host-side or family notes, not another 1.1.6 address catalog.

What the application looks like

STM32F1. Image base 0x08000000. App Thumb 0x08004000–0x0801F400. Addresses in the catalog are flash VAs on the stripped extract, not file offsets into a framed .led.

Boot. Reset_Handler copies .data, zeros .bss through 0x20005eac, then a ctor sweep builds on the order of fifty objects (knobs, tick, ports, debounce, sequence blocks).

Loop and IRQs. One forever loop. SysTick, TIM2, USART1 (DIN MIDI), a USB ISR thunk, and EXTI0 (GPIOD pulse into the step/play path). TIM2 does not itself walk the tick object.

Three input buses.

  • Keys go through 0x0801b750.
  • Buttons use a compact ID table (Hold/Shift/Oct−/Oct+/Tap/Rec/Stop/Play, else Chord). Shift held is RAM 0x200010d2.
  • Analog kinds 1–4 map to Type / Notes / Vel / Strum (0x62–0x65); kind 0 and ≥5 map to Rate (0x66).

Time. Tick object 0x20002bec: step at +0x38, length at +0x10, tempo halfword at +0xe (clamped 3000–24000, then TIM2 ARR). Time Div is a byte on 0x20001000. Swing values are a nine-byte flash table at 0x0801ec64; the panel write lands at sequence-slot +0x401.

Voice out. A step plays only if voice-0 pitch is not a rest/tie (0x81/0x82) — that check is the emission gate, not seq_step_gate bit 7 (retention). Key notes leave on DIN via USART1. USB MIDI out is a separate call site, not a slot on the DIN/display object. Pattern mode is engine +0x10 == 6 (panel CC 21 = 7).

Protocol. App-mode GET is mapped. Host SET is Arturia v2 verb 02 — the same 02 00 41 <id> <value> shape as the GET reply. There is no sibling SET TBB in the app.

Persist. Slot flash can be loaded into RAM. The STM32 FLASH_KEYR unlock sequence is identified. The application trigger that commits a slot is not.

Clock / sync. A four-pin GPIOD object (0x20004f00) is read through IDR bit tests and can gate TIM2 disable and MIDI-Start. Arturia’s docs say the rear DIP selects Internal / USB / MIDI / Sync In (reboot after a change). Which of the four pins is the jack vs a DIP is still a hypothesis.

Panel occupancy. Shift+keys 1–16 are Keyboard MIDI channel, not free. Shift+Tap is tempo. Chord-then-Rec lights Rec. The map is stock-shift-map.md.

Ghidra names for confirmed function starts only: firmware-re/ghidra/recreate.py.

What’s in the tree

firmware-re/notes/address-catalog.md   accepted addresses
firmware-re/notes/scans/               raw Capstone dumps (A–BA)
firmware-re/notes/model/               narratives + proposed rows (A–AS)
firmware-re/notes/stock-shift-map.md   panel occupancy
firmware-re/ghidra/recreate.py         Ghidra name script
firmware-re/scripts/scan_firmware.py   TBB / bl-to / cmp-imm
firmware-re/scripts/led_codec.py       .led parse / extract / retarget
firmware-re/scripts/emulate_ks37.py    Unicorn harness
firmware-re/captures/                  occupancy + listen logs
docs/                                  method, safety, lab setup
midi_control_centre_analysis/          notes on MCC (no vendor exe)

Scripts under scripts/ and firmware-re/scripts/ are the lab wrappers (scan, listen, flash-from-Windows, occupancy). They expect a local extract and a device. Default flash is dry-run.

firmware-re/patches/ is leftover source from an earlier Euclidean / scale-chord experiment. It is not a supported build in this repo, and the .led images are not checked in.

If you have the extract

# image: firmware-re/firmware-images/keystep37_1.1.6.579_flash.bin
# file offset 0 = 0x08000000

python3 firmware-re/scripts/scan_firmware.py

Ghidra: import that bin as ARM:LE:32:Cortex at 0x08000000, then run recreate.py as a post-script.

.led packaging (checksums, extract, retarget) is firmware-re/scripts/led_codec.py. Always retarget after you edit a framed image.

Safety

This repo does not ship a firmware image. If you work on a device anyway: do not write the bootloader, the app vector table at 0x08004000, the USB/MIDI stack, or sequence-slot flash at 0x0803B000. Keep an untouched stock .led for recovery. Details: docs/firmware-safety-rules.md.

Daniel Gruss reverse-engineered the original KeyStep. Confirmed here: hex-encoded .led, unsigned app, SysEx update, separate bootloader. KeyStep 37 checksums are the Huaxin/midiplus segment scheme, not a 20-byte flat header. The two binaries do not overlap.

License

Notes, scripts, and other files in this repository are MIT (Copyright 2026 Jim Curlis). Keep the copyright and permission notice with copies.

That license covers this work only. It does not grant rights in Arturia’s firmware, trademarks, manuals, MIDI Control Center, or any .led / flash extract you obtain yourself. Those stay with their owners and are not published here.

Questions

A few things (factory / service-mode details among them) are kept out of these public notes on purpose. If you’re working on something similar and want more, DM me.