[comp.sys.amiga.tech] A guide for prospective device driver writers

mwandel@bnr-rsc.UUCP (Markus Wandel) (06/01/90)

Below is a file which I wrote recently about Amiga device drivers.  I think
it might be useful to some people, and lacking other convenient distribution
mechansims, I'll just post it here.  This doesn't have to be the final version
of the document; feedback would be appreciated and if someone wants to write
an additional section covering material I don't, I'd appreciate that too.

Is there *any* real documentation from Commodore on this subject?

Markus Wandel
uunet!bnrgate!bnr-rsc!mwandel

------------------------------- cut here ------------------------------------

AMIGA DEVICE DRIVER GUIDE
-------------------------

Copyright (c) 1990 Markus Wandel

Version 0.12, May 21, 1990.

Distribution: Free in unmodified form.

Disclaimer: I don't know what I'm talking about; no guarantee is made of
the correctness of anything in this document.  Not all of this will conform
to Commodore sanctioned programming and documentation practices.  Version
1.4/2.0 of the Amiga OS may render part of this obsolete, particularly
where I mention the internals of exec functions.

Corrections: If you find something wrong and would like it corrected, get
in touch with me so I can have an up-to-date master copy.  Currently
I can be reached at (613) 591-7698.  Should this number become invalid,
call my parents at (705) 785-3383 or (705) 736-2285 (summer) and get the
current one.


TABLE OF CONTENTS
-----------------

0. INTRODUCTION
1. DEVICE STRUCTURE
    1.1. DEVICE NODES
    1.2. CONSTRUCTING A DEVICE NODE
    1.3. STANDARD DEVICE FORMAT
    1.4. DEVICES IN ROM
    1.5. DEVICES ON DISK
    1.6. JUMP VECTOR CHECKSUMS
2. DEVICE I/O PROTOCOL
    2.1. THE I/O REQUEST STRUCTURE
    2.2. OPENING AND CLOSING A DEVICE
    2.3. EXPUNGING A DEVICE
    2.4. UNIT STRUCTURES
    2.5. THE BEGINIO FUNCTION
    2.6. THE ABORTIO FUNCTION
    2.7. EXEC I/O FUNCTIONS
    2.8. CALLING BEGINIO DIRECTLY
    2.8. SYNCHRONOUS I/O
    2.9. ASYNCHRONOUS I/O
        2.9.1. WAITING FOR A SPECIFIC I/O REQUEST
        2.9.2. WAITING ON A SPECIFIC REPLY PORT
        2.9.3. GENERAL CASE
3. GENERIC COMMAND AND ERROR NUMBERS
    3.1. COMMANDS
        3.1.1. CMD_RESET
        3.1.2. CMD_READ
        3.1.3. CMD_WRITE
        3.1.4. CMD_UPDATE
        3.1.5. CMD_CLEAR
        3.1.6. CMD_STOP
        3.1.7. CMD_START
        3.1.8. CMD_FLUSH
    3.2. ERROR NUMBERS
4. DISK DEVICE DRIVERS
    4.1. COMMANDS
        4.1.1. CMD_READ AND CMD_WRITE
        4.1.2. TD_MOTOR
        4.1.3. TD_SEEK
        4.1.4. TD_FORMAT
        4.1.5. TD_PROTSTATUS
        4.1.6. TD_RAWREAD AND TD_RAWWRITE
        4.1.7. TD_GETDRIVETYPE
        4.1.8. TD_GETNUMTRACKS
        4.1.9. TD_CHANGESTATE
        4.1.10. OTHER COMMANDS
    4.2. ERROR NUMBERS
    4.3. SCSIDIRECT PROTOCOL
    4.4. A MINIMAL DISK COMMAND SUBSET
5. REFERENCES
6. REVISION HISTORY
}i}I_{_{_{wi}i{_}i{M_{_{_}i{_}i{_}i}ikkk

0. INTRODUCTION
---------------

A number of people have asked me to explain Amiga device drivers to them.
There is a lot to explain, and so I decided to write down all I know about
the subject.  Here is the result.

This is not a standalone document.  It assumes that you are familiar with
Amiga programming at the multiple task, message passing level.  It assumes
that you have the autodocs, include files, and the example device driver
from Commodore.  A lot of information from these documents is duplicated,
but not all.

I wish that I knew more about this subject than I do and that I was a better
writer.  Alas, I don't and I'm not.  This document contains all you need to
write a disk resident, autoloading and expungeable device driver, but you
may have to read it more than once.  To write a good, removeable-media disk
driver, you will have to do some additional research.


1. DEVICE STRUCTURE
-------------------

This section describes the data structure associated with a loaded device,
and how to get a device into the system.  As far as discussed in this
section, libraries and devices are identical.


1.1. DEVICE NODES

A device is known to the system by its device node.  The device node
consists of three parts:

    (a) a jump table to the device functions
    (b) a library structure
    (c) any private data that the device has

The "device address" is the base address of the library node; thus the
jump table is at negative offsets from the address, and everything else
is at positive offsets.

Each entry in the jump table is a "jmp" to a 32-bit address.  Thus the
first jump is at offset -6 from the device address, the second at offset
-12, and so forth.  The following four are standard for all devices and
libraries:

    -6: Open
   -12: Close
   -18: Expunge
   -24: ExtFunc

These are the entry points for opening, closing, and expunging (unloading)
the device, respectively.  The last one appears to be for future expansion.

Device drivers have two more standard functions:

   -30: BeginIO
   -36: AbortIO

These are the entry points for submitting an I/O request and cancelling a
pending one, respectively.

The library structure is shown below in "unwound" form.

    struct Library {
        struct  Node {
            struct  Node *ln_Succ;
            struct  Node *ln_Pred;
            UBYTE   ln_Type;
                /*  NT_DEVICE = 3  */
            BYTE    ln_Pri;
            char    *ln_Name;
        } lib_Node;
        UBYTE   lib_Flags;
                /*  LIBF_SUMMING = 1
                    LIBF_CHANGED = 2
                    LIBF_SUMUSED = 4
                    LIBF_DELEXP  = 8  */
        UBYTE   lib_pad;
        UWORD   lib_NegSize;
        UWORD   lib_PosSize;
        UWORD   lib_Version;
        UWORD   lib_Revision;
        APTR    lib_IdString;
        ULONG   lib_Sum;
        UWORD   lib_OpenCnt;
    };

The device is chained on the system device list by the node structure at
the top.  The device list can be found at ExecBase->DeviceList.  Of
interest is the "ln_Type" field, which must be NT_DEVICE, and the
"ln_Name" field, which must be the device name in standard form, such
as "serial.device".

The "lib_PosSize" and "lib_NegSize" fields indicate the number of bytes
used above and below the device base address.  Thus "lib_NegSize" is
the size of the jump vector, and "lib_PosSize" is the size of the library
structure plus the user's private data, if any.

"lib_Version", "lib_Revision", and "lib_IdString" store more information
about the device.  For "serial.device", version 34, revision 12, the
string would look like this:

    "serial 34.12 (27 Mar 1989)"

This appears to be the accepted standard format.

The "lib_Sum" field, along with the flag bits "LIBF_SUMMING",
"LIBF_CHANGED", and "LIBF_SUMUSED" are for the jump vector checksum
mechanism, which is discussed farther on.

The "lib_OpenCnt" field counts the number of times that the
device is currently open.  If this is not zero and an expunge is
requested, the device can use the "LIBF_DELEXP" flag to remember that
it should disappear at the earliest opportunity.


1.2. CONSTRUCTING A DEVICE NODE

In theory, you could just allocate all the memory needed for the device
node, manually initialize the jump vector and the required fields in the
library node, get ExecBase->DeviceList, do a Forbid(), and add the node
to the list using Enqueue(), AddHead(), or AddTail().  But there is an
easier way.  It is the following function:

    AddDevice(device)
              A1

This takes an initialized device node, does the Forbid()/Permit(), and
adds the node to ExecBase->DeviceList.  It also calls Sumkick() (discussed
later).  The node itself can be constructed with this function:

    library = MakeLibrary(vectors, structure, init, dataSize, segList)
    D0                    A0       A1         A2    D0        D1

The first parameter points to the table of function addresses for the
jump vector.  It can have one of the following formats:

    (a) Relative

        vectors: dc.w   -1
                 dc.w   func1-vectors
                 dc.w   func2-vectors
                 ...
                 dc.w   -1

    (b) Absolute

        vectors: dc.l   func1
                 dc.l   func2
                 ...
                 dc.l   -1

The "dataSize" parameter determines "positive size" of the device node.
It must be at least the size of a library structure.  The "negative
size" is implicit from the number of jump vector entries supplied.

The "MakeLibrary" function will allocate the appropriate amount of memory,
fill in the jump vector, then use InitStruct() to clear and initialize
the positive offset area.  The "structure" parameter points to a data
table for the InitStruct() function.  This function is a complex thing,
best used with the macros supplied.  Below is the table in Commodore's
example device driver:

    dataTable:
       INITBYTE   LN_TYPE,NT_DEVICE
       INITLONG   LN_NAME,myName
       INITBYTE   LIB_FLAGS,LIBF_SUMUSED!LIBF_CHANGED
       INITWORD   LIB_VERSION,VERSION
       INITWORD   LIB_REVISION,REVISION
       INITLONG   LIB_IDSTRING,idString
       DC.L   0

It means this:

       - store a byte NT_DEVICE at offset LN_TYPE
       - store the device name as a longword (pointer) at LN_NAME
       - store LIBF_SUMUSED!LIBF_CHANGED in the "lib_Flags" field
       - store the version, revision, and IdString in the appropriate fields.

If you want to know more about the Initstruct() function (which is very
flexible and can do more than suggested above), you should read the autodoc
for it and possibly the disassembly as well.

Finally, we have the "init" and "segList" parameters.  "init" is the
address of the device's own initialization code.  This is called after
construction of the node is complete.  "segList" describes where the device's
code is loaded.  The initialization code is called with the device node
address in D0 and the SegList parameter in A0.  The value it returns
is the value returned by MakeLibrary.  Thus the initialization code
will normally return the device node address as it received it.

The only remaining thing to note is that the "structure" and "init" fields
can be set to zero to not initialize the positive offset area and not
call any initialization code, respectively.

So to summarize what we have so far, the first-principles way to get a device
into the system is the following:

    - LoadSeg() the code into memory, if necessary
    - MakeLibrary() to build the library node
    - AddDevice() with the result to add the node to the device list.


1.3. STANDARD DEVICE FORMAT

Real devices are in a standard format, which allows them to be loaded
and initialized without any knowledge of their internals.  The standard
format is based on a "resident structure", or "RomTag".  Such structures
tag the various libraries and devices in the system ROM and in the LIBS:
and DEVS: directories on disk.  The structure is the following:

    struct Resident {
        UWORD rt_MatchWord;
                /*  RTC_MATCHWORD = 0x4AFC  */
        struct Resident *rt_MatchTag;
        APTR  rt_EndSkip;
        UBYTE rt_Flags;
                /*  RTF_COLDSTART = 1
                    RTF_AUTOINIT  = 128 */
        UBYTE rt_Version;
        UBYTE rt_Type;
                /*  NT_DEVICE = 3 */
        BYTE  rt_Pri;
        char  *rt_Name;
        char  *rt_IdString;
        APTR  rt_Init;
    };

The first two fields are special, allowing the structure to be found
during a ROM scan.  They are the official 68000 "illegal instruction",
opcode 0x4AFC, followed by a pointer to the instruction.  The next field
speeds up the ROM scan by pointing to the address at which the scan
should continue, usually at the end of the area tagged by this RomTag.

The "rt_Version" field has the familiar version number, and the "rt_Type"
field is NT_DEVICE for our application.  The "rt_Pri" field, together
with the "rt_Version" field, determines which of multiple modules with
the same name will be accepted, should such a situation occur.  The
"rt_Name" and "rt_IdString" fields identify the module; they should be
set to the device name and IdString as discussed earlier for a device
node.

Things tagged by RomTags are referred to as "resident modules", and
are initialized like this:

    InitResident(resident, segList)
                 A1        D1

Here, "resident" points to the RomTag, and "segList" identifies the
code associated with it (this is optional).

The InitResident() function first checks if the RTF_AUTOINIT flag is
set in the RomTag.  If it is not, it simply calls the routine pointed to
by "rt_Init" with A0 containing the value passed as "segList", and
returns.

If the RTF_AUTOINIT flag is set, then the function does all the device
initialization work for you.  In this case, "rt_Init" points to a data
table of four longwords, containing these parameters for the "MakeLibrary"
function:

    - dataSize
    - vectors
    - structure
    - init

InitResident calls the MakeLibrary function with these values, plus the
obligatory "segList" parameter.  Unless MakeLibrary() returns null, the
new library node is now added to the appropriate system list, based on
the "ln_Type" field in the node.  If it is NT_DEVICE, the node is added
to the device list, which is what we want.

You should now have enough information to create a standard device header
and initialization function, and to understand the one in Commodore's
example device driver.


1.4. DEVICES IN ROM

Devices in ROM are found during a scan of the entire ROM for RomTag
structures.  This occurs at boot time.  The RomTags which are found
are added to the resident module list in ExecBase.  All those whose
RTF_COLDSTART flag is set are automatically started up with InitResident().

Modules can be "ramkicked" using a mechanism which I will not describe
here.  This causes them to survive a reset, be found in RAM, and be
initialized along with the ROM modules at cold start time.  This is how
the RAD: bootable RAM disk works.


1.5. DEVICES ON DISK

Devices on disk reside in the DEVS: directory.  They are standard object
modules, and loaded with LoadSeg().  After they have been loaded, the
first hunk of the seglist is scanned for a RomTag, and the device is
initialized with an InitResident().  When building a disk resident device,
be careful that the linker does not add a dummy hunk at the front of your
code (old versions of BLINK do this), as this would cause your RomTag not
to be found.


1.6. JUMP VECTOR CHECKSUMS

The "lib_Sum" field in the library structure can be used to hold a checksum
of the jump vector, to guard against accidental or unauthorized
modification.  The "LIBF_SUMUSED" flag bit indicates that this should be
done.  The "LIBF_CHANGED" flag bit indicates that the jump vector has
been modified and the checksum is invalid.  The following function
implements the mechanism:

    SumLibrary(library)
               A1

This does the following:

    IF the LIBF_SUMUSED flag is set THEN
        Forbid()
        IF the LIBF_CHANGED flag is set THEN
            Compute checksum and store in lib_Sum
            Clear the LIBF_CHANGED flag
        ELSE
            IF the lib_Sum field is zero THEN
                Compute checksum and store in lib_Sum
            ELSE
                Compute checksum
                IF checksum does not match lib_Sum THEN
                    Put up recoverable alert #81000003
                    Store new checksum in lib_Sum
                ENDIF
            ENDIF
        ENDIF
        Permit()
    ENDIF

Thus when creating a new device node, it is best to set the LIBF_SUMUSED
and LIBF_CHANGED bits, so that the first call to SumLibrary() updates the
checksum and future calls detect modifications to the jump vector.

The system sets the LIBF_CHANGED bit and calls SumLibrary() as part of the
SetFunction() call to keep the checksum valid.


2. DEVICE I/O PROTOCOL
----------------------

This section describes the interface to a device once it is loaded and
initialized.


2.1. THE I/O REQUEST STRUCTURE

The system communicates with a device driver by use of an "I/O Request"
structure.  The most common version of this is shown below, but all
fields after the "io_Error" field are device driver specific and may
be omitted or redefined.

    struct IOStdReq {
        struct  Message {
            struct  Node {
                struct  Node *ln_Succ;
                struct  Node *ln_Pred;
                UBYTE   ln_Type;
                        /*  NT_MESSAGE  = 5
                            NT_REPLYMSG = 7 */
                BYTE    ln_Pri;
                char    *ln_Name;
            } mn_Node;
            struct  MsgPort *mn_ReplyPort;
            UWORD   mn_Length;
        } io_Message;
        struct  Device  *io_Device;
        struct  Unit    *io_Unit;
        UWORD   io_Command;
        UBYTE   io_Flags;
                /*  IOF_QUICK = 1 */
        BYTE    io_Error;
        ULONG   io_Actual;
        ULONG   io_Length;
        APTR    io_Data;
        ULONG   io_Offset;
    };

The first field in the I/O request is a standard message structure,
allowing it to be enqueued on message ports and replied to.  The
"io_Device" field points to the device structure, and the "io_Unit"
field points to a "unit" structure maintained by the device driver
for each functional unit (e.g. disk drive).  Thus these two fields
identify the target of the I/O request.


2.2. OPENING AND CLOSING A DEVICE

To communicate with a device, one needs at least one I/O request,
and a message port to which the device can return I/O requests which
it has finished processing.  These are easily created using the following
two C library functions:

    MyPort = CreatePort( name, pri )
    struct MsgPort *MyPort;
    char *name;
    BYTE pri;

    ioStdReq = CreateStdIO( ioReplyPort )
    struct IOStdReq *ioStdReq;
    struct MsgPort *ioReplyPort;

The first function creates a message port and associated signal bit, and
returns the address of the port.  The second function creates an IOStdReq
structure, fills in the address of the reply port, and returns the
address of the structure.  Then the device driver can be opened using
this function:

    error = OpenDevice(devName, unitNumber, iORequest, flags)
    D0                 A0       D0          A1         D1

The parameters are the name of the device (e.g. "trackdisk.device", the
unit number (e.g. 0 for the internal drive), the address of the finished
I/O request structure, and a "flags" word containing special information
for the device driver.  The following C code will open the trackdisk
driver for the internal drive, and close it again:

    /*
     *  Do not supply a name for the message port, since supplying a name
     *  will put it on the public message port list.
     */
    td_mp = CreatePort(0L,0L);
    if(!td_mp) exit(99);
    td_iob = CreateStdIO(td_mp);
    if(!td_iob) {
        DeletePort(td_mp);
        exit(99);
    }
    if(OpenDevice("trackdisk.device",0L,td_iob,0L)) {
        printf("Unable to open floppy device driver.\n");
        DeleteStdIO(td_iob);
        DeletePort(td_mp);
        exit(99);
    }

    /*
     *  I/O request is ready for operations here.
     */

    CloseDevice(td_iob);
    DeleteStdIO(td_iob);
    DeletePort(td_mp);

OpenDevice() and CloseDevice() are exec functions, but are intercepted by
something called "ramlib.library", which handles the loading and unloading
of disk resident devices and libraries.  Thus the OpenDevice() call may
cause the device to be loaded and initialized, and the CloseDevice() may
cause it to be unloaded.

The OpenDevice() eventually results in the initialized device driver being
called through its Open() function (at offset -6 in the jump vector).
This occurs in the context (task) of the calling program.  The device driver
is passed the following information:

    D0:  Unit number
    D1:  Flags
    A1:  I/O request pointer
    A6:  Device node pointer

The exec will have already cleared the "io_Error" field in the I/O request,
and stored the device node pointer in the "io_Device" field.

If the Open() function succeeds, it must initialize the "io_Unit" field
for later uses of the I/O request.  If it fails, it must set the
"io_Error" field to the appropriate number.  The value of the "io_Error"
field on exit is returned by the OpenDevice() function.

The device driver should keep track of the number of outstanding opens
using the "lib_OpenCnt" field in its device node.  Some device drivers
can support an arbitrary number of concurrent opens (e.g. disk drivers),
while others can be opened in "exclusive access" modes (e.g. serial ports).

The unit number and flags are two 32-bit words whose format is up to
the device driver writer.  For an example unit numbering scheme, see
the include file "devices/scsidisk.h", and for some example uses of the
flag bits, see "devices/serial.h".

The CloseDevice() call eventually results in the device driver being
called through its Close() function, at offset -12 in the jump vector.
The I/O request pointer is passed in A1, and the device node pointer in
A6.  The return value determines what happens with the closed device.
If it is zero, the device is kept around.   If it is non-zero, it means
that the device has performed a delayed expunge and wishes to be unloaded.
In this case, the return value is the "segList" parameter which was passed
to the device at initialization time.  More of this in the next section.


2.3. EXPUNGING A DEVICE

A device can be deleted from the system using this function:

    error = RemDevice(device)
    D0                A1

The system may issue this function itself, if it runs out of memory and
tries to reclaim space.  Either way, the device is called through its
Expunge() entry point, at offset -18 in the jump vector.  Register A6
is set up to point to the device node.

The device should now shut down its activity, i.e. remove interrupt
servers, deallocate buffers, and so on.  Then it should unlink its
device node from the device list, and deallocate the node, thus restoring
things to the state they were in just before it was started up with
InitResident().  Finally, it should return the "segList" parameter which
was passed to it at initialization time.  If the device came from disk,
the system will use this to unload its code.

If the device is not idle when the Expunge() call arrives, it can defer the
operation.  To do this, it sets the LIBF_DELEXP flag in the library
structure, and returns zero.  This indicates that it will delete itself
at the earliest opportunity.  When the last Close() call arrives, it
will shut itself down just as described above, and return the segList
value to indicate that it has done so and should be unloaded.


2.4. UNIT STRUCTURES

Many device drivers manage more than one functional unit.  For example,
the trackdisk driver can handle up to four floppy drives.  The preferred
approach is to use a separate "Unit" structure for each functional unit
(e.g. drive).  Normally, a unit structure consists of at least the following:

    struct Unit {
        struct  MsgPort unit_MsgPort;
        UBYTE   unit_flags;
                /*  UNITF_ACTIVE = 1
                    UNITF_INTASK = 2 */
        UBYTE   unit_pad;
        UWORD   unit_OpenCnt;
    };

When the device driver is opened, it uses the unit number to select the
appropriate unit structure, and stores the pointer to this structure in
the I/O request.  Later, it can queue up pending I/O requests on the
message port for processing by the unit task.


2.5. THE BEGINIO FUNCTION

All I/O requests enter the device driver through the BeginIO() function
in its jump vector.  The device driver is entered in the context of the
requesting task, with A6 pointing to the device node and A1 pointing to
the I/O request structure.

Normally, the device driver will now use PutMsg() to enqueue the I/O
request on a message port (in a Unit structure) for processing by an
internal task.  Then it can return from the BeginIO() function.  When
the exec checks to see if the I/O request is completed yet, it checks
its type field, and if it is NT_MESSAGE (as results from the PutMsg()
call) it knows that it is still in progress.  Eventually, the internal
task receives the I/O request, operates on it, and does a ReplyMsg().
This returns the I/O request to the caller's reply port, and also sets
its type to NT_REPLYMSG, signaling that it is finished.

It is clear that the device driver does not have to follow this procedure
exactly.  Short commands (such as checking if a disk is ready) can just
be done immediately, in the caller's context.  The I/O request must simply
be returned with ReplyMsg() at the end, and its type field must be
something other than NT_REPLYMSG if the BeginIO() function returns with
the I/O request not completed yet.

A special case of I/O processing is signaled by the IOF_QUICK flag.  When
it is set, it means that the requester has used the DoIO() function, and
thus will be doing nothing until the I/O is complete.  In this case, the
device driver can run the whole I/O operation in the caller's context and
return immediately.  Message passing and task switch overhead is eliminated.
When the BeginIO() function returns with the IOF_QUICK bit still set, it
means that the I/O operation is complete.

If the device driver sees the IOF_QUICK flag set but cannot perform the
I/O processing inline, it can simply clear the flag and return the I/O
request with ReplyMsg() as usual.

The BeginIO() function operates on the command and parameters in the I/O
request, and sets the "io_Error" field to indicate the result.  The exec
I/O functions return the value of this field to the caller; BeginIO() itself
does not return a value.  "io_Error" set to zero means that no error has
occurred.


2.6. THE ABORTIO FUNCTION

Some device driver operations, such as waiting for a timeout or input on
a serial port, may need to be aborted before they complete.  The AbortIO()
function is provided for this.  The device driver is entered through its
AbortIO() entry point with the address of the I/O request to be aborted
in A1, and the device node pointer in A6.  If the device driver determines
that the I/O request is indeed in progress and can successfully abort it,
it returns zero, otherwise it returns a non-zero error code.

A successfully aborted I/O request is returned by the normal method, i.e.
ReplyMsg().  The "io_Error" field should indicate that it did not complete
normally.


2.7. EXEC I/O FUNCTIONS

The following primitives are provided for communicating with device
drivers.  It is assumed that the driver has been opened with OpenDevice()
and an initialized I/O request exists.

    SendIO(iORequest)
           A1

This function calls the BeginIO() entry point in the device driver with
IOF_QUICK clear.  This means that the device driver should return the I/O
request with ReplyMsg().

    error = DoIO(iORequest)
    D0           A1

This function calls the BeginIO() entry point in the device driver with
IOF_QUICK set.  If the device driver leaves IOF_QUICK set, it returns to
the caller immediately.  The return value is the extended value of the
"io_Error" field in the I/O request.  If the IOF_QUICK bit is cleared,
it falls through to WaitIO().

    error = WaitIO(iORequest)
    D0             A1

This function waits for an I/O request to complete.  If the I/O request
has the IOF_QUICK flag set, it cannot possibly be in progress, so it
returns immediately.  Otherwise, the I/O request will be returned with
ReplyMsg(), and the function proceeds as follows:

    Get the signal number from the I/O request's reply port
    Disable()
    WHILE iORequest->io_Message.mn_Node.ln_Type != NT_REPLYMSG DO
        Wait() for the reply port signal
    ENDWHILE
    Unlink the I/O request from the reply port's message queue
    Enable()

Finally, it returns "io_Error" from the I/O request, extended to a
longword, as usual.

    result = CheckIO(iORequest)
    D0               A1

This function checks if the indicated I/O request is complete.  It is
considered complete if its IOF_QUICK bit is set, or if its type is
NT_REPLYMSG.  In this case, the function returns the address of the I/O
request.  If the I/O request is not complete, it returns zero.  This
function does not dequeue the I/O request from the reply port.

    error = AbortIO(iORequest)
    D0              A1

This function calls the AbortIO() entry point in the device driver, as
discussed earlier.


2.8. CALLING BEGINIO DIRECTLY

There is one operation which DoIO() and SendIO() cannot handle, and that
is sending an I/O request with the IOF_QUICK flag set, but not waiting
for it to complete.  That is "run this as quickly as possible but if it's
going to take a while, don't wait for it".  For this operation, the
user must set IOF_QUICK manually, then call the device driver directly
through its BeginIO() entry point.  The following C library function
will do the latter:

    void BeginIO( ioRequest )
    struct IOStdReq *ioRequest;

Since WaitIO() and CheckIO() know about the IOF_QUICK flag, I/O requests
submitted this way can be processed by WaitIO() and CheckIO() as usual.


2.8. SYNCHRONOUS I/O

Synchronous I/O is done when the caller does not wish to continue until
the I/O operation is complete.  In this case, the caller just sets up
the I/O request and does a DoIO() on it.  When DoIO() returns, the I/O
is complete.


2.9. ASYNCHRONOUS I/O

Asynchronous I/O is done when the caller wishes to submit an I/O request
and then do other things while it completes.  Such I/O is submitted by
SendIO() or BeginIO() as discussed earlier.  There are a variety of ways
to wait for completion of an asynchronous I/O request; several are discussed
below.


2.9.1. WAITING FOR A SPECIFIC I/O REQUEST

Waiting for just one specific I/O request is done with the WaitIO()
function.  The function will not return until that particular I/O request
is completed.


2.9.2. WAITING ON A SPECIFIC REPLY PORT

If a number of I/O requests are outstanding and all will arrive at the
same reply port, then the following can be used:

    WHILE I/O requests are outstanding DO
        WaitPort() on the port
        GetMsg() on the port
    ENDWHILE


2.9.3. GENERAL CASE

Often, a program will be waiting for one of a number of events to occur,
and the only thing these events have in common is that they will set a
signal.  Then the program must get the signal bits from all the I/O reply
ports, merge them with the other signal bits to wait for, and do a Wait()
on the result.  When the Wait() returns with a signal bit set which
corresponds to an I/O reply port, then a GetMsg() can be attempted on
that port to see if an I/O operation has completed.  In this manner,
I/O completions can be handled together with other events in any order.


3. GENERIC COMMAND AND ERROR NUMBERS
------------------------------------

This section lists the command and error numbers which are predefined
in the Amiga system for all types of device drivers.


3.1. COMMANDS

The command number is stored in the "io_Command" field of the I/O request.
The generic command numbers, as found in the include file "exec/io.h", are as
follows:

    #define CMD_INVALID 0
    #define CMD_RESET   1
    #define CMD_READ    2
    #define CMD_WRITE   3
    #define CMD_UPDATE  4
    #define CMD_CLEAR   5
    #define CMD_STOP    6
    #define CMD_START   7
    #define CMD_FLUSH   8

    #define CMD_NONSTD  9

It is seen that command number zero is invalid, and command numbers greater
than 9 are custom defined.  The remaining commands are as follows:


3.1.1. CMD_RESET

This resets the device to a known initial state.  Pending I/O requests not
processed at the time of this command should be returned with an error.


3.1.2. CMD_READ

This requests that "io_Length" items of data be read from location
"io_Offset" on the unit, and stored at "io_Data" in the caller's memory.
The actual amount of data transferred is returned in "io_Actual".  The
specifics depend on the device type.


3.1.3. CMD_WRITE

This requests that data be transferred from the caller's memory
to the I/O unit.  The arguments are the same as for CMD_READ.


3.1.4. CMD_UPDATE

This requests that all buffered, but unwritten data be forced out
to the I/O unit.  It might write out the track buffer in a disk device,
for example.


3.1.5. CMD_CLEAR

This requests that all data buffered by the device for the given unit
be invalidated.  Thus, for example, it would throw away data waiting in a
serial input buffer.


3.1.6. CMD_STOP

This requests that the unit stop processing commands.  I/O requests not
processed at the time of the CMD_STOP will wait until a CMD_START or
CMD_RESET is received or they are aborted.


3.1.7. CMD_START

This requests that the unit clear a CMD_STOP condition and resume processing
commands.  Only one CMD_START is required, regardless of how many CMD_STOPs
have been received.


3.1.8. CMD_FLUSH

This requests that the unit flush all pending commands.  All I/O requests
queued but not yet processed should be sent back with an error.


3.2. ERROR NUMBERS

The include file "exec/errors.h" lists the following standard error numbers.

    #define IOERR_OPENFAIL  -1  /* device/unit failed to open */
    #define IOERR_ABORTED   -2  /* request aborted */
    #define IOERR_NOCMD     -3  /* command not supported */
    #define IOERR_BADLENGTH -4  /* not a valid length */


4. DISK DEVICE DRIVERS
----------------------

Real device drivers usually support more commands than those listed in
section 3.  This section describes the command set used by disk device
drivers.  This information is needed to write a disk driver compatible
with the existing file systems and disk repair programs.

All "normal" disk commands use an I/O request of the structure given earlier.
The "extended" trackdisk commands, which use a larger I/O request, are not
discussed.


4.1. COMMANDS

The include file "devices/trackdisk.h" lists the following command numbers:

    #define TD_MOTOR        (CMD_NONSTD+0)      /*  9 */
    #define TD_SEEK         (CMD_NONSTD+1)      /* 10 */
    #define TD_FORMAT       (CMD_NONSTD+2)      /* 11 */
    #define TD_REMOVE       (CMD_NONSTD+3)      /* 12 */
    #define TD_CHANGENUM    (CMD_NONSTD+4)      /* 13 */
    #define TD_CHANGESTATE  (CMD_NONSTD+5)      /* 14 */
    #define TD_PROTSTATUS   (CMD_NONSTD+6)      /* 15 */
    #define TD_RAWREAD      (CMD_NONSTD+7)      /* 16 */
    #define TD_RAWWRITE     (CMD_NONSTD+8)      /* 17 */
    #define TD_GETDRIVETYPE (CMD_NONSTD+9)      /* 18 */
    #define TD_GETNUMTRACKS (CMD_NONSTD+10)     /* 19 */
    #define TD_ADDCHANGEINT (CMD_NONSTD+11)     /* 20 */
    #define TD_REMCHANGEINT (CMD_NONSTD+12)     /* 21 */

Some of these commands are specific to removeable media and/or the
trackdisk.device and need not be supported by a hard disk driver.  More
on this later.  The following sections describe the individual commands.


4.1.1. CMD_READ AND CMD_WRITE

These are generic commands, but the details are specific to the type of
device.  For disk devices, the "io_Length" and "io_Offset" fields must
be an exact multiple of the sector size supported by the device.  At
present, this is 512 bytes.  The "io_Actual" field could conceivably
return a different value from "io_Length" if an error stopped the operation
part way through.


4.1.2. TD_MOTOR

This command turns the floppy disk motor on and off.  "io_Length" should
be set to zero to turn it off, or one to turn it on.  "io_Actual" will
return the previous state of the motor.  The motor will turn on automatically
as required, but never off again; thus, the user must issue a TD_MOTOR
command to turn it off.


4.1.3. TD_SEEK

This command moves the read/write heads to the position indicated in
"io_Offset", which must be an exact multiple of the sector size.  Seeking
is implied in other commands, but this command can be used to "pre-seek"
the device if the needed position is known in advance of the
read/write operation.


4.1.4. TD_FORMAT

This command takes the same arguments and performs the same operation as
CMD_WRITE, with the following two exceptions:

    (a) The area to be written must be a formattable unit, i.e. in the
        trackdisk.device it must be one or more complete tracks.

    (b) The area is written with the data regardless of its previous
        contents; it need not already be formatted.

This describes the observed behaviour of the trackdisk.device.  The autodoc
for "TD_FORMAT" disagrees with this information.

Hard disk drivers typically implement TD_FORMAT as a simple call to
CMD_WRITE.  This allows the hard disk to be "high level formatted" with
the AmigaDOS "format" command.

Some ancient SASI/SCSI disk controller boards have a "format track" command,
so a driver targeted specifically at them could implement TD_FORMAT the
same way as the trackdisk.device does.


4.1.5. TD_PROTSTATUS

If there is a disk in the drive, this command returns "io_Actual" set to
zero if the disk is writeable, nonzero if protected.  If there is no disk,
the I/O request returns with the "io_Error" set to "TDERR_DiskChanged".


4.1.6. TD_RAWREAD AND TD_RAWWRITE

These commands read and write whole tracks of raw MFM data on the
trackdisk.device.  Refer to the autodocs for detailed information.


4.1.7. TD_GETDRIVETYPE

This command is trackdisk.device specific, and returns the type of disk
drive in "io_Actual".  This is one of the following:

    1:  3.5", 80 track drive
    2:  5.25", 40 track drive


4.1.8. TD_GETNUMTRACKS

This command is trackdisk.device specific, and returns the number of tracks
on the disk unit in "io_Actual".


4.1.9. TD_CHANGESTATE

This command checks to see if there is a disk in a removable-media drive.
It returns "io_Actual" set to zero if there is, nonzero if there is not.


4.1.10. OTHER COMMANDS

The remaining commands are not described because I am not sufficiently
familiar with them at this time.  The reader should refer to the appropriate
autodocs.  In some cases, experimentation with the trackdisk.device will
be needed to get all the details.


4.2. ERROR NUMBERS

The following error numbers are listed in the include file
"devices/trackdis.h".

    #define TDERR_NotSpecified   20 /* general catchall */
    #define TDERR_NoSecHdr       21 /* couldn't even find a sector */
    #define TDERR_BadSecPreamble 22 /* sector looked wrong */
    #define TDERR_BadSecID       23 /* ditto */
    #define TDERR_BadHdrSum      24 /* header had incorrect checksum */
    #define TDERR_BadSecSum      25 /* data had incorrect checksum */
    #define TDERR_TooFewSecs     26 /* couldn't find enough sectors */
    #define TDERR_BadSecHdr      27 /* another "sector looked wrong" */
    #define TDERR_WriteProt      28 /* can't write to a protected disk */
    #define TDERR_DiskChanged    29 /* no disk in the drive */
    #define TDERR_SeekError      30 /* couldn't find track 0 */
    #define TDERR_NoMem          31 /* ran out of memory */
    #define TDERR_BadUnitNum     32 /* asked for a unit > NUMUNITS */
    #define TDERR_BadDriveType   33 /* not a drive that trackdisk groks */
    #define TDERR_DriveInUse     34 /* someone else allocated the drive */
    #define TDERR_PostReset      35 /* user hit reset; awaiting doom */


4.3. SCSIDIRECT PROTOCOL

Most SCSI device drivers support an additional command to issue a generic
SCSI command.  This is described in detail in the include file
"devices/scsidisk.h".


4.4. A MINIMAL DISK COMMAND SUBSET

It is clear that to implement a full Amiga disk driver supporting removeable
media is a considerable task, and I don't even pretend to know all that is
required.  But if all you want is a simple hard disk driver, a very small
subset of the commands is sufficient.  To begin with, Expunge() can be
stubbed out to always return zero, indicating a delayed expunge which will
never get done.  AbortIO() can be stubbed out to always return a nonzero
result, indicating that it failed.  Unit structures can be done away with;
you can store anything in the "io_Unit" field of the I/O request.  In my
own device driver I just store the SCSI device number and a few flags there.
You can just return I/O error #20 (general catch-all) for anything that
went wrong, except possibly unimplemented commands.  Quick I/O need not
be done; just always clear IOF_QUICK and forget about it.  Send all I/O
requests to a single internal task for processing.  Finally, the
commands can be implemented as follows:

    CMD_READ, CMD_WRITE:        implement fully

    TD_FORMAT:                  same as CMD_WRITE

    TD_GETDRIVETYPE:            return 3.5" drive

    CMD_RESET, CMD_UPDATE,
    CMD_CLEAR, CMD_STOP,
    CMD_START, CMD_FLUSH,
    TD_MOTOR, TD_SEEK,
    TD_REMOVE, TD_CHANGENUM,
    TD_CHANGESTATE,
    TD_PROTSTATUS,
    TD_ADDCHANGEINT,
    TD_REMCHANGEINT:            clear "io_Actual" and return

    Others:                     reject with IOERR_NOCMD

The resulting driver works perfectly with fast and slow file systems,
and all the disk edit/repair utilities I've tried it with.  If you are
bringing up a hard disk from scratch, you can always get a hack driver
to work, then write a "proper" one with the hard disk running.


5. REFERENCES
-------------

- 1.3 Include files

- 1.2 Autodocs

- Commodore's example disk device driver, from the DevCon 88 disks.
  Be sure you get the one which launches a task, not the older ones which
  launch a process.  That doesn't work in an autoboot context.

- Exec 1.2 disassembly, by myself.  Get it from Fred Fish disk 188.


6. REVISION HISTORY
-------------------

0.10  (90/05/20)  Initial version
0.11  (90/05/21)  Proofread, minor updates, nicer format
0.12  (90/05/21)  Corrected a few typos

fsset@neptune.lerc.nasa.gov (Scott E. Townsend) (06/01/90)

In article <3159@bnr-rsc.UUCP> mwandel@bnr-rsc.UUCP (Markus Wandel) writes:
>Below is a file which I wrote recently about Amiga device drivers.  I think
>it might be useful to some people, and lacking other convenient distribution
>mechansims, I'll just post it here.  This doesn't have to be the final version
>of the document; feedback would be appreciated and if someone wants to write
>an additional section covering material I don't, I'd appreciate that too.
>
>Is there *any* real documentation from Commodore on this subject?
>
>Markus Wandel
>uunet!bnrgate!bnr-rsc!mwandel
>

Just a note that might be common knowledge to the net, but wasn't to me.

It appears that ramdev.asm in the 1.3 RKM Includes and Autodocs manual has
a bug.  The scenario is to build with the EXERCISE_TASK flag turned off.
This causes as much activity to occur in the calling task's context as
possible.  What I found was that for slow devices, it is possible for
the unit task to get hung in such a way that all further device activity
is frozen.  (I don't have any DevCon notes, so maybe this is fixed in
the notes referred to in the above article)

What seems to be happening is that the flag the task uses to determine if
the device is 'active' was causing a classic 'deadly-embrace'.  (Sorry, I
don't have my code here, so I don't remember the name of the flag)

The way I fixed my version of the driver was to use two flags, one to say
that the device/task was active, and one to say there are queued requests
to the device.  Then near TermIO (I think) I would check the flags and
potentially wake-up the task.

If anyone's interested, I can get at my code and explain in more detail
(hopefully more understandably also).  I should note that I never had
any problems with a fast device like ramdev.asm, I only seemed to get
into trouble with my (slow) software-driven IOMEGA/SCSI drivers with
multiple tasks beating-up on them.

By the way, I have a question about the SCSI direct definitions in
devices/scsi???.h/i:

The definitions for SCSIF_READ and SCSIF_WRITE seem a bit strange.  Since
the trailing prefix letter is 'F', I'm expecting this to be a mask
(rather than 'B' signifiying a bit number) Having two names seems to
imply some independence, yet the definitions of 0 and 1 implies
mutual exclusion (as long as they're masks)  If they're bit numbers, then
could BOTH be active for the same call?  I don't know of a SCSI
transaction which both reads and writes data. (But then all I have are these
ancient Alpha-10 drives, so maybe current SCSI can read & write)

Currently I'm using flags & SCSIF_WRITE as my read/write indication, 
but I'd like to know if I'm incompatible with the rest of the world.

--
------------------------------------------------------------------------
Scott Townsend               |   Phone: 216-433-8101
NASA Lewis Research Center   |   Mail Stop: 5-11
Cleveland, Ohio  44135       |   Email: fsset@neptune.lerc.nasa.gov
------------------------------------------------------------------------