BUGS IN THE ROM

Some of the Spectrum's "undocumented features"

The Spectrum, as you may well know, is a machine of very many bugs. Some of these bugs crop up only in BASIC, some occur only in the use of machine code, while others are manifest all the time. Though the ROM (or ROMs, as there is more than one type of Spectrum) cannot be written to, and therefore the bugs cannot in general be cured, knowledge of their existence can certainly help us to avoid them, or get round them in some way.

This article is therefore a compilation of all the bugs that I know of which occur in the 16K and 48K Spectrums, the 128 and 128+2, and also the ZX Interface One which runs the microdrives.

It is almost impossible to classify bugs in any sensible manner. What I have done is to group them into fairly large and ambiguous clumps such as "PRINT errors", "CALCULATOR errors", and so forth. Where possible credit has been given, but no doubt other people will have made simultaneous discovery of these bugs - this is always the way.

I shall start off with three bugs which I couldn't fit into any category at all.

MISCELLANEOUS ERRORS

The CLOSE # Bug
(from Understanding Your Spectrum, by Martin Wren-Hilton)

The CLOSE # bug is present on all Spectrums unless a ZX Interface One is attached. If you attempt to close a stream which has not previously been opened by using the BASIC CLOSE statement then the system will almost invariably crash. More often than not causing a system reset. The presence of the ZX Interface One cures this bug by transferring control to its own ROM (the Shadow ROM) whenever the program counter reaches address 1708. The bug is caused by the table in the 48K ROM at address 1716 not being terminated by a 00 byte. It is interesting to note that Sinclair could have cured this bug in the Spectrum 128 - after all, they did make a few other alterations to the ROM - but alas they chose to leave the bug in place despite its serious consequences.

The PLOT bug
(from ZX Computing Monthly - Toni Baker)

The BASIC PLOT statement suffers from the fact that it will accept negative co-ordinates. In fact the statement PLOT X, Y really turns out to mean PLOT ABS (X), ABS (Y). This is caused by a failure to test the D and E registers on return from the CALL_STK_TO_BC instruction at address 22DC.

The SCREEN ONE bug
(from ZX Computing Monthly - Toni Baker)

The Spectrum 128 offers a choice of two different areas of memory which may be used to hold the television picture. These are called SCREEN ZERO (the normal one, at address 4000) and SCREEN ONE (the alternative, at address 7C00). The instruction POKE 23388, 24 will activate screen one, whereas POKE 23388, 16 will activate screen zero. The bug occurs in the rare circumstances of the silicon disc area becoming full (in which case files may collide with the screen) or if more than 217 files, however small, are SAVE!d (in which case the catalogue collides with the screen. The bug is caused by the complete failure of the RAMdisc system to recognise the existence of screen one. The following BASIC program demonstrates this bug and is fun to watch.

10 POKE 23388, 24: REM activate screen one
20 FOR I = 1 TO 562
30 SAVE! "F" + STR$ I CODE 0, 1
40 NEXT I
50 PAUSE 0
60 POKE 23388, 16: REM activate screen zero

PRINT ERRORS

The CHR$ 9 error
(from Understanding Your Spectrum, by Dr. Ian Logan)

CHR$ 9 is supposed to print as "forwardspace" (the opposite of backspace). In other words the effect of PRINT CHR$ 9 is supposed to be that the print position is moved rightward by one character square without altering the contents of the screen. Sadly, this does not work. Not only does the print position not move at all, but the character at the current print position is accidentally recoloured with the current colours. The first of these faults is due to the fact that the cursor right subroutine at address 0A3D ends with RET, instead of with JP 0ADC, PO_STORE, however the second fault is due to sloppy thinking as the system variable MASK_T should have been assigned with FF and then restored.

The CHR$ 8 error
(from Understanding Your Spectrum, by Dr. Frank O'Hara)

CHR$ 8 prints as "backspace" - that is - when printed it will move the print position leftward by one character square. However - if the current print position is AT 1, 0; then backspace will not work. Furthermore, it is possible to backspace from position AT 0, 0; (which should not be possible) and this can produce some interesting results. The bug is caused by the cursor-left subroutine at address 0A23 testing for line one instead of line zero. The byte at address 0A33 should be 19h instead of 18h.

The STR$ error
(from Understanding Your Spectrum, by Tony Stratton)

This is a bug which manifests itself in both BASIC and machine code. In BASIC the statement PRINT "BUG"+STR$ 0.5 will simply print "0.5". In machine code the bug manifests itself in the PRINT_FP subroutine at address 2DE3, whose purpose is to print the floating point number at the top of the calculator stack to the current channel. This subroutine is called by the STR$ routine and in this way the bug also appears in BASIC. The bug occurs whenever the number at the top of the calculator stack (or the argument of STR$) lies between -1 and +1 exclusive (excluding zero). In such a case an erroneous zero is left at the top of the calculator stack, and it is this which causes all the problems. In BASIC the bug can be avoided by using temporary variables - e.g. LET A$=STR$ 0.5: PRINT "BUG"+A$ will work. In machine code it is advisable to use the following subroutine in place of PRINT_FP, as this takes the bug into account.

2A,65,5C  PRINT_FP_OK  LD   HL,(STKEND)    ; HL points to end of calculator stack.
22,5F,5C               LD   (X_PTR),HL     ; Store in system variable.
CD,E3,2D               CALL 2DE3; PRINT_FP ; Print the floating point number.
2A,5F,5C               LD   HL,(X_PTR)     ; HL points to end of calculator stack +5.
11,FB,FF               LD   DE,FFFE        ; DE = -5.
19                     ADD  HL,DE          ; HL points to end of calculator stack.
22,65,5C               LD   (STKEND),HL    ; Restore (STKEND) to its correct value.
FD,36,26,00            LD   (X_PTRhi),00   ; Cancel error pointer.
C9                     RET                 ; Return.

The COLOUR COMMANDS error
(from Master Your ZX Microdrive, by Andrew Pennell)

If any channel other than the screen or the keyboard is selected as the current channel (i.e. if the last thing PRINTed was to a channel other than "K" or "S") then the permanent colour commands will erroneously transmit the colour control codes to the last channel used. The bug is caused by the failure to select channel "S" at the start of the subroutine CO_TEMP_1 at address 21E1. It is possible to get round the problem by preceding each permanent colour command by a PRINT statement (e.g. PRINT;: INK 4).

The COLOUR CONTROLS error
(from ZX Computing Monthly - Toni Baker)

If any non-standard channel (e.g. a user-defined channel) is selected as the current channel then the colour statements such as PAPER 4 (etc.) will produce report C: Nonsense in BASIC if the channel output subroutine returns with the carry flag set. This bug is only visible in machine code. To cure the problem it is necessary to ensure that all channel output routines return with the carry reset, at least for control codes 10h to 15h and their second parameter.

The RS232 COLOUR COMMANDS error
(from ZX Computing Monthly - Toni Baker)

This is for users of 128K machines only. On both the Spectrum 128 and the Plus2 the command LPRINT INK 4 will produce error report C; Nonsense in BASIC. There are two reasons for this. Firstly, the new channel "P" (the built in RS232 socket) erroneously expects the colour controls to be followed by two parameters instead of just one, and secondly the ROM routine sets the carry flag (oops! see previous error). Sinclair may like to know that the byte at 086D (128) or 088C (128+2) should be 01 instead of 02, and, further, that the byte at 087C (128) or 089B (128+2) which currently reads CCF should be replaced by AND A.

CALCULATOR ERRORS

The DIVISION error
(from Understanding Your Spectrum, by Dr. Frank O'Hara)

The BASIC statement IF 1/2<>0.5 THEN PRINT "BUG" is sufficient to show that there is a bug somewhere in the works. The bug is in the division routine, which fails to register the thirty-fourth bit of a division. The JR displacement at address 3200 directs control to the label DIV_START instead of DIV_34TH. This means that numbers may not be correctly rounded up. You can avoid any major problems by using IF ABS (X-Y<2.15E+9) instead of IF X=Y.

The -65536 error
(from Understanding Your Spectrum, by Dr. Ian Logan)

The ROM is inconsistent about the calculator five-byte form of the number -65536. Some parts of the ROM assume that only numbers in the range -65535 to +65535 may be treated as integers, whereas other parts of the ROM allow an integer from -65536 (as opposed to a full loading point form). This error can be demonstrated by the now famous statement PRINT INT -65536 which gives -1.

The SCREEN$ error
(from Understanding Your Spectrum, by Stephen Kelly and others.)

The BASIC routine for calculating SCREEN$ accidentally leaves a duplicate entry of the string result at the top of the calculator stack. This means that the BASIC statement IF "X"=SCREEN$(0,0) THEN PRINT "BUG" will invariably print "BUG" no matter what happens to be on the screen at the time. The bug is fortunately not present in machine code, and calling the subroutine S_SCRN$_S will evaluate SCREEN$ correctly, leaving the stack correct. As with the STR$ error the best way to avoid it in BASIC is to use temporary variables, e.g.

LET A$=SCREEN$(0,0):IF "X"=A$ THEN PRINT "BUG" will work correctly.

The MOD/DIV error
(from ZX Computing Monthly - Toni Baker)

Calculator code 32 (initiated by RST 28) is supposed to remove two items from the calculator stack - X and Y, say, and replace them by the quantities X MOD Y, X DIV Y in that order. [Note: X MOD Y means X - Y * INT (X/Y), whereas X DIV Y means INT (X/Y) ]. Unfortunately, it fails to take into account the fact that the INT subroutine corrupts calculator memory zero whenever its argument is negative. This means that the quantity X MOD Y will be incorrect whenever X/Y is negative. The bug could have been cured had Sinclair ensured that the MOD/DIV subroutine at address 36A0 utilised memory one instead of memory zero.

The E-TO-FP error
(from ZX Computing Monthly - Toni Baker)

Calculator code 3C (initiated by RST 28) is supposed to multiply the item at the top of the calculator stack by the factor 10^A. Unfortunately, unlike the B register, the A register is not preserved by RST 28, and so by the time the byte 3C is reached the A register will have already been corrupted. The only way out of this problem is to use endcalc/LD A,XX/CALL 2D4F, E_TO_FP/RST 28 instead of the byte 3C in your code.

The INKEY$#0 error
(from ZX Computing Monthly - Toni Baker)

Normally, stream zero represents the keyboard, so we would expect INKEY$#0 to be similar in operation to INKEY$ (without a stream number). This, however, is not the case, and INKEY$#0 almost invariably produces an empty string, making it completely useless. Note also that calculator code 1A evaluates INKEY$#X (where X is the item at the top of the calculator stack) and this too will be useless whenever stream X represents the keyboard. The bug is in the subroutine at address 1634 which makes channel "K" the current channel. The instruction RES 5,(FLAGS) at address 1638 erroneously cancels out any key press which would otherwise have been detected. The bug could have been cured in the READ_IN subroutine at address 3645 by preserving the value of (FLAGS) during the call of CHAN_OPEN.

The FN error
(from ZX Computing Monthly - Toni Baker)

When a user-defined function is evaluated the value of the system variable (CH_ADD) which stores the address of the next character to be interpreted, is saved on the machine stack instead of in one of the dynamic variables such as (X_PTR) during the evaluation of the function. This means that any user-defined function which causes the BASIC program area to move up or down in memory will cause error report C: Nonsense in BASIC. This is not normally possible, but exceptions can occur if the DEF FN statement contains the function USR, and could also conceivably happen with the function INKEY#X. To avoid this bug you must ensure that any machine code subroutine which is called by a DEF FN statement does not disturb the BASIC program area (e.g. it is impossible to define a function FN D (X,Y) which would delete BASIC lines X to Y inclusive without also manipulating the machine stack).

The USR error
(from ZX Computing Monthly - Toni Baker)

Machine code programmers will be so familiar with this one that they don't even think of it as a bug any more. The problem is that the HL' register, whose value is required by the RST 28 sequence at address 2756, is not preserved by the USR subroutine at address 34B3. This means that any machine code subroutine which disturbs the value of HL' (it should contain 2758) will crash the system upon return. To avoid this bug simply ensure that HL' contains 2758 upon execution of the final RET statement of any machine code subroutine called by USR.

EDITING ERRORS

The SCROLL? error
(from Understanding Your Spectrum, by Dr. Ian Logan)

Whenever a prompt message such as "scroll?" Or the cassette messages appear, the Spectrum waits for you to press a key. The problem is that if you press the wrong key, things go wrong. Pressing TRUE VIDEO, INV VIDEO, CAPS LOCK, GRAPHIC or EXTEND MODE causes the previous edit line to appear at the bottom of the screen, with the 48K flashing cursor (even on 128K machines). This is because the KEY_INPUT subroutine at address 10A8 is designed to deal with CAPS LOCK, mode changing, and colour control parameters, however this is inappropriate for prompt messages.

The CURRENT LINE CURSOR error
(from Understanding Your Spectrum, by Paul Harrison)

This bug is applicable only on the 16K and 48K Spectrums. 128s don't have this problem because the EDIT key doesn't do quite the same thing. Type 9000 PRINT/9001/EDIT (note that "/" stands for ENTER) and, provided there are no lines greater than 9000, you'll see the problem - a GREATER-THAN symbol appears in the edit line. The bug occurs in the OUT_LINE routine at address 1855. The bug could have been avoided if the subroutine had refused to print the GREATER-THAN cursor whenever bit four of (FLAGS2) is set.

The LEADING SPACE error
(from Understanding Your Spectrum, by Dr. Ian Logan)

Bit zero of (FLAGS) is supposed to be set whenever a leading space is not required for a keyword token. Unfortunately, the ROM is not consistent about its use, as the one-line program

CLS: FOR I=1 TO 5: PRINT CHR$ 244: NEXT I

will prove. The problem would be solved if the flag were to be set every time CLS were executed, or whenever a control character 00 to 1F were printed.

The K-MODE error
(from Understanding Your Spectrum, by Chris Thornton)

This is another one for 16K and 48K Spectrums only. If, when editing program line, the cursor is a flashing "K" then the editor is said to be in K-mode. This means that the next key you press will be interpreted as a keyword, so that if you press "P" for instance, then you'll get PRINT. The problem is that if you hold a key down so that it repeats then K-mode remains in force. This means that you could end up with a line like "NEXT NEXT" instead of "NEXT N". The bug is caused by the K_REPEAT routine at address 0310. To work properly the routine would have to subtract hex A5 from the key value whenever this was greater than hex E5. This would replace keywords with capital letters.

The SYNTAX CHECK error
(from Master Your ZX Microdrive, by Andrew Pennell)

On the 16K and 48K Spectrums the four keywords ERASE, MOVE, FORMAT and CAT have incorrect syntaxes. The statements ERASE (string) : MOVE (string), (string) : FORMAT (string) : CAT will be accepted as program lines, but will give error messages if you try to run them. On the 128K Spectrums the statement MOVE (string), (string) will be accepted as a program line, but treated as a REM statement on running. The bug in the 48K machine is due to the fact that the syntaxes of these Interface One commands were incorrectly anticipated. On the 128 a wholly new syntax routine is included, and the commands ERASE and CAT have been taken over for use with the silicon disc system, whilst FORMAT has been taken over for use with the RS232. MOVE, however, is not used at all, and the original false syntax has been reproduced in an entirely new syntax routine in the new ROM.

HOOK CODE ERRORS

Hook codes are used to call subroutines in the Interface One's Shadow ROM. There are two errors in these:

The READ-N error
(from Master Your ZX Microdrive, by Andrew Pennell)

Hook code 2F (READ_N) was intended to read a block of data from the "N" channel (the local area Network). The carry flag is assigned to indicate success or failure, but unfortunately the carry flag is subsequently destroyed by a call to the border restore routine.

The SET-T-MCH error
(from Master Your ZX Microdrive, by Andrew Pennell)

On version one issues of the Interface One, hook code 2B (SET_T_MCH) accidentally calls the OPEN_M routine instead of SET_T_MCH, making it identical in operation to hook code 22. The bug has been cured in later editions of the Interface One, and in these later editions it correctly performs its task of creating a temporary "M" channel in the channel information area.

INTERRUPT ERRORS

The MONOPOLISING OF IY error
(from ZX Computing Monthly - Toni Baker)

Though not strictly an "error" as such it is worth noting that the Spectrum's interrupt routine contains the instruction INC (IY+40), which is intended to increment the high byte of the (FRAMES) variable (when necessary), however to do this IY must have a value of 5C3A at all times while interrupts are enabled (unless of course you define your own interrupt routines). Unfortunately, IY is not temporarily assigned with this value within the interrupt routine itself. The effect of this misdemeanour is that the value contained by IY must never be changed (unless interrupts are disabled or an alternative interrupt routine supplied). This restricts us, because it means that a machine code register which could have been available to us now must not be touched. It is possible to cure this condition by using the following program as an interrupt routine.

FD,E5       INT_ROUTINE  PUSH IY       ; Preserve the IY register.
FD,21,3A,5C              LD   IY,5C3A  ; IY contains value required by normal interrupt routine.
FF                       RST  38       ; Execute normal interrupt routine.
FD,E1                    POP  IY       ; Restore IY register.
C9                       RET           ; Return.

The SWAP error
(from ZX Computing Monthly - Toni Baker)

The SWAP routine which switches between the two ROMs of the 128K Spectrum invariably enables interrupts, which may not always be desired. Fortunately, this error may be cured by the following program:

                         ORG  5B00
C3,??,??    SWAP         JP   SWAP_OK    ; Redirect control to revised routine.

                         ORG  ????
F5          SWAP_OK      PUSH AF         ; Stack A and F.
C3                       PUSH BC         ; Stack B and C.
ED,5F                    LD   A,R        ; P/V flag = interrupt status.
F5                       PUSH AF         ; Stack interrupt status.
01,FD,7F                 LD   BC,7FFD    ; BC=port number required for paging.
3A,5C,5B                 LD   A,(BANK_M) ; A=current page.
EE,10                    XOR  10         ; Complement 'ROM' bit.
F3                       DI              ; Disable interrupts (in case an interrupt occurs between the next two instructions.
32,5C,5B                 LD   (BANK_M),A ; Store revised page.
ED,79                    OUT  (C),A      ; Actually change ROM.
F1                       POP  AF         ; P/V flag=former interrupt status.
E2,??,??                 JP   PO,SWAP_OK_2 ; Jump if interrupts previously disabled.
FB                       EI              ; Re-enable interrupts.
C1          SWAP_OK_2    POP  BC         ; Restore B and C.
F1                       POP  AF         ; Restore A and F.
C9                       RET             ; Return.

Simply write the instruction JP SWAP_OK to location 5B00, where SWAP_OK is the address of the program.

The PLAY error
(from ZX Computing Monthly - Toni Baker)

At address 09CD in the 128, or 09EC in the 128+2, the STK_FETCH subroutine is called from the old ROM. At this point interrupts are disabled and IY is now being used as a pointer to the master PLAY information block. Unfortunately, because of the two bugs listed above, interrupts are enabled during the STK_FETCH call, leaving IY containing the wrong value. This means that if an interrupt were to occur during execution of the subroutine then there would be a one in 65536 chance that (IY+40) will be corrupted - this corresponds to the volume setting for music channel A. In practice this circumstance is very rare indeed. Rewriting the SWAP routine as above will cure this bug.

ERROR ERRORS

This group of errors are bugs in the Spectrum's own error handling routines!

The INVALID FILENAME error
(from Master Your ZX Microdrive, by Andrew Pennell)

If the Interface One is attached then the error message "Invalid filename" is replaced by the message "Nonsense in BASIC", which is not particularly helpful.

The INVALID DEVICE EXPRESSION error
(from ZX Computing Monthly - Toni Baker)

Most ironic of all is the curious error which only occurs with the combination of ZX Interface One and Spectrum 128 or 128+2. If you have a BASIC line such as 1000 OPEN 4, "X" (the line number must be greater than 999) then you should by rights get the error message "Invalid device expression, 1000:1" - indeed, true to form, the Spectrum does indeed attempt to produce this message for us. Unfortunately, the message is just too long to fit on a line! The thirty three characters in the error message will not fit on a line of only thirty two squares. The Spectrum 128's marvellous little full screen editor only allows for a lower screen of two lines (including one blank line above the error message), but this particular error message takes three lines, not two! If you're using only the lower two lines as the editor then the effects of the bug aren't too bad: you merely get a few spurious things happening at the bottom of the screen. If you're using the whole screen as an editor however then the bug is FATAL, causing an unavoidable system reset in 48K mode! A shame really - the poor thing was only trying to tell you about a bug in your BASIC. Never mind - after all nobody is perfect.

NEW ERRORS

These errors have been reported after the original article was published.

The CLEAR PRINTER BUFFER Bug (48K mode only)

This routine should really start by checking bit 1 of FLAGS to see if stream 3 is in use. As a consequence of ignoring this the printer buffer is needlessly cleared after the COPY command and also when the COPY command is interrupted by pressing BREAK. Even worse, if stream 2 is in use, i.e. COPY, then the DFCC system variable is loaded with the address of the start of the printer buffer as the CLEAR-PRB routine exits to PO-STORE via CLS-SET. This will lead to an "Out of screen" error if subsequent PRINT commands do not include an AT clause. e.g.

PRINT : COPY : PRINT

The rest of the ROM is so well developed that this check must once have been in place. Instead there's a half-baked attempt to circumvent the use of 23681.

The PRCC Error (48K mode only)

This is more a bug in the manual although the ROM code is untidy (see above). The manual says that 23681 is unused whereas it is the high byte of PRCC and should never be altered. At one time the printer buffer followed the system variables as it did on the ZX81. With a few more system variables for network and RS232 the high byte could increment as characters were printed. When the printer buffer was moved to it's current location an attempt was made to set the low byte of the system variable directly within the CLEAR-PRB routine. However this instruction is superfluous and the variable is updated in full by PO-STORE. This can be demonstrated with

10 POKE 23681,64 : LLIST

This uses the first 256 bytes of the screen as a printer buffer and if you look carefully at the screen, you may be able to make out the listing.

The NEW COMMAND Bug (128K mode only)

The initialisation routine of the 128K ROMs (Sinclair 128K, Amstrad +2, Amstrad +3, and possibly +2A) intentionally set main memory to the following values
60435,0
65316,0
65317,236
This is very hard to detect as the only non-zero byte is immediately below the machine stack and soon overwritten. The NEW command unfortunately also sets these locations even if they are above RAMTOP. Consequently utilities which are designed to survive NEW don't.
Examples are Zeus assembler, Devpac, Basic extensions and other languages.

MAIN-4, (48 ROM-1303h)

There are two problems with the MAIN-4 routine. First if it is called with interrupts disabled it will crash because of the HALT instruction. Second it is missing a SET 1,(IY+1) instruction (which tells the Spectrum to use the printer instead of the screen). The result is that typing LPRINT "bug";: PRINT, pressing enter and then typing PRINT will generate an Out of screen error.

KEYCLICK, (128 ROM-26F1h)
(Ian Collier)

On the +3, the 128 (and presumably the +2) also has the wrong memory location to check the length of the keyclick. This is why it sounds deeper in 128 Basic.

LOWER SCREEN EDITING, (128 ROM-2DA1)
(Ian Collier)

Also spotted on the +3, replicated at this address on the 128 (and somewhere else on the +2).

128 MENU (128 ROM)

Not exactly a bug. The menu is centred over the copyright message on the 128 but on later machines it no longer looks centred because of the length of the copyright message. Amstrad only needed to change four bytes to centre it on the screen!

WRITE TO ROM (48 ROM-33FEh)

Not so much a bug as poor coding. The ROM tries to write four bytes starting at 0000h. On a real Spectrum there is usually no effect because you can't actually write to the ROM, but if a copy of the ROM is really in RAM (to give better compatibility on a +3 or +2A for instance), the 'ROM' could be overwritten.

The SCROLL? error (48 ROM)

Not only does this affect the 'Scroll?' prompt, but also the 'Start tape and press any key.' prompt. Pressing 'any' key will not always work!

The RAM test (48 ROM-11CEh)

Again, not a bug as such. The RAM test had a secondary function on the original Spectrum of detecting if it was a 48K or 16K machine but as it only tests the first 48K of RAM serves no purpose on 128 machines and could have been replaced with a more efficient memory wipe routine.

The EDIT key bug (48 ROM-1860h)

If you enter a line 9000 REM bug. Then enter 9001 and then press EDIT the '>' cursor appears in the line.

The DRAW commands (48 ROM)

The PLOT, DRAW, CIRCLE and POINT commands are limited to the normal printable area (32x22 characters). There was no reason to stop users using the lower two lines of the screen.

The GOTO bug (48 ROM)

The result you get of attempting to GOTO a non-existent line depends on whether it is above or below 16384. It appears the ROM originally supported line numbers up to 16383 and the only logical reason for limiting it to 9999 is the problem of error messages going off the screen.

The RESTORE bug (48 ROM)

Basically, typing RESTORE 61250 will put the machine in a continual loop, effectively crashing it.

The CHR$ 27 bug (48 ROM-09F7h, only affects 128)
(Andrew Owen)

The Spectrum character set is just like normal ASCII (except for the POUND and COPYRIGHT symbols) from code 32 to 127. But, codes lower than those are used for setting colour and position and so on, while some are not used at all. The upshot of which is, 128 machines cannot generate special printer codes (using code 27) from BASIC. Changing one byte from 20h to 18h would allow the use of codes 24-31, and while they would print garbage on the screen, they would be able to control a printer connected to the RS232 port (or parallel on the +3/+2A).

My thanks to Geoff Wearmouth, Ian Collier and Andrew Owen for expanding my original list and for their continued work on the Spectrums previously undocumented features.