Aros/Developer/Docs/Devices

from http://wandel.ca/homepage/execdis/devices_doc.txt

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

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, removable-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 removable 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 removable 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