# Keihin SH7054 (Triumph 865 twin, mechanical odo) — table map Reversed from `Nc()`/`Lb()` in `l.java` and validated against the stock reference maps. All offsets are in the reconstructed **flat ROM** (0x60000). Compute per map: ``` fe = c.a[Qd*48] # Qd from the s.a directory lookup on the map signature base = (fe[0] & 0x2F0) << 12 # = 0x50000 for the 865 twin family offset(table) = base + fe[] ``` Main tables are **32 rows (RPM) × 20 cols (throttle), 16-bit big-endian**. ## Axes | Axis | fe field | Entries | Values (20187) | Meaning | |---|---|---|---|---| | RPM (rows) | `fe[8]` | 32 | 0, 500, 900, 1000 … 10000 | engine speed, direct RPM | | Throttle (cols) | `fe[27]` | 20 | 0, 10, 20 … 780, 1000 | throttle %, ÷10 (0–100.0%) | ## Tables (validated by production 20187 vs aftermarket 20188 diff) | Table | fe | Offset (20187) | Value range | Notes | |---|---|---|---|---| | Main fuel — cyl 1 | 11 | 0x56990 | 951–10480 | **strong**: 75% diff, smooth VE, aftermarket richer | | Main fuel — cyl 2 | 12 | 0x56E90 | 951–10680 | **strong**: 67% diff | | Low-throttle fuel — cyl 1 | 15 | 0x55590 | 0–10760 | 70% diff; starts at 0 (closed throttle) | | Low-throttle fuel — cyl 2 | 16 | 0x55A90 | 0–10810 | 68% diff; cyl-1 pair | | Fuel — base/idle | 9 | 0x55500 | 0–10760 | 61% diff; lower values | | Ignition advance — gear 1 | 19 | 0x587D0 | 13–600 | 4 evenly-spaced (0x500) 20×32 tables | | Ignition advance — gears 2–5 | 20 | 0x58CD0 | 13–600 | main operating map | | Ignition advance — gear 6 | 21 | 0x591D0 | 13–600 | overdrive | | Ignition advance — neutral | 22 | 0x596D0 | 60–600 | idle/free-rev | ## AFR / target lambda `fe[2]` @ 0x52260 is an interleaved (value, breakpoint) curve where **128 = λ1.00 (stoich, 14.7 AFR)** — e.g. `… 128, 500, 128, 400, 128, 300 …`. The `128` cells are the closed-loop lambda targets; disabling closed loop (O2 delete) lets you set these richer. Exact dimensions still being confirmed. ## Scaling (confidence varies) - **Fuel** values are VE/fuel-mass in the ECU's internal units (≈ 0–10800). Real units (injector µs / VE %) need the ECU's fuel constant; relative changes are exact, absolute scaling TBD. - **Ignition** 13–600 is advance in an internal unit (candidate: value ÷ 10 = °BTDC, giving ~1.3–60°; the low-RPM row 90→14→20 fits a retard-then-advance curve). TBD. - **AFR** 128 = λ1.00 is solid (standard Keihin convention). ## Fuel/ignition trim array — RPM-indexed, 32 entries, `0x53690`-`0x536AF` **Located and axis-mapped (Aug 2026).** A 32-byte array sitting just before the device-flags region (`0x53801`, `TABLES.md` above / `DEVICES.md`), one byte per **RPM breakpoint on the same 32-point RPM axis** as the main tables (`fe[8]`: 0, 500, 900, 1000, ..., 10000). Not addressed via a `fe[]` pointer for the maps checked (no `fe` field in `c.a[Qd*48]` equals `0x3690` for `20187`'s `Qd=32`) — likely a fixed offset relative to the device-flags struct rather than an independently relocatable table, unconfirmed. Evidence for the RPM-axis mapping: diffing `20188Map2009AIRBOXBONNY.hex` (the Bonneville SAI/O2 delete map) against stock `20188` shows **exactly 2 of the 32 cells changed — at RPM=2400 and RPM=7000** — with all 30 other RPM-indexed cells byte-identical. That's `0x5369C` (index 12, RPM 2400) and `0x536AB` (index 27, RPM 7000) from earlier sessions, now understood as two specific points on this curve rather than free-floating bytes. Cross-checked against `20184dynoTuneSteveO2-Disable.hex` (America, open-pipe + airbox-snorkel-removed + O2-disable, real dyno tune) vs. stock `20186`: **nearly every one of the 32 cells changed**, by large, non-monotonic swings (e.g. RPM=1000: 231→63, RPM=4400: 235→5, RPM=5200: 25→204). Not a smooth curve shift — looks like cell-by-cell dyno-session adjustment (consistent with a human tuner nudging individual RPM points after successive pulls) rather than a single global correction. Confirms this is a real, independently-tunable-per-cell array, not a scalar. **Identity: confirmed as "Idle Fuel Trim (CO)".** Crawled tuneecu.net's full docs site (`../tuneecu_site_mirror/`) and found the exact match in `TuneECU_En/tests.html`, TuneECU's live-diagnostics parameter list: > "Idle Fuel Trim (CO): lets you adjust the fuel richness at idle." > Available for "**Triumph without O²-Sensor only**." > (Counterpart: "Long Term Fuel Trim... Triumph models **with** O²-Sensor > only" — the closed-loop equivalent for bikes that still have the sensor.) This is a direct, official match to what we observed empirically: the table is specifically the O2-delete-relevant trim, exactly why it's what the real delete/dyno maps touch and the stock maps don't need to. `co_trim`/`ift_co` string resources (`strings.xml`: "CO Trim" / "Idle Fuel Trim (CO)") back this — "CO" = idle mixture richness, the classic idle-adjustment convention carried over from carbureted-era tuning terminology. **Encoding, upgraded confidence (Aug 2026):** found TuneECU's actual "CO Trim" edit dialog (`MainActivity.java`'s `La()`) and its value-scaling logic. Two independent code paths converge on the same answer: 1. The dialog's clamp/display logic (`j8()`) treats the value as a plain signed range: `if (v < -128) v = -128; else if (v > 127) v = 127;`, displayed as `Integer.toString(v)` — no fractional scaling. 2. A separate live-value dispatcher (`n.java`, a large switch handling various PID-style codes) has a case doing manual two's-complement conversion — `if (raw > 127) raw -= 256;` — immediately before storing into the same field (`MainActivity.Mc`) the CO Trim dialog reads. **Conclusion: the raw byte is signed two's-complement (-128 to 127), used directly with no multiply/divide scaling factor** — not the unsigned 0-255 interpretation this project used everywhere earlier. **This retroactively resolves the "255 looks like a sentinel" puzzle** from the exhaust-family composability analysis (`COMMUNITY_TUNING.md`/`PERMUTATIONS.md`): unsigned `255` is simply signed `-1` — an entirely ordinary, near-neutral value, not a special case. Re-reading the earlier data with correct signs: the stock aftermarket-silencer family sits near `-1` (neutral), production/Arrow sits around `+69` to `+125` (already noticeably rich), and the real SAI/O2 delete map pushes to `+117` — a large, deliberate enrichment, exactly the physically-sensible story this parameter's name would predict. The composability conclusion (can't cleanly merge Arrow's delta with the delete map's delta) still holds — redone with correct signs, the combined correction still saturates past the `+127` clamp — but now for an intuitive reason (the needed correction is genuinely large) instead of an unexplained sentinel. **Not fully closed:** the *encoding* (signed int8, no scaling) is now well-supported by two independent sites; the exact real-world *unit* (is `+1` precisely "+1% CO", or a nearby-but-not-identical ECU-internal unit) isn't separately confirmed by either site. Raised from low to medium-high confidence — upgraded, not fully resolved. **Practical implication for composing tunes:** don't treat this array as "2 conflicting bytes" the way `compose_arrow_delete.py` originally did — it's a full 32-cell curve. A conservative mod (mufflers only) may touch 2 cells; a more aggressive one (open pipes + airbox) touches nearly all 32. Composing two independent tunes' curves here needs the same per-cell arithmetic treatment as the main VE tables (`PERMUTATIONS.md`), not a handful of spot fixes. ## SAI / O2 / lambda flags Now findable via the localised flat-ROM diff (20187 vs 20188), which isolates small single-byte changes (e.g. 0x50C13, 0x534AD, 0x59759) as prime toggle candidates. Confirming which is SAI vs O2 needs either bench testing or the finer `Nc` device logic — a follow-up. ## Status Fuel and ignition table **locations** are validated end-to-end. Remaining: exact scaling constants, AFR dimensions, and confirming the individual device flags.