Mastering Machine Code on Your ZX81
By Toni Baker

Sinclair ZX Spectrum
THE ARITHMETIC SUBROUTINES

ARITHMETIC SUBROUTINES

This chapter is divided into two sections - one for the OLD, and one for the NEW ROM. We'll tackle the OLD ROM first because it's easier.

[OLD ROM ARITHMETIC]

Numbers are represented in two bytes, and as such it is possible to store them in register pairs BC, DE, and HL. First of all we shall take a look at the five major arithmetic routines.

1) Addition. The address to call is 0D3E, or more intelligibly, CALL ADD. The subroutine adds together the number stored in DE and the number stored in HL. The result is then placed in HL. This may be demonstrated by the following program:

113900    ADDDEMO   LD DE,0039
211100              LD HL,0011
CD3E0D              CALL ADD
C9                  RET

Here DE is loaded with the number fifty-seven, and HL with seventeen. On return to BASIC the result stored in HL should be fifty-seven plus seventeen, so the command PRINT USR(adddemo) should generate the number seventy-four.

2) Subtraction. Just the same - DE is subtracted from HL and the result stored in HL. The address is 0D39. Thus to prove it:

113900    SUBDEMO   LD DE,0039
211100              LD HL,0011
CD390D              CALL SUB
C9                  RET

3) Multiplication. Up until now we have ignored multiplication completely, since there is no simple instruction which will multiply two numbers together. However, thanks to Uncle C, the ROM will do it for us. Simply CALL MULT, which is stored at address 0D44, and as if by magic DE will be multiplied by HL, the result as usual being stored in HL. Watch out for what happens to BC and DE though! They're not unaltered.

4) Division. As you'd by now expect, the instruction CALL DIV will divide HL by DE (ignoring any remainder of course, since we are dealing in integers). The address of DIV is 0D90.

5) Powers. Is raising one number to the power of another going to be any more difficult? No of course not. With elegant simplicity the instruction CALL POWER (at 0D0C [0D70]) will do just that, raising HL to the power of DE, and putting the answer away in HL, using repeated multiplication to compute the answer.

One very important function is the RANDOM NUMBER GENERATOR. This is held at location 0BED. To generate a random number between one and six (say to simulate the roll of a die), simply load HL with six and CALL RND. This is of course the same thing as RND(6). The number in the brackets should be placed in HL, and the final answer will end up in HL.

See if you can work out what this program does. What we're interested in is the number that it returns to BASIC.

211400    START     LD HL,0014
CDED0B              CALL RND
110A00              LD DE,000A
CD440D              CALL MULT
116400              LD DE,0064
19                  ADD HL,DE
C9                  RET

Let's see if you got it right. HL is loaded with 14 and RND is called, so HL is replaced by a new value, RND(20) (note that 14 (hex) is 20 (dec)). 0A is stored in DE, and the two are then multiplied together. We then have 10*RND(20). Finally 64 (hex) is added, giving 10*RND(20)+100.

We could use this routine in a games program. Suppose we needed to jump to a random destination. We could use the by now famous Tim Hartnell method of GOTO 10*RND(20)+100. Alternatively, if the above machine code were in a REM statement, say at address 16427, we could instead simply say GOTO USR(16427). This would do exactly the same job, except a little bit faster.

We'll leave the OLD ROM now, and turn to the rather more complex field of arithmetic on the NEW ROM.

NEW ROM ARITHMETIC

The first and most important point to note is that NEW ROM numbers are stored as five bytes, not two, and so they can't fit into a register-pair as they stand. Nor are the numbers in simple form, for while it is true that zero is, as you'd expect, 00 00 00 00 00, it is not true that one is 00 00 00 00 01! In fact one is represented by 81 00 00 00 00. Here is a list of the Sinclair representaion of the first few integers.

Decimal   Sinclair Form
0         00 00 00 00 00
1         81 00 00 00 00
2         82 00 00 00 00
3         82 40 00 00 00
4         83 00 00 00 00
5         83 20 00 00 00
6         83 40 00 00 00
7         83 60 00 00 00
8         84 00 00 00 00
9         84 10 00 00 00
10        84 20 00 00 00
    ...

and so on. There is a kind of pattern, but it's not instantly recognisable. Take a look at the negative numbers:

Decimal   Sinclair Form
-1        81 80 00 00 00
-2        82 80 00 00 00
-3        82 C0 00 00 00
-4        83 80 00 00 00
-5        83 A0 00 00 00
-6        83 C0 00 00 00
-7        83 E0 00 00 00
-8        84 80 00 00 00
-9        84 90 00 00 00
-10       84 A0 00 00 00

As you can see, you can instantly change a number from positive to negative just by adding 80 to the second byte. This doesn't apply to zero by the way - zero is represented uniquely to help speed the ROM up a little.

Knowing how the Sinclair Form is built up will slightly help your understanding of the ROM, so I will give here a brief explanation of how to turn decimal numbers into Sinclair numbers. It only takes a few simple steps.

STEP ONE: If the number is zero, then its Sinclair representation is 00 00 00 00 00.

STEP TWO: Ignoring the sign of the number, write it in binary (but without any leading zeroes). For example:

  7        111
-10       1010
 -4.25     100.01
  0.325      0.011
  0.375      0.011

Notice that the binary form has a BINARY point, not a DECIMAL point! 100.01 means one 4 plus no 2's plus no 1's (here we reach the binary point) plus no halves plus one quarter. The next column would have been an eighth.

STEP THREE is to work out a quantity called the EXPONENT. This is done as follows:

  • If the part of the number to the left of the binary point is not zero then the exponent is the number of digits to the left of the point.
  • If the part of the number to the left of the point is zero and the first digit after the point is one then the exponent is zero.
  • If the part of the number to the left of the point is zero and the first digit after the point is zero, then count the number of zeroes between the point and the first 1 - the exponent is minus this number.
The first byte of the Sinclair representation is 80 plus this exponent.

Decimal   Binary    Exponent  Sinclair Form
  7        111      3         83
-10       1010      4         84
 -4.25     100.01   3         83
  0.325      0.011 -1         7F
  0.375      0.011 -1         7F

STEP FOUR: Now we can ignore the binary point altogether - that is what the exponent is for - to tell the computer where the point is supposed to go. So ignoring the point, write the binary form starting with the first 1 and then add sufficient zeroes to the right to make the whole thing thirty-two binary (bits) in length.

  7       1110 0000 0000 0000 0000 0000 0000 0000
-10       1010 0000 0000 0000 0000 0000 0000 0000
 -4.25    1000 1000 0000 0000 0000 0000 0000 0000
  0.325   1100 0000 0000 0000 0000 0000 0000 0000
  0.375   1100 0000 0000 0000 0000 0000 0000 0000

STEP FIVE: It is here that we remember the sign of the original number. If the original number was negative then do nothing. If the original number was positive then replace the first one by a zero. Thus:

  7       0110 0000 0000 0000 0000 0000 0000 0000
-10       1010 0000 0000 0000 0000 0000 0000 0000
 -4.25    1000 1000 0000 0000 0000 0000 0000 0000
  0.325   0100 0000 0000 0000 0000 0000 0000 0000
  0.375   0100 0000 0000 0000 0000 0000 0000 0000

STEP SIX - Now just change these numbers straight into hex, like so, making sure you remember to put the exponent byte at the start:

  7       83 60 00 00 00
-10       84 A0 00 00 00
 -4.25    83 88 00 00 00
  0.325   7F 40 00 00 00
  0.375   7F 40 00 00 00

This is the form in which the ROM will be working. The largest exponent you may have is FF, so the largest positive number that can be stored is FF 7F FF FF FF. This turns out to be 1.7014118E38 (if you can't understand the "E" notation the E means "with the decimal point shifted (in the above case) 38 places to the right"[)]. In other words the number 170,141,180,000,000,000,000,000,000,000,000,000,000 which is a pretty vast quantity. It can still only store ten decimal places accurately though. The smallest positive number you can have (apart from zero) is 01 00 00 00 00, which happens to represent 2.9387359E-39. To you and me that's 0.000,000,000,000,000,000,000,000,000,000,000,000,002,938,735,9 which I'd say was pretty microscopic.

You can check all of this with the following BASIC program.

10 LET A=0
20 LET B=PEEK 16400+256*PEEK 16401
30 FOR I=1 TO 5
40 INPUT A$
50 POKE B+I,16*CODE A$+CODE A$(2)-476
60 NEXT I
70 PRINT A

Download available for 16K ZX81 -> chapter17-floatchk.
[I have heavily modified this downloadable version so that not only can you enter Sinclair Form numbers in one go, you can also convert from decimal to Sinclair Form too.]

The program sets up a variable A, and then overwrites its previous contents by POKEing into the variables area, one byte at a time (that's a letter I in line 50, not a number 1). If you run it and enter "82"/"40"/"00"/"00"/"00" (where / means newline) you'll find the number three printed. And so on.

An interesting little quirk is that if you input "00"/"80"/"00"/"00"/"00" (in theory this is minus-zero) the machine actually prints -C.6E-56 ! The letter C in mid-number, and an exponent of -56! Don't panic! This doesn't really happen in the ROM. We made it happen by POKEing something that doesn't make sense. The ROM does behave slightly more sensibly than human beings.

HOW TO USE FLOATING POINT NUMBERS PROPERLY

Having seen that a five byte number is too big to store in the registers, the next question is undoubtedly "Well where does it store them then?". Answer - it stores them in an area of RAM called the CALCULATOR STACK, which works very much like the ordinary stack except for two points: 1) It can store both floating point numbers and strings, and 2) it is the right way up, not upside down like the machine stack.

To push a number stored in the BC register onto the calculator stack all you need to do is call up a subroutine in the ROM. CALL STACKBC, as I've called it, will change BC into five byte form as described above and then push this number onto the top of the calculator stack. You can do the same for a number stored in A (i.e. a number between 0 and 255) by calling STACKA. The addresses to call are: 1519 151D (STACKA) and 151C 1520 (STACKBC).

CD1915              CALL STACKA
CD1D15              CALL STACKA       
CD1C15              CALL STACKBC
CD2015              CALL STACKBC       

Incidentally the first two instructions in the STACKA routine are LD C,A and LD B,00. It then leaps straight into STACKBC!

Conversely, to retrieve a number from the calculator stack we can CALL UNSTACK (address 0EA7), which removes a number from the calculator stack and stores it in the BC register.

[Thunor: 0EA7 is the 'FIND INTEGER' (->) subroutine which does actually call the 'FLOATING-POINT TO BC' (->) subroutine at 158A but it also quits to BASIC with an error code if there's a problem. Now this behaviour is not what you'd normally expect in machine code programs as they should be in control of these situations, and so I think that CALL UNSTACK should really be calling 158A directly and in fact it works because I've tried it in the 6 * 7 multiplication test below. I suggest that when you see CALL UNSTACK at 0EA7 you should bare in mind that calling 'FLOATING-POINT TO BC' at 158A might be a better idea.]

Arithmetic is quite straightforward. The addresses are:

ADD       1754      addition
ADD       1755      addition
SUB       174B      subtraction
SUB       174C      subtraction
MULT      17C5      multiplication
MULT      17C6      multiplication
DIV       1881      division
DIV       1882      division

They work like this: The five-byte number stored at an address specified by HL (this means the number is stored in locations (HL), (HL)+1, (HL)+2, (HL)+3, and (HL)+4) is added to, multiplied by, divided by, or has a second number subtracted from it. The second number is stored at an address specified by DE. After the calculation the result is stored in the five bytes beginning at address HL.

To multiply together the two numbers at the top of the calculator stack one method would be as follows:

2A1C40              LD HL,(STKEND)
11FBFF              LD DE,FFFB
19                  ADD HL,DE
E5                  PUSH HL
221C40              LD (STKEND),HL
19                  ADD HL,DE
D1                  POP DE
CDC517              CALL MULT
CDC617              CALL MULT

Can you follow exactly what is going on? HL is loaded with the contents of the system variable STKEND - which gives the address of the first byte after the end of the calculator stack. DE is loaded with minus five, thus HL is decreased by five. This new value is loaded back into STKEND because we start off with two items on the stack and want to end up with only one. This is the address of one of the numbers to be multiplied. If you follow the listing through carefully you'll see that DE ends up with this value. First though HL is decreased by five again, to find the start of the other number to be multiplied.

To check that it really does work, run this program.

3E06      START     LD A,06
CD1915              CALL STACKA
CD1D15              CALL STACKA
3E07                LD A,07
CD1915              CALL STACKA
CD1D15              CALL STACKA
2A1C40              LD HL,(STKEND)
11FBFF              LD DE,FFFB
19                  ADD HL,DE
E5                  PUSH HL
221C40              LD (STKEND),HL
19                  ADD HL,DE
D1                  POP DE
CDC517              CALL MULT
CDC617              CALL MULT
CDA70E              CALL UNSTACK
C9                  RET

Run it by typing PRINT USR start. What do you get?

But surely there must be easier ways to multiply six by seven. After all, the above program does look very complicated, and not something you'd easily remember. Well it's here that we really do start making full use of the ROM. The following program does exactly the same job, and I shall shortly explain why:

3E06      START     LD A,06
CD1915              CALL STACKA
CD1D15              CALL STACKA
3E07                LD A,07
CD1915              CALL STACKA
CD1D15              CALL STACKA
EF                  RST 28
04                  DEFB 04
34                  DEFB 34
CDA70E              CALL UNSTACK
C9                  RET

In the NEW ROM, RST 28 means "perform floating point arithmetic". The data that follows tells it precisely what calculations it's supposed to do. The byte 04 means multiply - all of the shuffling around of the calculator stack is done automatically. The byte 34 is used after a RST 28 instruction to indicate that there is no more data to come, and the next machine code instruction should follow.

The RST 28 data codes are:

ADD    0F
SUB    03
MULT   04
DIV    05

Don't forget you'll need a byte 34 as well though, to end the data.

You may be wondering what happens if the number on the top of the calculator stack is not an integer between 0 and 65535 (the maximum value any two byte register can hold). Well my first answer would be "try it for yourself and find out". Write a program that adds 8001 to 8001. Write a program that divides eight by three, then a program that divides seven by three. Write a program that subtracts five from zero, and another that subtracts a thousand from zero. But for those of you who are impatient I'll tell you anyway.

If the number at the top of the calculator stack is greater than 65535 then attempting to "unstack" it into BC will result in the program returning to command mode in fact - stopping with error code B (which means out of range).

If the number is a decimal then it will be rounded up or down (not just INTed) to the nearesr whole number. If the decimal part is less than 0.5 it will be rounded down, and if the decimal part is greater than or equal to 0.5 it will be rounded up.

If the number is negative then error B will result, causing an immediate return to BASIC and stopping the program, if there is one.

RST 28 allows you to do much, much more than just simple arithmetic. All of the functions of the ZX81 are available to you. The data code for any particular function is just the character code of that function minus AB. For instance, the character code of SIN is C7. C7 minus AB is 1C (if you don't believe me we'll do it in decimal - 199 minus 171 is 28). This means we can find the SIN of the number at the top of the calculator stack using the sequence:

EF                  RST 28
1C                  DEFB 1C (SIN)
34                  DEFB 34 (Exit)

To multiply two numbers (at the top of the calculator stack) together and then find the square root of the result we can use the sequence:

EF                  RST 28
04                  DEFB 04 (MULT)
25                  DEFB 25 (SQR)
34                  DEFB 34 (Exit)

If you're not absolutely convinced yet, run this program, which multiplies five by twenty, and then takes the square root.

3E05                LD A,05
CD1915              CALL STACKA
CD1D15              CALL STACKA
3E14                LD A,14
CD1915              CALL STACKA
CD1D15              CALL STACKA
EF                  RST 28
04                  DEFB 04 (MULT)
25                  DEFB 25 (SQR)
34                  DEFB 34 (Exit)
CDA70E              CALL UNSTACK
C9                  RET

You'll notice that this is the first time we've used more than one code in the RST 28 data. In fact you can use as many as you like, provided you end the list with 34.

To save you working it out for yourselves here is a list of the available functions that we are ready to use, together with their appropriate RST 28 code:

FUNCTION  CODE      FUNCTION  CODE
CODE      19        EXP       23
VAL       1A        INT       24
LEN       1B        SQR       25
SIN       1C        SGN       26
COS       1D        ABS       27
TAN       1E        PEEK      28
ASN       1F        USR       29
ACS       20        STR$      2A
ATN       21        CHR$      2B
LN        22        NOT       2C

Some of the entries in that list may surprise you. For instance we have the use of USR. This is very confusing - being allowed to use USR in the middle of a USR routine - but it's not really. Here's how it works. You've worked your way through a lot of RST 28 data, done a lot of calculation, and now you come across the code 29. What happens next is that the number at the top of the stack should be an integer between 0 and 65535 - or else you'll get an error B. This address is treated as a subroutine and CALLed. This subroutine will run exactly as you'd expect it to. When it's over (i.e. when a RET instruction is reached) the machine will go back to interpreting the RST 28 data from the next byte. USR will of course leave a new value at the top of the stack - the value held by BC at the end of the subroutine.

PEEK works in the same way, finding an address, PEEKing there, then pushing the result to the calculator stack.

All of the functions when used in this way will remove the number currently at the top of the calculator stack and replace it by a new one. For instance, if the number at the top of the stack is 3.5 and the function INT is called, the 3.5 will be removed and replaced by a new value, 3.

The string functions CODE, VAL, and LEN, also CHR$ and STR$ need a small amount of explaining. You see, as well as storing numbers, the calculator stack can also store strings, so if you start off with the number 2000 on the top of the stack, and you then call STR$ (by using code 2A in a RST 28 instruction) then the item at the top of the calculator stack will now be the string "2000". You can demonstrate this with the following:

01D007              LD BC,07D0
CD1C15              CALL STACKBC
CD2015              CALL STACKBC       
EF                  RST 28
2A                  DEFB 2A (STR$)
19                  DEFB 19 (CODE)
34                  DEFB 34 (Exit)
CDA70E              CALL UNSTACK
C9                  RET

This should produce the result of CODE STR$ 2000. Does it?

USING THE CALCULATOR'S MEMORY

If you take a quick glance at the manual you'll see that one of the system variables, MEMBOT, is thirty bytes long. This is the calculator's memory area. A quick calculation involving dividing by five, if you're up to it, shows that this leaves enough room to store six different five byte numbers. The six different areas of memory may each be used by RST 28 to store, or retrieve, numbers (but numbers only) from the top of the calculator stack. There are twelve different codes to achieve this - these are:

C0        stores number in memory location 0
C1        stores number in memory location 1
C2        stores number in memory location 2
C3        stores number in memory location 3
C4        stores number in memory location 4
C5        stores number in memory location 5

E0        recalls number from memory location 0
E1        recalls number from memory location 1
E2        recalls number from memory location 2
E3        recalls number from memory location 3
E4        recalls number from memory location 4
E5        recalls number from memory location 5

Storing a number copies it from the top of the stack, and recalling a number simply places it at the end of the stack - it doesn't overwrite the previous top item.

Let's see how we can use this. Suppose we want to find SIN X+COS X. We must use the following technique. Assume that X is at the top of the stack.

EF                  RST 28
C0                  DEFB C0 (STORE0)
1C                  DEFB 1C (SIN)
E0                  DEFB E0 (RCALL0)
1D                  DEFB 1D (COS)
0F                  DEFB 0F (ADD)
34                  DEFB 34 (Exit)

Note that the SIN routine changes X into SIN X. When we again recall X there are now two items on the stack: SIN X and X. The COS routine changes X into COS X, so that the two items on the stack, are now SIN X, and COS X. The ADD routine will replace both of these by one single number - the answer we want - SIN X plus COS X.

We have now performed a fairly complex trigonometric function in just eight bytes!

Let's see how we can remove a floating point number from the stack without restricting ourselves to integers less than 65536. The way the ROM does it is like this:

2A1C40              LD HL,(STKEND)
2B                  DEC HL
46                  LD B,(HL)
2B                  DEC HL
4E                  LD C,(HL)
2B                  DEC HL
56                  LD D,(HL)
2B                  DEC HL
5E                  LD E,(HL)
2B                  DEC HL
7E                  LD A,(HL)
221C40              LD (STKEND),HL

As you can probably see for yourself, a five byte number is removed from the stack and stored in the registers A, E, D, C, and B (in that order). You can CALL this routine from address 13E4 13F8.

If the first item in the variable store is X then having popped SIN X plus COS X from the stack you can then store the result back in X as follows:

2A1040              LD HL,(VARS)
23                  INC HL
77                  LD (HL),A
23                  INC HL
73                  LD (HL),E
23                  INC HL
72                  LD (HL),D
23                  INC HL
71                  LD (HL),C
23                  INC HL
70                  LD (HL),B
C9                  RET

You can see that it takes more bytes to store the answer than it does to find it in the first place!

Let's see what else we can do with RST 28. We can use the logical functions AND and OR (that is BASIC AND and BASIC OR). Both of these are available from RST 28, having byte codes 08 and 07 respectively. Also you can SWAP the two numbers at the top of the stack. Code 01 will do this.

The following sequence will raise one number to the power of another. Can you see why? After RST 28: 01 22 04 23 34.

[Thunor: Well that's it folks! After 61 days I've finally finished my HTML transcription of this book, on the same day that BBC4 showed the comedy docudrama Micro Men (->), dramatising the rivalry between Sir Clive Sinclair and Chris Curry of Acorn in the early 80s - fantastic stuff :)]

[ADDENDUM]

[Thunor: Just as I started to transcribe this chapter I received an email from tabbycat <tibbytab AT tesco DOT net> warning me about the forthcoming ROM address errors, therefore I was extra vigilant and I've (hopefully) flushed them all out; many thanks for the heads-up.]

Sinclair ZX Spectrum

  Previous Page Back Next Page