BE ENGINEERING INSIGHTS: Splitting Device Drivers and Bus Managers By Arve Hjonnevag The current release of the BeOS uses kernel add-ons to load device drivers. These device drivers communicate directly with the hardware to provide a standard interface for applications to use. The ISA and PCI buses can always be accessed directly by the CPU, and the kernel has built-in functions that let drivers access these. Other buses, however, like SCSI, IDE, USB, PCMCIA, and 1394 are usually accessed through the devices on the PCI or ISA bus. If someone wants to write a driver for a device connected to one of these buses, it's preferable not to have to communicate directly with the hardware for that bus. To help the implementation of devices connected to these buses, R4 will add the notion of loadable kernel modules that can be used to implement bus managers. A bus manager is a module that allows the driver to access a bus without detailed knowledge of the hardware that controls it. The bus manager will find modules that handle specific buses. The drivers can then scan all buses for the devices they can handle without having to know how to handle the controllers. For R4 the IDE driver has been split up into ATA and ATAPI drivers, an IDE bus manager, and controller modules for generic PCI IDE, BeBox IDE, and Mac IDE. Not all modules have been implemented yet, but the ATA and ATAPI drivers that have been implemented are more functional than the old driver, while allowing third party developers to add support for specific controllers. I will now show you what it takes to implement a module for an IDE controller, but keep in mind that every detail is subject to change. IDE.h defines the interface for the module: typedef struct { bus_manager_info binfo; uint32 (*get_nth_cookie) (uint32 bus); uint32 (*get_bus_count) (); int32 (*get_abs_bus_num) (uint32 cookie); status_t (*acquire_bus) (uint32 cookie); status_t (*release_bus) (uint32 cookie); status_t (*write_command_block_regs) (uint32 cookie, ide_task_file *tf, ide_reg_mask mask); status_t (*read_command_block_regs) (uint32 cookie, ide_task_file *tf, ide_reg_mask mask); uint8 (*get_altstatus) (uint32 cookie); void (*write_device_control) (uint32 cookie, uint8 val); void (*write_pio_16) (uint32 cookie, uint16 *data, uint16 count); void (*read_pio_16) (uint32 cookie, uint16 *data, uint16 count); status_t (*intwait) (uint32 cookie, bigtime_t timeout); status_t (*prepare_dma) (uint32 cookie, void *buffer, size_t *size, bool to_device); status_t (*finish_dma)(uint32 cookie); } ide_bus_info; A module for an IDE controller exports this structure with all the function pointers pointing to the respective functions. All functions have to be implemented, but prepare_dma and finish_dma may return an error if DMA is not supported, and all device drivers need to handle this case. binfo contains the generic bus manager module information. Currently this contains the module name, some flags and a function for initialization and uninitialization. At initialization time, the module finds the hardware it supports, and allocates resources. Specifically it may scan the PCI bus, create areas for DMA tables, and initialize structures and semaphores used to access each bus. At closing time all resources should be freed. The first three functions an IDE module implements are those that allow the IDE bus manager to iterate through all the buses a module handles and map them to a global bus number that the drivers will use. The functions are uint32 get_nth_cookie(uint32 bus) Returns a cookie that the driver uses when accessing the specified bus. The cookie needs to uniquely identify the specified bus. It will normally be a pointer to information about the bus, or an index into an array. uint32 get_bus_count() Returns the number of buses that this module implements. int32 get_abs_bus_num(uint32 cookie) Allows the bus manager to always number the primary and secondary IDE bus in a PC as 0 and 1, and it allows a driver to find out that a drive on a bus is the same as a specific drive that the BIOS uses. Partition utilities use this information to correctly fill in the CHS information in the partition table, and boot managers like lilo need this so that it will boot from the right disk. The function returns 0 if the bus is the primary IDE bus, 1 if it is the secondary, or -1 if it is neither. Let's now look at the functions to access the bus. Since only one device can be active on an IDE bus at a given time, we define two functions that gives a driver exclusive access to the bus. status_t acquire_bus(uint32 cookie); status_t release_bus(uint32 cookie); A normal implementation of these would look like this: static status_t acquire_bus(bus_info *cookie) { return acquire_sem_etc(cookie->mutex, 1, B_CAN_INTERRUPT, 0); } static status_t release_bus(bus_info *cookie) { return release_sem_etc(cookie->mutex, 1, B_DO_NOT_RESCHEDULE); } All the following functions assume that the driver has successfully called acquire_bus. The next four functions provide access to the IDE registers: status_t write_command_block_regs(uint32 cookie, ide_task_file *tf, ide_reg_mask mask); status_t read_command_block_regs(uint32 cookie, ide_task_file *tf, ide_reg_mask mask); uint8 get_altstatus(uint32 cookie); void write_device_control(uint32 cookie, uint8 val); read_command_block_regs and write_command_block_regs allow multiple registers to be updated with one call. All these are straightforward to implement. To do PIO data transfers we use two functions: void read_pio_16(uint32 cookie, uint16 *data, uint16 count); void write_pio_16(uint32 cookie, uint16 *data, uint16 count); These functions read or write the data passed in from and to the IDE data register. The argument count specifies how many 16-bit words to transfer. The last function that has to be implemented is status_t intwait(uint32 cookie, bigtime_t timeout); This function blocks the caller until an interrupt is received from the bus, or until the time specified in the timeout argument has elapsed. Finally we have two functions that a driver uses to do DMA transfers: status_t prepare_dma(uint32 cookie, void *buffer, size_t *size, bool to_device); status_t finish_dma(uint32 cookie); If DMA is not supported these functions return B_NOT_ALLOWED. If the controller supports DMA, prepare_dma sets up the required DMA tables and prepares the controller for a DMA read or write. It is the driver's responsibility to lock the memory before locking the bus. This order is necessary since locking memory may cause disk access to occur. You have now seen how the controller-specific part of an IDE driver can be separated from the device-specific parts. Since most PCs have motherboard IDE buses that are mostly hardware compatible, using modules for IDE controllers is not strictly necessary. It does, however provide cleaner device drivers, and an easy way to support controllers that deviate from the standard. Other bus types also exist where different controllers are incompatible, but it's not feasible for every device driver for these buses to know about different controllers.