An abstraction layer makes a convenient interface between device drivers and I/O hardware.
Many operating system vendors don't present a uniform interface between the device drivers and the I/O hardware. When you consider the vast number of I/O cards on the market today, the reason is obvious: writing drivers for each and every one is a task too daunting for any one organization to take on. So while a single-board computer (SBC) will typically come with software that ensures operation with the operating system—usually a board support package (BSP)—customers are forced to write or port device drivers for off-board peripherals.
This article shows how you can apply an abstraction layer to the problem of device drivers for SBCs, with a common set of routines that interface the BSP and device driver. The routines enable you to write a device driver without knowing the specific BSP, underlying hardware, or processor type.
There's no question that by using an abstraction layer between device drivers and BSPs, developers and customers save time and money on the integration. Interfacing a device driver to a BSP abstraction layer enables greater software reuse by both the vendor and the customer. Delivering one standard set of device drivers supporting all BSPs eliminates the need for recompiling to support the customer's hardware. An abstraction layer also enables customers to cost-effectively evaluate many different I/O products, operating systems, and SBCs, something they couldn't otherwise do without a large investment in software, hardware, and time.
Time is money
Embedded systems have become extremely complex, with hardware ranging from SBCs to motherboards to I/O devices in various form factors and protocols, and a plethora of operating systems, device drivers, and applications often running in a networked environment.
To enable this mix of software and hardware to work seamlessly, vendors use an abstraction layer that acts as an interface between SBCs or motherboards and the operating system or a BSP. The abstraction layer includes software routines that allow the operating system to manipulate the hardware. An extension of the operating system, BSPs handle access to hardware resources, such as memory, I/O, interrupts, and device registers.
Absent from most BSPs, however, is a set of well-defined routines that allow the operating system, SBC, and motherboard to communicate with add-on I/O hardware. There are just too many different I/O devices on the market for anyone to write drivers for all of them. So instead, operating-system vendors leave the task of I/O driver development to the I/O card manufacturer or customer, a job that can take weeks or months.
Writing device drivers requires an in-depth knowledge of the operating-system internals, application programming interface (API) and bus protocols. It also requires an in-depth knowledge of kernel-level debugging to ensure that the code activates I/O hardware directly and that it correctly interfaces to another software layer to drive the I/O hardware. The device drivers developed by most vendors are compiled for a specific manufacturer's BSP. A different SBC with a new BSP means the device driver must be ported, recompiled, and tested on the new board.
Spending time and money on a huge integration effort is a luxury few can afford. Rapid advancements in hardware development also make “time to market” critical as the original setup could become obsolete before the designer is even finished with the project, forcing a rewrite of applications and drivers and the eventual management of different versions of code. Off-the-shelf options for everything are becoming a necessity. That means a complete solution calls for an across-the-board approach with real-time operating systems, middleware, applications, and drivers that work together out of the box.
Abstraction layers enable a device driver to interact with a hardware device at a general, or abstract, level rather than at a detailed hardware level. It functions something like an API but resides at the device level, a layer below the standard API level as shown in Figure 1.
The calling program interacts with the device in a more general way than it otherwise would, providing the developer with a key advantage: independence from both the device driver and the BSP. The device driver code doesn't need to know the specifics of the SBC on which it will run; instead it interacts with a layer of software that deals with the computer hardware. Device driver and BSP interoperability is designed into the software and doesn't need to be tested in the system. The device driver will work with any BSP running the abstraction layer. The end customer, consequently, gets a device driver that doesn't need to be recompiled to run on his hardware; only the application-layer functions need to be ported to the specific BSP.
The abstraction layer makes both system and device-driver development easier by isolating the developer from all BSP issues and allowing the device driver to support all possible versions of the BSP without developer intervention. Without the BSP-to-device-driver abstraction layer, the end user operating a large system with multiple I/O cards running on a single CPU card would need to port each I/O card's device driver to the BSP. With the abstraction layer, only the abstraction layer itself is ported to the BSP and it only needs to be done a single time to support all the I/O cards.
An I/O abstraction layer generally deals with low-level hardware and software issues (for example, device-register programming, bus-mastering attributes, and timing issues) to enable developers to write device drivers that support various CPU boards. The device-driver-to-BSP abstraction layer is a thin layer of software that's located on top of the BSP. This abstraction layer defines common routines to handle interrupts, address translations, memory reads and writes, and clocking functions. It's designed to pull out of the device drivers all the specific hardware-dependent routines so the device driver is completely independent of the BSP.
Interrupts are external signals that get the attention of the CPU. When an interrupt occurs, control is transferred to the operating system, which determines the action to be taken. The I/O hardware determines what interrupts it can generate and how they're used by the hardware. The device driver will install an interrupt handler to process the hardware events by calling a routine to connect an interrupt software routine to a specific interrupt vector and perhaps enabling the interrupt bits for that processor. The device driver may also need to know how to disable these bits so the hardware can be ignored by the system. However, few operating systems define a common naming convention or arguments for installing, enabling, and disabling interrupts.
If the interrupt code is transferred to an abstraction layer with no hardware-specific references whatsoever, it can drive a wider variety of I/O hardware (in tandem with the right I/O driver, that is). Listing 1 shows a snippet of C code for VxWorks' “generic” interrupt routines.
Listing 1 C code for VxWork's “generic” interrupt routines
STATUS return_val = OK;
#if defined (PCPENTIUM_BSP)
return_val = pciIntConnect(
(VOIDFUNCPTR *)INUM_TO_IVEC(vector + INT_NUM_IRQ0),
#elif defined (RL4_BSP) return_val = pciIntConnect(
#if defined (MCP750_BSP)
return_val = intConnect(
(VOIDFUNCPTR *)INUM_TO_IVEC(vector + INT_NUM_IRQ0 +
#elif defined (SYNERGY_VSS4_BSP)
return_val = intConnect(
#error Code not written yet
#endif /* PCPENTIUM_BSP, RL4_BSP */
Each BSP defines differently what type of information is in the first argument of the function of the interrupt vector. There's no common definition for interrupt vector; therefore, BSP- dependent code creeps into the device driver. Seemingly benign, once the device driver is compiled with a specific pciIntConnect call, the device driver will not be able to run on all BSPs. Unique object code that connects interrupts in a different way would need to be delivered for each and every BSP. By having the device driver call the BSP-to-device-driver API abstraction-layer routine connect_irq , the BSP is independent. The source code for the connect_irq call is delivered to the customer so that the routine can be compiled and ported to their specific BSP if needed. To facilitate porting, the connect_irq routine in Listing 1 contains four examples of how to port to a specific BSP.
The BSP-to-device-driver API would also need routines to disconnect a C routine from a hardware interrupt, a routine to enable a hardware interrupt on the CPU board, and a routine to disable the hardware interrupt on the CPU board.
All instructions executing at the machine level require a physical memory address or machine address when referencing actual hardware. Machine addresses are derived using table lookups or algorithms. In a direct memory-access architecture that allows a peripheral to read and write to memory without the intervention of a CPU, a memory location accessed by an application maps a virtual address to real (physical) memory. During the course of execution, the same virtual address might be mapped to many different physical addresses as data and programs are paged out and then paged in to other locations.
As you might expect, no common calls assist with address translations between virtual addresses and physical addresses. Developers need an abstraction layer to create device drivers that are not BSP specific. At www.eetimes.com/design/embedded/source-code/4209753/05finseth-zip you can find the C code for VxWorks for a generic routine to convert an address as seen from the processor (local memory address) to the equivalent bus address. The input address is the address as seen by the CPU. The output address is the physical address a device would use to access the CPU address.
The BSP-to-device-driver API will also need a routine to convert a bus address to the equivalent processor address. The routine would take as an input the bus address to convert from a device like a PCI BAR register of VME memory address and return an address the processor can use to access a given hardware bus address. You can also find this code online at www.eetimes.com/design/embedded/source-code/4209753/05finseth-zip.
The two functions, local2bus_adrs and bus2local_adrs illustrate the need for the abstraction layer. Many of the CPU's BSPs don't define any routines to do the address translation, so it's not possible to generate a device-driver object code that is not BSP-dependent. The Motorola MCP750 BSP, for example, defines routines to do the address conversion in the latest version of the BSP, but earlier versions of the BSP had no similar functions. As a result, a device-driver object compiled for one version of the MCP750 will not work on all versions. An abstraction layer between the device driver software and the CPU's BSP would solve this problem.
Clearly, having an abstraction layer to shield the device driver from the CPU's BSP is a tremendous advantage. There are currently more than 450 CPU boards being sold in the embedded industry and each one of those CPUs has multiple versions of its BSP. The actual number of device drivers a vendor would have to develop and test would be gigantic if those device drivers contain BSP-dependent code. If the device drivers are written with the BSP-to-device-driver API interface, the same vendor would have to produce only a single object module for each CPU family.
Reading and writing to memory is also an area where BSP-dependent code can be introduced into the device driver. To create a BSP-independent device driver the abstraction layer must define routines to read and write 8-, 16-, 32-, and 64-bit data locations in memory. A routine to read an 8- and 32-bit data location in PCI I/O space is posted at www.eetimes.com/design/embedded/source-code/4209753/05finseth-zip.
Note that differences between big endian and little-endian processors are compensated for inside the data read 32-bit routines. The abstraction layer will reorganize the data structures to permit big-endian host processor to communicate with the device driver.
Integrating I/O hardware
In short, an abstraction layer defines a common set of routines that allow the device driver to talk to the BSP. The routines enable a device driver to be written without any knowledge of the specific BSP, the underlying hardware on which it runs, or even the processor type. Integrating a new piece of I/O hardware consists of booting the SBC, downloading the driver abstraction layer, and downloading the device driver. The device driver is written to be BSP-independent so no porting or recompiling is required.
An abstraction-layer approach eases the addition of new hardware with a porting file that contains the abstraction routines serving as a basis for the abstraction layer for new hardware. Developers can often use code from a similar SBC as a starting point for their own abstraction layer.
In the complex world of embedded computing with its phenomenal variety of hardware, processors, and operating systems, using an abstraction layer benefits both end user and manufacturer. The end user gets products that work together out of the box—no porting BSPs, no integration effort—saving time and money as well as virtually ensuring integration success. For manufacturers, it means writing one device driver that supports all BSPs. Customer satisfaction increases and support calls decrease, freeing resources to focus on crafting new products. esp
Steven Finseth is software engineering manager at SBS Technologies, where he is responsible for the development of software, including device drivers, application code, and production test software for SBS Technology hardware. Steve spent 14 years as civilian scientist for the U.S. Navy and holds a physics degree from the University of Minnesota, Minneapolis Institute of Technology. You can reach him at .