The Device Kit: Developing aKernel-Loadable Driver


The Device Kit: Developing aKernel-Loadable Driver

At the most basic level, devices (other than graphics cards) are controlled by system calls that the kernel traps and translates for the driver. Five different kinds of functions control input/output devices on the BeBox:

open() Opens a device for reading or writing.
close() Closes a device that was previously opened.
read() Reads data from the device.
write() Writes data to the device.
ioctl() Formats, initializes, queries, and otherwise controls the device.

All these functions, except ioctl(), are Posix-compliant. Instead of ioctl(), Posix defines a set of functions like tcsetattr() and tcflush() to control data terminals. These functions are supported, but they can be treated as special cases of ioctl(). (Posix also defines a fcntl() function for file control that has the same syntax as ioctl().)

When open(), close(), read() , write(), or ioctl() is called for a device, the kernel expects a driver to do the work that's required. Each driver must implement a set of functions that correspond directly to the five system calls. Everything the driver does to operate the device is initiated through one of these functions.

Because drivers run in the kernel's address space as extensions of the kernel, they must conform to the kernel's expectations. Separate sections discuss the three types of restrictions that the kernel imposes on drivers:

The exported functions that the kernel defines specifically for drivers are documented following these three sections.


Entry Points

The kernel loads a device driver when it's needed--typically when someone first attempts to open the device for reading or writing. Opening a device is a prerequisite to using it.

To the open() function, drivers are identified by a fictitious pathname beginning with /dev/. For example, this code opens the parallel port driver for writing:

   int fd = open("/dev/parallel", O_WRONLY);

The first thing the kernel must do is match the device name--"/dev/parallel" in this case --to a driver; it must find a driver for the device. The driver might be one that has already been loaded, or it might be one that the kernel must search for and load. Loadable drivers reside in the /system/drivers directory; this is where the kernel looks for drivers and where they all must be installed.

Once a driver has been located and loaded, the kernel begins communicating with it --first to get information from it and test whether it's the right driver, then to initialize it and have it open the device.

One key piece of information that the kernel needs from the driver is the names of all its devices. Another is a list of the functions it can call to exercise those devices. For each device, the driver implements a set of hook functions that open the device, control it, read data from it, write data to it, and perhaps eventually close it. These hooks correspond to the system functions discussed above.

To give the kernel initial access to this information, drivers make declarations --of functions and data structures--using names the kernel will look for. Five such names will be discussed in the following sections:

init_driver() Initializes the driver after it's loaded.
uninit_driver() Cleans up after the driver before it's unloaded.
devices Declares the devices and their hook functions.
publish_device_names() Lists the devices the driver handles.
find_device_entry() Associates a device with its hook functions.

These are the main entry points for driver control.


Driver Initialization

Immediately after loading a driver, the kernel gives it a chance to initialize itself. If the driver implements a function called init_driver(), the kernel will call it before proceeding with anything else--before asking the driver to open a device. The function should expect no arguments and return either B_ERROR or B_NO_ERROR :

      long init_driver(void)

A return of B_ERROR means that the driver can't continue; the kernel will consequently unload it. A return of B_NO_ERROR means that all is well; the kernel will continue by asking the driver to open a device. The absence of an init_driver() function is equivalent to a return of B_NO_ERROR.

init_driver() might go a long way toward initializing the data structures the driver uses to do its work--for example, setting up needed semaphores. However, details specific to a particular device should be left to the hook function that opens that device.

When the kernel is about to get rid of a driver, it gives the driver a chance to undo what init_driver() did. If the driver implements a function called uninit_driver(), the kernel will call it immediately before unloading the driver. This function has the same syntax as the initialization function:

      long uninit_driver(void)

This function can do nothing to prevent the driver from being unloaded. It should simply clean up after the driver--for example, delete semaphores--and return B_NO_ERROR .

Driver initialization and its opposite happen just once--when the driver is loaded and unloaded. In contrast, devices might be opened and closed many times while the driver continues to reside in the kernel.


Device Declarations

For the kernel to find the driver for a given device, all drivers must declare the names of the devices they control. For the kernel to be able to communicate with the driver to operate the device, every driver must declare a set of device-specific hook functions the kernel can call.

These declarations are made in a device_entry structure that maps the device name to the set of hook functions. This structure is declared in device/Drivers.h and contains the following fields:

const char *name The name of the device--for example, "/dev/serial". This is the same name that's passed to open(). Driver code can assign any name it wants to the device, but it must begin with the "/dev/" prefix, which distinguishes devices from ordinary files.
device_open_hook open The function that the kernel should call to open the device. The kernel will invoke this function to respond to open() system calls.
device_close_hook close The function that should be called to close the device. It corresponds to the close() system call.
device_control_hook control The function that the kernel should call to control the device, including querying the driver for information about it. The kernel will invoke this function to respond to ioctl() calls.
device_io_hook read The function that should be called to read data from the device. The kernel will invoke this function to respond to the read() system call.
device_io_hook write The function that should be called to write data to the device. It corresponds to the write() system call.

(The five functions are described in more detail under Hook Functions below.)

A driver declares one device_entry structure for each device it can drive. If it can handle more than one device, it must provide a device_entry structure for each one. If it permits a device to be referred to by more than one name, it must provide a structure for each name it recognizes.

There are two ways for a driver to provide the kernel with the information in a device_entry structure: If the list of devices is known at compile time, the driver can declare them statically. If the list might change at run time, it can return them dynamically.

Static Drivers

Most drivers are designed to handle a fixed set of known devices-- perhaps a single device or perhaps many. Such drivers should declare a null-terminated array of device_entry structures under the global name devices:

      device_entry devices[]

For example, the serial port driver might declare a devices array that looks like this:

   device_entry devices[7] = {
       {"/dev/serial1", open_func, close_func, control_func,
               read_func, write_func},
       {"/dev/serial2", open_func, close_func, control_func,
               read_func, write_func},
       {"/dev/serial3", open_func, close_func, control_func,
               read_func, write_func},
       {"/dev/serial4", open_func, close_func, control_func,
               read_func, write_func},
       {"/dev/com3", open_com_func, close_func, control_com_func,
               read_func, write_func},
       {"/dev/com4", open_com_func, close_func, control_com_func,
               read_func, write_func},
       0
   };

In this case, the driver handles the four serial ports seen on the back of the BeBox, each of which it identifies by a different name. It can also control "com3" and "com4" ports on an add-on board.

As this example illustrates, the hook functions declared in a device_entry structure are specific to the device. For the most part, the serial port driver above uses the same set of functions to operate all the devices, but declares special functions for opening and controlling "com3" and "com4".

Note also that the array is null terminated.

Dynamic Drivers

A driver can also provide device_entry information dynamically. Instead of a devices array, it implements two functions, publish_device_names() and find_device_entry().

The first of these functions should be declared as follows:

      char **publish_device_names(const char *deviceName) 

If passed a proposed deviceName that matches the name of a device the driver handles, or if passed a NULL device name, this function should return a null-terminated array of all the names of all the devices that it handles. For example, the serial port driver described above would return the following array:

   "/dev/serial1", 
   "/dev/serial2", 
   "/dev/serial3", 
   "/dev/serial4", 
   "/dev/com3", 
   "/dev/com4", 
   0

However, if the proposed device name doesn't match any that the driver handles, publish_device_names() should return NULL.

While publish_device_names() informs the kernel of the devices that the driver handles, find_device_entry() returns entry information about a particular device. It has the following form:

      device_entry *find_device_entry(const char *deviceName) 

If passed the name of a device that the driver knows about, this function should return the device_entry for that name. If the deviceName doesn't match one of the driver's devices, it should return NULL.

The kernel first calls publish_device_names() during the boot sequence to find what devices the driver handles. It may call the function again to update the list when it tries to match a driver to a specific device. If a match is made, it calls find_device_entry() to get the list of hook functions for the device.


Hook Functions

The five hook functions that are declared in a device_entry structure can have any names that you want to give them, provided that they don't clash with names that the kernel exports (see Exported Functions ). However, their syntax is strictly prescribed by the kernel (through type definitions found in device/Drivers.h ).

The five functions have two points in common: First, they each return an error code, which should be 0 (B_NO_ERROR) if there is no error. The error value is passed through as the return value for the open(), close(), read(), write(), or ioctl() system call that caused the kernel to invoke the driver function. The driver should return error values that are compatible with ones that are expected from those functions.

Second, all five functions are passed information identifying the device. As their first argument, they receive a pointer to a device_info structure (also defined in device/Drivers.h), which contains just two fields:

device_entry *entry The device_entry structure for the device that's being operated on. This is a copy of information that the driver declared in its devices array or that its find_device_entry() function returned.
void *private_data Arbitrary data that describes the device. This data is a way for the driver to record information about the device and have it persist between function calls. Although the kernel stores this data and passes it to the driver, the driver initializes it and maintains it; the kernel doesn't query or modify it.

Thus, all of the device-specific hook functions have the same return types and initial arguments:

long function(device_info *info, . . . )

Differences among the functions are discussed below.

Opening and Closing a Device

The function that opens a device is of type device_open_hook and the one that closes it is of type device_close_hook . They're defined as follows:

      typedef long (*device_open_hook)(device_info *info, ulong flags) 
      typedef long (*device_close_hook)(device_info *info) 

The flags mask that's passed to the open() system call is passed through to the device function. It typically will contain a flag like O_RDONLY, O_RDWR, or O_WRONLY.

Since the hook function that opens the device is the first one that's called, it might set up the device_info description of the device (to the extent that init_driver() hasn't already done so). It might also use that description, or some static data, to record whether or not the device is currently open. Typically, only one process can have a device open at a time. If the hook function sees that the device is already open, it can refuse to open it again.

Whatever values these functions return will also be returned by the open() and close() system calls.

Reading and Writing Data

The driver functions that read data from and write data to a device must be of type device_io_hook, which is defined as follows:

      typedef long (*device_io_hook)(device_info *info, 
         void *buffer, ulong numBytes, ulong position) 

The function that reads data from the device should place up to numBytes of data into the specified buffer. The hook that writes to the device should take numBytes of data from the buffer. The data should be read or written beginning at the position offset on the device. The offset is meaningful for some drivers (mostly drivers for storage devices), but can be ignored by others (such as a serial port driver).

Whatever values these functions return will also be returned by the read() and write() system calls.

Controlling the Device

The hook function that initializes, formats, queries, and otherwise controls a device is of type device_control_hook, defined as follows:

      typedef long (*device_control_hook)(device_info *info, ulong op, void *data) 

The second argument, op, is a constant that specifies the particular control operation that the function should perform. The third argument, data, points either to some information that the control function needs to carry out the op operation or to a data structure that it should fill in with information that the operation requests. The interpretation of the data pointer depends entirely on the nature of the operation and will differ from operation to operation; the op and data arguments go hand-in-hand.

For example, if the op code is SET_CONFIG, data might point to a structure with values that the control function should use to re-configure the device. If the operation is GET_ENABLED_STATE, data might point to an integer that the function would be expected to set to either 1 or 0. If it's RESTART, data might simply be NULL.

The kernel defines a number of control operations (which are explained in the next section). These are operations that the kernel might call upon any driver to perform.

If you define your own control operations (for an ioctl() call on your driver), you should be sure that they aren't confused with any that the kernel currently defines --or any that it will define in the future. We pledge that all system-defined control constants will have values below B_DEVICE_OP_CODES_END . The constants you define should be increments above this value. For example:

   enum {
       REPORT_STATUS = B_DEVICE_OP_CODES_END + 1,
       SET_TIMER,
       . . .
   }

If a control function doesn't recognize the op code it's passed or can't perform the requested operation, it should return B_ERROR (-1).


Control Operations

Several control operations are defined by the kernel. The kernel can request any driver to perform these operations, even in the absence of an ioctl() call. A control function should respond to as many of these requests as it can. It should respond to inappropriate or unrecognized requests by returning B_ERROR.

The set of system-defined control operations is described below.

B_GET_SIZE and B_SET_SIZE

These control operations request the driver to get and set the memory capacity of the physical device. The capacity is measured in bytes and is recorded as a ulong integer. For a B_GET_SIZE request, the control function should write this number to the location referred to by the data pointer. For B_SET_SIZE, data will be the requested number of bytes (not a pointer to it).

B_SET_BLOCKING_IO and B_SET_NONBLOCKING_IO

These operations determine whether or not the driver should block when reading and writing data. B_SET_BLOCKING_IO requests the driver to put itself in blocking mode. Its read function should wait for data to arrive if none is readily available and its write function should wait for the device to be ready to accept data if it's not immediately free to take it. If B_SET_NONBLOCKING_IO is requested, the read function should return immediately if there is no data available to read and the write function should return immediately if the device isn't ready to accept written data.

For these operations, the data argument doesn't contain a meaningful value.

B_GET_READ_STATUS and B_GET_WRITE_STATUS

These control operations request the driver to report whether or not it's ready to read and write without blocking. The control function should respond by placing TRUE or FALSE as a ulong integer in the location that data points to.

For B_GET_READ_STATUS, it should respond TRUE if there's data waiting to be read, and FALSE if not. For B_GET_WRITE_STATUS, it should respond TRUE if the device is free to accept data, and FALSE if not.

B_GET_GEOMETRY

This op code requests the driver to supply information about the physical configuration of the device; it's generally appropriate only for mass storage devices. The control function should write the requested information into the device_geometry structure that the data pointer refers to. A device_geometry structure contains the following fields:

ulong bytes_per_sector The number of bytes in each sector of storage.
ulong sectors_per_track The number of sectors in each track.
ulong cylinder_count The number of cylinders.
ulong head_count The number of heads.
bool removable Whether or not the storage medium can be removed (TRUE if it can be, FALSE if not).
bool read_only Whether or not the medium can be read but not written (TRUE if it cannot be written, FALSE if it can).
bool write_once Whether or not the medium can be written once, after which it becomes read-only ( TRUE if it can be written only once, FALSE if it cannot be written or can be written more than once).

B_FORMAT

This operation requests the control function to format the device. The data argument doesn't contain any valid information.


Exported Functions

After a driver has been loaded, it runs as part of the kernel in the kernel's address space. It therefore is restricted to calling functions (a) that it implements or (b) that the kernel makes available to it. The driver links against the kernel alone; it cannot also independently link to something else, even the standard C library.

The kernel exports five kinds of functions so that they're available to a driver:

Functions from all five groups are listed in the sections below. Special driver functions are documented in detail in the section entitled Functions for Drivers .


Support Kit Functions

The kernel exports the following Support Kit functions:

read_16_swap() atomic_and()
read_32_swap() atomic_or()
write_16_swap() atomic_add()
write_32_swap() real_time_clock()

See the chapter on the Support Kit for descriptions of these functions.


Kernel Kit Functions

Most functions from the Kernel Kit are available to drivers. However, a few are not, sometimes because it would make no sense for a driver to call the function, and sometimes because it's difficult for the kernel to provide its very basic services to its own modules. In some cases, a special function is defined for drivers that takes the place of the missing Kit function. For example, spawn_thread() can't spawn a thread in the kernel. Since drivers run in the kernel, they need to use the special spawn_kernel_thread() instead. Similarly, debugger() can't be used to debug the kernel. Drivers should call kernel_debugger() instead.

The following Kernel Kit functions are exported for drivers:

Semaphores
create_sem() get_sem_info()
acquire_sem() get_nth_sem_info()
acquire_sem_etc() get_sem_count()
release_sem() set_sem_owner()
release_sem_etc() delete_sem()

Threads
find_thread() suspend_thread()
rename_thread() resume_thread()
set_thread_priority() wait_for_thread()
get_thread_info() exit_thread()
get_nth_thread_info() kill_thread()

Teams
kill_team()
get_team_info()
get_nth_team_info()

Ports
create_port() find_port()
read_port() port_count()
read_port_etc() port_buffer_size()
write_port() port_buffer_size_etc()
write_port_etc() set_port_owner()
get_port_info() delete_port()
get_nth_port_info()

Time
snooze()
system_time()

Other
area_for()
get_system_info()


C Library Functions

The kernel exports a small number of functions from the standard C library. They include:

Functions declared in stdlib.h
atof() malloc()
atoi() calloc()
atol() free()
strtod() abs()
strtol() div()
strtoul() labs()
bsearch() ldiv()
qsort()

Functions and macros declared in ctype.h
isalnum() ispunct()
isalpha() isspace()
iscntrl() isprint()
isdigit() isgraph()
isxdigit()
islower() tolower()
isupper() toupper()

Functions declared in string.h
strlen() strspn()
strcat() strcspn()
strncat() strstr()
strcpy() strpbrk()
strncpy() memset()
strcmp() memchr()
strncmp() memcmp()
strchr() memcpy()
strrchr() memmove()

Functions declared in stdio.h
sprintf()
vsprintf()

The driver accesses these functions from the kernel, not from the library.


System Calls

The kernel also exports the five system calls that control devices:

open()
close()
read()
write()
ioctl()


Kernel Functions for Drivers

The kernel defines the following functions especially for drivers. For full documentation of these functions, see Functions for Drivers .

Spinlocks:
acquire_spinlock()
release_spinlock()

Disabling interrupts:
disable_interrupts()
restore_interrupts()

Interrupt handling:
set_io_interrupt_handler() set_isa_interrupt_handler()
disable_io_interrupt() disable_isa_interrupt()
enable_io_interrupt() enable_isa_interrupt()

Memory management:
lock_memory() isa_address()
unlock_memory() ram_address()
get_memory_map()

ISA DMA:
start_isa_dma() lock_isa_dma_channel()
start_scattered_isa_dma() unlock_isa_dma_channel()
make_isa_dma_table()

PCI:
read_pci_config()
write_pci_config()
get_nth_pci_info()

Debugging:
dprintf()
set_dprintf_enabled()
kernel_debugger()

Hardware versions:
motherboard_version()
io_card_version()

SCSI common access method:
xpt_init() xpt_action()
xpt_ccb_alloc() xpt_bus_register()
xpt_ccb_free() xpt_bus_deregister()

Other
spin()
spawn_kernel_thread()


Installation

The driver must be compiled as an add-on image, which in practical terms is much the same as compiling a shared library. The Kernel Kit chapter explains add-on images, and the Metrowerks CodeWarrior manual gives compilation instructions. In summary, you'll need to specify the following options for the linker (as LDFLAGS in the makefile):

For the kernel to be able to find the compiled driver, it must be installed in the /system/drivers directory. This is the only place that the kernel looks for drivers to load.

When an attempt is made to open a device, the kernel first looks for its driver among those that are already loaded. Failing that, it looks on a floppy disk (in /fd/system/drivers ). Failing to find one there, it looks next on the boot disk (in /boot/system/drivers ).

If the /system/drivers directory contains more than one driver for the same device, it's indeterminate which one will be loaded.

You can give your driver any name you wish, as long as it doesn't match the name of another file in /system/drivers.






The Be Book, HTML Edition, for Developer Release 8 of the Be Operating System.

Copyright © 1996 Be, Inc. All rights reserved.

Be, the Be logo, BeBox, BeOS, BeWare, and GeekPort are trademarks of Be, Inc.

Last modified September 6, 1996.