Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Embedded Systems

Simulation and Device-Driver Development

Eddy Quicksall and Ken Gibson

, January 01, 1997


Dr. Dobb's Journal January 1997: Embedded Systems

Simulation and Device-Driver Development

Eddy and Ken write device drivers at Adaptec's Boulder Technology Center. They can be reached at eddy_quicksall@corp .adaptec.com and ken_gibson@corp .adaptec.com.


When writing device drivers for new hardware designs, you typically have to write and debug the driver concurrently with hardware development, then ship the driver almost immediately after hardware is available. This is difficult because of restricted visibility to drivers running under an operating system and the use of debuggers with limited capabilities. Also, it is difficult to create the variety of stimuli required to test unusual code paths within the driver, such as error handling and command-abort procedures.

One solution to these problems is to write an application-level simulator that, to your device driver, looks just like the operating-system environment it will run in. The simulator causes the routines in your driver to execute in the same ways that they will execute in the real operating system. You can use a fully functional application-level debugger, have complete control over the stimulus provided to the driver to test unusual conditions, and debug in this environment before any hardware is available.

This is possible because a device driver is simply a program that has a few specific entry points, and makes calls to only a predefined set of operating-system-supplied service routines. In addition, the hardware devices being controlled are usually accessed through macros or functions that read and write the control and status registers. The driver writes bit patterns to control registers, which causes the device to set bits in a status register and optionally generate interrupts that, to the driver, look like a call to its interrupt-handler routine. So, the simulator provides these input and output routines that return the correct bit patterns in status registers, and call the driver's interrupt handler at the correct time. The simulator also provides versions of the system-service routines that return realistic return values, and finally, it uses a main processing loop that issues I/O requests to the driver to simulate run-time operation.

To demonstrate this, we present a simulation environment in this article that allows the sample Adaptec AHA-1540 SCSI miniport driver provided with the Windows NT DDK to be built as a Visual C Console application and run under the Visual C debugger. A SCSI miniport is a specific type of Windows 95/NT device driver designed to control SCSI host adapters, such as the AHA-1540. The simulator contains three main parts:

  • A set of routines that simulate loading and initializing a miniport driver under Windows.
  • A main work loop that simulates issuing I/O requests and interrupt notifications to the driver once Windows is up and running.
  • Two routines that simulate the AHA-1540 hardware.

The complete source for the simulator, the full Visual C project (including supporting .h files from the Windows 95/NT DDK), and the sample SCSI-miniport driver from the Windows NT DDK are available electronically; see "Availability," page 3.

Hardware Simulation

The AHA-1540 presents five registers to the processor's I/O space. In a Windows SCSI miniport driver, I/O-mapped registers are accessed using the ScsiPortReadPortUchar and ScsiPortWritePortUchar functions; see Listing One Each takes an I/O address parameter, and the WritePortUchar version takes a data byte to write, while the ReadPortUchar version returns the data byte that was read. We simulate the AHA-1540 by using static variables to remember the contents of the readable registers and by providing functions with these names that return the same values as reads and writes to the real hardware.

The two write-only registers in the AHA-1540 are the Control and the Command /Data Out registers; the three read-only registers are the Status, Data, and Interrupt registers. The write-only Control register contains bits to initiate hard and soft resets, and an interrupt-reset bit. If the interrupt-reset bit is set on a write to the Control register, ScsiPortWritePortUchar clears any interrupt-status bits that may be set in the statusReg variable so that a subsequent call to ScsiPortReadPortUchar will return the correct Status register contents. If the driver sets one of the hard or soft reset bits in the Control register, ScsiPortWritePortUchar first resets the statusReg, dataReg, and interruptReg variables to their initial value, then sets one of the pending bits in the interruptReg. interruptReg is checked by the simulator's main processing loop, which calls the driver's interrupt-handler function when an interrupt is pending.

ScsiPortWritePortUchar must also simulate more complicated AHA-1540 commands such as mailbox initialization and SCSI command execution. To issue SCSI commands to the AHA-1540, the driver builds a SCSI Command Control Block (CCB), places the address of the CCB in a Mailbox Out array, and writes the AC_START_SCSI_COMMAND opcode to the AHA-1540's Command register. Each mailbox entry contains a 1-byte command and a 3-byte CCB address. Mailbox entries are organized into an array of Mailbox Out entries for delivering CCBs to the AHA-1540 followed by an equal-size array of Mailbox In entries for delivering completed commands back to the driver. When the driver initializes, it allocates the Mailbox In and Out arrays and tells the AHA-1540 where it is located by writing the AC_MAILBOX_INITIALIZATION opcode to the Command/ Data Out register, followed by the number of mailboxes in each array, followed by the 3-byte base address of the mailbox array.

When ScsiPortWritePortUchar detects the Mailbox Initialize opcode, it sets the inInitMbox flag to True and the dataCount variable to four to remember that the next four bytes written to the Data Out register will be the mailbox array size and base address. ScsiPortWritePortUchar saves the array size in its mBoxCount variable and collects the three bytes of base address into its pMailboxOut pointer and calculates the offset to pMailboxIn. ScsiPortWritePortUchar can then respond to Start SCSI Command opcodes. It retrieves the CCB pointer out of the Mailbox Out, updates the status field in the CCB, places the CCB pointer in the Mailbox In array and sets the interrupt pending bit in interruptReg to post a completion interrupt back to the driver.

ScsiPortReadPortUchar is the complementary function that simulates reads of the AHA-1540's read-only registers. When the driver reads the Status and Interrupt registers, ScsiPortReadPortUchar just returns the contents of the statusReg or interruptReg variable. The Data In register is more complicated to simulate because it may return strings of information through multiple reads. For example, the Adapter Inquiry command causes the AHA-1540 to return four bytes of configuration information. The simulator uses static arrays that contain the sequences of bytes returned by such commands. When ScsiPortWritePortUchar detects an Adapter Inquiry, it points dataRegPtr to the beginning of the adapterInquiryData array and sets dataCount to four. Then, each time ScsiPortReadPortUchar is called, it returns the byte referenced by dataRegPtr, then increments dataRegPtr and decrements dataCount.

Driver Initialization

The next major portion of the simulator is the code to simulate loading and initializing the driver under Windows. All device drivers must have an exported, global function that the operating system can call to load and initialize the driver, and the first step in the simulator's main routine is to call this entry point. For a miniport driver, this is called DriverEntry and is contained in aha154x.c in the sample driver provided in the NT DDK. A miniport driver's DriverEntry must allocate and initialize a data structure of type HW_INITIALIZATION_DATA as defined in srb.h, which is provided by the Windows DDK. This structure contains pointers to the rest of the driver's entry points, fields to specify types of memory required, as well as various configuration parameters. The driver passes the structure HW_INITIALIZATION_DATA to Windows through a callback to a function called ScsiPortInitialize. This begins a sequence of calls back and forth between Windows and the driver, during which they cooperate to locate and initialize all AHA-1540 adapters in the system.

Our simulator's ScsiPortInitialize function duplicates this calling sequence from the point of view of the driver being tested. It first looks at the HW_INITIALIZATION_DATA to verify that the driver provided function pointers for required entry points. It then allocates blocks of memory for the driver based on values specified in the HW_INITIALIZATION_ DATA. ScsiPortInitialize allocates an extension that will be provided along with each SCSI Request Block (SRB): a Logical Unit extension for use by the driver to store information about each device connected to the SCSI bus, and a HW device extension to store variables for each AHA-1540 adapter in the system. We simulate only one adapter with one SCSI device attached and only need to allocate one of each extension. Next, since the HW_INITIALIZATION_DATA is provided by the driver as a stack variable, the simulator makes a static copy for later reference.

The next step in ScsiPortInitialize is to allocate a PORT_CONFIGURATION_ INFO structure that is also defined in srb.h and initialize it with configuration information, such as the type of system bus, the interrupt type, and assorted SCSI configuration information. One of the fields in this structure is a pointer to an ACCESS_RANGE structure that contains a base address and length for a block of I/O registers. ScsiPortInitialize allocates an ACCESS_ RANGE structure for the driver so that it can report the location of the AHA-1540 registers when it finds one. ScsiPortInitialize then calls back into the driver's HwFindAdapter entry point, which looks for its register signature at all the possible base I/O addresses where an AHA-1540 may reside. In the simulator, this causes calls to ScsiPortReadPortUchar as previously described, which will return the power-on initialization values for AHA-1540 registers.

When the driver finds an AHA-1540 in HwFindAdapter, it fills in the base address and length in the ACCESS_RANGE structure referenced by HW_INITIALIZATION_DATA and then returns to ScsiPortInitialize with a return value indicating that an adapter was found. ScsiPortInitialize then passes the structure into the driver's HwInitialize routine. The miniport driver's HwInitialize routine performs all the steps required to prepare it for run-time operation. This involves many calls into ScsiPortWritePortUchar and ScsiPortReadPortUchar to reset the AHA-1540, obtain hardware configuration information, set operational parameters, and initialize the mailboxes.

Once the AHA-1540 is initialized, HwInitialize returns to ScsiPortInitialize, which returns back up the call chain through DriverEntry and then back to Windows in the real environment or, in our simulation environment, back to the main routine. The driver is now loaded and initialized and the main routine can now start issuing SCSI commands.

System Services

One set of services used by drivers are routines to allocate physical memory and to convert between physical and virtual addresses. In our simulator, these routines are simple because the driver and the simulator being tested execute in the same application memory space. ScsiPortGetPhysicalAddress, ScsiPortGetVirtualAddress, and ScsiPortConvertUlongToPhysicalAddress are the address-conversion routines for Windows miniport drivers. The simulator treats all addresses as pointers in C, and no conversion is required. Similarly, ScsiPortGetUncachedExtension, which allocates page-locked physical memory in a Windows miniport driver, simply mallocs the requested amount of memory and returns a pointer to it. In some NT systems, even I/O space can be mapped through virtual memory so Windows provides the ScsiPortGetDeviceBase to perform the conversion; but again, in simulation, no conversion is required.

Other system services include the ScsiPortStallExecution service, which normally delays for some number of microseconds; however, since real-time has no meaning in simulation, it simply returns. ScsiPortMoveMemory is a service for quickly copying memory, and ScsiDebugPrint prints to a debug port in the driver. In our simulator, this calls printf if you define DBG in the Visual C project settings. ScsiPortLogError sends an entry to the error log on NT. When debugging in the simulator, this can log events to a file or print a message to the screen.

Main Routine

Once the driver is initialized, you can simulate issuing SCSI I/Os. When Windows wants to issue a command to a SCSI device, it builds a SCSI Request Block (SRB) structure as defined in srb.h and passes it to the miniport driver through a call to its HwStartIo function. Our simulator allocates several SRBs and uses an array of pointers to the SRBs, called the pSrbWorkList, that can be quickly reordered in an editor to modify the workload provided to the driver. The main processing loop steps through the pSrbWorkList issuing each SRB to the driver. As each SRB is issued, the simulator increments its testsToGo variable to record how many SRBs are outstanding. After each SRB is issued, the miniport driver calls back to Window's ScsiPortNotification function to indicate that it is ready to process another SRB. The simulator keeps a Boolean nextSrbOk flag that it sets False after issuing an SRB. When the driver calls the simulator's ScsiPortNotification function, it resets nextSrbOk to True.

After each SRB is issued, the main loop checks interruptReg to see if an interrupt should be posted back to the driver. If an interrupt is supposed to be posted, the main loop calls the driver's interrupt entry point. The miniport driver's interrupt handler detects a completed SCSI command in its mailbox and calls back into ScsiPortNotification with a notification type of RequestComplete. Our simulated ScsiPortNotification decrements testsToGo to indicate that an SRB has completed.

To exercise handling one I/O at a time, ScsiPortWritePortUchar immediately returns each CCB back to the driver and sets the pending bit in interruptReg as soon as it receives a command. To test more-complicated overlapped I/O, the simulator can queue up some number of CCBs before returning them and setting the interrupt-pending bit. To handle a case such as this, the main routine continues to loop, calling the driver's interrupt entry after issuing the entire pSrbWorkList until testsToGo equals zero.

This kind of flexibility lets you force interrupts at known, inopportune times and create conditions in the driver that would be difficult to reproduce when running under the real operating system. Also, since the simulator is synchronized by the main loop, it is easy to repeat the conditions that exercise bugs in driver code multiple times until all the bugs are fixed.

DDJ

Listing One

//       ScsiPortWritePortUchar and ScsiPortReadPortUchar// These functions are used by the driver to read and write to I/O mapped 
// registers. Routines are used to simulate the Adaptec 1540. Writing a 
// control register initiates some action in the hardware. Reading returns 
// appropriate status bits.
//
VOID ScsiPortWritePortUchar( IN PUCHAR Port, IN UCHAR Value)
{
    switch( (UINT)Port )  {
        case CTRL_STAT_REG:
            if( Value & IOP_INTERRUPT_RESET ) {
                // Clear any pending interrupt
                interruptReg = 0;
            }
            if( Value & IOP_HARD_RESET ) {
                statusReg = (IOP_SCSI_HBA_IDLE
                             +IOP_MAILBOX_INIT_REQUIRED);
            }
            break;
        case CMD_DATA_REG:
            if( statusReg & IOP_COMMAND_DATA_OUT_FULL ) {
                // Already have a command, ignore like 1540 does
                break;
            }
            else  {
                if( inInitMbox ) {
                    switch( dataCount-- ) {
                        case 4:
                            // First byte tells number of mailboxes
                            mBoxCount = Value;
                            break;
                        case 3:
                            //Third byte of the mailbox address
                            *(ULONG *)&pMailboxOut |= (Value << 16);
                            break;
                        case 2:
                            *(ULONG *)&pMailboxOut |= (Value << 8);
                            break;
                        case 1:
                            *(ULONG *)&pMailboxOut |= Value;
                            pMailboxIn = (MBI*)(pMailboxOut
                                                + mBoxCount);
                            interruptReg |= IOP_COMMAND_COMPLETE
                                          | IOP_ANY_INTERRUPT;
                            inInitMbox = FALSE;
                            break;
                        default:
                            break;
                    }
                } else if (readSetupData) {
                    // Setup to return the number of bytes requested
                    // by the driver.
                    readSetupData = FALSE;
                    dataCount = Value;
                    intOnDataComplete = TRUE;
                    dataRegPtr = adapterSetupData;
                    statusReg |= IOP_DATA_IN_PORT_FULL;
                    adapterSetupData[4] = mBoxCount;
                    adapterSetupData[5] = (UCHAR)(((ULONG)pMailboxOut
                                          >> 16) & 0xFF);
                    adapterSetupData[6] = (UCHAR)(((ULONG)pMailboxOut
                                          >>  8) & 0xFF);
                    adapterSetupData[7] = (UCHAR)(((ULONG)pMailboxOut) & 0xFF);
                }
                else if ( dataCount > 0 ) {
                    // Just Throw away initialization bytes
                    if (--dataCount==0) {
                        interruptReg |= IOP_COMMAND_COMPLETE
                                      | IOP_ANY_INTERRUPT;
                    }
                } else {
                  statusReg &= ~IOP_INVALID_COMMAND;
                  switch( Value ) {
                    case AC_ADAPTER_INQUIRY:
                        // Set up to return 4 bytes of Adapter Data
                        // through the Command/Data register
                        dataRegPtr = adapterInquiryData;
                        dataCount = 4;
                        intOnDataComplete = TRUE;
                        statusReg |= IOP_DATA_IN_PORT_FULL;
                        break;
                    case AC_RET_CONFIGURATION_DATA:
                        // Set up to return 3 bytes of Config Data
                        // through the Command/Data register
                        dataRegPtr = adapterConfigData;
                        dataCount = 3; // 3 bytes of config data
                        intOnDataComplete = TRUE;
                        statusReg |= IOP_DATA_IN_PORT_FULL;
                        break;
                    case AC_RETURN_SETUP_DATA:
                        readSetupData = TRUE;
                        break;
                    case AC_SET_HA_OPTION:
                        dataCount = 2;
                        intOnDataComplete = TRUE;
                        break;
                    case AC_SET_MAILBOX_INTERFACE:
                        dataCount = 2;
                        intOnDataComplete = TRUE;
                        break;
                    case AC_GET_BIOS_INFO:
                        // 1540 returns 2 bytes of BIOS info
                        dataRegPtr = adapterBiosData;
                        dataCount = 2;
                        intOnDataComplete = TRUE;
                        statusReg |= IOP_DATA_IN_PORT_FULL;
                        break;
                    case AC_MAILBOX_INITIALIZATION:
                        // Driver provides number of mailboxes and
                        // the 3-byte physical address
                        inInitMbox = TRUE;
                        dataCount = 4;
                        statusReg &= ~IOP_DATA_IN_PORT_FULL;
                        break;
                    case AC_SET_TRANSFER_SPEED:
                    case AC_SET_BUS_ON_TIME:
                    case AC_SET_BUS_OFF_TIME:
                        // Driver is going to give 1540 one byte of
                        // SCSI initialization parameters.
                        dataCount = 1;
                        break;
                    case AC_SET_SELECTION_TIMEOUT:
                        // Driver will give 4 bytes of SCSI bus
                        // initialization parameters
                        dataCount = 4;
                        break;
                    case AC_START_SCSI_COMMAND:
                        // Simulate executing a SCSI Command
                        {
                            int i, j;
                            CCB *pCmdBlock;
                            MBO *pMbo;  // Mailbox Out
                            MBI *pMbi;  // Mailbox In


</p>
                            // Find the new SCSI Command in the Mailbox
                            pMbo = pMailboxOut;
                            for( i=0; i<mBoxCount; ++i, ++pMbo ) {
                                if( pMbo->Command == MBO_START ) {
                                    pCmdBlock = (CCB*)
                                        (pMbo->Address.Lsb +
                                         (pMbo->Address.Mid<<8) +
                                         (pMbo->Address.Msb<<16));
                                    // For now just report good status
                                    // and return to the driver
                                    pCmdBlock->HostStatus =CCB_COMPLETE;
                                    pCmdBlock->TargetStatus = 0;
                                    pCmdBlock->DataLength.Lsb
                                        = pCmdBlock->DataLength.Mid
                                        = pCmdBlock->DataLength.Msb=0;
                                    pMbo->Command = MBO_FREE;


</p>
                                    // Post the mailbox back to the driver
                                    pMbi = pMailboxIn + mbiIndex;
                                    for( j=0; j<mBoxCount; ++j ) {
                                        if( pMbi->Status == MBI_FREE )
                                        {
                                            pMbi->Status =MBI_SUCCESS;
                                            pMbi->Address.Lsb
                                                = (UCHAR)((ULONG)
                                                pCmdBlock & 0xFF);
                                            pMbi->Address.Mid
                                                = (UCHAR)((ULONG)
                                                pCmdBlock>>8 & 0xFF);
                                            pMbi->Address.Msb
                                                = (UCHAR)((ULONG)
                                                pCmdBlock>>16 & 0xFF);
                                            mbiIndex
                                             = (mbiIndex+1)%mBoxCount;
                                            interruptReg
                                                  = IOP_MBI_FULL
                                                  | IOP_ANY_INTERRUPT;
                                            break;
                                        }
                                        mbiIndex = (mbiIndex+1)%mBoxCount;
                                        pMbi = pMailboxIn + mbiIndex;
                                    }
                                }
                            }
                        }
                        break;
                    default:
                        // This is an invalid command
                        interruptReg |= IOP_COMMAND_COMPLETE
                                      | IOP_ANY_INTERRUPT;
                        statusReg |= IOP_INVALID_COMMAND;
                        break;
                  } // end switch
                } // end else
            }
            break;
        default:
            break;      // Interrupt register not written
    }
}
UCHAR ScsiPortReadPortUchar( IN PUCHAR Port)
{
    UCHAR   retVal;
    // Just return the current contents of the appropriate register
    switch( (UINT)Port ) {
        case CTRL_STAT_REG:
            return statusReg;
        case INT_REG:
            return interruptReg;
        case CMD_DATA_REG:
            if( dataCount > 0 ) {
                retVal = *(dataRegPtr++);
                if( --dataCount == 0 ) {
                    // No more data, clear DATA IN PORT FULL
                    statusReg &= ~IOP_DATA_IN_PORT_FULL;
                    if( intOnDataComplete )  {
                        // Command is complete, post an interrupt
                        interruptReg |= IOP_COMMAND_COMPLETE
                                      | IOP_ANY_INTERRUPT;
                    }
                } else {
                    // Immediately indicate next byte is ready
                    statusReg |= IOP_DATA_IN_PORT_FULL;
                }
            } else {
                retVal = dataReg;
            }
            return retVal;
        default:
            // Driver is accessing some register that is not part of
            // the adapter.  May want to flag an error since this
            // could be a bug in the driver.  We will just return -1
             return 0xFF;
    }
}
// Read a memory location (the BIOS for our driver)
UCHAR ScsiPortReadRegisterUchar( IN PUCHAR Register )
{
    return 0;   // Give 0 since there is no BIOS in simulation
}

Back to Article


Copyright © 1997, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.