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.
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.
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).
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 |
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.
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:.
|
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.
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.
...
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 |
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:
Post a Comment