Bundles several feature streams that have been iterating on the working tree: - Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the identification card, manual KWP write + transaction log, ROM/EEPROM dump card with progress banner and completion message, persisted custom-commands library, persisted EEPROM passwords library. New service primitives: IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync. Persistence mirrors the Clients XML pattern in two new files (custom_commands.xml, eeprom_passwords.xml). - Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and progress dialog VM, gated on dashboard alarms. - BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at 0x0106 via IKwpService.ReadBipStatusAsync; status definitions in BipStatusDefinition. - Tests page redesign: TestSectionCard + PhaseTileView replacing the old TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/ TestSectionView controls and their VMs. - Pump command sliders: Fluent thick-track style with overhang thumb, click-anywhere-and-drag, mouse-wheel adjustment. - Window startup: app.manifest declares PerMonitorV2 DPI awareness, MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and maximizes there (after the hook is in place) so the app fits the work area exactly on any display configuration. - Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and dump-functions reference docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
9.6 KiB
Markdown
207 lines
9.6 KiB
Markdown
## 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.
|
||
|
||
--- |