Authors
Publication
Publication Details
Volume: 2 Issue: 1
Date
Pages
This is the first of an occasional and indefinite series on machine-code programming for the 2068. I do not intend to take you through the rudiments of Z80 instructions, nor even the discovery and invention of the poked REM statement, but if you know how to do it even a little bit, then maybe I can give you a few ideas, or at least prep you for your next local Sinclair ROM trivia quiz.
It will help if you have some kind of machine programming aid at hand, a disassembler, an assembler, an efficient PEEK and POKE loop, or at least something more illuminating than a flashing K, in order to look at the code in the ROM. There are a few people that survived the 80’s without HotZ, and it will not be a requirement to read this article.
I will be happy to receive your comments on my mistakes and omissions. I cannot promise you individual answers and snippets of code, but I will try to reply here to all items of general interest.
For unravelling a ROM, I prefer to start with the jump tables, which provide lists of the entry points of many of the main routines in the operating system. Once the main jump tables are understood, then it is generally possible to work out most of the auxiliary routines.
The way to find the main BASIC jump table in the ZX80’s was to PEEK out the value of the syntax table pointer (T_ADDR), which Uncle Clive had thoughtfully labelled “very unlikely to be useful” in the ZX manual. I noticed that Timex removed that comment in the 2068 manual: have they succeeded in making it truly useless? Happily not. PRINT PEEK 23668 (and 23669) give 158 and 25, which convert to the hex address 199E. (Don’t fiddle with multiplying out the decimals; just convert each byte directly to hex: 25d is 19H, etc.) Now 199E just happens to contain the address of the PRINT routine in BASIC, because I asked the machine to PRINT what I PEEKed. This address is near the beginning of the syntax table, which actually begins at 197BH. Finding the table this way saves hours of hunting.
The syntax table is not a pure jump table. It contains a liberal scattering of “command class” bytes and indications of required separators for commands like the comma in PLOT X,Y. Nevertheless, it is not a difficult task to work out the valid addresses in the table and to start studying the routines at those addresses to see which BASIC commands they implement. (Well, knowing the ZX ROM does help a little.) Table 1 lists the addresses from this table for the 2068 BASIC commands.
I intend here to browse through the list to suggest some possible uses of these routines. However, using BASIC commands in MC is like trying metric tools on an American engine: some of them work, a little, for some things. Here are a few of the best; others can be worked out by analogy.
Table 1
BASIC COMMAND ADDRESSES IN THE 2068 ROM
GOTO 1EF1
READ 1D97
IF 1C5B
DATA 1E82
GOSUB 1F99
RESTORE 1E9D
STOP 1C59
DRAW 26DB
RETURN 1FD4
COPY 0A02
FOR 1C78
LPRINT 2155
NEXT 1D55
LLIST 1541
PRINT 2159
BEEP 0436
INPUT 222B
CIRCLE 2679
DIM 2FC0
OUT 1F04
REM 1B00
BORDER 243E
NEW 0D1D
DEF FN 201D
RUN 1F2B
OPEN 142A
LIST 1545
CLOSE 139F
POKE 1F0A
FORMAT 25CC
RANDOMIZE 1ED4
MOVE 25D0
CONTINUE 1EE4
ERASE 25D4
CLEAR 1F36
CAT 25C8
CLS 08A6
DELETE 20D1
PLOT 2635
RESET 2454
PAUSE 1FEB
SOUND 2128
Those below are not in the syntax table nor the floating point table:
LET 2EBD
INK 1BF9
STICK 2902
PAPER "
FREE 2934
FLASH "
SAVE 01AB (EXROM)
INVERSE "
LOAD "
OVER "
VERIFY "
MERGE "
GOTO
On the ZX80 I wrote a BASIC disassembler and was amazed at its deliberative approach to the screen. To speed it up I started patching in machine-code routines. I needed routines that would take calls from anywhere and re-enter the BASIC program at a line to be decided in code. That was when I adopted the GOTO routine from Z80 code. In a “real” program the result is spaghetti, seeds of impossible logic, but if you are fond of potato racing and other such self-imposed handicaps, you might enjoy it.
The GOTO routine starts with a CALL to a very useful routine at 1F23, which loads the BASIC line number from the calculator (calc) stack into the BC register pair, rounding to the nearest integer. It then proceeds to put BC into HL. In the normal course of a machine code subroutine, you are not likely to have become deeply involved with the calc stack, so prepare first and enter this routine late with the initial value, the BASIC line number, in HL. That enables you to jump to the GOTO routine at 1EF6. The program at that point checks the validity of your line number (incorrectly) as less than 61439, and requires you to begin with the first statement. If you enter at 1EF8 with D already set to the statement number, then you can return to a particular statement (in D) of a line number (in HL).
Ending your code for a USR call with:
LD D,02 Statement number
LD HL,26AC 9900 decimal
JP 1EF8
The GOTO will put you back in BASIC starting with statement 2 of line 9900. If there is no statement 2, you will fall into the “Statement lost” error trap. The best use of this tactic would be to force a single re-entry point, independent of where the USR call was made, so as not to play havoc with program logic.
You can print long strings to the screen by using the BASIC PRINT routine, and it will serve you fairly well for messages that you can set up at some fixed position in RAM. The rules are these: start with a quote (or comma/apostrophe); the address of that quote should be put into HL and passed to CH_ADD, which is BASIC’s pointer for most commands. End with a quote and a carriage return (0D). Outside the quotes, the semicolon, apostrophe, and comma will work as they do in BASIC.
If you know how to format a floating-point form, or if you just write the PRINT statement in BASIC and then copy it up to your machine code area, you can use the AT, TAB, INK, PAPER and suchlike items as well. What you need in RAM are the sequence of characters that follow (not including) PRINT up to and including the 0D (carriage return). Then just point CH_ADD to the first character and jump to 2159:
LD HL, POINTER
LD (CH_ADD),HL
JP 2159
There is a more elementary approach to screen printing, the well-known Sinclair approach of loading the character code into the A register and doing an RST 10. All registers are preserved, and it’s as fast as you’re likely to need for most programs.
There are a couple of things you need to know to use the 2068’s RST 10. First of all, you had better set up the right output channel, or strange lockups are likely. Channel 2 will give you the main screen, channel 3 the 2040 printer output, and channel 1 or 0 the lower (edit) screen. The way to set the channel is:
LD A, CHANNEL_NUMBER 1,2,3
CALL 1230 CHANNEL OPEN
If you stay in machine code, the open channel stays open, but if you’re jumping between BASIC and machine code, you should probably set it every time you start again in MC.
You will also want some kind of PRINT AT routine, so that you can RST 10 your character anywhere on the screen. The normal print-position set in the 2068 uses a “backwards” pair of line and column numbers, such that the top line is 24 decimal (18 hex) and the left column is 33 decimal (21 hex). It is often handy to use the following to invert a proper line and column position in BC into backward notation:
LD BC, LINE_COLUMN (1,1 TOP LEFT)
PRAT: LD A,21
SUB C
LD C,A
LD A,18
SUB B
LD B,A
CALL 0914
RET
The last call sets up the proper values in the system variables S_POSN and DF_CC. Use this as a set-up subroutine anytime you want a new print position. Do the LD BC, LINE_COLUMN in your main code and call the subroutine at PRAT. After the call, LD A,CHAR__CODE and RST 10 will put the character where you want it.
If you open the printer channel, LPRINT will work in a way similar to PRINT and RST 10 will output to the printer after you open channel 3.
INPUT
You can use the INPUT routine from MC in much the same way as you would use PRINT. Set CH_ADD to point to an area of RAM that holds the characters that follow an INPUT command, then jump to or CALL 222B (INPUT). The routine will prompt you just as BASIC does and will take your entry and put it into the proper place among the BASIC variables.
Your problem in machine code will be finding the input after the entry. Fortunately, there is a look-up routine in the ROM made just for the purpose, located at 2C70. Once again, CH_ADD is used as a pointer. You must have the variable name somewhere in RAM (e.g. A$, X, UNKNOWN). Then point CH_ADD to the first character of the variable name and CALL 2C70. On return, for a simple string, HL will point three bytes short of the current value of the variable, and the two intervening bytes will hold the length. The following code will move the string to whatever address you set in DE.
LD HL,VARIABLE_NAME_P0INTER
LD (CH_ADD),HL
CALL 2C70 LOOK FOR IT
INC HL
LD C,(HL) GET LENGTH
INC HL
LD B,(HL)
INC HL
LD DE,WHERE_YOU_WANT IT
LDIR MOVE IT
RET IF FINISHED
If the variable is numeric, you will have to deal with the floating point form (5 bytes). You can set HL to point to the first of the five bytes and CALL 3773 to move it to the calc stack. A numeric value follows directly after the address returned in HL after the lookup routine (2C70). Consult pages 256 and 257 of your 2068 manual for the format of other variable types. If you have a good single-stepper, step through the first three commands above and then look up the area pointed to by HL (as data, not code) to discover the offset to the actual variable value.
CLS
Here’s one that’s easy. Just call it for the traditional screen clear. However, if you are using the screen in machine code and have reduced the size of DF_SZ to 0 for full screen printing, then you will get a crash on the 2068. DF_SZ should be at least 2 for a normal CLS. You can increase it, do the CLS and set DF_SZ back to zero immediately if you choose.
PLOT
The PLOT command is one of those commands that gets its coordinates from the calc stack. It does so in the very first line of the routine with a CALL 2307, which loads the last value on the calc stack to B, the next last to C, and reports an error if either is over 255. To avoid fooling with the calc stack and details of floating point, all you have to do is to load BC with the coordinates of the point you want and to enter the routine at the second instruction at 2638.
The following little test routine should give you the idea by drawing a short diagonal line:
LD BC,2133 START COORDS
LD A,22 LENGTH COUNTER
DIAG: PUSH AF SAVE ON STACK
PUSH BC
CALL 2638 PLOT
POP BC GET PREVIOUS
POP AF VALUES
DEC A TEST COUNTER
RET Z IF DONE
INC B STEP COORDS.
INC C
JR DIAG PLOT AGAIN
DRAW
The DRAW command has two forms, with either two or three parameters. With just two parameters, DRAW will function without the calc stack, but with three parameters the calc stack is necessary, so it’s time to learn how to put numbers there. The routine at 30E6 is STACK_A, and puts the number in A onto the calc stack. (For bigger numbers, not needed here, use STACK_BC at 30E9.) With both DRAW and CIRCLE, the interpreter gets the first two parameters onto the stack before the command routine begins. The first thing the command routines do is to look for the third parameter in a BASIC line, and again you will want to call in late.
The fifth line of the DRAW routine at 26E3 jumps for the two-parameter form to 27D0, which calls the actual line-draw routine and exits by the temporary colors routine (0888). So the best way in will be at 27D0, but first it’s necessary to set the current plot point by loading the system variable COORDS and to put an X and a Y displacement onto the calc stack.
It works like this:
LP HL,0101 TO START LOWER LEFT
LD (COORDS),HL SET START COORDINATES
LD A,82 X DISPLACEMENT
CALL 30E6 STACK IT
LD A,55 Y DISPLACEMENT
CALL 30E6 STACK IT
CALL 27D0 DRAW LINE
EXX RESET HL
LD HL,2B16
EXX
RET
The last four instructions are necessary if and when you return to BASIC. The DRAW routine uses the exchange registers, but the USR call also uses HL’ to store the address 2B16, which must be there when your call comes back to BASIC. Try this reset whenever you’ve been making ROM calls from MC and get an Out of Memory or Nonsense in BASIC error when you come back to BASIC.
The three-parameter form of DRAW begins at 26E6 by reading the third parameter from the BASIC line to the calc stack. If you’ve already stacked the third parameter in your MC routine, then the proper entry point is at 26ED, where the floating point processor takes over.
A new problem is that the third parameter is the angle in radians. STACK_A will only stack integers, which doesn’t give you much latitude in specifying the angle. Instead, you can take advantage of the routine at 3076, which will read the characters of a decimal number from RAM to the calc stack. The routine uses CH_ADD as a pointer, and it’s a good idea to save and restore the original value of that pointer to prevent your BASIC program from getting lost. Put the characters of the decimal number somewhere in RAM and, for neatness sake, end it with an 0D (carriage return) so the end of the number will be clear. Then try the following:
NPTR: DB "3.58" ANGLE IN RADIANS
DB 0D END NUMBER
CURV: LD HL,3333 START POINT
LD (COORDS),HL SET START COORDINATES
LD A,82 X DISPLACEMENT
CALL 30E6 STACK IT
LD A,55 Y DISPLACEMENT
CALL 30E6 STACK IT
RST 18 GET CH_ADD TO HL
PUSH HL AND SAVE IT
LD HL,NPTR NUMBER POINTER
LD (CH_ADD),HL TO CH_ADD
LD A,(HL) FIRST CHARACTER TO A
CALL 3076 STACK THE NUMBER
CALL 26ED DRAW-CURVE
EXX
POP HL GET OLD CH_ADD
LD (CH_ ADD) ,HL AND RESTORE IT
LD HL,2B16
EXX
POP HL GET OLD CH_ADD
LD (CH _ADD),HL AND RESTORE IT
RET
Be sure to call in at CURV, not at NPTR. Like BASIC, this routine will trap you if you run off the screen, so it might be best to get your parameters right with BASIC first.
CIRCLE
The CIRCLE command is very similar to the three parameter form of DRAW, but it does not require any initial setting of COORDS. The CIRCLE routine begins by checking for a comma and then reading in the third parameter (radius) from the BASIC line, so once again you should prepare first and call in late. The working part of the CIRCLE command begins at 2686, so try the following:
DB "3.58" RADIUS
DB 0D END NUMBER
LD A,82 X COORDINATE
CALL 30E6 STACK IT
LD A,55 Y COORDINATE
CALL 30E6 STACK IT
RST 18 GET CH_ADD TO HL
PUSH HL AND SAVE IT
LD HL,NPTR NUMBER POINTER
LD (CH_ADD),HL TO CH _ADD
LD A,(HL) FIRST CHARACTER TO A
CALL 3076 STACK THE NUMBER
CALL 2686 DRAW THE CIRCLE
EXX RESET HL
LD HL,2B16
EXX
POP HL GET OLD CH_ADD
LD (CH_ADD),HL AND RESTORE IT
RET
BEEP
I will leave an implementation of this command up to you. The routine at 0436 expects two numbers on the stack. You can put them on as integers with CALL 30E6 or by using the routine at 3076 to stack a decimal (complete with minus sign). Reset HL’ before returning.
Another way of using the BEEP is to set DE and HL and CALL 03F3. DE should hold the frequency (Hz) of the note multiplied by the duration (sec), and HL is computed from the formula (4,375,000/freq) – 30, where freq is in Hz.
NEXT TIME
Not all of BASIC is listed in the syntax table. Most of the functions and a few commands are interpreted by the “floating point interpreter,” a Sinclair specialty that is accessed by RST 28 in both the ZX and the 2068. RST 28 is just a jump to 371AH in the 2068. I browsed there for a few dozen bytes until I saw the LD DE,3696 at address 374D. 3696 is a valid and nearby ROM address; the “code” there disassembles as nonsense, looks like a jump table, quacks like a jump table. The functions there are listed in almost the same order as they were in the ZX. Learning to use these from machine code will give you some real programming power.
Next issue I’ll try to explain some ways to use these functions and do painless floating point programming, with a couple of tricks that I haven’t actually tried yet. If they don’t work I’ll think of an excuse and tell you about something else.