ADC Home > Reference Library > Technical Notes > Legacy Documents > Hardware & Drivers >

Legacy Documentclose button

Important: This document is part of the Legacy section of the ADC Reference Library. This information should not be used for new development.

Current information on this Reference Library topic can be found here:

Driver Education

CONTENTS

This Technical Note describes in detail the operation of the Device Manager and its interaction with device drivers. It provides the background needed for correct operation of third-party device drivers and also presents a method for finding an entry in the Unit Table for a driver.

Many aspects of the Device Manager remain mysterious to even the most studious of Macintosh developers, especially those who might wish to accomplish tasks not directly supported by the current offering of calls. For example, developers might wish to install their own driver at INIT time or wish to manage their own I/O queues for processing. A solid knowledge of the Device Manager helps in these tasks, as well as in simply writing a normal, no-frills device driver.

A general-usage driver, which is best installed at INIT time, provides services to software that might run on a Macintosh at any time. This type of driver is typically implemented by having a file of type INIT, cdev, or RDEV that users move into their System Folder. This file contains 'INIT' and 'DRVR' resources, among others. The 'INIT' resource gets loaded into memory and executed, at which time it installs the driver.

[Aug 01 1990]






Caveat--Low Memory and System Data Structures

Warning: This Note involves the discussion and use of low-memory globals and operating system internal data structures. It is a plain fact that using these puts software at a compatibility risk; therefore, DTS recommends you approach the information in this Note in the following manner:

  1. If you do not have to use a low-memory global or system data structure, then don't.
  2. If you do have to use a low-memory global or system data structure, use it only as described in this Note, or in other, authorized Apple Computer, Inc., technical documentation.
  3. When using a low-memory global or system data structure in an authorized manner, do so in a way that encapsulates and isolates this dependency from the rest of this software. This way, if Apple alters the structure of space-time out from under you, there is one nice, tidy spot that you have to modify.
  4. When Apple provides a system-independent manner by which you can obtain the same information, modify your code to use this method.

An example of points three and four would be using a module that returns a low-memory global instead of always reaching into low-memory directly. For example, when this Note references the low-memory global UTableBase, the software that needs the value stored there should call a routine like the following:

              FUNCTION GetUTableBase : Ptr;
              CONST
                  UTableBase = $11C;
              TYPE
                  LongPtr = ^LongInt;
              BEGIN
                  GetUTableBase := POINTER(LongPtr(UTableBase)^);
              END;

Then, if Apple were to provide a Gestalt call to get the same value, only this one module would (and should) be changed. If you are blessed enough to be using a high-level or, better yet, an object-oriented language, you can even further isolate the dependency by encapsulating a whole mechanism that relies on low-memory globals and system data structures into a module that can be completely replaced if needed.

It should go without saying that simply because this Note uses some low-memory globals and system data structures, it does not mean Apple has gone soft on software that uses them, especially when they violate the aforementioned guidelines.

Back to top

Driven to Tears

A solid understanding of the Device Manager begins with knowledge of its data structures. Although much of the following is described in Inside Macintosh, Apple has made some changes since its publication.

First, there is the structure of the unit table. It is a non-relocatable block in the system heap that is pointed to by the low-memory global UTableBase ($11C). The unit table is a contiguous table of handles to Device Control Entry (DCE) records. The offset of a handle within the unit table (its entry, or "slot") determines the unit number for the driver, as well as the refnum for that handle, since a unit number relates to a refnum in the following way:

    refnum = ~( unit number )

If the handle at a particular slot is NIL, there is no DCE and thus no driver installed for that refnum. There is another low-memory global, UnitNtryCnt ($1D2), which is the count of entries in the unit table. This low-memory global can be used to know when to stop searching the unit table.

Many of the slots in the unit table are committed to certain devices due to the fact that the corresponding reference numbers are committed to those devices. For example, the refnum -3 is reserved for the .Print driver; therefore, unit number -(-3)-1=2 is reserved, which means that the slot at offset 8 (4 bytes per handle * 2) in the unit table is reserved for the handle to the DCE of the .Print driver. This relationship is true even if the slot is currently NIL, because a lot of the operating system, Toolbox, and applications make heavy assumptions about the refnum of a particular device. There are more detailed lists in Inside Macintosh, but following is a summary list of what unit numbers are committed to what device types:

Unit Number Range Refnum Range Usage

0 through 11 -1 through -12 Serial, disk, AppleTalk, printer, and so on

12 through 31 -13 through -32 Desk accessories

32 through 39 -33 through -40 SCSI devices

40 through 47 -41 through -48 AppleShare and other reserved for Apple

48 through 63, 127 -49 through -64, -128 Slot drivers--open for other drivers as well

The entry for unit numbers 48 and above requires some explanation. First, on the Macintosh 512KE and Macintosh Plus, the unit table does not go past unit number 47; this point is addressed later. On the Macintosh SE, the unit table is large enough to hold up to unit number 63. On the Macintosh II family of machines, the unit table is large enough to hold up to unit number 127; UnitNtryCnt is probably lower, though not lower than 64.

After documenting the device control entry blocks in Inside Macintosh, Volume II, Apple has expanded them to deal with new features like the Slot Manager. The new DCE looks the same as the old DCE, but contains a few additional fields:

    TYPE AuxDCE = PACKED RECORD
                     dCtlDriver:    Ptr;
                     dCtlFlags:     INTEGER;
                     dCtlQHdr:      QHdr;
                     dCtlPosition:  LONGINT;
                     dCtlStorage:   Handle;
                     dCtlRefnum:    INTEGER;
                     dCtlCurTicks:  LONGINT;
                     dCtlWindow:    Ptr;
                     dCtlDelay:     INTEGER;
                     dCtlEMask:     INTEGER;
                     dCtlMenu:      INTEGER;

The following fields are the additions for the new DCE record:

                     dCtlSlot:      Byte;
                     dCtlSlotId:    Byte;
                     dCtlDevBase:   LONGINT;
                     reserved:      LONGINT;
                     dCtlExtDev:    Byte;
                     fillByte:      Byte;
                  END;

The core structure of parameter blocks has remained the same as described in Inside Macintosh. They begin with the standard I/O queue fields:

    TYPE ParamBlockRec = RECORD
                            qLink:         QElemPtr;
                            qType:         INTEGER;
                            ioTrap:        INTEGER;
                            ioCmdAddr:     Ptr;
                            ioCompletion:  ProcPtr;
                            ioResult:      OSErr;
                            ioNamePtr:     StringPtr;
                            ioVRefNum:     INTEGER;
                            ioRefNum:      INTEGER;
                            ...
                         END;

The rest of the parameter block is heavily dependent on the device driver that uses it and what kind of driver call is being made.

The Unit Table, the DCE, and the Parameter Block all work together to provide all the information necessary for the application, Device Manager, and device driver to communicate with one another. For example, the structure of a RAM-based driver that has been opened and called asynchronously a few times might look like that shown in Figure 1.

Basic Device Manager Data Structures

Figure 1. Basic Device Manager Data Structures.

Back to top

Routine Maintenance

A device driver is a block of data composed of a header followed by executable code. The header contains flags, a delay amount, an event mask, a menu ID, an optional name of the driver, and offsets to the routines that are in the executable code. Inside Macintosh describes device drivers as being 'DRVR' resources. Although this is typically the case, it should be noted that this is not necessarily a requirement, as long as certain rules are followed. Details of these rules are discussed later in this Note.

A device driver can implement five routines. The Device Manager calls these routines when certain operating system traps are executed. The traps are called with register A0 pointing to the parameter block. Some of these traps can be called in one of two different modes: immediate and asynchronous. If bit 9 of the trap word is set, the call is immediate, while if bit 10 is set, the call is asynchronous. The device driver should be able to respond to every call by the Device Manager in at least some fashion, even if just to return. The device driver routines, the traps that cause the Device Manager to call the routines, and the various modes in which the calls can be made are as follows:

Routine Trap Modes

Open _OpenImmediate

Prime _Read, _Write Immediate, Asynchronous

Control _Control Immediate, Asynchronous

Control _KillIO Immediate

Status _Status Immediate, Asynchronous

Close _Close Immediate

_Open, _Close, and _KillIO are always immediate because of the way they are handled by the Device Manager. Do not make asynchronous _Open, _Close, or _KillIO calls, and do not specify the IMMED bit for them explicitly. It is extremely rare that a driver will support immediate _Read and _Write calls properly. These calls should be used only when specifically allowed according to the documentation for a driver.

From the driver's point of view, KillIO is handled as a _Control call with a csCode = 1. However, this is a facade produced artificially by the Device Manager. Calling a driver with _Control and csCode = 1 is not the same thing as a KillIO and none of the normal dequeueing action will take place.

Especially important:

Developers should never make a Device Manager call at interrupt time, including VBLs, Time Manager tasks, deferred tasks, and so on, unless the call is asynchronous and the underlying driver is capable of returning to the caller before the I/O completes. Deviations from this practice will result in severe interrupt latency or even system hangs.

Back to top

What the Glue Do

High-level languages often come with libraries that provide an easy-to-use interface ("glue") to the machine-level Device Manager traps. Many of these calls are documented in Inside Macintosh. These routines are further divided into two groups. The first group is composed of "low-level" routines; these routines take an already complete parameter block passed to them, point register A0 at it, and execute the proper trap with the correct mode. The second group is composed of "high-level" routines, which set up a parameter block and required registers with data provided in the call parameters, then execute the Device Manager traps. For example, the OpenDriver call creates a parameter block on the stack, completes the required fields, points register A0 to the block, and executes the _Open trap.

Following is a list of high-level routines and the trap glue they execute:

Routine Trap

OpenDriver _Open

CloseDriver _Close

FSRead _Read

FSWrite _Write

Control _Control

Status _Status

KillIO _KillIO

Back to top

Yes, We're Open

The Open routine of a device driver is called as a result of an _Open trap being executed. What _Open does depends on what machine is running, but is fairly consistent across the machine models, with the exception of NuBus(TM) slot drivers on a Macintosh with a Slot Manager. The following is a pseudo-code description followed by a narrative:

IF call is NOT from _OpenDeskAcc AND filename does NOT begin with "." THEN
    pass control to file system
ELSE [
    IF driver is for slot device THEN [    {explained in narrative below}
        search unit table from 48 through UnitNtryCnt for match
        IF NOT found THEN
            call _GetNamedResource for driver name type 'DRVR'
        IF NOT successful THEN
            call _SGetDriver for driver
        IF NOT successful THEN
            return error
        {found the driver}
        search unit table from 48 through UnitNtryCnt for NIL DCE handle
        IF NOT found THEN [
            bump up UnitNtryCnt by 4
            IF maximum UnitNtryCnt exceeded THEN
                return error ]
        {found a slot}
        install driver ]
    ELSE [
        search unit table from 0 through UnitNtryCnt for match
        IF NOT found THEN
            call _GetNamedResource for driver name type 'DRVR'
        IF NOT successful THEN
            return error
        {found driver}
        get resource ID of 'DRVR' resource
        IF unit number not already in use THEN
            install driver ]    {otherwise, leave old driver there}
    IF driver NOT already open THEN [
        move Drvr fields into corresponding DCE fields
        IF driver is RAM-based THEN
            set RAM-based flag in dCtlFlags field of DCE
        IF driver is for slot device THEN
            call _SFindDevBase and put result into DCE ]
    IF driver needsLock THEN
        lock driver and DCE
    IF driver NOT already open OR called from _OpenDeskAcc THEN
        call Open routine of driver
    IF any error resulted THEN [
        clear ioRefnum field
        clear driverOpen bit of dCtlFlags
        unlock driver and DCE ]
    ELSE
        unlock driver and DCE if allowed
    set ioResult field to result ]

First, _Open checks to see if the call is from _OpenDeskAcc. This check is made by looking for a special value in the ioFileType field of the parameter block, which _OpenDeskAcc sets. The filename should also begin with a null byte ($00), but this is not checked. If the call is not from _OpenDeskAcc, _Open checks to see if the filename in the parameter block begins with a period (.). If the filename does not begin with a period, control is passed to the file system. Otherwise, if the machine is a Macintosh with the Slot Manager, then a check is made to see if the driver is for a slot device. If bit 9 of the _Open trap word is set and bit 0 of the ioFlags word is set or the ioSlot field of the parameter block is not 0, then the driver is assumed to be for a slot device.

If the driver is for a slot device, the unit table is searched, starting at unit number 48 and ending at UnitNtryCnt. If the dCtlSlot, dCtlSlotID, and dCtlExtDev fields of none of the DCEs of the unit numbers match the fields of the parameter block, then _GetNamedResource is called, using the name of the driver and type 'DRVR'. If that fails, then _SGetDriver is called to load the driver from the card's ROM. If that attempt fails, an error is reported. In the case where one of the attempts succeeds, the unit table is searched after loading the new driver, starting at unit number 48 and ending at UnitNtryCnt, for an unused (NIL) slot. If none are found, the value of UnitNtryCnt is incremented by four. If the value exceeds 128, then an error is reported. Otherwise driver uses the newly created slot.

If the driver is not for a slot device or has been determined to be a desk accessory, the unit table is searched, starting at unit number 0 and ending at UnitNtryCnt. If none of the names for the installed drivers in the table matches the filename of the parameter block, then _GetNamedResource is called, using the name of the driver and type 'DRVR'. If that attempt fails, an error is reported. If the attempt succeeds, the ID of the resource is assumed to be the unit number of the driver and is mapped into the equivalent refnum. If the slot for that refnum is already occupied, then the driver that is already there remains there.

Once the device driver is installed, or it has been determined that a driver already occupies the slot in the unit table, the driver is checked to see if it has already been opened. If it has not, the driver is checked to see if it is RAM-based or and the dCtlFlags field of the driver's DCE is set accordingly, along with being combined with the rest of the DrvrFlags field of the driver header. The DrvrDelay, DrvrEMask, and DrvrMenu fields from the driver header are also moved into the corresponding fields of the DCE. If the driver is for a slot device, _SFindDevBase is called for the slot and ID of the driver's device and the result is put into the DCE.

Once the DCE fields have been completed, or it is determined that the driver is already open, the driver and DCE are locked if needed. The permissions are then checked, returning an error if incorrect, and the Open routine of the driver is called if the driver is not already open or if the call was from _OpenDeskAcc. If the driver returns any error, then the ioResult field of the parameter block and the driverOpen bit of the dCtlFlags field are cleared, and the driver and its DCE are unlocked. If the driver returns no errors, then it and the DCE are unlocked, if allowed. In either case, the result from the driver's Open routine is put in the ioResult field of the parameter block.

Back to top

That's Great, But What Are You Driving At?

So, of what practical value is all this? If you are trying to provide a nonslot driver that can be installed at INIT time and used later by other software, the best method for finding a unit number in the unit table is the way the _Open trap on the Macintosh II family finds a unit number in the unit table for slot device drivers. Unfortunately, you may want to run on other Macintosh models, and it would be a bit kludgy to fake being a slot device driver, so you cannot just call _Open and pretend to be a slot device driver. Additionally, it is likely that you may wish that the driver's Open routine not be called until it is actually opened with the _Open trap by software that really wants to use it; therefore, your INIT code must mimic the pertinent code of the _Open trap.

The first action should be to call _GetNamedResource with the name of the driver and its resource type (typically 'DRVR', although it is not required, since you are loading it). The resource that contains your driver must have the system heap bit set in its resource attributes, so it is loaded into the system heap where it can continue to exist, long after the INIT has gone away. Note that if for some incredible reason, your driver is greater than 16K, you might want to include a 'sysz' resource to increase the available space in the system heap.

Next, you must call _DetachResource with the handle to your resource, so it is not removed when your INIT file is closed. Now you are ready to find a slot in the unit table for your driver. First check if unit numbers 48 and higher are even available by checking UnitNtryCnt.

If UnitNtryCnt is 48, you have a bit of a problem in that there are no empty slots available in the unit table. You can rectify this, however, by resizing the unit table. DTS recommends you accomplish that task by creating a new unit table that is larger than the old one.

To resize the unit table, first create a new, nonrelocatable block in the system heap that is the new size you want, and clear it to zeros. The following assembly-language code fragment gives an example:

    MOVE.W     D1,D0                    ;D1 = requested # slots
    MULU.W     #4,D0                    ;turn it into size
    _NewPtr,SYS,CLEAR                   ;create clear block in system heap
    BNE        Error                    ;check for errors!

Next, you must copy the contents of the old unit table into the new unit table, point UTableBase to the new unit table, and adjust the value of UnitNtryCnt. While doing all of that, it would be most inconvenient if an interrupt occurred; therefore, you must turn off interrupts during the process. The following assembly-language code fragment, which would follow the previous code, gives an example:

    MOVE       SR,-(SP)                 ;save old interrupt status
    OR           #0700,SR               ;disable all maskable interrupts
    MOVEA.L    A0,A1                    ;A0 (address new unit table) -> A1
    MOVEA.L    UTableBase,A0            ;old unit table -> A0
    MOVE.W     UnitNtryCnt,D0           ;number of entries -> D0
    MULU.W     #4,D0                    ;size of old table -> D0
    _BlockMove                          ;copy old table to new table
    _DisposPtr                          ;get rid of old table
    MOVE.L     A1,UTableBase            ;make us new unit table
    MOVE.W     D1,UnitNtryCnt           ;update number of entries
    MOVE       (SP)+,SR                 ;restore old interrupt status

DTS suggests that if UnitNtryCnt was originally 48, you increase it to 64, adjusting the table size as shown.

At this point, with the unit table resized or already at a size to hold more than 48 unit numbers, it may be searched for an empty slot, starting at unit number 48 and ending at UnitNtryCnt. If, in the case where the unit table already held more than 48 unit numbers, no empty slot was found, then the unit table might be able to be expanded as was described previously. This time, however, the process can be a bit more complicated.

It could very well be that the unit table itself is already larger than what UnitNtryCnt would indicate, either because the machine is a Macintosh II-class machine or somebody else has changed things ahead of your INIT. The best action to take would be to call _GetPtrSize on the unit table, divide the result by four, rounding down, and compare that number to UnitNtryCnt. If UnitNtryCnt is lower than that result, you can increment UnitNtryCnt by any amount that keeps it less than or equal to the maximum allowable size that the unit table can handle. Four is a good number, because it reduces the need for someone else to do the same check later on without also making the search for a given driver. Once UnitNtryCnt has been incremented, you know you have an empty slot waiting.

If UnitNtryCnt is already equal to the size of the unit table divided by four, you should expand the unit table as described previously, choosing a size around 16 or 32 bytes greater than the old size. Remember always to check the result of the _NewPtr call; it would be catastrophic to copy the old unit table into the low-memory global area.

Once you have found a slot for the driver in the unit table, call _DriverInstall with the corresponding refnum and pointer to the driver. This call creates a DCE for the driver and sets up the correct refnum in the DCE. Next, move the handle to the driver into the dCtlDriver field of the DCE, then move the DrvrFlags, DrvrDelay, DrvrEMask, and DrvrMenu fields of the driver header into the dCtlFlags, dCtlDelay, dCtlEMask, and dCtlMenu fields of the DCE. Finally, set the dRamBased bit in the dCtlFlags field of the DCE. That's all there is to it.

Back to top

Immediate Asynchronicity

The _Read, _Write, _Status, and _Control traps differ from _Open, _Close, and _KillIO in that a request can be "queued," (that is, inserted into a waiting list of requests for that device driver). This queue allows requests to be saved for processing later if the driver is busy handling another request. As the device driver finishes servicing each request, the next request in line is passed to the driver until no more requests remain. Calls made to _Open, _Close, and _KillIO, however, must be handled immediately, so they are never queued. This behavior is not a problem with Open requests, since there are usually no requests already queued. What happens with Close requests is described later in this Note.

As noted earlier, these traps can be optionally executed immediately, asynchronously, or both. Normally, when a trap is executed, it means that control should not return to the caller until the request has been completed. That is, the requested task completely finishes doing whatever was asked and needs to do no further action. For example, assume a _Read trap is called for a serial driver. If called normally, control would not return to the caller until a character was received.

An asynchronously executed trap should return to the caller as soon as possible, even if the request cannot be completed before returning. In the previous example, if _Read were executed asynchronously, it would return control as soon as the request was noted rather than waiting until a character was received. When a character finally was received, the ioResult of the parameter block would reflect that fact, and any ioCompletion routine would be executed.

When a trap is executed immediately, it means that the request is not queued, but rather sent immediately to the driver, whether or not it is busy handling another request. Immediate requests are typically not I/O-related in nature. If the example call were to have been executed immediately, it is not clear what the proper response of the driver should be; should it wait until a character is available, thereby accomplishing the task but violating the concept of "immediateness," or if no character is available, should it just return immediately without ever completing the task? A better use for immediate calls is for checking the status of a driver, using the _Status trap. Note that the _KillIO trap, used for aborting all processes the driver might be involved in, doesn't need the immediate bit set. _KillIO is always executed immediately.

Note that Inside Macintoshstates that it is the caller's responsibility to know if a driver can handle a particular call being made immediately.

Back to top

You Can Feel It When You Drive

When _Read or _Write is executed, the Device Manager checks the refnum. If it is not negative, it is assumed that it is a file reference number and control passes to the file system. When _Status is executed, the Device Manager first checks if the csCode is one (1). If it is, then the DCE handle is calculated and returned in the csParam field and control is returned to the caller.

Beyond this special handling, the Device Manager processes _Read, _Write, _Status and _Control traps in much the same way. First, however, it checks if the call was a _Read or _Write. If it was either of these, then the ioActCount field of the parameter block is cleared to indicate that no bytes have been read or written yet.

Next, the ioResult field of the parameter block is set to one (1) to indicate the call is "in progress." The ioTrap field is filled with the trap word that was executed and the qType is set to ioQType, 2. The Device Manager checks to see if the driver is open and if it is capable of handling the kind of call made. If either of these tests fail, it returns an error. Otherwise, it checks the "immediate" bit (bit 9) of the trap word, and if set, it goes straight to the code that calls the appropriate driver routine. If the call was not immediate, the Device Manager checks the "asynchronous" bit (bit 10) of the trap word, and if set, it passes control via a BRA to the code that queues the request and calls the appropriate driver routine. If this bit is clear, the ioCompletion field of the parameter block is cleared and the same code is called via a BSR. On returning from that code, the Device Manager executes a loop (the infamous _SyncWait) that tests the ioResult field and exits when it is less than or equal to 0.

To further clarify the difference between asynchronous and synchronous calls: by doing a BSR call in the synchronous case, the Device Manager leaves its return address on the stack, thus regaining control after the driver routine it called returns. This allows the Device Manager to keep control, waiting until the call has completed (the ioResult field becomes nonpositive), before returning control to the code that executed the _Read, _Write, _Status, or _Control trap. In the asynchronous case, no return address is left on the stack; therefore, control returns to the code that executed the trap when the driver routine returns. It is very important to recognize that the Device Manager is doing the work in handling the difference between asynchronous and synchronous calls. For almost every conceivable case, the driver routine does not have to worry about that difference.

The Device Manager gives control to the driver routine during an asynchronous or synchronous trap (not an immediate trap) with the following process. It saves the interrupt level, disables interrupts, and adds the request to the head of the driver's queue. Next, it checks the drvrActive bit of the driver's dCtlFlags field of the device control entry. If this bit is set, the driver is busy handling another request. In this case, the Device Manager restores the interrupt level, exits the code, and returns control to either the code that waits for the ioResult field to become less than or equal to 0 (the synchronous case) or the code that executed the trap (the asynchronous case). If the drvrActive bit is not set, the Device Manager sets it to indicate the driver is busy and the interrupt level is restored.

This is now the point at which an immediate call would have entered as well as where the code continues for synchronous and asynchronous calls. Notice how none of the processing described in the previous paragraph was done in the case of an immediate call; the request was not added to the driver queue and no check was made for whether the driver was busy.

At this point, if the trap is _Read or _Write, the Device Manager checks the ioByteCount field; if it is 0, it is assumed that the call is complete and control is passed to IODone, which is described later. In addition, it adjusts the dCtlPosition field to reflect the ioPosMode and ioPosOffset values in the parameter block.

At this point, the Device Manager checks to make sure the driver is loaded by doing a _LoadResource if it finds the driver has been purged. It then locks the driver and calls the correct routine within the driver by using the offsets given in the driver header.

Back to top

Sorry, We're Closed

When the _Close trap is called, the first thing the Device Manager does is check the refnum. If it is not negative, it passes control to the file system. Otherwise, it searches the unit table and checks if the driver is open and exists if it is not. If the driver is open, the Device Manager waits in a loop until the drvrActive bit of the dCtlFlags field is clear, indicating the driver is no longer busy. Once the driver is not busy, the Device Manager calls the driver's Close routine. If no error is returned, the dOpened flag is cleared. If the driver is for a slot device, then the refnum in the Slot Resource Table is cleared with a call to _SUpdateSRT.

Note that the device driver is not removed from the unit table, nor is the driver or its device control entry disposed. To remove a driver and dispose of its device control entry, you must call _DrvrRemove. The driver itself is usually removed by getting purged (it must be purgeable).

Back to top

Get Outta the Way, Ya Sunday Driver

The driver's Open, Prime, Status, Control, and Close routines are called under different circumstances and need to respond correctly to these situations.

The routines can be broken into two groups:

Open and Close These routines are called only one way and must be completed before returning.

Prime, Status, and Control These routines can be called in different ways and might be able to defer completion of a request.

Handling Open and Close is simple; you must complete all the processing that is to be done before returning, and you do so by simply returning with a result code in register D0.

In the case of Prime, Status, and Control, things get a bit more complicated. If called with the immediate bit of the trap word set, the routine must complete the request and simply return with register D0 containing the result code. If not called with this bit of the trap word set, the routine should, if possible, complete the request and return via a JMP to JIODone. If the request cannot be completed immediately, the routine should simply return with register D0 set to noErr. Since the request cannot be completed immediately, it is implied that some mechanism is used to indicate deferred completion of the request. This might be through an interrupt being generated that itself signals the completion of the request, or it might be an interrupt that allows a periodic function to poll something that would indicate the completion of the request. Whatever the case, once the request has been completed, the code responsible for completing it should perform a JMP to JIODone with register A1 pointing to the device control entry for the driver and register D0 containing the result code.

Back to top

What the Heck Does JIODone Do, Anyway?

JIODone is nothing more than a utility provided by the Device Manager for use by device drivers when they wish to indicate the completion of a queued request. Since it is only used for queued requests, it is inappropriate to use JIODone in completing Open, Close and "immediate" Prime, Status, and Control requests.

What JIODone does is first look at the queue header of the device control entry. It assumes that the queue header points to the request that is being completed. If it is NIL, it exits immediately after unlocking the driver, if the driver can be unlocked. Otherwise, the interrupt level is saved, interrupts are disabled, and the drvrActive flag is cleared, to indicate the driver is no longer busy. Then the driver is unlocked, if allowed, the request is removed from the driver's queue, and the interrupt level is restored. Next, register D0 (the result code) is stuffed into the ioResult field of the request, then if a completion routine was specified (ioCompletion is non-NIL), it gets called. At this point, interrupts are disabled once again, and if any more requests are pending for the driver, the driver is called again at the point where the Device Manager checks to see if the driver is busy. If no more requests are pending, JIODone restores the interrupt level again and returns.

If you call JIODone when there is not an appropriate I/O request pending in the queue, very strange and mysterious things occur. When debugging difficult device driver bugs, be absolutely certain that JIODone is not being called inappropriately.

Back to top

Driving the Point Home

In summary, the following concepts are important to recognize:

1. Since the Open routine of a driver is only called once from the _Open trap, and when it is called, the parameter block is not attached to a queue, the Open routine of the driver should only perform actions such as validating opening the driver and doing data initialization. Do not attempt to use the Open routine in the capacity of something like a _Control call.

2. Likewise, since the Close routine of a driver is called only once by the _Close trap, the Close routine should only perform actions that undo the actions of the Open routine, such as deallocation of data structures, and so on.

3. _Read, _Write, _Status and _Control all act very similar with just a few minor differences. During _Read and _Write calls, the ioActCount field automatically gets cleared and the dCtlPosition field gets updated in accordance with ioPosOffset and ioPosMode. A _Status call with csCode = 1 automatically results in the Device Manager returning the device control entry handle. Other than this difference and the fact that the _Read and _Write traps call the driver's Prime routine, the _Status trap calls the Status routine, and the _Control trap calls the Control routine, these traps behave the same.

4. Because the Device Manager takes care of the different processing requirements of asynchronous and synchronous calls, the Prime, Status, and Control routines of a driver can ignore the difference and handle both kinds of calls in the same way. That is, when the call is completed, jump through JIODone. If the call cannot be completed immediately, just return.

5. Because immediate calls to the Prime, Status, and Control routines of a driver do not have the parameter block added to the head of the queue, you should not exit to JIODone when the call is completed, but rather just return.

Back to top

Crucial References for Writing a Macintosh SCSI Disk Driver

The following is a partial list of specific references that we've found handy for writing SCSI related drivers. Some of the information enclosed is SCSI specific. Some relates to device drivers on the Macintosh. Some relates to other system level programming usually found necessary when writing system level software. Thanks go to Craig Prouse, formerly of DTS, who compiled the original list on which this list is based.

SCSI Development Package

This provides a package of written documentation from DTS including full sample code for an existing driver. Version 1.0 was in assembly language and supported only the old (now unsupported) partition map format. Version 2.0 is rewritten mostly in C, and is available in the snippets folder on the Developer CD Series.

Standards

These official standards, while difficult reading, are indispensable for determining proper operation of low-level SCSI Manager code. Don't go any further without them.

SCSI-1: ANSI X3.131-1986

CCS: X3T9.2/85-52, 1986 (Revision 4.B)

SCSI-2: ANSW X3.131-1992 (Revision 10h)

SCSI-3: ANSI X3.131-199x

Available from:

SCSI Bulletin Board System: (719) 574-0424 or (316) 636-8700
anonymous ftp from rex.cs.tulane.edu

SCSI-1 and CCS:

American National Standards Institute

1430 Broadway
New York, NY 10018
SCSI-2 and most other standards:
Global Engineering Documents
2805 McGraw
Irvine, CA 92714
(800) 854-7179 or (714) 261-1455

Inside Macintosh, Volume II

Chapter 6: The Device Manager

The developer should be thoroughly familiar with all aspects of this chapter and should begin by writing a simple device driver shell that can be installed by a system extension. Once this is accomplished the desired specific features may be implemented.

Chapter 11: The Vertical Retrace Manager

Drivers supporting removable media as a rule use a VBL task to occasionally poll the device to check for a disk insertion. If a disk insertion is sensed, then the driver calls PostEvent to notify the Finder or Standard File. There is more, and better information about VBL tasks as well. The best example of how to write a VBL is in MultiFinder Technote #180. There is supplemental information on VBLs is in the Time Manager chapter of Inside Macintosh Volume VI, and there are virtual memory considerations for VBLs in Technote #285.

Inside Macintosh, Volume IV

Chapter 19: The File Manager

Interestingly enough, one does not need a great deal of familiarity with HFS in order to write a disk driver. Nevertheless, it can't hurt to be familiar with this chapter. There is one poorly documented fact that bears mentioning here: HFS always calls the disk driver with a drive number in the vRefNum field of the parameter block. It does not pass an actual vRefNum to the driver.

Chapter 20: The Device Manager

With the Mac Plus and later, the unit table expanded to accommodate SCSI drivers. There are reserved slots in the unit table for SCSI devices and these unit numbers have a simple correspondence to their reserved SCSI IDs. This is documented only very briefly. Blink and you'll miss it. The driver for SCSI ID n must be installed at unit number (32+n). Do not install it anywhere else or other drivers may not recognize your existence and this can lead to conflicts.

Chapter 31: SCSI Manager

Most of the basic information here is still valid, but there are a couple of caveats. First, the SCSIStat call returns hardware information about the 5380 SCSI chip. Newer Macintoshes may not use the 5380 and SCSIStat may therefore not return the type of information that's documented here. See the "Fear No SCSI" tech note for more details. Also, the Device Partition Map documented on page 292 is no longer supported. Refer to Inside Macintosh Volume V for later information. The old format may be supported optionally but is not required. The new partition map format is required.

Inside Macintosh, Volume V

Chapter 31: SCSI Manager

Inside Macintosh Volume V defines the new partition map structure that is required of all new drivers. It also gives good documentation on the polled vs. the blind SCSI transfer modes.

Inside Macintosh, Volume VI

Chapter 23: Time Manager

This chapter provides just a bit more documentation on VBLs and perhaps offers a few alternatives.

Chapter 28: Memory Management

This is the chapter that finally explains A5 and virtual memory. It is extremely important for SCSI driver developers to understand at least how virtual memory works, and for removable drives, also how to work with A5.

Guide to the Macintosh Family Hardware

Chapter 9: The SCSI Manager

This chapter gives the definitive description of polled vs. blind transfer modes for those who are curious and really want to understand what's going on in hardware and why blink mode is so much faster.

Macintosh Technical Notes

#36

Drive Queue Elements

This Technote contains an explanation of the drive queue and example code for how to add a drive to the queue. For drives containing multiple partitions, it's mostly a matter of searching beyond the first valid HFS partition in the partition map and adding more than one drive to the drive queue. Where it gets difficult is when a single driver then has to provide control for all of those mounted volumes.

#71

Finding Drivers in the Unit Table

The key to avoiding conflicts between different drivers installed in the same system is giving drivers the ability to sense the presence of other drivers when installing. For example, if a driver is stored on a removable cartridge and loads at system boot time, then the driver in the Extensions folder would not be necessary. That driver should be able to see that there is already a driver installed in its unit table slot and it should not install over the existing driver. Other similar situations are possible. Technote #71 gives some ideas about how drivers can become aware of other drivers by searching the unit table. Use this information in conjunction with the information in Chapter 20 of Inside Macintosh Volume IV.

#108

_AddDrive, _DrvrInstall, and _DrvrRemove

The most reliable way to install a driver, particularly a SCSI driver that is not stored as a resource and the unit number for which must be determined at runtime, is to do it manually. Rather than depending on OpenDriver, load the resource into the system heap explicitly. Call _DrvrInstall to allocate its Device Control Entry and fill out the DCE by hand, setting up all the pointers yourself. It's not as difficult as it sounds, it's reliable, and DTS provides full sample code showing how to do it. See the SCSI Development Package.

#180

MultiFinder Miscellanea
Somehow, the best example of how to write a VBL is hidden in a MultiFinder Tech Note.

#187

Don't Look at ioPosOffset

This is a very short and concise Technote that helps explain what to do in one very small but very confusing part of your device driver, where read and write calls are converted into logical block addresses for SCSI. Don't miss this one, and see the example code as well.

#285

Coping With VM and Memory Mappings

Because virtual memory depends on the SCSI bus to perform paging, SCSI driver writers must understand how virtual memory and the SCSI manager interact. Virtual memory also affects how VBLs are run, which may affect everything from checking for disk insertions, to displaying progress indicators during disk formatting.

M.HD.Fear No SCSI

This excellent tech note by Colleen Delgadillo includes answers to frequently asked SCSI questions; a sample of how to call the SCSI manager; information about differences between the Quadra class machines and their new SCSI chip; and other valuable new information about the SCSI manager. As Colleen says, "the target controls the bus."

Back to top

References

Inside Macintosh, Volumes II, IV, and V, The Device Manager

Inside Macintosh, Volume I, The Desk Manager

Technical Note #71, Finding Drivers in the Unit Table

Technical Note #108, _AddDrive, _DrvrInstall, and _DrvrRemove

NuBus is a trademark of Texas Instruments.

Back to top

Downloadables

Acrobat gif

Acrobat version of this Note (164K)

Download


Back to top


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.