This program is a hexadecimal machine-code editor and assembler aid for entering Z80 opcodes directly into reserved RAM above RAMTOP. It reads RAMTOP from system variables at addresses 16388–16389, then lets the user type hex byte pairs interactively, displaying them in a grid of 10 columns with a moveable cursor. Commands dispatched via a lookup array F(20) support operations including GO (execute via USR), LOAD (POKE bytes into RAM), SAVE, DELETE, cursor movement (positive and negative), relative jump calculation with asterisk-placeholder adjustment, a DOWNLOAD routine that prepends a self-contained tape-loader header sequence, and a full screen PRINT redisplay. The relative-jump subroutine at line 1300 correctly handles the two’s-complement wrapping for backward branches and accounts for placeholder asterisks in the hex string that represent not-yet-resolved bytes. RAMTOP is manipulated at line 9060 by poking address 16389 to reserve blocks of 256 bytes, after which the program re-saves and restarts itself.
Program Analysis
Program Structure
The program is divided into clearly delineated functional blocks, each occupying a line-number range and labelled with inverse-video REM comments:
| Line range | Purpose |
|---|---|
| 10–50 | Memory reservation check / entry gate |
| 60–141 | Initialisation: read RAMTOP, build command dispatch table F() |
| 200–360 | Main input loop: strip spaces, classify input, dispatch or append hex |
| 400–470 | Cursor print / erase subroutines |
| 500–520 | Command dispatcher |
| 600–660 | Append hex bytes to display grid |
| 700–750 | GO – execute code at RAMTOP via USR |
| 800–890 | LOAD – POKE hex string into RAM |
| 900–970 | Move cursor forward |
| 1000–1070 | Move cursor backward |
| 1100–1195 | Redisplay entire hex buffer |
| 1300–1390 | Relative-jump calculation |
| 1400–1460 | SAVE program |
| 1600–1640 | DELETE bytes at cursor |
| 2000–2090 | Asterisk-placeholder adjustment for relative jumps |
| 3000–3130 | DOWNLOAD – prepend tape-loader stub and POKE all bytes |
| 9000–9110 | Reserve memory by poking RAMTOP, re-save and restart |
Command Dispatch Mechanism
Commands are single characters whose ASCII codes (minus 43) index into the array F(20), populated at lines 131–141. The dispatcher at line 510 is simply:
GOSUB F(CODE I$(1)-43)
The trigger condition at line 300 is CODE I$(1)>=44 — i.e. any character with ASCII code above 43 (comma) that is not a hex digit handled earlier. The mapping is:
| Character | ASCII | Index (ASCII−43) | GOSUB target | Function |
|---|---|---|---|---|
| , | 44 | 1 | 700 | GO (execute) |
| 3 | 51 | 6 (but 3=hex digit, unreachable) | 800 | LOAD |
| . | 46 | 3 | — | (unassigned) |
| + | 43 | — | — | (below threshold) |
| F(6)=800 | Triggered by ASCII 43+6=49 = ‘1’ — potential conflict with hex digit ‘1’ | |||
In practice the hex digits 0–9 and A–F have ASCII codes 48–70. The check at line 300 (CODE I$(1)>=44) means single-character hex digits like ‘,’ (44) would be misrouted. The program sidesteps most conflicts because hex input is always exactly two characters, making LEN I$>1 true and causing the two-character hex path to be taken. However the dispatch table assignments at lines 132–141 use indices 6–18, corresponding to ASCII codes 49–61, which overlap with hex digit characters — this is a latent ambiguity for single-character commands that happen to be hex digits.
RAMTOP and Memory Reservation
The ZX81/TS1000 system variable RAMTOP is a 16-bit little-endian value stored at addresses 16388–16389. Line 60 reads it as:
LET RT=PEEK 16388+256*PEEK 16389
When memory has not yet been reserved, the program branches to line 9000, which pokes address 16389 with 128-NB (where NB is the number of 256-byte blocks). This effectively lowers RAMTOP by NB×256 bytes, carving out space above BASIC for machine code. The program then re-saves itself and calls RUN to restart cleanly with the new RAMTOP in effect.
Hex Buffer and Display
All entered bytes are kept in the string H$ as pairs of ASCII hex characters. Placeholder ** pairs are inserted at the current cursor position CI and later replaced during POKE operations. The display grid at lines 610–650 and 1160–1190 prints pairs separated by spaces in rows of 10, with a line break (two extra spaces) after every 10th byte, creating a 10-column layout. The cursor position is displayed as an inverse > character, computed by:
PRINT AT 2+INT(CI/10), 3*(CI-10*INT(CI/10)); "%>"
This maps CI to a row/column on screen without needing a 2D array.
LOAD Subroutine and POKE Loop
The LOAD routine (lines 800–890) prepends a short Z80 stub to H$:
"3E1EED47FD210040C9"
This is: LD A,1EH / LD I,A / LD IX,4000H / RET — a standard setup sequence. The POKE loop at lines 830–890 steps through H$ two characters at a time, skipping * placeholders, and converts each hex pair to a byte value using the formula:
16*(CODE H$(I)-28) + CODE H$(I+1)-28
This relies on the ZX81 character set where ‘0’ = code 28, ‘A’ = code 38, so CODE digit - 28 gives the numeric value 0–15 for hex digits 0–9 and A–F.
Relative Jump Calculation
The subroutine at lines 1300–1390 calculates the signed byte displacement for a Z80 relative jump (e.g. JR, DJNZ). The user supplies a target cursor index JCI; the displacement is JS = JCI - CI - 1. Before computing, subroutine 2000 counts * placeholder characters between the current position and the target and adjusts JS accordingly, since placeholders occupy space in H$ but not in the final machine code. If the displacement exceeds the range −128 to +127, an error message is displayed. For negative displacements, two’s-complement wrapping is applied: JS = JS + 256. The byte is then encoded back into two hex-digit characters using the same code-offset arithmetic.
DOWNLOAD Routine
Lines 3000–3130 implement a self-contained tape-loader download. It prepends a longer Z80 loader stub to H$:
"00002A04404E23462B118240C5011D0009C1EDB03E1EED47FD210040C9"
This stub reads the byte count from the first two bytes at RAMTOP (little-endian), then performs a block load (LDIR) before the standard setup sequence. The total byte count (hex buffer length minus asterisks, plus 9 bytes for the stub) is computed at lines 3020–3050 and POKEd as a 16-bit little-endian value at RAMTOP and RAMTOP+1.
Notable Techniques
- Space-stripping loop (lines 220–260) walks
I$index-by-index and removes all spaces using string slicing, avoiding any built-in trim function. - The
F()dispatch array avoids a long chain ofIF/GOTOstatements for command routing. - Cursor display uses integer division (
INT(CI/10)) rather than a modulo operator, since ZX81 BASIC has noMOD. - Hex-to-byte conversion exploits the contiguous ZX81 character codes for digits and letters, requiring no lookup table.
- The
**placeholder convention allows multi-byte instructions to be entered incrementally, with operand bytes filled in later.
Bugs and Anomalies
- Line 3060 contains a typo:
"LENGHT"instead of"LENGTH". - The delay loop at lines 9040–9050 (
FOR T=1 TO 20: NEXT T) is extremely short and unlikely to provide a meaningful pause on any hardware speed. - The command dispatch index calculation (
CODE I$(1)-43) overlaps with ASCII codes for hex digit characters when commands are single characters, creating a potential ambiguity for single-character inputs that are also valid hex digits (e.g. ‘1’, ‘2’, ‘3’). - The SAVE at line 9100 saves to filename
"1024%5"(where%5is an inverse ‘5’), which encodes an auto-run flag in a non-standard way relative to the BASIC line-number convention. - Line 1044’s cursor-move-negative code does not call
GOSUB 1100to refresh the display after moving, whereas the positive-move routine at line 960 does callGOSUB 400— both do call 400, so display is consistent; this is not a bug.
Content
Source Code
10 PRINT "HAVE YOU RESERVED MEMORY?"
20 INPUT A$
30 IF A$="" THEN GOTO 50
40 IF A$(1)="N" THEN GOTO 9000
50 CLS
60 LET RT=PEEK 16388+256*PEEK 16389
70 PRINT "RAMTOP= ";RT
80 PRINT "HEX CODE:"
100 LET CI=0
110 LET H$=""
120 GOSUB 400
130 DIM F(20)
131 LET F(1)=700
132 LET F(6)=800
133 LET F(7)=900
134 LET F(8)=1000
135 LET F(10)=1100
136 LET F(11)=1200
137 LET F(12)=1300
138 LET F(13)=1400
139 LET F(14)=1500
140 LET F(18)=1600
141 LET F(17)=3000
200 INPUT I$
210 LET A=0
220 LET A=A+1
230 IF A>LEN I$ THEN GOTO 300
240 IF I$(A)<>" " THEN GOTO 220
250 LET I$=I$( TO A-1)+I$(A+1 TO )
260 GOTO 230
300 IF CODE I$(1)>=44 THEN GOTO 500
310 LET I$=I$+"**"
320 LET H$=H$( TO 2*CI)+I$+H$(2*CI+1 TO )
330 GOSUB 450
340 GOSUB 600
350 GOSUB 400
360 GOTO 200
400 REM %P%R%I%N%T% %C%U%R%S%O%R
410 PRINT AT 2+INT (CI/10),3*(CI-10*INT (CI/10));"%>";
420 RETURN
450 REM %U%N%P%R%I%N%T% %C%U%R%S%O%R
460 PRINT AT 2+INT (CI/10),3*(CI-10*INT (CI/10));" ";
470 RETURN
500 REM %R%O%U%T%I%N%E%S
510 GOSUB F(CODE I$(1)-43)
520 GOTO 200
600 REM %P%R%I%N%T% %A%P%P%E%N%D%E%D% %T%E%X%T
610 FOR J=1 TO LEN I$/2
620 PRINT I$(2*J-1 TO 2*J)+" ";
630 LET CI=CI+1
640 IF CI=10*INT (CI/10) THEN PRINT " ";
650 NEXT J
660 RETURN
700 REM %G%O
710 CLS
720 PRINT "NUMBER OF DATA BYTES?"
730 INPUT D
740 CLS
750 LET Y=USR (RT+D)
760 RETURN
800 REM %L%O%A%D
810 LET H$=H$+"3E1EED47FD210040C9"
820 LET J=RT-1
830 LET I=-1
840 LET J=J+1
850 LET I=I+2
860 IF I>LEN I$ THEN RETURN
870 IF H$(I)="*" THEN GOTO 850
880 POKE J,16*(CODE H$(I)-28)+CODE H$(I+1)-28
890 GOTO 840
900 REM %M%O%V%E% %C%U%R%S%O%R% %P%O%S%I%T%I%V%E%L%Y
910 GOSUB 450
920 IF LEN I$=1 THEN LET CM=1
930 IF LEN I$>1 THEN LET CM=VAL I$(2 TO )
940 LET CI=CI+CM
950 IF CI>LEN H$/2 THEN LET CI=LEN H$/2
960 GOSUB 400
970 RETURN
1000 REM %M%O%V%E% %C%U%R%S%O%R% %N%E%G%A%T%I%V%E%L%Y
1010 GOSUB 450
1020 IF LEN I$=1 THEN LET CM=1
1030 IF LEN I$>1 THEN LET CM=VAL I$(2 TO )
1040 LET CI=CI-CM
1050 IF CI<0 THEN LET CI=0
1060 GOSUB 400
1070 RETURN
1100 REM %P%R%I%N%T
1110 CLS
1120 PRINT "RAMTOP= ";RT
1130 PRINT "HEX CODE:"
1140 PRINT " ";
1150 LET CI=0
1160 FOR J=1 TO LEN H$/2
1170 PRINT H$(2*J-1 TO 2*J)+" ";
1185 LET CI=CI+1
1187 IF CI=10*INT (CI/10) THEN PRINT " ";
1190 NEXT J
1192 GOSUB 400
1195 RETURN
1300 REM %R%E%L%A%T%I%V%E% %J%U%M%P%S
1310 LET JCI=VAL I$(2 TO )
1320 LET JS=JCI-CI-1
1325 GOSUB 2000
1330 IF JS>=-128 AND JS<=127 THEN GOTO 1357
1335 CLS
1340 PRINT AT 10,6;"INVALID JUMP SIZE"
1345 FOR Q=1 TO 20
1346 NEXT Q
1350 GOSUB 1100
1355 RETURN
1357 IF JS<0 THEN LET JS=JS+256
1360 LET X1=INT (JS/16)
1365 LET X0=JS-16*X1
1367 LET X$=CHR$ (X1+28)+CHR$ (X0+28)
1370 LET H$=H$( TO 2*CI)+X$+H$(2*CI+1 TO )
1375 LET CI=CI+1
1380 GOSUB 1100
1390 RETURN
1400 REM %S%A%V%E
1410 PRINT "%S%A%V%E.INPUT PROGRAM NAME. (DEFAULT NAME ""H"")"
1420 INPUT N$
1430 IF N$="" THEN LET N$="H"
1440 SAVE N$
1450 CLS
1460 GOTO 10
1600 REM %D%E%L%E%T%E
1610 IF LEN I$=1 THEN LET K=1
1620 IF LEN I$>1 THEN LET K=VAL I$(2 TO )
1630 LET H$=H$( TO 2*CI)+H$(2*CI+2*K+1 TO )
1640 RETURN
2000 REM %A%S%T%E%R%I%S%K% %A%D%J%U%S%T
2010 IF JS<0 THEN LET W$=H$(2*JCI+1 TO 2*CI)
2020 IF JS>=0 THEN LET W$=H$(2*CI+1 TO 2*JCI)
2030 LET SC=0
2040 FOR T=1 TO LEN W$
2050 IF W$(T)="*" THEN LET SC=SC+1
2060 NEXT T
2070 IF JS<0 THEN LET JS=JS+SC/2
2080 IF JS>0 THEN LET JS=JS-SC/2
2090 RETURN
3000 REM %D%O%W%N%L%O%A%D
3010 LET LH=0
3020 FOR I=1 TO LEN H$
3030 IF H$(I)<>"*" THEN LET LH=LH+1
3040 NEXT I
3050 LET LH=LH/2+9
3060 PRINT AT 21,0;"LENGHT ";LH;" BYTES."
3070 LET H$="00002A04404E23462B118240C5011D0009C1EDB03E1EED47FD210040C9"+H$
3080 GOSUB 800
3090 LET LS=INT (LH/256)
3100 LET LJ=LH-256*LS
3110 POKE RT,LJ
3120 POKE RT+1,LS
3130 RETURN
9000 REM %R%E%S%E%R%V%E% %M%E%M%O%R%Y
9010 PRINT "HOW MANY 256-BYTE BLOCKS?"
9020 INPUT NB
9030 PRINT "AFTER LIST PRESS RUN"
9040 FOR T=1 TO 20
9050 NEXT T
9060 POKE 16389,128-NB
9070 PRINT USR 1040
9080 STOP
9090 CLEAR
9100 SAVE "1024%5"
9110 RUN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

