Device Drivers

Since KUDOS is a realistic operating system, it can use hardware devices to interact with the outside world. Hardware devices include things like consoles, disks and network interface adapters.

Device drivers provide an interface between the hardware devices and the operating system. Device drivers use two hardware-provided mechanisms intensively: they depend on hardware generated interrupts and command the hardware with memory mapped I/O.

Most hardware devices generate interrupts when they have completed the previous action or when some asynchronous event, such as user input, occurs. Device drivers implement handlers for these interrupts and react to events.

Memory mapped I/O is an interface to the hardware components. The underlying machine provides certain memory addresses which are actually ports in hardware. This makes it possible to send and receive data to and from hardware components. Certain components also support block data transfers with direct memory access (DMA). In DMA the data is copied between main memory and the device without going through the CPU. Completion of DMA transfer usually causes an interrupt.

Interrupt driven device drivers can be thought to have two halves, top and bottom. The top half is implemented as a set of functions which can be called from threads to get service from the device. The bottom half is the interrupt handler which is run asynchronously whenever an interrupt is generated by the device. It should be noted that the bottom half might be called also when the interrupt was actually generated by some other device which shares the same interrupt request channel (IRQ).

Top and bottom halves of a device driver typically share some data structures and require synchronized access to that data. The threads calling the service functions on the top half might also need to sleep and wait for the device. Resource waiting (also called blocking or sleeping) is implemented by using the sleep queue or semaphores. The syncronization on the data structures however needs to be done on a lower level since interrupt handlers cannot sleep and wait for access to the data. Thus the data structures need to be synchronized by disabling interrupts and acquiring a spinlock which protects the data. In interrupt handlers interrupts are already disabled and only spinlock acquiring is needed.

Interrupt Handlers

All device drivers include an interrupt handler. When an interrupt occurs the system needs to know which interrupt handlers need to be called. This mechanism is implemented with an interrupt handler registration scheme. When the device drivers are initialized, they will register their interrupt handler to be called whenever specified interrupts occur. When an interrupt occurs, the interrupt handling mechanism will then call all interrupt handlers which are registered with the occured interrupt. This means that the interrupt handler might be called although the device has not generated an interrupt.

The registered interrupt handlers are kept in the table interrupt handlers which holds elements of type interrupt entry t. The fields of this structure are described in the following table:

Type Name Explanation
device_t device The device for which this interrupt is registered.
uint32_t irq The interrupt mask. Bits 8 through 15 indicate the interrupts that this handler is registered for. The interrupt handler is called whenever at least one of these interrupts has occured.
void (*) (device_t *) handler The interrupt handler function called when an interrupt occurs. The argument given to this function is device.
Fields in structure interrupt entry_t.

void interrupt_register (uint32_t irq, void (*handler)(device_t *), device_t device)

  • Registers an interrupt handler for the device. irq is an interrupt mask, which indicates the interrupts this device has registered. Bits 8 through 15 indicate the registered interrupts. handler is the interrupt handler called when at least one of the specified interrupts has occured. This function can only be called during bootup.
  • Implementation:
    1. Find the first unused entry in interrupt_handlers.
    2. Insert the given parameters to the found table entry.

void interrupt_handle (uint32_t cause)

  • Called when an interrupt has occured. The argument cause contains the Cause register. Goes through the registered interrupt handlers and calls those interrupt handlers that have registered the occured interrupt.
  • Implementation:
    1. Clear software interrupts.
    2. Call the appropriate interrupt handlers.
    3. Call the scheduler if appropriate.

Device Abstraction Layers

The device driver interface in KUDOS contains several abstraction layers. All device drivers must implement standard interface functions (initialization function and possibly interrupt handler) and most will also additionally implement functions for some generic device type. Three generic device types are provided in KUDOS: generic character device (gcd), generic block device (gbd) and generic network device (gnd). These can be thought as “superclasses” from which the actual device drivers are inherited.

Generic character device is a device which provides uni- or bidirectional bytestream. The only such device preimplemented in KUDOS is the console. Generic block device is a device which provides random read/write access to fixed sized blocks. The only such device implemented is the disk driver. These interfaces could also be used to implement stream based network protocol or network block device, for example. The interface for generic network device is also given.

All device drivers must have an initialization function. A pointer to this function must be placed in the drivers_available array in drivers/$ARCH/drivers.c, together with a designated name and a device typecode identifier. Device typecodes which are defined in drivers/device.h. The system will initialize the device drivers on bootup for each device in the system by calling these initialization functions. This initialization is done by device_init(), found in drivers/$ARCH/device.c.

Device Driver Initialization

Every device driver’s initialization function must return a pointer to the device descriptor (device_t) for this device, described in kudos/drivers/device.h.

Device driver initialization code is called from init() on bootup. The function called is:

void device_init(void)

Finds all devices connected to the system and attempts to initialize device drivers for them.

Implementation:

  1. Loop through the device descriptor area of YAMS.
  2. For each found device, try to find the driver by scanning through the list of available drivers (drivers_available in kudos/drivers/$ARCH/drivers.c).
  3. If a matching driver is found, call its initialization function and print the match to the console. Store the initialized driver instance to the device driver table device_table.
  4. Otherwise print a warning about an unrecognized device.

After device drivers are initialized, we must have some mechanism to get a handle of a specific device. This can be done with the device_get function:

device_t *device_get(uint32_t typecode, uint32_t n)

Finds initialized device driver based on the type of the device and sequence number. Returns nth initialized driver for device with type typecode. The sequencing begins from zero. If device driver matching the specifield type and sequence number if not found, the function returns NULL.

Generic Character Device

The generic character device (GCD) is an abstraction for any character-buffered (stream based) I/O device (e.g. a terminal). A GCD specifies read and write functions for the device, which have the same syntax for every GCD. Thus, when using GCD for all character device implementations, the code which reads or writes them does not have to care whether the device is a TTY or some other character device.

The generic character device is implemented as a structure with the fields described in the gcd_t structure in kudos/drivers/gcd.h.

Generic Block Device

The generic block device (GBD) is an abstraction of a block-oriented device (e.g. a disk). GBD consists of a function interface and a request data structure that abstracts the blocks to be handled. All functions are implemented by the actual device driver.

The function interface is provided by the gbd_t data structure in kudos/drivers/gbd.h. To use this interface, it is necessary to describe requests in detail; for this, the gbd_request_t data structure is used. This structure includes all necessary information related to the reading or writing of a block.

The GBD interface supports both synchronous and asynchronous calls (see the gbd.h file for the practical details).

In case of asynchronous calls, the gbd interface functions will return immediately. This means that the user must wait on an associated kernel semaphore before continuing. Memory reserved for the request may not be released until the semaphore is released. The thread using a GBD device must be very careful especially with reserving memory from function stacks (ie. static allocation). If the function is exited before the request is served, the memory area of the request may corrupt.

In case of synchronous calls, the gbd interface functions will block until the request is handled. The memory of the request data structure may be released when control is returned.