## 5. EEPROM memory map (8-bit address, 0x00–0xFF) The on-board serial EEPROM has 256 bytes addressed `0x00–0xFF`. The ECU's internal mirror after boot covers a sub-range only (`FUN_7568` loads EEPROM 0x40–0x8B into RAM 0x400–0x44B); the upper area is read on demand. Observed structure (from cross-dump comparison + disassembly): | EEPROM range | Purpose | Notes | |---|---|---| | 0x00–0x3F | Pump cal record A — operating parameters | Read at boot via `FUN_4A79` (record-based, 8-byte chunks). Content is largely zero in current dumps. Erasable via cmd 0x05. | | 0x40–0x8B | Pump cal record B — primary | Block-loaded into RAM 0x400–0x44B at boot by `FUN_7568`. Contains: temp offset (0x42), dphi seed (0x44) + checksum (0x45), accel-comp seed (0x48), angle-table seed (0x4C), CKP loop seeds (0x50–0x56), redundant copies (0x79–0x7A), serial number ASCII (0x80–0x88) | | 0x8C–0xBF | Pump cal record C — extension | Not loaded into RAM at boot. Readable only via K-line cmd 0x19 after Zone 3 unlock. **Per user observation 2026-05-07: present in physical EEPROM, never consumed by running ECU code.** Likely OEM/factory metadata. | | 0xC0–0xCF | Reserved / unused | Outside any zone bounds in observed configs | | 0xD0 | Lockout backoff counter | Written by `FUN_29D4` failure path, read at boot by `FUN_3AD1`. Persists failed-auth state across power cycles. | | 0xD7–0xDF | Zone 0 seed (9 bytes) | Read into RAM 0xB7+ as part of cmd 0x18 success response | | 0xE1–0xE9 | Zone 1 seed (9 bytes) | Same | | 0xEA–0xF2 | Zone 2 seed / Zone 8 magic seed (9 bytes) | Same. Shared between Zone 2 and Zone 8. | | 0xF3–0xFB | Zone 3 seed (9 bytes) | Same | | 0xFC | Zone 0 use counter | Incremented on each cmd 0x18 + RB0=0x10 success | | 0xFD | Zone 1 use counter | Same | | 0xFE | Zone 2 / Zone 8 use counter | **Magic-zone access leaves this trace** | | 0xFF | Zone 3 use counter | Same | The "cal record" boundaries (A/B/C) are inferred from access patterns; the EEPROM does not have explicit headers separating them. --- ## 6. Security architecture ### 6.1 Zone descriptor table A 4-entry × 12-byte table at **ROM 0x5FA0** drives zone authentication. Each entry has: | Offset | Field | Type | Purpose | |---|---|---|---| | +0 | `alt_key` | u16-LE | Alternative key (used by cmd 0x18 with RB0 ≠ 0x10) | | +2 | `exp_key` | u16-LE | Primary expected key | | +4 | `zone_start` | u16-LE | First EEPROM address the zone covers | | +6 | `zone_end` | u16-LE | Last EEPROM address (inclusive) | | +8 | `flag_byte` | u8 | RE6/RE7 bit pattern set on success | | +9 | `seed_off` | u8 | EEPROM offset of the 9-byte seed read on success | | +10 | `cnt_off` | u8 | EEPROM offset of the per-zone use counter | | +11 | reserved | u8 | usually 0xFF (unused) | A **fifth implicit "magic zone 8"** is hard-coded in `FUN_29D4`: - Key: **0x4453** (ASCII "DS") - Range: 0x0000–0xFFFE (effectively the whole 64KB MCU address space) - On success: `RE7 = 0xFF` (every gate-bit set, including the cmd 0x1A write gate) - Seed/counter: shares Zone 2's offsets (0xEA / 0xFE) ### 6.3 Authentication flow (`FUN_29D4`) Tester side: 1. **Build cmd 0x18 block** with: - `RB3` = 0x03 (request length / sub-count) - `RB4` = zone selector (0, 1, 2, 3, or 8) - `RB5` = key high byte - `RB6` = key low byte - `RB7` = any non-zero byte (acts as continuation flag — without it, the ECU's `RWC4 = 0` reset short-circuits the success path) - `RB0` = 0x10 if you also want write access enabled (sets the upper-nibble flag bits AND increments the use counter) - Otherwise `RB0` = block length (4 + data bytes) 2. **TX block, observe response.** ECU returns success block (`RB2 = 0xF0`, payload = the 9-byte seed + zone bounds) or error (`RB2` = 0xE5/error pattern). 3. **On success**, internal flags are set: - Zone 0 → `RE6 |= 0x01` (read) or `|= 0x11` if RB0=0x10 (read+write) - Zone 1 → `RE6 |= 0x02` or `|= 0x22` - Zone 2 → `RE6 |= 0x04` or `|= 0x44` - Zone 3 → `RE7 |= 0x08` - Zone 8 → `RE7 = 0xFF` (and cmd 0x1A becomes available) 4. The flags persist for the rest of the K-line session; they reset on session end / power cycle. ECU-side details an implementer should be aware of: - The 9-byte seed read at `EEPROM[seed_off..seed_off+8]` is **echoed back to the tester in the response**. It does not gate access — pure transport, presumably so the tool can fingerprint the unit. - `RE8` (lockout state) must be 0 to attempt auth. If non-zero (set by previous failed attempts), the ECU silently fails until the lockout timer (`FUN_38BE`) decrements RE8 to 0. - Wildcard match: if `exp_key == 0xFFFF` in any zone descriptor, ANY tester key is accepted. **None of the observed dumps use this**, but a future ROM variant might. - Sentinel: if `exp_key == 0x5555`, the zone is permanently disabled (cannot be unlocked). None observed yet either. ### 6.4 Lockout mechanism Implemented in `FUN_29D4` failure path + `FUN_38BE` timer. ``` fail #1 -> RE9 = 1, RE8 = 1, stored to EEPROM 0xD0 fail #2 -> RE9 = 2, RE8 = 2 fail #3 -> RE9 = 4 ... fail #8 -> RE9 = 128 fail #9+ -> RE9 = 240 (saturated) ``` `RE8` decrements once per ~30000 ticks (active session) or ~120000 ticks (idle). Each decrement re-writes EEPROM 0xD0 — the lockout therefore **persists across power cycles**. A wedged unit can require minutes to hours of waiting to retry. **Recommendation for the reader software:** read EEPROM 0xD0 before any auth attempt (via cmd 0x19 after Zone 3 unlock — the only safe path that doesn't risk further lockout). ## 7. Read recipes ### 7.1 Recipe A — public read of 0x00–0xBF (Zone 3) This is the safest operation: it requires a static key (0x00FF) that is identical across every variant we have dumps for, has no destructive side-effects, does not increment the magic-zone counter, and covers the full "general data" portion of the EEPROM. ``` # Step 1 — unlock Zone 3 TX: [ 06 NN 18 03 03 00 FF FF 03 ] len=06, seq=NN, cmd=0x18, RB3=03, RB4=03 (Zone 3), RB5=00, RB6=FF (key 0x00FF), RB7=FF (continuation flag), end=03 ECU response (success): RB2 = 0xF0, plus 9-byte seed @ 0xF3..0xFB. ECU now has RE7 |= 0x08. # Step 2 — read EEPROM in 13-byte chunks for offset in 0x00, 0x0D, 0x1A, 0x27, 0x34, 0x41, 0x4E, 0x5B, 0x68, 0x75, 0x82, 0x8F, 0x9C, 0xA9, 0xB6: TX: [ 04 NN 19 0D 00 offset 03 ] cmd=0x19 (read EEPROM), RB3=0x0D (13 bytes), RB5=offset RX: 13 bytes from EEPROM[offset..offset+12] in the response payload # 15 chunks * 13 bytes = 195 bytes, covering 0x00..0xC2 # Trim or adjust the last chunk's RB3 to 0x0A so it stops at 0xBF inclusive. ``` ### 7.2 Recipe B — full read of 0x00–0xFF (Zone 0) Reads bytes that Zone 3 cannot see (0xC0–0xFF — counters, seeds, lockout). Requires the OEM key 0x00A6. ``` # Step 1 — unlock Zone 0 TX: [ 06 NN 18 03 00 00 A6 01 03 ] RB4=00 (Zone 0), RB5=00, RB6=A6, RB7=01 # Step 2 — read EEPROM via cmd 0x03 (per-zone path) for offset in 0x00, 0x0A, 0x14, ..., 0xF6: TX: [ 06 NN 03 0A 00 offset 03 ] cmd=0x03, RB3=0x0A (10 bytes), RB4:RB5 = 0x00:offset (16-bit address) # 26 chunks * 10 bytes = 260 bytes, slightly over-reads; # adjust the last chunk's RB3 to stop at 0xFF. ``` ### 7.3 Recipe C — magic full access (Zone 8) — read AND write ``` # Step 1 — unlock Zone 8 with the master key TX: [ 06 NN 18 03 08 44 53 01 03 ] RB4=08 (magic zone), RB5=44, RB6=53 (key "DS" = 0x4453), RB7=01 # After: RE7 = 0xFF (every gate-bit set). # All read commands work; cmd 0x1A is also available for writing. # WARNING: this access path increments EEPROM[0xFE] (the use counter). # The increment is non-destructive but forensically observable — service # tools at the OEM can read 0xFE to see how many times Zone 8 was accessed. ``` ### 7.4 Recipe D — fast-path RAM-mirror dump (no auth) For EEPROM 0x40–0x8B specifically, you don't need to talk to the EEPROM driver at all — the data is mirrored into RAM 0x400–0x44B at boot and accessible via cmd 0x01 (read RAM, no auth required). ``` for offset in 0x400, 0x40D, 0x41A, ..., 0x444: TX: [ 04 NN 01 0D high_addr low_addr 03 ] cmd=0x01 (read RAM/ROM), RB3=0x0D, RB4=high, RB5=low # 6 chunks covering 0x400..0x44B = exact mirror of EEPROM 0x40..0x8B. ``` This recipe is ideal for monitoring tools that want to observe the live EEPROM cache without authentication overhead. --- ## 8. Write recipes — DANGER ZONE Writes require either Zone 0/1/2 unlock with `RB0 = 0x10` (cmd 0x0C path) or Zone 8 (cmd 0x1A path). ### 8.1 Cmd 0x0C — write via OEM zone (with RB0 = 0x10 unlock) ``` # Step 1 — unlock Zone 0 with write enabled TX: [ 06 NN 18 03 00 00 A6 01 03 ] # NOTE: RB0 must be 0x10, not 0x06, # to set the read+write flag pattern # (RC8 << 4 | RC8) and increment 0xFC. ``` (Implementer task: verify the RB0=0x10 vs RB0=0x06 distinction by experiment — it's set in `FUN_29D4 @ 0x29D4` based on `RB0 == 0x10` test, but the call path that actually populates `RB0` differs between the dispatcher's path-A and path-B.) ``` # Step 2 — cmd 0x0C write with verify TX: [ 0E NN 0C NN 00 offset b1 b2 b3 b4 b5 b6 b7 b8 b9 b10 03 ] cmd=0x0C, RB3=count<11, RB4:RB5 = address, RB6+ = bytes to write ``` ### 8.2 Cmd 0x1A — write via Zone 8 (after master unlock) After Zone 8 is unlocked (RE7 = 0xFF), cmd 0x1A becomes available: ``` TX: [ ... NN 1A count rb4 offset b1..bN 03 ] cmd=0x1A, RB3=count<11, RB5=EEPROM offset (8-bit), data bytes from RAM 0xB6+ ``` Each byte is written and immediately read-back-verified internally by `FUN_22EA`. On verify failure, the response indicates failure and the partial write is left in place. ---