This guide is adapted from the original thread on EvoM:
The beginners' guide to Evo ECU table lookups
EcuFlash provides an XML file for each ROM, which defines a bunch of tables (1D, 2D, and 3D) that can be edited by the user. Every 2D or 3D table is indexed by one or two "axis" values; it's how you look up values in the table. By way of example, the High Octane Fuel Map has both RPM and Load axes; to look up a value in the table, you first look up the current engine speed (RPM) and calculated load, and the AFR target value you're looking for is where the two intersect on the table. (2D works the same way, but there's only one dimension to worry about.)
The XML tells you where in the ROM the table and axis table data is located. Back to our example, the High Octane Fuel Map table in 96530006 is listed as being at address "33bd", with the "Engine Load" axis as being at address "68b0" and the RPM axis at address "6888". If you pull those up in IDA Pro, you'll see the data listed there, just as EcuFlash promised, but that's not the whole story; if you've ever done this, you'll notice that IDA doesn't have any code or data references listed for those ROM addresses. Every table, and every "axis table", has a header attached to it, just before the data an end-user of EcuFlash normally edits. That header data is used by a set of standard routines that every Evo ROM provides to assist in performing lookups, and the address of that header is what IDA ends up generating code and data references to.
By understanding how the ECU performs lookups in tables, it can help you to both understand when you're looking at code that performs table lookups (thus helping you locate new tables), and help you understand why you get back the values you do when the ECU performs a table lookup.
To get to the value in a table, you have to first look up the axis positions. The axis table header, just prior to the data itself, defines an address to store lookup results in, followed by the address of "current value" of that axis. For example, if the table axis is "RPM", second address is where the current vehicle RPM is stored in memory. The next word defines the number of elements in the axis, and then the axis data follows.
So, axis tables look like this:
- A long word for the result address.
- A long word for the value to look up.
- A word for the length of the axis.
- A series of words, as long as the length, containing the axis data.
Axis lookup is done by calling the routine at sub_CC6. You tell sub_CC6 where the axis header is by assigning its address to r4.
For 2D tables, you perform one axis lookup; for 3D tables, you perform two. The result of each call to sub_CC6 is a value stored in that axis' "result address"; it's the position in the axis that most closely (rounding down) matches the current value of the axis (ie. the position on an RPM axis that most closely matches the current engine speed).
Once you have the axis positions back from sub_CC6, you need to look up the actual table value. For byte-width tables, you call sub_C28; for word-width tables, sub_E02.
The table header has some similarities to the axis header. The first byte (or word, for sub_E02) determines whether the table has two dimensions (0x2) or three (0x3). The second byte (or word) is a global "adder" that is added to any value returned from the table. Next, a long word describes where the position on the X-axis is stored in memory (returned from sub_CC6); in a 3D table, an additional long word is included, as well as a one-byte value denoting the length of a row. After that, the table data follows, either in word (for sub_E02) or byte (sub_C28) form.
So, tables look like this:
- A byte (or word, for word-sized tables) for the number of dimensions: 2 = 2D, 3 = 3D.
- A byte (or word, for word-sized tables) for a value "added" to all values returned from the table.
- A long word for the position on the X-axis.
- Optionally, a long word for the position on the Y-axis in a 3D table.
- Optionally, a byte (or word, for word-sized tables) for the length of each row in a 3D table.
- A series of words or bytes containing the table data.
Note that the X and Y position addresses must either match the result addresses from the axis tables, or match a pair of addresses that you have manually copied the axis lookup results into.
Just like with axis lookups, you set r4 to the address of the table you want to perform the look-up in. When control is returned, r0 contains an interpolated value based on how "close" the axis values were to a labelled position on the axis.
So, if you see calls to sub_CC6 when reading through your disassembly of a ROM, it's an indication that an axis lookup is being performed, and if you see calls to sub_C28 or sub_E02, there's a table lookup happening. Looking at the lines of code leading up to that for any assignments to register r4 will tell you where the axis or table headers are located. Normally, the code right after a call to sub_C28 or sub_E02 will assign r0 to some memory address (or eventually get there, by bouncing it around from register to register), which you can log via the MUT table if you want to keep an eye on the value, and can give you an idea of what other code that uses that value is doing (for example, code that deals with the result from a High Octane Fuel Map lookup is probably involved in fueling).
Hopefully, this can help some newer folks get started with understanding one of the most common activities ECU code participates in.