This program generates a custom character set font by reading the built-in ROM character data (addresses 15616–16383) and transforming individual byte values through a series of substitution rules before storing the results at address 64000. The transformation logic applies piecewise replacements to each byte — for example, value 32 becomes 48, value 64 becomes 96 — effectively widening or shifting the pixel patterns to create a bolder or enhanced typeface. System variables at 23606/23607 (CHARS low/high bytes) are then POKEd to point to the new data at 64000 (offset by 256), redirecting the ROM to use the custom font. After printing instructions and producing two hardcopy listings via COPY (one with the new font, one with the standard font at 23607=60), the program calls NEW to self-destruct, leaving only the custom character data in RAM. The font can be preserved with SAVE “text” CODE 64000,1536, which also captures the UDG area.
Program Analysis
Program Structure
The program is organised into four broad phases:
- Initialisation (lines 1–23): A short startup fanfare via
BEEP, screen setup, a “please wait” message, and a temporary adjustment to theCHARSsystem variable. - Font generation – printable ASCII range (lines 40–210): Iterates over ROM bytes 15616–16135 (characters 32–127, the normal printable set), applies substitution rules to each byte, and writes the result to 64000 onward.
- Font generation – upper range (lines 215–520): Iterates over ROM bytes 16136–16383 (the remaining character data / token characters), applies a separate but similar set of substitution rules, writing from address 64520 onward.
- Output and self-destruction (lines 620–860, subroutines 1000–2030): Redirects the CHARS pointer to the new data, displays instructions, produces two COPY hardcopies (one enhanced, one normal), and ends with
NEW.
System Variable Manipulation
The ZX Spectrum/TS2068 system variable CHARS is a two-byte pointer at addresses 23606–23607. It normally points 256 bytes before the start of the ROM character table so that CHR$ 32 (space) maps to offset 0. The program exploits this in several ways:
| Line(s) | POKE | Effect |
|---|---|---|
| 2, 23 | 23609,125 / 23607,150 | Temporary adjustments during generation (23609 is FLAGS2; 23607 alone shifts the font mid-run) |
| 620–630 | 23607,249 / 23606,0 | Points CHARS to 63999+1 = 64000, activating the new font (249 × 256 = 63744; +256 offset = 64000) |
| 810 | 23607,60 | Points CHARS back toward ROM standard font for the second COPY comparison |
Byte-Substitution Font Transformation
The core algorithm (lines 80–210 and 220–520) reads each byte of the ROM font data with PEEK, passes it through a chain of IF comparisons, and substitutes a replacement value. This is not a mathematical transform but a hand-crafted lookup table encoded as sequential IF statements. Because multiple IFs can match the same variable in a single pass (BASIC evaluates each IF line independently), there is a subtle bug risk: a substituted value could be matched and re-substituted by a later rule on the same iteration. For example in the first loop, line 91 changes 128→192, and no later rule matches 192, so this is safe; but line 92 has a compound statement (IF b=98 THEN LET b=102: IF b=82 THEN LET b=114) where the second condition can never trigger because the first assignment changes b to 102 before evaluation — the IF b=82 clause is unreachable as written.
Memory Layout
| Address range | Purpose |
|---|---|
| 15616–16135 | ROM character bitmaps for CHR$ 32–127 (printable ASCII) |
| 16136–16383 | ROM character bitmaps, upper portion |
| 64000–64519 | New font data for printable ASCII range (written by first loop) |
| 64520–64999 | New font data for upper range (written by second loop) |
| 64000–65535 | Protected by CLEAR 63999 (line 10) so BASIC heap cannot overwrite it |
Notable Techniques
- Self-destruction via
NEW: Line 860 callsNEW, which wipes the BASIC program and variables but leaves the font data aboveRAMTOP(set byCLEAR 63999) intact. - Dual hardcopy: Lines 800 and 840 each call
COPYto produce a printer listing of the instructions — one with the new font active, one with the standard ROM font — for direct comparison on paper. - Descending BEEP scale: Subroutine 2000 plays a descending glide from note −20 to −30 as a pacing separator between instruction paragraphs.
- Startup fanfare: Lines 3 uses five
BEEPcalls at pitches 0, 12, 24, 36, 48 semitones (a two-octave chromatic ascent in octave steps) as an audio cue that the program has started. - Zero-initialisation pass: Lines 40–60 zero all 1001 bytes from 64000 to 65000 before writing font data, ensuring unused positions default to blank rather than random RAM content.
- Author credit: Line 650 embeds
"@ m.p.biddell"in the confirmation message.
Bugs and Anomalies
- Dead compound condition (line 92):
IF b=98 THEN LET b=102: IF b=82 THEN LET b=114— afterbis set to 102, the conditionb=82is always false, so theLET b=114branch is never executed. - Possible re-substitution: In the second loop, several substitution chains could theoretically cascade (e.g. 48→56 at line 243, then 56→60 at line 242 on a later iteration), but since each byte is processed exactly once per loop iteration and the
IFs are on separate lines, only one substitution per byte value can fire — the risk is real but limited to values that are both a source and a target within the same pass. - Gap at 64520: The second loop starts writing at
a=64520(line 215), leaving bytes 64500–64519 as zeros from the initialisation pass, creating a small gap between the two font segments.
Content
Image Gallery
Source Code
1 REM "CHR$ gen"
2 POKE 23609,125
3 BEEP .2,0: BEEP .2,12: BEEP .2,24: BEEP .2,36: BEEP .2,48
10 CLEAR 63999
15 PAPER 0: BORDER 0: CLS
20 PRINT AT 10,0; INK 7; FLASH 1; PAPER 1;"PLEASE WAIT-CREATING NEW SCRIPT"
23 POKE 23607,150
40 FOR j=64000 TO 65000
50 POKE j,0
60 NEXT j
70 LET a=64000
80 FOR j=15616 TO 16135
90 LET b=(PEEK j)
91 IF b=128 THEN LET b=b+64
92 IF b=98 THEN LET b=102: IF b=82 THEN LET b=114
93 IF b=70 THEN LET b=102
101 IF b=68 THEN LET b=b+40
105 IF b=66 THEN LET b=102
110 IF b=64 THEN LET b=b+32
120 IF b=32 THEN LET b=b+16
130 IF b=16 THEN LET b=b+8
135 IF b=8 THEN LET b=b+4
140 IF b=4 THEN LET b=b+2
150 IF b=2 THEN LET b=b+4
160 IF b>255 THEN GO TO 200
170 IF b=0 THEN GO TO 200
180 POKE a,b
200 LET a=a+1
210 NEXT j
215 LET a=64520
220 FOR j=16136 TO 16383
222 LET b=PEEK j
230 IF b=120 THEN LET b=124
231 IF b=146 THEN LET b=214
232 IF b=84 THEN LET b=214
233 IF b=104 THEN LET b=238
234 IF b=96 THEN LET b=112
235 IF b=68 THEN LET b=102
238 IF b=64 THEN LET b=96
240 IF b=60 THEN LET b=126
242 IF b=56 THEN LET b=60
243 IF b=48 THEN LET b=56
244 IF b=40 THEN LET b=44
245 IF b=36 THEN LET b=38
248 IF b=34 THEN LET b=102
250 IF b=32 THEN LET b=96
255 IF b=28 THEN LET b=60
260 IF b=24 THEN LET b=30
265 IF b=16 THEN LET b=24
270 IF b=12 THEN LET b=30
280 IF b=4 THEN LET b=6
500 POKE a,b
510 LET a=a+1
520 NEXT j
620 POKE 23607,249
630 POKE 23606,00
635 PAPER 0
636 BORDER 0
640 CLS
650 PRINT AT 10,0; INK 5; PAPER 0;"CHARACTERS loaded-@ m.p.biddell"
660 PRINT
670 PAUSE 200
680 CLS
690 GO SUB 1000
705 PAUSE 100
800 COPY
810 POKE 23607,60
815 CLS
820 GO SUB 1000
840 COPY
850 PAUSE 100
860 NEW
1000 PRINT INK 6;"INSTRUCTIONS-NEW CHARACTER FONT"
1010 GO SUB 2000
1020 PRINT INK 5;"1. After this program has self destructed, poke 23607,249 to turn on the new character set."
1030 GO SUB 2000
1040 PRINT INK 5;"2. Then load your own program inthe normal way, Poke 23607,249 and it is instantly converted to the new text. Now use LLIST to see the enhanced output from the 2040-printer."
1050 GO SUB 2000
1060 PRINT INK 5;"3. Now Poke 23607,60 and LLIST .Compare this normal listing withthe enhanced one."
1070 GO SUB 2000
1080 PRINT INK 5;"4. SAVE 'text'CODE ,64000,1536 (this also saves U.D.G's.)"
1090 GO SUB 2000
1100 RETURN
2000 PRINT : FOR j=20 TO 30
2010 BEEP .1,-j
2020 NEXT j
2030 RETURN
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.