- SCSI Controllers and Peripherals
SCSI 컨트롤러 및 주변기기(SCSI Controllers and Peripherals)
You can write drivers for both SCSI controllers and SCSI peripherals with the Driver Kit.
Drivers for SCSI controllers should generally be implemented as subclasses of IOSCSIController. Drivers for SCSI devices are indirect drivers that are typically implemented as subclasses of IODevice. These indirect drivers use the IOSCSIControllerExported protocol to communicate with the SCSI controller driver object, which must conform to the IOSCSIControllerExported protocol. (Required protocols and the role they play in connecting drivers are discussed in Chapter 2.)
Figure 3-1 illustrates the position of SCSI driver classes in the Driver Kit class hierarchy.
For more information on writing a SCSI driver, see the IOSCSIController and IODevice class descriptions.
An example of a SCSI controller driver is located in /NextDeveloper/Examples/DriverKit/Adaptec1542B. For an example of a SCSI tape drive controller, see NextDeveloper/Examples/DriverKit/SCSITape.
The following hardware is required or recommended for development and support efforts:
- A workstation with NEXTSTEP User and Developer software
- A second NEXTSTEP workstation with NEXTSTEP User software. This is strongly recommended: It's virtually guaranteed that you'll corrupt your disk. It's essentially mandatory if you're developing a boot driver. Furthermore, the second station allows you to debug the loaded driver at source level. Set up a procedure to quickly recover the contents of your disk.
- SCSI Host adapter
- Peripherals for testing the adapter: hard disk, CD-ROM, tape drive
- SCSI analyzer
Basic SCSI Controller Driver Operations
The basic operations needed for a SCSI driver are the following:
- Instantiating and initializing a driver object
- Initiating commands
- Handling interrupts and command completion
- Handling timeouts
Instantiating and Initializing a Driver Object
Override IODevice's probe: and initFromDeviceDescription: methods.
Implement probe: to test for system resources such as I/O ports and to verify the presence of hardware. If the hardware is present, create a driver instance and return YES. If invalid values are found during verification, probe: shouldn't create an instance; it should instead send an appropriate diagnostic message and return NO.
Your initFromDeviceDescription: method must invoke super's implementation:
IOSCSIController's initFromDeviceDescription: method starts up the default I/O thread provided by IODevice and initializes its instance variables. Your initFromDeviceDescription: method should initialize the hardware state and software structures such as queues and locks.
Implement resetSCSIBus (in the IOSCSIControllerExported protocol) to reset the SCSI bus for your hardware.
Implement executeRequest:buffer:client: (also in the IOSCSIControllerExported protocol). This exported method should convert the command and data (in the IOSCSIRequest struct passed to it) into the format for the specific hardware and place it in a command buffer. Enqueue the buffer in some well-known location--a queuing instance variable you define in your subclass, for example. Send a Mach message with the ID IO_COMMAND_MSG to the I/O thread's interrupt port to notify the I/O thread that it should execute a command that's been placed in global data. Wait for the command to complete; you can synchronize this with the I/O thread by using an NXConditionLock object in the command buffer. (For example, you set the lock to a CMD_READY state and then do a lockWhen:CMD_COMPLETE. The I/O thread sets the lock state to CMD_COMPLETE when it's done. See the example in Chapter 2.) Return SCSI and driver status.
The commandRequestOccurred method is invoked by the I/O thread when it receives a Mach message with the ID IO_COMMAND_MSG. Implement this method to dequeue all commands that have been queued for execution. Send them to the host adapter, using the private methods and functions that you implement for your hardware. If the host adapter isn't able to accept all the enqueued commands, wait until an interrupt message arrives indicating that the host adapter has completed commands previously sent to it and may now accept more commands.
Handling Interrupts and Command Completion
When the I/O thread receives a message with the ID IO_INTERRUPT_MSG, it invokes the interruptOccurred method against the driver instance. Your implementation of this method should find all commands that the host adapter has completed, mark their respective command buffers complete, and dequeue them. It should reinvoke the commandRequestOccurred method to process any remaining enqueued commands.
Just before the I/O thread tells the hardware to execute a command, it should call the IOScheduleFunc() function to arrange for a specified timeout function to be called at a certain time in the future. If the timeout function is called, it sends a Mach message with the ID IO_TIMEOUT_MSG to the I/O thread.
The timeoutOccurred method is invoked by the I/O thread if it receives a message with the ID IO_TIMEOUT_MSG. Your implementation of this method should abort pending commands and reset the SCSI bus.
You need to consider a few other issues in implementing a SCSI driver.
Sending Messages to the I/O Thread
During initialization, get the I/O thread's interrupt port:
port = [self interruptPort];
Also get the port's name in the kernel's IPC (inter process communication) name space:
ioTaskPort = IOConvertPort(port, IO_KernelIOTask, IO_Kernel);
Use the msg_send_from_kernel() function to actually send a message from the timeout function or from executeRequest:buffer:client: to the I/O thread. You can't use msg_send() because when a driver executes outside the I/O task, it no longer has send rights to ports that it had in the I/O task. The same applies to any method or function that you specified in a call to IOScheduleFunc().
To specify the buffer allocation alignment restrictions that apply to your driver, all you need to do is implement the IOSCSIControllerExported protocol's method getDMAAlignment, which returns the DMA alignment requirements for the current architecture. This method must fill in all four fields of an IODMAAlignment structure that indicates buffer starting points and total length for reading and writing.
Client drivers can use getDMAAlignment to obtain alignment requirements. They can then use the IOAlign() macro to determine how much memory they really need to allocate. These drivers should do the allocation with allocateBufferofLength:actualStart:actualLength: that allocates well-aligned memory, which is required for calls to executeRequest:buffer:client:.
Mapping Virtual Memory
This is generally not a concern unless the driver itself must touch data, such as in programmed I/O. In these cases, use IOPhysicalFromVirtual() to get the physical address of the desired data. Of course, there's no guarantee that you can access every physical address--you only get a valid physical address if the memory is wired down.
Use IOMapPhysicalIntoIOTask() to create a virtual address in the IOTask's virtual address space. Deallocate this virtual memory by calling IOUnmapPhysicalFromIOTask().
Maximum Data Transfer
If you implement the method maxTransfer, it may simplify your design. Upper layers can use the value returned by this method to determine the maximum data transfer size your driver can handle. They won't try to send commands that attempt to transfer more data than the driver can handle.
A suite of methods such as maxQueueLength are available to return statistics used by iostat and other commands. The example located in /NextDeveloper/Examples/DriverKit/Adaptec1542B illustrates gathering these statistics.
SCSI Peripheral Drivers
To write a SCSI peripheral device driver, create a subclass of IODevice. Use the methods in the IOSCSIControllerExported protocol to allow the SCSI peripheral driver object to talk to the SCSI controller object. Some of this protocol's key methods include:
- executeRequest:buffer:client:, which sends SCSI commands to a peripheral device.
- getDMAAlignment:, which returns DMA alignment requirements.
- allocateBufferOfLength:actualStart:actualLength:, which allocates and returns a pointer to well-aligned memory, required for invoking executeRequest:buffer:client:. It's used with other alignment functions such as IOAlign() and getDMAAlignment:.
- reserveTarget:lun:forOwner: and releaseTarget:lun:forOwner:, which respectively reserve and release a specified target/lun pair.
- resetSCSIBus, which resets the SCSI bus.
Implement the probe: method to get the id of the SCSI controller object from the IODeviceDescription object that's handed to probe: as its parameter. In addition, probe: may send a SCSI INQUIRY command to each target/lun pair on its controller to see if a peripheral supported by the driver is connected to the SCSI bus. For every peripheral it finds, probe: should instantiate a SCSI peripheral driver object.
For an example of a SCSI tape drive controller, see /NextDeveloper/Examples/DriverKit/SCSITape.