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.

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.

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.

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.

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.

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