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