Skip to content

Instantly share code, notes, and snippets.

@gnif
Last active May 1, 2026 01:57
Show Gist options
  • Select an option

  • Save gnif/85b8abb5a418ad36f0cc3ed0c8aa1207 to your computer and use it in GitHub Desktop.

Select an option

Save gnif/85b8abb5a418ad36f0cc3ed0c8aa1207 to your computer and use it in GitHub Desktop.
2013 Subaru Outback Cluster EEPROM Reverse Engineering
For whatever reason, the Subaru clusters I have tested appear to read about 8–10 km/h low from the factory on KPH models, and Subaru do not provide any user calibration method. Because of that, I purchased several clusters from wreckers and compared their EEPROM dumps to identify the calibration fields so the cluster can be corrected.
I have dumped and compared EEPROMs from three different clusters:
- 2013 Diesel Outback, KPH, RHD, CVT
- 2013 Petrol Outback, MPH, LHD, MT
- 2013 Diesel Outback, KPH, LHD, MT
One interesting note: the MPH cluster I imported from the US was made in Mexico, and removing the needles tends to pull the entire shaft out of the movement. By comparison, the Japan-made clusters are much easier to disassemble without damaging the gauge movements.
What follows is what I have discovered so far.
Note: I will not be posting details on odometer tampering. That is not the goal here.
Many parts of the EEPROM are organised as 10-byte records:
- bytes +0 to +7 = data
- byte +8 = XOR checksum
- byte +9 = unknown trailing byte
The XOR checksum is:
uint8_t calc_config_checksum(const uint8_t b[10])
{
uint8_t x = 0xFF;
for (int i = 0; i < 8; i++)
x ^= b[i];
return x;
}
This record format is valid continuously from 0x00 to 0xF9 in all three dumps, and also appears again in several later regions.
Configuration / Region / Features
0x00
Bit 0: 1 = [S] Feature flashing (SI drive?)
Bit 3: 1 = Disable EyeSight
Bit 6: 1 = Left-hand drive
0x01
Bit 2: 1 = AT, 0 = MT
Bits 5–7 control language / units behaviour.
Tested values:
0 = KPH & Japanese
1 = MPH & English
2 = KPH & English
3 = KPH & English
4 = KPH & English
5 = KPH & Invalid / No Text
6 = KPH & English
7 = KPH & Japanese
0x02
Bit 5: 1 = Enable washer fluid level
0x03–0x07 = Unknown
0x08 = XOR checksum
0x09 = Unknown trailing byte
Unknown
0x0A–0x11 = Unknown
0x12 = XOR checksum
0x13 = Unknown trailing byte (seen as 0x60)
Unknown
0x14–0x1B = Unknown
0x1C = XOR checksum
0x1D = Unknown trailing byte
Speedometer calibration block
0x1E–0x1F = Unknown
0x20-0x21 = Linear speed scale multiplier
Diesel = 8863
Petrol = 5506
Formula = new_scale = round(old_scale * desired_indicated_speed / current_indicated_speed)
0x26 = XOR checksum
0x27 = Unknown trailing byte
Speedometer calibration block
0x1E–0x1F = Unknown
0x20-0x21 = Linear speed scale multiplier
Diesel = ???
Petrol = ???
Formula = ???
0x26 = XOR checksum
0x27 = Unknown trailing byte
Speedometer calibration block
0x28–0x2F = Speed input breakpoints, 4x u16 little-endian, units = KPH * 100
0x30 = XOR checksum
0x31 = Unknown trailing byte
Diesel KPH, RHD, CVT:
[2000, 12000, 22000, 24649]
Petrol MPH, LHD, MT:
[1609, 12874, 22530, 25026]
Diesel KPH, LHD, MT:
[2000, 12000, 22000, 23895]
Note: the MPH cluster still stores these in KPH * 100. The values correspond to:
10 mph = 16.09 km/h -> 1609
80 mph = 128.74 km/h -> 12874
140 mph = 225.30 km/h -> 22530
Speedometer output / angle breakpoints
0x32–0x39 = Speedometer output / angle breakpoints, 4x u16 little-endian
0x3A = XOR checksum
0x3B = Unknown trailing byte
Diesel KPH, RHD, CVT:
[527, 2868, 5206, 5825]
Petrol MPH, LHD, MT:
[396, 3008, 5247, 5825]
Diesel KPH, LHD, MT:
[527, 2946, 5367, 5825]
These values are not simple standalone step counts. The gauge movements are driven by separate SIN and COS coils directly from the MCU, so these are likely angle / drive calibration values for the air-core movement.
Confirmed behaviour:
- 0x38 is 5825 in all three known-good dumps
- 0x36 is not a free value; it matches the interpolated value derived from 0x34 and 0x38
Using:
x2 = input at 0x2A
x3 = input at 0x2C
x4 = input at 0x2E
y2 = output at 0x34
y4 = output at 0x38
the expected value at 0x36 is:
y3 = y2 + (x3 - x2) * (y4 - y2) / (x4 - x2)
rounded to nearest integer.
This reproduces the factory values in all three dumps:
Diesel KPH, RHD, CVT: 5206
Petrol MPH, LHD, MT: 5247
Diesel KPH, LHD, MT: 5367
So 0x36 appears to be a derived checkpoint rather than an independently chosen calibration point.
The two diesel clusters have identical tachometer tables but different speedometer tables, so the speedometer calibration appears to vary by cluster / drivetrain variant.
Tachometer calibration block
0x46–0x4D = Tach input breakpoints, 4x u16 little-endian, units = RPM
0x4E = XOR checksum
0x4F = Unknown trailing byte
Diesel KPH, RHD, CVT:
[1000, 3000, 5000, 5721]
Petrol MPH, LHD, MT:
[1000, 4000, 7000, 8208]
Diesel KPH, LHD, MT:
[1000, 3000, 5000, 5721]
0x50–0x57 = Tachometer output / angle breakpoints, 4x u16 little-endian
0x58 = XOR checksum
0x59 = Unknown trailing byte
Diesel KPH, RHD, CVT:
[1031, 3092, 5154, 5897]
Petrol MPH, LHD, MT:
[700, 2837, 4967, 5825]
Diesel KPH, LHD, MT:
[1031, 3092, 5154, 5897]
The tachometer follows the same general structure as the speedometer.
Using:
x2 = input at 0x48
x3 = input at 0x4A
x4 = input at 0x4C
y2 = output at 0x52
y4 = output at 0x56
the expected value at 0x54 is:
y3 = y2 + (x3 - x2) * (y4 - y2) / (x4 - x2)
rounded to nearest integer.
This reproduces the factory values in both diesel and petrol dumps.
Variant / feature candidates
0x42 and 0xA6 are strong candidate variant fields.
They differ between the diesel CVT dump and both manual dumps, but are not yet proven to be MT/CVT flags specifically.
Odometer block
0x1A0–0x1BF
Immob coding
0x2D8–0x2E3
Unknown data.
This region changes when re-pairing keys, so it is likely immobiliser-related coding.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment