Part 26
Using machine code
Subjects covered...
USR with numeric argument
This section is written for those who understand Z80 machine code i.e.
the set of instructions that the Z80 processor chip users. If you do
not, but would like to, there are plenty of books about it. You should
get one called something along the lines of... 'Z80 machine code (or
assembly language) for the absolute beginner', and if it mentions the
'+3' or other computers in the ZX Spectrum range, so much the better.
Machine code programs are normally written in assembly language,
which, although cryptic, is not too difficult to understand with
practice. You can see the assembly language instructions in part 28 of
this chapter. However, to run them on the +3 you need to code the
program into a sequence of bytes - then called machine code. This
translation is usually done by the computer itself using a program
called an assembler. There is no assembler built in to the +3, but you
will be able to buy one on disk or tape. Failing that, you will have
to do the translation yourself, provided that the program is not too
long.
Let's take as an example the program...
ld bc, 99
ret
...which loads the BC register pair with 99. This translates into the
four machine code bytes 1, 99, 0 (for ld bc, 99) and 201 (for RET).
(If you look up codes 1 and 201 in part 28 of this chapter, you will
find that corresponds to ld bc, NN - where NN stands for any two-byte
number, and 201 corresponds to ret.)
When you have got your machine code program, the next step is to get
into the computer - (an assembler would probably do this
automatically). You need to decide whereabouts in memory to locate it
- the best thing is to make extra space for it between the BASIC area
and the user-defined graphics.
If you type...
CLEAR 65267
...this will give you a space of 100 (for good measure) bytes starting
at address 65268.
To put in the machine code program, you would run a BASIC program
something like...
10 LET a=65268
20 READ n: POKE a,n
30 LET a=a+1: GO TO 20
40 DATA 1,99,0,201
(This will stop with the report E Out of DATA when it has filled in
the four bytes you specified.)
To run the machine code, you use the function USR - but this time with
a numeric argument, i.e. the starting address. Its result is the value
of the BC register on return from the machine code program, so if you
type...
PRINT USR 65268
...you will get the answer 99.
The return address to BASIC is 'stacked' in the usual way, so return
is by a Z80 ret instruction. You should not use the IY and I registers
in a machine code routine that expects to use the BASIC interrupt
mechanism. If you are writing a program that might eventually run on
an older Spectrum (up to and including the +2), you should not load I
with values between 40h and 7Fh (even if you never use IM 2). Values
between C0h and FFh for I should also be avoided if contended memory
(i.e. RAM 4 to 7) is to be paged in between C000h and FFFFh. This is due
to an interaction between the video controller and the Z80 refresh
mechanism, and can cause otherwise inexplicable crashes, screen
corruption or other undesirable effects. This, you should only vector
IM 2 interrupts to between 8000h and BFFFh, unless you are very
confident of your memory mapping (or you are only going to run your
program on the +3 where this problem does not exist).
The system variable at 5CB0h (23728) was documented on previous models
of the Spectrum as 'Not used'. It is now used on the +3 as an NMI jump
vector. If an NMI occurs, this address is checked. If it contains a 0,
then no action is taken. However, for any other (non-zero) value, a
jump will be made to the address given by this variable. NMIs must not
occur while the disk system is active.
There are a number of standard pitfalls when programming a banked
system such as the +3 from machine code. If you are experiencing
problems, check that your stack is not being paged out during
interrupts, and that your interrupt routine is always where you expect
it to be (it is advisable to disable interrupts during paging
operations). It is also recommended that you keep a copy of the
current bank register setting in unpaged RAM somewhere as the ports
are write-only. BASIC and the editor use the system variables BANKM
and BANK678 for 7FFDh and 1FFDh respectively.
If you call +3DOS routines, remember that interrupts should be enabled
upon entry to the routines. Remember also that the stack must be below
BFE0h (49120) and above 4000h (16384), and that there must be at least
50 words of stack space available.
You can save your machine code program easily enough with (for
example)...
SAVE "name" CODE 65268,4
On the face of it, there is no way of saving the program so that when
loaded it automatically runs itself; however, you can get around this
by using the short BASIC program...
10 LOAD "name" CODE 65268,4
20 PRINT USR 65268
...which should also be saved (as a separate program) using the
command (for example)...
SAVE "loader" LINE 10
Then you may run the machine code from BASIC using the single
command...
LOAD "loader"
...which loads and automatically runs the BASIC program which in turn
loads and runs the machine code.
Calling +3DOS from BASIC
When BASIC's USR function is used, the code it references is entered
with the memory configured as illustrated below (left), i.e. the ROM
switched in at the bottom of memory in the address range
(000h...3FFFh) is ROM 3 (the 48 BASIC ROM). The RAM page at the top of
memory is page 0 and the machine stack resides in this area (unless
the CLEAR command has been used to reduce it to somewhere below
C000h). As explained in part 27 of this chapter (which describes the
+3DOS routines), DOS can only be called with RAM page 7 switched in at
the top of memory, the stack held somewhere in that range
4000h...BFE0h, and ROM 2 (the DOS ROM) switched in at the bottom of
memory (000h...3FFFh). This configuration is illustrated below
(right).
In BASIC (using DOS)
(after USR)
+---------+ +---------+
| Page 0 | <-SP | Page 7 |
C000h +---------+ +---------+
| Page 2 | | Page 2 | <- SP
8000h +---------+ +---------+
| Page 5 | | Page 5 |
4000h +---------+ +---------+
| ROM 3 | | ROM 2 |
0000h +---------+ +---------+
Consequently, it will be necessary to switch both ROM and RAM, and
move the stack before and after calling one of the entries in the DOS
jump table. The following very simple example shows one way of
achieving the desired set up in order to call DOS CATALOG.
If BASIC's CLEAR command has been used so that the BASIC stack is
below BFE0h (49120), then it is not necessary to move the stack.
However, we have done so in the following example to demonstrate the
technique when this is not the case.
A simple example to call DOS CATALOG...
org 7000h
mystak equ 9FFFh ;arbitrary value picked to be below BFE0h and above 4000h
staksto equ 9000h ;somewhere to put BASIC's stack pointer
bankm equ 585Ch ;system variable that holds the last value output to 7FFDh
port1 equ 7FFDh ;address of ROM/RAM switching port in I/O map
catbuff equ 8000h ;somewhere for DOS to put its catalog
dos_catalog equ 011Eh ;the DOS routine to call
demo:
di ;unwise to switch RAM/ROM without disabling interrupts
ld (staksto),sp ;save BASIC's stack pointer
ld bc,port1 ;the horizontal ROM switch/RAM switch I/O address
ld a,(bankm) ;system variable that holds current switch state
res 4,a ;move right to left in horizontal ROM switch (3 to 2)
or 7 ;switch in RAM page 7
ld (bankm),a ;must keep system variable up to date (very important)
out (c),a ;make the switch
ld sp,mystak ;make sure stack is above 4000h and below BFE0h
ei ;interrupts can now be enabled
;
;The above will have switched in the DOS ROM and RAM page 7. The stack has also
;been located in a "safe" position for calling DOS
;
;The following is the code to set up and call DOS CATALOG. This is where your
;own code would be placed.
;
ld hl,catbuff ;somewhere for DOS to put the catalog
ld de,catbuff+1 ;
ld bc,1024 ;maximum is actually 64x13+13 = 845
ld (hl),0
ldir ;make sure at least first entry is zeroised
ld b,64 ;the number of entries in the buffer
ld c,1 ;include system files in the catalog
ld de,catbuff ;the location to be filled with the disk catalog
ld hl,stardstar ;the file name ("*.*")
call dos_catalog ;call the DOS entry
push af ;save flags and possible error number returned by DOS
pop hl
ld (dosret),hl ;put it where it can be seen from BASIC
ld c,b ;move number of files in catalog to low byte of BC
ld b,0 ;this will be returned in BASIC by the USR function
;
;If the above worked, then BC holds number of files in catalog, the "catbuff"
;will be filled with the alphanumerically sorted catalog and the carry flag but
;in "dosret" will be set. This will be peeked from BASIC to check if all went
;well.
;
;Having made the call to DOS, it is now necessary to undo the ROM and RAM
;switch and put BASIC's stack back to where it was on entry. The following
;will achieve this.
di ;about to ROM/RAM switch so be careful
push bc ;save number of files
ld bc,port1 ;I/O address of horizontal ROM/RAM switch
ld a,(bankm) ;get current switch state
set 4,a ;move left to right (ROM 2 to ROM 3)
and F8h ;also want RAM page 0
ld (bankm),a ;update the system variable (very important)
out (c),a ;make the switch
pop bc ;get back the saved number of files in catalog
ld sp,(staksto) ;put BASIC's stack back
ret ;return to BASIC, value in BC is returned to USR
stardstar:
defb "*.*",FFh ;the file name, must be terminated with FFh
dosret:
defw 0 ;a variable to be peeked from BASIC to see if it worked
As some of you may not have an assembler available, the following is a
BASIC program that pokes the above code into memory, calls it, and
then uses the value returned by the USR function and the contents of
'dosret' to print a very simple catalog of the disk.
10 LET sum=0
20 FOR i=28672 TO 28758
30 READ n
40 POKE i,n : LET sum=sum+n
50 NEXT i
60 IF sum <> 9387 THEN PRINT "Error in DATA" : STOP
70 LET x= USR 28672
80 IF INT ( PEEK (28757)/2)= PEEK (28757)/2 THEN PRINT "Disk Error ";
PEEK (28758): STOP
90 IF x=1 THEN PRINT "No file found": STOP
100 FOR i=0 TO x-2
110 FOR j=0 TO 10
120 PRINT CHR$ ( PEEK (32781+i*13+j));
130 NEXT j
140 PRINT
150 NEXT i
160 DATA 243,237,115,0,144,1,253,127,58,92,91,203,167,246,7,50,92,91,237,
121,49,255,159,251
170 DATA 33,0,128,17,1,128,1,0,4,54,0,237,176,6,64,14,1,17,0,128,33,81,112,
205,30,1,245,225,34,85,112,72,6,0
180 DATA 243,197,1,253,127,58,92,91,203,231,230,248,50,92,91,237,121,193,
237,123,0,144,201
190 DATA 42,46,42,255,0,0
The addresses picked for the above code and its data areas are
completely arbitrary. However, it is a good idea to keep things in the
central 32K wherever possible so as not to run into the pitfall of
accidentally switching out a vital variable or piece of code.
If interrupts are to be enabled (as is the case in the above example),
it is imperative that the system is kept up to date about the latest
ROM switch. This mean that the user must make the BANK678 system
variable reflect the last value output to the port at 1FFDh. As shown
by the above example, the general technique is to take a copy of the
variable in A, set/reset the relevant its, update the system variable
then make the switch with an OUT instruction. Interrupts must be
disabled while the system variable does not reflect the current state
of the port. The port at 1FFDh doesn't just control the ROM switch, so
setting the variable to absolute values would be very unwise. Using
AND/OR with a bit mask or SET/RES instructions is the preferred method
of updating the variable.
Just as BANK678 reflects the last value output to 1FFDh, BANKM should
also be kept up to date with the last value output to 7FFDh. Again, it
is unwise to use absolute values, as the port is used for other
purposes. For example, the bottom 3 bits of the port are used to
select the RAM page that is switched into the memory area
C000h...FFFFh (this is also shown in the above example). naturally,
when more than one bit is to be set/reset, a bit mask used with OR/AND
is the more efficient method. note that RAM paging was described in
the section entitled 'Memory management' in part 24 of this chapter.
The above was a very simple example of calling DOS routines. The
following shows one or two extra techniques that you may find useful.
However, if you are not already familiar with assembler programming,
it might be better to skip this example.
Although part 20 of this chapter suggested that the opening menu's
Loader option first looks for a file called * and the one called DISK
before trying to load the first file from tape - this isn't exactly
the whole story. The first operation actually tries to load a
bootstrap sector from the disk in drive A. The sector on side 0, track
0, sector 1 will be used as a loader (bootstrap) if the system finds
that the 9 bit checksum of the sector is 3. The following program
ensures that the checksum of 512 bytes conforms to this requirement,
then writes the information to the disk in the correct position. Once
a disk has been modified in this way, the Loader option can be used to
automatically load and run the disk. Alternatively, the BASIC command
LOAD "*" can be used.
This example was developed using the M80 on a CP/M based machine - so
the method to ensure that the code is assembled relative to the
correct address might be different from that used by your own
assembler.
;
;Simple example program to write a boot sector to the disk in drive A:.
;
;by Cliff Lawson
;copyright (c) AMSTRAD plc. 1987
;
.z80 ;ignore this if not using M80
bank1 equ 07FFDh ;"horizontal" and RAM switch port
bankm equ 05B5Ch ;associated system variable
bank2 equ 01FFDh ;"vertical" switch port
bank678 equ 05B67h ;associated system variable
select equ 01601h ;BASIC routine to open stream
dos_ref_xdpb equ 0151h ;
dd_write_sector equ 0166h ;see part 27 of this chapter
dd_login equ 0175h ;
org 0
.phase 07000h
;
;(This allows M80 to generate a .COM file that has addresses relative to 7000h.
;Assemble with "M80 = prog" and link with "L80 /p:0,/d:0,prog,prog/n:p/y/e"
;This can be headed with COPY...TO SPECTRUM FORMAT and loaded with
;LOAD...CODE 28672.
;
start:
ld (olstak),sp ;save BASIC's stack pointer
ld sp,mystak ;put stack below switched RAM pages
push iy ;save IY on stack for the moment
ld a,"A" ;drive A:
ld iy,dos_ref_xdpb ;make IX point to XDPB A: (necessary for
call dodos ;calling DD routines)
ld c,0 ;log in disk in unit 0 so that writing sectors
push ix ;wont say "disk has been changed"
ld iy,dd_login
call dodos
pop ix
ld hl,bootsector
ld bc,512 ;going to checksum 512 bytes of sector
xor a
ld (bootsector+15),a ;reset checksum for starters
ld e,a ;E will hold 8 bit sum
ckloop:
ld a,e
add a,(hl) ;this loop makes 8 bit checksum of 512 bytes
ld e,a ;sector in E
inc hl
dec bc
ld a,b
or c
jr nz,ckloop
ld a,e ;A now has 8 bit checksum of the sector
cpl ;ones complement (+1 will give negative value)
add a,4 ;add 3 to make sum = 3 + 1 to make twos complement
ld (bootsector+15),a ;will make bytes checksum to 3 mod 256
ld b,0 ;page 0 at C000h
ld c,0 ;unit 0
ld d,0 ;track 0
ld e,0 ;sector 1 (0 because of logical/physical trans.)
ld hl,bootsector ;address of info. to write as boot sector
ld iy,dd_write_sector
call dodos ;actually write sector to disk
pop iy ;put IY back to BASIC can reference its system
;variables
ld sp,(olstak) ;put original stack back
ret ;return to USR call in BASIC
dodos:
;
;IY holds the address of the DOS routine to be run. All other registers are
;passed intact to the DOS routine and are returned from it.
;
;Stack is somewhere in central 32K (conforming to DOS requirements), so save AF
;and BC will not be switched out.
;
push af
push bc ;temp save registers while switching
ld a,(bankm) ;RAM/ROM switching system variable
or 7 ;want RAM page 7
res 4,a ;and DOS ROM
ld bc,bank1 ;port used for horiz ROM switch and RAM paging
di
ld (bankm),a ;keep system variables up to date
out (c),a ;RAM page 7 to top and DOS ROM
ei
pop bc
pop af
call jumptoit ;go sub routine address in IY
push af ;return from JP (IY) will be to here
push bc
ld a,(bankm)
and 0F8h ;reset bits for page 0
set 4,a ;switch to ROM 3 (48 BASIC)
ld bc,bank1
di
ld (bankm),a
out (c),a ;switch back to RAM page 0 and 48 BASIC
ei
pop bc
pop af
ret
jumptoit:
jp (iy) ;standard way to CALL (IY), by calling this jump
olstak:
dw 0 ;somewhere to put BASIC's stack pointer
ds 100
mystak: ;enough stack to meet +3DOS requirements
bootsector:
.dephase ;these are M80 pseudo ops. your assembler
.phase 0FE00h ;may use something different
;
;Bootstrap will load into page 3 at address FE00h. The code will be entered at
;FE10h.
;
;Before it is written to track 0, sector 1, the bootstrap has byte 15
;changed so that it will checksum to 3 mod 256.
;
;Boot will switch the memory so that the 48 BASIC ROM is at the bottom.
;Next up is page 5 - the screen, then page 2, and the top will keep
;page 3, as it would be unwise to switch out the bootstrap. BASIC
;routines can be called with any RAM page switched in at the top, but
;the stack shouldn't be in the TSTACK area.
bootstart:
;
;The bootstrap sector contains the 16 bytes disk specification at the start.
;The following values are for a AMSTRAD PCW range CF2/Spectrum +3 format disk.
;
db 0 ;+3 format
db 0 ;single sided
db 40 ;40 tracks per side
db 9 ;9 sectors per track
db 2 ;log2(512)-7 = sector size
db 1 ;1 reserved track
db 3 ;blocks
db 2 ;2 directory blocks
db 02Ah ;gap length (r/w)
db 052h ;page length (format)
ds 5,0 ;5 reserved bytes
cksum: db 0 ;checksum must = 3 mod 256 for the sector
;
;The bootstrap will be entered here with the 4, 7, 6, 3 RAM pages switched in.
;To print something, we need 48 BASIC in at the bottom, page 5 (the screen and
;system variables) next up. The next page will be 0, and the top will be kept
;as page 3 because it still contains the bootstrap and stack (stack is FE00h on
;entry).
;
di
ld a,(bankm)
and 0F8h
or 3 ;RAM page 3 (as it holds bootstrap)
set 4,a ;right-hand ROMs
ld bc,bank1
ld (bankm),a
out (c),a ;switch RAM and horizontal ROM
ld a,(bank678)
and 0F8h
or 4 ;set bit 2 and reset bit 0 (gives ROM 3)
ld bc,bank2
ld (bank678),a
out (c),a ;should now have R3,5,2,3
ld a,2
call select ;BASIC ROM routine to open stream (A)
ld hl,message
call print ;print a message
eloop:
;
;end with an endless loop changing the border. This is where your own code
;for a game or operating system would go.
;
ld a,r ;a not-very-random random number
out (0feh),a ;switch the border
jr eloop ;and loop
print:
ld a,(hl) ;this just loops printing characters
cp 0FFh ;until if finds FFh
ret z
rst 10h ;with 48K ROM in, this will print char in A
inc hl
jr print
message:
defb 16,2,17,7,19,0,22,10,1,"Hello, good evening and welcome",0ffh
cliff:
ds 512-(cliff-bootstart),0 ;fill to end of sector with 0s
end
There are one or two things that may be worth noting about this
example. The first is that because BASIC normally has the address of
the ERR NR system variable held in IY (so it can easily reference its
system variables). It is important to store IY and replace it before
returning to the original USR call.
Just as before, the stack is moved so that is sits in the central 32K
of memory. This will allow +3DOS routines to be called without having
to move it again.
The 'dodos' subroutine may be useful in your own programs. It only
uses the IY register - which isn't used by the +3DOS system and allows
a call to be made to any of the +3DOS routines.
The program uses DOS REF XDPB to make IX point at the relevant XDPB
for drive A:. It then logs in the disk in A: so that it can be written
to. After calculating and modifying the checksum byte for the
information to be written to the boot sector of the disk, it writes
the boot sector using DD WRITE SECTOR.
No checks are made to see that there is even a disk interface, and
possible errors are ignored - the routine isn't designed to be used by
those unfamiliar with possible pitfalls. The routine can be called
with the BASIC command...
USR 28672
...which will come back with whatever number BC happens to contain
after completion of the routine.
The boot sector that is written to the disk has a standard disk
specification in the first 16 bytes. This is followed by the bootstrap
code that will be entered at address FE10h. As will be described in
the interface for DOS BOOT (see part 27 of this chapter), the memory
will initially be set up as 4, 7, 6, 3; however, the BASIC system
variables are still intact and BASIC can be operated by switching in
the correct ROM (3) t5o the bottom of memory and making sure that page
5 is in the 4000h...7FFFh area of memory.
This very simple boot program just uses the BASIC ROM to print a
greeting then enters a tight loop changing the border colour. It could
be modified to load a large binary file and enter it or perform any
other action you desired.