Talking to hardware

QNX SDP8.0Programmer's GuideDeveloper

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 OS. Generally, the type of driver you're writing will determine the driver model you'll follow. For example, graphics drivers follow one particular model that 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 (except for 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:
  • We declared regbase with the volatile keyword to prevent the compiler from optimizing out accesses to the device's registers.
  • We specified the PROT_NOCACHE flag to ensure that the CPU won't defer or omit read/write cycles to the device's registers.
    Note: On ARM targets, PROT_NOCACHE causes RAM to be mapped as normal noncached, but non-RAM to be mapped as strongly ordered device memory. For finer control, see shm_ctl_special().
Now you may access the device's memory using the regbase pointer. For example:
regbase[SHUTDOWN_REGISTER] = 0xdeadbeef;

IRQs

You can attach a thread or an event to the interrupt device by calling either InterruptAttachThread() or InterruptAttachEvent(). For example:
InterruptAttachThread(IRQ_NUM, _NTO_INTR_FLAGS_NO_UNMASK);
The essential difference between InterruptAttachThread() and InterruptAttachEvent() is the way in which the driver is notified that the device has triggered an interrupt:
  • Using InterruptAttachThread() provides the least overhead and better interrupt latency. This is the preferred option. However, there are limitations; you can only attach a thread to one interrupt and can only block the thread using InterruptWait().
  • With InterruptAttachEvent(), you specify an event to be delivered to the driver when the device triggers an interrupt. If the event is a SIGEV_INTR event, then the call is equivalent to InterruptAttachThread(). Otherwise, the kernel creates a new thread that only runs in its kernel persona. This thread waits for the kernel interrupt notification, then delivers the user-requested event to a user-space thread, so the extra overhead increases the latency.

For more information, see the Handling Hardware Interrupts chapter in this guide.

Page updated: