Home/GPS/Trimble/4000/4000SSi Deep Dive

Trimble 4000SSi ROM Monitor

I originally thought the ROM monitor was entirely a function of the PPU; that is when the receiver was in ‘PPU Active’ mode, the CPU was acquiescing the bus to the PPU MCU, and the PPU was accessing memory directly.

But then I realized that the chip selects that were being set by loader.exe when the monitor first started were of course internal 68332 registers, and that those chip selects only applied to addresses generated by the 68332, so the ROM monitor must be running on the 68332. But where was the code coming from? The 68332 has no ROM attached to it, only battery backed RAM, and when the battery dies, the monitor still works.

I knew it had to be in RAM, but I couldn’t find anything in the RAM address space I knew mapped to the RAM on the board. I went back to the 68332 datasheet, and learned that there is 2k of RAM inside the 68332, the ‘TPURAM’, which can be used as normal RAM. Like regular memory, there is a register that sets the base address of this RAM. I probed this register via the monitor, and found that the TPURAM was living at 0x80000, and there appeared to be machine code in the first part of this block. I knew that the monitor read out the ‘hard coded’ receiver details from this area of memory, so I had a feeling that this space is somehow being populated by the PPU.

I disassembled the block, and it did indeed look promising, and after a bit of code review, it became clear this was indeed the ROM monitor code. The entire monitor fits in 562 bytes of memory, with another 259 bytes used for buffering the monitor commands. It turns out there are 10 monitor commands. I found the commands I knew about, 0x80 to read memory, 0x82 to write memory, 0x86 to change the baud rate and 0x88 to exit monitor mode. I was disappointed to find that a bunch of commands were basically ‘pings’; 4 of the commands produce a short predictable output every time, but these commands may have different functionality on later receiver models.

The only other interesting command is 0x89, which takes a 32 bit address as 4 bytes, and jumps to the address. Probably the most interesting monitor command of them all. See below for the disassembly.

How is the monitor code getting into the TPU in the first place?

I’m not entirely sure. Is the PPU sitting on the bus and acting as a slow memory device? Maybe the PPU is talking to the 68332 via the Background Debugging Mode pins? Maybe the PPU is using DMA To access the TPURAM directly? The 68332 documentation is unclear on bus arbitration. The address pins are described as output-only, which means they would be tri-stated during DMA, making all memory inside the MCU inaccessible. The address pins being output-only isn’t entirely true, though, as the minimal documentation of the “factory only” slave mode lets on that in this mode the internal in-memory register RAM is accessible by external accesses to the bus. Maybe the PPU commanders the external bus, giving it access to the system SRAM. Then it could place the monitor code there, along with some instructions to copy the monitor from SRAM to TPURAM.

ROM Monitor Assembly

        org $80000

begin:
        movea.l #$FFFA00,a0     ; SIM base address
        move.b  #0,$21(a0)      ; SYPCR - disable watchdogs, monitors
        move.w  #$7F84,4(a0)    ; SYNCR - fiddle with clock
        movea.l #$FFFC00,a0     ; QSM base address
        move.b  #$11,$17(a0)    ; DDRQS - PORT QS Data Direction Register
                                ; QS0 = output
                                ; QS4 = output
                                ; all others inputs
        move.b  #$A,$16(a0)     ; PQSPAR - PORT QS Pin Assignment Register
                                ; QS1 = MOSI
                                ; QS3 = PCS0/SS
                                ; all others GPIO
        move.w  #$37,8(a0)      ; SCCR0 - SCI Control Register 0
                                ; 0x37 = 9600 baud
        move.w  #$100C,$A(a0)   ; SCCR1 - SCI Control Register 1
                                ; bit 2 = 1 = Receiver Enable
                                ; bit 3 = 1 = Transmitter Enable
                                ; bit 13 = 1 = TXD is open-drain output
        lea     0,sp
        adda.l  #$800,sp        ; stick the stack in regular RAM

loop_top:       

        lea     byte_238,a1
        movea.l a1,a2
        movea.l a1,a3
        movea.l a1,a4
        moveq   #0,d1
        moveq   #0,d2
        moveq   #$7F,d3
        lea     state,a5
        move.b  #0,(a5)

get_byte:

        move.l  $C(a0),d0
        btst    #$16,d0         ; RDRF -  Receive Data Register Full Flag
        beq.s   get_byte        ; wait until a byte is available
        move.l  d0,d5
        andi.l  #$30000,d5      ;  check for framing or parity errors
        bne.s   loop_top
        andi.l  #$FF,d0
        tst.b   state
        bne.w   in_command      ; bytes received after stx
                                ; byte 0 = stx
                                ; byte 1 = status
                                ; byte 2 = command
                                ; byte 3 = length
        cmpi.b  #2,d0           ; stx?
        bne.w   check_ack       ; Nope. ENQ/ACK Echo?
        lea state,a5            
        move.b  #1,(a5)
        bra.s   get_byte

check_ack:  
        cmpi.b  #5,d0           ; ENQ/ACK Echo?
        bne.w   check_bel       ; Nope. 0x07/0x08 Echo?
        bsr.w   sub_send_ack
        bra.s   get_byte

check_bel:
        cmpi.b  #7,d0           ; 0x07/0x08 Echo
        bne.s   get_byte
        bsr.w   sub_send_backspace
        bra.s   get_byte

in_command:
        addq.w  #1,d1           
        cmpi.w  #3,d1
        bne.w   rec_cmd_byte
        lea length,a5           ; save length
        move.w  d0,(a5)
        move.l  d0,d3
        addq.l  #4,d3           ; save number of bytes expected in d3
        bra.w   update_checksum

rec_cmd_byte:

        cmpi.w  #2,d1
        bne.w   rec_msg_byte
        move.l  d0,d4           ; save command in d4
        bra.w   update_checksum

rec_msg_byte:

        cmp.w   d3,d1           ; all expected bytes received?
        beq.w   check_checksum
        bgt.w   rec_etx_byte    ; etx

update_checksum:

        add.b   d0,d2
        cmpi.w  #4,d1
        blt.w   still_in_header
        move.b  d0,(a2)+

still_in_header:

        bra.w   get_byte

check_checksum:
        cmp.b   d0,d2
        beq.w   get_byte        ; receive the ETX
        bra.w   loop_top        ; abort

rec_etx_byte:

        cmpi.b  #3,d0           ; etx
        beq.w   process_command
        bra.w   loop_top

jump_table:             
        jmp cmd_80              ; 0x80 - write memory
        jmp loop_top            ; 0x81 - do nothing
        jmp cmd_82              ; 0x82 - read memory
        jmp cmd_83              ; 0x83 - ack ping
        jmp cmd_84              ; 0x84 - replies with 0x94 packet containing 4 zeros
        jmp cmd_85              ; 0x85 - replies with 0x95 packet containing 8 zeros
        jmp cmd_86              ; 0x86 - set baud rate
        jmp cmd_87              ; 0x87 - bs ping
        jmp cmd_88              ; 0x88 - exit monitor
        jmp cmd_89              ; 0x89 - jump

process_command:

        subi.w  #$80,d4 ; 'Ä'
        cmpi.b  #9,d4
        bgt.w   loop_top
        jmp jump_table(pc,d4.l*4)
        jmp (a5)                ; ??? Seems like an orphan instruction

cmd_87:                         ; bs ping

        bsr.w   sub_send_backspace
        bra.w   loop_top

cmd_80:                         ; write memory

        bsr.w   sub_send_ack        ; write memory
        moveq   #0,d0
        move.w  length,d0       ;  length
        subi.w  #5,d0
        move.l  (a4)+,d5
        swap    d5
        movea.l d5,a6

cmd_80_loop:

        move.b  (a4)+,(a6)+
        dbf d0,cmd_80_loop
        bra.w   loop_top

cmd_83:                         ; ack ping
        bsr.w   sub_send_ack
        bra.w   loop_top

cmd_89:                         ; jump to address

        bsr.w   sub_send_ack
        movea.l (a4),a4
        jmp (a4)

cmd_84:                         ; replies with 0x94 packet containing 4 zeros
        lea $23A,a3
        move.b  #$94,(a3)+      ; reply with 0x94 packet
        move.b  #4,(a3)+
        move.l  #0,(a3)+        ; containing 4 zero bytes
        bsr.w   sub_send_reply
        bra.w   loop_top

cmd_85:                         ; replies with 0x95 packet containing 9 zeros
        lea $23A,a3
        move.b  #$95,(a3)+      ; reply with a 0x95 packet
        moveq   #9,d0
        move.b  d0,(a3)+
        subq.w  #1,d0

cmd_85_loop:

        move.b  #0,(a3)+        ; containing 9 zero bytes
        dbf d0,cmd_85_loop
        bsr.w   sub_send_reply
        bra.w   loop_top


sub_send_ack:

        btst    #8,$C(a0)
        beq.s   sub_send_ack
        moveq   #6,d0
        move.w  d0,$E(a0)
        rts

sub_send_backspace:

        btst    #8,$C(a0)       ; Transmit Data Register Empty Flag
        beq.s   sub_send_backspace  ; loop until previous transmit is done
        moveq   #8,d0
        move.w  d0,$E(a0)       ; send 0x08
        rts

cmd_86:                         ; set baud rate

        bsr.s   sub_send_ack    ; set baud rate
        moveq   #0,d1
        move.w  byte_238,d1
        move.w  d1,8(a0)        ; SCCR0
        bra.w   loop_top

cmd_88:                         ; exit monitor

        reset
        move.b  #$11,$15(a0)    ; PORTQS
                                ; QS0 = 1
                                ; QS4 = 1
                                ; I'm guessing this tells the PPU to take over, because the receiver reboots after this command has been run
        bra.w   loop_top


cmd_82:                         ; read memory
        movea.l byte_238,a6
        moveq   #0,d3
        move.b  byte_23C,d3
        move.w  d3,d1
        addq.w  #4,d1
        lea $23A,a3
        move.b  #$92,(a3)+      ; packet type
        move.b  d1,(a3)+        ; packet length
        move.l  a6,(a3)+        ; address
        subi.w  #1,d3

cmd_82_loop:

        move.b  (a6)+,(a3)+     ; copy bytes in
        dbf d3,cmd_82_loop
        bsr.w   sub_send_reply  ; send stuff
        bra.w   loop_top


sub_send_reply:

        movea.l a1,a4
        move.b  #2,(a4)+
        move.b  #0,(a4)
        moveq   #0,d2

calc_checksum:

        add.b   (a4)+,d2        ; calculate checksum
        cmpa.l  a4,a3
        bgt.s   calc_checksum
        move.b  d2,(a3)+        ; checksum
        move.b  #3,(a3)+        ; etx
        movea.l a1,a4

send_byte:

        btst    #8,$C(a0)
        beq.s   send_byte
        move.b  (a4)+,d0
        move.w  d0,$E(a0)
        cmpa.l  a4,a3
        bgt.s   send_byte
        rts


byte_238:   dc.b 4,0            
byte_23C:   dc.b 252,0
length:     dc.w 0
state:      dc.b 0