Which TriCore registers are safe to modify, and which ones crash the ECU?

Of the TriCore registers, four are off-limits to modify: A0, A1, A8, and A9. A0/A1 control contexts and processing zones; A8/A9 are tied to the stack pointer, the data area, and the vector table — touch them and the ECU can crash. Every other register is usable once you know what it carries, and the split between upper and lower context tells you which is which.

TriCore has 32 registers: address registers A0–A15 (they hold memory addresses) and data registers D0–D15 (they hold the numbers being calculated — fuel mass, RPM, torque). The reason “can I change this register?” has a real answer is that the two halves behave differently across a function call.

Upper context is safe inside a function

Upper context — A8–A15 and D8–D15 — is saved by the system on CALL and restored on RET. So inside a subroutine you can use D8–D15 as free scratch: whatever the caller had in them is put back on return. If D15 changes inside a called function, the caller’s D15 is untouched afterwards.

The catch is on the address side of upper context. A8 and A9 are system registers even though they live in the upper range, and A10 is the stack pointer. So the working rule is: among the upper-context address registers, prefer A11–A15 for your own work, and leave A8–A10 alone.

Lower context is how parameters move

Lower context — A2–A7 and D0–D7 — is not preserved across a call. That is the point, not a flaw: it is how parameters pass from caller to callee and results come back. In practice compilers stick to D2–D7 and A2–A7. So when a value arrives in D4 at the top of a function and a result leaves in A4 or D2 at the bottom, you are reading the function’s inputs and outputs.

If you want to reuse a lower-context register and keep its value across a call, you have two safe options: save it to RAM yourself first, or confirm it gets reinitialised after the return. Otherwise assume it is gone.

What it looks like in a real function

Take a concrete subfunction call in Ghidra. A10 (the stack pointer) is used for stack manipulation. D4 arrives as a parameter from the caller and gets shifted into D15 for local work. On the way out, A4 and D2 carry values back. Inside the callee, that D15 is entirely local — after RET the caller sees its own D15 unchanged, because upper context was restored.

Read a function that way and the register question answers itself line by line: D4 is an input, D15 is temporary scratch, A4 and D2 are outputs, A10 is the stack you do not improvise on.

One thing has to be true before any of this reads cleanly: the disassembly itself has to resolve. If yours shows jumps into empty space and references to nowhere, that is a compressed-readout problem, not a register one — here is how to spot and fix it.

Quick reference

Register group Safe to use? Why
A0, A1 No Control contexts / processing zones
A8, A9 No Stack pointer / data area / vector table
A10 No (leave alone) Stack pointer
A11–A15 Yes (upper) Preserved on CALL/RET
D8–D15 Yes (upper) Free scratch inside a function
A2–A7, D0–D7 Yes, with care (lower) Carry parameters; not preserved across a call

Related on Tuners Guild

Method credit: this is the register model taught in Thomas Pirowski’s Ghidra TriCore Fundamental curriculum.

Your turn

Have you crashed an ECU by writing to the wrong register — or found one the compiler used in a way this table doesn’t cover? Post the register and the ECU below — the compiler edge cases are the ones worth collecting.