Your Spectrum
Issue 11, February 1985 - BASIC from Machine Code
Home Contents KwikPik
title Being able to access Basic commands from machine code is not always as easy as it sounds, especially when dealing with Microdrives and the like. Join David Threlfall as he rummages around in the Spectrum's ROM. The complicated manipulation of the stack pointers and error pointer (ERR_SP) is not necessary unless you plan to use Interface 1 commands. If you don't follow this method then only the first use of this facility will work and the command may not return to your routine at all, even after successful execution.
The routine 'BASIC' has been written so that it's called with the HL register pair pointing to the tokenised version of the command which you wish to have executed. This allows the routine to be used repeatedly, with different commands if necessary. In all cases, the last byte of the command must be that representing carriage return (13 decimal).
Most Spectrum users will be aware that numbers are stored in a rather odd way in Sinclair Basic. First, there are the ASCII characters making up the number, then the single byte 14 followed by the five byte binary representation of the number. The ASCII form is converted to binary form during syntax checking. If we want to avoid this checking, some way has to be found which doesn't entail us translating any numbers we use into binary. Fortunately there is a way, using the VAL function. Simply write numbers as VAL "number"; for example, if we intend to include the number 500 in the code to be executed, we need only write VAL "500".

FOR EXAMPLE ...

There are many uses for this routine, but for now I'll give just two examples. Say you wish to SAVE 3000 bytes of code starting at location 50000 into a cassette file called "fred". The normal Basic command would be:
SAVE "fred" CODE 50000,3000

To execute this from machine code we can insert:
SAVE "fred" CODE VAL "50000",VAL "3000"

At label TEXT in the assembler listing. Note that the word 'SAVE' is inserted with its token value (248), as are all the other keywords like CODE and VAL.
The second example is both more complicated and more useful. It's difficult to use the line editor on the Spectrum for reading a string of characters, but the following code inserted at TEXT will read in a$ and put it at locations 30000 upwards - allowing you to access it from your machine code (the byte '0' marks the end of the string):
INPUT a$: FOR k=VAL "1" TO LEN a$: POKE VAL "29999"+k, CODE a$(k): NEXT k: POKE k+VAL "30000", VAL "0"

The only restriction imposed is that the total length of the input line must be less than 128 characters. This can be increased very easily by making more space and copying more with the LDIR.
Finally, there's no way back to Basic from here because we've erased the input buffer, upset the Basic code pointer and destroyed the floating point stack. These are all repairable, but a RST 8 return to Basic is safe and simple.
There are many functions on the ZX Spectrum that are not easy to use from machine code. The simple PLOT, colour and AT features are straightforward. Tape commands such as SAVE and LOAD are a little more difficult, but the Interface 1 commands for Microdrive, RS232 and streams are almost impossible without a very detailed understanding of the ROM routines involved.
The technique to be described here isn't generous enough to allow the spectacular gains in speed you would normally expect from machine code. However, it is very simple to use and there's no advantage in speeding up the rate at which you get to the Microdrive routines when the drives themselves have an access time of several seconds.
The method is simply to build a tokenised version of the command to be executed in the Basic input line area (pointed to by the system variable E_LINE).
The steps involved are as follows:
1. Clear out all the areas above the input area. This will also empty the workspace and floating point stack.
2. Make space for your command in the input buffer.
3. Build the command into the buffer - this will usually be just a question of copying into that area.
4. Manipulate the error stack pointer (ERR_SP) so that the machine will return to your routine whatever happens.
5. Call the ROM routine which evaluates a single line of Basic.
6. On return, clear work areas and test the error flag. If there was an error go back to Basic.
The assembler listing provided is all that's necessary; it's fairly short and all Basic commands can be operated on in the same way. The code is liberally commented, but a few more notes would undoubtedly be helpful.
[Lines marked in red are corrections for errors in the printed listing.]
CODE ASSEMBLER COMMENTS
  LD HL,TEXT
CALL BASIC
RST 8
DEFB 255
Make HL point to the required Basic code.
Call this routine from your machine code.
Return to command mode with error '0'.
BASIC PUSH HL
CALL 5808
LD HL,(23641)
LD BC,130
CALL 5717
INC HL

EX DE,HL
POP HL
LD BC,128
LDIR
SET 7,(IY+48)
LD (IY+0),255
LD (IY+10),1
LD HL,(23613)
PUSH HL
LD HL,BACK
PUSH HL
LD (23613),HL
CALL 7050
POP HL
 
Use a ROM routine to tidy up the buffers.
Point HL at the system variable E_LINE.
Make 130 bytes of space in the input buffer.
Make space routine in the ROM.

Move HL into DE for an LDIR.
Point HL to our Basic text.
Maximum length of text.
Move text to the input buffer.
Signal line execution.
Clear any old errors.
Set the NSPPC system variable to the first statement.
Keep the value of ERR_SP (error stack
pointer) for later repair.
BACK is a return address.
Put HL on the stack in case of error return.
Keep the stack pointer in ERR_SP.
Call the 'line evaluation' routine.
If we return here then discard this value.
BACK POP HL
LD (23613),HL
CALL 5808
LD A,(23610)
CP 255
JP NZ,4867
RET
May return here if there is an error.
Put ERR_SP back again.
Tidy the buffers.
Was there an error? Look in ERR_NO.
255 is OK.
If not, jump back to Basic.
Return to the calling routine.
TEXT   Your TEXT goes here in an assembler string.
  DEFB 13 Terminate the string with carriage return.
Home Contents KwikPik