As mentioned earlier in this chapter, writing device drivers is like writing any other program. Only core OS services reside in kernel address space; everything else, including device drivers, resides in process or user address space. This means that a device driver has all the services that are available to regular applications.
Many models are available to driver developers under QNX Neutrino. Generally, the type of driver you're writing will determine the driver model you'll follow. For example, graphics drivers follow one particular model, which allows them to plug into the Screen graphics subsystem, network drivers follow a different model, and so on.
On the other hand, depending on the type of device you're targeting, it may not make sense to follow any existing driver model at all.
This section provides an overview of accessing and controlling device-level hardware in general.
If you're targeting a closed embedded system with a fixed set of hardware, your driver may be able to assume that the hardware it's going to control is present in the system and is configured in a certain way. But if you're targeting more generic systems, you want to first determine whether the device is present. Then you need to figure out how the device is configured (e.g., what memory ranges and interrupt level belong to the device).
For some devices, there's a standard mechanism for determining configuration. Devices that interface to the PCI bus have such a mechanism; each PCI device has a unique vendor and device ID assigned to it. The following piece of code demonstrates how, for a given PCI device, to determine whether the device is present in the system and what resources have been assigned to it:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <hw/pci.h> int main() { struct pci_dev_info info; void *hdl; int i; memset(&info, 0, sizeof (info)); if (pci_attach(0) < 0) { perror("pci_attach"); exit(EXIT_FAILURE); } /* * Fill in the Vendor and Device ID for a 3dfx VooDoo3 * graphics adapter. */ info.VendorId = 0x121a; info.DeviceId = 5; if ((hdl = pci_attach_device(0, PCI_SHARE|PCI_INIT_ALL, 0, &info)) == 0) { perror("pci_attach_device"); exit(EXIT_FAILURE); } for (i = 0; i < 6; i++) { if (info.BaseAddressSize[i] > 0) printf("Aperture %d: " "Base 0x%llx Length %d bytes Type %s\n", i, PCI_IS_MEM(info.CpuBaseAddress[i]) ? PCI_MEM_ADDR(info.CpuBaseAddress[i]) : PCI_IO_ADDR(info.CpuBaseAddress[i]), info.BaseAddressSize[i], PCI_IS_MEM(info.CpuBaseAddress[i]) ? "MEM" : "IO"); } printf("IRQ 0x%x\n", info.Irq); pci_detach_device(hdl); return EXIT_SUCCESS; }
For more information, see the pci_*() functions in the C Library Reference.
Different buses have different mechanisms for determining which resources have been assigned to the device. On some buses, such as the ISA bus, there's no such mechanism. How do you determine whether an ISA device is present in the system and how it's configured? The answer is card-dependent (with the exception of PnP ISA devices).
Once you've determined what resources have been assigned to the device, you're now ready to start communicating with the hardware. How you do this depends on the resources.
Before a thread may attempt any port I/O operations, it must be running at the correct privilege level; otherwise you'll get a protection fault. To get I/O privileges, call ThreadCtl():
ThreadCtl(_NTO_TCTL_IO, 0);
Next you need to map the I/O base address (one of the addresses returned in the CpuBaseAddress array of the pci_dev_info structure above) into your process's address space, using mmap_device_io(). For example:
uintptr_t iobase; iobase = mmap_device_io(info.BaseAddressSize[2], info.CpuBaseAddress[2]);
Now you may perform port I/O, using functions such as in8(), in32(), out8(), and so on, adding the register index to iobase to address a specific register:
out32(iobase + SHUTDOWN_REGISTER, 0xdeadbeef);
Note that the call to mmap_device_io() isn't necessary on x86 systems, but it's still a good idea to include it for the sake of portability. In the case of some legacy x86 hardware, it may not make sense to call it; for example, a VGA-compatible device has I/O ports at well-known, fixed locations (e.g., 0x3c0, 0x3d4, 0x3d5) with no concept of an I/O base as such. You could access the VGA controller, for example, as follows:
out8(0x3d4, 0x11); out8(0x3d5, in8(0x3d5) & ~0x80);
For some devices, registers are accessed via regular memory operations. To gain access to a device's registers, you need to map them to a pointer in the driver's virtual address space by calling mmap_device_memory(). For example:
volatile uint32_t *regbase; /* device has 32-bit registers */ regbase = mmap_device_memory(NULL, info.BaseAddressSize[0], PROT_READ|PROT_WRITE|PROT_NOCACHE, 0, info.CpuBaseAddress[0]);
Note the following:
Now you may access the device's memory using the regbase pointer. For example:
regbase[SHUTDOWN_REGISTER] = 0xdeadbeef;
You can attach an interrupt handler to the device by calling either InterruptAttach() or InterruptAttachEvent(). For example:
InterruptAttach(_NTO_INTR_CLASS_EXTERNAL | info.Irq, handler, NULL, 0, _NTO_INTR_FLAGS_END);
The essential difference between InterruptAttach() and InterruptAttachEvent() is the way in which the driver is notified that the device has triggered an interrupt:
We recommend that you do the bare minimum within the handler (e.g., acknowledge the interrupt at the hardware level) and then deliver an event to the driver. The driver then completes the rest of the work at process level at the driver's normal priority.
For more information, see the "Writing an Interrupt Handler" chapter in this guide.