# MSI-88 Startup Boot Trace — Annotated Raw trace: `msi88_startup_trace.txt` (13,781 lines) Disassembly: `msi88.asm` (CDP1802ACE system ROM, 8 KB) The MSI/88e is a CDP1802-based handheld data terminal. Its firmware is a stack-based bytecode interpreter. Boot is a short native 1802 preamble that initializes the interpreter registers, checks the mode switch and the program-loaded signature, then hands control to a *bytecode* self-test program at `0x1E2B`. With no program loaded the self-test builds an `ERR 2` + RAM-size message, drives the MAN2815 display/transmit loop, and finally halts with `IDL` (power-down) at `0x03DA`. > Note: this trace ends just *before* the final `IDL` at `0x07D3` — it captures > the complete self-test through the last display-refresh burst. The terminal > then powers down (see "Phase 10" below). Register roles (from `msi88.asm` header): | Reg | Role | |-----|------| | R0 | Program counter (`P=R0` after `DIS`) | | R1 | Native subroutine library base (`0x08BC`) | | R2 | Return / hardware stack (`0x809C`, grows down) | | R3 | Dispatch target (`SEP R3`) | | R6 | Port-2 shadow / scratch (`0x8100`) | | R7 | Low dispatch-table page (`0x0B00`) | | R8 | High dispatch-table page (`0x0A00`) | | RA | Pointer table (`0x8206+`) | | RB | Handler / working pointer | | RD | Pseudo program counter (R13) | | RE | Data stack pointer (R14, `0x8100`, grows down) | | RF | Interpreter re-entry (`0x0011`, `SEP RF`) | --- ## 1802 Native Code Boot Sequence ### Phase 1: Reset & Register Initialization (0x0000–0x1F8E) ``` ; === RESET VECTOR — DIS sets P=R0, X=R0, IE=0 === 0000: DIS P=R0, X=R0 ; === Blank the MAN2815 display (segment + digit-select) === 0002: OUT 1 [00] ; segment LOW byte = 0 0004: OUT 2 [00] ; digit select = 0 0006: OUT 4 [00] ; segment HIGH byte = 0 ; === Set up RF = 0x0011 (interpreter re-entry) === 0008: GHI R0 D=00 0009: PHI RF RF=0040 ; RF.1 = 0x00 000A: LDI 11 D=11 000C: PLO RF RF=0011 ; RF = 0x0011 (SEP RF re-enters the fetch loop) 000D: LBR 1F60 ; jump to init routine ; === 1F60: INIT — initialise all interpreter registers === 1F60: LDI 80 D=80 1F62: PHI R2 R2=80B2 1F63: LDI 9C D=9C 1F65: PLO R2 R2=809C ; R2 = return stack pointer 1F66: LDI 81 D=81 1F68: PHI RE RE=81AA ; RE.1 = 0x81 (data stack page) 1F69: PHI R6 R6=8117 ; R6.1 = 0x81 (port-2 shadow / scratch) 1F6A: LDI 00 D=00 1F6C: PLO RE RE=8100 ; RE = data stack top 1F6D: PLO R6 R6=8100 ; R6 = port-2 shadow ; === Clear scratch bytes 0x8100–0x8102 === 1F6E: STR R6 M(8100)=00 1F6F: INC R6 R6=8101 1F70: STR R6 M(8101)=00 1F71: INC R6 R6=8102 1F72: STR R6 M(8102)=00 ; === RA = 0x8206 pointer table; load RC from M(8206):M(8207) === 1F73: LDI 82 D=82 1F75: PHI RA RA=82D0 1F76: LDI 06 D=06 1F78: PLO RA RA=8206 1F79: LDA RA D=M(8206)=00 1F7A: PHI RC RC=008E ; RC.1 = M(8206) 1F7B: LDA RA D=M(8207)=00 1F7C: PLO RC RC=0000 ; RC = 0x0000 (no warm-boot entry) ; === R5 = 0x8209 (page table), R8.1 = 0x0A, R7.1 = 0x0B (dispatch pages) === 1F7D: GHI RA D=82 1F7E: PHI R5 R5=8277 ; R5.1 = 0x82 1F7F: LDI 09 D=09 1F81: PLO R5 R5=8209 1F82: LDI 0A D=0A 1F84: PHI R8 R8=0A55 ; high dispatch-table page 0x0A00 1F85: LDI 0B D=0B 1F87: PHI R7 R7=0B57 ; low dispatch-table page 0x0B00 ; === R1 = 0x08BC (native subroutine library base) === 1F88: LDI 08 D=08 1F8A: PHI R1 R1=08D4 1F8B: LDI BC D=BC 1F8D: PLO R1 R1=08BC ; === RET restores X=RE, P=R0 from the packed byte 0xE0 at 0x1F8F === 1F8E: RET P=R0, X=RE ; M(1F8F)=0xE0 → X=E(RE), P=0(R0); IE=1 ``` The `RET` at `0x1F8E` is the classic 1802 idiom: `RET` loads `X,P` from `M(RX)`. Here `X=R0` and `R0` now points at the data byte `0xE0` just past the `RET`, so the nibbles encode `X=RE`, `P=R0` in one instruction. ### Phase 2: Mode Switch, Recorder Handshake & Battery Check (0x1F90–0x1FC3) ``` ; === RB = 0x8000 (RAM flags) === 1F90: GHI R2 D=80 1F91: PHI RB RB=8052 1F92: GLO RE D=00 1F93: PLO RB RB=8000 1F94: LDA RB D=M(8000)=00 ; read buffer flag 1F95: BNZ 98 ; not taken (0) 1F97: STR RB M(8001)=00 1F98: GLO RB D=01 1F99: INC RB RB=8002 1F9A: STR RB M(8002)=01 ; buffer-active flag = 1 1F9B: DEC RB RB=8001 1F9C: DEC RE RE=80FF ; RE → scratch byte 0x80FF ; === Mode switch check: INP 2 low 2 bits === 1F9D: INP 2 D=M(80FF)=01 ; mode switches = 0x01 (power on) 1F9E: ANI 03 D=01 1FA0: BZ B4 ; skip recorder handshake if (sw & 3)==0 — not taken ; === Recorder/transmitter handshake (poll INP 6 busy, then OUT 6) === 1FA2: INP 6 D=M(80FF)=50 ; recorder busy line 1FA3: SHL D=A0, DF=0 ; busy bit (7) → DF 1FA4: BDF A2 ; loop while busy 1FA6: LDI 05 D=05 1FA8: STR RE M(80FF)=05 1FA9: OUT 6 [05] ; recorder control = 0x05 (motor/load) 1FAA: DEC RE RE=80FF 1FAB: INP 6 D=M(80FF)=50 1FAC: SHL D=A0, DF=0 1FAD: BDF AB ; loop while busy 1FAF: LDI 01 D=01 1FB1: STR RE M(80FF)=01 1FB2: OUT 6 [01] ; recorder control = 0x01 (idle/stop) 1FB3: DEC RE RE=80FF ; === INP 1 bit 3 = battery/voltage flag === 1FB4: INP 1 D=M(80FF)=47 ; keyboard/status port (idle = 0x47) 1FB5: ANI 08 D=00 ; bit 3 1FB7: BZ C3 ; bit 3 clear → skip alternate boot path ; (1FB9–1FC2 alternate path: RD=0x1EEF monitor entry — skipped here) ``` ### Phase 3: Program-Loaded Signature Check (0x1FC3–0x1FCD) ``` ; === Check RAM 0x8103:0x8104 for the 0x5A5B "program loaded" signature === 1FC3: INC R6 R6=8103 1FC4: LDA R6 D=M(8103)=00 1FC5: XRI 5A D=5A ; 0x00 ^ 0x5A = 0x5A 1FC7: STR RE M(80FF)=5A 1FC8: LDA R6 D=M(8104)=00 1FC9: XRI 5B D=5B ; 0x00 ^ 0x5B = 0x5B 1FCB: OR D=5B ; 5A | 5B = 5B (≠ 0 → no program) 1FCC: INC RE RE=8100 1FCD: BZ F4 ; signature matched → warm boot (not taken here) ; No program loaded → fall through to the self-test ``` If `M(0x8103):M(0x8104)` held `0x5A:0x5B`, both `XRI` results would be `0`, the `OR` would be `0`, and `BZ 1FF4` would vector to the loaded program's entry. Here both bytes are `0`, so the signature fails and the terminal runs the self-test / idle program instead. ### Phase 4: Self-Test Program Setup (0x1FCF–0x1FF3) ``` ; === Clear buffer flags, set the self-test bytecode entry RD=0x1E2B === 1FCF: GHI RF D=00 1FD0: STR RB M(8001)=00 1FD1: DEC RB RB=8000 1FD2: STR RB M(8000)=00 1FD3: LDI 1E D=1E 1FD5: PHI RD RD=1E6A 1FD6: LDI 2B D=2B 1FD8: PLO RD RD=1E2B ; RD = self-test bytecode entry ; === Set page-table byte M(8209)=0x0B, M(8212)=0x1D === 1FD9: LDI 0B D=0B 1FDB: STR R5 M(8209)=0B 1FDC: LDI 12 D=12 1FDE: PLO R5 R5=8212 1FDF: LDI 1D D=1D 1FE1: STR R5 M(8212)=1D ; === Font-base pointer M(8105):M(8106) = 0x04AC === 1FE2: LDI 04 D=04 1FE4: STR R6 M(8105)=04 1FE5: INC R6 R6=8106 1FE6: LDI AC D=AC 1FE8: STR R6 M(8106)=AC ; === Message pointer M(81D1):M(81D2) = 0x0693 ("ERR" string) === 1FE9: LDI D1 D=D1 1FEB: PLO R6 R6=81D1 1FEC: LDI 06 D=06 1FEE: STR R6 M(81D1)=06 1FEF: INC R6 R6=81D2 1FF0: LDI 93 D=93 1FF2: STR R6 M(81D2)=93 ; === Enter the bytecode interpreter (SEP RF → 0x0011 fetch loop) === 1FF3: SEP RF ``` ### Phase 5: Interpreter Fetch Loop — first token (0x0011) The interpreter is a `SEP`-based token dispatcher. Entry `0x0011` fetches one token byte from `M(RD)`, `RD++`; `SHL` moves bit 7 into `DF`; tokens `>= 0x80` dispatch through the high table (`R8`/`0x0A00`), tokens `< 0x80` through the low table (`R7`/`0x0B00`). An odd table target is a *data-subroutine* `CALL` (push `RD` on `R2`, re-enter at the odd address); an even target is native `SEP R3`. ``` 0011: LDA RD D=M(1E2B)=E8 ; first token = 0xE8 (OUT 2, const=40) 0012: SHL D=D0, DF=1 ; bit 7 set → high table 0013: BNF 2A ; not taken 0015: PLO R8 R8=0AD0 ; R8 = 0x0A00 + 0xE8 = 0x0AE8 → 0x0AD0 (×2 index) 0016: LDA R8 D=M(0AD0)=1E 0017: PHI R3 R3=1E5E 0018: LDN R8 D=M(0AD1)=23 0019: PLO R3 R3=1E23 ; R3 = handler 0x1E23 (odd → CALL) 001A: ANI 01 D=01 001C: BZ 10 ; not taken (data-subroutine CALL) 001E: DEC R2 R2=809B ; push return address 0x1E2C onto R2 001F: GLO RD D=2C 0020: STR R2 M(809B)=2C 0021: GHI RD D=1E 0022: DEC R2 R2=809A 0023: STR R2 M(809A)=1E 0024: GLO R3 D=23 0025: PLO RD RD=1E23 ; RD = handler entry 0026: GHI R3 D=1E 0027: PHI RD RD=1E23 0028: BR 11 ; re-enter fetch loop at the handler ``` `0xE8` is `OUT 2, const=40` — it sets bit 6 of the port-2 shadow, the MAN2815 digit-select strobe base. Its handler `0x1E23` is `PUSH 0040; OUT 2, TOS.0; RET`, after which `RD` pops back to `0x1E2C`. --- ## Pseudo (Bytecode) Self-Test Program The bytecode stream at `0x1E2B` is the operator/monitor program executed when no user program is loaded: RAM test → build `ERR 2` + RAM-size message → display refresh / transmit bursts → `IDLE` power-down. ### Phase 6: RAM Boundary Scan (0x1E2C–0x1E43) `CKRAM` (opcode `0x1D`) probes a start address: it write-verifies `0x00` and `0xFF` at each byte, restores it, and advances, stopping (and re-pushing) at the first *non-writable* byte. The self-test calls it with addresses stepping up in `0x400` blocks, starting from `0x0000`, to find where RAM begins. ``` ; Start with address 0, store to the working pointer M(81D7) 1E2C PUSH 0000 ; DS: 0000 1E2D DUP ; DS: 0000, 0000 1E2E PUSH 81D7 1E2F LD [TOS], NOS ; M(81D7)=0000 (RAM probe pointer) 1E30 DUP 1E31 CKRAM TOS ; probe from 0000 → still 0000 (ROM, non-writable) 1E32 OVER 1E33 OVER 1E34 ISEQ TOS, NOS ; 0000 == 0000 → 1 (boundary unchanged) 1E35 JZ 1E40 ; not taken (value=1) 1E37 DROP 1E38 PUSH 0400 ; step = 0x400 1E3B ADD TOS, NOS ; next address += 0x400 1E3C DUP 1E3D PUSH 0000 1E3E JP 1E43 1E43 JZ 1E2E ; value=0 → loop again ; ... repeats for 0400, 0800, 0C00, 1000, 1400, 1800, 1C00 (all ROM) ... ; ... CKRAM(2000) returns A800 — the first non-writable byte above RAM ... ; Loop exit iteration (address 0x2000 → boundary 0xA800): 1E2F LD [TOS], NOS ; M(81D7)=2000 1E30 DUP 1E31 CKRAM TOS ; DS: A800, 2000 ← probe hit RAM, walked to 0xA800 1E32 OVER 1E33 OVER 1E34 ISEQ TOS, NOS ; A800 == 2000 → 0 (boundary moved) 1E35 JZ 1E40 ; taken 1E40 SWAP 1E41 DROP 1E42 PUSH 0001 1E43 JZ 1E2E ; value=1 → fall through (loop ends) ; DS: A800 (top of writable memory) ``` The probe finds writable memory extending up to `0xA800`, so the last usable RAM byte is `0xA7FF`. ### Phase 7: RAM Size Computation (0x1E45–0x1E77) The firmware now turns the boundary into a display value. In this trace it computes `0x8800` (34,816 bytes = 34 KB). The arithmetic splits neatly into the two board variants documented in the hardware: `0x2800` (10 KB) above `0x8000` plus `0x6000` (24 KB) below it. ``` 1E45 DUP ; DS: A800, A800 1E46 SUB TOS, 1 ; A7FF = last RAM byte 1E47 DUP 1E48 PUSH 81D9 1E49 LD [TOS], NOS ; M(81D9)=A7FF 1E4A ISNEG TOS ; A7FF negative (bit15 set) → 1 1E4B JZ 1E60 ; not taken 1E4D PUSH 81D7 1E4E LD TOS, [TOS] ; M(81D7)=2000 1E4F ISNEG TOS ; 2000 not negative → 0 1E50 JZ 1E55 ; taken 1E55 PUSH 7FFF 1E58 PUSH 81D9 1E59 LD [TOS], NOS ; M(81D9)=7FFF 1E5A PUSH 7FFF 1E5D SWAP ; DS: A800, 7FFF 1E5E JP 1E67 ; Compute (top-of-RAM − 0x8000) + (0x8000 − base): 1E67 SUB TOS, 1 ; A7FF 1E68 DUP 1E69 PUSH 8133 1E6A LD [TOS], NOS ; M(8133)=A7FF 1E6B DUP 1E6C PUSH 8131 1E6D LD [TOS], NOS ; M(8131)=A7FF 1E6E PUSH 7FFF 1E71 SUB TOS, NOS ; A7FF − 7FFF = 2800 (RAM above 0x8000) 1E72 SWAP ; DS: 7FFF, 2800 1E73 PUSH 81D7 1E74 LD TOS, [TOS] ; M(81D7)=2000 1E75 SUB TOS, 1 ; 1FFF 1E76 SUB TOS, NOS ; 7FFF − 1FFF = 6000 (RAM below 0x8000) 1E77 ADD TOS, NOS ; 6000 + 2800 = 8800 (total RAM size) 1E78 PUSH 32 ; 0x32 = ASCII '2' (status code) 1E7A CALL 07DB ; display value + message ``` ### Phase 8: Build Display Message — `CALL 07DB` (0x07DB–0x07F9) `07DB` is `LDVAL TOS` + a 4-digit hex conversion, then `CALL 07CF`. `LDVAL` (opcode `0x0C`) clears the display, loads the standard message into the `0x810B` buffer, and writes the status byte. The `0x32` ('2') + the `HASH` pointer table selects the `"ERR"` message (base `0x0693`), so the buffer becomes `ERR 2` with the RAM-size digits appended at offset 12. ``` 07DB LDVAL TOS ; load "ERR" msg, write '2' at buffer offset 4 07DC PUSH 04 ; 4 digits 07DE PUSHR 0 ; loop frame: counter=0, limit=4 ; --- hex-digit loop: convert 0x8800 → ASCII "8800" right-to-left --- 07DF PUSH 0010 ; divisor 16 07E0 DIVR NOS, TOS.0 ; value / 16 → [quot, rem] 07E1 SWAP 07E2 PUSH 30 ; '0' 07E4 ADD TOS, NOS ; rem + '0' = ASCII digit 07E5 DUP 07E6 PUSH 39 ; '9' 07E8 ISGT NOS, TOS ; > '9'? (hex A–F) 07E9 JZ 07EE ; no → skip +7 correction 07EB PUSH 07 07ED ADD TOS, NOS ; +7 ('A'–'F') 07EE PUSH 810B ; display buffer base 07EF PUSH 0C ; offset 12 07F1 ADD TOS, NOS ; 0x8117 07F2 PUSH R ; loop counter 07F3 SUB TOS, NOS ; 8117 − counter (right-to-left) 07F4 LD [TOS], NOS.0 ; store ASCII digit 07F5 LOOP 07DF ; repeat ×4 ; Iteration 1: 8800/16 = 880 r 0 → '0' at 8117 ; Iteration 2: 880/16 = 88 r 0 → '0' at 8116 ; Iteration 3: 88/16 = 8 r 8 → '8' at 8115 ; Iteration 4: 8/16 = 0 r 8 → '8' at 8114 ; Buffer (0x810B+): "ERR 2 8800" (8114-8117 = "8800") 07F7 DROP 07F8 CALL 07CF ; display refresh + transmit, then IDLE 07F9 RET ``` ### Phase 9: Display Refresh & Transmit — `CALL 07CF` (0x07CF–0x07D4) `07CF` performs four `CALL 05F7` refreshes, then executes `IDLE` (power-down). ``` 07CF CALL 05F7 ; refresh 1 07D0 CALL 05F7 ; refresh 2 07D1 CALL 05F7 ; refresh 3 07D2 CALL 05F7 ; refresh 4 07D3 IDLE ; terminal power-down (opcode 0xEC) 07D4 RET ``` `05F7` is the display/transmit delay routine: **7 iterations** of { **200-iteration** busy-wait delay + **16 × SEND** }: ``` 05F7 PUSH 07 ; outer limit = 7 05F9 PUSHR 0 ; outer frame: counter=0, limit=7 05FA PUSH C8 ; delay count = 200 05FC PUSHR 0 ; inner frame: counter=0, limit=200 ; --- 200-iteration busy-wait delay (self-loop) --- 05FD LOOP 05FD ; first iteration ; ... 199 more iterations (counter 0x0002..0x00C8) ... 05FD LOOP 05FD ; last iteration — frame pops 05FF PUSH 0010 ; 16 SENDs 0600 CALL 05F3 ; OUT 2,40 ; 16×SEND ; OUTM 2,40 0601 LOOP 05FA ; repeat outer loop ×7 0603 RET ``` `05F3` sets the digit strobe, emits a 16-`SEND` burst, then clears the strobe: ``` 05F3 OUT 2, 40 ; set port-2 bit 6 (digit strobe) 05F4 CALL 05ED ; 16 × SEND 05F5 OUTM 2, 40 ; clear port-2 bit 6 05F6 RET ``` `05ED` is the 16-`SEND` burst: ``` 05ED PUSHR 0 ; frame: counter=0, limit=16 (TOS was 0x0010) 05EE SEND ; first SEND — refresh MAN2815 + scan keyboard 05EF DROP 05F0 LOOP 05EE ; repeat ; ... 14 more SEND/DROP iterations ... 05EE SEND ; last SEND 05EF DROP 05F0 LOOP 05EE ; final loop — frame pops 05F2 RET ``` Each `SEND` (opcode `0xEF`, native handler `0x05AC`) reads the mode switch, and if active outputs the `0x810B` display buffer byte-by-byte on **port 7** with an **INP 6** busy poll, clears `M(0x8002)`, then falls into the native display refresh `J0408` — which multiplexes the 16-digit MAN2815 (segments on OUT 1/4, digit select on OUT 2) and scans the keyboard (rows on OUT 2, columns on INP 1). The trace ends on the return of the 4th `CALL 05F7`: ``` ; ... (7 × 16 SENDs for refresh 4 omitted) ... 05F2 RET ->05F5 05F6 RET ->0601 0603 RET ->07D3 ; ← trace ends here ``` ### Phase 10: IDLE / Power-Down (0x07D3 → 0x03DA) Just after the trace ends, the interpreter fetches `0xEC` (`IDLE`) at `0x07D3`. Its native handler is at `0x03D0`: ``` ; PSEUDO INSTRUCTION: IDLE (opcode 0xEC) — terminal halt / power-down 03D0: SEX R3 03D1: OUT 2 [00] ; clear digit select 03D3: DIS ; IE = 0 (no interrupt/DMA source) 03D5: GHI RF 03D6: PHI R0 ; R0 = 0x00xx (halt PC) 03D7: PLO R0 03D8: OUT 2 [80] ; power-down strobe 03DA: IDL ; halt — display frozen until power-cycle ``` This is the terminal **powering down to save battery** when no program is loaded. The display freezes showing the last self-test message (`ERR 2 8800`), and the unit wakes only on a power-cycle. --- ## Summary | Phase | Address Range | Purpose | |-------|--------------|---------| | 1 | 0x0000–0x1F8E | Reset, blank display, init interpreter registers (R2/R6/RE/RA/R5/R7/R8/R1/RF), `RET`→P=R0,X=RE | | 2 | 0x1F90–0x1FC3 | Buffer flags, mode-switch check, recorder handshake (OUT 6), battery flag (INP 1 bit 3) | | 3 | 0x1FC3–0x1FCD | Program-loaded signature check (`0x5A5B` @ RAM 0x8103:8104) — fails → self-test | | 4 | 0x1FCF–0x1FF3 | Set self-test entry RD=0x1E2B, font base 0x04AC, message ptr 0x0693 ("ERR"), `SEP RF` | | 5 | 0x0011 | Interpreter fetch loop — first token `0xE8` (`OUT 2,40`) | | 6 | 1E2C–1E43 | RAM boundary scan via `CKRAM` (0x400 steps, finds top at 0xA800) | | 7 | 1E45–1E77 | RAM size computation → 0x8800 | | 8 | 07DB–07F9 | Build `ERR 2` + "8800" message in display buffer (4-digit hex loop) | | 9 | 07CF–0603 | Display refresh / transmit: 4 × (7 × (200-delay + 16 SENDs)) | | 10 | 07D3→03DA | `IDLE` — `DIS` + `IDL` power-down (battery save) |