Code Snippets
13/11/2025
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