This report documents the four confirmed cooling-fan map objects in the default/original firmware image:
Image: DV4A-14C204-SB.BIN
SHA-256: da37907baf7c2c4086c48b4249458a6f36c8f98a7a4b3ce66682270e521d350a
Size: 4 MiB
Architecture: Infineon TriCore, little-endian
Image base: 0x80000000
Address conversion for the raw image:
file offset = address - 0x80000000
The analysis is static. No ECU communication or flashing was performed.
Conversions shown in this report are presentation-only; firmware float values remain unchanged.
°C = (°F - 32) × 5 / 9
km/h = mph × 1.609344
| Firmware value | Metric conversion |
|---|---|
| 24°F | −4.44°C |
| 70°F | 21.11°C |
| 80°F | 26.67°C |
| 90°F | 32.22°C |
| 100°F | 37.78°C |
| 110°F | 43.33°C |
| 205°F | 96.11°C |
| 210°F | 98.89°C |
| 215°F | 101.67°C |
| 220°F | 104.44°C |
| 225°F | 107.22°C |
| 230°F | 110.00°C |
| Firmware value | Metric conversion |
|---|---|
| 3 mph | 4.83 km/h |
| 10 mph | 16.09 km/h |
| 30 mph | 48.28 km/h |
| 40 mph | 64.37 km/h |
| 60 mph | 96.56 km/h |
| Ghidra label | Address | File offset | Dimensions | Interpreted purpose |
|---|---|---|---|---|
FAN_AC_PRESSURE_AMBIENT_MAP |
0x80173C68 |
0x173C68 |
6 × 6 | Base fan demand from A/C pressure and ambient temperature |
FAN_AC_SPEED_CORRECTION_MAP |
0x80173B8C |
0x173B8C |
5 × 5 | Vehicle-speed reduction subtracted from A/C-derived fan demand |
FAN_COOLANT_TEMP_AMBIENT_MAP |
0x80173F0C |
0x173F0C |
6 × 6 | Base fan demand from coolant and ambient temperatures |
FAN_COOLANT_SPEED_CORRECTION_MAP |
0x80173E68 |
0x173E68 |
5 × 5 | Vehicle-speed reduction subtracted from coolant-derived fan demand |
All four objects are typed in the persistent Ghidra project as FanMap2DDescriptor. Their axes use FanAxisPoint entries, and the matrices are row-major float arrays.
Ghidra label: FAN_AC_PRESSURE_AMBIENT_MAP
Address: 0x80173C68
File offset: 0x173C68
Dimensions: 6 × 6
Output: fan demand, percent
- X: pressure-like values
60, 200, 250, 275, 325, 341 - Y: ambient temperature:
- 24°F (−4.44°C)
- 70°F (21.11°C)
- 80°F (26.67°C)
- 90°F (32.22°C)
- 100°F (37.78°C)
- 110°F (43.33°C)
The X values are strongly characteristic of A/C high-side pressure, probably in psi, but the exact engineering unit is inferred because no Ford/Bosch symbol file is available.
| Ambient temperature | 60 | 200 | 250 | 275 | 325 | 341 |
|---|---|---|---|---|---|---|
| 24°F (−4.44°C) | 21 | 21 | 71 | 71 | 81 | 100 |
| 70°F (21.11°C) | 21 | 40 | 71 | 71 | 81 | 100 |
| 80°F (26.67°C) | 21 | 45 | 71 | 71 | 81 | 100 |
| 90°F (32.22°C) | 21 | 69 | 75 | 81 | 81 | 100 |
| 100°F (37.78°C) | 65 | 71 | 90 | 90 | 100 | 100 |
| 110°F (43.33°C) | 82 | 100 | 100 | 100 | 100 | 100 |
The plot uses ambient temperature in °C. The footer preserves the original Fahrenheit breakpoints.
Interpretation: fan demand generally rises with both the pressure-like input and ambient temperature. The map reaches 100% across much of the high-pressure/high-ambient region.
Ghidra label: FAN_AC_SPEED_CORRECTION_MAP
Address: 0x80173B8C
File offset: 0x173B8C
Dimensions: 5 × 5
Output: correction, percent
- X: vehicle speed:
- 3 mph (4.83 km/h)
- 10 mph (16.09 km/h)
- 30 mph (48.28 km/h)
- 40 mph (64.37 km/h)
- 60 mph (96.56 km/h)
- Y: A/C fan request:
20, 30, 50, 60, 100%
The vehicle-speed interpretation has medium-high confidence based on values and data flow from candidate RAM 0xD00103A0.
| A/C fan request | 3 mph (4.83 km/h) | 10 mph (16.09 km/h) | 30 mph (48.28 km/h) | 40 mph (64.37 km/h) | 60 mph (96.56 km/h) |
|---|---|---|---|---|---|
| 20% | 0 | 0 | 0 | 15 | 20 |
| 30% | 0 | 0 | 0 | 20 | 30 |
| 50% | 0 | 0 | 0 | 30 | 35 |
| 60% | 0 | 0 | 0 | 5 | 45 |
| 100% | 0 | 0 | 0 | 0 | 6 |
Ghidra label: FAN_COOLANT_TEMP_AMBIENT_MAP
Address: 0x80173F0C
File offset: 0x173F0C
Dimensions: 6 × 6
Output: fan demand, percent
- X: engine coolant temperature:
- 205°F (96.11°C)
- 210°F (98.89°C)
- 215°F (101.67°C)
- 220°F (104.44°C)
- 225°F (107.22°C)
- 230°F (110.00°C)
- Y: ambient temperature:
- 24°F (−4.44°C)
- 70°F (21.11°C)
- 80°F (26.67°C)
- 90°F (32.22°C)
- 100°F (37.78°C)
- 110°F (43.33°C)
The temperature interpretation has high confidence from the breakpoints, data flow, and resulting percentage demand.
| Ambient temperature | 205°F (96.11°C) | 210°F (98.89°C) | 215°F (101.67°C) | 220°F (104.44°C) | 225°F (107.22°C) | 230°F (110.00°C) |
|---|---|---|---|---|---|---|
| 24°F (−4.44°C) | 0 | 9 | 12 | 20 | 60 | 100 |
| 70°F (21.11°C) | 0 | 10 | 15 | 20 | 60 | 100 |
| 80°F (26.67°C) | 0 | 10 | 15 | 25 | 70 | 100 |
| 90°F (32.22°C) | 0 | 10 | 15 | 30 | 80 | 100 |
| 100°F (37.78°C) | 0 | 10 | 15 | 30 | 80 | 100 |
| 110°F (43.33°C) | 0 | 10 | 15 | 30 | 80 | 100 |
Both temperature axes use °C. The footer preserves the original Fahrenheit breakpoints.
Interpretation: demand begins near zero at 205°F (96.11°C), rises gradually through 220°F (104.44°C), then rises sharply to 60–80% at 225°F (107.22°C) and 100% at 230°F (110.00°C).
Ghidra label: FAN_COOLANT_SPEED_CORRECTION_MAP
Address: 0x80173E68
File offset: 0x173E68
Dimensions: 5 × 5
Output: correction, percent
- X: vehicle speed:
- 3 mph (4.83 km/h)
- 10 mph (16.09 km/h)
- 30 mph (48.28 km/h)
- 40 mph (64.37 km/h)
- 60 mph (96.56 km/h)
- Y: coolant fan request:
10, 11, 30, 50, 100%
| Coolant fan request | 3 mph (4.83 km/h) | 10 mph (16.09 km/h) | 30 mph (48.28 km/h) | 40 mph (64.37 km/h) | 60 mph (96.56 km/h) |
|---|---|---|---|---|---|
| 10% | 0 | 0 | 0 | 0 | 0 |
| 11% | 0 | 0 | 3 | 11 | 11 |
| 30% | 0 | 0 | 0 | 5 | 15 |
| 50% | 0 | 0 | 0 | 0 | 0 |
| 100% | 0 | 0 | 0 | 0 | 0 |
Address: 0x80174720
File offset: 0x174720
Ghidra label: FAN_MIN_ACTIVE_COMMAND_PCT
Type: float
Value: 7.5%
This scalar is the default firmware's minimum active fan command.
0x8027B7A2 Fan_CalcAcPressureRequest
inputs: candidate A/C pressure at 0xD000288C
candidate ambient temperature at 0xD0016460
map: 0x80173C68 FAN_AC_PRESSURE_AMBIENT_MAP
helper: 0x8025819C interpolation
output: 0xD000C948
The A/C speed-correction map at 0x80173B8C participates in the A/C request path.
0x8027BD08 Fan_CalcCoolantRequest
inputs: candidate coolant temperature at 0xD0002A2C
candidate ambient temperature at 0xD0016460
candidate vehicle speed at 0xD00103A0
maps: 0x80173F0C FAN_COOLANT_TEMP_AMBIENT_MAP
0x80173E68 FAN_COOLANT_SPEED_CORRECTION_MAP
helper: 0x8025819C interpolation
output: 0xD000C940
0x8027BEB0 Fan_UpdateAllNormalRequests
0x8027C5AA Fan_ArbitrateNormalRequests
0x8027CA7C final normal/failsafe request write to 0xD000C944
0x80234E64 Fan_ApplyDiagOverrideAndConditionOutput
The final physical transport after output conditioning—direct PWM/peripheral or a fan-control-module message—has not yet been conclusively identified.
- Map addresses, dimensions, descriptors, axis arrays, and row-major float matrices.
- Coolant and ambient temperature breakpoint values.
- Vehicle-speed-like breakpoint values.
- Percentage output values.
- Integration of the maps into A/C and coolant fan-demand calculations.
- Normal request arbitration into RAM
0xD000C944. - Default minimum active command of
7.5%.
0xD000288Cas filtered A/C high-side pressure: high confidence.- Pressure-axis engineering unit as psi: high confidence, not symbol-confirmed.
0xD0002A2Cas engine coolant temperature in °F: high confidence.0xD0016460as ambient-air temperature in °F: high confidence.0xD00103A0as vehicle speed in mph: medium-high confidence.
This ECU protects the calibration with THREE independent integrity layers. A modified image must satisfy all three, or the PCM boots, runs for a few minutes, then crashes when its background integrity monitor runs. This was discovered the hard way over two failed VBF flashes; a PCM-Flash-style binary flasher that "updates CRC" fixes all three, which is why binary flashing worked while the VBF (missing a layer) did not.
Correct fix order (do NOT reorder): Layer 3 → Layer 1 → Layer 2. The
fix_all_checksums.py wrapper runs them in order and verifies all three.
Standard MED17 CRC32 / ADD32 / ADD16 block checksums plus the Dataset #0
RSA/signature adjustment region at 0x1FFF78–0x1FFFFB. Corrected by
medc17_checksum_tool.py (reports 11/11 valid). WinOLS and most tools handle
this layer. Its adjust region is OUTSIDE every Layer-3 sub-block and outside the
Layer-2 region, so running it second is safe.
Layer 2 — 16-bit calibration "balance / monitoring" checksum (hidden)
A 16-bit balance word that medc17/WinOLS do NOT compute (PCM-Flash-style
flashers do). Reverse-engineered by differencing two OEM datasets identical except
in Dataset #0 calibration (DV4A-14C204-SB.VBF vs -JE.VBF); both OEM balance
words reproduce exactly.
Region summed : 0x140000 – 0x1FFE60 (exclusive end; Dataset #0 calibration payload)
Method : sum of 16-bit little-endian words
Balance word : low u16 at file offset 0x3FFC84
(high u16 stays 0xFFFF; lives in the AppSW#1 DEADBEEF identity trailer)
Invariant : sum16(region) + balance ≡ 0xADC5 (mod 0x10000)
Correction : balance = (0xADC5 - sum16(region)) & 0xFFFF
OEM cross-check: SB stored 0x0091 (sum 0xAD34), JE stored 0xE787 (sum
0xC63E), both → 0xADC5. Tool: fix_layer2_checksum.py.
Layer 3 — Dataset #0 per-sub-block sum16 verification table (hidden)
Found by sniffing an actual working binary flash and diffing the three flashed
regions against our BIN: the flasher had changed 14 bytes we never touched. It is a
table of per-64 KiB-sub-block checksums covering all of Dataset #0.
medc17 and the Layer-2 tool both miss it. A stale entry passes boot but trips the
background monitor → delayed crash (this was the true root cause of both VBF
crashes; Layer 2 alone was necessary but not sufficient).
Table : file 0x1C8E20 (ECU 0x801C8E20), 13 entries x 16 bytes
Entry : [start u32 LE][end u32 LE][sum u32 LE][~sum u32 LE]
Method (sum) : 32-bit sum of 16-bit little-endian words over [start,end] inclusive
Self-check : sum XOR ~sum == 0xFFFFFFFF (all 13 entries)
Coverage : DS0 0x80140078 – 0x801FFF6F in 64 KiB sub-blocks
Self-covering : the table sits inside the 0x801C0000 sub-block, so recomputing an
entry changes a covered block -> iterate to a fixpoint (the tool does)
Correction : sum = sum16words(sub-block); ~sum = sum ^ 0xFFFFFFFF
Validated: all 13 entries of the sniffed working image reproduce exactly. Our
crashing file had 2 stale entries (the sub-blocks holding the output floor
0x80157C68 and the fan maps 0x80173xxx). Tool: fix_layer3_checksum.py.
General Bosch note (Layers 2 & 3): a block/sub-block closes with a balance/sum word (before a
DEADBEEFmarker for the block trailers) so its word-sum hits a fixed constant. Confirmed publicly (VietDiesel / TuningHost: "WinOLS and many flashers do not calculate such a checksum for the monitoring area; PCM Flash does").
fix_all_checksums.py Run all 3 layers in order (L3->L1->L2) + verify. USE THIS.
fix_layer3_checksum.py Check/fix Layer-3 DS0 sub-block sum16 table
fix_layer2_checksum.py Check/fix Layer-2 balance word; writes <name>.crc.<ext>
medc17_checksum_tool.py Layer-1 analyze/correct (--correct -o / --json)
build_crc_vbf.py Rebuild the full VBF from a fully-corrected BIN
fix_balance_checksum.py (legacy) one-off Layer-2 fix for fan-v2
DV4A-14C204-SB.fan-v2.crc.BIN
SHA-256: 5a5c05c6acc82535786c3d61c6df438055bc059620a9509021df2cb21888ed33
Layer 1: 11/11 Layer 2: balance 0xBA67 Layer 3: 13/13 sub-block sums valid
Byte-matches the sniffed working flash in DS0 except medc17's own signature-adjust
region (both individually valid).
DV4A-14C204-SB.fan-v2.crc.VBF 21-block, file_crc32=0x7D32D756
DV4A-14C204-SB.fan-map.crc.BIN fan maps only, A/C map left stock; all 3 layers valid
SHA-256: 1594a81fcd95bf95895f99e7fa31b13d262ee141c05c48808e99c1b097c4000e
DV4A-14C204-SB.fan-map.crc.VBF 21-block, file_crc32=0x9A5A37FD
Do not reuse any earlier VBF/BIN that predates the Layer-3 fix — they carry stale sub-block sums and/or a stale balance word and caused the delayed crashes. Flash a current
*.crc.*/*.full.VBFpackage (all three layers valid), and prefer the full 21-block layout.

