Bryan Waters is a software engineer for Maynard Electronics and can be reached at 460 E Semoran Blvd., Casselberry, FL 32707; phone: 407-263-3574.
Programming on the Macintosh has always been an exercise in research, usually requiring bits of information from a dozen different sources. Device drivers provide a good example of this process, and in this article I'll share some of the information I've gathered and discovered, and describe the structure of a Macintosh device driver. I'll then present a driver template written in Think C 4.0.
A set of system routines called the "Device Manager" gives access to device drivers on the Macintosh. The calls provided by this Manager define the application's interface to the drivers. The Manager includes functions for opening and closing drivers, synchronous and asynchronous I/O, control and status calls.
Opening and closing drivers are performed by two routines, OpenDriver and CloseDriver, while the I/O routines are implemented through the FSRead and FSWrite calls. The Control routine provides an interface for sending control information to and from the driver, and the Status routine returns status information about the driver and its current state. (These are all high-level routines, meaning that the interfaces have been simplified at the expense of functionality. Each of these routines in turn calls a low-level parameter block-based routine to perform the desired function.)
Drivers are used for many purposes: Accessing block devices such as disk drives, providing a common interface to many different printers, and even for implementing networks. But drivers aren't always used for talking to a hardware device. They can also be used for Inter-Application Communication, and particularly for things such as implementing desk accessories on the Macintosh.
There are four general classes of drivers on the Macintosh: System drivers, desk accessories, slot drivers, and device-independent (general usage) drivers. System drivers are used as network drivers, SCSI drivers, and printer drivers, and most are stored in the Mac's ROM. Desk accessories, on the other hand, are a special case of driver designed to be used as "mini-applications" that can be accessed from any application. Though somewhat limited in size, desk accessories can have their own menus and windows, and receive and handle events in a manner similar to applications. But because they must coexist with applications, desk accessories cannot take the same liberties with the system as do some applications. NuBus slot drivers are usually loaded into memory from devices on the NuBus. And general usage drivers, usually not associated with any hardware, can be used to implement such things as Inter-Application Communication or a network E-mail system.
Device drivers are accessed through the Unit Table stored in the Macintosh's system heap. A pointer to the Unit Table is stored in the low-memory global UTableBase ($11 C), and the size of the Unit Table is stored in the low-memory global UntryCNt ($1D2). The Unit Table contains entries from which each specific driver is referenced. When a driver is installed in the Unit Table, a device control entry structure is allocated in the system heap, and its handle is installed in the appropriate entry in the unit table. The device control entry, commonly referred to as the DCE, is then filled out with the driver's attributes. The format of the device control entry structure is shown in Example 1. The high-order byte of the field dCtlFlags is set from the drvrFlags field in the driver's header, while the low-order byte is set up at driver installation time. The bits are defined in Figure 1.
typedef struct {
Ptr dCtlDriver ; /* Pointer to ROM driver, or a handle to RAM
driver*/
short dCtlFlags ; /* Driver flags */
QHdr dCtlQHdr ; /* Driver I/O queue header */
long dCtlPosition ; /* Current position; used by block device
drivers*/
} DCtlEntry, *DCtlPtr, **DCtlHandle ;
bit5: set if the driver is open bit6: set if the driver is RAM based bit7: set if the driver is currently executing
The driver itself is usually stored as a DRVR resource, although it can be loaded in from a hardware device. The format of the DRVR resource is shown in Figure 2. Table 1 provides further details on this format.
byte 0:drvrFlags (length 2 bytes)
byte 2:drvrDelay (length 2 bytes)
byte 4:drvrEMask (length 2 bytes)
byte 6:drvrMenu (length 2 bytes)
byte 8:drvrOpen (length 2 bytes)
byte 10:drvrPrime (length 2 bytes)
byte 12:drvrCtl (length 2 bytes)
byte 14:drvrStatus (length 2 bytes)
byte 16:drvrClose (length 2 bytes)
byte 18:drvrName (length 1 byte)
byte 19:drvrName + 1 (length n bytes)
.
.
.
byte 19+n:driver routines
Resource Description
----------------------
drvrFlags The high order byte of the drvrFlags field contains the
following bit fields:
dReadEnable (bit 8) is set if the driver can respond to
Read calls.
dWriteEnable (bit 9) is set if the dirver can respond to
Write calls.
dCtlEnable (bit 10) is set if the driver can respond to
Control calls.
dStatEnable (bit 11) is set if the driver can respond to
Status calls.
dNeedGoodbye (bit 12) is set if driver needs to call be
called before the application heap is reinitialized.
dNeedTime (bit 13) is set if driver needs to be called
periodically.
dNeedLock (bit 14) is set if driver should be locked in
memory.
drvrDelay This field is used if the dNeedTime bit of the drvrFlags
field is set. It defines how often the dirver will be
called to perform periodic actions. This is done by a
control call with csCode 65.
drvrEMask This field is the event mask used by desk accessories. This
contains flags for the desk accessory to define which events
it can receive.
drvrMenu This field is also used by desk accessories. If a desk
accessory has it's own menu, then the menuID of the menu
is stored here.
drvrOpen Each of these fields contain an offset to their respective
routines.
drvrPrime
drvrCtl
drvrStatus
drvrClose
drvrName The device driver's name. The name of a non-desk accessory
device driver always starts with a "." by convention.
Each driver can have five routines to handle calls from the device manager. The routines are: open, prime, control, status, and close. The open and close routines perform initialization and clean up functions for the driver. Read and write calls are handled through the prime routine. The driver's status routine returns the status of the driver to the device manager, and the control routine is for control functions pertaining to the driver's task. When one of the driver's routines is called, a pointer to the parameter block for that call is passed in implementing function register A0, and a pointer to the device control entry is passed in A1. The call-specific information is passed in a parameter block as shown in Example 2.
Control/Status calls
/* parameter block for Control/Status calls */
typedef struct {
QElemPtr qLink; /*Link to next parameter block in
driver queue*/
int qType; /*Queue type*/
int ioTrap; /*Trap to make call;
PBControl = $A004 */
Ptr ioCmdAddr; /*Trap address*/
ProcPtr ioCompletion; /* Completion routines address*/
OsErr ioResult; /*Result of call*/
StringPtr ioNamePtr; /*Driver name*/
int ioVRefNum; /*Volume reference number*/
int ioRefNum; /*Driver reference number*/
int csCode; /*Type of Control/Status call*/
int csParam(11); /*Control/Status information*/
} cntrlParam;
Prime (Read/Write) calls
typedef struct{
QElemPtr dLink; /*Link to next parameter block in
queue*/
int qType; /*Queue type */
int ioTrap; /*Trap used to make call;
PBRead = $A002*/
Ptr ioCmdAddr; /*Trap address */
ProcPtr ioCompletion; /*Completion routines address */
OsErr ioResult; /*Result of call */
StringPtr ioNamePtr; /*Driver name */
int ioVRefNum; /*Volume reference number */
int ioRefNum; /*Driver reference number */
SignedByte ioVersNum; /*Not used */
SignedByte ioPermssn; /*Read/Write permission (for block
device drivers)*/
Ptr ioMisc; /*Not used */
Ptr ioBuffer; /*Pointer to data buffer */
long ioReqCount; /*Requested number of bytes */
long ioActCount; /*Actual number of bytes */
int ioPosMode; /*Positioning mode (block device
drivers) */
long ioPosOffset; /*Positioning offset (block device
drivers) */
} ioParam;
The csCode field is used as a selector for requesting a specific function or specific information from a control/status call. When defining the separate control calls for a driver, the programmer must take into account that some csCodes are predefined, such as the accRun (csCode 65), which is used to give the driver time if the dNeedTime bit is set in the driver's DCE.
The ioPosMode, and the ioPosOffset fields are used for block device drivers to position the current read/write. The valid modes are fsAtMark, fsFromStart, fsFromMark. The current position is contained in the driver's dCtlEntryHandle. If the mode is fsAtMark, ioPosOffset should be ignored and the operation started at the current position. If it is fsFromStart, or fsFromMark, then ioPosOffset is added to the beginning of the device, or the current position respectively, to obtain the starting position for the operation. The constants used to determine mode are: fsAtMark = 0, fsFromStart = 1, fsFromMark = 3. And the ioTrap field can be used to determine whether operation is a read or write, and whether it is synchronous or asynchronous.
I/O requests for drivers are, for the most part, managed by the Device Manager, which calls the driver at the appropriate time to handle enqueuing asynchronous requests. A call to the IODone routine informs the Device Manager that the request was completed. The address of this routine is stored in the low memory global jIODone at address $8FC. If the request was asynchronous and our driver was unable to complete the call, then we must exit via an RTS. If the call was completed we must JMP to the IODone routine, at which point the Device Manager dequeues the request and calls the request's completion routine. The Open and Close routines will always be called synchronously.
Two other routines that a driver may use are Fetch and Stash, which are used as an aid for asynchronous I/O. The Fetch routine, called using the jFetch vector at address $8F4, simply returns one byte at a time from the request's data buffer pointed to by ioBuffer, while incrementing ioActCount by one. The Stash routine is used for Read requests, and simply stuffs bytes into the request's buffer, incrementing ioActCount by one. Its vector is jStash at address $8F8. The setup for these calls is listed in Table 2.
Call Description
_________________________________________________________________________
IODone Asynchronous I/O completion call.
Entry: store pointer to device control entry in A1.
Fetch Fetches a byte fpr a wrote request (asynchronous only).
Entry: pointer to device control entry in A1.
Exit: one byte stored in DO (if bit 15 is set then it is
the last character in the buffer).
Stash States a byte for a read request (asynchronous only).
Entry: pointer to device control entry in A1 byte to be
stashed.
Exit: if bit 15 of DO is set then last byte has been stashed.
Think C provides a mechanism for developing drivers entirely in C using a stub to call main() with the parameter block, the DCE, and a selector to specify the type of call. The prototype for the main routine is:
int main (cntrlParam *ioBlock_ptr, DCtlPtr dce_ptr, int call_type);
The call_type parameter is used to specify which type of Device Manager call was actually made to the driver. The body of the driver should be set up as a switch statement based on call_type, which is shown in Example 3.
int main( cntrlParam *io_ptr, DCtlPtr dce_ptr, int call_type )
{
switch( call_type ) {
case 0: /* open */
case 1: /* prime */
case 2: /* control */
case 3: /* status */
case 4: /* close */
}
return result ;
}
The ioBlock_ptr parameter contains a pointer to the current request parameter block, and the second parameter is a pointer to the driver's device control entry. Think C also allows you to declare globals, and when the driver is loaded, the stub will allocate the memory required and store a handle to it in the dCtlStorage field of the device control entry. If the stub routine could not allocate the memory for the globals, then dCtlStorage will be 0, and the driver's open routine should return a negative error value. It is not necessary to call the IODone routine, as the driver stub provided with Think C does this automatically. If an asynchronous request could not be completed, all that is necessary is to return a 1 to the stub.
The driver presented in this article was written using the Object C extensions in Think C 4.0. Listing One (page 72) shows driver.h, the header file, while Listing Two (page 72) lists driver.c, the source file. In order to use the template, simply declare a subclass of the class driver, and override whatever methods are necessary (see Example 4). For more information on Object C, see the Think C User's Guide for Version 4.0. Also, a New() routine must be provided to allocate the subclass as shown in Example 5.
struct my_driver:driver {
int my_storage ;
/* overridden routines */
void Open ( ) ;
void Close ( ) ;
void Read ( ) ;
void Write ( ) ;
} ;
driver *New( )
{
return new( my_driver ) ;
}
The Fetch() and Stash() routines were provided for use with asynchronous calls, but should not be used for non-immediate calls. The async field of the driver class can be used to determine whether a call was asynchronous. After the driver is completed, all that needs to be done is to install it in the unit table. Although it sounds simple in theory, this can be as much fun as writing the driver itself, so I will leave driver installation for another article.
_WRITING MACINTOSH DEVICE DRIVERS_
by Bryan Waters
[LISTING ONE]
Copyright © 1989, Dr. Dobb's Journal
/* driver class declaration */
#ifndef _driver_h_
#define _driver_h_
struct driver : indirect {
cntrlParam *pb ;/* parameter block */
DCtlPtr dc ; /* device control entry field */
int async ; /* this is 1 if the call is asynchronous, else 0 */
int status ; /* return status */
/* methods */
void Open( ) ;
void Prime( ) ;
void Read( ) ;
void Write( ) ;
void Control( ) ;
void Status( ) ;
void Close( ) ;
void Idle( ) ;
void Error( int ) ;
} ;
/* generic allocation routine */
driver *New(void ) ;
/* some useful defines */
#ifndef NULL
#define NULL 0L
#endif
/* define jump vectors */
#define jFetch 0x8f4
#define jStash 0x8f8
/*This is not needed since the IODone routine is automatically called by Think C's driver stub
#define jIODone 0x8fc
*/
/*#define asyncTrpBit 0x0200*/ /* already defined in Think C MacHeaders */
#define ioNotCompleted 1
#endif
[LISTING TWO]
#include <DeviceMgr.h>
#include "driver.h"
#define OPEN 0
#define PRIME 1
#define CONTROL 2
#define STATUS 3
#define CLOSE 4
driver *drvr = NULL ;
int main( cntrlParam *paramBlock, DCtlPtr devCtlEnt, int rout )
{
int result = 0 ;
if( rout != OPEN && drvr != NULL ) {
drvr->pb = paramBlock ;
drvr->dc = devCtlEnt ;
drvr->async = paramBlock->ioTrap&asyncTrpBit ; /* is it asynchronous */
drvr->status = 0 ;
}else if( rout != OPEN ){
return badUnitErr ; /* driver not open, but installed */
}
switch( rout ) {
case OPEN:
if( drvr == NULL ) {
if( devCtlEnt->dCtlStorage == NULL ) {
result = -1 ;
}else{
drvr = New( ) ;
if( drvr == NULL ) {
result = -1 ;
}else{
drvr->pb = paramBlock ;
drvr->dc = devCtlEnt ;
drvr->Open( ) ;
}
}
}
break ;
case PRIME:
drvr->Prime( ) ;
break ;
case CONTROL:
drvr->Control( ) ;
break ;
case STATUS:
drvr->Status( ) ;
break ;
case CLOSE:
drvr->Close( ) ;
result = drvr->status ;
delete( drvr ) ;
drvr = NULL ;
break ;
}
/* if the driver is open, then return result from the driver */
if( drvr != NULL ) result = drvr->status ;
return result ;
}
void driver::Open( )
{
status = 0 ;
return ;
}
void driver::Prime( )
{
if( pb->ioTrap&0x00FF == aRdCmd ) { /* is it a read command */
Read( ) ;
}else{
Write( ) ; /* no so it must be a write command */
}
return ;
}
void driver::Read( )
{
/* override this routine to implement reads from the device */
/* if the operation is asynchronous, and could not be completed right away, the
/* in order to find out whether a call is asynchronous, simply
}
void driver::Write( )
{
/* override this routine to implement writes to the device */
/* if the operation is asynchronous, and could not be completed right away, then
/* in order to find out whether a call is asynchronous, simpl
}
void driver::Control( )
{
switch( pb->csCode ) {
case accRun:
Idle( ) ;
break ;
case goodBye:
Close( ) ;
break ;
}
}
void driver::Status( )
{
status = 0 ;
return ;
}
void driver::Idle( )
{
return ;
}
void driver::Close( )
{
/* if close is successful, set status to 0, else set status to closeErr(-24) */
/* eg. if( cannot close for some reason ) { Error( closeErr )
status = 0 ;
return ;
}
void driver::Error( int err_val )
{
/* This routine sets status to the error number specified by err_val. Note: this must be a negative number, preferably
a valid Macintosh e
status = err_val ;
}
/* Routine: Fetch
Description: This routine gets the next character from the ioBuffer and increments ioActCount by 1. If it is the last
character to sen
Result: returns the next character to send to the device.
Note: this routine should only be used on asynchronous calls
*/
char Fetch( DCtlPtr dc, int *last_char )
{
int data ;
asm{
move.l dc, A1
move.l jFetch, A0
jsr (A0)
move.b D0, data
}
*last_char = data & 0x8000 ;
return data&0x00FF ;
}
/* Routine: Stash
Description: This routine takes care of stashing a byte of data read from a device into the ioBuffer
and incrementing the ioActCount.
Result: returns true after stashing the last byte requested.
Note: this routine should only be used on asynchronous calls
*/
int Stash( DCtlPtr dc, char stash_data )
{
int data ;
asm{
move.l dc, A1
move.b stash_data, D0
move.l jStash, A0
jsr (A0)
move.b D0, data
}
return data&0x8000 ;
}
Example 1: The format of the device control entry structure.
typedef struct {
Ptr dCtlDriver ; /* Pointer to ROM driver, or a handle to RAM driver*/
short dCtlFlags ; /* Driver flags */
QHdr dCtlQHdr ; /* Driver I/O queue header */
long dCtlPosition ; /* Current position; used by block device drivers*/
} DCtlEntry, *DCtlPtr, **DCtlHandle ;
Example 2: Call specific information
Control/Status calls
/* parameter block for Control/Status calls */
typedef struct{
QElemPtr qLink; /*Link to next parameter block in driver queue*/
int qType; /*Queue type*/
int ioTrap; /*Trap to make call; PBControl = $A004 */
Ptr ioCmdAddr; /*Trap address*/
ProcPtr ioCompletion; /*Completion routines address*/
OsErr ioResult; /*Result of call*/
StringPtr ioNamePtr; /*Driver name*/
int ioVRefNum; /*Volume reference number*/
int ioRefNum; /*Driver reference number*/
int csCode; /*Type of Control/Status call*/
int csParam[11]; /*Control/Status information*/
} cntrlParam;
Prime (Read/Write) calls
typedef struct{
QElemPtr dLink; /*Link to next parameter block in queue*/
int qType; /*Queue type */
int ioTrap; /*Trap used to make call; PBRead = $A002*/
Ptr ioCmdAddr; /*Trap address */
ProcPtr ioCompletion; /*Completion routines address */
OsErr ioResult; /*Result of call */
StringPtr ioNamePtr; /*Driver name */
int ioVRefNum; /*Volume reference number */
int ioRefNum; /*Driver reference number */
SignedByte ioVersNum; /*Not used */
SignedByte ioPermssn; /*Read/Write permission (for block device drivers)*/
Ptr ioMisc;/*Not used */
Ptr ioBuffer; /*Pointer to data buffer */
long ioReqCount; /*Requested number of bytes */
long ioActCount; /*Actual number of bytes */
int ioPosMode; /*Positioning mode (block device drivers) */
long ioPosOffset; /*Positioning offset (block device drivers)*/
} ioParam;
Example 3: The body of the driver should be set up as a switch
statement based on call_type
int main( cntrlParam *io_ptr, DCtlPtr dce_ptr, int call_type )
{
switch( call_type ) {
case 0: /* open */
case 1: /* prime */
case 2: /* control */
case 3: /* status */
case 4: /* close */
}
return result ;
}
Example 4: Overriding methods
struct my_driver:driver {
int my_storage ;
/* overridden routines */
void Open( ) ;
void Close( ) ;
void Read( ) ;
void Write( ) ;
} ;
Example 5: A New( ) routine allocates the subclass
driver *New( )
{
return new( my_driver ) ;
}