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 5B5Ch ;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.