June 92 - Creating PCI Device Drivers
Creating PCI Device Drivers
MARTIN MINOW
The new PCI-based Power Macintosh computers bring with them a subset of the
functionality to be offered by the next generation of I/O architecture. New support for
device drivers makes it possible to develop forward-compatible drivers for PCI
devices, while at the same time making them much easier to write and eliminating
their dependence on the Macintosh Toolbox. Key features of the new driver model are
described in this article and illustrated by the accompanying sample PCI device driver.
Writing Macintosh device drivers has always been something of a black art. Details of
how to do it are hidden in obscure places in the documentation and often discovered only
by developers willing to disassemble Macintosh ROMs and system files. But this art
that's flourished for more than a decade is about to get a lot less arcane.
The PCI-based Power Macintosh computers are the first of a new generation of
computers with support for a driver model that's independent of the 68000 processor
family and the Macintosh Toolbox. Existing 680x0 drivers will continue to work on
the PCI machines (although this may not be true for future systems); a third-party
NuBusTM adapter enables the use of existing hardware devices and drivers without
change. But drivers for PCI hardware devices must be written in accordance with the
driver model supported in the new system software release, which makes them
simpler to develop and maintain.
This article will give you an overview of the new device driver model, without
attempting to cover everything (which would fill a book and already has). After
discussing key features, it suggests how you might go about converting an existing
driver to drive a PCI device. The remainder of the article looks at some of the
individual parts of a forward-compatible PCI device driver. The sample code excerpted
here and included in its entirety on this issue's CD offers a complete device driver that
illustrates most of the features of the new driver model. Of course, you won't be able to
use the driver without the hardware, and you'll need updated headers and libraries to
recompile it.
How to write device drivers for PCI-based Macintosh computers is explained in
detail in Designing PCI Cards and Drivers for Power Macintosh Computers .*
KEY FEATURES OF THE NEW DRIVER MODEL
The following list of features will give you some idea of the rationale behind the move
away from a device driver architecture that's served the Macintosh operating system
for more than a decade. Some of these features address problems of the old
architecture, while some anticipate new requirements.
A simplified set of driver services independent of the Macintosh Toolbox
The existing Device Manager design is closely tied to specific features of the
Macintosh Toolbox. The new system software release supports only a small set of
driver services, which are independent of the Toolbox and are limited to just those
things that drivers need to do; they don't let drivers display dialogs, open files, read
resources, or draw on the screen. This greatly simplifies both the driver's task (the
driver interacts only with the actual hardware) and the operating system's task (the
OS needn't have a file system or screen available when starting up drivers).
Independence from the 68000 processor family
The old device driver architecture is highly dependent on specific features of the
680x0 processor architecture. For example, the way code segments are organized and
the conventions for passing parameters depend on the 680x0 architecture and make
the old driver code different from other code modules. This means that drivers can't be
written in native PowerPC code -- or must make use of computationally expensive
mixed-mode switches.
Also, in the 680x0 architecture, critical sections and atomic operations use
assembly-language sequences to disable interrupts. The PowerPC processor has a
completely different interrupt structure, effectively making these techniques
impossible to transport directly to native PowerPC code.
In the new system software, support for the driver model is independent of any
particular processor, hiding processor-specific requirements in operating system
libraries. Drivers can be compiled into native PowerPC code and can be written in a
high-level language such as C. Because they're standard PowerPC code fragments, they
aren't bound by the segment size limitations of the 680x0 architecture; they can be
created with standard compilers and debugged with the Macintosh two- machine
debugger.
A more flexible configuration facility
Driver configuration in the old architecture requires the ability to read resources
from a parameter file, or from a 6-byte nonvolatile RAM area indexed by NuBus slot.
These ad hoc configuration mechanisms based on the Resource Manager, File Manager,
and Slot Manager are replaced in the new system software by a more flexible
configuration facility that's used throughout the system.
Drivers use a systemwide name registry for configuration. Each device has an entry in
the Name Registry containing properties pertinent to that device. Device drivers can
also store and retrieve private properties. Device configuration programs (control
panels and utility applications) should use the registry to set and retrieve device
parameters.
System-independent device configuration
Devices can use Open Firmware to provide operating system configuration as well as
system- independent bootstrap device drivers. Open Firmware is an
architecture-independent IEEE standard for hardware devices based on the FORTH
language. When the system is started up, it executes functions stored in each device's
expansion ROM that provide parameters to the system. A device can also provide FORTH
code to allow the system to execute I/O operations on the device. This means a card can
be used to bootstrap an operating system without having operating system-specific
code in its expansion ROM.
Open Firmware and the bootstrap process are described in detail in IEEE
document 1275 -- 1994 Standard for Boot (Initialization, Configuration) Firmware
.*
Grouping by family
Drivers are grouped into generalfamilies , and family-specific libraries simplify
their common tasks. Currently, four families are defined: video, communications,
SCSI (through SCSI Manager 4.3), and NDRV (a catch-all for other devices, such as
data acquisition hardware). The sample code is for a device driver in the NDRV family.
Direct support for important capabilities
The existing Device Manager doesn't directly support certain capabilities, such as
concurrent I/O (required by network devices) and driver replacement. Driver
writers who need these capabilities have had to implement them independently, which
is difficult, error-prone, and often dependent on a particular operating system
release. The new system software supports these capabilities in a consistent manner.
A choice of storage
Drivers can be stored in the hardware expansion ROM or in a file of type 'ndrv' in the
Extensions folder. A later driver version stored in this folder can replace an earlier
version stored in the hardware expansion ROM.
Forward compatibility
Device drivers written for the new system software will run without modification
under Copland, the new generation of the Mac OS forthcoming from Apple, if they use
only the restricted system programming interface and follow the addressing guidelines
inDesigning PCI Cards and Drivers for Power Macintosh Computers .
For more on Copland, see "Copland: The Mac OS Moves Into the Future" in this
issue of develop .*
CONVERTING AN EXISTING DRIVER
To illustrate how you'd go about converting an existing device driver to drive a PCI
device, let's suppose you've developed a document scanner with an optical character
recognition (OCR) facility. The document scanner is currently controlled by a NuBus
board that you designed, and you're building a PCI board to support the scanner on
future Macintosh machines.
A useful way to approach the conversion effort is to conceptualize the device driver as
consisting of three generally independent layers:
• A high-level component that connects the device driver to the operating
system and processes requests.
• A mid-level component that has the device driver's task-specific
intelligence. For example, this might contain OCR algorithms. This part is
unique to each driver and generally hardware independent.
• The low-level bus interface "hardware abstraction layer" that directly
manipulates the external device and thus is always device dependent.
At the same time, you might also organize the code in each of these three layers into the
following functional groups:
• data transfer operations (Read, Write)
• interrupt service routines
• initialization and termination
• configuration and control (power management, parameterization)
Let's look at what you would do to each of these layers and groups.
First, you would throw out the high-level component in your driver that interacts
with the Device Manager and replace it with the considerably simpler request
processing of the new system software release. You would need to add support for the
Initialize, Finalize, Superseded, and Replace commands (discussed later), as they have
no direct counterpart in the existing Device Manager. You would also need to revise the
way you complete an I/O request: instead of storing values in 68000 registers and
jumping to jIODone, your driver would call IOCommandIsComplete.
The mid-level component in your driver would include scanner management and, in
particular, OCR algorithms. These algorithms comprise the intelligence that sets your
product apart from its competition. To convert your driver to a PCI device driver, you
would recompile (or rewrite) the algorithms for the PowerPC processor. If the
algorithms were in 68000 assembly language, you could get started by making
mixed-mode calls between the new driver and the existing functions; however, this
won't work with Copland, and I would recommend "going native" as soon as possible.
You would replace the low-level bus interface that manipulates registers on a NuBus
card with code that manipulates PCI registers. Because this is specific to a particular
hardware device, it won't be discussed in this article, but the sample driver on the CD
shows you how to access PCI device registers.
You would also create Open Firmware boot code to allow your card to be recognized
during system initialization. Because the new driver model doesn't use Macintosh
Toolbox services, you would have to redesign your driver to (1) use the Name
Registry for configuration instead of resources and parameter files, and (2) use the
new timer services, replacing any dependency on the accRun PBControl call (the
sample code shows how to call timer services, although it's not discussed here).
How your new driver code would look will become clearer in the next sections, where
we examine key parts of the sample device driver. To get the whole picture, see the
sample driver in its entirety on the CD.
The remainder of this article introduces a number of new operating system functions,
as well as a few new libraries, managers, and such. "A Glossary of New Operating
System Terms" will help you navigate through the new territory.
A GLOSSARY OF NEW OPERATING SYSTEM TERMS
CheckpointIO. A function that releases memory that had been configured by
PrepareMemoryForIO.
DoDriverIO. A function provided by the driver that carries out all device driver
tasks. When you build a driver, it must export this function to the Device Manager.
DriverDescription. An information block named TheDriverDescription that the
Driver Loader Library uses to connect a device driver with its associated hardware.
When you build a driver, it must export this block to the Driver Loader Library.
Driver Loader Library. A library of functions used by the Device Manager to
locate and initialize all drivers. It uses the DriverDescription structure to match a
driver with the hardware actually present on a machine.
Driver Services Library. A family-independent library of driver services
limited to just those things that drivers need to do.
Expansion Bus Manager. A library that provides access to PCI configuration
registers.
GetInterruptFunctions. A function that retrieves the current interrupt service
functions established for this device.
GetLogicalPageSize. A function that retrieves the size of the physical page.
Normally called once when the driver is initialized.
InstallInterruptFunctions. A function that replaces the current interrupt
functions with functions specific to this device driver.IOCommandIsComplete. A function that completes the current request by returning the final status to the caller,
calling an I/O completion routine if provided, and starting the next transfer if
necessary.
MemAllocatePhysicallyContiguous. A function that allocates a contiguous block
of memory whose address can be passed, as a single unit, to a hardware device. This is
essential for frame buffers and similar memory areas that must be accessed by both
the CPU and an external device.
Name Registry. A database that organizes all system configuration information. Each
device's entry in the registry contains a set of properties that can be accessed with
RegistryPropertyGet and RegistryPropertyGetSize.
PoolAllocateResident. A function that allocates and optionally clears memory in the
system's resident pool. This replaces NewPtrSys, which isn't available to
forward-compatible PCI device drivers.
PoolDeallocate. A function that frees memory allocated by PoolAllocateResident.
PrepareMemoryForIO. A function that converts a logical address range to a set of
physical addresses and configures as much as possible of the corresponding physical
memory space for subsequent direct memory access.
QueueSecondaryInterrupt. A function that runs a secondary interrupt service
routine at a noninterrupt level.
RegistryPropertyGet, RegistryPropertyGetSize. Functions that retrieve,
respectively, the contents and the size of a property, given its name and a value that
identifies the current Name Registry entity.
Software task. An independently scheduled software module that can call driver
services, including PrepareMemoryForIO. Software tasks can be used to replace
time-based processing that previously used the PBControl accRun service.
SynchronizeIO. A function that executes the processor I/O synchronize ( eieio)
instruction.
A LOOK AT THE SAMPLE DRIVER: CONFIGURATION AND
CONTROL
Now we'll look at key pieces of the sample driver, starting with the code for
configuration and control. As mentioned earlier, the sample driver is a member of the
NDRV family. To the operating system, an NDRV driver is a PowerPC code fragment
containing two exported symbols: TheDriverDescription and DoDriverIO. (Although all
drivers have a TheDriverDescription structure, the particular driver family they
belong to determines which other exported symbols are required.)
TheDriverDescription is a static structure, shown in Listing 1, that provides
information to the operating system about the device that this driver controls. The
driver will be loaded only if the device is present. TheDriverDescription also indicates
whether the driver is controlled by a family interface (such as Open Transport for the
communications family) and specifies the driver name to be used by operating system
functions to refer to it. The Driver Loader extracts TheDriverDescription from the
code fragment before the driver executes; thus it must be statically initialized.
Listing 1. TheDriverDescription
DriverDescription TheDriverDescription = {
/* This section lets the Driver Loader identify the structure
version. */
kTheDescriptionSignature,
kInitialDriverDescriptor,
/* This section identifies the PCI hardware. It also ensures
that the correct revision is loaded. */
"\pMyPCIDevice", /* Hardware name */
kMyPCIRevisionID, kMyVersionMinor,
kMyVersionStage, kMyVersionRevision,
/* These flags control when the driver is loaded and opened,
and control Device Manager operation. They also name the
driver to the operating system. */
( (1 * kDriverIsLoadedUponDiscovery) /* Load at system startup */
| (1 * kDriverIsOpenedUponLoad) /* Open when loaded */