Pyro device driver API

Basic description

Device drivers in Pyro are abstracted through the filesystem in much the same way as in UNIX. All device drivers export their functionality through special files inside the /dev/ directory or a sub-directory of /dev/. Unlike in traditional UNIXes the /dev/ directory in Pyro is hierarchial and is very dynamic in that device drivers can add and remove nodes at runtime if it for example controll a removable device that might be added or removed after the driver is initialized.

Driver initialization

A driver is just an ELF binary built in much the same way as a regular DLL. When a driver is loaded by the kernel it will search for a exported symbol named "device_init" and call it to allow the driver to initialize itself. The device_init() function is passed a unique device ID that is dynamically assigned to each driver when loaded and used by the kernel to identify the driver later.

Just before the driver is unloaded the kernel will look for a global function named "device_uninit()" and if found it will be called to allow the driver to clean up.

There is also a "device_release()" function which is called for every claimed device before device_uninit() is called for the driver.

This device_init() and device_uninit() are the only functions that will be called directly by the kernel. For the driver to do any usefull work it must export one or more device-nodes through the Pyro Device FS. This is a logical file system that is mounted at "/dev/" and controll all devices. Each device is present as a magic file located in "/dev/" or a sub-directory of "/dev/". Initially "/dev/" only contain "/dev/null" and "/dev/zero" which is controlled by the Pyro VFS itself. All other directory and device-nodes are created by device drivers. A device driver can create a device-node with create_device_node() and remove it with delete_device_node(). When creating a device node the driver must provide a function pointer table with entry points to the drivers functionality. The functions in the table will be called by the Pyro VFS to controll the device. The most important functions are read(), write() and ioctl() but there are also functions to open/close the device aswell as functions called by select() to make it possible for one thread to wait for IO on multiple devices.

A typical device driver will create one node in device_init() and delete it again in device_uninit().

int g_nMyDeviceNode = -1;
status_t device_init( int nDeviceID )
{
struct MyDeviceNode sNode;
g_nMyDeviceNode = create_device_node( nDeviceID, -1, "misc/mydevice", &g_sOperations, &sNode );
if ( g_nMyDeviceNode < 0 ) {
return( g_nMyDeviceNode ); /* Failed to create the device node */
} else {
return( EOK );
}
}
status_t device_uninit( int nDeviceID )
{
delete_device_node( g_nMyDeviceNode );
return( EOK );
}

How a driver is located

Since the nodes inside /dev/ are created by the device drivers themself and not by the kernel it is not directly obvious what driver should be loaded if an application tries to open for example "/dev/video/cpia".

If this is the first time someone attempts to open the CPiA device the driver is not loaded and "/dev/video/cpia" does not exists. If this is the first time anything inside /dev/ is touched neighter does the "/dev/video" directory.

To make it possible for the kernel to search for drivers in a efficient way the driver-binaries are located in a directory tree similar to the resulting tree inside /dev/. For example the CPiA driver from the above example whould be located in "/System/drivers/dev/video/cpia".

If the kernel is asked to open for example "/dev/video/cpia" it would start by opening the "/dev" directory which would cause the "/System/drivers/dev" directory to be iterated. During the iteration all drivers found will be attempted to load and initiate and all directories will be replicated inside /dev/. Since "/System/drivers/dev" contains a directory named "video" this will cause "/dev/video" to be created. When "/dev/" is successfully opened it will attempt to open "/dev/video" which should now exist. Opening "/dev/video" will cause the "/System/drivers/dev/video" directory to be iterated and the "cpia" binary to be loaded. The CPiA driver will then probe for a CPiA device and if found it will create a device node named "/dev/video/cpia" which will then be found and opened when the kernel descend into the "/dev/video" directory.

In the trivial example above there was direct match between the name of the driver and the name of the device inside /dev. Since one driver might export more than one device this is not always the case. For example a IDE disk driver whould export one device for each disk connected to the bus and one device for each partitions found on those disks. The device-tree exported by a IDE driver might look something like this:

/dev/disk/hda/raw
/dev/disk/hda/0
/dev/disk/hda/1
/dev/disk/hdb/raw
/dev/disk/hdc/raw
/dev/disk/hdc/0

In this case the ide driver should be located in "/System/drivers/dev/disk/ide". If someone attempts to open the first partition on the master disk connected to the first controller it whould have to open "/dev/disk/ide/hda/0".

When descending the path the kernel will first create the "/dev/disk" and the "/dev/disk/ide" directory. Then it will load the ide-driver which will detect that there are 3 disks connected to the two controllers before decoding the partition tables and add all the nodes listed above. At his point "/dev/disk/ide/hda/0" already exists and no other drivers need to be loaded to fullfill the request.

Using busmanagers

To keep the kernel small and avoid recompiling when adding support for new hardware technology, the management code for busses like PCI and USB lives in kernel modules called busmanagers. These busmanagers are loaded by the bootmanager if they are necessary for loading the system ( e.g. the PCI busmanager is necessary for the IDE driver ) or later after the root disk is mounted. Using busmanagers from drivers is really easy: You just have to ask the kernel to give you access to a busmanager with a specific name and the API version you support. If you would like to access the PCI bus, maybe to scan for supported devices, you would do something like this:

PCI_bus_s* psBus = get_busmanager( PCI_BUS_NAME, PCI_BUS_VERSION );
if( psBus == NULL )
{
// handle error
}

You would now have a pointer to a PCI_bus_s structure, which is defined in the PCI busmanager header file and contains pointers to the busmanager's functions. Access to the USB bus works similar, alhough of course the functions in the USB_bus_s structure are different.

Device management

Although the Pyro kernel driver interface is easy to use, it has one problem: It is possible that two device drivers try to access one device. Also, the kernel does not know what devices are supported by the drivers and so cannot show a list of the supported/not supported hardware to the user. To solve this problems, the kernel contains a special device manager. A device can be registered by any device driver or busmanager and is later claimed by the driver that wants to access the device. All later tries by other drivers to claim the device will fail, and so the driver knows that it should not use this device. If you write a PCI or USB device driver, the busmanager can give you the handles to the devices it knows. All you have to do is to call the claim_device() function:

int nHandle = sMyDevice.nHandle // get Handle from the busmanager
if( claim_device( nDeviceID, // device ID of the driver
nHandle,
"My device", // name for the device
DEVICE_AUDIO ) // type of the device
!= 0 ) {
// handle error
}

If you have claimed any device then the kernel will call the "device_release()" driver function for this device. An implementation could look like this:

status_t device_release( int nDeviceID, int nDeviceHandle, void* pData )
{
release_device( nDeviceHandle ); // Release device
... // free device data, release irq, ...
}

The data pointer points to the data set which is the by the set_device_data() function.

Please note that if you support hardware that can be removed while the computer is running ( e.g. USB devices ) you have to call release_device( nHandle ) if the busmanager informs you about the removal.

If you want to support hardware, which is not managed by any busmanager, you can do a register_device() call yourself:

int nHandle = register_device( "My device", // name
"isa" ); // busname
claim_device( nDeviceID, nHandle, "My device", DEVICE_AUDIO );

This will not prevent two drivers to access one device, but it will show some nice information to the user.

Powermanagement

Before the computer goes into standby mode the kernel will call the "device_suspend()" function of the driver. In this function you should save the hardware state and put the device into a lower power mode:

status_t device_suspend( int nDeviceID, int nDeviceHandle, void* pData )
{
... // save state
}

When the computer wakes up again, the kernel calls the device_resume() function which takes the same parameters as the device_suspend() function. The driver has to wakeup the device and restore its state here.

Reducing boottime by disabling device drivers.

To avoid unnecessary tries to load drivers, it is possible to disable device drivers. This feature can currently be used by PCI, ACPI and USB device drivers. If you did not find any supported hardware, you should call disable_device_on_bus( nDeviceID, "bus name" ). This will disable the device driver until the busmanager detects a hardware change.