Skip to content

Latest commit

 

History

History
303 lines (225 loc) · 21.6 KB

zx82.md

File metadata and controls

303 lines (225 loc) · 21.6 KB

ZX82 Replacement ROM for ZX Spectrum

Introduction

In 1982, the ROM program with which the ZX Spectrum (both the 16k and the 48k versions) shipped was a proof-of-concept prototype, meant to be replaced as soon as the final version was ready. Alas, for various reasons it never happened. Several subsystems in the ROM remained unfinished and quite a few bugs remained together with legacy solutions from ZX81 which were planned to be upgraded for the more powerful machine.

This ROM attempts to be what the ZX Spectrum's final ROM could have been, should it have been finished. The Manual written by dr. Steve Vickers served as its primary specification. Both authors of the original ROM, dr. Steve Vickers and John Grant have been interviewed by email in the course of its creation. As much as possible, compatibility with existing ZX Spectrum software and hardware has been maintained by preserving important entry points. If you find some software or hardware that is not compatible with this ROM, please file an issue. I cannot promise to resolve it, but it will be certainly considered.

In addition to fixing bugs or finishing unfinished subsystems, this ROM also contains a few changes aiming at extensibility. These changes have been made in such a way as to not degrade performance compared with the ROM with which the ZX Spectrum shipped, mainly providing alternative execution paths upon error conditions.

This document describes all changes made to the ROM and is aimed at both users and developers.

Interrupt Subsystem

Non-Maskable Interrupt (NMI)

In the original ROM, the system variable NMIADD (address $5CB0) is initialized to zero, causing the NMI service routine to reset. Any change to its value causes non-maskable interrupts to be ignored. There is plenty of evidence in the ROM and in the literature that this is not the intended behavior. In this ROM, NMIADD is initialized to a warm restart routine returning the user to the command line, if possible (acting like a stronger BREAK) and the service routine has been modified to execute it upon NMI. If NMIADD is changed to zero, it will ignore NMI events.

Authors of extension ROMs that page over this ROM are advised to copy the service routine from this ROM so that NMI behavior does not depend on which ROM bank happens to be paged in and upon initialization point NMIADD to a small routine in RAM paging in the appropriate ROM bank before jumping to the intended NMI handler.

Maskable Interrupt (IM1)

All dependencies on the value of IY have been eliminated from the maskable frame interrupt service routine at address $0038. Thus, any Z80 register can be safely changed while interrupts are enabled. The functionality of the IM1 service routine has not changed: it increments the 24-bit frame counter and reads the keyboard.

Extensions that page over the IM1 interrupt service routine are advised to have their interrupt service routine set up in such a way that this ROM's interrupt service routine is also executed every frame.

The I/O Subsystem

I/O Abstraction Model

The I/O abstraction with streams and channels has not been finished in the original ROM; according to Steve Vickers, the code in there is a plug. In this ROM, it has been extended to the point that it allows writing device drivers for the screen and the keyboard, allowing real or virtual terminals, pipes, completely different video hardware and many other interesting and useful things. This is mainly achieved through making RST $10 call the output service routine with the carry flag set, while other functionality of the device is exposed through calling the output service routine with the the carry flag cleared. Such calls to the output service routine are called IOCTL.

By convention, IOCTL with register A holding zero resets the hardware attached to the channel. For the S channel, it clears the screen, aborts stateful output sequences (such as color controls), resets the pixel pointer to the origin, restores permanent attributes and places the caret to the top left corner. For the K channel, it only clears the console part of the screen, resets it to two rows, places the caret to the bottom left corner and aborts stateful input and output sequences. The P channel flushes the printer buffer in addition to aborting stateful output sequences.

Other IOCTL calls are not standardized, the same code might have a very different meaning for different devices. All access to abstract peripherial devices is routed through the appropriate channels' service routines.

Keyboard Inputs

The keyboard click sound has been moved to the input service routine of the K channel and it thus indicates that a typed character has been read off the channel. INPUT from other channels no longer emit a click after each byte read. Furthermore, setting system variable PIP (address $5C39) to zero disables keyboard clicks altogether. It is initialized to 1.

As a side effect, switching to and from Extended input mode also emits a keyboard click. Prompts such as scroll? or Start tape, then press any key... no longer crash upon pressing mode keys, but also respond with an audible click to any keypress. If you wish to record tape audio without these clicks, zero PIP first.

Control Characters

As one can expect reading the manual, cursor movement controls CHR$ 8 through CHR$ 11 move the caret, if sent to S or K channels. CHR$ 12 (ASCII form feed) clears the screen.

CHR$ 14 and CHR$ 15 indicate that the next character is a cursor, which should be displayed FLASHing or in INVERSE, respectively, for the editor cursor and the line cursor. Device drivers thus have great freedom in how they display these cursors. The line cursor is only displayed in automatic listing, explicit LIST and LLIST commands do not display it. The default S channel driver displays the line cursor in inverse, as can be seen in the mock screenshots of the manual. Alternative device drivers can highlight the entire program line, for example. Similarly, the editor cursor is now displayed through the output service routine and can thus be changed by the K channel driver.

The tabulator control CHR$ 6 also delimits INPUT values in addition to the newline CHR$ 13. This way, comma separated outputs in PRINT or LPRINT can be read into individual variables by INPUT as originally intended.

Hitting DELETE with the cursor after attribute control sequences deletes the whole sequence.

Graphics

CLS resets the S channel via IOCTL 0.

High resolution graphics commands PLOT, DRAW and CIRCLE are also routed through the S channel's output service routine as follows:

Attribute changes are sent into the channel as control sequences, with the carry flag set.

PLOT calls IOCTL 2, that is the output service routine with the carry flag cleared and register A holding the value 2. The two coordinates are on the calculator stack.

DRAW with two arguments calls IOCTL 3, DRAW with three arguments calls IOCTL 4 with the arguments on the calculator stack.

CIRCLE calls IOCTL 5 with the three arguments on the calculator stack. The circle-drawing algorithm has also been changed to a faster and more accurate one.

The BASIC Interpreter

The BASIC interpreter in the ZX Spectrum traces its lineage to the ZX80. Most of it has been inherited directly from the ZX81. According to both of its authors, John Grant and Steve Vickers, some solutions adequate for machines with 1 or 2 kilobytes of RAM were no longer adequate for a machine with 16 or 48 kilobytes of RAM, but were left in there nonetheless due to time pressure from Sinclair.

One such solution is program line lookup by sequential scanning from the beginning of the program. This is especially damaging because of the design decision to store all jump destinations into the BASIC program as a line number and statement number, which allows for CONTINUE even after modifications of the program; an otherwise commendable convenience of Sinclair BASIC. Thus, GO TO, GO SUB, RUN, RETURN, NEXT and CONTINUE all search through every line of the program from the beginning to their destination. For programs with hundreds or thousands of lines, this causes major slowdowns. This ROM implements binary search guaranteeing that program lines will be found in at most 12 iterations. The required line index takes 2 extra bytes per program line and 2 additional bytes for its own length. It is located between the command line's terminating $0D and the $80 (changed to $81 to indicate the presence of the index) separating it from the workspace. It is discarded immediately upon returning to BASIC editor and created lazily upon the first attempt to use it. In its absence, the system falls back to sequential scanning. This way, maximum compatibility is achieved with the ROM with which the ZX Spectrum ended up shipping.

The Calculator Subsystem

This subsystem in the ZX Spectrum consists of two major parts: the expression scanner mostly inherited from the ZX80 (written by John Grant) and the calculator VM mostly inherited from the ZX81 (written by Steve Vickers). Both are very mature, well written and optimized pieces of software, but the actual subroutines they call for implementing the opcodes of the VM and the operations in expressions are less then optimal in ROM with which the ZX Spectrum ended up shipping.

There are typically three kinds of problems that this ROM fixes:

  1. Some operations (typically those new to the ZX Spectrum, not present in ZX81) contain bugs that affect some corner cases not tested before the ZX Spectrum's launch.
  2. Some code inherited from the ZX81 has been very aggressively optimized for brevity at the expense of performance to fit into the 8k ROM of the ZX81. The Spectrum does not face that constraint, so performance can be substantially improved.
  3. Minor calculator bugs (typically resulting in loss of 1 significant bit of precision) inherited from the ZX81.

Arithmetics

While the ZX80 does all arithmetics in integers and the ZX81 does all arithmetics in floating-point, the ZX Spectrum can do both, switching between them entirely transparently for the user. This is a major innovation both in comparison to these earlier versions of Sinclair BASIC and in comparison to Microsoft BASIC, which requires the programmer to declare the type of each variable to be integer or floating point. The key insight of Steve Vickers is that in an interpreted language, checking the type of the variable causes exactly as much overhead as checking the type of the value. Unfortunately, this innovative mechanism mis-handles the corner case of -65536 in two instances: addition and truncation. This ROM fixes both by unambiguously treating this number in floating point form as the rest of the calculator does. Try PRINT -65000-536 and PRINT INT -65536 to see the problem with the original ROM.

Decimal to floating point conversion contains an inaccuracy inherited from the ZX81, fixed in this ROM. Try PRINT 1/2=0.5 to see the problem with the original ROM.

Binary literals with the BIN keyword (a novelty in the ZX Spectrum's expression scanner) impose constraints in the original ROM that are not mentioned in the manual: the maximum number of digits is 16 and only integer numbers are accepted. These constraints have been removed in this ROM by unifying the processing pipelines for decimal and binary literals. You can write one half as BIN 0.1 in this ROM and express 32-bit integers in binary.

Another inaccuracy inherited from the ZX81 fixed in this ROM affects division. Try PRINT (1/61)*61=1.

The random number generator using integer arithmetics in ZX80 has been converted to floating point in the ZX81 which slows it down considerably. The ZX Spectrum inherited the ZX81 version for no good reason. This ROM uses integer RND, which behaves exactly the same as the one in the original ROM, except being much-much faster.

Powers (accessible through the operator) and square root (SQR) have been subject to aggressive optimization for brevity in ZX81, affecting both accuracy and speed. The ZX81-inherited routines have been changed to more performant and more accurate versions in this ROM.

The frequently used constant table in both the ZX81 and the ZX Spectrum's original ROM are stored in compressed format which is quite surprising, given that its size together with the uncompression routine is larger than just storing these constants without compression, which also speeds up quite a few operations. This is exactly what this ROM does.

Stack-handing errors affecting STR$ and SCREEN$ operators have been fixed. Try PRINT "2"+STR$ 0.5 and PRINT "error"=SCREEN$ (0,0) with the original ROM to see the fixed errors in action.

User-Defined Functions

User defined functions through DEF FN instructions and the FN keyword in expressions is an improvement over the ZX81, which required storing user defined functions in string variables and evaluating them with VAL or VAL$, because it allows argument variables to be local to the function. It is also substantially faster due to moving syntax checking and numeric literal interpreting from run time to edit time.

However, the actual implementation in the ROM is barely usable, because of how it implemented local variables. The manual mentions that recursive functions are prohibited, but this implementation also prohibits composition with the same function; a constraint not mentioned in the manual and therefore considered a bug. If you enter 10 DEF FN a(a,b)=a+b that is supposed to define FN a as addition, PRINT FN a(2,FN a(1,1)) will output 3 with the original ROM, even though 2+(1+1)=4. This ROM fixes this problem. Not in the most optimal manner, but to maintain compatibility with software written for the original ROM. The lack of tail calls and conditional evaluation (except by ZX81-style trickery with VAL or VAL$) leave the manual's advice against recursive functions reasonable.

Variable Lookup

The handling of numeric variables with long names, while inherited from ZX81, is a surprisinly buggy part of the expression scanner in both computers' ROM. Thus, in ZX82 it has been re-written in a way that fixes its bugs and substantially improves performance of variable lookup for both long variable names and short ones.

Extensibility

Many solutions in the ZX Spectrum's operating system have been designed to be extensible, but the rush to market prevented the authors from defining clean APIs for extensions. One such API in this ROM is the channel driver API detailed above, allowing for driviers for different video hardware, keyboards, terminals and so on. Another such API has also been defined for extending the BASIC interpreter as follows:

The use of extended features not present in this ROM would result in an error condition. Thus, the presence of extensions is checked before the error report and an API call is made that can be used to implement the extension. This implies no performance penalty for already implemented functionality, since only execution paths that would result in an error report have been modified. There are exceptions from this rule, but they are, in my opinion, justified.

In particular, extensions are turned on by setting bit 4 of system variable FLAGS (address $5C3B); this is in line with how the original 128k BASIC and its derivatives indicate their interpreter being active. Extensions can be implemented in ROM (or RAM) banks paged in the $0000..$3FFF address space, so the extension API has been defined in such a way to allow for this. This ROM makes no assumptions about the actual bank switching mechanism; any such mechanism can be used.

A bank switching routine must be placed at the address $5B00 and it must begin with a PUSH AF instruction. It should switch memory banks both ways. This is in line with the original ROM of 128k ZX Spectrums and their derivatives, though implementers of ZX82 extensions are advised not to disable interrupts like the original 128k ROM does, as it might result in unpredictably missed frame counter increments and key press events. The P channel driver in this ROM allows for a safe relocation of the ZX Printer buffer by changing the high byte (address $5C81) of system variable PR_CC.

A jump table is to be placed in the extending ROM (or RAM) beginning with the address $3CB7. If an extension hook is not to be used, it must jump right back to $5B00. If the extending memory is only 8 kilobytes (like in the case of Timex 2068), the bank switching routine at $5B00 should reset bit 13 of the return address before returning to the extending memory bank.

Writing New Device Drivers

When the I/O service routines of channel drivers are called, this ROM is paged in the $0000..$3FFF address range. Thus, if you wish to implement channel drivers in memory banks in this address range, you should place a short routine in the RAM paging in the appropriate memory bank and jumping there. Before returning to the address on top of the stack, the device driver must make sure to page back this ROM. Channel drivers, as well as auxiliary memory paging routines are allowed to change BC, DE and HL registers (but not their shadow counterparts).

Extending the Expression Scanner

The following error conditions have been hooked in the expression scanner for extensibility:

  • Digits outside of the 0..9 range in converting numeric literals into binary. This allows number systems with base larger than 10. Particularly useful for hexadecimal literals.
  • Prefix operators (i.e. functions) outside of the CODE..NOT range. Allows defining new functions.
  • Infix operators with mismatched types, e.g. multiplying a string by a number.
  • Infix (or postfix) operator outside of the predefined set.
  • Improper closing of parentheses. This allows for multiple arguments for originally single-argument functions.

Additionally, global variable lookup is prepared for string variables with long names. They are stored in the VARS area very similarly to numeric variables with long names, except that the last character in their name is a $ sign rather than a letter. It is followed by a zero byte. The string's length and the string itself follow just like in the case of string variables with a single letter name. Such variables are found, if they are contained in expressions and properly skipped, when looking for something else. However, the syntax checker does not allow them in line with the ZX Spectrum's manual. It will err upon encountering the $ after the long variable name, interpreting it as a postfix operator not present in the infix operator table. Thus, in order to allow long-named string variables, this hook must be used as follows:

In run time, it should give a 2 Variable not found report. During syntax check, it should give a C Nonsense in Basic error, if it follows a string expression or value (bit 6 of FLAGS clear) or a numeric expression (register D zero) or a numeric literal (DE pointing at STKEND). Otherwise, it is following a long-named numeric variable, turning it into a long-named string variable. Thus, bit 6 of FLAGS must be cleared indicating a string and a return should be made to S-OPERTR (label L2723) to check infix/postfix operators again. Note that it is best to check for $ last among such operators, because then it will impose no performance penalty in run time.

Adding New BASIC Instructions

Originally, instruction token codes are $CE (DEF FN) or more. Any code smaller than that results in a C Nonsense in BASIC report, which has been hooked in ZX82. Any code between $21 and $CD except $3A (which is the statement-separating colon :) can be interpreted as an instruction, allowing for a maximum of 172 additional instructions.

Of course, channel output service routines also need to to output the appropriate token codes.

Extending Existing BASIC Instructions

The following error conditions have been hooked in the BASIC interpreter that allow for extensibility:

  • Wrong argument separator
  • End of statement not colon or newline
  • POKE instruction's second argument is a string

In addition to this, the signle-letter variable argument of DIM, FOR and NEXT instructions has been hooked as well as the variable-to-be-assigned in LET, READ and INPUT instructions. This goes against the design principle of only hooking error conditions motivated by not impairing performance compared to the original ROM. However, variable lookup has been sped up so much by fixing its bugs that it is faster than the original even with these hooks.

Adding New Reports and Changing Existing Ones

There is a hook in the main execution loop just before printing the report message, allowing for changing existing error reports (e.g. localization) and adding new ones.

Adding Local Contexts and Variables

The check for the end of the GO SUB stack requires a $3Exx word on top of the CPU stack just like in all iterations of Sinclair BASIC beginning with ZX80, but ZX82 actually places a $3E00 word there. Thus, various local contexts and local variables can be put on the CPU stack, guarded by a $3Exx marker, where xx is different from zero. This will result in RETURN failing in a hooked error condition which can be used to reclaim the local context.

The lookup of such local variables is enabled by writing 1 into the high byte of DEFADD system variable (address $5C0B), resulting in a hook call before searching global variables. Normally, it is either 0 or the high byte of a pointer to the arguments of a user defined function, which is necessarily $5C or higher.

Other Possibilities