113 lines
13 KiB
Markdown
113 lines
13 KiB
Markdown
# Lofi-12 XT — Custom Firmware Tweakability Report
|
||
|
||
Based on: `Lofi-12 XT.bin` v1.1.156 (1,595,605 B), v1.2.179 (1,606,197 B),
|
||
v1.5.205 (1,707,049 B); structural notes in `*/reversed.md`; cross-version
|
||
diff in `rev-diff.md`. All three images parse cleanly with the same tooling.
|
||
|
||
Date: 2026-09-26, updated 2026-09-30. Hardware dumped since (IC300/IC301 via
|
||
CH341a); checksum solved; Ghidra lab built. Still static-only — no modded image
|
||
has been boot-tested on-device yet.
|
||
|
||
## TL;DR
|
||
|
||
| Question | Answer |
|
||
|---|---|
|
||
| Feasible to build a modded firmware? | **Yes, plausibly — but only up to a ceiling.** Container is fully parsed, code is identified (TI C674x DSP, unencrypted/unpacked), data sect is rich with symbols. Three versions give us a feature-diff oracle. |
|
||
| #1 blocker | **Solved 2026-09-30:** `cmtd+0x08` = CRC32-IEEE init 0 over the image with bytes `[8:12]` replaced by `0xC27C6282` (proven 3/3; `tools/` stamps it automatically). |
|
||
| Risk of bricking? | **Low if careful.** The `.bin` is a DDR snapshot loaded by the separate SPI-flash bootloader (MX25L12833F), and the updater lives in that bootloader - a bad image aborts `SYSTEM UPDATE` without killing recovery. Confirmed recovery: hold PAD while powering on with a stock SD. |
|
||
| How far can we go? | **Text/UI swaps, asset swaps, constant tuning, small code patches: realistic. New DSP effects, new file formats, USB/stack changes: out of reach without a major RE campaign.** |
|
||
| Recommended first mod | Same-length UI string swap on v1.5.205 (`Threshold` -> `ThresholX`), forged image already built and self-verified (`462F735B`) - awaiting the first on-device boot-test. |
|
||
|
||
## 1. What we actually have (and why it matters)
|
||
|
||
1. **Complete container format.** `[cmtd 48B][mmtd 36B][sect ×6–7 → EOF, exact fit]` verified on all three builds (chain math `next_off = off + 12 + len` tiles EOF exactly). Repacking is therefore a byte-shuffling problem, not a guessing problem. Section roles are stable across releases: asset blob (~85 kB) → float tables → 512 B record table → big code sect (1.39→1.49 MB) → 8 B zero slot (comes and goes) → strings/rodata (115→125 kB) → float ramp tail.
|
||
2. **Code CPU identified: TI C674x DSP (C6000 family), little-endian.** All three entry points disassemble under C64x-LE to the same reset prelude (`ZERO b0; mvc b0,ier; mvc b0,csr; …` — interrupt disable + stack align); only stack immediates differ. ARM/Thumb disassembly of the same bytes yields ~nothing. The `.bin` load addresses are all `0xC2xxxxxx` = DDR2 (SoC is TMS320C6748: ARM9 + C674x, 1 Gbit DDR2 at `0xC0000000`). So: DSP does the application; ARM9 is presumably bootloader-only in SPI flash.
|
||
3. **No packing, no encryption, no obfuscation.** Big-sect entropy is constant 6.887–6.890 with ~12.6% zeros across all builds; code/data mix with a low-entropy zero-padded tail. Nothing suggests compression or crypto was introduced between v1.1 and v1.5.
|
||
4. **Symbol-rich rodata.** The strings sect contains full C++ RTTI/mangled names (libc++ `NSt3__`, i.e. TI clang-based CGT toolchain): `FileUtil::…`, `App*Menu`, `N3HAL*Driver`, `N8SoundOSC…`, plus UI strings, RIFF/WAVE/MIDI tags, project chunk tags (`PJST/PTDT/SONG/…`), and a full C674x crash dumper (`Legacy NMI Exception`, `A0–A31/B0–B31`, `NTSR/ITSR/…`). Code↔data cross-refs grow monotonically (5,854→5,909→6,362 code-ptrs; 2,932→2,967→3,141 self-ptrs) — i.e. vtables/function tables live in the strings sect and can anchor disassembly.
|
||
5. **Three-point version oracle.** v1.1→v1.2 is a +10 kB small delta (only user-visible string adds: `TRACK MUTE`, `AppPad::handleKeyPadOnSliceMode`); v1.2→v1.5 is a +91 kB big delta (audio export family, MIDI Note Map, 4 new master FX `MIsolator/MRackComp/SlipRoll/HoldDelay`, `REVERSE`, precount rework with `EibiEUliE` overloads, `FileUtil::removeResourceFork` + `Rb` params, `Fv→Fvi/Fvii/Fviii` callback migration, display-pipeline symbol turnover). This tells us exactly which subsystems are self-contained enough to backport/forward-port.
|
||
|
||
## 2. Risk assessment
|
||
|
||
| Risk | Level | Notes |
|
||
|---|---|---|
|
||
| Permanent brick (bootloader kill) | **Low** | Bootloader lives in SPI flash (MX25L12833F, dumped + mapped — see `docs/bootloader.md`), update `.bin` is a DDR image parsed by it. A corrupt `.bin` aborts `SYSTEM UPDATE`; recovery is the PAD-held boot with stock SD. |
|
||
| Soft-brick / failed boot loop | **Medium** | Wrong checksum, bad entry point, or misaligned sect will likely hang or drop back to updater. Mitigation: keep a known-good SD card with stock v1.5.205, document the hold-PAD-while-powering-on update flow, never ship without a revert image. |
|
||
| Silent data corruption (projects/SD) | **Medium** | FileUtil rework in v1.5 touches recursive FS ops + resource forks. Patches near file I/O deserve extra caution and SD-card backups. |
|
||
| Checksum | **Solved, no longer a risk** | `cmtd+0x08` reproduced on all 3 builds; `mmtd+0x38` flags need no handling (covered as-is). |
|
||
| Legal / warranty | **Medium** | Distributing full modded images contains Sonicware IP. Prefer distributing patches (xdelta/bps + script) against user-supplied stock `.bin`. Warranty implications unknown. |
|
||
| No debugger mapped | **Medium** | UART1_TX/RX + SPI + SD test points are named on the silkscreen (see `docs/hardware.md`), continuity untraced. Crash dumper strings suggest NMI output exists — worth hunting on-board before any code patch. |
|
||
|
||
**Bottom line on risk:** string/asset-only mods with a solved checksum are low-risk. Any code-segment edit without a debugger or recovery plan is medium-risk. Touching anything outside the `.bin` (SPI flash, AT32F421 USB MCU) is high-risk and out of scope.
|
||
|
||
## 3. Tweakability ladder (easiest → hardest)
|
||
|
||
### Level 0 — Trivial (days, one person, no DSP expertise)
|
||
- **Same-length UI string swaps.** The strings sect is NUL-separated with padding (e.g. `Threshold\x00\x00\x00\x00ENCODER TEST`). Any replacement of equal byte-length (pad with spaces/NULs) keeps every address stable — no pointer fixups, no checksum-of-lengths to worry about (only the one global checksum). Ideal for renaming menu items, translating UI, joke builds, ownership marks.
|
||
- **Asset/blob swaps.** Sect #0 (~85 kB, `7e xx` patterned bitmap/font/waveform data) can be recolored or redrawn in place as long as length is preserved. Fonts/icons are the likely content.
|
||
- **Version-string spoofing.** `cmtd`/`mmtd` version triples are plain LE u32s — trivial to bump for fork identification (checksum auto-recomputed, flags preserved as-is).
|
||
|
||
### Level 1 — Easy (slack-space + constants, basic C6000 asm)
|
||
- **Longer/shorter strings via slack.** Top zero-runs in the v1.5 strings sect include 388, 256, 223, and many 196-byte gaps — enough to relocate a handful of lengthened strings and patch their pointers. Requires finding xrefs (the 6,362 code-ptrs give a starting map) but no code-cave engineering.
|
||
- **Numeric constant tuning.** Float tables (sects #1/#6) and the float ramp tail are plain LE float32 — filter/window coeffs, tempo/level defaults, threshold values. Tunable by hex-edit + listen. Same for the 512 B record table (preset/pattern-shaped fixed records).
|
||
- **Behavior flags / timeouts / limits.** Precount on/off defaults, LED timings, "1,024 files per folder" caps, `ARE YOU SURE?` confirm gates are typically single immediates or branches — findable via string xref → code.
|
||
|
||
### Level 2 — Medium (real code patches, needs Ghidra + C6000 skill)
|
||
- **Feature NOPs / unlocks.** Skipping a confirm dialog, forcing precount off, enabling hidden menus: overwrite a branch with NOP or invert a compare. Needs rebase-aware disassembly (load code sect at its DDR base, e.g. `C2B80C00` for v1.5) and vtable-anchored function boundaries.
|
||
- **Branch-to-cave mini-features.** The 8 B zero slot is *not* usable code space (too small, and it vanishes in v1.2 — it's alignment churn), but the low-entropy zero-padded tail of the code sect (`+0x140000…`, entropy ~3.0) offers code-cave room for small injected routines (extra MIDI mapping, custom pad behavior) reached by patching a call site.
|
||
- **Backports between versions.** Because v1.1→v1.2 is tiny, diffing those two isolates e.g. `TRACK MUTE` / slice-mode handling almost cleanly — the most realistic "port feature X to version Y" target. v1.2→v1.5 features are larger families and harder to lift.
|
||
|
||
### Level 3 — Hard (weeks–months, DSP + toolchain mastery)
|
||
- **New/changed DSP behavior** (custom filter, altered timestretch/slice engine, new LFO shape). Requires understanding TI CGT calling conventions, `addkpc`-relative addressing, fixed-vs-float pipelines — plus listening tests per iteration since there is no emulator.
|
||
- **Display-pipeline changes.** v1.5 turned over all `RenderBufferIhLi128ELi128ELi1327E` symbols; the GUI stack is evidently custom and version-fragile.
|
||
- **FileUtil / project-format changes.** `Rb` params, `removeResourceFork`, `Fv→Fvi` callback migration mean v1.5's FS layer differs structurally from v1.1/v1.2 — grafting across that boundary is genuinely hard.
|
||
|
||
### Level 4 — Out of reach (even with current assets)
|
||
- **Custom bootloader / SPI-flash layout changes.** Dumps exist and the AIS
|
||
image is mapped (`docs/bootloader.md`), but reflashing a bad bootloader can
|
||
only be recovered via external programmer — out of scope for SD-only modding.
|
||
- **USB stack (AT32F421 MCU).** Separate chip, separate firmware, not in scope of these images.
|
||
- **New codecs / new file formats / USB audio / major new FX algorithms from scratch.** No source, no SDK, no DSP build chain verified; would amount to writing C674x DSP code blind.
|
||
|
||
## 4. What we can tweak vs. what is beyond reach (concrete list)
|
||
|
||
**Realistic mods:**
|
||
- Rename/translate/reword any menu, dialog, error string (`AUDIO EXPORT`, `ARE YOU SURE?`, `MIXDOWN FILE NAME:`, …).
|
||
- Redraw/replace bitmap assets (fonts, icons, waveform glyphs) in sect #0.
|
||
- Retune float constants (filter coeffs, ramp, default tempo/thresholds) and fixed-record presets.
|
||
- NOP confirm dialogs, change defaults (precount, mute behavior), remap pad/MIDI-note handling, adjust LED update logic (`StepEditPageController::updateAppLED` exists as a named target).
|
||
- Backport small v1.2 behaviors (track/pattern mute, slice-mode pad handling) across versions; cherry-pick single v1.5 strings/behaviors that are data-driven.
|
||
|
||
**Beyond reach (without new inputs):**
|
||
- New master FX on par with `MIsolator/MRackComp/SlipRoll/HoldDelay` (these were ~90 kB of new code — a team-sized effort to replicate blind).
|
||
- Audio export itself as a backport to v1.1/v1.2 (whole class family: `AppAudioExport*`, `DialogAudioExporting`, `MixDown~`).
|
||
- Reliable larger-than-slack additions (no dynamic allocator mapped; memory map beyond the tiled `C2B809E8…C2DBC698` range has gaps marked only as descriptors/BSS).
|
||
- USB behavior, SD-driver changes (`N3HAL*Driver` family is named but unmapped), sample-rate/format support changes.
|
||
|
||
## 5. Toolchain & skills required
|
||
|
||
- **Repacker:** done — `tools/lofi_image.py` parses/packs the exact-fit chain and
|
||
stamps the valid checksum (`compute_checksum`, proven 3/3).
|
||
- **Disassembly:** working lab — Ghidra 12.1.3 + ghidra-c6000 + PyGhidra drivers
|
||
(`ghidra/headless/`), proven on the bootloader CRC routine; `analysis/gen_funcmap.py`
|
||
for vtable-anchored maps; capstone `CS_ARCH_TMS320C64X` for quick preludes;
|
||
rebase-aware diffing via function hashes (raw byte-diff is defeated by rebasing
|
||
`C2589800→C258D800→C2B80C00`).
|
||
- **Patching:** C674x assembly literacy, pointer/xref hunting from the strings sect, xdelta/bps distribution to avoid shipping Sonicware binaries.
|
||
- **Hardware (before any code mod boots):** revert flow is hold-PAD-while-powering-on
|
||
with stock SD; PCB photographed (`docs/photos/`); UART1_TX/RX test points named
|
||
on silkscreen (continuity untraced); SPI dumps banked locally; SD-card backups.
|
||
|
||
## 6. Suggested plan (lowest-risk order)
|
||
|
||
1. ~~Crack `cmtd+0x08`~~ — **done** (CRC32 + `0xC27C6282` seed; `tools/` stamps it).
|
||
2. **Prove the loop with a Level-0 mod.** Same-length string swap on v1.5.205 → boot → revert to stock. If this fails, stop: no higher level is viable.
|
||
3. **Map xrefs for one Level-1 target** (e.g. a confirm dialog or precount default) using string→code pointers; patch, test, revert.
|
||
4. **Only then** attempt Ghidra full-disassembly + v1.1↔v1.2 micro-diff for a first Level-2 backport.
|
||
5. **Do not touch** SPI flash, USB MCU, or FS-write paths until UART/crash logs are captured and a revert is drill-tested.
|
||
|
||
## 7. Verdict
|
||
|
||
- **Feasibility: moderate-to-good for small mods, poor for big features.** The format is open, packing is solved, the code is identified and unprotected, and three versions triangulate features well. The project no longer lives or dies on the checksum — it lives or dies on the first on-device boot test.
|
||
- **Risk: contained if disciplined** (DDR-image-only, stock revert on hand, string-first progression), and uncontained if the bootloader or USB MCU is touched.
|
||
- **Ceiling with current assets:** customized UI/assets, tuned constants, disabled annoyances, small behavior patches, possibly one backported mini-feature. New DSP engines, new I/O, and custom bootloaders remain out of reach.
|