Firmware re

IVT
The IVT (interrupt vector table / exception vector table) is an array of addresses, each of which is associated with certain exception number. When an interrupt or exception occurs (this includes power-on reset), the mcu fetches the appropriate address and continues execution at that address. This mechanism is described in detail in the datasheet.

The mcu has a hardware register (vbr) that must be set to the desired IVT location, which can be anywhere in ROM or RAM. At power-on reset vbr is set to zero. Many ROMs use this flexibility to have both a minimal IVT used at power-on, and a secondary IVT for normal use. See Power-on reset code for more details.

Power-on reset code
There are two first checks for ROM state :
 * 1) checks for a "NHU\0xF8" signature near the end of the ROM
 * 2) checks for possibly a "ROM_written" flag, also near the end of the ROM.

If either of these checks fail, the ROM boots in a type of Nissan bootloader mode; it seems it would accept a limited set of iso14230 commands, based on static analysis. This is untested.

If the checks succeed, then the code sets the vbr register to point to the secondary vector table. Note : some older ROMs (without the "LOADERxx" metadata structure in the ROM) do not have a secondary vector table at all !

Sasha @ RR posts the following example of signatures in a "MY06 350Z ROM":

ROM:000A79B0 .datab.b h'585D0, h'FF ROM:000FFF80 NissanMarkWritten_byte_FFF80:.data.b 1 ; DATA XREF: ROM:off_2A8�o ROM:000FFF81 .data.b h'FF ROM:000FFF82 .data.b h'FF ROM:000FFF83 .data.b h'FF ROM:000FFF84 NissanSignature_byte_FFF84:.data.b h'4E ; N ; DATA XREF: ROM:off_2A0�o ROM:000FFF85 .data.b h'48 ; H ROM:000FFF86 .data.b h'55 ; U ROM:000FFF87 .data.b h'F8 ; ° It appears Nissan always use those same 4 bytes for all its ROMs {0x4E 0x48 0x55 0xF8}

LOADER metadata
This " LOADER struct" is a small block of data (roughly 50 bytes), always found relatively near the beginning of the ROM, that contains a few strings:


 * " LOADERxx" such as "LOADER40" etc. Most likely the loader version.
 * "DATABASE"
 * a CPU string such as "SH705513N". This doesn't necessarily indicate the mcu type; some ECUs have 7055 here even though the mcu is a 7058. The last two digits before 'N' seem to always match the CPUcode in the FID struct, and give some insight into the structure of the ROM.

This looks a lot like a type of primary bootloader that would be flashed at the ECU factory, before the final programming is determined. Then, later during manufacturing the final ROM would be reflashed using this bootloader. This is just speculation however.

FID metadata
" FID struct " This is a slightly bigger block of data that I now split in two parts to ease analysis. The first part (FID base) is approximately 60 to 70 bytes long and contains these elements:


 * FID string, for " firmware ID" (my own terminology, I have no official source to confirm what this is). This looks like "5X61BECCNA" and other seemingly random combinations of upper case letters and numbers. So far these strings have not proved to be very unique: some ROMs can be slightly different and still share the same FID, but have a different ECUID.
 * " DATABASE"
 * CPU string + CPU code, such as "SH705822". The first four digits are so far always the type of mcu used. The last two digits (CPU code) are some kind of version number, also giving some insight into the structure of the ROM, in particular of the fields present in the second part of the FID struct.
 * in some cases, MSTCR values. This may be a coincidence - often these values are the same as the ones used at reset to set the MSTCR/SYSCR register (this enables/disables certain peripheral clocks). I have found no code referencing these values directly.

The second half ("RAMF" - my own terminology) is mostly pointers to RAM areas, and in some cases information about checksum areas (see https://nissanecu.miraheze.org/wiki/Checksums ) TODO

Serial comms : SID tree
This is how I call the functions that parse ISO14230 requests. SID stands for service ID, see https://nissanecu.miraheze.org/wiki/Std_14230 and in particular the standards themselves (the SSF version is freely available).

There are two main types of trees: messy and clean. The type of tree is mainly decided by what compiler and compiler options were used, in other words "coin toss".

The original source code no doubt contained a few "switch" statements such as

switch (SID_requestnumber) { case 0x1A: ...	case 0x27: .... } With a number of complications: in some operating modes, certain requests are denied (especially during SID27 + SID36 operations).

The messy tree (e.g. CF43D) has all the handling code inlined in one huge function. The clean tree (e.g. 6Z68A) has every SID handler in a separate subfunction; this makes it much easier to see how each handler works.

In every ROM, there are at least two SID trees: a larger one handling all the requests available when connecting normally to the ECU, and a smaller one that may be part of a rescue/production reprogramming mode. Which tree is called depends on a complex selection function, and involves a ridiculous number of global variables and flags.

So far most (all?) 7055 and 7058 ROMs have some interrupt driven code that parses each incoming byte and assembles them into complete 14230 frames. The data part of these frames gets copied at a fixed location in RAM, 0xffff8003. The few bytes at 0xffff8000, before the data payload, contain source/destination addresses (and length?) and usually aren't very important.

Since the first byte the payload is the SID, the handlers work on the subsequent bytes.

Call tables
These ECUs don't use an OS; they mainly use interrupts and large tables of function pointers, polled continuously in a loop. It turns out these call tables are fairly big (often over 100 entries), this is very helpful for delimiting functions and automating code analysis, for example in IDA.

Disassembly analysis and SH assembly patterns
I won't duplicate the info found in the datasheet and the software Manual (read them!!), these are just highlights and tips that aren't immediately obvious. Nissan ROMs are probably compiled with the Renesas "shc" C compiler, which generates sometimes confusing or obfuscated code sequences.

Finding RAM references
This is a big part of analyzing algorithms. Variables are stored in RAM and it becomes necessary to track who writes to those locations, and who reads them. I wrote a number of tools to assist with this, see nissutils in particular cli_utils/test_findrefs.c.

Many references will still elude automatic analysis, but luckily this often has a simple explanation. Many important variables in RAM are copied around in blocks - presumably to maintain state atomically. The pattern is roughly thus: ;this is the reset code from the second IVT. alt_poweronreset: ;does a bunch of stuff, then ;in the last block of code (looped), we find something like this

lastloop_top: ;unimportant mov.l  @(h'10,r11), r13 stc    sr, r2	and     r10, r2	ldc     r2, sr

shortloop: ;also unimportant mov.b  @(h'11,gbr), r0	tst     #1, r0	bt      loc_1005A mov.l  @(h'10,r11), r2	mov     #h'48, r0 ; 'H'	sub     r13, r2	mov.l   r2, @(r0,r9)

;here is the key: function main_copystuff mov.l  @(h'64,pc), r2 ; [000100D0] = main_copystuff jsr    @r2 ; main_copystuff nop

;...... other unimportant stuff bra    lastloop_top nop

Sign extension for RAM addresses
Very, very frequent. Put to good use since RAM addresses are often >= 0xFFFF 8000, and the SH "mov" instruction is sign-extending. This allows the compiler to load a 16-bit value (the lower 16bits of the address) from a literal pool, which sign-extends to the desired RAM address, instead of storing the desired 32-bit value in the literal pool. mov.w @(0x60,PC), r11   ;if the 16bit word stored at (PC + 0x60) is 0x8438, this would set r11 = 0xFFFF8438

shll8 to get peripheral register base address
This is most probably due to the fact that peripheral registers are defined as a "struct" in the compiler headers, and the compiler almost always accesses the members by adding the base address of the struct with "offsetof(struct_member)" :

mov 0xFFFFFFF5, r2	;encoded as "mov -0x0B, r2", or "mov -11, r2" ! shll8 r2		; now r2=0xFFFFF500 (&TCNT6A), the base address for the Channel 6 timer ! mov 0, r0 mov.w r0, @(0x10,r2)	; clear 0xFFFF F510 (BFR6A), the actual register we're interested in.

Function calling convention
Almost every function respects this ABI convention (described in the compiler docs; GCC and Renesas SHC ABIs are compatible):


 * Arguments to functions are passed in r4,r5,r6,r7 as needed;
 * Return values of functions are returned in r0.

Delay slot
This is well explained in the datasheets, but easy to forget when starting out: most branch/jump instructions execute the next opcode before jumping to their destination. Some examples:

bra next_iter mov 0, r0 .... next_iter: more code Easy. r0 gets set to zero before we reach next_iter.

Also very common: mov @(0x66, pc), r2	;r2 = 0x6a24 jsr @r2		;sub_6A24 mov 0x68, r4 Here, as defined by the ABI, the first argument to sub_6A24 is passed in r4.

More confusing: bsr sub_whatever1 mov 0x55, r5	bsr sub_whatever2 mov.w r0, @(0x18, gbr) In this case, the last mov.w is actually storing the return value of the *first* function call (sub_whatever1), just before executing sub_whatever2 !

Interrupt disable/lock
A very common pattern; quick to recognize. The first half updates the IMASK bits in the sr register to disable all interrupts, saving the original IMASK for later. The second half is the exact opposite and restores the IMASK bits. The purpose of this maneuver is to protect a section of code so it runs from beginning to end without interruption.

stc    sr, r0 shlr2   r0 shlr2   r0 and     #h'F, r0 mov     r0, r4	;saving original IMASK to r4 stc     sr, r0 mov.w   @(h'FA,pc), r1 ; [00045BB2] = h'FFFFFF0F and    r1, r0	;useless or     #h'F0, r0	;set IMASK to b'1111 ldc    r0, sr	;update

..... critical code here .....

mov    r4, r0 and     #h'F, r0 shll2   r0 shll2   r0 stc     sr, r2 mov.w   @(h'16,pc), r3 ; [00045BB2] = h'FFFFFF0F and    r3, r2	;clear IMASK bits or     r0, r2	;restore orig imask ldc    r2, sr

stack management
How a stack works is mostly common knowledge and beyond the scope of this page, so I'll skip over the basics. Unlike x86, there are no dedicated push/pop opcodes. To accomplish the same, r15 is used as a stack pointer. Then, in function prologues, this type of code sequence is used to save registers:

mov.l  r13, @-r15 mov.l  r14, @-r15 sts.l  pr, @-r15 stc.l  gbr, @-r15 ...

The "@-" notation means "pre-decrement", so the r15 value is updated before storing each register to the stack area. Then just before returning, the function will restore the regs with the corresponding "@r15+" post-increment operator:

... ldc.l  @r15+, gbr lds.l  @r15+, pr mov.l   @r15+, r14 rts mov.l  @r15+, r13

Note the crafty use of the delay slot !

global variable structs
Confusing and inconsistent, some global vars are accessed sometimes with a direct address, sometimes with gbr-based addressing. TODO

algo
see Checksum algorithms

Checksum functions
For the standard checksum (calculated over the entire ROM), these functions are usually present:


 * main checksum calculation function which computes the sums on a fraction of the ROM and updates the current totals. This function eventually runs a few times before the whole ROM is covered.
 * single step function, where one dword of the ROM is added to the current computation. I assume this is to make a very fast function, maybe called from time-sensitive sections where the main function would take too much time.
 * comparison function, that runs after the current computation is finished, and compares the calculated values with the hardcoded expected values.
 * validation function, checks the result of the comparison and sets some flags and DTCs if necessary.

For the alternate checksums (alt, alt2), the functions are very similar.

EEPROM
see EEPROM access functions

security / encryption
It seems everything uses the same algorithm. Somewhere Nissan calls this algo "01", so I'll use that notation. It operates thus: They are the inverse of each other, i.e. ( rev_01( fwd_01(data, scode), scode) == data )
 * 1) uint32_t fwd_01(uint32_t data, uint32_t scode);   // encodes 'data' using 'scode'.
 * 2) uint32_t rev_01(uint32_t data, uint32_t scode);   // decodes 'data' using 'scode'.

In both forward (encrypt) and reverse (decrypt) directions, this uses three helper functions, one of which simply swaps data between registers and is common to both directions (the two other helper functions are different for encrypting and decrypting). The swap function is fairly easy to find and leads directly to SID27 and SID36 code.

The details of the algorithm itself are not very important, but are implemented and partly documented in the source code for nisprog CLI tools (TODO : links ?).

The fwd / rev functions, for some reason, don't strictly follow the ABI convention of passing the three first parameters in the r4, r5, r6 registers. Instead they assume the key to be stored in hardcoded RAM locations. Even more, these RAM locations are often the same between ROMs. One set of popular locations is 0xFFFF8416 and 0xFFFF8418, where each half-key is stored and used. Again the details of this aren't very important.

The 01 algo is used for on most known ROMs. Algo description and equivalent C code [posted here].
 * SID 27 key exchange (use "fwd_01" to generate key from seed)
 * SID 36 RAM bootloader kernel download ("rev_01" to decode the payload; fwd_01 was used to encrypt it)
 * SID 36 firmware payload download (rev_01 to decode the payload)

Of keys
Every ROM has a set of three keys, one for SID27 and two for SID36. The SID27 key is used in the first step of the reflashing process for the security handshake.

The first SID36 key is used to decrypt the payload (such as a reflashing kernel) sent from the host, which is then executed from RAM.

The second SID36 key is contained in the ROM, but not used directly by it. It is only copied into a "preload" structure just before running the payload (in effect passing a bunch of parameters to the payload); the original Nissan kernels then retrieve it and use it to decrypt the new ROM payload. This third key is of course only used during a factory reflash, and is not very useful to us.

Some keysets are common to more than one ROM, so far there are a few ways to determine which keyset is used:


 * maintain a list of ECUID - keyset pairs
 * similar ECUIDs often use the same keyset, this can be used to expand the list for new ECUIDs
 * dump the ROM and look for known keys, this often works because some keysets occur frequently
 * dump the ROM and run a key finding utility, this almost always works except for new code patterns that the utility isn't aware of
 * dump the ROM and manually find the keys, a last resort method.

Arbitrary code execution
Note : this section applies mainly to K-line based ECUs. It's possible some of the steps will work on CAN-only ECUs ?

This is the stepping stone towards reflashing over the OBD K line or CAN interface, without opening the ECU case. Some nice things that can be done :
 * Run a "fastdump" kernel to read out the ROM much faster than the standard K line method
 * Read / write the external EEPROM
 * Reflash all, or part of the ROM

The method is as follows :
 * 1) write / compile payload, either as position-independant code, or linked to run at the ECU's "RAMjump" address.
 * 2) Pad payload to next multiple of 32 bytes
 * 3) Calculate "cks16" : unsigned 16bit sum of all 8-bit bytes of the (unencrypted) payload
 * 4) encrypt payload using "sid36 key 1"
 * 5) Connect to ECU
 * 6) SID 27 seed/key exchange
 * 7) SID 34 80, enter programming mode
 * 8) SID 36 XX YY 0x20 B_00 B_01 B_02... B_31
 * 9) Increment XXYY at every line : "36 00 00 20 ...... 36 00 01 20 ...."
 * 10) SID 37 cks16_H cks16_L TransferExit
 * 11) SID BF 00 ramjump check
 * 12) SID BF 01 actual ramjump.

The payload must be crafted to disable all interrupts and take over the manual generation of the WDT pulse train. Or, it might be possible (if the goal isn't a reflash) to use the firmware's mechanism for the WDT ? i.e. enable a limited set of interrupts, etc)

A very simple test payload that simply freezes the ECU (causing the supervisor to reset it) could look like 00 02	stc	sr, r0	CB F0	or	#h'F0, r0	40 0E	ldc	r0, sr deathloop: AF FE	bra	deathloop 00 09	nop

Finding DTC code and data
There's a few ways to go about this but here's my current method:
 * 1) search the ROM for 4-byte values like 0x00000605 (for P0605), or any other DTC known to exist on that ROM. P0605 (ECM failure) seems pretty common accross most ROMs.
 * 2) from those multiple occurences it's pretty easy to recognize the DTC descriptor table (see Firmware_re section below)
 * 3) follow the xrefs to that table, to figure out
 * 4) number of DTCs defined for this ROM (approx 250, give or take)
 * 5) location of the DTC status block in RAM
 * 6) DTC_set function
 * 7) DTC options block

Older method, I don't do this any more
 * 1) find the ISO 9141 code (hint : "mov 0x6B, X" near a "mov 0x48, X")
 * 2) go to the mode 1  request handler
 * 3) browse to the part of the code that handles mode 1 PID 1, this reports the number of DTCs set.
 * 4) it does this by looping over the DTC status block in RAM and checking some flags for each possible DTC. This gives two pieces of information:
 * 5) number of DTCs defined for this ROM (approx 250, give or take)
 * 6) location of the DTC status block in RAM
 * 7) go back a few steps and find the OBD mode 3 request handler (this one returns the DTC numbers such as "12 20" for P1220, etc.). This gives us locations of two data structures in the ROM:
 * 8) DTC descriptor block
 * 9) DTC options (?) block

data structures

 * DTC_status block

Each DTC has 3 bytes of status: struct dtc_status_t { u8	flags_0;	//? u8	flags_1;	//bit field ? u8	flags_2;	//bit field ? }; Contents of each flag needs to be investigated. I expect them to contain the required info for multi-trip detection logic and clearing conditions, pending/set status, etc.

The status block itself is an array of those 3-byte structures: struct dtc_status_t DTC_status_block[total_number_of_DTCs];

RAM:FFFFB23E                dtcstatus <0, 0, 0>     ; 30 RAM:FFFFB23E                dtcstatus <0, 0, 0>     ; 31 RAM:FFFFB23E                dtcstatus <0, 0, 0>     ; 32 RAM:FFFFB23E                dtcstatus <0, 0, 0>     ; 33 RAM:FFFFB23E                dtcstatus <0, h'C0, h'4B>; 34 RAM:FFFFB23E                dtcstatus <0, 0, 0>     ; 35
 * Example from 6Z68A showing DTC index # 34 (code P1612 according to descriptor block) with some other unknown flags set

// Each DTC has a descriptor (0x0c bytes each) struct dtc_descr_t { u8 field_0; u8 field_1; u8 padding_0;	//maybe always 0? u8 padding_1;	//maybe always 0? u32 dtc_code;	//like 0x605 for DTC "P0605" u32 flags_2;	//bit field ? }; // And stored in the ROM is the descriptor table: struct dtc_descr_t DTC_descriptors[total_number_of_DTCs];
 * DTC_descriptor block

Note : a same dtc_code can used multiple times in that table, probably to distinguish between causes for the same DTC code. Here too, more investigation would be needed. ROM:0000E0C4 DTC_descr:     dtcdescr ; 0 ROM:0000E0C4 ROM:0000E0C4                dtcdescr ; 1 ROM:0000E0C4                dtcdescr ; 2 ROM:0000E0C4                dtcdescr ; 3 ROM:0000E0C4                dtcdescr ; 4 ROM:0000E0C4                dtcdescr ; 5 ....
 * example from CF43D

This one is a bit unusual; instead of being an array of data structures like the previous two blocks, this one is an array of pointers, each referencing the actual 16 bit value that contains the option flags. Some of these flags appear to enable/disable setting of the DTC, prevent it from being reported by standard OBD requests, etc ? u16 *DTC_options[total_number_of_DTCs]; Example from CF43D: ROM:0000ED78 DTC_optionwords:.data.l 0xD714      ; 0 ROM:0000ED78                .data.l 0xD704       ; 1 ROM:0000ED78                .data.l 0xD63A       ; 2 ROM:0000ED78                .data.l 0xD53A       ; 3 .... ROM:0000D704 word_D704:     .data.w h'20 ROM:0000D706 word_D706:     .data.w h'20 ROM:0000D708 word_D708:     .data.w h'43E3 No idea what the values stand for.
 * DTC_options block
 * and some example option words :

ADC
Unfortunately the ADC-related code is spread out across a number of access functions, and a few different methods are used throughout the ROM. Here are a few (incomplete) notes.

get_ADC_from_table method
This is used for about a dozen ADC channels. There's a data structure in the ROM that describes a set of channels, giving for each the address of the control and data registers: struct ADC_descriptor_t { u8 *p_AD_CR u8 *p_AD_CSR;	// u32 flags;	//unknown; top byte seems to be index of ADC channel within its block? u16 *p_AD_data; }; // Example from 6Z68A: ROM:000032C0 ADCtable_32C0: ADC_descr1 ; 0 ROM:000032C0                ADC_descr1 ; 1 ROM:000032C0                ADC_descr1 ; 2 ROM:000032C0                ADC_descr1 ; 3 ROM:000032C0                ADC_descr1 ; 4 This table is used by the ADC_getfromtable function, that takes one argument (index into that ADCtable) and returns the desired (ADC value >> 6). This shift simply aligns the 10 - bit data to the least significant bit positions.

Direct method
This uses the shll8 trick described above, adding the required offset to access the ADC value directly. Example from 6Z68A: ROM:0008C51A                mov     #h'FFFFFFF8, r14 ROM:0008C51E                shll8   r14             ; r14 = 0xffff f800 = &ADDR0H ROM:0008C520                mov.w   @r14, r2        ; get ADDR0 val ROM:0008C522                extu.w  r2, r0 ROM:0008C524                 shlr2   r0 ROM:0008C526                 shlr2   r0 ROM:0008C528                 shlr2   r0		;align to LSB ROM:0008C52A                mov.w   r0, @(h'11A,gbr) ; copy to ADCvals area in RAM ROM:0008C52C                mov.w   @(2,r14), r0    ; get ADDR1 (ASCD) ...

iso14230 Common Identifiers (CID)
These ROMs implement many CIDs for querying/retrieving both realtime parameters and built-in values. This is done with SID 22, documented in the 14230 docs and a dedicated SID 22 page. The data returned by the ECU seems is 1 to 4 bytes long depending on CIDH.

Every CID is 2 bytes, CIDH and CIDL. etc. the "support mask" is implemented exactly like OBD Mode 1 PID 00 struct CID_table_t { u32 *support_mask; u16 *CID_data[31]; }; //one for every supported CIDH range. example: struct CID_table_t CID12_00_table; struct CID_table_t CID12_20_table; struct CID_table_t CID13_00_table; //etc. They're usually near each other in the ROM but in no particular order. Note: some CIDH ranges may have a truncated table, i.e. containing less than 31 CID_data pointers.
 * 0x12 0x00 : get mask of supported CIDs in range 12 01 to 12 20. If 12 20 is supported:
 * 0x12 0x20 : get mask of supported CIDs in range 12 21 to 12 40.

ROM:000035D0 CID1200_table: .data.l CID1200_supmask ROM:000035D0                .data.l engRPM?_86C8, h'FFFFFFFF, unk_FFFFB168, unk_FFFF8452 ROM:000035D0                .data.l h'FFFFFFFF, unk_FFFF2566, h'FFFFFFFF, unk_FFFF84A2 ROM:000035D0                .data.l word_FFFF382C, h'FFFFFFFF, h'FFFFFFFF, unk_FFFF8458 ROM:000035D0                .data.l AN6_APS1_9764, unk_FFFF9768, unk_FFFF972C, unk_FFFF970E ROM:000035D0                .data.l h'FFFFFFFF, h'FFFFFFFF, h'FFFFFFFF, unk_FFFF3684 ROM:000035D0                .data.l unk_FFFF368E, unk_FFFF3322, h'FFFFFFFF, unk_FFFF17C4 ROM:000035D0                .data.l unk_FFFF8604, unk_FFFF84E6, word_745A, unk_745C ROM:000035D0                .data.l h'FFFFFFFF, h'FFFFFFFF, h'FFFFFFFF
 * Example from 6Z68A

Timer blocks
Many parts of the code need to measure short time intervals. One method frequently used in these ROMs is the use of timer blocks.
 * 1) each "timer block" is an array of variables (for example bytes) somewhere in RAM. Each of these variables is an independent timer.
 * 2) one of the ATU timers is set up to trigger a periodic interrupt, typically ATU31_IMI3A
 * 3) this interrupt handler decrements each value of every timer block by one, until they reach zero. I think all the timer blocks are down-counting like this?
 * 4) to use those timers, the code will set one of the timers to the desired time interval, and check when it reaches zero.

Excerpt from CF43D interrupt handler, updating the timer block at 0xffff3bd4 :

loc_25DC:                              ; CODE XREF: INT_ATU31_IMI3A+1AE mov.l  #tmrblock_3BD4, r5	mov     #h'FFFFFFC9, r4	extu.b  r4, r4	mov     r5, r2	add     r4, r2          ; r2= &tmrblock_3bd4 + 0xc9 (end of tmr block) cmp/hs r2, r5	bt/s    finished_3bd4 mov    r5, r6

tb_3bd4_loop:                          ; CODE XREF: INT_ATU31_IMI3A+1EE mov.b  @r6, r2	tst     r2, r2	bt      t3bd4_next      ; b if tmr value reached 0 mov.b  @r6, r2	add     #-1, r2         ; decrement tmr extu.b r2, r2	mov.b   r2, @r6         ; update tmr value

t3bd4_next:                            ; CODE XREF: INT_ATU31_IMI3A+1DC add    #1, r6	mov     r5, r2	add     r4, r2          ; inc ptr cmp/hs r2, r6	bf      tb_3bd4_loop

finished_3bd4:                         ; CODE XREF: INT_ATU31_IMI3A+1D4

And here's some typical code using timer # 0x55 with an interval of 0x0A: (also from CF43D)

; assume gbr was set to the timerblock base address loc_55AAC: mov.b  @(h'55,gbr), r0	tst     r0, r0	bf      exit	;exit if tmr hasn't reached 0 mov    #h'A, r0	bsr     sub_55AC4 mov.b  r0, @(h'55,gbr)	;reload tmr to 0x0A again bsr    sub_55B3C nop

exit: ldc.l  @r15+, gbr lds.l  @r15+, pr	rts nop

ASCD code
To find the ASCD stuffI started with some code that parsed ADC values, then found an area in RAM where a couple of those values are stored; I already knew (from tracing signals on the PCB) which ADC channel was wired to the ASCD switch, so I just followed references to that channel and ended up in the ASCD stuff.

Now it's easier to just look at the call tables; the ASCD function is always in the same place: the second call table, item number 5 or 6:

Fuel cut
Rough, rough guide to finding fuel cut values - refer to CF43D / CM31C xml defs for starting point

bsr X or r0, rN bsr or bsr or ...
 * second last calltable (big, ~100 funcs)
 * around item #21 (around #21 to #29, approx) : there's a fairly linear func, sized ~ 0x100 to 0x124, with a bunch of
 * pattern : there's a "tst 0x80; bf Y" that skips two 'bsr's. Skip those.
 * the third bsr leads to The Func
 * The Func has a bunch of "mov.w  @(i,rN), r0" with i=2,4,6 at the beginning. Those are the u16 fuel cut vals