Pages 275–281 · Markdown

Chapter 25 – Using Machine Code

Using Machine Code

Computers do not respond directly to BASIC, or any other higher level programming language. Instead, such languages are either interpreted or compiled into what is known as machine code, and it is this which is understood by the CPU. The kind of processor that is built into the computer determines the type of machine code that is used. The ZX Spectrum range contains a Z80 processor, and so one writes Z80 machine code when addressing the processor directly. Specifically for the ZX Spectrum Next, the CPU is an updated one called Z80N which contains a superset of the instructions found in the Z80.
This section is written mainly for those who understand Z80 machine code. If you do not, but would like to, you might choose to read a book about it. Suitable titles will be something along the lines of Z80 machine code (or assembly language) for the absolute beginner. If it also mentions one of the computers in the ZX Spectrum range, so much the better. You might also like to read online resources and find tools such as the Design-Design Zeus cross-assembler1 at: https://www.desdes.com/products/oldfiles/, or the z88dk suite which includes apart from a C compiler, also an assembler, at www.z88dk.org, and last but not least the specnext.com forums.

Rather than write the numerical values of a machine code program directly, people usually choose to use mnemonics, known as assembly language, which, although cryptic, is not too difficult to understand with practice. You can see the assembly language instructions understood by the ZX Spectrum Next's CPU in Appendix A.

For a computer to execute this code the program must be converted into a sequence of bytes – in this form it is called machine code. This translation is usually done by a computer, using a program called an assembler. There is no assembler built into the ZX Spectrum Next ROM, however, three, loadable ones are included in the System/Next™ distribution: Zeus, Odin and SPED kindly provided by Neil Mottershead and Simon Brattel for the first, Matt Davies for the second and César Hernández Bañó for the latter respectively. It is also possible do the translation yourself, but this can be a painstaking process.

Let's take as an example the program:

ld bc, 99
ret

This will load the BC register pair with 99 and then return. This translates into the four machine code bytes 1, 99, 0 (ld bc, 99) and 201 (ret). (If you look up codes 1 and 201 in Appendix A, you will find that 1 corresponds to ld bc, NN – where NN stands for any two-byte number; and 201 corresponds to ret.)

Using CLEAR to Make Space

Once you have written your machine code program, the next step is to load it into the computer's memory. You need to decide whereabouts in memory to locate it – the best thing is to make extra space for it between the NextBASIC area and the user-defined graphics.

If you enter the command:

CLEAR 65267

This will give you a space of 100 (for good measure) bytes starting at address 65268.To create the machine code program, you may run a NextBASIC program like this:

10 a=65268
20 READ n: POKE a,n

1 A cross-assembler is an assembler that runs on a different system than the one it produces code for. For example z88dk is usually executed on a PC running Linux or Windows Operating Systems.

30 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.

Using USR to run machine code

To run the machine code, you use the function USR or its –preferred– BANK command variant. In its simplest form USR must be provided with a numeric argument, i.e. the starting address or the bank offset. Its result is the value of the BC register on return from the machine code program, so assuming you type:

PRINT USR 65268

It will return the value 99.

The return address to NextBASIC 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 NextBASIC interrupt mechanism. To perform the exact same function by using the BANK variant, make the following changes to our program:

10 %a=0
20 BANK NEW %b
30 READ %n : BANK %b POKE %a,%n
40 %a+=1: GO TO 30
50 DATA 1,99,0,201

RUN it and you'll see the E Out of Data error again; Now it's time to execute it and it's done by giving:

PRINT % BANK b USR 0

There are a few more variants of USR that differ in key points and make the life of the machine-code programmer a bit easier. These are:

USR$ addr
BANK n USR$ offset

which call the machine code routine at addr (or offset in bank n). Instead however of returning the 16-bit number found in BC (as with USR addr), USR$ returns a string, defined by the start address returned by the machine-code routine in DE and length in BC.

Additionally, USR as well as USR$ (and their BANK variants) can be provided with optional parameters like:

USR(addr, param1[, param2 [, param3...]]])

USR$(addr, param1[, param2 [, param3...]]])

BANK n USR(addr, param1[, param2 [, param3...]]])

BANK n USR$(addr, param1[, param2 [, param3...]]])

which can be passed to the machine-code routine (instead of just the start address in BC).

If a single additional parameter (param1) is present, this is passed in BC (if it is numeric) or as an address DE and length BC (if it is a string).

The type of the parameter passed to the routine is indicated by the zero flag: if set, the parameter is a string (in DE,BC); if clear, the parameter is a number in BC.

Additionally, the type of the expected result is indicated by the carry flag: if set, the expected result is a number (in BC); if clear, the expected result is a string (in DE,BC).

All further parameters are left on the calculator stack for the machine-code routine to use with calculator operations or retrieve using standard ROM routines such as FIND-INT1, FIND-INT2, STK-FETCH. On entry, A contains the number of additional parameters on the calculator stack (0-16) and HL contains a bitmask indicating the type of each parameter. Bit 15, indicates the type of the final parameter (and will be the first to be retrieved from the calculator stack), so types can be read by shifting each bit in turn to the carry flag with ADD HL,HL. Type bits are 0 for string, 1 for numeric.

Routines must remove all additional parameters from the calculator stack, otherwise it will be unbalanced and the expression may be calculated incorrectly.

If you are writing a program to run with the 48K or 128K ROM, you should not load I with values between 40h and 7Fh (even if you never use IM 2). When using one of the 128K ROMs, values between C0h and FFh for I should also be avoided if you plan on enabling contention for your target machine / personality and 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 ULA and the Z80 refresh mechanism, which can cause apparently inexplicable crashes, screen corruption or other undesirable effects. Thus, you should only use vector IM 2 interrupts between 8000h and BFFFh unless you are very confident of your memory mapping (or you are only going to run your program on the +2A, +3e or Next personalities where this problem does not exist).

There are a number of standard pitfalls when programming a banked system such as the ZX Spectrum Next 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. NextBASIC and the editor use the system variables BANKM and BANK678 for 7FFDh and 1FFDh respectively.

If you call NextZXOS routines, remember that interrupts should be enabled upon entry to the routines. Remember also that the stack must be below 49120 (BFE0h) and above 16384 (4000h), 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

or, in case you used the BANK variant

SAVE "name" BANK %b, 0, 4

There is no way of saving the program such that when loaded it automatically runs itself; however, you can get round this by using the short NextBASIC program:

10 LOAD "name" CODE 65268,4
20 PRINT USR 65268

Which should also be saved as a separate program, using a command of the following form:

SAVE "loader" LINE 10

You may run the machine code from NextBASIC using the single command:

LOAD "loader"

This then loads and automatically runs the NextBASIC program, which in turn loads and runs the machine code. You can try and make a version with the BANK variant as well as that's safer and always preferred.

Calling NextZXOS from NextBASIC

When NextBASIC's USR function is used, the code it references is entered with the memory configured with the ROM switched in at the bottom of memory in the address range (000h – 3FFFh) being ROM 3 (the 48 BASIC ROM). The RAM page at the top of memory is Bank 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 the accompanying documents explaining the NextZXOS API (found in the c:/docs/nextzxos/ folder in your System/Next™ distribution), NextZXOS can only be called with RAM page 7 switched in at the top of memory, the stack held somewhere in that range 4000h to BFE0h, and ROM 2 (the NextZXOS ROM) switched in at the bottom of memory (000h to 3FFFh).

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.

If the CLEAR command has been used so that the NextBASIC stack is below 49120 (BFE0h), 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 cata
                                          ;log
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 yourown code would be
                                          ;placed.
                                          ;
                ld hl,catbuff             ;somewhere for DOS to put the cata
                                          ;log
                ld de,catbuff+1           ;
                ld bc,1024                ;maximum (for +3DOS) is actually
                                          ;64x13+13 = 845
                ld (hl),0
                ldir                      ;make sure at least first entry is
                                          ;zeroed
                ld b,64                   ;the number of entries in the
                                          ;buffer
                ld c,1                    ;include system files in the cata
                                          ;log
                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 num
                                          ;ber returned by DOS
                pop hl
                ld (dosret),hl            ;put it where it can be seen from
                                          ; NextBASIC
                ld c,b                    ;move number of files in catalog to
                                          ;low byte of BC
                ld b,0                    ;this will be returned in NextBASIC
                                          ;by the USR function
                                          ;
                                          ;If the above worked, then BC holds
                                          ;number of files in catalog, the
                                          ;"catbuff"
                                          ;will be filled with the alpha-
                                          ;numerically sorted catalog and the
                                          ;carry flag but
                                          ;in "dosret" will be set. This will
                                          ;be peeked from NextBASIC 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 NextBASIC's stack back
                ret                       ;return to NextBASIC, 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 NextBASIC 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 sum=0
20 FOR i=28672 TO 28758
30   READ n
40   POKE i,n : sum+=n
50 NEXT i
60 IF sum <> 9387 THEN PRINT
   "Error in DATA" : STOP
70 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,2
    46,7,50,92,91,237,121,49,2
    55,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,9
    2,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 means, 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 bits, update the system variable then make the switch with an OUT instruction. Interrupts must be disabled while the system variable does not reflect the cur-

rent 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 through 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 Memory Management section in Chapter 24.

The above was a very simple example of calling DOS routines. It works – apart from the ZX Spectrum Next – on the ZX Spectrum +3 and ZX Spectrum +3e as well.

Opcodes Prefixes

Some Assembler opcodes are preceded by a prefix byte which changes the opcode represented by the following byte.

Assembler opcode prefixes CBh (203) and EDh (237) alter the meaning of certain instructions, as indicated in the 5th and 6th columns of Appendix A. This includes the provision of some entirely new opcodes for the ZX Spectrum Next.

Assembler opcode prefixes DDh (221) and FDh (253) alter the meaning of certain instructions that ordinarily refer to the H or L registers, so that they refer to either the component registers of IX or IY register respectively. For example, the instruction LD H,n will load the value of n into the H register. Preceding this two-byte instruction with the IX register's opcode prefix DDh, would result in the most significant 8 bits of the IX register being loaded with that value instead.

This general transformation rule is modified when the original instruction contains (HL), with this component replaced by (IX +N) and any other reference to HL left unaffected. For instance:

DDh 66h is interpreted as ld h,(ix + N)

A DDh opcode will be ignored, interpreted as nop, if it precedes DDh, EDh or FDh. Similar rules apply to the FDh instruction.


ZX Spectrum Next User Manual, 3rd Edition (ISBN 978-1-5272-5496-1), written and illustrated by Phoebus R. Dokos. Copyright © 2020-2024 Phoebus Dokos / SpecNext Ltd. Licensed under CC BY-NC-SA 4.0. This is a transcription and can contain errors; check any doubt against the printed page.