Talking to hardware
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.
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
uintptr_t iobase;
iobase = mmap_device_io(base_address_size, cpu_base_address);
out32(iobase + SHUTDOWN_REGISTER, 0xdeadbeef);
out8(0x3d4, 0x11);
out8(0x3d5, in8(0x3d5) & ~0x80);
Memory-mapped resources
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);
- 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().
regbase[SHUTDOWN_REGISTER] = 0xdeadbeef;
IRQs
InterruptAttachThread(IRQ_NUM, _NTO_INTR_FLAGS_NO_UNMASK);
- 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.