Talking to hardware

Updated: April 19, 2023

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.

Some hardware operations require I/O privileges; otherwise you'll get a protection fault. To get I/O privileges for a thread, make sure that your process has the PROCMGR_AID_IO ability enabled (see procmgr_ability()), and then call ThreadCtl():

ret = ThreadCtl(_NTO_TCTL_IO, 0);
if (ret == -1) {
   // An error occurred.
}

Probing the hardware

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. For more information, see the PCI Server User's Guide.

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).

Accessing the hardware

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.

I/O resources

In order to perform port I/O, you need to map the I/O base address into your process's address space, using mmap_device_io(). For example:

uintptr_t iobase;

iobase = mmap_device_io(base_address_size, cpu_base_address);

To do the port I/O, use 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);

Memory-mapped resources

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(). For example:

volatile uint32_t *regbase; /* device has 32-bit registers */

regbase = mmap( NULL, base_address_size, PROT_READ | PROT_WRITE | PROT_NOCACHE,
                MAP_PHYS | MAP_SHARED, NOFD, cpu_base_address);

Note the following:

Now you may access the device's memory using the regbase pointer. For example:

regbase[SHUTDOWN_REGISTER] = 0xdeadbeef;

IRQs

You can attach an interrupt handler to the device by calling either InterruptAttach() or InterruptAttachEvent(). For example:

InterruptAttach(_NTO_INTR_CLASS_EXTERNAL | irq,
                handler, NULL, 0, _NTO_INTR_FLAGS_END);
Note: The driver must successfully call ThreadCtl(_NTO_TCTL_IO, 0) before it can attach an interrupt.

The essential difference between InterruptAttach() and InterruptAttachEvent() is the way in which the driver is notified that the device has triggered an interrupt:

For more information, see the Writing an Interrupt Handler chapter in this guide.