Peeking the Option ROM From a BASIC Program

From Bitchin100 DocGarden
Jump to navigationJump to search

The question came up on Facebook as to whether you can read the contents of an Option ROM from BASIC. The short answer is not really, or at least, not without some work.

Why? The 8085 has a 65536 byte address space. That space is apportioned between ROM at the bottom and RAM at the top. The 32K Option ROM, when enabled, occupies the same address space as the 32K main ROM. When the Option ROM is running, the BASIC ROM isn’t, and vice versa,, because all their jumps, internal data references and interrupt handlers are located to execute in the same space: the lower 32,768 bytes of the 8085 CPU’s address space. The upper 32K of the address space is permanently devoted to the RAM.

OK, well, BASIC has an OUT I/O routine and BASIC has a PEEK routine. To switch to the option ROM, you OUT 1 to I/O register 224. To switch back you OUT 0 to I/O register 224. So knowing just enough to get ourselves in trouble, a naive approach to PEEKing the option ROM would be:

 OUT 224,1:?PEEK(0): OUT224,0

to print the first byte of the Option ROM, and because I'm almost clever, I of course switch back to the Main ROM at the end of it like a good BASIC citizen.

Workey? No. No workey.

The problem is that the BASIC interpreter will abruptly disappear with OUT 224,1. Boom. Poof. Gone. So the BASIC interpreter loop that executes your BASIC program line by line, token by token, will never get to anything after the first OUT... it will instead lock-up or, perhaps, crash spectacularly.

Because of that, I don't think there's any way to PEEK the option ROM from pure BASIC. But rather than let that hurdle stop us, instead, we need to contrive some machine code with no ROM dependencies and embed it in our BASIC program. This code will run completely from RAM, which always stays mapped in, so our code can swap the standard ROM out and the Option ROM in without causing a crash.

So we’re writing a machine language (ML) subroutine. We can interface ML with BASIC. BASIC gives us a way to invoke a ML subroutine, the “CALL” function. We will design a routine we can CALL, passing along the address of the Option ROM location we want, and get back the byte value that is at that offset in the Option ROM.

How does CALL work? The CALL statement takes three parameters: a subroutine entry point, an 8-bit parameter, and a 16-bit parameter. The subroutine is called, and the parameters are passed in the A and HL registers, respectively. At the end of the subroutine, CALL’ed code must effectuate return to the BASIC interpreter, leaving the stack as it found it.


Clearly, we need to pass the address to PEEK. The 16-bit parameter could be good for that. We don’t have any use for an 8-bit input to our subroutine. But what about passing back the byte "PEEKed"? That's an "out" parameter. It sure would have been great if Microsoft had made CALL return a 8 or 16-bit value.

But it is not so, and the BASIC ROM is literally etched in stone for many of our machines.

Oh well. No point in crying into our cereal about it... we just need to put our thinking caps back on. What we can do instead is pass a parameter array of integers as our 16-bit value. That is, instead of passing one value, we pass a value that refers (a reference) to a modifiable object.

A single array reference can convey an arbitrary number of values and pointers. And since we're passing a reference to the array object, the variables at each index of the array can be modified by the ML routine, supporting "in" and "out" parameters.

DIM P%(2)
' P%(0): return value
P%(1) = 1000 : ' Address in Option ROM to peek

So assuming our subroutine is at address PK% (we’ll get to that later), an invocation of our subroutine to find out what’s at address 1000 of the Option ROM would look like:

P%(1) = 1000: CALL PK%, 0,VARPTR(P%(0)): ?P%(0)

So now that we know what we want the routine to do and how we will invoke it, let's code it.

For each line I will show the assembly instruction, as well as the hex and what I call Funny Hex for each instruction. Funny Hex is like hex but ABCDEF are represented by :;<=>? , for simplified conversion. Any character in Funny Hex, you can subtract 48 (ASCII encoding of zero) and it will give you a value between 0 and 15

First I need to resolve a level of indirection on the value of HL to load the “address” parameter into the HL register.

; HL starts as the address of the integer parameter array
; Load HL with param[1] since it's the address to peek
  XCHG  ; $EB  >;
  INX D ; $13  13  ; uh oh, need a listable alternative
  INX D ; $13  13  ; again
  LHLI  ; $ED  >=

That was my first cut at that... and it would work, but $13 is an unprintable ASCII control character (^S / DC3/ XOFF). We have an opportunity here to craft code that can be embedded directly into a BASIC string, execute in place, and have no LIST or EDIT issues. That makes for much shorter BASIC code, with no "poke loop" overhead and no need to run from ALT-LCD. To do that, we need all instructions (and operands!) we use to be ASCII 32 (printable space) or higher.

So to avoid problems we instead:

  PUSH H ; $E5 >5
  INX H  ; $23 23
  INX H  ; $23 23
  XCHG   ; $EB >;
  LHLI   ; $ED >=
; we're going to set the 224 ($E0) register to 1 to switch in the option rom
  XRA A  ; $AF :?  
  INR A  ; $3C 3<

; disable interrupts so the hardware won't interrupt us. I’m not sure if this is actually necessary, since Option ROMs have their own interrupt handlers.
  DI     ; $F3 ?3

; Switch in the option ROM
  OUT 224 ; $D3 $E0 / =3 >0  

; L <- option ROM byte referenced by HL
  MOV L,M ; $6E 6>

; Switch back to the standard ROM (same I/O register as we used to enable, but with A=0 instead of A=1)
  XRA A   ; $AF :?
  OUT 224 ; $D3 $E0 / =3 >0

; restore interrupts
  EI      ; $FB ?;

; load the peeked value into HL (high byte is always zero)
  XRA A   ; $AF :?
  MOV H,A ; $67 67

; Address of parameter array (references the first parameter) is on stack, restore it
  POP D   ; $D1 =1

; pass the peeked value as the return value
  SHLI    ; $D9 =9

; return to the BASIC interpreter
  RET     ; $C9 <9

The full assembly:

 PUSH H ; $E5 >5
 INX H  ; $23 23
 INX H  ; $23 23
 XCHG   ; $EB >;
 LHLI   ; $ED >=
 XRA A  ; $AF :?  
 INR A  ; $3C 3<
 DI     ; $F3 ?3
 OUT 224 ; $D3 $E0 / =3 >0  
 MOV L,M ; $6E 6>
 XRA A   ; $AF :?
 OUT 224 ; $D3 $E0 / =3 >0
 EI      ; $FB ?;
 XRA A   ; $AF :?
 MOV H,A ; $67 67
 POP D   ; $D1 =1
 SHLI    ; $D9 =9
 RET     ; $C9 <9

That works out to 20 bytes of machine code.

Here’s a 7-bit ASCII encoded BASIC program that modifies an embedded string to hold the subroutine. Run it once, and it will do the work of converting the Funny Hex into a safe binary string (note I had to split the Funny Hex string to avoid a Mediawiki crash).

100 DEFINT A-K:DEFSTR S-Z
105 U="####################"
110 S=">52323>;>=:?3<"+"?3=3>06>:?=3>0?;:?67=1=9<9#"
112 K=256%
120 A=VARPTR(S)+1:A=PEEK(A)+K*(PEEK(A+1)+K*(PEEK(A+1)>127)):T="":C=0
125 D=VARPTR(U)+1:D=PEEK(D)+K*(PEEK(D+1)+K*(PEEK(D+1)>127))
126 E=D
130 C=PEEK(A)-48:IFC=-13GOTO160:ELSEC=16*C+PEEK(A+1)-48:T=T+CHR$(C):A=A+2:POKEE,C:E=E+1:GOTO 130
160 REM

Run it and LIST it, and line 105 will now look like this:

OptROMPeek.png


Very compact… although we used a POKE loop to produce it (to avoid having to type graphic characters), you don’t need to keep the POKEr. Think of it as bootstrap, or a stage rocket. So now you can remove lines 110, 120, 126, 130, retaining the execute in place string ML and the CALL address computation.

Now to try it… add some test code:

170 DIMP%(2)
180 P%(1)=3
190 CALL D,0,VARPTR(P%(0))
200 PRINTP%(0)

This code declares a parameter array, sets the Option ROM address to peek to 3, and does the CALL. The PEEK’d value is returned in P%(0).

The full BASIC sample code:

100 DEFINT A-K:DEFSTR S-Z
105 U="####################"
110 S=">52323>;>=:?3<"+"?3=3>06>:?=3>0?;:?67=1=9<9#":K=256%
120 A=VARPTR(S)+1:A=PEEK(A)+K*(PEEK(A+1)+K*(PEEK(A+1)>127)):T="":C=0
125 D=VARPTR(U)+1:D=PEEK(D)+K*(PEEK(D+1)+K*(PEEK(D+1)>127)):E=D
130 C=PEEK(A)-48:IFC=-13GOTO160:ELSEC=16*C+PEEK(A+1)-48:T=T+CHR$(C):A=A+2:POKEE,C:E=E+1:GOTO 130
160 REM
170 DIMP%(2)
180 P%(1)=3
190 CALLD,0,VARPTR(P%(0))
200 PRINTP%(0)

And the made code in Unicode, I guess:

100 DEFINT A-K:DEFSTR S-Z
105 U="å##ëí¯<óÓàn¯Óàû¯gÑÙÉ"
112 K=256%
125 D=VARPTR(U)+1:D=PEEK(D)+K*(PEEK(D+1)+K*(PEEK(D+1)>127))
160 REM
170 DIMP%(2)
180 P%(1)=3
190 CALLD,0,VARPTR(P%(0))
200 PRINTP%(0)

Note that this code does seem to paste right into CloudT, CLOAD and run. There (to Unicode) and Back Again.

Other possible approaches would be to copy a whole 16-bit word from the Option ROM rather than just a byte at a time. Or even better, you could pass a source offset, a buffer address and size to copy from the Option ROM in one fell swoop. Or maybe you want the output already in hex, ready to display or dump out the serial port. I leave these enhancements as an exercise for the reader.

Also you may want to try building safe ML strings by hand. It’s possible! You will want to make a table for yourself of all the instruction opcodes, including a cheat sheet of how to type graphical characters. You can have a look at the 8085_Reference for the full instruction set.