Device driver basics


Device drivers are difficult to write. Understanding the hardware can be the hardest part. Often the documentation is hard to read and understand because it is written from a hardware designer's perspective (that is, if you can get the documentation -- many manufacturers are very reluctant to give it out). Drivers work directly with the kernel - a bad pointer can crash the whole OS. And, finally, you can't use warm and comfortable debugging tools, since you are working in the kernel.

Still sure that you want to write one? OK. Let's talk a little bit about the API.

There are 5 (!) functions and one global that you must export. They are:

  • status_t init_hardware(void)
    called when the system boots. Must return B_OK if the hardware exists.

  • status_t init_driver(void)
    called to initialize your driver. Must return B_OK, or the attempt to open the driver will fail.

  • void uninit_driver(void)
    called when driver is unloaded from memory.

  • const char **publish_devices(void)
    returns an array of char *'s that name the devices this driver will export. Names are relative to /dev.

  • int api_version
    this global tells the driver system which version of the API you built with. Populate it with B_CUR_DRIVER_API_VERSION.

and, finally:

  • device_hooks *find_device(const char *name)
    returns an array of function pointers that provide the dev file system calls to: open, close, free, control, read, write, select, deselect, readv, writev (in that order)

So, to look at this from a very high level, implement the 5 directly called functions, the global export, and some/all of the device hooks, and you are all set. The API here is very easy to understand.

Let's talk a little bit about how to build and test these. A simple way to build is to use Be's makefile-engine. These three example lines demonstrate what needs to be set:

NAME= usbrawpci
SRCS= usbraw.c

The driver should be copied into ~/config/add-ons/kernel/drivers/bin. A link should be made from there to the location under ~/config/add-ons/kernel/drivers where you want the driver to show up in devfs (/dev). For example:

ln -s ~/config/add-ons/kernel/drivers/bin/usbrawpci

Debugging is hard. No two ways about that. The best way is with a terminal hooked up to your serial port. Assuming that you do not have such a thing (I don't), you can enable logging to a file. Look in ~/config/settings/kernel/drivers/sample. There is a file there named "kernel". It is a sample of a kernel config. Copy it up one level (to the drivers directory) and uncomment the line "syslog_debug_output". This causes the kernel to write the log to /var/log/syslog. Now you can use dprintf to print data to the syslog - it works pretty much exactly like printf.

Synchronization is important because you could (potentially) have two or more users using the driver at the same time. Imagine 2 apps running on a dual processor box, perfectly timed so that they both start to write to your device at the same time. Both would be in your driver code at the *same time*. That means that all of the setup work that A is doing to get ready to write, B is changing at (nearly) the same time. Very bad. There are a couple of ways to protect your code that are commonly used in BeOS drivers. One is spinlocks. This is conceptually:

static volatile myLock;
init_hardware() {myLock=0;}
lock() {while (myLock); myLock=1;}

I say conceptually, because this would not work in a multi-processor or pre-emptive kernel situation, so special magic is done to make this work right. In any case, I am sure that it is obvious to you that this wastes a lot of CPU cycles. For that reason, it should be used only to protect small, fast bits of code that is happening inside of an interrupt. Fortunately for us, we do not have to write spinlocks. You can create them simply with:

spinlock foo;             // Create a spinlock
cpu_staus old; // To hold all of the cpu info
old=disable_interrupts(); // Shhh - don't interrupt
acquire_spinlock(&foo); // Get the lock
.. do stuff... // Get done fast
release_spinlock(&foo); // Unlock
restore_interrupts(old); // OK - interrupt me now.

Final note on spinlocks: consult with the BeBook on what you can and can not do inside spinlocks - your options are very limited. But, then again, you shouldn't do very much in your interrupt code anyway.

So spinlocks are all fine for interrupt code. What about for non-interrupt code (which most of the driver should be)? Semaphores are your answer. Just like in user land.

This is basically all there is to writing device drivers. The hard part is getting (and understanding) the specifications for the hardware. BeOS makes it very easy to create drivers once you know what the hardware needs.

However, just to make this even a bit easier, I made up a "template". It is completely untested, but you may find it helpful; feel free to download it from here.