Code-level analysis of the stock dump. About 53k instructions were decoded with a custom C166 disassembler: 96 standard task entry points plus 78 additional ones, 114 map references and 92 calibration maps. Every map is tied to the function that reads it and to the variables on its axes. Below: how the firmware is organised, the calculation chains (fuel, boost, EGR, injection timing) and the full map list.
These points matter for Stage 1. Each one was checked against the code that reads the map, not inferred from the shape of the data.
The boost setpoint is clipped by a 2100 mbar constant. After all corrections, task 0x160C4 takes min(setpoint, [0x3C3AA]). Raising the boost map 0x3C496 alone does nothing above 2100 until this constant is raised. Next to it is the deviation check: underboost above +250 mbar or overboost above −150 mbar (parameters 0x3C3B4/0x3C3B6) sets a fault on path 0x1D.
This is the EGR air-mass setpoint, not injection timing. Titanium labels it “Phase of injection”, but the code says otherwise. The map output (C2E4) is read only by task 0x1EBB6, which compares it with the MAF air mass (C216) through a PI controller, inverts the output with a 100 % − x curve and, on deviation, sets path 0x1E, i.e. P0400 (flag 0x3AFEE). The area 0x3A440–0x3A4D2 is not read by any visible code; its purpose is unknown, do not write there.
Not a boost limiter. The map (rpm × fuel, 20.3…33.2) feeds C214 → C252, a value sent over CAN to the VP44 pump together with the fuel quantity. It is the start-of-injection request. Do not treat it as a boost limiter.
Not a fuel limiter. This is the boost valve pilot duty (rpm × fuel, up to 65.00 %). It is added to the pressure PI controller in task 0x1F6B8.
The active limiters are “Injection at part throttle” and “Torque limiter 2”. The copies 0x3B55A (“Injection ext”) and 0x3B8EA (“Torque limiter 1”) are only blended in with a factor from curve 0x3B926, which is all zeros in stock, so these copies have no effect on fuelling. Edit the active pair.
| Addresses | Contents |
|---|---|
| 00000–08AA5 | Filled with C3. At these addresses the CPU sees its internal mask ROM: bootloader, interrupt vectors, task scheduler, crank synchronisation, immobilizer check and library routines. The code makes 833 calls there, to 96 distinct addresses. Map interpolation: 00:20D6 (2D) and 00:20AC (1D). |
| 08AA6–0FFFF | Code. The 0C000–0FFFF range is called through the segment 7 mirror (07:Cxxx) because in segment 0 those addresses hold the C167 XRAM/SFRs. |
| 10000–37FFF | Application code. 55 AA at 10800, entry at 10804. Diagnostic command dispatcher at 17290 (81 jmpa handlers at 1729C). |
| 38000–3FFFF | Calibration, via DPP0=0E and DPP1=0F. Maps, parameters, map pointer tables (38DD0–391D8), fault tables (3AA3E–3B3B8 and 3DA0E–3DE3E), task stack tops (38030–38042). |
All calculations are split into tasks called by the scheduler in ROM. 96 tasks begin with the standard prologue mov r0,[0040h]; sub r0,#frame; mov [r0],[FD86h], i.e. they allocate a frame on their own stack (stack tops at 0x3803C–0x38042). Another 78 entry points start directly with cmp r0,[FD86h]. Tasks exit via jmps 00:07B4, stack overflow goes to 00:0276. The task order table lives in ROM, so call order and rate cannot be recovered from the dump.
A task first copies the globals it needs into its work area (C0FA–C126, FC66–FC6A), computes, and writes the result to one or two globals. Intermediate values are mirrored into monitoring variables DCxx–DExx. Their addresses are what map headers store as “axis ID”, which is why the boost map Y axis is DD94 = C1EC + C1D0.
Header: [ID X][nx][X…][ID Y][ny][Y…][data]. Data is stored row by row along X: index = x·ny + y. The code fetches the map address from a pointer table, not directly. Example: mov r12,[100Ch] reads 0x3900C → 0x446E → map 0x3C46E. Then r13=X, r14=Y and calls 00:20D6, result in r4.
All faults are reported via calls 01:4002: r2 = (path<<8) | ~bit, r1 = pointer to an 8-byte debounce record. Each path has a header with its P-code and an enable flag. Example: path 0x1E → P0400, flag 0x3AFEE.
Pedal: ADC → C1CC (0.01 %). Driver demand = f(rpm, pedal).
Start fuel = f(rpm, coolant).
Maximum fuel. If air mass is plausible (flag C0D2 ≥ 30h), the smoke limiter is used over rpm and air mass C216; otherwise a fixed 60 mg (). A temperature correction is subtracted, then the rate of rise is limited. The lower of this and the torque limiter is taken (in limp mode , 30 mg, instead). The contribution of the copies / is multiplied by factor , which is 0. Then: multiplication by factor C1D6, coolant correction , over-temperature derating and , limp-mode curves /.
Coordinator. Combines driver demand, idle (with values of 800–1100 the map looks like the idle speed setpoint; path 0x355B6 → 0x24C98 → C1F4) and what looks like a torque model: fuel→torque , friction , inverse conversion . Reads the C2BE limit.
Additive term C1D0 (looks like an anti-jerk damper, not verified) and final fuel quantity.
Exchange with the pump control unit (PSG) over CAN via ROM routines 7CD8/7D30. Sent: fuel C1D2, C250, C252 (timing) and C254. Received: 4 words into C246–C24C. One more value is written to C242; judging by the axis of map 0x3CDF8 it is a temperature, probably pump fuel temperature.
Fuel units (0.01 mg/stroke) are consistent across all maps and the boost axis, but no constant in the code proves them. The roles of 0x2327C, 0x34A34 and 0x18052 were derived from their inputs and outputs; these tasks were not traced line by line.
Boost pressure and atmospheric pressure sensors.
y = C1EC + C1D0. Base (rpm, y). At altitude (rpm, y) is added with weight (atm.): 100 % at 750 mbar and below, 0 % from 950 mbar. Then a temperature factor , a dynamic term via ROM 22F2, and an offset (zeros). The result is clamped to at least 0 and at most [0x3C3AA] = 2100.
Controller. Error = C224 − C220, fed to a P term (ROM 21A2) and an I term (21C4). The pilot (rpm, y) is added and the result is clamped to 0…65.00 % (0x3C3F2/0x3C3F6). Deviation monitoring runs only inside window and at valid temperatures and pressure: underboost above 250 mbar sets bit 4 of path 0x1D, overboost above 150 mbar sets bit 5.
MAF: two ADC samples (C00C, C00E) → characteristic → average → divide by rpm → correction . Plausibility check sets path 0x0B (P0100).
Setpoint: (rpm, C1EC+C1D0), plus corrections for atmospheric pressure , temperature and coolant (up to +545 mg on a cold engine, i.e. EGR closed). There is also a boost-deviation correction (, zeros in stock). Limits come from the block at 0x3A2A0.
PI controller in ROM 2430 (parameters 0x3A6AC, 0x3A6B6, 0x3A6C0) on the error C2E4 − C216. The output is inverted by curve (100 % − x). Modes in C9D8 substitute a fixed duty from the block at 0x3A68A: low rpm, high fuel, cold engine, sensor faults. Deviation monitoring runs inside window ; this is path 0x1E (P0400).
Keeping the valve closed in software is described in docs/frontera-x22dth/tuning-notes.md. A physical blanking plate remains the most reliable option where it can be fitted.
Cold start: (rpm, fuel) × coolant weight . Altitude: × atmospheric pressure weight . The sum is limited by (16.00), then a limited C210 is added.
Base (rpm, fuel), plus temperature correction (rpm, C242), plus , C20E and offset 0x3CB4E. Then limited via ROM 2756. The second channel (, ) is zero in stock.
Role confirmed: the value goes into the pump message next to the fuel quantity. Units are not confirmed. 20.3…33.2 looks like degrees × 100, but the pump may rescale it. Better left alone for Stage 1.
Maps the code actually reads through MAP2D/CURVE1D. “Confidence” shows how reliable the label is: H — role visible from code and values, M — from inputs and outputs, L — a guess. Click a row to see the map description. Titanium names are given in brackets.
| Header | Data | Size | Role | Conf. | Task |
|---|
| Path | P-code in table | Enable flag | Set by | Monitors |
|---|---|---|---|---|
| 0x1E | P0400 | 0x3AFEE = 1 | 0x1EBB6 | EGR loop air mass deviation (bits 4/5: above/below) |
| 0x1D | P1125 | 0x3B0FE = 1 | 0x1F6B8 | Boost deviation: underboost (bit 4), overboost (bit 5) |
| 0x0B | P0100 | 0x3AE96 = 1 | 0x15C84 etc. | MAF sensor |
| 0x09 | P0105 | 0x3AE2E = 1 | 0x1F2AC etc. | Pressure sensor |
| 0x05 | P0115 | 0x3AD1E = 1 | 0x1C55A etc. | Coolant sensor |
| 0x3D | — | — | 0x22240 | Checks inside the fuel limiter |
P-codes were read from the path headers in the calibration. Path 0x1E → P0400 → 0x3AFEE has been confirmed in practice. The other mappings were built the same way but not verified individually. For some paths (0x12/0x13, 0x24/0x25, 0x38–0x3D) the header could not be identified unambiguously.
reti at all.