--- title: "Hex Code Memory Loader" id: 57444 type: "computer_media" slug: "hex-code-memory-loader" url: "http://localhost/computer_media/hex-code-memory-loader/" markdown_url: "http://localhost/computer_media/hex-code-memory-loader.md" published_at: "2024-10-05T13:50:34+00:00" modified_at: "2026-04-03T07:58:29+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2024/10/245_Mem.png" excerpt: "A full interactive hex editor lets you enter, navigate, and execute Z80 machine code directly in reserved RAM, complete with relative-jump calculation and a tape-loader downloader." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 1000" slug: "ts1000" taxonomy: "post_tag" url: "http://localhost/tag/ts1000/" model: - name: "Timex/Sinclair 1000" slug: "ts-1000" taxonomy: "model" url: "http://localhost/model/ts-1000/" genre: - name: "Programming" slug: "programming" taxonomy: "genre" url: "http://localhost/type/programming/" media_contents: - id: 56736 title: "Timex Sinclair Public Domain Library Tape 1005" type: "computer_media" url: "http://localhost/computer_media/timex-sinclair-public-domain-library-tape-1005/" media_type: "Program" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2024/10/245_Mem.png" - url: "http://localhost/wp-content/uploads/2024/10/245_Mem1.png" media_type_tags: "Programming" --- 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 of `IF`/`GOTO` statements for command routing. - Cursor display uses integer division (`INT(CI/10)`) rather than a modulo operator, since ZX81 BASIC has no `MOD`. - 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 `%5` is 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 1100` to refresh the display after moving, whereas the positive-move routine at line 960 does call `GOSUB 400` — both do call 400, so display is consistent; this is not a bug. ## 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 ```