Building SCSIsupport into your code
Brian is an independent programmer specializing in device drivers and software for SCSI peripherals. He can be reached at bsawert@grdpnt.flagstaff.az.us or on CompuServe at 72027,2143.
In the near future, as CD-ROM drives, optical drives, and scanners become standard fare, SCSI devices will likely dominate the PC peripheral market. And as the market for SCSI peripherals grows, so will the demand for software that supports them.
But SCSI is not without its downside. SCSI protocol has generally been complex, a clear-cut standard for SCSI PC hardware has yet to emerge, and DOS doesn't offer direct support for SCSI. The Advanced SCSI Programming Interface (ASPI), however, offers a straightforward way to build SCSI capability into your code. In this article, I'll examine the ASPI standard and discuss how to use ASPI to solve SCSI-support problems.
The Problem with SCSI
Although the SCSI standard defines a communications protocol and a command set, the missing link for the DOS programmer is the interface to the SCSI bus. If you tackle the problem through low-level hardware programming, you restrict your software to a particular type or class of SCSI host adapter.
ASPI, proposed by Adaptec, defines a set of high-level functions for communicating with devices attached to the SCSI bus. It's designed to protect you from the pitfalls of hardware-level programming and provide a standard interface to SCSI host adapters.
The Advanced SCSI Programming Interface
Adaptec, Trantor, and Future Domain all offer ASPI compatibility with their host adapters, ensuring hardware independence and a broader market for software written to the standard. Even unusual host adapters such as Trantor's MiniSCSI parallel-port adapter offer ASPI support.
ASPI offers many advantages to the SCSI programmer. Because it is widely supported, it provides hardware and platform independence. Furthermore, ASPI manager software is available for DOS, OS/2, Novell NetWare, and other operating systems. Best of all, ASPI provides a high-level function set that's easy to use and can dramatically shorten your development time. ASPI hides the inner workings of SCSI protocol from the programmer. The ASPI manager handles the gritty details of arbitration, selection, and message passing, returning status codes and sense data when appropriate. You still need a basic knowledge of SCSI structures and protocol to use ASPI effectively, but it can shorten your learning curve by making experimentation easy.
ASPI does have its shortcomings. For example, the ASPI manager does not return a transfer count. If your device does not support the illegal-length indicator (ILI) and residual count in the sense data, there's no way to check how much data was transferred. Some ASPI managers fail to recognize certain devices, restricting the operations you can perform on them. And the number of different status codes returned by an ASPI call requires additional error checking by the calling program.
How ASPI Works
Under DOS, ASPI takes the form of a character device driver. The driver installs the ASPI manager--a device named $SCSIMGR. At startup, the ASPI manager polls the SCSI bus, looking for attached devices. A device must be turned on before bootup in order for ASPI to recognize it. The only time you access the ASPI driver through DOS is when you initialize it. At other times, a far call to the ASPI-manager entry point gives access to the ASPI function set. This makes ASPI functions safe to use from within a device driver or memory-resident program.
Using ASPI requires knowledge of the SCSI command set and capabilities for the device you wish to support. Keep in mind that ASPI provides interface functions, not high-level SCSI functions. The ASPI manager merely passes data through to the SCSI device without modifying or inspecting it. A point of confusion sometimes arises because ASPI parameters such as transfer length are in Intel order, while SCSI command-descriptor block (CDB) parameters appear in order of MSB to LSB.
The ASPI function set is small, but powerful. Some of the functions return information about the driver and environment, while others actually communicate with the SCSI device. The ASPI specification defines the functions in Table 1. Probably the most useful function in the ASPI set is Function 2 (execute SCSI I/O request). This function passes a SCSI CDB to the device, handles data transfer, and returns messages and status codes. If the request results in a Check Condition status, ASPI requests sense data from the device, making it available to the calling program. Consider this function a direct channel to the SCSI bus, as you will use it for most of your SCSI requests.
To initialize the ASPI manager, get a file handle to the ASPI driver by issuing a DOS open call to the $SCSIMGR device. Next, use a DOS IOCTL read to obtain the ASPI entry-point address. This is a far address you call to execute any of the ASPI functions. Last of all, close the file handle. You won't need to access the driver through DOS again. Listing One (page 159) illustrates these steps.
All ASPI functions use a SCSI request block (SRB). The SRB has a common 8-byte header for each function, containing an ASPI command code (see Table 2), host-adapter number, and request flags. The header also contains a status byte that the ASPI manager uses to return the outcome of the ASPI request. Calling an ASPI function requires filling in the proper SRB for the function, pushing the far address of the SRB on the stack, and making a far call to the ASPI entry point.
Functions that only access the ASPI manager return immediately. Functions that communicate with the SCSI device may return with the status byte set to 0, indicating that the request is still in progress. ASPI offers two methods for determining whether a SCSI request has completed: polling and posting. Polling, as the name implies, simply means periodically checking the status byte for a nonzero value. Posting, on the other hand, tells the ASPI manager to execute a post-processing routine when the SCSI request has completed. You enable posting by setting the Post bit in the request flags and passing the address of the routine in the SRB. The ASPI documentation recommends that applications use polling, reserving posting for device drivers or TSR programs, which may be interrupt driven.
SCSI I/O Under ASPI
The most complex function in the ASPI specification is also the most powerful. Function 2 (execute SCSI I/O request) is the gateway to the SCSI bus. The SRB for this function contains the data-buffer address and size, the target SCSI ID, and the SCSI CDB. Function 2 is also the most awkward to use. In addition to the ASPI status byte, it returns status codes for both the host and the target device. It also returns sense data if the SCSI request produces any. Finding the sense data can be tricky. It does not reside at a fixed offset in the SRB, but appears immediately after the SCSI CDB. You must keep track of the size of the CDB to locate the sense data. Once you are aware of these issues, using this function becomes quite simple. Just fill in the SRB, call the ASPI entry point, and watch the status byte until the request completes. Listing Two (page 159) demonstrates how to call the ASPI I/O function using polling.
ASPI Under Windows
Using ASPI under Windows is a bit more complex. Because the ASPI manager expects real-mode addresses in segment:offset form, the protected-mode selector:offset type of address will not work properly. You must pass real-mode addresses for the SRB and data buffers.
The entry-point address the ASPI manager returns is also a real-mode address. This means that under protected mode, you cannot call this address directly.
The solution to both problems is to use Windows' DPMI services, but there is a catch here that may not be obvious: These services are not available to a Windows application; you can only use them from within a DLL.
If you're starting to get discouraged, take heart. Accessing the ASPI manager requires only three DPMI functions. Function 100h (allocate DOS memory block) and function 101h (free DOS memory block) manage real-mode memory for the ASPI SRB and data buffers. The Windows functions GlobalDOSAlloc and GlobalDOSFree use these services, and they are easier to work with than the corresponding DPMI functions. Be careful, however, that you don't use real-mode buffers too freely. Allocating real-mode memory means taking it from the address space below 1 megabyte, where memory is a limited resource.
The only DPMI function you must use directly is function 301h (call real-mode procedure with far-return frame). Since the ASPI entry point is a real-mode address, you risk a protection fault if you try to call it from within Windows. Function 301h lets you call the entry point from protected mode, although the procedure is somewhat cumbersome. The calling function must fill a structure with register values for the real-mode procedure and set up a stack for passing parameters. The DPMI server can provide a default stack, but the size is limited. Stack usage may vary between ASPI drivers, so you may wish to provide your own.
Listing Three (page 160) recasts the call to the ASPI entry point as a DLL function. The call requires some inline assembly code and uses the DPMI default stack for simplicity. The real-mode call structure duplicates the 32- and 16-bit CPU registers. Setting up the call requires filling the CS:IP fields in the structure with the real-mode address of the ASPI entry point. Pass the pointer to the SRB on the protected-mode stack, setting CX to the number of words pushed. The DPMI server will create a real-mode stack with these parameters. Last of all, execute an INT 31h to call the procedure.
If you want to know more about DPMI functions, download the DPMI spec from the CompuServe Intel forum. For examples of using Windows DPMI services, look in the Microsoft Software Library on CompuServe.
Conclusion
The complete listings (available electronically, see "Availability," page 3) for these examples include source code for an ASPI library and an application to inventory the SCSI bus. For Windows programmers, the listings also include source code for an ASPI DLL.
If you want to investigate ASPI further, Adaptec sells the ASPI Software Developer's Kit, which contains the specification and programming guides for DOS, Windows, OS/2, and NetWare, as well as utilities and sample code. (Call Adaptec at 800-442-7274 or call their BBS at 408-945-7727.)
Table 1: ASPI functions.
Function Description ----------------------------------- Function 0 Host-adapter inquiry Function 1 Get device type Function 2 Execute SCSI I/O request Function 3 Abort SCSI I/O request Function 4 Reset SCSI device Function 5 Set host-adapter parameters Function 6 Get disk-drive parameters
Table 2: The ASPI DOS specification.
Command Code Description ------------------------------------------------------------------ Command Code 0 Returns information about installed host Host-adapter Inquiry adapters: the number of adapters installed and strings identifying the ASPI manager and host adapter. Command Code 1 Returns the peripheral device type code for Get Device Type the device at a given SCSI target ID. Command Code 2 Executes a SCSI command, passing a CDB to a Execute SCSI I/O Request device and managing data transfer and status return. Requests sense data if a Check Condition status occurs. Command Code 3 Aborts a pending SCSI operation. Abort SCSI I/O Request Command Code 4 Resets the device at a given SCSI target ID. Reset SCSI Device Command Code 5 Sets unique parameters for a host adapter. Few Set Host-adapter manufacturers implement this function. Parameters Command Code 6 Returns information for a SCSI disk drive. Get Disk-drive Returns preferred head and sector Information translations and flags for INT 13h support. This is a recent addition to the ASPI spec and is not widely supported.
// ------ Initializing the ASPI driver. ------- #include "aspi.h" // ASPI definitions and constants #include "scsi.h" // SCSI definitions and constants // -------------------- defines and macros -------------------- #define IOCTL_READ 2 // IOCTL read function #define ASPI_NAME "SCSIMGR$" // SCSI manager driver name // -------------------- global variables -------------------- void (far *ASPIRoutine) (aspi_req_t far *); // far address of ASPI routine BYTE f_installed; // flag for ASPI existence aspi_req_t *srb; // SCSI Request Block pointers // ---------------------------------------------------------------------- // Routine to check for and initialize ASPI driver. // Usage: int aspi_open(void); // Returns nonzero on success, 0 if ASPI driver not found or init failed. int aspi_open(void) { int dh; // ASPI driver handle if (!f_installed) { // not initialized if ((dh = open(ASPI_NAME, O_RDONLY | O_BINARY)) != -1) { // open device driver if (ioctl(dh, IOCTL_READ, (void *) &ASPIRoutine, sizeof(ASPIRoutine)) == sizeof(ASPIRoutine)) { // got ASPI entry point srb = (aspi_req_t *) malloc(sizeof(aspi_req_t)); if (srb != NULL) { // allocated SRB buffers f_installed++; // set installed flag } } close(dh); // close device driver } } return(f_installed); } // ---------------------------------------------------------------------- // Routine to close and clean up. // Usage: void aspi_close(void); // Called with nothing. Returns nothing. void aspi_close(void) { if (f_installed) { // already initialized free(srb); // deallocate buffers f_installed = 0; // clear installed flag } return; }[LISTING TWO]
<a name="00d4_000f"> // ------ Calling the ASPI entry point and ASPI I/O. ------- #include "aspi.h" // ASPI definitions and constants #include "scsi.h" // SCSI definitions and constants // -------------------- defines and macros -------------------- #define BUSY_WAIT 0x1000 // repeat count for busy check #define MIN_GRP_1 0x20 // minimum SCSI group 1 command // -------------------- global variables -------------------- void (far *ASPIRoutine) (aspi_req_t far *); // far address of ASPI routine BYTE f_installed; // flag for ASPI existence BYTE aspi_stat; // ASPI status byte BYTE host_stat; // host status byte BYTE targ_stat; // target status byte BYTE host_num; // host adapter number (0 default) aspi_req_t *srb; // SCSI Request Block pointers sense_block_t *srb_sense; // pointer to SRB sense data // ---------------------------------------------------------------------- // Routine to call ASPI driver. // Usage: int aspi_func(aspi_req_t *ar); // Called with pointer to SCSI Request Block (SRB). // Returns nonzero on success, 0 on error. int aspi_func(aspi_req_t *ar) { int retval = 0; if (f_installed) { // ASPI manager initialized ASPIRoutine((aspi_req_t far *) ar); // call ASPI manager retval++; } return(retval); } // ---------------------------------------------------------------------- // Execute SCSI I/O through ASPI interface. // Usage: int aspi_io(BYTE *cdb, BYTE far *dbuff, WORD dbytes, // BYTE flags, BYTE id, WORD *stat); // Called with pointer to data buffer, data buffer size, pointer to CDB, // request flags, and target ID. Returns ASPI status on success, -1 on error. // Fills stat variable with host status in high byte, target in low byte. int aspi_io(BYTE *cdb, BYTE far *dbuff, WORD dbytes, BYTE flags, BYTE id, WORD *stat) { int cdbsize; int timeout; // timeout counter for polling int retval = -1; memset(srb, 0, sizeof(aspi_req_t)); // clear SRB srb->command = SCSI_IO; // set command byte srb->hostnum = host_num; // set host adapter number srb->su.s2.targid = id; // set target SCSI ID srb->reqflags = flags; // set request flags srb->su.s2.databufptr = dbuff; // set pointer to data buffer srb->su.s2.datalength = dbytes; // set data buffer length srb->su.s2.senselength = sizeof(sense_block_t); // set sense data buffer length cdbsize = sizeof(group_0_t); // assume 6 byte CDB if (*((BYTE *) cdb) >= MIN_GRP_1 && *((BYTE *) cdb) < MIN_GRP_6) { // CDB size is 10 bytes cdbsize = sizeof(group_1_t); } srb->su.s2.cdblength = cdbsize; // set CDB length srb->su.s2.senselength = MAX_SENSE; // sense sense data length memcpy(srb->su.s2.scsicdb, cdb, cdbsize); // copy CDB to SRB srb_sense = srb->su.s2.scsicdb + cdbsize; // point to sense data buffer if (aspi_func(srb)) { // ASPI call succeeded timeout = BUSY_WAIT; // set timeout counter while (srb->status == REQ_INPROG && timeout > 0) { // request in progress - keep polling timeout--; // decrement timeout counter } retval = aspi_stat = srb->status; // save ASPI status if (aspi_stat != REQ_INPROG) { // request completed host_stat = srb->su.s2.hoststat; // save host status targ_stat = srb->su.s2.targstat; // save target status *stat = ((WORD) host_stat << 8) | targ_stat; // return combined SCSI status } } return(retval); } <a name="00d4_0010"><a name="00d4_0011">[LISTING THREE]
<a name="00d4_0011"> // ------ Initializing and using ASPI in a DLL. ------ #include "aspi.h" // ASPI definitions and constants #include "scsi.h" // SCSI definitions and constants // -------------------- defines and macros ------------------- #define IOCTL_READ 2 // IOCTL read function #define ASPI_NAME "SCSIMGR$" // SCSI manager driver name #define DPMI_INT 0x31 // DPMI interrupt number #define REAL_CALL 0x301 // real mode call function typedef struct RealCall { // struct for real mode call DWORD edi, esi, ebp, reserved, ebx, edx, ecx, eax; WORD flags, es, ds, fs, gs, ip, cs, sp, ss; } RealCall_t ; // -------------------- global variables ------------------- void (far *ASPIRoutine) (aspi_req_t far *); // far address of ASPI routine BYTE f_installed; // flag for ASPI existence aspi_req_t _FAR *srb; // SCSI Request Block pointers DWORD dwPtr; // return from GlobalDOSAlloc // -------------------- local functions ------------------- void far *MaptoReal(void far *pptr); // map to real mode address int AspiCall(void far *aspiproc, aspi_req_t far *ar); // DPMI function // ---------------------------------------------------------------------- // Routine to check for and initialize ASPI driver. // Usage: int FUNC aspi_open(void); // Returns nonzero on success, 0 if ASPI driver not found or init failed. int FUNC aspi_open(void) { int dh; // ASPI driver handle UINT wSRB; // selector for buffer memory if (!f_installed) { // not initialized if ((dh = open(ASPI_NAME, O_RDONLY | O_BINARY)) != -1) { // open device driver if (ioctl(dh, IOCTL_READ, (void far *) &ASPIRoutine, sizeof(ASPIRoutine)) == sizeof(ASPIRoutine)) { // got ASPI entry point dwPtr = GlobalDosAlloc(sizeof(aspi_req_t)); if (dwPtr[0] != NULL) { // allocated SRB buffer wSRB = LOWORD(dwPtr[0]); // extract selector GlobalPageLock(wSRB); // lock memory srb = (aspi_req_t _FAR *) MAKELP(wSRB, 0); f_installed++; // set installed flag } } close(dh); // close device driver } } return(f_installed); } // ---------------------------------------------------------------------- // Routine to close and clean up. // Usage: void FUNC aspi_close(void); // Called with nothing. Returns nothing. void FUNC aspi_close(void) { UINT wSRB; // selector for buffer memory if (f_installed) { // already initialized wSRB = FP_SEG(srb); // extract selector from pointer GlobalPageUnlock(wSRB); // unlock buffer GlobalDosFree(wSRB); // deallocate buffer dwPtr = NULL; srb = NULL; f_installed = 0; // clear installed flag } return; } // ---------------------------------------------------------------------- // Routine to call ASPI driver. // Usage: int aspi_func(aspi_req_t _FAR *ar); // Called with pointer to SCSI Request Block (SRB). // Returns nonzero on success, 0 on error. int aspi_func(aspi_req_t _FAR *ar) { aspi_req_t far *arptr; // real mode pointer int retval = 0; if (f_installed) { // ASPI manager initialized if ((arptr = (aspi_req_t far *) MaptoReal(ar)) != NULL) { // got a valid real mode pointer retval = AspiCall(ASPIRoutine, arptr); // call ASPI through DPMI } } return(retval); } // ---------------------------------------------------------------------- // Routine to map protected mode pointer to real mode. // Usage: void far *MaptoReal(void far *pptr); // Returns real mode pointer on success, NULL on error. void far *MaptoReal(void far *pptr) { WORD sel; // protected mode selector void far *ptr = NULL; // real mode pointer sel = FP_SEG(pptr); // get selector value if (sel == LOWORD(dwPtr)) { // found matching selector ptr = MAKELP(HIWORD(dwPtr), FP_OFF(pptr)); // build real mode pointer } return(ptr); } // ---------------------------------------------------------------------- // Call ASPI manager through DPMI server. // Usage: int AspiCall(void far *aspiproc, aspi_req_t far *ar); // Returns 1 on success, 0 otherwise. int AspiCall(void far *aspiproc, aspi_req_t far *ar) { RealCall_t rcs; // real mode call struct int retval = 0; int npush; void far *sptr; memset(&rcs, 0, sizeof(RealCall_t)); // clear call structure rcs.cs = HIWORD(aspiproc); // point to real mode proc rcs.ip = LOWORD(aspiproc); npush = sizeof(aspi_req_t far *) / sizeof(WORD); // words to pass on stack sptr = (void far *) &rcs; // far pointer to call structure asm { push di // save registers push es sub bx, bx // don't reset A20 line mov cx, npush; // number of words to copy to stack les di, sptr // point ES:DI to call structure mov ax, REAL_CALL // load function number push WORD PTR [ar + 2] // push SRB address push WORD PTR [ar] int DPMI_INT // call DPMI server pop ax // restore stack count pop ax pop es // restore registers pop di jc asm_exit // DPMI error mov retval, 1 // return 1 for success } asm_exit: return(retval); } End Listings
Copyright © 1994, Dr. Dobb's Journal