FID files on Amstrad CP/M
Disclaimer: Some of this information (in particular, any relating to the PCW) has been obtained by:
- Inference;
- Deduction;
- Disassembling existing code.
I disclaim all responsibility if it should turn out that it:
- Isn't true;
- Doesn't work;
- Is not supported by (hypothetical) future releases of CP/M or LocoScript.
Support for FID files
FID files (Field Installable Device Drivers) are a system used in Amstrad CP/M to load drivers for hardware without having to change the BIOS itself. There are four systems which use FID files:
- Amstrad CP/M Plus - not all versions. Versions known to support FID files are 1.7H, 1.11+ and 2.9+.
- LocoScript 2 v2.30 and later; LocoScript 3; and presumably LocoScript 4.
- Spectrum +3 CP/M Plus - all versions.
- My implementations of CP/M 2 and 3 on the PCW16.
FID File format
When the system is booted, FIDs are loaded from the boot disc into memory bank 0. This happens when the BIOS and BDOS are present, but the CCP has not yet been started.
A FID file has to be in Digital Research Paged Relocatable format. The exact layout of this file format is described below, but since LINK.COM can generate it, details should not be necessary.
To create a PRL file using LINK.COM, add the [OP]
switch:
LINK FILENAME[OP]
REN FILENAME.FID=FILENAME.PRL
After the PRL header generated by LINK, there is a special FID header. This is 32 bytes long:
xx00: JP FID_EMS xx03: DB 'MACHINE ' ;Computer name, see below xx0B: DB 'FID' ;File type, see below xx0E: DW version ;FID version number, specific to FID xx10: DW checksum ;see below xx12: DB low bound ;Spectrum only xx13: DB high bound ;Spectrum only xx14: DS 12 ;Reserved
The meaning of these fields is as follows:
- JP FID_EMS
- Jump to the FID's startup routine; called just after the FID has loaded.
- DB 'MACHINE '
- The computer on which the FID will run. For the +3, this is the string 'SPECTRUM'; for the PCW, this is 8 bytes all containing 1Ah; for the PCW16, this is the string 'ANNE '. After the FID has loaded, this will be replaced by the actual disc filename of the FID, including attributes.
- DB 'FID'
- This will be replaced by the filetype of the FID, including attributes. On a +3, a PCW16 or in the LocoScript environment, this will always be 'FID'; but very late versions of PCW CP/M (1.14/2.14+) actually search for "*.FI?" (see FIB files for more details).
- DW version
- A version number you can use for your own purposes.
- DW checksum
- The checksum is the sum of all bytes in the PRL file, from the beginning of its header to the end of its relocation map. Locomotive's FIDCSUM.BAS (or, for the impatient, my FIDCSUM.COM) will calculate this.
- DB low bound
DB high bound - The bounds are used on the +3 to limit where a FID file is loaded. They are ignored on the PCW and the PCW16. The low bound is the first page that the FID file can use - eg 40h if it must load at 4000h or higher. The high bound is the first page that the file cannot use - eg 80h if the file must go entirely below 8000h. Normally, these are left as 0.
- DS 12
- Reserved. Under CP/M on the +3 and PCW, the last two bytes (at xx1Eh) point at the FID file previously loaded, or 0 if this is the first FID file to be loaded.
Locomotive recommend that anything which you want application programs to be able to access easily should be put immediately after the header — for example, service routines or internal variables.
FID files are not allowed to make calls to the BIOS or BDOS; they should only make the documented SuperVisor Calls.
SuperVisor Calls
The SuperVisor Calls are values set in the FID when it loads, by the host environment. They correspond to addresses of routines or variables within CP/M or LocoScript.
If you are writing in RMAC or M80, then the SVCs are defined by the following statements at the start of the source file (before any code).
On the PCW, the SVCs are:
SVC_D_HOOK EQU $+0FE00h ;SVC 0 SVC_C_HOOK EQU $+0FE01h ;SVC 1 SVC_D_CHANGED EQU $+0FE02h ;SVC 2 SVC_D_SETUP EQU $+0FE03h ;SVC 3
On the Spectrum +3, there are rather more of them:
SVC_BANK_05 EQU $+0FE00h ;SVC 0 SVC_BANK_68 EQU $+0FE01h ;SVC 1 SVC_CATCHUP EQU $+0FE02h ;etc. SVC_SCB EQU $+0FE03h SVC_C_HOOK EQU $+0FE04h SVC_D_HOOK EQU $+0FE05h SVC_D_CHANGED EQU $+0FE06h SVC_ALLOCATE EQU $+0FE07h SVC_MAX_ALLOCATE EQU $+0FE08h SVC_DEALLOCATE EQU $+0FE09h SVC_C_FIND EQU $+0FE0Ah
My PCW16 implementation currently supports:
SVC_MEMLIST EQU $+0FE00h ;SVC 0 SVC_CPM_ADDR EQU $+0FE01h ;SVC 1 SVC_SCB EQU $+0FE03h ;SVC 3 CP/M Plus only SVC_C_HOOK EQU $+0FE04h ;SVC 4 SVC_D_HOOK EQU $+0FE05h ;SVC 5 SVC_D_CHANGED EQU $+0FE06h ;SVC 6 SVC_C_FIND EQU $+0FE0Ah ;SVC 10
The SVCs must only be used as 16-bit words.
SVC_D_HOOK
Add a disc drive to the system.
This routine must only be called from the FID_EMS routine.
Entered with:
- B = drive letter. 0 => A: ... 0Fh => P:. 0FFh => first available letter.
- DE = address of disc device jumpblock (within the FID).
- HL = maximum no. of bytes required for a double-bit allocation vector.
- IX = maximum no. of bytes required for directory checksum vector.
- IY = maximum no. of bytes required for hash table.
Returns:
- Carry set if OK.
- B = drive letter;
- IX -> 17-byte DPB for drive.
- A corrupt.
- Carry clear if error.
- A = reason:
- 1 => out of memory.
- 2 => drive letter in use.
- B, IX corrupt.
- C, DE, HL, IY corrupt.
SVC_C_HOOK
Add a character device (printer, screen etc) to the system.
This routine must only be called from the FID_EMS routine.
Entered with:
- DE = address of character device jumpblock (within the FID).
- HL = address of a copy of the DEVTBL entry.
- [PCW16] B = device type (0 for CRT, 1 for LPT, 2 for PTP, 3 for PTR, 4 for TTY).
Returns:
- Carry set if OK.
- B = device number 0-13;
- DE -> the actual DEVTBL entry.
- A corrupt.
- Carry clear if error.
- A = reason:
- 1 => no devices available (LocoScript always reports this).
- 2 => device name in use.
- B, DE corrupt.
- A, HL corrupt.
SVC_D_CHANGED
Signal that a disc may have been changed. It can be called during an interrupt.
Entered with B=drive; returns with all registers preserved.
SVC_D_SETUP [PCW only]
Call to DD SETUP.
This call is present in PCW CP/M from versions 1.11 / 2.11 upward. On earlier versions (eg 2.9) it will be left as FF03h. It is intended for use by FIB files to set up disc timing parameters. Call with:
- HL = 0
- B = flags. These correspond to those used by
DD EQUIPMENT
on the Spectrum +3:Bits 0-1: Sidedness 1: Single-sided 2: Double-sided Bits 2-3: Track spacing 1: Single track 2: Double track Bit 5: Drive type 0: 3" 1: 3.5"
Sample values:- 05h: 180k 3" drive.
- 0Ah: 720k 3" drive.
- 2Ah: 720k 3.5" drive.
- C = Unit number, 0-3.
- DE = address of timing parameter block — see DD SETUP.
Returns with AF, BC, DE and HL corrupt.
SVC_MEMLIST [PCW16 only]
Address of list of memory banks used by CP/M.
The list is formed of bytes M0-M9. The memory bank arrangements are:
- Bank 0: M0, M1, M2, M8
- Bank 1: M5, M6, M7, M8
- Bank 2: M0, M3, M4, M8
- Bank 3: M0, M9, M2, M8
Although CP/M 2 does not support multiple banks, the PCW16 BIOS does in fact implement the SELMEM call, and these are the memory banks it uses. FID files are loaded into Bank 2; the TPA is Bank 1, and Bank 0 contains a copy of the BDOS and CCP.
SVC_CPM_ADDR [PCW16 only]
Convert a CP/M address to a Rosanne bank and offset.
Entered with:
- C = CP/M bank.
- HL = address within bank.
Returns:
- C = Rosanne block number.
- HL unchanged (suitable for use as a block offset)
- All other registers preserved.
SVC_BANK_05 [+3 only]
Address of the BANKM system variable, which is not 23388. This variable holds the last value written to port 7FFDh.
SVC_BANK_68 [+3 only]
Address of the BANK678 system variable, which is not 23399. This variable holds the last value written to port 1FFDh.
SVC_CATCHUP [+3 only]
Simulate a timer interrupt.
This routine should be called if you think it likely that some interrupts have been missed.
Entered with:
- Interrupts disabled.
Returns:
- Interrupts still disabled.
- AF, BC, DE, HL corrupt.
SVC_SCB [+3 and PCW16 only]
Address of the System Control Block, xx9Ch.
In the PCW16 implementation, this is 0 if the FID is being loaded into a CP/M 2 system. It is important to check that SVC_SCB is nonzero before using the SCB.
SVC_ALLOCATE [+3 only]
Allocate some memory.
This routine must only be called from the FID_EMS routine.
Entered with:
- DE=bytes required.
Returns:
- If OK: Carry set, HL = address of memory.
- If not enough memory available: Carry reset, HL corrupt.
- A, BC, DE corrupt.
This function (and the two following ones) are in fact present in the FID loader under PCW CP/M, but they are not exposed to FIDs. This may be because they have no LocoScript equivalent — or it may be that they were exposed so that the RAMDISC.FID driver supplied with +3 CP/M could be written.
SVC_MAX_ALLOCATE [+3 only]
Allocate all free memory.
This routine must only be called from the FID_EMS routine.
Returns:
- If OK: Carry set, HL = address of memory, DE = length of memory
- If not enough memory available: Carry reset, DE & HL corrupt.
- A, BC, corrupt.
SVC_DEALLOCATE [+3 only]
Deallocate some memory.
This routine must only be called from the FID_EMS routine. It can be used to deallocate memory allocated when the FID was loaded.
Entered with:
- HL=address of area to free.
- DE=no. of bytes to free.
Returns:
- AF, BC, DE, HL corrupt.
SVC_C_FIND [+3 and PCW16 only]
Find another character device driver.
This routine must only be called from the FID_EMS routine.
Entered with:
- HL=address of the 6-byte character device name (eg "LPT ")
Returns:
- Carry set if found; DE = address of character device jumpblock.
- Carry reset if not found; DE corrupt.
- A, BC, HL corrupt.
The jumpblock that is retrieved may belong to another FID, or to CP/M itself. It may not consist entirely of JP nnnn instructions, so your code should jump to it rather than trying to calculate the addresses of the routines at which it points.
As with the memory allocation functions, this function is present in PCW CP/M, but not exposed to FIDs.
So, what actually goes in the FID?
Routines supplied by the FID
FID_EMS
This is the one piece of code which the FID must provide. It is called immediately after the FID has loaded. It is entered with:
- DE = environment version - D=major, E=minor (currently 0.0).
- If D is nonzero, the FID should terminate with an "invalid version" error.
- If E is nonzero, more facilities have been added.
- C = language to display messages in. Keeping 12 different sets
of messages in a FID might just be regarded as a waste of space,
but...
- 0 : USA
- 1 : France
- 2 : Germany
- 3 : UK
- 4 : Denmark / Norway
- 5 : Sweden
- 6 : Italy
- 7 : Spain
- 20: Holland
- 21: Finland
- 23: Wales
- 24: Portugal
The FID will return with the carry flag set if it wants to stay, and reset if it does not. If it is returning with carry reset, it must deallocate any memory it had previously allocated with SVC_ALLOCATE or SVC_MAX_ALLOCATE. It must not return carry reset if it has used SVC_x_HOOK to add itself to the BIOS.
On return, HL should point at a message (terminated with CR, LF, 0FFh) which will be printed by the BIOS. It should fit on one line; under CP/M, it can contain escape sequences. LocoScript will try to centre the message on its startup screen.
Character device jumpblock
The character device jumpblock is passed to SVC_C_HOOK when new character devices are added. It reads:
JP FID_C_INIT JP FID_C_I_STATUS JP FID_C_INPUT JP FID_C_O_STATUS JP FID_C_OUTPUT JP FID_C_M_STATUS JP FID_C_MESSAGE
FID_C_INIT
This is called when the device is first added to the devices table, and also whenever CP/M changes the baud rate using DEVICE.
- Entered with:
- B = device number 0-13
- DE-> device table entry
- Can corrupt AF, BC, DE, HL.
FID_C_I_STATUS
This is used to check if the device is ready to provide input to CP/M.
- Entered with:
- B = device number 0-13
- DE-> device table entry
- Returns carry set if yes, carry clear if no.
- Can corrupt A, BC, DE, HL.
FID_C_INPUT
Wait for a character and return it.
- Entered with:
- B = device number 0-13
- DE-> device table entry
- Returns A=character.
- Can corrupt F, BC, DE, HL.
FID_C_O_STATUS
This is used to check if the device is ready to take output from CP/M.
- Entered with:
- B = device number 0-13
- DE-> device table entry
- Returns carry set if yes, carry clear if no.
- Can corrupt A, BC, DE, HL.
FID_C_OUTPUT
Output a character, return when it is done.
- Entered with:
- B = device number 0-13
- C = character
- DE-> device table entry
- Can corrupt AF, BC, DE, HL.
FID_C_M_STATUS
This is used to check if the device is ready to take system message output from CP/M.
- Entered with:
- B = device number 0-13
- DE-> device table entry
- Returns carry set if yes, carry clear if no.
- Can corrupt A, BC, DE, HL.
FID_C_MESSAGE
Output a system message character, return when it is done.
- Entered with:
- B = device number 0-13
- C = character
- DE-> device table entry
- Can corrupt AF, BC, DE, HL.
Disc device jumpblock
The disc device jumpblock is passed to SVC_D_HOOK when new disc devices are added.
It reads:
JP FID_D_LOGON JP FID_D_READ JP FID_D_WRITE JP FID_D_FLUSH JP FID_D_MESS
FID_D_LOGON
Log in a disc. Initialise the DPB.
- Entered with:
- B = drive
- IX -> 17-byte DPB
- Returns:
- If OK:
- Carry set
- A=0
- B corrupt.
- If unrecoverable error:
- Carry clear
- Zero set
- A=0FFh if media changed, otherwise 1
- B corrupt.
- If recoverable error:
- Carry clear
- Zero clear
- A=FFh if media changed, otherwise 1
- B=internal message number
- C, DE, HL, IX, IY can be corrupted.
- If OK:
FID_D_READ
Read a sector (512 bytes).
- Entered with:
- B = drive
- DE = sector number (using figures from the DPB)
- HL = track number (using figures from the DPB)
- IX -> 17-byte DPB
- IY = address to load sector
- Returns:
- If OK:
- Carry set
- A=0
- B corrupt.
- If unrecoverable error:
- Carry clear
- Zero set
- A=0FFh if media changed, otherwise 1
- B corrupt.
- If recoverable error:
- Carry clear
- Zero clear
- A=FFh if media changed, otherwise 1
- B=internal message number
- C, DE, HL, IX, IY can be corrupted.
- If OK:
FID_D_WRITE
Write a sector (512 bytes).
- Entered with:
- B = drive
- C = deblocking code
- DE = sector number (using figures from the DPB)
- HL = track number (using figures from the DPB)
- IX -> 17-byte DPB
- IY = address of sector to write
- Returns:
- If OK:
- Carry set
- A=0
- B corrupt.
- If unrecoverable error:
- Carry clear
- Zero set
- A=0FFh if media changed, 2 if read-only, otherwise 1
- B corrupt.
- If recoverable error:
- Carry clear
- Zero clear
- A=FFh if media changed, 2 if read-only, otherwise 1
- B=internal message number
- C, DE, HL, IX, IY can be corrupted.
- If OK:
FID_D_FLUSH
Write anything to disc that needs writing.
- Entered with:
- B = drive
- IX -> 17-byte DPB
- Returns:
- If OK:
- Carry set
- A=0
- B corrupt.
- If unrecoverable error:
- Carry clear
- Zero set
- A=0FFh if media changed, 2 if read-only, otherwise 1
- B corrupt.
- If recoverable error:
- Carry clear
- Zero clear
- A=FFh if media changed, 2 if read-only, otherwise 1
- B=internal message number
- C, DE, HL, IX, IY can be corrupted.
- If OK:
FID_D_MESS
Expand an internal message number to an ASCII string. The string should be at most 50 characters long and end with 0FFh. The message "Retry, Ignore or Cancel?" will be appended to it when it is displayed.
- Entered with:
- B = message number
- Returns:
- If OK:
- Carry set
- HL = address of message
- If B was invalid:
- Carry clear
- HL corrupt.
- A, BC, DE can be corrupted.
- If OK:
Talking to a FID
If you have a COM file which wants to talk to a FID, then it should first check that the CP/M version under which it is running supports FIDs at all. If it does, then call FIND FID to see if your FID is loaded.
FIND FID returns the address of the FID header (the JP FID_EMS instruction). Calls or bytes which you want to access should be in the bytes just after the header — ie, starting at byte 20h. Since the FID is loaded on a page boundary, offsets can be calculated by loading the L register with the offset.
Assuming your FID is loaded, then you can make calls to it:
LD DE,fidname CALL FIND_FID LD L,20h LD (CALLAD),HL ; ;Set up any parameters ; CALL USERF CALLAD: DW 0
Or you can access data in the FID. Either put some transfer code above 0C000h and call it with USERF, or copy blocks in/out with XMOVE and MOVE.
Breaking the rules
If you're writing a device driver, then the routine interfaces above come in very handy. But since FIDs are loaded into the same memory bank as the XBIOS, the opportunities for mischief are quite extensive.
Test for CP/M
This test is used by Cirtech in CEN.FID:
TEST: LD HL,0FC00h LD DE,3 LD B,1Eh TEST1: LD A,(HL) CP 0C3h RET NZ ADD HL,DE DJNZ TEST1 XOR A RET
— it checks that the BIOS jumpblock is present, and returns Z if it is or NZ if it isn't. This should be used if you're going to play around with the internals of CP/M or LocoScript.
SVCSCB on a PCW
This is only possible under CP/M, so use the test above. Then, you can use:
SVCSCB EQU 0FB9Ch
and it should work.
Calling Rosanne
Any FID loaded in the PCW16 can call any Rosanne function except
os_app_exit
and os_release_allmem
. Future versions
may impose more restrictions on calls that can be used.
The three supplied FID files use precisely this technique for disc I/O, memory allocation and screen output.
Calling the XBIOS
Although a FID is emphatically not allowed to call the BIOS or BDOS, it seems to be possible to call at least some of the XBIOS routines. You will have to experiment to find out which. The XBIOS is probably not present under LocoScript (I haven't looked) so use the CP/M test again.
In PCW16 CP/M, you must not call USERF, and the XBIOS is not present in the same bank as the FID files; so you can't call it.
Hooking the XBIOS
Suppose you wanted to change the beep sound. Since there is no SVC_SOUND_HOOK command, a little subtlety is called for.
On both the +3 and the PCW, the beeper is controlled by OUT commands. A quick search (using, for example, SID with SIDRSX and BANKRSX) will quickly reveal the piece of code responsible.
The thing to do is avoid absolute addresses. Things move around from version to version, and it isn't fun maintaining a vast table of the known addresses of things in different versions.
Instead, write a routine in FID_EMS to scan memory for a characteristic pattern of bytes matching this routine. Whenever the signature is matched (there may be more than one copy of the routine in memory) insert a jump to your replacement. While this technique isn't foolproof, it seemed to work for me in the two FIDs I wrote which use it.
FIB files
A .FIB file is a file in the .FID format, containing disc timing parameters for a PCW add-on drive. .FIB files are not supported on the Spectrum +3.
Under CP/M, the two file types are treated interchangeably; under LocoScript, .FIB files are read in immediately the system is started, and .FID files only after LocoScript/Spell/File/Mail have all signed on.
When a .FIB file is loaded, it will install its disc timing parameters and depart.
These .FID files allow the +3 floppy drives to be run at different timings.
Curiously, although PCW CP/M had the FID_D_SETUP call since version 1.11 / 2.11, it only became able to load .FIB files in version 1.14 / 2.14. Before then, the drives could be driven at different rates, but the .FIB file would have had to be renamed to .FID.
PRL file format
PRL files are designed to be loadable on a page boundary — an address which is a multiple of 256. They are used by Digital Research for such things as RSXs or GSX graphics drivers.
This description does not cover all variations of the PRL file format. For that, see PRL File Format.
A PRL file starts with a 256-byte header:
DB 0 ;unused byte DW len ;Length of code + initialised data, bytes DB 0 ;unused byte DW bsslen ;Length of uninitialised data, bytes DS 249 ;Unused
In practice, a FID will nearly always set bsslen to 0, and keep all its variables in one place. This is partly because neither M80 nor RMAC properly supports uninitialised data areas.
A PRL file can be created by Digital Research's linker LINK.COM:
LINK FILENAME[OP]
REN FILENAME.FID=FILENAME.PRL
and uninitialised data can be allocated using the M
switch:
LINK FILENAME[OP, Mxxxx]
— where xxxx = no. of bytes to allocate, Hex.
After the 256 bytes of header, there are len bytes of FID code, and then ( len + 7 ) / 8 bytes of relocation map. Each byte in the map corresponds to 8 bytes in the PRL file, with bit 7 corresponding to the lowest byte and bit 0 corresponding to the highest. So bit 7 of the first map byte corresponds to the first byte after the header of the PRL file.
If a bit is set in the map, a byte in the file is relocatable. For a PRL file whose header was loaded at xx00 hex, relocatable bytes should have xx added to them. In practice, the header is discarded; so an alternative formula is that if the first byte of FID code is at xx00h, then xx-1 should be added to relocatable bytes.
In FID files, relocatable bytes with a value of 0FFh refer to the SuperVisor Calls. In this case, both the relocatable byte and the one before it will be replaced by the address of the SVC:
01 FF | | | +---This byte marked relocatable +------This byte not marked relocatablewill be replaced by the address of SVC 1.
FID files also contain a signature and checksum which are described above.
John Elliott 1 October 2009