Code Snippets
18/12/2025


18/12/2025 - TYPE-IN WAVY PATTERN

Do you remember those old type-ins that would run for about 12 years, and produce some sort of mathematical pattern on screen?

Let's pay homage to that old artform and show off The Power of BASIC™.

This program draws an animated pattern, then uses the Spectrum 128/+2/+2A/+3 ramdisk to save and display the animation frames far more quickly than it took to draw them. Sinclair BASIC does not have GET and PUT commands to copy chunks of the screen, unlike some other BASICs, so the 128K ramdisk does the job.

It takes 20 minutes and 40 seconds to draw the animation, so you know it must be good*  (* This statement is not legally binding).

RUN the program and grab a cup of tea. Then a cup of coffee. Then another one. And another. Perhaps, consider setting up your own plantation and start growing your own coffee. The animation might have finished drawing by then.

You may be tempted to try to run this on a faster machine, but there is no point. There is no faster machine than a ZX Spectrum 128K.

wavypattern.zip

 

This could easily be in a 1960s/1970s sci-fi

Is it moving left, or are all the points bouncing up and down? No-one knows...

 

Options

GOTO 200 - use this if you want to run an animation that's already saved in the ramdisk, without having to recalculate everything! (Don't use RUN 200 - it will erase the variables p3basic and frames).

GOTO 1000 - saves the BASIC file (you can add a line number, if you wish).

GOSUB 4000 - will clear all the files from the ramdisk, if you want to run the whole drawing sequence again (it will produce an error if a file doesn't exist, so this is left for you to run manually).

You can also modify the program to get different patterns. The modifications are explained below. Your imagination (and your patience) are the only limits!

 

Explanations

First, we initialise.

In line 10 we set the number of animation frames. 16 is the default value, but you can change this to a higher number that takes longer to draw (but there are only so many that can be stored in the ramdisk), or a lower number (which is quicker to draw) but produces a less satisfying result. After that, line 10 must detect whether we're running under +3 BASIC. PEEK 23399=4 is the simplest way (but is not foolproof).

In lines 20 and 30 the number of frames is capped, based on the size of the ramdisk. Roughly 76K for the 128/+2, 62K for the +2A/+2B, and 58K for the +3. (If you're running on a +2A/+2B, setting the cap to 20 would be ok here).

In line 40, variable x2 is the starting phase of the sine wave, and variable y is used to add a little bit of variation in the wave (line 65 can be commented out to add the value to the next animation frame).

     5 REM Takes 20m,40s to run!
    10 LET frames=16: LET p3basic=0: IF PEEK 23399=4 THEN LET p3basic=1
    20 IF p3basic=0 AND frames>32 THEN LET frames=32
    30 IF p3basic=1 AND frames>19 THEN LET frames=19
    40 LET y=0: LET x2=0

 

Drawing: Here's where the (very) slow magic happens...

Line 60 can be changed to draw more than output per frame (this makes drawing take much longer!) In this case, you probably want to comment out line 65, in order to add a little noise, otherwise you're more likely to see gaps that form lines in the image.

Line 70 counts from 0 to 2*PI (a full cycle of the sine wave). The step value can be changed to determine the number of pixels drawn (and you'll probably want to alter the magic number in line 90 as well).

Line 80 has some magic numbers that can be explained. 40.6 is (roughly) 255/6.283, so that the image fits the 256-pixel width of the screen (but saves having an extra division in the main loop). 32 is the (half) height of the wave, so it's 64 pixels tall and 79 is the offset from the bottom of the screen to the middle of the wave (it's actually 95 pixels from the bottom of the screen, as the PLOT command counts from the bottom and only uses 176 out of the 192 possible Y pixels). It's deliberately occupying only the middle third of the screen because that allows more frames in the (limited) ramdisk.

Line 90 is an actual magic number, randomly selected because it looked good.

Line 120 updates the phase in the cycle for each animation frame, while line 130 saves the image to the ramdisk.

    50 FOR f=1 TO frames
    55 PRINT AT 0,0;f
    60 FOR z=1 TO 1
    65 LET y=0
    70 FOR x=0 TO 6.283 STEP .01
    80 PLOT 40.6*x,(32*SIN (x2+x+SIN (z*y)))+79
    90 LET y=y+0.16
    100 NEXT x
    110 NEXT z
    120 LET x2=x2+ 2*PI/frames
    130 GO SUB 2000: REM SAVE
    140 CLS 
    150 NEXT f

 

Animating: This is where the (quicker) magic happens.

In line 240, each frame is loaded from the ramdisk. The PAUSE in line 230 is 1/50th of a second (on PAL 50 systems). It may be increased, if you want the animation to run slower.

Line 250 can be uncommented, if you want the frame number to display in the top-left of the screen.

    200 REM ANIMATE
    210 BORDER 0: PAPER 0: INK 4: CLS 
    220 FOR f=1 TO frames
    230 PAUSE 1
    240 GO SUB 3000: REM LOAD
    250 REM :PRINT AT 0,0;("0" AND f<10);f;
    260 NEXT f
    270 GO TO 220
    999 STOP 

 

Saving the program to tape (or disk)

Simply clears the variables and saves the BASIC program. You may alter it to SAVE "pattern" LINE 10, if you want it to autorun.

    1000 CLEAR : SAVE "pattern"
    1010 STOP 

 

Saving the animation frames to the ramdisk

This section handles the saving of the middle third of the screen to the ramdisk. Line 2010 is the 128/+2 syntax, while line 2020 is the +2A/+2B/+3 syntax.

18432 is the address of the middle third of the screen and 2048 is the length of 1/3rd of the screen.

The statement ("0" AND f<10) ensures that a zero is added to the filename, if the value of f is less than 10. So, the filenames are scr01, scr02, scr03, ... , scr14, scr15, scr16.

Note: each 2K file seems to take 3K on the +2A/+2B/+3 ramdisk.

    2000 REM SAVE
    2010 IF p3basic=0 THEN SAVE !"scr"+("0" AND f<10)+STR$ fCODE 18432,2048
    2020 IF p3basic=1 THEN SAVE "m:scr"+("0" AND f<10)+STR$ fCODE 18432,2048
    2030 RETURN 

 

Loading the animation frames from the ramdisk

This section handles the loading of the middle third of the screen from the ramdisk. Line 3010 is the 128/+2 syntax, while line 3020 is the +2A/+2B/+3 syntax.

Loading from the 128/+2 ramdisk seems to be quicker than the +2A/+2B/+3 ramdisk.

    3000 REM LOAD
    3010 IF p3basic=0 THEN LOAD !"scr"+("0" AND f<10)+STR$ fCODE 
    3020 IF p3basic=1 THEN LOAD "m:scr"+("0" AND f<10)+STR$ fCODE 
    3030 RETURN 

 

Erasing the animation frames from the ramdisk

This section erases the files from the ramdisk (as long as the variables frames and p3basic haven't been cleared!)

Line 4010 is the 128/+2 syntax, while line 4020 is the +2A/+2B/+3 syntax.

    4000 REM ERASE (will return an error if file doesn't exist)
    4005 FOR f=1 TO frames
    4010 IF p3basic=0 THEN ERASE !"scr"+("0" AND f<10)+STR$ f
    4020 IF p3basic=1 THEN ERASE "m:scr"+("0" AND f<10)+STR$ f
    4025 NEXT f
    4030 RETURN 

 

I was running out of ideas already

Line 60 FOR Z=1 TO 8. Line 65 commented out. It produces a noisy ribbon, in roughly 2 hours and 45 minutes...

 

OK, I added a 48K version...

It works on a lot more machines (and plays back the animation a little faster). But it's no longer entirely The Power of BASIC™!

Some of the lines are different, as a bit of machine code is required...

Line 10 has to set up the machine code. CLEAR is one below the address of the machine code (and also below the addresses where the animation frames are held).

Line 20 now holds the number of animation frames, and line 30 caps the number at 16.

      10 CLEAR 31999: GO SUB 5000
      20 LET frames=16
      30 IF frames>16 THEN LET frames=16

 

The PAUSE has to be a little longer to try to match the speed of the all-BASIC 128K version.

     230 PAUSE 2

 

Line 2010 calculates the address to write to, line 2020 sets the source as 18432, line 2030 sets the destination as the desired address, then line 2040 calls the machine code.

The animation frames are held at 2K intervals from 32768-65535.

    2000 REM SAVE
    2010 LET addr=((f-1)*2048)+32768
    2020 POKE 32001,0: POKE 32002,72
    2030 POKE 32004,addr-256*INT (addr/256): POKE 32005,INT (addr/256)
    2040 RANDOMIZE USR 32000
    2050 RETURN

 

Line 3010 calculates the address to read from, line 3020 sets the source as the desired address, line 3030 sets the destination as 18432, then line 3040 calls the machine code.

    3000 REM LOAD
    3010 LET addr=((f-1)*2048)+32768
    3020 POKE 32001,addr-256*INT (addr/256): POKE 32002,INT (addr/256)
    3030 POKE 32004,0: POKE 32005,72
    3040 RANDOMIZE USR 32000
    3050 RETURN

 

Erase is no longer required, as there are no files, so it's blank.

    4000 REM ERASE (will return an error if file doesn't exist)
    4030 RETURN

 

This sets up the machine code. The value 999 is used as an end marker, to save a little effort by not having to calculate the end address.

    5000 REM Setup 48K m/code
    5010 FOR x=32000 TO 9e9: READ a: IF a<999 THEN POKE x,a: NEXT x
    5020 RETURN
    9999 DATA 33,0,72,17,0,128,1,0,8,237,176,201,999

 

The machine code is (naturally):

    32000 LD HL,18432		; The src address (which is modified by BASIC)
    32003 LD DE,32768		; The dest address (which is modified by BASIC)
    32006 LD BC,2048		; The length
    32009 LDIR			; Copy memory
    32012 RET

 

Put your favourite demo music on in the background

Line 70 FOR x=0 TO 6.283 STEP 0.05. It produces a pointless wavy pattern. At least it's a little quicker!

 


13/11/2025 - FDC1-XBAS

The FDC-1 was the very first ZX Spectrum disk interface made by Technology Research, in 1983, but it did not allow the use of any BASIC commands. You had to RANDOMIZE USR 64512, type your DOS commands, then return to BASIC by typing BAS. (NOTE: It's uncertain whether it was the Macronics FIZ or the FDC-1 that was the very first floppy disk interface for the ZX Spectrum).

That was rather limiting and it received some criticism for it, in the reviews of the time. So, it's no surprise that later models (the Betadisk) included some method of invoking commands from either the BASIC editor line or from within programs.

I wondered if something could be done about this. The solution I came up with involved using DEF FN combined with USR, to extend BASIC (in a slightly awkward way).

0 DEF FN d(x$)=USR VAL "PEEK 23635+256*PEEK 23636+43": REM XXXXX.....

Hidden in the REM statement is some machine code that passes the string through to the address in TR-DOS and executes it. The result is a level of integration into BASIC that is enough to do some useful things. This line takes up 91 bytes and can be MERGEd into another BASIC program, so that disk commands can be used within it. (It is also possible to change the line number and the letter of the function name, if required).

It could be made shorter, but I made it capable of handling BASIC at different start addresses, should something have moved it.

 

Commands can be sent from BASIC in the form:

LET err = FN d("COMMAND")

The maximum length of an FDC-1 command is 32 bytes.

Note that FDC-1 commands are case sensitive, so make them uppercase unless it's specifically referring to a lowercase filename. It may also trigger a request for a password, should the disk require it.

 

The PASMO source code is listed below, it's a very simple example of passing a string to machine code using DEF FN.

LINENUM				EQU		0					; Can't be deleted/edited, etc

	ORG 23755

	db high(LINENUM), low(LINENUM)
	dw LineEnd-LineStart

LineStart:	
; + 4
	db 206, 100                                                                     ; DEF FN d 
	db 040, 120, 036, 014, 000, 000	                                                ; (x$ 014 000 000
	db 000, 000, 000, 041, 061, 192                                                 ; 000 000 000) = USR
	db 176, 034, 190, 050, 051, 054                                                 ; VAL "PEEK 236
	db 051, 053, 043, 050, 053, 054                                                 ; 35+256
	db 042, 190, 050, 051, 054, 051                                                 ; *PEEK 2363
	db 054, 043, 052, 051, 034, 058                                                 ; 6+43":
	db 234                                                                          ; "REM  

; + 43
; Code in here! (23755 + 43 = 23798)

; Get base address (usually $FCF4 / 64756)
	CALL $FC09									; 64521
	LD HL,7
	ADD HL,BC									; HL = buffer address (base + 7)

; Clear string buffer (32 bytes long)
	PUSH HL
;	PUSH HL
;	POP DE
;	INC DE
	LD D,H										; Avoids the 19,1 (INC DE/LD BC,xxxx) combination that causes BRIGHT 1 on the listing
	LD E,L
	INC E
	LD BC,32 - 1
	LD (HL),32									; Fill with spaces
	LDIR

; Copy string to buffer
	LD HL,($5C0B)								        ; 23563 DEFADD (Address of DEF FN parameters)
;	LD BC,4
	LD C,4										; B will be 0, so save a byte by not setting it
	ADD HL,BC									; Get address of pointer and length
	LD E,(HL)
	INC HL
	LD D,(HL)									; Got string pointer
	INC HL
	LD C,(HL)
	INC HL
	LD B,(HL)									; Got string length
	;INC HL

; Cap length of BC at 32 (this routine saves 3 bytes over the other CAP routine)
	LD HL,-33
	ADD HL,BC
	JR NC,_NO_CAP
	LD BC,32
_NO_CAP:

	EX DE,HL									; HL = string pointer
	POP DE										; DE = FDC-1 string buffer
	LDIR										; Copy

; Call command processor
; Return error/status in BC (save a byte by doing a JP instead of a CALL/RET)
	JP $FC0C									; 65424

; CR marking end of the line.
	db 13
LineEnd:

.END

 

The error number is returned from the function, so you can also perform error-checking. BASIC does not automatically stop upon an error.

0 = No error
1 = No files (and, effectively, unrecognised command)
2 = File exists
3 = No space
(There is no error 4!)
5 = Record number overflow (something from LOAD/MERGE?)
6 = User protected
7 = Disc error/Write protected

Error 1 is also shown when doing a DIR and the directory is empty. In this case it isn't an error, although it looks like one. The error numbers are pretty close to the later Betadisk errors.

 

In use, it's possible to have a BASIC loader, which then loads a screen, a main CODE block, and then runs it, as was the standard game/application format at the time. This improves compatibility considerably, as long as the program isn't touching the upper 1K of RAM used by the interface (or 4K by the very early versions). The FDC-1 cannot load or save arrays, though.

The other advantage of using DEF FN, is that it can handle variables and string concatenations without problems.

Here is an example. Imagine you had saved ten screens, named SCR01 to SCR10 and wanted to load them in sequence, similar to a flick-book animation (a common example used with Spectrum disk drives at the time).

10 FOR x = 1 TO 10
20 LET err = FN d("LOAD SCR" + ("0" AND x < 10) + STR$ x)
30 NEXT x

 

A TAP file, PASMO source, and instructions are included in the download. If you want to assemble and modify it yourself, you can. Unfortunately, PASMO can't save a chunk of code as a BASIC file, so you have to use an emulator to load the new binary over the currently loaded version (which can be quite awkward, if it has changed size) or find a utility to edit the TAP file header. I admit it's an untidy solution.

Once loaded from tape, you use itself to save the file to a formatted disk image...

PRINT FN d("SAVE $XBAS")

From a freshly reset TR-DOS (or SM-DOS, if it's the Sandy FDD2 clone) you can then type...

$XBAS

... (instead of the usual BAS) and that will take you to BASIC with the magical line 0 already in place.

Now, the three people in the world who actually own one can try it.

 

FDC-1 XBAS zip

One thing to note. While developing it, I wondered whether I could have the DEF FN after the REM statement. That way I could save a byte (my USR address would always be "+5" instead of "+xx") and I wouldn't have to keep changing that address every time my code got bigger or smaller.

The answer was no. While Sinclair BASIC could still find the DEF FN and make the call, it would then miscalculate an address and corrupt the line! That mysterious "014, 000, 000, 000, 000, 000" after the "DEF FN d(x$" is used to hold some information, including the address of the string from the calling FN d() statement. When it miscalculates, it writes that information over another part of the line. So you must have the DEF FN before the REM statement.

I'm not sure whether this particular bug has been documented?


(C) Jane McKay, 2025