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
- The full architecture: TriCore architecture for ECU reverse engineers — memory, registers, and context explained end to end.
- Learn it on real firmware: Ghidra TriCore Fundamental reads real Bosch EDC17 and MED17 code, register by register.
- Where this sits in the path: Learning Path L0→L3.
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.