August 16, 2022

Firmware Reverse Engineering II (Briot Example)

In this post I will continue what I started in the previous post in which I presented how to import and prepare Ghidra to analyze the Biot Tracer firmware, whose circuit was also reversed engineered.

Briot Firmware Reverse Engineer

The Briot Tracer is used as an example to show (at least partially) the firmware reverse engineering process. Since it uses an external memory (EPROM) the process for obtaining the firmware is simple which makes it ideal as a test case.

As it is not the purpose of this post to study the complete firmware, only the RS232 communication is analyzed in order to gain some insight into how this communication occurs.

Where to start?

In this post references to parts of the Briot Tracer schematic will be used. These references will have the following format: (Ref.: page, coordinate).

It is a good idea to start by taking a look at the Briot Tracer schematic to identify how it performs the task to be analyzed. In this case RS232 communications are performed by U5 (Ref.: 10, 2C) an SCC2691 Universal Asynchronous Receiver/Transmitter (UART).

To identify where in the code, for example, a character received by the UART is read, a list of its registers and their corresponding addresses is needed. This list is presented in the SCC2691 datasheet (Page 7, Table 1) and their addresses must be completed by studying the address decoding of the schematic (Ref.: 6, 5A).

The address of each one of the UART registers is presented next.

Address Read Write
0x8080 MR1, MR2 MR1, MR2
0x8081 SR CSR
0x8082 BRG Test CR
0x8083 RHR THR
0x8084 1x/16x Test ACR
0x8085 ISR IMR
0x8086 CTU CTUR
0x8087 CTL CTLR

Two registers of special interest are RHR and THR. These are used to send and receive data to and from the UART. To send a character the microcontroller must write it to address 0x8083 (THR register), and to get a received character it must read it at address 0x8083 (RHR register).

It is necessary to find in which parts of the code the accesses to memory address 0x8083 occur in order to know which functions are responsible for sending and receiving the characters and how they do it.

In the previous post an external memory block named IO_RS232 was created in Ghidra, for the memory range 0x8080-0x8087. The memory address 0x8083 is located in this memory block as well as the references from where it is accessed. These references are presented next.

...
// UART RHR (read) and THR (write) registers
DAT_EXTMEM_8083:      XREF[14]: X1_INT:195a(*), X1_INT:195d(R),
                                X1_INT:197d(*), X1_INT:1980(W),
                                FUN_CODE_1a5d:1a80(*),
                                FUN_CODE_1a5d:1a83(R),
                                FUN_CODE_1a5d:1a88(*),
                                FUN_CODE_1a5d:1a8b(W),
                                FUN_CODE_1a5d:1a99(*),
                                FUN_CODE_1a5d:1a9c(R),
                                FUN_CODE_1a5d:1aa2(*),
                                FUN_CODE_1a5d:1aa5(W),
                                FUN_CODE_1a5d:1ab3(*),
                                FUN_CODE_1a5d:1ab6(R)
...

Note that for a read access Ghidra adds an (R) to the name of the function where such access occurs, for a write access a (W) and for a read/write access a (*).

It can be seen that this memory address is accessed by the X1_INT and FUN_CODE_1a5d functions. To understand how data is sent and received by the UART, the code of these functions must be analyzed.

Before we start analyzing these functions we need to deal with an issue involving the registers of the 80C552 in Ghidra. The 80C552 is a derivative of the 8051 with other peripherals, so it has a different set of registers to configure and use these peripherals.

80C552 Register Labels Correction on Ghidra (click to show/hide)

The 80C552 is not in the Ghidra catalog of known processors but it is an 8051 derivative and Ghidra can be used to load code compiled for it.

In the following image the Special Function Registers (SFR) of the 80C552 is presented. This information can be found in the 80C552 datasheet (Page 56, Figure 44).

80C552 Special Function Registers
Special Function Registers of the 80C552

The following image presents a map of the SFRs constructed from the information presented by Ghidra in the Listing window when SFRs are selected in the Program Trees.

Ghidra Special Function Registers
Ghidra Special Function Registers

To avoid misinterpretation of the code presented by Ghidra, the SFR labels must be corrected according to the 80C552 datasheet.

Not only the SFRs have to be corrected, but also the SFR-BITS that correspond to the bit-level access of the SFRs. The information needed to do this can be found in the 80C552 data sheet (page 58, Figure 47).

To change the label of a register, double-click SFR in the Program Trees window, then right-click the label of the register you want to correct in the Listing window and select the Edit Label option, or press the [L] key after placing the cursor on the label of the register you want to change.

When Ghidra encounters an instruction that accesses an unknown register, it creates a label for it in the format DAT_SFR_xx, where xx corresponds to the memory address of the register. This may be the result of decoding data in memory as instructions, or registers that are unknown to Ghidra as a result of loading code for a different microcontroller. This decoding leads to erroneous code, in the sense that the resulting code is unintended.

To remove an erroneous instruction, use the Clear Code Bytes option by right-clicking on an instruction in the Listing window. To do this, go to the address indicated by one of the CODE:nnnn references on the right by double-clicking on it and then press the [C] key. After doing this, the CODE:nnnn reference you clicked on should disappear. After deleting all the code bytes corresponding to each reference of a register labeled DAT_SFR_*, that register label should disappear.

The procedure for correcting SFR-BITS is the same as for correcting SFRs.

X1_INT Function Analysis

The function X1_INT is the Interrupt Service Routine (ISR) that is executed when an External Interrupt 1 (X1) is recognized. The name X_INT was assigned following a procedure explained next. The original label of this function was FUN_CODE_1949.

Ghidra allows you to correct the parameters and the name of a function. To do this, click on the function name (usually named FUN_CODE_xxxx) in the Decompile window and select the Edit Function Signature option, or press the [F] key after positioning the cursor on the function name in the Listing window.

In the schematic it can be seen that the INT1 pin of the microcontroller (Ref.: 4, 6B) is connected to the INT pin of the UART IC(Ref.: 10, 2C). It is likely that the UART IC generates an interrupt to the microcontroller, for example, when a character has been received, which confirms the relationship between this interrupt and the sending of data via the UART.

At the end, the complete Interrupt Vector Table (IVT) is presented and analyzed.

The X1 ISR is responsible for obtaining the character received by the UART IC and storing it in the RX queue, as well as sending an available character in the TX queue to the UART IC for transmission.

To store a value in the RX queue, the FUN_CODE_1a06 function is called, and to retrieve a character from the TX queue, the FUN_CODE_19b6 function is called.

Complete Code and Analysis of X1_INT and Related Functions (click to show/hide)

The complete X1 ISR code is presented next, with comments added to explain its operation.

...
X1_INT(void)

// Preserves the registers modified by the ISR
1949: c0 e0     PUSH   A
194b: c0 d0     PUSH   PSW
194d: c0 83     PUSH   DPH
194f: c0 82     PUSH   DPL
1951: c0 f0     PUSH   B

// Reads the SR register of the UART IC and checks if a character has been received (SR[0]==1). If no character has been received, it jumps to address 0x196a
1953: 90 80 81  MOV    DPTR,#0x8081
1956: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8081
1957: 30 e0 10  JNB    ACC.0,LAB_CODE_196a

// Reads the character received from the UART IC (RHR register) in register A, copies it to register B, loads DPTR with 0x1481 and calls the function at address 0x1a06 (This function is analyzed later)
195a: 90 80 83  MOV    DPTR,#0x8083
195d: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8083
195e: f5 f0     MOV    B,A
1960: 90 14 81  MOV    DPTR,#0x1481
1963: 12 1a 06  LCALL  FUN_CODE_1a06

// Reads the SR register of the UART IC
1966: 90 80 81  MOV    DPTR,#0x8081
1969: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8081

LAB_CODE_196a:
// Check if a transmission can be initiated (SR[2]==1), if not, jump to address 0x1981
196a: 30 e2 14  JNB    ACC.2,LAB_CODE_1981

// If a transmission can be initiated, load 0x14a5 in DPTR and call the function at address 0x19b6 (This function is analyzed later)
196d: 90 14 a5  MOV    DPTR,#0x14a5
1970: 12 19 b6  LCALL  FUN_CODE_19b6

// After returning, if the CY flag is not set, it jumps to address 0x197d. Otherwise, it enables the RxRDY interrupt and jumps to address 0x1981
1973: 50 08     JNC    LAB_CODE_197d
1975: 90 80 85  MOV    DPTR,#0x8085
1978: 74 04     MOV    A,#0x4
197a: f0        MOVX   @DPTR=>DAT_EXTMEM_8085,A
197b: 80 04     SJMP   LAB_CODE_1981

LAB_CODE_197d:
// Writes the value of register A to the THR register of the UART IC (Sends the character to the UART IC for transmission)
197d: 90 80 83  MOV    DPTR,#0x8083
1980: f0        MOVX   @DPTR=>DAT_EXTMEM_8083,A

LAB_CODE_1981:
// Restores the registers modified by the ISR and returns from the interrupt (RETI enables interrupts)
1981: d0 f0     POP    B
1983: d0 82     POP    DPL
1985: d0 83     POP    DPH
1987: d0 d0     POP    PSW
1989: d0 e0     POP    A
198b: 32        RETI
...

An alaysis of the FUN_CODE_1a06 and FUN_CODE_19b6 functions is presented next.

FUN_CODE_0x1a06 Function Analysis

An analysis of the code executed before the CALL instruction shows that this function receives two parameters: a pointer in the DPTR register and a value in the B register. The procedure explained above for modifying the parameters and the name of a function was used to assign the parameters described before.

This function stores the character received in the B register to the queue whose pointer is received in the DPTR register. If the queue is not full, it clears the carry flag (CY) and returns. If the queue is full, the received character is lost and the function returns with the carry flag set.

This function is only called from the X1_INT function.

The complete FUN_CODE_0x1a06 function code is presented next, with comments added to explain its operation.

...
// When called, DPTR is 0x1481, and B contains the character received by the UART
FUN_CODE_1a06(byte * pointer, byte received_value)

// Stores value at memory address 0x1481 in DAT_INTMRM_5c
1a06: e0        MOVX   A,@pointer
1a07: f5 5c     MOV    DAT_INTMEM_5c,A

// Compare the values at RAM addresses 0x1481 and 0x1482. If they are not equal, jump to 0x1a10
1a09: a3        INC    pointer
1a0a: e0        MOVX   A,@pointer
1a0b: b5 5c 02  CJNE   A,DAT_INTMEM_5c,LAB_CODE_1a10

// Sets the carry flag (CY=1) and returns
1a0e: d3        SETB   CY
1a0f: 22        RET

LAB_CODE_1a10:
// Increments the value stored at memory address 0x1482
1a10: 04        INC    A
1a11: f0        MOVX   @pointer,A

// Gets the value at memory address 0x1484, increments it by 1 and compares it to the value at DAT_INTMEM_5c (the original value at memory address 0x1481)
// If they are not equal jump to 0x1a1b, otherwise load A with 0x00 (circular queue)
1a12: a3        INC    pointer
1a13: a3        INC    pointer
1a14: e0        MOVX   A,@pointer
1a15: 04        INC    A
1a16: b5 5c 02  CJNE   A,DAT_INTMEM_5c,LAB_CODE_1a1b
1a19: 74 00     MOV    A,#0x0

LAB_CODE_1a1b:
// Loads at memory address 0x1484 the value of A (either 0x00, or the value at address 0x1484 incremented by 1)
1a1b: f0        MOVX   @pointer,A

// Loads into DPTR the value 0x1485 added to the value at memory address 0x1484
1a1c: a3        INC    pointer
1a1d: 25 82     ADD    A,pointer DPL
1a1f: f5 82     MOV    pointer DPL,A
1a21: e5 83     MOV    A,pointer DPH
1a23: 34 00     ADDC   A,#0x0
1a25: f5 83     MOV    pointer DPH,A

// Loads in A the value received as parameter (character to be stored in the queue) and saves it in the memory address pointed by DPTR
1a27: e5 f0     MOV    A,received_value
1a29: f0        MOVX   @pointer,A

// Clears the carry flag (CY=0) and returns
1a2a: c3        CLR    CY
1a2b: 22        RET
...

There seems to be some kind of error in Ghidra (version 10.1.4), or at least an error in the way Ghidra presents the code in the Listing View, which can lead to confusion.

The instructions at addresses 0x1a1d to 0x1a121, and 0x1a25 should be presented as accesses to DPL or DPH instead of DPTR and the assembly code should look as follows:.

...
LAB_CODE_1a1b:
1a1b: f0        MOVX   @pointer,A
1a1c: a3        INC    pointer
1a1d: 25 82     ADD    A,DPL
1a1f: f5 82     MOV    DPL,A
1a21: e5 83     MOV    A,DPH
1a23: 34 00     ADDC   A,#0x0
1a25: f5 83     MOV    DPH,A
1a27: e5 f0     MOV    A,received_value
1a29: f0        MOVX   @pointer,A
1a2a: c3        CLR    CY
1a2b: 22        RET
...

I'm not sure how Ghidra should present the assembler code for these instructions considering that it receives a 16-bit value in the data pointer register (DPTR), but clearly this way is confusing, or just plain wrong.
FUN_CODE_0x19b6 Function Analysis

This function returns in the A register a character from the queue whose pointer is received in the DPTR register. If the queue is not empty, it clears the carry flag (CY) and returns. If the queue is empty, the function returns with the carry flag set.

This function is only called from the X1_INT function.

The complete FUN_CODE_0x19b6 function code is presented next, with comments added to explain its operation.

...
// When called, DPTR is 0x14a5
FUN_CODE_19b6(byte * pointer)

// Stores value at memory address 0x14a5 in DAT_INTMRM_5c
19b6: e0        MOVX   A,@pointer
19b7: f5 5c     MOV    DAT_INTMEM_5c,A

// If the content of memory address 0x14a6 is non-zero, it jumps to 0x19c0; otherwise, it sets the carry flag (CY=1) and returns
19b9: a3        INC    pointer
19ba: e0        MOVX   A,@pointer
19bb: b4 00 02  CJNE   A,#0x0,LAB_CODE_19c0
19be: d3        SETB   CY
19bf: 22        RET

LAB_CODE_19c0:
// Decrements value at memory address 0x14a6
19c0: 14        DEC    A
19c1: f0        MOVX   @pointer,A

// Gets the value at memory address 0x14a7, increments it by 1 and compares it with the value at DAT_INTMEM_5c (the original value at memory address 0x14a5)
// If they are not equal, jump to 0x19ca, otherwise load 0x00 in register A (circular queue)
19c2: a3        INC    pointer
19c3: e0        MOVX   A,@pointer
19c4: 04        INC    A
19c5: b5 5c 02  CJNE   A,DAT_INTMEM_5c,LAB_CODE_19ca
19c8: 74 00     MOV    A,#0x0

LAB_CODE_19ca:
// Loads at memory address 0x14a7, the value of A (either 0x00 or the original value at address 0x14a7 incremented by 1)
19ca: f0        MOVX   @pointer,A

// Loads into DPTR the value 0x14a9, added to the value at memory address 0x14a7
19cb: a3        INC    pointer
19cc: a3        INC    pointer
19cd: 25 82     ADD    A,DPL
19cf: f5 82     MOV    DPL,A
19d1: e5 83     MOV    A,DPH
19d3: 34 00     ADDC   A,#0x0
19d5: f5 83     MOV    DPH,A

// Loads into register A the byte at the memory address pointed to by DPTR (0x14a9 added to the value at address 0x14a7)
19d7: e0        MOVX   A,@pointer

// Clears the carry flag (CY=0) and returns
19d8: c3        CLR    CY
19d9: 22        RET
...

To understand how the RX and TX queues work it might be interesting to find out where they are initialized. This information is useful to know their size and how the information is stored with respect to the available space in each one of them..

UART Reception and Transmission Queues

Several methods of analysis can be followed in Ghidra to find out where queue initialization takes place.

Most likely, the initialization of the queues is done at the beginning of the code execution and to know where, it is a good idea to follow the program execution flow, starting from memory address 0x0000 (memory loaded in the program counter (PC) after a reset).

Another way is to look for read or write instructions at the memory addresses where the queues are located, e.g. MOV DPTR,0x1481 (opcode 90 14 81) and MOV DPTR,0x14a5 (opcode 90 14 a5) seem to be good candidates.

Another way is to check the memory address where each of the queues is located to see the cross references to that address, i.e. from where accesses are made to each of the queues.

The third option will be used. This allows us not only to find where the queues are initialized, but also other parts of the code where the queues are accessed. The memory address where the RX and TX queues are located and the cross references to these addresses (code from which these memory addresses are accessed) are presented next.

...
// RX queue at RAM address 0x1481 (EXTMEM:1481:)
DAT_EXTMEM_1481:       XREF[3]: FUN_CODE_18eb:18eb(*),
                                X1_INT:1960(*),
                                FUN_CODE_1a3a:1a3a(*)
1481:   ??
1482:   ??
...
// TX queue at RAM address 0x14a5 (EXTMEM:14a5:)
DAT_EXTMEM_14a5:       XREF[3]: FUN_CODE_18eb:18f3(*),
                                X1_INT:1960(*),
                                FUN_CODE_1a47:1a4d(*)
14a5:   ??
14a6:   ??
...

The RX queue appears to be 36 bytes long (0x24) since there seems to be no other data between the beginning of the RX queue and the beginning of the TX queue. It is very likely that both queues have the same length, which should be confirmed by analyzing the code of the functions that initialize them. Since a queue requires at least some sort of index to indicate the amount of free space, the actual usable space in the queue to store data is less than 36 bytes..

Looking at the cross references to the queues it can be seen that the functions that access the queues are: X1_INT (the ISR of X1 analyzed before), FUN_CODE_18eb, FUN_CODE_1a3a (only accesses the RX queue) and FUN_CODE_1a47 (only accesses the TX queue).

The FUN_CODE_18eb function initializes the RX and TX queues by initializing the values specifying the length and number of bytes occupied. It also configures the UART IC. The queues are initialized as follows: the first byte is loaded with 0x20 (the queue length), the second byte with 0x00 while the third and fourth bytes are loaded with 0x19.

The function FUN_CODE_1a3a gets a value from the RX queue (calling a function very similar to the already analyzed FUN_CODE_19b6 function). If the queue is not empty, it indicates that the value obtained from the queue is available at memory address 0x16c4, by setting bit 23.0. This function is called several times during execution, some addresses from where it is called are: 0x2cd3, 0x2cf5, 0x2d44, 0x2d84, etc.

The function FUN_CODE_1a47 obtains the value stored at memory address 0x16c5 and loads it into the TX queue (by calling a function very similar to the already analyzed FUN_CODE_1a06 function). If the value was successfully loaded into the queue it indicates this by setting bit 22.7. This function is called several times during execution, some of the addresses from where it is called are: 0x2f62, 0x2fd0, 0x30b5, 0x30f8, etc.

Analysis of functions FUN_CODE_18eb, FUN_CODE_1a3a and FUN_CODE_1a47 (click to show/hide)
FUN_CODE_18eb Function Analysis

The complete FUN_CODE_18eb function code is presented next, with comments added to explain its operation.

...
// Function initializing the RX and TX queues and the UART IC
FUN_CODE_18eb(void)

// Initializes the RX queue
18eb: 90 14 81  MOV    DPTR,#0x1481
18ee: 74 20     MOV    A,#0x20
18f0: 12 1a 2c  LCALL  FUN_CODE_1a2c

// Initializes the TX queue
18f3: 90 14 a5  MOV    DPTR,#0x14a5
18f6: 74 20     MOV    A,#0x20
18f8: 12 1a 2c  LCALL  FUN_CODE_1a2c

// UART initialization (multiple writes to U5 registers)
// Write 0x1a in the CR register: Resets the MR pointer (MR is pointed to MR1), disables TX and RX
18fb: 90 80 82  MOV    DPTR,#DAT_EXTMEM_8082
18fe: 74 1a     MOV    A,#0x1a
1900: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A
1901: 12 19 44  LCALL  FUN_CODE_1944

// Write 0x2a to CR register: Resets receiver, disables TX and RX
1904: 74 2a     MOV    A,#0x2a
1906: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A
1907: 12 19 44  LCALL  FUN_CODE_1944

// Writes 0x3a to CR register: Resets transmitter, disables TX and RX
190a: 74 3a     MOV    A,#0x3a
190c: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A
190d: 12 19 44  LCALL  FUN_CODE_1944

// Writes 0x4a to CR register: Resets error status, disables TX and RX
1910: 74 4a     MOV    A,#0x4a
1912: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A
1913: 12 19 44  LCALL  FUN_CODE_1944

// Writes 0x07 to register MR1: manual RxRTS, interrupt RxRDY, error mode = char, odd parity, 8 bits (after writing to MR1, MR register points to MR2)
1916: 90 80 80  MOV    DPTR,#DAT_EXTMEM_8080
1919: 74 07     MOV    A,#0x7
191b: f0        MOVX   @DPTR=>DAT_EXTMEM_8080,A
191c: 12 19 44  LCALL  FUN_CODE_1944

// Writes 0x07 to MR2 register: Normal mode, No TxRTS, No TxCTS, One stop bit
191f: 90 80 80  MOV    DPTR,#DAT_EXTMEM_8080
1922: 74 07     MOV    A,#0x7
1924: f0        MOVX   @DPTR=>DAT_EXTMEM_8080,A

// Writes 0x68 to ACR register: BRG set to 1, crystal/external clock, power down off, RTS active low
1925: 90 80 84  MOV    DPTR,#DAT_EXTMEM_8084
1928: 74 68     MOV    A,#0x68
192a: f0        MOVX   @DPTR=>DAT_EXTMEM_8084,A

// Writes 0xbb to CSR: TX and RX baud rate 9600
192b: 90 80 81  MOV    DPTR,#DAT_EXTMEM_8081
192e: 74 bb     MOV    A,#0xbb
1930: f0        MOVX   @DPTR=>DAT_EXTMEM_8081,A
1931: 12 19 44  LCALL  FUN_CODE_1944

// Writes 0x5 to IMR: RxRDY/FFULL and TxRDY interrupts
1934: 90 80 85  MOV    DPTR,#DAT_EXTMEM_8085
1937: 74 05     MOV    A,#0x5
1939: f0        MOVX   @DPTR=>DAT_EXTMEM_8085,A
193a: 12 19 44  LCALL  FUN_CODE_1944

// Writes 0xa5 to CR: Asserts RTS, enables TX and RX
193d: 90 80 82  MOV    DPTR,#DAT_EXTMEM_8082
1940: 74 a5     MOV    A,#0xa5
1942: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A
...
// Returns
1943: 22        RET
...

The UART uses a line rate of 9600 bps, with 8 data bits, one stop bit and one parity bit, this is often referred to as 9600-811.
The RTS signal is (probably) used to indicate when it is ready to receive data. The status of this signal must be managed manually by the software rather than automatically by the UART IC.
Two interrupts are enabled: RxRDY which indicates that a new character has been received, and TxRDY which indicates that the transmit FIFO is empty and a new transmission can be started

The function FUN_CODE_1a2c initialize the queues and its code is presented next, with comments added to explain its operation.

...
// Function that initializes the RX (called with A=0x20 and DPTR=0x1481) and TX (called with A=0x20 and DPTR=0x14a5) queues
FUN_CODE_1a2c(byte value, byte * pointer)

// Loads the value received at the address pointed to by the pointer and into the B register
1a2c: f5 f0     MOV    B,A
1a2e: f0        MOVX   @pointer,A

// Loads 0x00 at address pointed by: pointer+1
1a2f: a3        INC    pointer
1a30: e4        CLR    A
1a31: f0        MOVX   @pointer,A

// Loads the received value minus 1 at the address pointed by pointer+2
1a32: a3        INC    pointer
1a33: e5 f0     MOV    A,B
1a35: 14        DEC    A
1a36: f0        MOVX   @pointer,A

// Loads the received value minus 1 at the address pointed by pointer+3
1a37: a3        INC    pointer
1a38: f0        MOVX   @pointer,A

// Returns
1a39: 22        RET
...

The function FUN_CODE_1944 implements a delay of $\class{inlineFormula}{\rm{51,75\mu s}}$, resulting from executing the function at a frequency of 16MHz.

...
// Delay function (~52us)
FUN_CODE_1944(void)
1944:   MOV    R7,#0x20
LAB_CODE_19c0:
1946:   DJNZ   R7,LAB_CODE_1946
1948:   RET
...

To calculate the delay introduced by the execution of this function we have to add the machine cycles of each instruction and multiply them by the time each machine cycle takes.

The LCALL instruction takes 2 machine cycles, MOV takes 1 cycle, DJNZ takes 2 cycles multiplied by the value of R7 (0x20h = 32) and finally RET takes 2 cycles. This gives a total of 69 machine cycles. Since each machine cycle has 12 clock cycles, the machine cycle time is $\class{inlineFormula}{\rm{T_m = \frac{12}{16*10^{6}} = 0,75\mu s}}$, which gives a total execution time of $\class{inlineFormula}{\rm{delay = 51,75\mu s}}$.

FUN_CODE_1a3a Function Analysis

The function FUN_CODE_1a3a is presented next, with comments added to explain its operation.

...
FUN_CODE_1a3a(void)
// Loads 0x1481 in DPTR and calls the function FUN_CODE_198c
// FUN_CODE_198c returns in the A register the value received and in CY an indicator of whether a new value has been received
1a3a: 90 14 81  MOV    DPTR,#0x1481
1a3d: 12 19 8c  LCALL  FUN_CODE_198c

// Copies the received value to memory address 0x16c4, and the new value received flag to bit register 23.0
1a40: 92 18     MOV    23.0,CY
1a42: 90 16 c4  MOV    DPTR,#LAB_CODE_16c4
1a45: f0        MOVX   @DPTR=>DAT_EXTMEM_16c4,A
1a46: 22        RET
...

The FUN_CODE_198c function is similar to the FUN_CODE_1a06 function that writes a value to the UART RX queue. The main difference is that FUN_CODE_198c disables the X1 interrupt at startup and re-enables it before returning to the thread. This is done to avoid data corruption in the UART RX queue if, for example, an X1 interrupt occurs during the execution of this function.

The FUN_CODE_198c function verifies that the UART RX queue is not empty, if it is not empty it reads and returns a value from it (the last one). The value read is returned in the A register, and CY indicates whether a value has been read from the queue or not.

...
FUN_CODE_198c(byte * pointer)
198c: c2 aa     CLR    EX1
198e: e0        MOVX   A,@pointer
198f: f5 5c     MOV    DAT_INTMEM_5c,A
1991: a3        INC    pointer
1992: e0        MOVX   A,@pointer
1993: b4 00 04  CJNE   A,#0x0,LAB_CODE_199a
1996: d3        SETB   CY
1997: d2 aa     SETB   EX1
1999: 22        RET

LAB_CODE_199a:
199a: 14        DEC    A
199b: f0        MOVX   @pointer,A
199c: a3        INC    pointer
199d: e0        MOVX   A,@pointer
199e: 04        INC    A
199f: b5 5c 02  CJNE   A,DAT_INTMEM_5c,LAB_CODE_19a4
19a2: 74 00     MOV    A,#0x0

LAB_CODE_19a4:
19a4: f0        MOVX   @pointer,A
19a5: a3        INC    pointer
19a6: a3        INC    pointer
19a7: 25 82     ADD    A,DPL
19a9: f5 82     MOV    DPL,A
19ab: e5 83     MOV    A,DPH
19ad: 34 00     ADDC   A,#0x0
19af: f5 83     MOV    DPH,A
19b1: e0        MOVX   A,@pointer
19b2: c3        CLR    CY
19b3: d2 aa     SETB   EX1
19b5: 22        RET
...

FUN_CODE_1a47 Function Analysis

The function FUN_CODE_1a47 is presented next, with comments added to explain its operation.

...
FUN_CODE_1a47(void)
1a47: 90 16 c5  MOV    DPTR,#0x16c5
1a4a: e0        MOVX   A,@DPTR=>DAT_EXTMEM_16c5
1a4b: f5 f0     MOV    B,A
1a4d: 90 14 a5  MOV    DPTR,#0x14a5
1a50: 12 19 da  LCALL  FUN_CODE_19da
1a53: 92 17     MOV    22.7,CY
1a55: 22        RET
...

The FUN_CODE_19da function is quite similar to FUN_CODE_19b6 which sends a value stored in the TX queue using the UART interface. The FUN_CODE_19da function disables the X1 interrupt at startup and re-enables it before returning. This is done to avoid data corruption in the TX queue that may occur if an X1 interrupt occurs during the execution of this function.

The FUN_CODE_19da function verifies that the TX queue is not full, if it is not full it stores the received value in the queue. It returns in CY an indicator whether it was possible to store the value in the queue or not.

...
FUN_CODE_19da(byte * pointer, byte value)
19da: c2 aa     CLR    EX1
19dc: e0        MOVX   A,@pointer
19dd: f5 5c     MOV    DAT_INTMEM_5c,A
19df: a3        INC    pointer
19e0: e0        MOVX   A,@pointer
19e1: b5 5c 04  CJNE   A,DAT_INTMEM_5c,LAB_CODE_19e8
19e4: d3        SETB   CY
19e5: d2 aa     SETB   EX1
19e7: 22        RET

LAB_CODE_19e8:
19e8: 04        INC    A
19e9: f0        MOVX   @pointer,A
19ea: a3        INC    pointer
19eb: a3        INC    pointer
19ec: e0        MOVX   A,@pointer
19ed: 04        INC    A
19ee: b5 5c 02  CJNE   A,DAT_INTMEM_5c,LAB_CODE_19f3
19f1: 74 00     MOV    A,#0x0

LAB_CODE_19f3:
19f3: f0        MOVX   @pointer,A
19f4: a3        INC    pointer
19f5: 25 82     ADD    A,DPL
19f7: f5 82     MOV    DPL,A
19f9: e5 83     MOV    A,DPH
19fb: 34 00     ADDC   A,#0x0
19fd: f5 83     MOV    DPH,A
19ff: e5 f0     MOV    A,value
1a01: f0        MOVX   @pointer,A
1a02: c3        CLR    CY
1a03: d2 aa     SETB   EX1
1a05: 22        RET
...

FUN_CODE_1a5d Function Analysis

The FUN_CODE_1a5d function is called during initialization and its purpose is to test the hardware UART. It does not test the communication with the host machine, since a loop test is performed on the UART IC.

Analysis of FUN_CODE_1a5d Function (click to show/hide)

...
FUN_CODE_1a5d(void)
// Initializes R4 to 0x01
1a5d: 7c 01     MOV    R4,#0x1

// Puts the UART in local loop mode with a stop bit and calls the FUN_CODE_1944 function (delay)
1a5f: 90 80 80  MOV    DPTR,0x8080
1a62: 74 87     MOV    A,#0x87
1a64: f0        MOVX   @DPTR=>DAT_EXTMEM_8080,A
1a65: 12 19 44  LCALL  FUN_CODE_1944

// Disables all the UART interrupts and calls the FUN_CODE_1944 function (delay)
1a68: 90 80 85  MOV    DPTR,0x8085
1a6b: 74 00     MOV    A,#0x0
1a6d: f0        MOVX   @DPTR=>DAT_EXTMEM_8085,A
1a6e: 12 19 44  LCALL  FUN_CODE_1944

// Puts a high level in MPO, then enables TX and RX. The high level in the MPO is received by the host machine, probably some handshake
1a71: 90 80 82  MOV    DPTR,0x8082
1a74: 74 b5     MOV    A,#0xb5
1a76: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A

// Initializes register R7 (used as a loop counter)
1a77: 7f 00     MOV    R7,#0x0

LAB_CODE_1a79:
// Reads the UART status register (SR)
1a79: 90 80 81  MOV    DPTR,0x8081
1a7c: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8081
// Checks if the UART RX FIFO is empty (SR[0]==0). If it is empty, it jumps to 0x1a84
1a7d: 30 e0 04  JNB    ACC.0,LAB_CODE_1a84
// If the RX FIFO is not empty it reads RHR, decrements register R7 and jumps to 0x1a799
1a80: 90 80 83  MOV    DPTR,0x8083
1a83: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8083

LAB_CODE_1a84:
1a84: df f3     DJNZ   R7,LAB_CODE_1a79

// When it gets here the RX FIFO is empty. Transmit 0x55 and initialize register R7
1a86: 74 55     MOV    A,#0x55
1a88: 90 80 83  MOV    DPTR,0x8083
1a8b: f0        MOVX   @DPTR=>DAT_EXTMEM_8083,A
1a8c: 7f 00     MOV    R7,#0x0

LAB_CODE_1a8e:
// Reads the UART status register (SR)
1a8e: 90 80 81  MOV    DPTR,0x8081
1a91: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8081
// Check if the UART RX FIFO is not empty (SR[0]==1). If it is not empty jump to0x1a99
1a92: 20 e0 04  JB     ACC.0,LAB_CODE_1a99
// If the UART RX FIFO is empty decrement register R7, if R7 is not zero it jumps to 0x1a8e otherwise it continues
1a95: df f7     DJNZ   R7,LAB_CODE_1a8e
// If it gets here no character has been received during 255 receive checks. Jumps to 0x1abc
1a97: 80 23     SJMP   LAB_CODE_1abc

LAB_CODE_1a99:
// Reads the received character and compares it with 0x55 (the transmitted character). If they are not equal, it jumps to 0x1abc
1a99: 90 80 83  MOV    DPTR,0x8083
1a9c: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8083
1a9d: b4 55 1c  CJNE   A,#0x55,LAB_CODE_1abc
// If it gets here, character 0x55 was transmitted and received correctly. Transmit 0xaa (one's complement of 0x55) and initialize register R7
1aa0: 74 aa     MOV    A,#0xaa
1aa2: 90 80 83  MOV    DPTR,0x8083
1aa5: f0        MOVX   @DPTR=>DAT_EXTMEM_8083,A
1aa6: 7f 00     MOV    R7,#0x0

LAB_CODE_1aa8:
// Reads the UART status register (SR)
1aa8: 90 80 81  MOV    DPTR,0x8081
1aab: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8081
// Checks if the UART RX FIFO is not empty (SR[0]==1). If it is not empty it jumps to0x1ab3
1aac: 20 e0 04  JB     ACC.0,LAB_CODE_1ab3
// If the UART RX FIFO is empty it decrements register R7. If R7 is not zero it jumps to 0x1aa8, otherwise continues
1aaf: df f7     DJNZ   R7,LAB_CODE_1aa8
// If it gets here no character has been received during 255 receive checks. Jumps to 0x1abc
1ab1: 80 09     SJMP   LAB_CODE_1abc

LAB_CODE_1ab3:
// Reads the received character and compares it with 0xaa (the transmitted character). If they are not equal, it jumps to 0x1abc
1ab3: 90 80 83  MOV    DPTR,0x8083
1ab6: e0        MOVX   A,@DPTR=>DAT_EXTMEM_8083
1ab7: b4 aa 02  CJNE   A,#0xaa,LAB_CODE_1abc
// If it gets here both transmitted characters (0x55 and 0xaa) were transmitted and received correctly. Loads 0x00 in register R4
1aba: 7c 00     MOV    R4,#0x0

LAB_CODE_1abc:
// Puts the UART in Normal mode with one stop bit and calls the FUN_CODE_1944 function (delay)
1abc: 90 80 80  MOV    DPTR,0x8080
1abf: 74 07     MOV    A,#0x7
1ac1: f0        MOVX   @DPTR=>DAT_EXTMEM_8080,A
1ac2: 12 19 44  LCALL  FUN_CODE_1944
// Enables RxRDY and TxRDY UART interrupts and calls the FUN_CODE_1944 function (delay)
1ac5: 90 80 85  MOV    DPTR,0x8085
1ac8: 74 05     MOV    A,#0x5
1aca: f0        MOVX   @DPTR=>DAT_EXTMEM_8085,A
1acb: 12 19 44  LCALL  FUN_CODE_1944
// Puts a low level in MPO, then enables TX and RX. The low level in the MPO is received by the host machine, probably some handshake
1ace: 90 80 82  MOV    DPTR,0x8082
1ad1: 74 a5     MOV    A,#0xa5
1ad3: f0        MOVX   @DPTR=>DAT_EXTMEM_8082,A
Loads 0x00 into register R4 and return
1ad4: 7c 00     MOV    R4,#0x0
1ad6: 22        RET
...

It looks like this function tries to indicate if an error has occurred during the verification, by setting R4 with 0x01 at first and then changing its value to 0x00 if no error occurs. However just before returning it always loads 0x00 in register R4, as if they finally regretted performing the verification. Perhaps they did not come to a conclusion on what to do if an error occurs?

The test pattern used to perform the loop test (0xaa and 0x55) is typically used to test a RAM. By converting them to binary (0xaa=10101010b and 0x55=010101010101) it is easy to see that by writing and comparing the value read back, each bit of a memory address is tested. Repeating this procedure for each memory address verifies its integrity.

Briot Tracer Interrupt Vector Table

In some cases, information on how a device works can be obtained from the Interrupt Vector Table (IVT). The Briot Tracer IVT is presented next, indicating for each interrupt source whether it is used and at which address the corresponding interrupt routine is available.

It can be confirmed by looking at the IVT that in fact the function X1_INT is the ISR for External Interrupt X1 by comparing the address of the function (0x1949) and the CALL address of the ISR (also 0x1949).

Source Name ROM
Address
Used Routine
Address
External Interrupt 0 X0 0x03 N 0x0194
Timer 0 Overflow T0 0x0B Y 0x00FD
External Interrupt 1 X1 0x13 Y 0x1949
Timer 1 Overflow T1 0x1B Y 0x0102
SIO0 (UART) S0 0x23 N 0x0194
SIO1 (I2C) S1 0x2B Y 0x2B (local)
T2 Capture 0 CT0 0x33 Y 0x0136
T2 Capture 1 CT1 0x3B N 0x0194
T2 Capture 2 CT2 0x43 N 0x0194
T2 Capture 3 CT3 0x4B N 0x0194
ADC Completion ADC 0x53 N 0x0194
T2 Compare 0 CM0 0x5B N 0x0194
T2 Compare 1 CM1 0x63 N 0x0194
T2 Compare 2 CM2 0x6B N 0x0194
T2 Overflow T2 0x73 N 0x0194

The first thing to notice is that for all unused interrupts there is a jump to address 0x0194 where the interrupts are disabled and the microcontroller enters an infinite loop:

...
LAB_CODE_0194:
// Disables interrupts then enters an infinite loop at address 0x196
0194: c2 af     CL     EA
0196: 80 fe     SJMP   LAB_CODE_0196
...

This is a common practice that provides a safe landing place in case of error, which blocks the operation of the machine.

What happens if the error occurs when a motor is in motion and the motor's stop signal is triggered by an interruption? That motor will never stop, which in the worst case can overheat and break the motor controller and the motor itself. An overheated motor can cause a fire. Doesn't seem to be a safe place anymore, does it?
A better solution would have been to send a stop signal to all motors (whether moving or not) before disabling the interrupts.
General comments on Briot Tracer Interrupts

Interrupt X0 is not used, but the output of U10D is connected to pin 32 (P3.2/INT0) of the microcontroller (Ref.: 4, 6B), via R51 (Ref.: 4, 3A), so it is likely that the code checks the logic level of P3.2 somewhere.

The S0 (UART) interrupt is not used. This is confirmed by looking at the schematic as pins 24 and 25 (P3.0/RxD and P3.1/TxD respectively) of the microcontroller (Ref.: 4, 6B) are connected to two uninstalled resistors: R31 and R32 ((Ref.: 10, 1C). Therefore the UART hardware of the microcontroller is not used.

Interrupts T0 and T1 are used. Following pin 28 (P3.4/T0) of the microcontroller (Ref.: 4, 6B) it can be seen that it is connected to pin 13 of U16D (Ref.: 12, 1B) which is the result of inverting the Yellow Wire output signal of the encoder, and to pin 5 (RST) of U8 (Ref.: 12, 2A), a Quadrature Decoder. T0 is likely to be used to trigger timer 0 when the Quadrature Decoder (U8) is reset by the Rho encoder. Pin 29 (P3.5/T1) of the microcontroller (Ref.: 4, 6B) is connected to pin 19 of U3 (Ref.: 5, 2B) which is the Phase1 signal of the Z-axis motor controller. Therefore, it seems that T1 is used to change the direction of motion of the Z-axis motor.

Interrupt X1 is used and was previously analyzed.

The CT0 interrupt is used. Timer 2 of the 80C552 is very flexible, but this implies a rather convoluted configuration that can lead to very different uses. As this could lead to a very long explanation with extensive firmware and circuit analysis, it will not be discussed here. Perhaps in some future post.

Finally there are a bunch of MOV R7,A instructions between the interrupt vectors. These are the result of disassembling 0xFF which is the default value of the memory when there is no instruction (it is also the default value of an empty EPROM). These erroneously decoded instructions can be cleared by selecting them and pressing the [C] key (keyboard shortcut for Clear Code Bytes) on the keyboard. This way the view of the disassembled code is more clear.

I hope this post can help to reverse engineer the firmware of a device in order to understand how it works. It was not the goal to explain in detail how Ghidra works, but rather to give a walkthrough of the way of thinking when analyzing the firmware and the first steps to take.

No comments: