Building SCSIsupport into your code
Brian is an independent programmer specializing in device drivers and software for SCSI peripherals. He can be reached at [email protected] 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.
[LISTING ONE]
Copyright © 1994, Dr. Dobb's Journal
// ------ 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