3. Samples
3.1 Sample PDD: DRIVER1$
The Header:
The drivers header structure is defined in DEVHDR.H:
#define DEV_CBNAME 8 struct SysDev { unsigned long SDevNext; /* Pointer to next device header */ unsigned short SDevAtt; /* Attributes of the device */ unsigned short SDevStrat; /* Strategy entry point */ unsigned short SDevInt; /* Interrupt entry point */ unsigned char SDevName[DEV_CBNAME]; /* name (block uses only 1st byte) */ unsigned short SDevProtCS; /* Protect-mode CS of strategy entry pt */ unsigned short SDevProtDS; /* Protect-mode DS */ unsigned short SDevRealCS; /* Real-mode CS of strategy entry pt */ unsigned short SDevRealDS; /* Real-mode DS */ }; struct SysDev3 { struct SysDev SysDevBef3; unsigned long SDevCaps; /* bit map of DD /MM restrictions */ };
The DRIVER1$ sample initializes the structure at the very first point in the source code. This is necessary because the device header needs to be the very first data in the drivers data segment.
struct SysDev3 device = { { (ULONG)0xFFFFFFFF, (USHORT)(DEV_CHAR_DEV | DEV_30 | DEVLEV_3), (USHORT)STRATEGY, (USHORT)0, "DRIVER1$", (USHORT)0, (USHORT)0, (USHORT)0, (USHORT)0, }, (ULONG)(DEV_IOCTL2 | DEV_PARALLEL) };
SDevNext is set to 0xffffffff (value 1) because the driver drives only one device. The drivers attributes SDevAtt are set to be a character DD (DEV_CHAR_DEV) which supports the open/close commands (DEV_30) and level 3 driver features (DEVLEV_3). SDevStrat points to the offset of the DDs strategy routine, in this sample STRATEGY. The STRATEGY routine resides in a small assembler startup code. Because this driver doesn't support IDC (inter DD communication), the member SDevInt is set to 0.
ATTENTION: This member is NOT the pointer to the interrupt routine!!! The comment in the header DEVHDR.H is WRONG!!! It is in fact the offset of the IDC routine!
The next 8 bytes for the member SDevName present the drivers name (in the sample DRIVER1$). Sometimes, driver names end with a $-sign. This can help to recognize that it is a driver name and not a file name.
ATTENTION: The name's length needs to be 8 bytes. If a shorter name is used you have to append blanks to fill it up to 8 bytes.
The next 8 bytes are reserved. The last member SDevCaps specifies some additional attributes for level 3 drivers. The sample driver supports IOCtl2 calls and implements a parallel port driver.
The startup code:
The assembler startup code implements the function STRATEGY:
_STRATEGY proc far __acrtused: ;C startup point dummy push 0 jmp start ;request device 0 start: push es ;request packet address push bx call _main ;call C main pop bx ;result request packet pop es add sp,2 ;cleanup 0 for 1st device mov word ptr es:[bx+3],ax ;return code from main ret _STRATEGY endp
The device header member SDevStrat points to the label _STRATEGY (the compiler automatically puts an underline sign in front of each symbol). If an application calls a driver relevant API function, the kernel builds a request packet with the API parameters and calls the _STRATEGY routine. The kernels puts the address of the request packet into the registers ES and BX, before it calls this function. The _STRATEGY code is runs in the applications context (with the applications thread)!
The assembler code prepares two arguments for the _main() function, which resides in the C code. The first argument is the far address (16:16 address) to the request packet and the second is the device number (16 bit value). Remember the C convention - parameters are pushed from right to left! In the sample, the device number is 0.
After returning from the _main() function, the status word in the request packet must be set to the return value of the _main() function!
int main(PRPH rp, int devno) { ... ... switch(rp->Cmd) { case CMDInit: return Init((PRPINITIN)rp); case CMDINPUT: ... ... return STDON; case CMDOUTPUT: ... ... return STDON; case CMDOpen: ... ... return STDON; case CMDClose: ... ... return STDON; default: return (STDON | STERR | STATUS_ERR_UNKCMD); } }
The C main() function receives the request packet pointer (and the device number, but it's ignored in the sample).
The request packet:
The request packet is divided into two parts. The request packet header and the command specific data structure. The request packet header has the following structure (all the request packet stuff is defined in REQHDR.H):
typedef struct _RPH RPH; typedef struct _RPH FAR *PRPH; typedef struct _RPH *NPRPH; typedef struct _RPH { /* RPH */ UCHAR Len; UCHAR Unit; UCHAR Cmd; USHORT Status; UCHAR Flags; UCHAR Reserved_1[3]; PRPH Link; } RPH; /* Status word in RPH */ #define STERR 0x8000 /* Bit 15 - Error */ #define STINTER 0x0400 /* Bit 10 - Interim character */ #define STBUI 0x0200 /* Bit 9 - Busy */ #define STDON 0x0100 /* Bit 8 - Done */ #define STECODE 0x00FF /* Error code */ #define WRECODE 0x0000 #define STATUS_DONE 0x0100 #define STATUS_ERR_UNKCMD 0x8003
The Cmd member specifies the command of the request. Usually, the main() function implements a switch-statement for the Cmd member to decide what to do.
The init-command:
One important command (and the very first command) is the init-command. While booting is in progress, the OS/2 loader loads the DD into memory and passes this packet to the strategy routine.
typedef struct _RPINIT { /* RPINI */ RPH rph; UCHAR Unit; PFN DevHlpEP; PSZ InitArgs; UCHAR DriveNum; } RPINITIN, FAR *PRPINITIN;
Usually, a DD can not call standard OS/2 API functions. Instead the DD needs to call special functions (device helper routines). There is one entry point to call these functions - the DevHlpEP member. After receiving the init-command, this pointer needs to be saved. If you want to use the standard device helper library DHCALLS.LIB (delivered with the DDK) the name needs to be "Device_Help". The library code uses this global variable to call the kernel device helper functions.
The member InitArgs points to the command line arguments of the driver (the sample doesn't use it).
Before leaving the main() function, the init-request packet needs to be set up with some arguments (and with a different structure).
typedef struct _RPINITOUT { /* RPINO */ RPH rph; UCHAR Unit; USHORT CodeEnd; USHORT DataEnd; PBPBS BPBArray; USHORT Status; } RPINITOUT, FAR *PRPINITOUT;
The members CodeEnd and DataEnd should point to the real endpoints of the code and data segment. The kernel can save memory while shrinking the size of both segments to the necessary size. Because the init-command will only be received ONCE, this code can be deleted too. To achieve this, the init code needs to be placed at the very last end of the code segment.
int Init(PRPINITIN rp) { /* save device helper entry */ Device_Help = rp->DevHlpEP; /* display init message */ DosPutMessage(1,2,CrLf); DosPutMessage(1,strlen(InitMessage1),InitMessage1); DosPutMessage(1,strlen(InitMessage2),InitMessage2); /* set end of CS and DS */ ((PRPINITOUT)rp)->CodeEnd = (USHORT)&Init; ((PRPINITOUT)rp)->DataEnd = (USHORT)&EndMarker; return STDON; }
While calling the Init() function, the main() function casts the request packet parameter to the proper init-command structure PRPINITIN. The function saves the device helper entry point and prints a startup message on the screen (remember, some OS/2 API calls could be used at init time). The CodeEnd member is set to the Init() function itself and the DataEnd member is set to a dummy global variable, which needs to be placed at the very end of the data segment. The STDON value needs to be returned, to signal the correct initializing process.
The open-command:
An application may open the driver with the following C library code:
... ... FILE *cfh; cfh = fopen("DRIVER1$","wb+"); ... ...
The kernel forms a request packet for this open command and calls the strategy routine (by analyzing the Cmd member with the switch command).
case CMDOpen: /* check, if device is already open */ if(deviceOpen) { /* Device is already open, return with the DONE, ERROR and BUSY flags */ return (STDON | STERR | STBUI); } else { /* Device not open, set open flag */ /* Get PID out of the local info segment. If the GetDosVar function fails, the DONE, ERROR and UNKNOWN_COMMAND flags are set */ if(DevHelp_GetDOSVar(DHGETDOSV_LOCINFOSEG,0,&ptemp)) return (STDON | STERR | STATUS_ERR_UNKCMD); currentPID = ((struct InfoSegLDT FAR*)ptemp)->LIS_CurProcID; deviceOpen = 1; } return STDON;
Because it's an easy sample, it is designed for single process usage (none share). If the device is already open, an error code will be returned and the fopen() function will fail. This checking will be done with the global variable "deviceOpen", which is initialized with 0. But if the device could be used, the caller's PID will be saved, and the "deviceOpen" flag will be set. The callers PID is one piece of information in the local info segment which can be retrieved by calling the device helper function DevHelp_GetDOSVar(). If everything is ok, the status STDON is returned. In this sample the open function is simply to protect the device from multiple open calls. No hardware specific initialization is necessary.
The read-command:
After opening, the application may try to read data from the device with the following code sample:
cret = fgetc(cfh);
The sample driver's code for the read-command is implemented as follows:
case CMDINPUT: /* check, if the transfer buffer has at least a size of 1 byte */ if (((PRP_RWV)rp)->NumSectors < 1) return (STDON | STERR | ERROR_INSUFFICIENT_BUFFER); /* set read byte count in request packet to 0 (error handling) */ ((PRP_RWV)rp)->NumSectors = 0; /* read byte from port to a temporary buffer */ inputByte = inp(0x379); /* the transfer address for the read request packet is a physical address and need to be convert to a virtual address */ if(DevHelp_PhysToVirt(((PRP_RWV)rp)->XferAddr,1,&ptemp,&ustemp)) return (STDON | STERR | STATUS_ERR_UNKCMD); /* copy the temporary buffer to the virtual address */ ((PBYTE)ptemp)[0] = inputByte; /* set read byte count in request packet to 1 */ ((PRP_RWV)rp)->NumSectors = 1; return STDON;
First, the size of the transfer buffer is checked, if it has at least 1 byte. The request packet member NumSectors needs to be set to the number of successfully read bytes, before returning. If an error occurs, this member should be set to 0, therefore this is done first. With the inp() command, the driver reads one byte from the port 0x379 which is the 2nd parallel port register (status word). The request packet contains the member XferAddr which is a pointer to the read buffer of the application. This pointer is a 32 bit PHYSICAL ADDRESS and needs to be converted to a 16:16 VIRTUAL ADDRESS for further usage. The kernel has already locked the memory and checked the access privileges. Now, the byte can be transferred to the application's read buffer. The last action sets the number of successfully transferred bytes to 1 and returns with the status STDON.
The write-command:
The application may write one byte to the sample driver with the following sample code:
cret = fputc(0xff,cfh);
The driver write-section looks like this:
case CMDOUTPUT: /* check, if the transfer buffer has at least a size of 1 byte */ if (((PRP_RWV)rp)->NumSectors < 1) return (STDON | STERR | ERROR_INSUFFICIENT_BUFFER); /* set write byte count in request packet to 0 (error handling) */ ((PRP_RWV)rp)->NumSectors = 0; /* convert physical transfer address to virtual address */ if(DevHelp_PhysToVirt(((PRP_RWV)rp)->XferAddr, 1, &ptemp, &ustemp)) return (STDON | STERR | STATUS_ERR_UNKCMD); /* put byte to printer data port */ outp(0x378,((PBYTE)ptemp)[0]); /* set read byte count in request packet to 1 */ ((PRP_RWV)rp)->NumSectors = 1; return STDON;
First, the transfer address is converted to a virtual address. The outp() function puts the first byte from the transfer buffer to the port 0x378 (the data port of the parallel interface). The number of transferred bytes is set to 1 and the function returns with the status STDON.
The close-command:
The application may close the device with the following code:
cret = fclose(cfh);
The driver section for the close command is:
case CMDClose: /* check, if the device is open */ if(!deviceOpen) return (STDON | STERR | STATUS_ERR_UNKCMD); /* get PID out of the local info segment. If the GetDosVar function fails, the DONE, ERROR and UNKNOWN_COMMAND flags are set */ if (DevHelp_GetDOSVar(DHGETDOSV_LOCINFOSEG,0,&ptemp)) return (STDON | STERR | STATUS_ERR_UNKCMD); /* check, if the correct PID want to close the device */ if(currentPID != ((struct InfoSegLDT FAR*)ptemp)->LIS_CurProcID) return (STDON | STERR | STATUS_ERR_UNKCMD); /* set device status close */ deviceOpen = 0; return STDON;
The device can only be closed, if it is already open. Only the process which opened the device has the privilege to close it. The PID can be found in the local info segment. The variable "deviceOpen" is set to 0 to indicate that the device is closed.
Warpstock Europe 1999 |