Advanced SCSI Programming Interface, or ASPI, is the glue that binds most major CDR software packages to the CDR drives. With so many different types of hardware out there, it's hard for software developers to add support in for each and every one of them. Adaptec, Inc. took that as a problem and created the ASPI layer that SCSI owners have grown to love.

Think of the following process flow:
Software -> Generic ASPI Layer -> Specific ASPI Interpreter -> SCSI Drive. Because not all drive manufacturers could decide on drivers that all had the same interface, Adaptec made a standard that forced them to agree on one section of the process flow. What the device drivers wanted to do underneath that was up to them. Now, software developers are able to interact with SCSI devices at a pretty high level, leaving the messier of workings to others like Adaptec and Tekram.

This article will attempt to give you an idea about how the makers of CDRWin, FireBurner, BlindRead, CloneCD, and others went about writing their applications to do what they wanted to do.

As a brief warning... there are many people out there who could program circles around me. I don't profess to be a master C/C++ programmer, but I know enough to be dangerous. =) So, if you've got suggestions about how to change things, by all means, let me know, and I'll take them under considerations.

Adaptec used to have their ASPI SDK (Software Development Kit) available for download from their FTP site, but that is no more. It now costs $13.50 USD to obtain. Ugh. I'd recommend a search for "ASPI SDK" on your favorite search engine (such as Google) to see if you can find more information about where one might find what we need. For reference, there's 2 important files for Win32 ASPI Programming: wnaspi32.h (winaspi.h for 16-bit version) and scsidefs.h. Since it appears that I can no longer distribute the files, your best bet is to grab the horribly mutilated .h -> .html files located at those links. All we really need are the header files, as we'll be interfacing with the dynamic link library (dll). If you find better links, please do let me know, thanks!

Any sample code that is provided will probably be fairly straight C, with *possibly* some C++ mixed in. It will be compilable under Microsoft Visual C++. I do not own Borland/Inprise, so I can't check that out. Porting the code shouldn't be a problem. If anyone would like to port the code to their favorite language (straight C++, Delphi/Pascal, etc.), let me know and I'll link your code port.

All source code offered will be under the Open Source GPL, unless otherwise noted. This is for fun and learning, let's keep it that way.

Once you've got your compiler and header files up (header files would help a lot for reference even if you weren't going to compile anything), we'll be ready to start exploring ASPI Programming!

Programming Conventions

Before we begin, I'm going to be using some notations for 32 bit architectures that could be intuitive, or not. Here's a table of them:

Type Size in Bytes Description
Void N/A Nothing; No return or function arguments
Byte 1 Unsigned 8-bit value
Word 2 Unsigned 16-bit value
DWord 4 Double Word; Unsigned 32-bit value
LPVoid 4 Generic 32-bit Pointer
LPByte 4 32-bit Pointer to an array/buffer of BYTEs.
LPSRB 4 Generic SRB Pointer. SRB is a SCSI Request Block.
*Note: The 'LP' notation represents a "Long Pointer".

ASPI Functions -- WNASPI32.DLL

Once we know that, we can get into ASPI functions. As you most likely already know, any PC with a SCSI card needs to have the ASPI Layer installed for proper working action with many SCSI applications, like CDRWin and Fireburner. Once this ASPI Layer is installed, you'll have a WNASPI32.DLL in your windows/system. For those of you still using Win 3.1 and will be using winaspi.dll, you're on your own. There's many tricks for buffer locking that need to take place that probably isn't worth my time going into. So, I'll be concentrating on 32-bit windows, which includes the Win9x and Win2k like of OSs.

WNASPI32.DLL has 5 functions that your program can create hooks into (also called "Entry Points"). These and their uses are:

Entry Point Description
GetASPI32SupportInfo Will initialize ASPI, and return basic information
SendASPI32Command Submits SCSI Request Block (SRB) to ASPI for execution.
GetASPI32Buffer Allocates a buffer for data
FreeASPI32Buffer Releases buffers allocated by GetASPI32Buffer
TranslateASPI32Address Translates the standard ASPI HostAdapter/ID/LUN address triples to/from Win95 DEVNODEs.

Those descriptions are severely lacking, but they fit nicely into a table so I thought I'd use them. Now I'll step through each one and explain what the function arguments are and what their return values are.

DWORD GetASPI32SupportInfo ( VOID );

This function needs no parameters, and returns a single 32-bit Double Word in response.

Response Field 31.....16|15......8|7.......0 _____________________________ | MBZ | Status | Host | | | Code | Adapter| | | | Count | \--------+---------+--------/

MBZ is Must Be Zero. Any properly working ASPI layer will set bits [31:16] to 0x0h. To understand the Status Code, we must go back to one of the header files I mentioned before, wnaspi32.h. If the call is successful, the Status Code returned will either be SS_COMP or SS_NO_ADAPTERS. If SS_NO_ADAPTERS is returned, then the function call properly initialized ASPI, but no Host Adapters were found.

Now, if the function called failed, there could be any number of errors. Peruse the "SRB Status" section of wnaspi32.h for the list. So far, so good!

This next function is where the real work happens. SendASPI32Command( LPSRB ) is the means of telling the SCSI device what to do. Before you continue, take a moment to look at the "ASPI Command Definitions" section of wnaspi32.h.

SendASPI32Command() requires a pointer to a SCSI Request Block, or SRB, which it will use to decode what exactly you're passing along to the SCSI Device. These SRBs are defined exactly, so they must be byte packed. Often, compilers such as Microsoft Visual C++ and Borland/Inprise C++ won't pack them in to help with speed. Here's why:

Let's say you've got a C struct that contains 2 BYTEs and 1 DWORD.

typedef struct
BYTE atapiBYTEs;
BYTE firewireBYTEs;
} MassStorage;

When this struct is used in a program, Visual C++ will optimize for speed and put everything on DWORD (32-bit) boundaries, since Intel processors (386 and up) are 32-bit machines. It will pad the actual data with junk so the processor can use it quicker: (the |, or pipes, are on 8-bit or Byte boundaries)

| junk0 | junk1 | junk2 | atapiBYTEs |
| junk0 | junk1 | junk2 | fireiwireBYTEs |
| DWordUp |

Why is this quicker? Well, because of the way that ia32 has evolved from the 16-bit 8086, and Intel's desire for programming and binary compatibility, the original 16-bit 8086 AX register is the lower 16-bits of the 32-bit register, EAX. Because a lot of flow control was done on byte boundaries, the 8086 could access the high 8-bits or the lower 8-bits of the AX register by checking out the AH and AL registers. Since 32-bit programs can still access the AH and AL registers, MSVC++ will align them with those register locations. Your Pentium III will still load the entire DWORD, but the program will only use what lines up with the AL register. Clever, huh? If the compiler didn't do this, in order to read junk1, the processor would have to perform a right shift by 16-bits of the register for it to line up with an 8-bit boundary.

Unfortunately, ASPI doesn't like this. If it expects the first BYTE to be the command, and the next byte to be the status, and the 3rd byte to be the Host Adapter's ID (see struct SRB_ExecSCSICmd in wnaspi32.h), and the compiler decided to seperate those, the ASPI Layer would die a horrible death. ASPI can't guess what the compiler is going to do on the binary level, so it has to specify it. So, be sure to tell the compiler to byte-pack the SRB definitions with the following code:

// Pack(1) in MSVC++
#pragma pack(1)
// Then turn off packing
#pragma pack()

Piece of cake. In fact, depending on the completeness of a wnaspi32.h you have, all of this is already setup for you in the header file, and surrounded by #ifdef statements depending on which C/C++ compiler you're using.

Anyway, back to SendASPI32Command(). You must first determine what kind of command you'd like to send. According to wnaspi32.h, you can:

1) Query ASPI for Host Adapter Information.
2) Get device type for a SCSI target.
3) Send a SCSI Command (CDB - Command Descriptor Block) to a SCSI target.
4) Abort a SRB.
5) Send a Bus Device Reset to a SCSI target.
6) Return BIOS information for a SCSI target (Win9x only).
7) Rescan the SCSI Bus.
8) Change the Timeout values for commands.

Each of these different functions takes a different command struct. I could examine them all, but I have a feeling that those who are really interested would only read a couple then try to do the rest on their own, and those that aren't interested would only skip over the rest... so I'll do number 1, Query ASPI for Host Adapter Information, then number 2, getting device type for a SCSI target.

But, I'm getting ahead of myself. We still have 3 more WNASPI32.DLL functions to discover!


This function will allocate some storage space for us in Virtual Memory. Normally we could just use VirtualAlloc, but sometimes the space is so physically fragmented that it'll make transfer on bus-mastering host adapters impossible. So, we can use this function. PASPI32BUFF is a pointer to the ASPI32BUFF struct in wnaspi32.h. It needs a DWORD containing the length in bytes of the buffer, a ZeroFill flag that ASPI will use to determine if it should zero fill the buffer (otherwise the buffer will hold whatever data was last put in that spot), and a Reserved DWORD which MBZ. If the function call returns a TRUE (1), then there's a section in the PASPI32BUFF struct that will have the address to the block/array of bytes reserved for your use. If you were writing a CD burning program, you'd create ASPI32Buffers to hold the data you wish to write to CD.


This function takes the same struct as GetASPI32Buffer, but the ZeroFill and Reserved are both MBZ. Now you must set the address that you were originally given plus the size of the buffer/array in bytes that you requested so ASPI can release it back to the OS. If the address or the size is not exactly correct, the function will fail. You've got to keep track of that information in your program!

BOOL TranslateASPI32Address( PDWORD pTriple, PDWORD pDEVNODE )

Here we're getting into some Windows-specific Plug-n-Play topics. In Windows 9x (and I would assume Win2k, but not NT), DEVNODEs are associated with WM_DEVICECHANGE messages. So, it's possible to associate ASPI target addresses with plug-n-play events.

pTriple is an encoding of the standard SCSI HostAdapter/SCSI_ID/LUN triple, stored as 0x00HHIILL, where HH is the Host Adapter, II is the ID, and LL is the LUN. Make pDEVNODE 0x0 if you which to translate the Path to a DEVNODE. After you call the function and it returns TRUE (1), the pDEVNODE will now contain the DEVNODE (even though you originally set it to a 0). If DEVNODE is non-zero, the function will assume that you're giving it a valid DEVNODE to translate to the 0x00HHIILL Triple, and will transform the first argument accordingly. It's messy, but efficient. Want to use DEVNODEs with Plug-n-play events? Go read a Windows plug-n-play book. I only care about the pure SCSI aspects.   Smile

Using SendASPI32Command()

First off, I'll include some code snippits about how to query for Host Adapter Information.


First, we need the proper SRB. Looking through wnaspi32.h, we'll notice that SRB_HAInquiry appears to be the proper one. Once we declare it, we'll need to populate its members with the proper data, and finally, send it.

Of course, before we can do any of this, we'll have needed to initialize the ASPI and make sure Windows has it loaded. Let's do that first:

// MSVC++

/* Global Variables */
HINSTANCE hWNASPI32; // Handle to ASPI for Win32

/* Function Prototypes for WNASPI32.DLL */
DWORD (*GetASPI32SupportInfo)(void);
DWORD (*SendASPI32Command)(LPSRB);
BOOL (*FreeASPI32Buffer)( PASPI32BUFF );
BOOL (*TranslateASPI32Address)( PDWORD, PDWORD );

// Then, inside your Main/WinMain function:

/* Load Real ASPI Layer, WNASPI32.DLL */
hWNASPI32 = LoadLibrary("WNASPI32.DLL");

if(!hWNASPI32) /* If Load Failed */
MessageBox(NULL, TEXT("LoadLibrary(\"WNASPI32.DLL\") Failed."), TEXT("MYSCSIAPP Failure!"), MB_ICONERROR);
return FALSE;

GetASPI32SupportInfo = (DWORD (*)(void)) GetProcAddress(hWNASPI32,"GetASPI32SupportInfo");
SendASPI32Command = (DWORD (*)(void *)) GetProcAddress(hWNASPI32,"SendASPI32Command");
GetASPI32Buffer = (BOOL (*)(void *)) GetProcAddress(hWNASPI32,"GetASPI32Buffer");
FreeASPI32Buffer = (BOOL (*)(void *)) GetProcAddress(hWNASPI32,"FreeASPI32Buffer");
TranslateASPI32Address = (BOOL (*)(void *)) GetProcAddress(hWNASPI32,"TranslateASPI32Address");

if( !GetASPI32SupportInfo || !SendASPI32Command )
MessageBox(NULL, TEXT("Cannot find critical WNASPI32.DLL Functions!"), TEXT("Fatal BSASPI32 Error!"), MB_ICONERROR);
return FALSE;


The 2 functions we absolutely need are GetASPI32SupportInfo() and SendASPI32Command(). If they're not found, we can't go any further. Older ASPIs only had these 2 functions. Newer ones have all 5.

So, now we have access to the functions. We've now got the tools to interact with SCSI devices!

Now, let's use GetASPI32SupportInfo() to figure out what our SCSI subsystem looks like.

DWORD SupportInfo;
SupportInfo = GetASPI32SupportInfo();
HACount = HIBYTE(LOWORD(SupportInfo));
ASPIStatus = LOBYTE(LOWORD(SupportInfo));
// TODO:
// If ASPIStatus != SS_COMP and != SS_NO_ADAPTERS, you must handle the error.

That's it! We now know how many SCSI Host Adapters are in our system, and we know the Status of the ASPI API. We're all ready to query a Host Adapter to find out more about what's connected to that system.

To do that, we need the use of the SRB_HAInquiry struct from wnaspi32.h

typedef struct // Offset
{ // HX/DEC
BYTE SRB_Cmd; // 00/000 ASPI command code = SC_HA_INQUIRY
BYTE SRB_Status; // 01/001 ASPI command status byte
BYTE SRB_HaId; // 02/002 ASPI host adapter number
BYTE SRB_Flags; // 03/003 ASPI request flags
DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0
BYTE HA_Count; // 08/008 Number of host adapters present
BYTE HA_SCSI_ID; // 09/009 SCSI ID of host adapter
BYTE HA_ManagerId[16]; // 0A/010 String describing the manager
BYTE HA_Identifier[16]; // 1A/026 String describing the host adapter
BYTE HA_Unique[16]; // 2A/042 Host Adapter Unique parameters
WORD HA_Rsvd1; // 3A/058 Reserved, MUST = 0
SRB_HAInquiry, *PSRB_HAInquiry, FAR *LPSRB_HAInquiry;

Let's consider for a moment that HACount from the SupportInfo section of our code returned 2. That means it's safe to say that we can query Host Adapter #1. We'll do that now.

SRB_HAInquiry HAInquiry; // Create a data struct for our command.
memset( &HAInquiry, 0, sizeof(SRB_HAInquiry) ); // Zero out the SRB
HAInquiry.SRB_HaId = 1;
SendASPI32Command( (LPSRB)&HAInquiry ); // Give it a pointer to our packet

Now, the ASPI manager will query Host Adapter 1 with the packet we constructed. Next we'll check the SRB_Status byte to determine if it succeeded or not. ASPI will set it to SS_COMP if it succeeded.

if( HAInquiry.SRB_Status != SS_COMP )
{ //Handle errors here

We've now got access to all kinds of information about our host adapter, such as its SCSI ID (usually 7), the manager ID("ASPI for Win32"), the Host Identifier(Adaptec, Tekram, etc.), and some unique parameters. Feel free to experiment and view these strings and values. For information about the important Unique parameters, consult Adaptec's ASPI Technical Reference (aspi32.pdf).

So far all we've been talking to is the ASPI interface itself and the Host Adapter. Now we'll fiddle around with sending a packet to a particular ID on the Host Adapter.

We'll need to send a SC_GET_DEV_TYPE to all SCSI IDs on the bus to determine exactly what's connected. The header file scsidefs.h and the ASPI Technical Reference will give you a listing of those.

I happend to know that my Plextor UltraPlex 40x CD-ROM is hooked up to SCSI ID 6. Normally you'll want to query every ID inside every adapter, but since this is just an example, I'll leave it up to you to modify the code for it.

typedef struct // Offset
{ // HX/DEC
BYTE SRB_Cmd; // 00/000 ASPI command code = SC_GET_DEV_TYPE
BYTE SRB_Status; // 01/001 ASPI command status byte
BYTE SRB_HaId; // 02/002 ASPI host adapter number
BYTE SRB_Flags; // 03/003 Reserved, MUST = 0
DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0
BYTE SRB_Target; // 08/008 Target's SCSI ID
BYTE SRB_Lun; // 09/009 Target's LUN number
BYTE SRB_DeviceType; // 0A/010 Target's peripheral device type
BYTE SRB_Rsvd1; // 0B/011 Reserved, MUST = 0

Here's our SRB for SC_GET_DEV_TYPE. Notice we have to set the command, HA number, the ID, and the LUN (Logical Unit Number). Let's do that.

memset( &GDEVBlock, 0, sizeof( SRB_GDEVBlock ); // Zero out all fields
GDEVBlock.SRB_HaId = 1; // Host Adapter
GDEVBlock.SRB_Target = 6; // SCSI ID

SendASPI32Command( (LPSRB)&GDEVBlock );

// Now, check the SRB_Status field to see if it completed properly with a SS_COMP.
if( GDEVBlock.SRB_Status != SS_COMP )
{ // Handle Error Code
else // No error
if( GDEVBlock.SRB_DeviceType == DTYPE_CDROM )
{ // It found it!
printf("I found a CD-ROM on Host Adapter %d, at SCSI ID %d",
GDEVBlock.SRB_HaId, GDEVBlock.SRB_Target );


Good stuff, huh? Well, next time, we'll take a look at what it takes to create an ASPI 'spy'. We'll be making a DLL that other programs such as FireBurner can hook into and log their commands. Our ASPI 'spy' will, of course, pass the commands to the real ASPI Layer once we've logged the information, so it'll be seamless. This will give us a better idea about how to command a SCSI bus and the devices it connects.

Until then, play around with some code that will initialize ASPI, and scan all host adapters and all IDs, and print out devices as it finds it. Have fun!