Hello + two open-source Convers+ firmware patches (BT-Audio titles, gauge sweep)

Newly activated users can post here
Post Reply
mocarz
Posts: 2
Joined: 15 Jul 2026, 06:35

Hello + two open-source Convers+ firmware patches (BT-Audio titles, gauge sweep)

Post by mocarz »

Hello everyone,

I've been reading here for a while and finally got my account working, so let me introduce myself. I drive a Mondeo MK4 facelift with a Convers+ cluster and I've spent the two months reverse-engineering its firmware. This forum was a huge help along the way, so I'd like to give something back.

I've published two open-source projects. Both patch the cluster firmware itself — no external hardware, no Raspberry Pi, no gateway injecting fake CAN IDs. And neither repo contains any Ford firmware: only the tools, so everyone patches their own dump.

1) convers-bt-audio — native Bluetooth-Audio track title & artist on the cluster
https://github.com/andrzejogh/convers-bt-audio

This one ties directly into the "Transmitting infodata on MM-CAN" and "can bus node app" threads. You had already mapped the media IDs there — USB titles on 4C7, and 4B0/4B1 as "Bluetooth Musik". The factory firmware happily shows USB titles (4C7) but simply ignores the Bluetooth-Audio stream. So instead of injecting text from the outside, I patched the firmware to route 0x4B1 into the same media store the factory uses for 0x4C7. The result: the cluster shows the BT track title and artist natively, exactly like it does for USB — up to 18 characters per field. Confirmed working on a real 1412-FL (part CS7T-14C026-CD).

2) convers-gauge-sweep — welcome needle sweep at ignition
https://github.com/andrzejogh/convers-gauge-sweep

A single full needle sweep when the cluster powers up, driven by the cluster's own factory sweep routine rather than any external signal. It ships in two variants: a default one with no menu entry, and one that adds a "Gauge sweep" item to the Advanced menu for people running m0rtar's modified firmware. This is a newer project, published for testing — feedback from other cars is very welcome.

Both projects come with full documentation and a Unicorn-based emulator test suite, so the patches are verified before anyone flashes anything.

Along the way I worked out a few things I'd be glad to share and discuss in the disassembly sections: the SRAM layout derived from the C-runtime startup, the gauge module's state machine, and the format of the Advanced-menu descriptors.

One small thing — my account can read everything but can't yet download attachments or post in the technical sub-forums. If an admin could activate that, I'd love to contribute directly to the Convers+ disassembly threads.

Thanks for running this place,
mocarz
Go4IT
Pro
Posts: 983
Joined: 08 Feb 2019, 12:25

Re: Hello + two open-source Convers+ firmware patches (BT-Audio titles, gauge sweep)

Post by Go4IT »

This is a very neat and interesting project, i'm curious to get deeper into details. Can you elaborate more about the simulator and how found find the entrypoints to inject own code and how you compile that code for the MAC?
mocarz
Posts: 2
Joined: 15 Jul 2026, 06:35

Re: Hello + two open-source Convers+ firmware patches (BT-Audio titles, gauge sweep)

Post by mocarz »

Thank you Go4IT — coming from you, who built this place and wrote so much of what I learned from, that really means a lot. I'll go into as much detail as I can, and please correct anything that's off — some of this is still hypothesis on my side.

Up front: a large part of this was done with AI assistance (Claude / Claude Code). I did the reverse engineering and the decisions, but the AI was a real force multiplier — driving Ghidra and Capstone, reading large amounts of Thumb disassembly, and writing the emulator harness and the assembler. The habit that made it dependable: treat it as a hypothesis generator and verify every claim against the emulator or the raw bytes. It was confidently wrong about big-endianness and a few addresses early on, so I check everything now. I'm glad to attach the analysis scripts it produced (the Ghidra/Capstone helpers and the harness).

1. The simulator (and setup)

Two different things here.

What I use day to day is a thin harness on the Unicorn Engine (the CPU core from QEMU), configured big-endian ARM / Thumb / ARMv4T. Setup is:
  • map the application code from 0x5000,
  • map SRAM at 0x40000000, length 0xC000 (48 KB),
  • set SP to the factory value 0x4000BFEC (taken straight from the reset code — more below),
  • then call individual firmware functions directly instead of booting the image.
Booting for real needs peripherals I don't model, so I jump into the function I care about — for the gauge work that's the real task FUN_0x353DE (@0x3540E) — with registers and SRAM pre-loaded. Two hooks do the rest: a memory-write hook logs every SRAM write (this is how I see exactly what a function touches), and a code hook stubs the calls that would hit hardware, returning a canned value so the function runs to completion. I keep a map of ~3820 function entry points from the disassembly so I can fire any of them on demand. That let me check the gauge patch behaviour — one sweep, resets across a restart, never writes outside its own state word, never sweeps while driving — over many iterations in seconds. 20 such tests ship with the patch.

The full end-to-end emulation is a different, harder achievement, and credit for that goes to another member here, wojtkowiak — he got a complete emulator running and could explain it far better than I can. He's also waiting on activation here; if his account were enabled too, I think he'd happily share that work.

2. Finding the entrypoints, and the memory map

The SRAM map mattered most, and I recovered it by disassembling the C-runtime startup (the ARM code at 0x5348..0x5410) and reading what it initialises:

Code: Select all

0x40000000..0x40000810   noinit  - startup never touches this
0x40000810..0x4000190C   .data   - copied from ROM 0xD630C on every start
0x4000190C..0x4000B008   .bss    - zeroed on every start
0x4000B008..0x4000B7F0   heap    - filled 0xEFEFEFEF on every start
0x4000B7F0               lower stack canary (0x5A, checked at 0x1E5EE)
0x4000B7F4..0x4000BFEC   stack   - filled 0xEFEFEFEF on every start
0x4000BFEC               upper stack canary = initial SP (set at 0x1E5E6)
0x4000BFF0..0x4000BFFF   noinit  - and not one reference to it anywhere in the code
Those last 16 bytes are where the gauge patch keeps its state (two words at 0x4000BFF4 and 0x4000BFF8, tagged with a 0x5B39 header so I can tell my data from random power-up garbage). Justifying that location took some care: the whole .bss is occupied — my worst bug was the sweep firing while driving, because an early state byte lived inside a 500-byte text buffer at 0x400092DE that gets reused. What convinced me 0x4000BFF0..BFFF is safe is that the firmware's own address validator at 0x43F74 uses 0x4000BFFF as the RAM upper bound — so it's provably valid RAM, yet nothing references it and startup never clears it, which is exactly what a persistent flag needs.

For the injection points themselves, static disassembly to form a hypothesis, the harness to confirm:
  • Gauge sweep: the module runs a state machine (a dispatcher on a state variable). The consumer is FUN_0x353DE: when the power state returned by FUN_0x22C32 is 2 or 3 (I read it from 0x40006B31) and the module state *(u8)0x400075E8 == 9, it acts on the flags at 0x400075EA. The physical movement is entirely factory — FUN_0x34F6E drives zero -> full-scale -> zero, each stage gated by FUN_0x34B38 confirming the steppers arrived. So my patch is literally setting one bit (bit 7 at 0x400075EA); the sweep is 100% the cluster's own routine. I hook two points: 0x34FCA (the power-ON branch) to arm and gate it, and 0x35682 (the dispatcher case for module state 22) to clear the once-per-ignition flag.
  • BT-Audio: the cluster accepts a fixed set of media IDs into the FlexCAN mailboxes (acceptance table at 0x79446: 4B1, 4B3, 4C0, 4C1, 4C6, 4C7, 4D0, 4D2, 4D4, 4D5). USB titles (0x4C7) have a callback (FUN_0x25c3f) and ISO-TP reassembly (0x25641) that fill the media text store; 0x4B1 is accepted into the mailbox, but its content never reaches that store. So I hook one bl at 0x236F6 — before the mailbox-type split, so it runs for every frame — into a cave that reads the mailbox directly (per-bus base 0x7A0D4, mailbox = base[bus] + mb*16 + 0x80), takes CAN-ID = (word >> 18) & 0x7FF, and if it's 0x4B1 does the ISO-TP reassembly itself into the media store (0x40009300), then returns the mailbox pointer untouched so the original handler is none the wiser. One gotcha: 0x4B0 carries the same text, but handling both interleaves and corrupts the buffer, so I filter strictly to 0x4B1.
3. Compiling for the MAC7116

Because the patches are small — the trampoline plus a few dozen instructions — I didn't set up a cross-compiler. I hand-wrote a small Thumb-1 assembler in Python that emits the exact encodings, including the 4-byte BL that reaches the cave. That gives byte-exact control and lets me diff the output against the surrounding factory code. Each patch is additive: place the code in an unused region of the code partition (I've used 0x83240 and 0x9B8AC, each a few hundred bytes of padding), repoint one existing bl to a trampoline there, and the trampoline ends by calling that bl's original target — so the factory path always still runs.

For a real toolchain the equivalent is arm-none-eabi-gcc with -mbig-endian -mthumb -mcpu=arm7tdmi -mfloat-abi=soft, remembering it's a BE-32 target (big-endian code and data) and that the code has to be placed at the cave address via the linker script. For patches this size the hand assembler was simply less friction.

The hardest parts
  1. Big-endian Thumb. Nearly every tool — and the AI — defaults to little-endian; until Ghidra, Capstone, Unicorn and the model were all consistently BE, the output looked plausible and was wrong. This cost the most time by far.
  2. Stubbing peripherals in the harness — working out which calls touch hardware so the function under test runs to the end.
  3. Timebase. An early gauge version trusted the 128 ms coarse timer (0x40006672, from the ISR FUN_0x1E6D4) and gave four sweeps at ignition on the real car. I stopped trusting wall-clock timing and switched to counting task passes instead — that fixed it.
  4. Finding a safe byte of RAM — the 0x4000BFF0 story above.


Happy to go deeper in the disassembly section once I can post there, and to share the harness and the scripts.

Best regards,
mocarz
Post Reply