This website does readability filtering of other pages. All styles, scripts, forms and ads are stripped. If you want your website excluded or have other feedback, use this form.

CP/M information archive : FID files on Amstrad CP/M

CP/M pages
Home -> CP/M -> FID files

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.
    • 20h: No drive (used by DISABLEB.FIB when removing drive B: from the system)
    • 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:

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.

FID_C_I_STATUS

This is used to check if the device is ready to provide input to CP/M.

  • Entered with:
  • 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:
  • 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:
  • 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:
  • 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:
  • 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:
  • 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:
    • C, DE, HL, IX, IY can be corrupted.

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:
    • C, DE, HL, IX, IY can be corrupted.

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:
    • C, DE, HL, IX, IY can be corrupted.

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:
    • C, DE, HL, IX, IY can be corrupted.

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.

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 relocatable
will 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