In part 1 we covered the history of Prestel and Prism, and in part 2 we cracked open the VTX5000 to examine the hardware. Now how did they squeeze a viewdata terminal app into 8KB...

The ROM Map

The 8K ROM at $0000-$1FFF is packed tight:

Address RangeSizeContents
$0000-$002639 bytesJump table (API entry points)
$0027-$003818 bytesRST $38 maskable interrupt handler
$003A-$11004,295 bytesEmbedded Spectrum BASIC program
$1101-$1B1C2,588 bytesZ80 machine code
$1B1D-$1B1F3 bytesPadding
$1B20-$1CFF480 bytes5×8 pixel character font (96 chars)
$1D00-$1F82643 bytesStartup splash screen (Viewdata frame)
$1F83-$1FCB73 bytesConfiguration data and defaults
$1FCC-$1FFF52 bytesROM paging trampolines

A whopping 4K of the ROM is a BASIC program we'll get to in a minute and half a KB goes on that SAA5050-like 8x5 font and another half-KB on a Micronet 800 splash screen.

That leaves just 3KB and the most interesting part is 2.5KB of optimized Z80 handling the performance-critical modem I/O, Viewdata rendering, keyboard scanning, ring buffers etc.

Memory Paging & Trampolines

Before we get to that though we'll look very quickly at the glue code to manage the switching between ROMs.

The Spectrum's own ROM sits at $0000-$3FFF and unlike say, the BBC Micro, there isn't a region for third parties to jump in and out of. So, the VTX5000 takes over that address space by way of the Spectrum expansion port ROMCS line. ROM Chip Select, as it is known, disables the internal ROM when active, allowing an external device to respond to reads in that address range instead. As mentioned in part 2 the Intel 8251 UART otherwise-unused RTS line is used to control this paging:

LD A,$15        ; Command: RTS=0 → VTX ROM paged in
OUT ($FF),A

LD A,$35        ; Command: RTS=1 → Spectrum ROM restored
OUT ($FF),A

The problem with paging is that you don't want code changing underneath where you're currently executing. If you have multiple banks like a 128K machine not a problem just page out a different bank but when you're dealing with a single bank such as between the Speccy and VTX ROMs that occupy the same space you need a different solution.

The VTX ROM solves this with trampoline routines - small routines that live outside the being-paged bank so they're safe. During cold start, the init code copies 128 bytes from the end of the ROM ($1FCC-$1FFF) to the top of RAM ($FFCC-$FFFF on a 48K Spectrum, or $7FCC-$7FFF on a 16K). These trampolines handle all the actual ROM switching:

; Trampoline at $FFED (copied from $1FED):
; Called by RST $38 handler to page in Spectrum ROM for interrupts
TRAMP_INT_SPECROM:
    LD A,$35            ; Page in Spectrum ROM
    OUT ($FF),A
    EX (SP),HL          ; Bus-cycle delay to allow bus to settle
    EX (SP),HL
    DEFB $FF            ; RST $38 → runs Spectrum's interrupt handler

There are four trampolines in total:

TrampolinePurpose
TRAMP_INT_SPECROM ($FFED)RST $38 → page in Spectrum ROM, run its interrupt handler
TRAMP_INT_RETURN ($FFF4)Return from interrupt → page VTX ROM back in
TRAMP_CALL_SPECROM ($FFCC)Call a routine in the Spectrum ROM (for PRINT, etc.)
TRAMP_RETURN_SPECROM ($FFDA)Return to Spectrum ROM (used to launch BASIC)
TRAMP_START_BASIC ($FFE3)Page in VTX ROM to setup BASIC terminal code

Interrupt Handling

That's not the end of our paging trouble though. The Z80's maskable interrupt fires 50 times per second from the Spectrum's ULA. When the VTX ROM is paged in, the interrupt vector at RST $38 ($0038) lands in our code, not the Spectrum's ROM handling which we still need it to do for frame counting, keyboard scanning etc.

The VTX5000 version of the handler is short:

RST38_HANDLER:
    PUSH AF
    BIT 7,(IY+$7B)       ; Test RAM size flag: 0=16K, 1=48K
    JP Z,$7FED            ; 16K: trampoline at top of 16K RAM
    JP $FFED              ; 48K: trampoline at top of 48K RAM

It pushes AF (), checks whether we're on a 16K or 48K machine, then jumps to the appropriate trampoline. The trampoline pages in the Spectrum ROM, the Z80 immediately executes RST $38 in that ROM (which does the keyboard scan, updates FRAMES, etc.), and then the return trampoline pages us back. It adds about a 200 t-state overhead 50 times a second but given we're just while the VTX5000 is in control.

Cold Start

The power-on cold start routine at $1101 has to get RAM, ROM, and I/O all set up in the right order:

COLD_START:
    DI                        ; Disable interrupts during setup
    LD IY,$5C3A               ; IY -> Spectrum system variables (ERR_NR)
    LD A,$3F
    LD I,A                    ; Set I=$3F for IM2 interrupt vector table
    LD HL,$5C00               ; Start of system variables area
    LD DE,$5C01
    LD BC,$A3FF               ; Clear $A3FF bytes ($5C00 to $FFFF)
    LD (HL),$00
    LDIR                      ; Zero all RAM from $5C00 upward

; --- RAM size test ---
    LD A,$55                  ; Test pattern 1
    LD (HL),A                 ; Write to top of RAM
    CP (HL)                   ; Read back and compare
    JR NZ,.ram_16k            ; If mismatch, RAM is only 16K
    CPL                       ; Test pattern 2 ($AA)
    LD (HL),A
    CP (HL)
    JR Z,.ram_ok              ; If match, full 48K RAM confirmed
.ram_16k:
    RES 7,H                  ; Limit HL to 16K range ($7FFF max)
.ram_ok:

; --- Copy ROM paging trampolines to top of RAM ---
; The trampolines at $1F80-$1FFF handle interrupt routing and ROM paging.
; They must reside in RAM since they switch between VTX and Spectrum ROMs.
    LD DE,$1FFF               ; Destination: top of ROM address space
    LD BC,$0080               ; 128 bytes to copy
    EX DE,HL                  ; HL=dest(top of RAM), DE=$1FFF
    LDDR                      ; Copy $1F80-$1FFF to top of RAM (going down)

; --- Copy character font + splash screen + config to RAM ---
    LD BC,$FEC0               ; Offset adjustment
    EX DE,HL
    ADD HL,BC                 ; Adjust destination pointer
    EX DE,HL
    LD BC,$0280               ; 640 bytes (font + splash + config)
    LDDR                      ; Copy $1B00-$1D7F area to RAM below trampolines

; --- Set RAMTOP for BASIC ---
    LD HL,$FCA9               ; Offset to calculate BASIC RAMTOP
    ADD HL,DE                 ; HL = safe top for BASIC (below our data)
    LD ($5CB4),HL             ; Store as P_RAMT (physical RAM top for BASIC)
    LD SP,$6000               ; Temporary stack in mid-RAM

; --- Initialise hardware and copy BASIC program ---
    CALL GET_IX               ; Set IX to device state block
    CALL HW_INIT              ; Initialise 8251 USART and ports

; --- Copy boot code to RAM and execute ---
; A small loader is copied to $6200 and executed from there. It pages in
; the Spectrum ROM to copy the PRINT/channel routines, then pages back.
    LD HL,$1156               ; Source: embedded copy routine in this ROM
    LD DE,$6200               ; Destination: safe RAM area
    LD BC,$0026               ; 38 bytes of copy code
    LDIR                      ; Copy the loader to RAM
    JP $6200                  ; Execute from RAM (we can't page ROM while running from it)

That boot loader needs to page in the Spectrum ROM to copy some of its output routines (the PRINT channel handler at $1203), but we can't do that while executing from the VTX ROM - the trampoline problem again. So a 38-byte stub gets copied to $6200 in RAM, safely above the ROM address range, and runs from there. It pages in the Spectrum ROM, copies 174 bytes of output routines, patches a jump instruction, then pages the VTX ROM back in.

BASIC

As the ROM map shows, over half the ROM is a Spectrum BASIC program. The machine code handles the time-critical stuff - modem I/O, screen rendering, keyboard - and the BASIC program drives everything else via PEEK, POKE, and USR calls.

The glue between the two is a device state block near the top of RAM ($FF80 on 48K, $7F80 on 16K), always pointed to by the IX register. Cursor position, display attributes, modem config, parser state, timeout counters, ring buffer pointers - it's all in there. Think of it as a big struct passed by pointer through every call.

The BASIC program handles the user interface - menus, login, file operations, the mailbox messaging system:

  • Log on to Prestel/Micronet (automatically or manually)
  • Enter the Micronet terminal for browsing
  • Save and load Viewdata frames to cassette tape
  • Download telesoftware
  • Compose and send mailbox messages
  • Print frames
  • Drop to the BASIC prompt

The full VTX5000 BASIC program is tightly written - single-character variable names, no comments beyond the copyright REM on line 10, data-driven menus. The structure is typical BASIC of the era (computed GO TOs, RESTORE/READ for menu data) so I won't dwell on it. What's more interesting is what the program actually does.

Logging On

On first run the user's 10-digit Prestel ID is all question marks. If it starts with ? the code prompts for a new one, validates it's exactly 10 characters, and pokes it byte-by-byte into the device state block where the machine code can find it:

1450 LET I$=q$: FOR i=o TO 9: POKE (id+i), CODE q$(i+l): NEXT i

Then it displays "Phone Computer" in flashing text - your cue to dial the Prestel number (the VTX has no dialer), wait for the modem tone, and press a key. Logging off sends *90## which was the Prestel logout sequence, confirms with a "Log OFF ?" prompt that only accepts y, then flips the modem flags to disconnect and returns to the menu.

The Mailbox Editor

The mailbox system (lines 7000-7900) is a surprisingly complete message editor crammed into a few hundred bytes of BASIC. You type lines at an INPUT prompt, each one getting packed into a string buffer b$ with a 557-byte limit. The first two bytes of the buffer store the total message length as a 16-bit value:

7240 LET i=i-3: LET b$(l)= CHR$ (i-256* INT (i/256)): LET b$(2)= CHR$  INT (i/256)

That's manually encoding a little-endian 16-bit length into the first two characters of a string - the Spectrum doesn't have any integer packing, so you get to do it yourself with CHR$ and INT. You can save messages to tape, load them back, preview them on screen, and send them over the Prestel connection. Lines ending in # are treated as Prestel "send" characters.

Telesoftware: Self-Replacing Code

The telesoftware downloader (lines 6000-6320) is the wildest part. It moves CLEAR to reclaim RAM below the program, adjusts the Spectrum's PROG pointer directly via POKE, then calls USR to receive the data over the modem. When the download finishes the BASIC program has effectively replaced itself in memory with whatever was received, and runs it.

6105 POKE fmod,123- CODE s$: POKE mf,dl: LET ix=ix-999:
     POKE 23732,ix-256* INT (ix/256): POKE 23733, INT (ix/256):
     LET X= USR str

That's directly writing to the Spectrum's PROG system variable (addresses 23732/23733) to redirect where BASIC thinks its program lives. After the machine code receives the telesoftware, control returns to BASIC... but it's no longer the same program. The Prestel service would send complete BASIC programs this way - games, utilities, even other terminal software - and they'd just appear and run.

Acorn's Teletext adapter did similar things for downloading BBC BASIC programs over Ceefax and I have memories of hanging out of our school IT class window antenna in-hand trying to get uncorrupted blocks...

In part 4 we'll dig into the machine code portion.

0 responses

  1. Avatar for

    Information is only used to show your comment. See my Privacy Policy.