ExFont 1.1 is a character set editor that allows users to redesign both the main character set (CHR$ 32–127) and user-defined graphics (CHR$ 144–164) by directly POKEing new bitmap data into memory. The program stores a replacement font as a binary CODE block loaded at address 64600, and switches between the built-in and custom character sets by POKEing the system variable at addresses 23606–23607 (the CHARS pointer). Navigation within the 8×8 pixel grid editor is handled via the STICK command, with FIRE toggling individual bits between set (█) and clear (space). Each row of a character is represented as an 8-character string using block graphics for 1-bits, and subroutines at lines 100 and 200 convert between these binary string representations and decimal byte values for PEEK/POKE operations. A short ascending BEEP sequence at line 9100 serves as an audio prompt before each INPUT statement.
Program Analysis
Program Structure
The program is organized into clearly labeled subroutine blocks, each preceded by a REM banner. Execution begins at line 8000 (initialization), which loads the CODE block, sets up variables, displays notes, and falls through to the main menu at line 8100. The broad structure is:
- Lines 100–190: Binary string → decimal byte conversion (bin→dec) subroutine
- Lines 200–400: Decimal byte → binary string conversion (dec→bin) subroutine
- Lines 1000–1260: Character range selection loop
- Lines 1500–1620: Per-character display and editing driver
- Lines 3000–3950: Interactive pixel editor (STICK-based)
- Lines 5002–5050: Commit edits, POKE bytes back, refresh display
- Lines 8000–8090: Initialization
- Lines 8100–8300: Main menu
- Lines 8400–8430: SAVE routines
- Lines 8500–8640: Notes/startup screen
- Lines 9000–9070: Character set toggle
- Lines 9100–9150: Audio prompt subroutine
- Lines 9200–9450: Print character set comparison screen
Memory and Font Addressing
The program exploits the TS2068/Spectrum system variable at addresses 23606–23607, which holds a pointer to the character set bitmap data (offset by 256 from CHR$ 32). Two font bases are used:
| Font | CHARS pointer value | Base address |
|---|---|---|
| Built-in ROM font | 15360 (POKE 23606,0 : POKE 23607,60) | 15616 |
| Custom font (RAM) | 64344 (POKE 23606,88 : POKE 23607,251) | 64600 |
The custom CODE block occupies 936 bytes starting at 64600, covering CHR$ 32–127 (768 bytes) and UDGs 144–164 (168 bytes). Address calculation at line 1520 handles UDGs separately: add=64472+((c-32)*8), while the main set uses add=64600+((c-32)*8) at line 1525. CLEAR 64599 at line 8010 protects this area from BASIC’s heap.
Binary String Representation
Each 8×8 character is held in a two-dimensional string array N$(8,8). Each row is an 8-character string where a set pixel is represented by the block graphic █ (\::, character 143) and a clear pixel by UDG \a (character 144, displayed as a space-equivalent). The dec→bin subroutine (line 200) extracts bits with repeated halving and remainder testing; the bin→dec subroutine (line 100) reconstructs bytes by scanning the string and treating the block graphic as “1” and the UDG as “0” via CODE(B$(p))-48 — though this relies on the specific code values of those characters rather than a true ASCII “0”/”1″ comparison.
The Pixel Editor
The interactive editor (lines 3200–3950) places an 8×8 grid at screen rows 14–21, columns 15–22. A flashing cursor character \d (UDG d, code 157) marks the current cell. The editor polls STICK(1,1) for directional movement and STICK(2,1) for the fire button simultaneously with INKEY$ for ENTER to quit.
Movement is handled by four labeled goto targets (3600=up, 3700=down, 3800=left, 3900=right), each printing the underlying cell content before repositioning the cursor and wrapping at boundaries. Bit toggling at lines 3400 and 3500 spins on STICK(2,1)>0 to wait for button release before returning to the main poll loop, preventing repeated triggers.
The cursor position in screen coordinates (x,y) maps to the N$ array via N$(x-13, y-14): row index x-13 (range 1–8) and column index y-14 (range 1–8).
Notable Techniques
- CHARS pointer toggling: The toggle routine at line 9000 reads the current pointer value and swaps between the two known states, allowing live comparison of ROM and custom glyphs.
- Side-by-side font comparison (line 9200): For each character, the routine temporarily POKEs the ROM base, prints the ROM glyph, then POKEs the custom base and prints the custom glyph, yielding a two-column comparison in a single loop pass.
- Audio prompt (line 9100): An eight-step ascending BEEP sequence (semitones 2,5,7,10,12,15,17,20) plays before every INPUT, providing consistent audio feedback without interrupting screen layout.
- Separator string
Z$: Initialized at line 8020 as 32 cursor-left characters (\b= CHR$ 8), used as a horizontal rule by overprinting the current line. - Skipping undefined codes: Lines 1512–1515 skip codes 124, 126, and 128–143 (the block graphics range), since these are not user-redefinable in the same way, and the loop variable
cis advanced directly rather than using a conditional skip flag.
Bugs and Anomalies
- Missing line 5000: Line 3280 executes
GO TO 5000on ENTER, but line 5000 does not exist; execution continues at line 5002. This is an intentional technique — the interpreter advances to the next available line. - CODE K$ at line 8215: The condition
CODE INKEY$>5 OR CODE INKEY$<1callsINKEY$twice in a single statement with no guarantee the same key is read both times; however, since this line has no effect (it falls through to 8220 regardless), it is functionally harmless dead code. - bin→dec code arithmetic: Line 130 uses
CODE(B$(p))-48. After substitution,B$(p)is either “1” (CODE 49, giving 1) or “0” (CODE 48, giving 0), so the arithmetic is correct — but only because the subroutine first replaces the graphic characters with literal digit characters at lines 122–123. - UDG address formula: Line 1520 uses
64472+((c-144)*8+8*112)simplified to64472+((c-32)*8). Since UDGs start at code 144 and their bitmaps are placed after the 96 main-set characters (96×8=768 bytes from 64600 = 65368), the formula actually points 896 bytes past 64600 for code 144, which is 65368 — matching a layout where UDG data immediately follows the main set. This is consistent with the 936-byte save length (768+168=936).
Content
Source Code
1 REM \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b existo:ray software 85 \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b
2 REM ExFont 1.1
3 REM R. L. Moulder
4 REM 206 Yeonas Dr. SW
5 REM Vienna, Va 22180
100 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c bin->dec
108 FOR x=1 TO 8
109 LET B$=N$(x)
110 LET n(2,x)=0
120 FOR p=1 TO 8
122 IF B$(p)="\::" THEN LET B$(p)="1"
123 IF B$(p)="\a" THEN LET B$(p)="0"
130 LET n(2,x)=n(2,x)*2+(CODE (B$(p))-48)
140 NEXT p
145 POKE n(1,x),n(2,x)
150 NEXT x
190 RETURN
200 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c dec->bin
210 DIM B$(8)
220 LET p=8
230 LET B$="\a\a\a\a\a\a\a\a"
240 LET d=n(2,x)
250 LET n=INT (d/2)
260 IF d-2*n=1 THEN LET B$(p)="\::"
280 LET d=n
290 LET p=p-1
300 IF d>0 THEN GO TO 250
390 LET N$(x)=B$
400 RETURN
1000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c set CHR$ and loop
1010 CLS
1020 PRINT AT 3,0;" Enter code # of FIRST and LAST characters to be re-designed."
1030 INK 3: PRINT AT 5,0;Z$: INK 5
1040 PRINT AT 6,0;" Main Character Set 32 \g\h 127 User Graphics 144 \g\h 164"
1045 PRINT AT 10,0;" NOTE: Program skips code #'s 128 thru 143, 124 & 126 automatically"
1050 PRINT AT 13,0;" To change one character only use its code # for both inputs."
1060 INK 3: PRINT AT 15,0;Z$
1200 GO SUB 9100: INPUT "Enter First Code # ";c1
1205 IF c1<32 OR c1>164 THEN GO TO 1200
1210 INK 5: PRINT AT 17,1;"CODE ";c1;" (";CHR$ c1;")";" thru ";
1220 GO SUB 9100: INPUT "Enter Second Code # ";c2
1225 IF c2<32 OR c2>164 OR c2<c1 THEN GO TO 1220
1230 PRINT "CODE ";c2;" (";CHR$ c2;")"
1240 GO SUB 9100: PRINT AT 20,1;"Is This Correct? (y/n) "
1245 LET K$=INKEY$
1250 IF K$="n" THEN GO TO 1000
1255 IF K$="y" THEN GO TO 1500
1260 GO TO 1245
1500 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c print address & cont.
1505 CLS : INK 3
1510 FOR c=c1 TO c2
1512 IF c=124 THEN LET c=125
1513 IF c=126 THEN LET c=127
1515 IF c>127 AND c<144 THEN LET c=144
1520 IF c>=144 THEN LET add=64472+((c-32)*8)
1525 IF c<=127 THEN LET add=64600+((c-32)*8)
1528 INK 6: PRINT AT 1,3;"CHR$ CODE #": PRINT AT 1,14;c: INK 6: PAPER 1: PRINT AT 1,18;CHR$ c: INK 3: PAPER 0
1530 FOR x=1 TO 8
1535 LET n(1,x)=add+x-1: LET n(2,x)=PEEK (add+x-1)
1540 PRINT AT 2+x,1;n(1,x);TAB 8;n(2,x);
1550 GO SUB 200
1555 PRINT TAB 15;N$(x)
1560 NEXT x
1570 GO SUB 3000
1605 CLS
1610 NEXT c
1620 GO TO 8100
3000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c change CHR$ ?
3005 GO SUB 9100
3010 INK 5: PRINT AT 15,0;"Change character? (y/n or ENTER)": INK 3
3020 LET K$=INKEY$
3030 IF K$="y" THEN GO TO 3200
3040 IF K$="n" THEN RETURN
3045 IF CODE K$=13 THEN RETURN
3050 GO TO 3020
3200 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c Redesign CHR$
3205 INK 6: PRINT AT 15,0;" "
3210 PRINT AT 13,0;"Use STICK to";AT 14,0;"move cursor."
3211 PRINT AT 16,0;"Press FIRE";AT 17,0;"button to";AT 18,0;"flip bits."
3212 PRINT AT 20,0;"Press ENTER";AT 21,0;"to quit."
3215 INK 3: PRINT AT 13,15;"76543210": INK 5
3220 FOR x=1 TO 8
3230 INK 3: PRINT AT 13+x,14;x-1: INK 5: PRINT AT 13+x,15;N$(x)
3240 NEXT x
3250 LET x=14: LET y=15: FLASH 1: PRINT AT x,y;"\d": FLASH 0
3260 LET st= STICK (1,1)
3270 LET bt= STICK (2,1)
3280 IF CODE INKEY$=13 THEN GO TO 5000
3290 IF bt=1 AND N$(x-13,y-14)="\a" THEN GO TO 3500
3300 IF bt=1 AND N$(x-13,y-14)="\::" THEN GO TO 3400
3310 IF st=1 THEN GO TO 3600
3320 IF st=2 THEN GO TO 3700
3330 IF st=4 THEN GO TO 3800
3340 IF st=8 THEN GO TO 3900
3390 GO TO 3260
3400 LET N$(x-13,y-14)="\a"
3410 FLASH 1: PRINT AT x,y;"\d": FLASH 0
3420 IF STICK (2,1)>0 THEN GO TO 3420
3430 GO TO 3260
3500 LET N$(x-13,y-14)="\::"
3510 FLASH 1: PRINT AT x,y;"\d": FLASH 0
3520 IF STICK (2,1)>0 THEN GO TO 3520
3530 GO TO 3260
3600 PRINT AT x,y;N$(x-13,y-14)
3610 LET x=x-1
3620 IF x<14 THEN LET x=21
3640 FLASH 1: PRINT AT x,y;"\d": FLASH 0
3650 GO TO 3260
3700 PRINT AT x,y;N$(x-13,y-14)
3710 LET x=x+1
3720 IF x>21 THEN LET x=14
3740 FLASH 1: PRINT AT x,y;"\d": FLASH 0
3750 GO TO 3260
3800 PRINT AT x,y;N$(x-13,y-14)
3810 LET y=y-1
3820 IF y<15 THEN LET y=22
3840 FLASH 1: PRINT AT x,y;"\d": FLASH 0
3850 GO TO 3260
3900 PRINT AT x,y;N$(x-13,y-14)
3910 LET y=y+1
3920 IF y>22 THEN LET y=15
3940 FLASH 1: PRINT AT x,y;"\d": FLASH 0
3950 GO TO 3260
5002 FOR x=12 TO 21
5004 PRINT AT x,0;" "
5006 NEXT x
5008 GO SUB 100
5020 FOR x=1 TO 8
5030 PRINT AT 13+x,1;n(1,x);" ";n(2,x)
5040 NEXT x
5045 PAUSE 45
5050 RETURN
8000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c initialize variables
8005 BORDER 0: PAPER 0: INK 5
8010 CLEAR 64599
8015 LOAD "cd"CODE 64600
8020 LET Z$="\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
8030 DIM N$(8,8)
8040 DIM n(2,8)
8050 POKE 23606,88: POKE 23607,251
8090 GO SUB 8500
8100 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c print menu
8105 CLS
8110 PRINT AT 4,0;"existo:ray software 1 9 8 5"
8115 INK 3: PRINT AT 3,0;Z$: INK 5
8117 PRINT AT 2,0;"ExFont 1.1 2 0 6 8"
8120 INK 3: PRINT AT 7,2;"1.": INK 5: PRINT AT 7,4;" Design Main Character Set CHR$ 32 thru 127"
8130 PRINT AT 9,5;"Or User Graphics CHR$ 144 thru 164"
8140 INK 3: PRINT AT 11,2;"2.": INK 5: PRINT AT 11,4;" Print Definable Characters"
8150 INK 3: PRINT AT 12,2;"3.": INK 5: PRINT AT 12,4;" SAVE Program and Code"
8160 INK 3: PRINT AT 13,2;"4.": INK 5: PRINT AT 13,4;" SAVE Code Only (64600,936)"
8170 INK 3: PRINT AT 14,2;"5.": INK 5: PRINT AT 14,4;" Toggle Character Sets"
8200 FLASH 1: INK 3: PRINT AT 17,2;"\g\h": FLASH 0: INK 5: PRINT AT 17,5;"Enter # 1 thru 5"
8210 GO SUB 9100
8215 IF CODE INKEY$>5 OR CODE INKEY$<1 THEN GO TO 8220
8220 IF INKEY$="1" THEN GO TO 1000
8230 IF INKEY$="2" THEN GO TO 9200
8240 IF INKEY$="3" THEN GO TO 8400
8250 IF INKEY$="4" THEN GO TO 8420
8260 IF INKEY$="5" THEN GO TO 9000
8300 GO TO 8220
8400 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c SAVE
8405 PRINT AT 17,0;" ": FLASH 1: PRINT AT 17,7;"SAVING": FLASH 0
8410 SAVE "ef" LINE 8000
8420 SAVE "cd"CODE 64600,936
8430 CLS : GO TO 8100
8500 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c NOTES
8505 CLS
8510 PRINT AT 4,0;"existo:ray software 1 9 8 5"
8520 PRINT AT 2,0;"ExFont 1.1 2 0 6 8"
8530 INK 3: PRINT AT 3,0;Z$
8540 PRINT AT 0,0;" notes notes notes notes notes": INK 5
8550 PRINT AT 7,2;"Use <GO TO 8100> to restart."
8560 PRINT AT 9,2;"To use a new character set in other programs: CLEAR 64599 LOAD \fcd\fCODE 64600 POKE 23606,88 POKE 23607,251"
8570 PRINT AT 16,2;"To return to TS characters: POKE 23606,0 POKE 23607,60"
8600 PRINT AT 20,0;"<c> to copy any other key to continue"
8610 LET I$=INKEY$
8620 IF I$="c" THEN COPY
8630 IF I$="" THEN GO TO 8610
8640 RETURN
9000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c POKE CHARS
9010 LET p=PEEK 23606+256*PEEK 23607
9020 IF p=64344 THEN GO TO 9040
9030 IF p=15360 THEN GO TO 9060
9040 POKE 23606,0: POKE 23607,60
9050 CLS : GO TO 8100
9060 POKE 23606,88: POKE 23607,251
9070 CLS : GO TO 8100
9100 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c INPUT PROMPT BEEP
9105 BEEP .01,2
9110 BEEP .01,5
9115 BEEP .01,7
9120 BEEP .01,10
9125 BEEP .01,12
9130 BEEP .01,15
9135 BEEP .01,17
9140 BEEP .01,20
9150 RETURN
9200 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c PRINT character set
9201 CLS
9210 PRINT "MAIN CHARACTER SET": PRINT "\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e"
9220 FOR x=32 TO 127
9222 IF x=124 THEN LET x=125
9223 IF x=126 THEN LET x=127
9225 POKE 23606,0: POKE 23607,60
9230 PRINT CHR$ x;
9240 POKE 23606,88: POKE 23607,251
9250 PRINT CHR$ x;" ";
9260 NEXT x
9265 PRINT : PRINT : PRINT "USER GRAPHICS": PRINT "\e\e\e\e\e\e\e\e\e\e\e\e\e"
9270 FOR x=144 TO 164
9272 POKE 23606,0: POKE 23607,60
9273 PRINT CHR$ (x-79);
9275 POKE 23606,88: POKE 23607,251
9280 PRINT CHR$ x;" ";
9290 NEXT x
9295 GO SUB 9100
9300 PRINT : PRINT : PRINT "<c>to COPY any other key to ret"
9400 LET I$=INKEY$
9410 IF I$="c" THEN COPY
9420 IF I$="" THEN GO TO 9400
9450 CLS : GO TO 8100
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.




