Be Developers' Conference

Writing BeOS Device Drivers
Bob Herold

 

Robert Herold: Hello. Hi, I'm Bob Herold, and I am going to talk to you about writing device drivers for the BeOS.

First, you might ask why would you need to write a device driver? A lot of you probably are here because you have written device drivers for other operating systems, you want to know how to do it on the BeOS, so you may already know this, but for those of you who haven't written device drivers before, they are useful for adding hardware to the system and interfacing with that hardware.

Basically, a device driver is there to control hardware operation, and another useful purpose for device drivers is actually to provide a universal service to all applications that can be implemented as an add-on to the kernel. One example of that that we have in our system is the zero device driver, that's something that actually doesn't touch hardware, but the software-only driver that provides the universal service.

There are three types of BeOS device drivers. There is basically regular device drivers, which exist in the /dev hierarchy of the file system. There are also device drivers for running SCSI controllers, and those are called SIMs. And they actually are part of what's a SCSI standard that... the common access method or CAM.

Another kind of, we call them drivers, they are really not drivers in the technical sense, they don't exist in the kernel address space, but graphic cards drivers, they are basically add-ons to the app server.

So today I'm just going to be talking about regular device drivers, the device drivers that live in the /dev hierarchy. How do you access device drivers from programs? It's a simple API, open, close, read/write, lseek and ioctl. For those of you who come from a UNIX background, those are familiar. It's very similar, in fact identical, to the API that's actually used to the sort of C API for getting at files.

So some things that are useful to know about device drivers, device drivers are add-ons to the kernel. And so they are separately compiled, and when you create your device driver you actually link against a library, that is the library of exported functions from the kernel.

The kernel manages loading and unloading of the drivers at run time, they are actually not built in statically with the kernel. Your device driver runs in supervisor mode, and it runs with interrupts enabled, except of course the interrupt handler which is called when you take an interrupt. And that's run with interrupt disabled.

Where does the kernel go and find device drivers? There are three places it looks first, it can look on a floppy and then it can look in a subdirectory of the config directory in your home directory and then of course it looks in the BeOS directory. The reason for having this three level search order is so that you can override drivers, and also so that if while you are developing a driver you make a mistake and you make a driver that actually crashes the system, while you are booting you can actually override it in one of the other directories and even from a floppy.

It turns out to be quite useful while you are actually developing the device driver.

Now a word about sort of terminology. The notion of a device versus a driver. A driver is the actual object file, it's the thing that you produce when you go and compile something. So it is the actual shared library container of object code. And a device is a name that the driver will publish to the system so that applications can refer to it. So if you want to manage some sound hardware, you will be publishing names of sound devices and the kernel will actually insert those names into the /dev hierarchy in the places that you specify.

The kernel loader actually loads the driver at run time and it calls a special routine that you export from your driver to publish the names of the devices that you support. And when a client program needs to use the services of your driver, it actually opens the device by that name.

So how do you build a device driver? Well, there are five entry points that you can export from your driver that identify it as such to the kernel. Two of them are required, the first one is the publish_devices entry point and that's the means by which the kernel asks the driver what names of devices that driver supports. And then when somebody actually decides to open a driver, they pass in a name to the open call and the kernel will then take that name and call drivers with a find_device call asking if they support that name.

And the optional entry points, one is inti_hardware, that's called basically when the system is booting, that gives your driver a chance to do any kind of one-time initializations of the hardware that it supports or anything else that it chooses to do, you know, just once only during the lifetime of a system boot. init_driver is called whenever a driver is loaded and not an init_driver is called conversely whenever a device is unloaded. Those two routines give it a chance to set up any global drivers it may need while it's loaded and conversely take those down when it's unloaded.

Now, how does work actually get done? How does any I/O actually get done through your driver? The find_device call returns a pointer to a structure which has function pointers to all the hooks for actually doing I/O with a particular device. Open, read/write control, close and free are the hooks. And they serve... I mean they are fairly self-explanatory; open gets called when a client calls open, similarly read, write control gets called when a program does an ioctl call to your device. Close gets called of course when the client program calls close, and free is really the only tricky one, it's possible when your device is closed for there still to be outstanding I/O running in your driver. And the kernel keeps track of all that and when the last person, when the last thread is actually exited your driver, it calls free and allows you to, you know, finalize that device.

So how does your driver actually manage each device? It's possible for a driver to support many different devices, how does it go and distinguish between all of them? It uses something called a cookie. When the kernel calls open, it's expected, it gives you a place to return a cookie that's going to be associated with that particular open instance of that device. And that cookie in turn is passed to all the other hooks when the kernel calls them. So you can use that cookie to actually keep any, any data that you want in it. It's just a void *pointer that the kernel passes around so you can set it up to be anything you want. It can be a number, a pointer to a private data structure, whatever you choose to use it for. The kernel makes no interpretation of the semantics of it, so it's up to the driver to decide how to use it.

So, we can go to a very simple example. The zero device driver, this is not one that actually manages any hardware but shows you the basic structure of a device driver. So we will go and take a look at it.

There we go.

So I have done this Metrowerks project, go and open up the project. And have a look at the source code. So, first of all, way at the bottom, and look at the two essential routines. publish_devices returns a list of names of the devices that the driver supports. In this case, we make up a new name for the zero device driver, we are going to call it my zero, this is an array of character string pointers, terminated by null, by a null entry. So when the kernel calls publish_devices, you are expected to return a pointer to that array. Then, find_device is called when the kernel is trying to open a device driver, and you can go and look in your list of names of devices to see if it's one that you support.

Now you might say well, you know, I already passed back the names to the kernel, but this is just another chance for your driver to make some interpretation on the name and decide if it actually wants to go and open this device. So it can actually return a pointer to the hook functions for that particular device. In this case it's just a set of fixed function pointers to open, close and read/write, control, et cetera. And if it's not a name that you know about, you just return null saying hey, it's not a name that I know about, please don't open the device here.

Let's go look at the hook functions themselves. This is a very simple driver, the open hook function, the zero devices is really a read only device, so we check to make sure that the kernel is trying to open us in read only mode. It doesn't really make sense to write to basically a source of zeros. So we make sure that we are opened in read only mode, and if we are not, we return an error code, otherwise we return saying okay, it's fine.

In this case it's really a simple driver, we don't really need to manage any per device, per-open-instance data, so we actually don't do anything with the cookie parameter. But if we chose to, we would actually fill in this value with some piece of information that we want to have the kernel use to track different open instances.

Closing it is very simple, there is really nothing to do. Again, for the free call, it's very simple, there is nothing to do, we don't define any control calls for this device so we just return an error code saying control calls, all control calls are invalid for this device.

And for the read function, the operation here is the thing that this device driver does, basically it fills up anything that you pass it with zeros. So the kernel, the parameters are the cookie that was returned by open, position, which isn't really applicable here, that's more applicable for a device driver managing like a disk or tape or something like that. A pointer to the data buffer, and the actual length and bytes of the data buffer. And so we go ahead and fill that data buffer with that many zeros. And return, no error.

And you know, if the kernel... the kernel will never call this because it's a read only device, but just in case it does, we return an error code saying read only device. So let's find the project window again. We will actually go and try it out.

Let's go ahead and build this.

It's done. Now, so here is my actual new device driver. We can go and look right now in the /dev directory, and we see what's in there right now. There really isn't... there is the zero device that actually ships with the BeOS, which is almost the exact same source code, but there is no device named my zero. So we can go and copy this over into, I created an alias for the driver directory that's in the home directory, so I just go and copy it over into here, and we can see that it's in there. Now if we go and look in the device directory, in the /dev directory, we can see my zero device. And we can actually go and try to use it by just doing an octl dump from it, and we will see that it just produces a lot of zeros. And in this case it will continue to produce zeros forever, so we are just going to go ahead and kill it with a ^C.

So that's a very simple device, you have actually seen all the entry points, all the hook functions, you have seen how it's built, you can see you can just drag it into one of the directories that the kernel looks for device drivers in and it actually shows up in the device hierarchy right away.

A Speaker: What were the libraries that you were including and what were your settings?

Robert Herold: Very good. So let's go back and take a look at that.

The question was, what libraries were in the project and what were the settings on the project? This library is, it's in the /develop/lib/x86 directory, and this is basically a library of all the exported functions from the kernel. The kernel exports the whole standard C library, or the whole, I mean most of the standard C library, and also a lot of the kernel functions that you actually need to get useful work done in a device driver. And this one is just some run time glue for doing things like arithmetic and stuff like that. So I think on the R3 disk we didn't have a stationary setup for a device driver. But that's something that we plan to make in the future.

So basically, you know, you get project stationary so you can just have a device driver framework with these libraries all set up.

Yes?

A Speaker: Would you explain the library (inaudible).

Robert Herold: Pardon?

A Speaker: On the PowerPC...

Robert Herold: I can't do it off the top of my head.

A Speaker: Just the kernel.

A Speaker: You link against...

Robert Herold: That's right. You link against.

A Speaker: You create a sublink to the kernel link.

Robert Herold: I think on the R3 PowerPC we will have driver stationary there that will do all that. I think there are issues with the IDE, you can't have it drag SIM links into the IDE but we will create things to fool it into...

A Speaker: What were the settings?

Robert Herold: Let's go into the settings. The ones that change from sort of the empty project settings are, let's see, where is it, it's not there.

X86 project, you change it from application to shared library, and then you change the name from, you know, not application, you change it to whatever the name of your driver you want it to be.

And the other one you need to change from the default settings are in the linker, you basically need to get rid of underscore underscore start in this field. Because this really doesn't have a main entry point, it's just a shared library. Okay. So that's the zero device.

Now, given that we were shipping both a PowerPC and an Intel version, it's useful to, if you are writing for a device that could actually go into both architectures, to make your drivers aware and aware of endian issues and to make them independent of the different byte order on the two different processors.

We have provided some macros that actually make that pretty easy, there is the host to little-endian and host to big-endian macros, and the xxx there stands for different types of data that you might need to swap. There are 16-bit and 32-bit integers, 64-bit integers, floats and doubles, and you use those primarily when writing out. Of course you are writing from the host to a little-endian bus or host to a big-endian bus.

Conversely, when you are reading data in going from a big-endian or little-endian bus to the host, so you use the opposite direction of macro.

And if you need to access I/O space, on either the PCI bus or if you need to access I/O space on the ISA bus, which is present in the Intel architecture and also on the BeBox, we have simplified that and made it uniform across all the platforms by having some exported function calls from the kernel that basically do 8-, 16- and 32-bit reads and writes to I/O space, and you basically pass in the port address and the value that you want to write or just the port address if you want to read it.

Some other services that the kernel provides, just in terms of dealing with memory, in a device driver, you may be using a DMA device that actually goes and reads the data directly out of memory, you need to lock any buffers that may have been passed down to you. So they don't get moved around by the virtual memory system in the kernel while doing I/O. So we have locked memory and unlocked memory calls which allow you to do that.

Once you have locked down a buffer, you need to find out actually where it is in physical memory so that you can give those physical memory addresses to your DMA device. So the get_memory_map call is to do that.

A Speaker: Can you guarantee that you can get a physically contiguous block of say images size or are you getting like fragments?

Robert Herold: Yes and yes. For client buffers you don't know what you are getting, so there is no way of guaranteeing it, there is no call that says go make this client buffer contiguous. However, there are calls, like for instance the create_area call, you can specify that as a way to create a new buffer that is physically contiguous and you can also, even on Intel you can specify that it go below 60 Meg if you happen to be doing DMA from the ISA bus. If you actually do need to go and do contiguous I/O imagesr than a page, you can create your own buffers and do that.

A Speaker: You do that within your kernel space, from within your driver?

Robert Herold: Yes, from within your driver. That creates a new area for you within the kernel address space to do that. So the get_memory_map call is how you actually find out where all your data is in physical memory. You also have the services of the kernel heap, you can use malloc and free, and that will create block locked buffers for you. Those will be in the kernel heap, they won't necessarily be continuous, so you may need to call get_memory_map on those, but it is a way of having temporary buffers that are locked.

Let's see. Some other kernel services that are extremely useful when writing device drivers, there are semaphores, acquire, release, another important one, set_owner. If you create semaphores in your device driver, chances are your device driver is being called as a result of an open that was called... that was done from some application. If you just create the semaphore right there, without setting the owner, the default owner is that application. And if that application then, you know, exits, but somebody else is using the driver, that semaphore will be deleted, much to the surprise of your driver. So the... you have to change the default behavior of create_semaphore to actually set the owner of the semaphore to be the kernel. So that's an important one.

There are also the services of spinlocks. Semaphores are basically a mechanism for locking things and when you don't get the lock, you block and you are rescheduled. Sometimes that's not desirable behavior, for instance if you are managing a data structure that is shared between some... a read call and a data structure that is actually shared with an interrupt handler, you need to be able to control access to that data structure within the context of an interrupt handler. So for that you use spinlocks and you also need, in order to do spinlocks correctly, you need to be able to disable and restore the state of the interrupt enable bit in the processor. So we have calls to do that. Basically disable interrupts and restore interrupts. So those are the two synchronization mechanisms that the kernel provides.

For anyone who actually works on device drivers, in the preview release, in preview release 2 the interface changed slightly, the old interface continues to be supported, but the calls for basically managing interrupt handlers and enabling and disabling of interrupts have been replaced by new calls for installing and removing interrupt handlers. The reason for doing this was you know with the advent of Intel architecture and the IRQ mechanism and also the way interrupts are managed on the PCI bus in Intel architecture, you have a need to share IRQ lines between different devices. And the old API basically did not have any mechanism for allowing you to do that effectively. So with a new API install interrupt handler, remove I/O interrupt handler, the first interrupt handler that gets installed automatically enables the interrupt and subsequent installs just basically tack interrupt handlers on to the end of a list of interrupt handlers to call when that interrupt occurs.

And consequently and conversely remove I/O interrupt... removes them all, and the last one that gets removed, the kernel disables the interrupt automatically.

There are some new features, I talked about interrupt sharing. And also now the kernel actually pays attention to the value that's returned from your interrupt handler. It used to be that you could return anything, it really didn't care. Now it's a flag to the kernel as to whether your device is the one that was responsible for that interrupt and handled that interrupt. So the kernel would just call all the interrupt handlers in a chain until one of them returns true, saying okay, that was my interrupt. That's the way interrupt sharing is done.

The other two new features that I already talked about are the functions for accessing I/O space and the byte swapping macros.

In terms of where we are heading in the future, as all this wonderful new hardware in the Intel world we want to support things such as USB and IEEE-1394, as we start to think about those, we realize we need to abstract the notion of what a bus was so that support for all buses didn't have to be statically compiled into the kernel but could actually be implemented separately and done as add-ons to the kernel. So we are going to invent the notion of bus managers and that will actually change the API slightly for the PCI bus and the ISA bus, but we will continue to support the old API as we go forward with those. But it will allow you to have basically an API that will be unique to any particular bus implementation and allow you to have that bus implementation be separate from the kernel, and load it separately and actually it also, the kernel will become much smarter... the boot manager will become much smarter about what components it actually loads with the kernel based on what hardware is actually installed on your system.

So you won't get a huge monolithic kernel with support for everything, it's a kernel that has loaded the drivers, bus managers that it needs to support is present there.

The other thing we plan to do is multi-level drivers, so you have... sort of meta or abstract drivers that open up other drivers that do specific work. One example might be a keyboard driver where you have the old AT style keyboard or something that sits out on USB or something like that.

Okay. That's it. Any questions?

A Speaker: What kind of kernel debugging support...

Robert Herold: I couldn't hear.

A Speaker: Kernel debugging support.

Robert Herold: Kernel debugging support. The question is, what kind of kernel debugging support do we have? Well, it's primitive.

A Speaker: Hey!

Robert Herold: It does require a serial port. Basically we use one of the serial ports and there is the ability to basically print it out, out the serial port. There also is a kernel debugger so you can actually go and stop the system dead in its tracks, look at the state of things and poke around with kernel debugger.

A Speaker: Any third party?

Robert Herold: No, all that stuff is actually built into the kernel.

A Speaker: You talked about the driver (inaudible) kernel you implied a manual start and stop. In the real hardware driver, can you manually start and stop it without having to...

Robert Herold: Yes. I mean it's...

A Speaker: Repeat the question.

Robert Herold: Okay, the question is...

A Speaker: Manual starting and stopping with drivers.

Robert Herold: Can you manually start and stop?

A Speaker: Hardware.

Robert Herold: Hardware drivers.

Drivers aren't actually started until an application requests that they be started. They are sort of... the kernel knows about them.

A Speaker: I'm asking you if the... you are already booted and you, as the person wanting to test, don't want your driver booting with the kernel but you want to be able to start the actual driver yourself. And then you can run applications without every time you need to make a change you have to reboot your system. I want to be able to copy my driver...

Robert Herold: Yes. Let me see if I can rephrase it. You want to be able to not have the kernel look at drivers when you are starting up, then later put the driver someplace where the kernel will recognize it and you can have an application that calls it, test your driver, and then figure things out and then be able to sort of shut the thing down so the kernel doesn't look at it anymore.

A Speaker: Redo it every time.

Robert Herold: Right. You can actually do that now just by, you know, copying the driver into one of the directories, the kernel will notice it's there, or... and conversely will... the next time it actually goes and scans through the driver directory it will notice it's not there and remove it from the list of drivers it knows about.

A Speaker: If it's already there, can you move it and then put another one in?

Robert Herold: Yes.

A Speaker: So if your driver is running on the... the only way to stop it is remove it from the directory.

Robert Herold: When your... the question is if your driver is running is there any way to remove it from the directory? Yes, you can just remove it from the directory and the kernel will not see it there anymore. If an application has it open, that's a risky thing to do. But yes, the kernel... it is dynamic in terms of recognizing drivers when they are in the search path for drivers and when they are not in the search path it removes anything that used to be there when the driver was in the search path.

A Speaker: (Inaudible.)

Robert Herold: I'm sorry?

A Speaker: Your example was a character device, how is a block device different?

Robert Herold: How is a block device different? Currently they are not different. Maybe... it's an idea we have had to actually support the blocked devices at a higher level than the driver level, actually support them in the file system independently. Right now they are not supported in the file system independently, so even a block device usually gets passed a pointer to a buffer and a number of bytes. And you actually go and deal with it yourself. Right now there is no difference, there may be in the future.

Back there.

A Speaker: What are your plans for dealing with AGP memory?

Robert Herold: With AGP memory? DMA?

A Speaker: In the operating system what hooks are you going to have for setting aside areas of AGP memory and interacting with it?

Jon Watte: You have a physical address space, say map physical memory.

A Speaker: Wait. Graphics drivers, you use AGP, you go to AGP support, to basically create AGP addition. It would be done, it will be done probably next or in R5. It depends on the need of the AGP graphics driver.

Robert Herold: I mean the graphic device drivers... graphic device drivers right now actually exist in user space, they are add-ons to the app server. The graphic driver model may undergo some revisions to basically make it a bit more flexible to bright graphic drivers. That's one of the things you are going to need to do is actually get access to AGP memory. There will be facilities to do that. Yes.

A Speaker: You mentioned that the driver was scanning the significant directory for drivers. Does it do for the periodically...

Robert Herold: It does it on a lazy basis when devices are opened, when it needs to.

A Speaker: You can do a BScan type thing?

Robert Herold: The question is when does the kernel go and rescan the device hierarchy to look for new device names. The answer is it does it when somebody is interested in opening a new device.

A Speaker: If you do a /dev.

Robert Herold: That does it. It then needs to go find all the devices. So, that's how it does.

Any other questions?

Jon Watte: Did you talk about the importance of putting drivers in the appropriate subdirectory?

Robert Herold: No, I didn't, but I can put in a plug for you.

Jon Watte has been assiduously reorganizing the /dev hierarchy because it was starting to get crowded at the top level with everybody's favorite drivers.

Maybe you want to say a couple words about.

Jon Watte: For some /dev L /dev, different classes of drivers live in different subdirectories, it's important if you write a MIDI driver, it must be specific. And depending on what class of driver you are writing there may already be a good place for that driver to live. So you should definitely send an e-mail and we can send you sample code.

Robert Herold: If you are working on audio drivers... right now there are sort of a one fixed place where the current audio server looks for them.

A Speaker: Right.

Robert Herold: And that's in?

A Speaker: That's going away.

Robert Herold: That's going away with the R4 media kit.

A Speaker: If you don't know where your driver is supposed to go, set it in /dev/misc. It will jumble all the various drivers but specific APIs, that's a good starting point.

Robert Herold: More questions?

Okay. Thanks for coming.

(Applause.)


Transcription provided by:

Pulone & Stromberg
1520 Parkmoor Avenue
San Jose, California
408.280.1252
dhoyman@rtreporters.com