ZX Basic Features
The ‘Division Error’
If you run the following program on a standard Spectrum, or on an emulator with the standard ROM, then the results are predictably correct.
10 PRINT .25 = 1/4
20 PRINT .9999999948
30 FOR I = 0 TO 1 STEP .25 : PRINT I : NEXT I : PRINT “end”
If you run the same program on an emulator using a modified ROM, with the corrected “division error”, then the results are somewhat inaccurate.
Normal ZX ROM Modified ROM
1 0
.99999999 1
0 0
0.25 0.25
0.5 0.5
0.75 0.75
1.0 end
end
Clearly something is amiss.
Floating-point Arithmetic
The ZX81 and the ZX Spectrum use a similar floating-point number representation and the following applies to both machines.
A four byte, 32-bit mantissa is used with the most significant bit, the implied set bit, being used as a sign bit for the number.
This bit is reset to denote a positive number and left set to denote a negative number.
The byte that precedes the mantissa is the exponent byte. This holds the number of times the imaginary decimal point that precedes the mantissa has to be moved to restore the number from it’s normalized state.
The normal state is when the bits of the mantissa are moved left (or right) so that the most significant bit is set.
A number like a half ( .1 binary ) or three-quarters ( .11 binary) is already normalized and has a plus zero exponent ( $80).
A number like a quarter ( .01 binary ) is shifted left and the exponent decreased ( $7F ).
A vast range of numbers, of which 1/65536 through 65536 is just a small subset, can be held accurately with a mantissa consisting of four zero bytes.
In contrast, just as a third cannot be held accurately in the decimal system (giving .33333 recurring), so a tenth cannot be held accurately in a binary mantissa and becomes .00110011 recurring. Only machines like the Jupiter Ace, which use Binary Coded Decimal, can hold a tenth accurately.
Table 1.
|
ROMS |
ZX ROM |
Div 34th |
DEC-TO-FP |
BOTH |
Internal Storage |
||||
|
.5 |
7F 7F FF FF FF |
80 00 00 00 00 |
80 00 00 00 00 |
80 00 00 00 00 |
|
½ |
80 00 00 00 00 |
80 00 00 00 00 |
80 00 00 00 00 |
80 00 00 00 00 |
|
.25 |
7E 7F FF FF FF |
7F 00 00 00 01 |
7E 7F FF FF FF |
7F 00 00 00 00 |
|
¼ |
7F 00 00 00 00 |
7F 00 00 00 00 |
7F 00 00 00 00 |
7F 00 00 00 00 |
|
.125 |
7D 7F FF FF FF |
7E 00 00 00 01 |
7D 7F FF FF FF |
7E 00 00 00 00 |
|
1/8 |
7E 00 00 00 00 |
7E 00 00 00 00 |
7E 00 00 00 00 |
7E 00 00 00 00 |
|
.0625 |
7C 7F FF FF FF |
7D 00 00 00 01 |
7C 7F FF FF FF |
7D 00 00 00 00 |
|
1/16 |
7D 00 00 00 00 |
7D 00 00 00 00 |
7D 00 00 00 00 |
7D 00 00 00 00 |
|
.03125 |
7C 00 00 00 00 |
7C 00 00 00 01 |
7C 00 00 00 00 |
7C 00 00 00 01 |
|
1/32 |
7C 00 00 00 00 |
7C 00 00 00 00 |
7C 00 00 00 00 |
7C 00 00 00 00 |
|
.015625 |
7B 00 00 00 00 |
7B 00 00 00 01 |
7B 00 00 00 00 |
7B 00 00 00 00 |
|
1/64 |
7B 00 00 00 00 |
7B 00 00 00 00 |
7B 00 00 00 00 |
7B 00 00 00 00 |
|
.0078125 |
79 7F FF FF FF |
7A 00 00 00 01 |
7A 00 00 00 00 |
7A 00 00 00 00 |
|
1/128 |
7A 00 00 00 00 |
7A 00 00 00 00 |
7A 00 00 00 00 |
7A 00 00 00 00 |
|
.00390625 |
79 00 00 00 00 |
79 00 00 00 01 |
79 00 00 00 00 |
79 00 00 00 00 |
|
1/256 |
79 00 00 00 00 |
79 00 00 00 00 |
79 00 00 00 00 |
79 00 00 00 00 |
|
.001953125 |
77 7F FF FF FF |
78 00 00 00 01 |
77 7F FF FF FF |
78 00 00 00 00 |
|
1/512 |
78 00 00 00 00 |
78 00 00 00 00 |
78 00 00 00 00 |
78 00 00 00 00 |
|
.000976525 |
76 7F FF FF FF |
77 00 00 00 01 |
76 7F FF FF FF |
77 00 00 00 00 |
|
1/1024 |
77 00 00 00 00 |
77 00 00 00 00 |
77 00 00 00 00 |
77 00 00 00 00 |
Boolean Logic |
||||
|
.5 = 1/2 |
TRUE |
TRUE |
TRUE |
TRUE |
|
½ = .5 |
FALSE |
TRUE |
TRUE |
TRUE |
|
.25 = 1/4 |
TRUE |
FALSE |
TRUE |
TRUE |
|
¼ = .25 |
FALSE |
FALSE |
FALSE |
TRUE |
|
.125 = 1/8 |
TRUE |
FALSE |
TRUE |
TRUE |
|
1/8 =.125 |
FALSE |
FALSE |
FALSE |
TRUE |
|
.0625 = 1/16 |
TRUE |
FALSE |
TRUE |
TRUE |
|
1/16 = .0625 |
FALSE |
FALSE |
FALSE |
TRUE |
|
.03125 = 1/32 |
TRUE |
FALSE |
TRUE |
FALSE |
|
1/32 = .03125 |
TRUE |
FALSE |
TRUE |
FALSE |
|
.015625 = 1/64 |
TRUE |
FALSE |
TRUE |
TRUE |
|
1/64 = .015625 |
TRUE |
FALSE |
TRUE |
TRUE |
|
.0078125 = 1/128 |
TRUE |
FALSE |
TRUE |
TRUE |
|
1/128 = .0078125 |
FALSE |
FALSE |
TRUE |
TRUE |
|
.00390625=1/256 |
TRUE |
FALSE |
TRUE |
TRUE |
|
1/256=.00390625 |
TRUE |
FALSE |
TRUE |
TRUE |
Fractional Loop Steps |
||||
|
.5 |
CORRECT |
CORRECT |
CORRECT |
CORRECT |
|
.25 |
CORRECT |
WRONG |
CORRECT |
CORRECT |
|
.125 |
CORRECT |
WRONG |
CORRECT |
CORRECT |
|
.0625 |
CORRECT |
WRONG |
CORRECT |
CORRECT |
|
.03125 |
CORRECT |
WRONG |
CORRECT |
WRONG |
|
.015625 |
CORRECT |
WRONG |
CORRECT |
CORRECT |
|
.0078125 |
CORRECT |
WRONG |
CORRECT |
CORRECT |
Print values |
||||
|
.123456785 |
0.12345678 |
0.12345679 |
0.12345678 |
0.12345678 |
|
.100000015 |
0.10000001 |
0.10000002 |
0.10000001 |
0.10000002 |
|
.200000015 |
0.20000002 |
0.20000002 |
0.20000002 |
0.20000002 |
|
.876543215 |
0.87654322 |
0.87654322 |
0.87654322 |
0.87654322 |
|
.9999999948 |
0.99999999 |
1 |
0.99999999 |
0.99999999 |
Finally |
||||
|
0.1 storage |
7D 4C CC CC CC |
7D 4C CC CC CD |
7D 4C CC CC CC |
7D 4C CC CC CD |
|
1/10 storage |
7D 4C CC CC CC |
7D 4C CC CC CD |
7D 4C CC CC CC |
7D 4C CC CC CD |
|
1 storage |
81 00 00 00 00 |
81 00 00 00 00 |
81 00 00 00 00 |
81 00 00 00 00 |
|
2 storage |
82 00 00 00 00 |
82 00 00 00 00 |
82 00 00 00 00 |
82 00 00 00 00 |
|
5 storage |
83 20 00 00 00 |
83 20 00 00 00 |
83 20 00 00 00 |
83 20 00 00 00 |
|
10 storage |
84 20 00 00 00 |
84 20 00 00 00 |
84 20 00 00 00 |
84 20 00 00 00 |
The first section of the above table shows the internal format of a selection of numbers as stored in the ZX computers with the first column showing results for the standard ROM.
The Division Error (credit to Dr. Frank O’Hara)
This error, a failure to round the 34th bit in the division routine, leads to, for example;
0.5 having the floating-point form 7F 7F FF FF FF
but ½ having the floating-point form 80 00 00 00 00.
What the quoted book “Understanding Your Spectrum” fails to point out is that all the other decimal fractions from .25 down are rounded up too much leading to a failure of all comparisons of binary fractions less than a half. Similarly, for-next loops with a fractional step terminate before the limit value and, when printing decimal numbers, the window of accuracy is merely shifted downwards. The figures in the second column have been obtained from a ROM that implements the correction, as suggested, by jumping back further to the label DIV-34TH in the division routine.
The Spectrum and ZX81 can successfully calculate ½ and have an accurate representation of a half in their table of constants so it would be illustrative to see how the evaluation of .5 arrives at a less than accurate answer.
When the ZX81 and Spectrum encounter a decimal digit in a Basic Line or expression then the number is parsed left to right. Digits before the decimal point are converted using routine INT-TO-FP.
If there are no digits preceding the decimal point then a zero is stacked as the starting value.
Then the number 1 is placed in a variable and a loop entered for each digit found.
For the first digit the variable (1) is fetched divided by ten, giving the inaccurate result .1 (a tenth).
The result is then multiplied by the digit thereby compounding the inaccuracy. A half is calculated as .1 * 5.
On the next pass, the variable is further divided by ten giving one-hundredth for the multiplicand of the second digit.
It occurred to me that, rather than reduce the variable to an inaccurate component, it would be best to multiply the variable by ten and divide the digit directly by the variable.
So, if the first digit is 5, then fetch the variable 1, multiply by ten and then divide giving 5/10 which leads to the accurate representation of a half. This only requires two bytes to be altered in the ROM as follows.
2CDA NXT-DGT-1 RST 0018,GET-CHAR
CALL 2D22,STK-DIGIT
JR C,2CEB,E-FORMAT
RST 0028,FP-CALC
DEFB +E0, get-mem-0 ; fetch the variable (initially one)
DEFB +A4, stk-ten ;
DEFB +04, multiply ; (was +05, div) – now multiply
; giving 10,100, 1000 …
DEFB +C0, st-mem-0 ; store the variable for next time through
DEFB +05, division ; (was +04, mult) – now division
; for example 5/10, 9/10.
DEFB +0F, addition ; add to running total.
DEFB +38, end-calc ; end calculator mode.
RST 0020,NEXT-CHAR ; move to next character.
JR 2CDA,NXT-DGT-1 ; loop back to test for more digits.
The results for a ROM featuring this change are shown in the third column.
I then decided to produce a ROM featuring both changes. The results were initially very exciting with the internal representation being exact in all cases. I then hit a snag. The number .03125 was stored incorrectly. Furthermore the error was on the wrong side so that comparisons between .03125 and 1/32 didn’t work. This is something that the standard Spectrum performed correctly. The results are shown in the right hand column.
In conclusion, it can be seen that a number of modifications can affect the accuracy of ZX floating point division.
Altering the DEC-TO-FP routine to avoid imprecise intermediate values increases the accuracy without any known drawbacks. This particularly applies to the benchmark test ½ =.5.
Other schemes are not without their drawbacks, but, of the options available, the poorest choice is to implement Dr. Frank O’Hara’s division error correction on it’s own.
The original Spectrum ROM was not so bad as an all-rounder and there certainly never was a ‘division error’.
The Complete ZX81 ROM Disassembly Part B, Dr. Frank O’Hara, 1982.
Understanding Your Spectrum, Dr. Ian Logan, 1982.
The Complete Spectrum ROM Disassembly, Dr Ian Logan and Dr. Frank O’Hara, 1983
Das Sinclair Spectrum ROM, R. Arenz and M. Görlitz, 1984.
The Pocket Guide to the Sinclair Spectrum, Steven Vickers, 1984.
The ZX Programmer’s Companion, John Grant and Catherine Grant, 1984.