Files
uhryniuk f830f8937d Log real-world road test results and correct/extend the CO-trim theory
New research/ROAD_TEST_LOG.md: the 2026-09-15 flash of
20262-arrow-delete-lowthrottle-composed.hex was a major real-world
success (stalling fully resolved, decel backfire much reduced but not
eliminated). Captures the live diagnostic reasoning that followed:
correcting an overly-lean-leaning framing mid-session (residual decel pop
is more likely a rich-unburned-mixture-with-no-SAI-assist problem, not a
lean one), a structural hypothesis that the Idle Fuel Trim (CO) curve
likely applies whenever the throttle is closed at any RPM rather than
only at a literal stop (only 2 of its 32 points are currently corrected,
and the untouched middle range lines up with the residual pop's RPM
window), and the reasoning for rejecting airbox-baffle removal as a fix
for decel popping specifically (wrong mechanism -- closed throttle isn't
airbox-limited -- and risks worsening the separately-flagged, still
unaddressed main-VE-table leanness gap).

Also updates TABLES.md with the CO-trim applicability open question, and
brings TUNING_GUIDE.md's "local research assets" section up to date (it
still said maps were gitignored/unflashable, both stale).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FP2GaxS9HkUdL5sLBnjKje
2026-09-16 09:07:35 -05:00

9.1 KiB
Raw Permalink Blame History

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[<field>]

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.

Open question, raised from real-world testing (2026-09-15/16, see ../ROAD_TEST_LOG.md): does "idle" in this table's name mean literal engine-stopped idle, or does it apply whenever the throttle is closed/near-closed regardless of RPM? TuneECU's own docs say "at idle," but the table has real, non-trivial entries all the way out to 10,000 RPM — hard to explain if it only ever applied near a stop. Circumstantial support for "applies whenever throttle is closed": a real-world decel sweep through the uncorrected middle of this curve (RPM 2600-6500, composed only touched points 2400 and 7000) produced residual popping in exactly that RPM range, while the low-throttle table fix (a genuinely different, confirmed-idle-adjacent table) independently and completely fixed literal stopped-idle stalling. That's consistent with this curve being live during the whole closed-throttle deceleration, not just at a stop — but it's inference from one road test, not confirmed in the decompile. Worth designing a deliberate test for if this gets revisited: does the CO Trim value visibly change on TuneECU's live-diagnostics screen while decelerating at 4000 RPM with the throttle shut, or only once RPM actually reaches idle?

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.