OpenPOD: Portable driver API specification¶
Introduction¶
OpenPOD is a specification of a driver API, structure and lifecycle.
Aim of this specification is to provide small and alternative operating systems with common basis to share drivers around.
Source codes for the project (and for this book) can be found in OpenPOD GitHub repository.
Rationale¶
There are tens of operating system projects in the world. From tiny experimental works to medium size projects to big and big old ones with a bing community.
All of them need drivers. All of OS authors work on quite the same drivers from early start and for ever and ever.
It seems just natural for OS authors community to define a standard for a driver API to share work around and help each other.
This document is an attempt to start such effort.
Goal is to define API and driver structure that is:
- Simple enough. There were previous efforts to define portable driver API which required driver (even simplest one) to be unnecessarily complex. This specification lets driver author to start from a simple skeleton and make trivial driver in a day or so.
- Extencible. It must let complex and specific driver to have reach API into the kernel which does not limit driver designer too much and does not enforce him to come around and add non-standard entry points.
- Support different device classes naturally. Video and network drivers are really different and provide APIs that have quite nothing in common. But drivers in each class generally provide identical entry points.
- Support runtime device addition and removal. Driver must be able to inform kernel about status change and add or remove devices in run time.
- Support drivers providing different types of devices. PS2 driver exports both keyboard and mouse. Video card driver can provide sound device for an HDMI port, and so on.
- Let driver to be both statically and dynamically linked. Though, current specification is not really checked in a project with a dynamic driver loading, there is corresponding support in API and data structures.
Current state of specification: It is partially complete and waits for your comments.
The best way to comment is to create an issue in project’s GitHub repository.
What’s inside¶
This specification covers:
- Services provided to the driver by kernel
- Driver start / device discover / stop procedures (driver lifecycle)
- Device announce to the kernel (device lifecycle)
- Device API
- Configuration (PCI) information access
- Asyncronous IO requests
Driver structure¶
OpenPOD driver includes:
Driver descriptor. A structure that defines global (not device specific) properties an entry points.
Driver descriptor is a required part of a driver.
Device descriptor. A structure which describes each device exported by driver. Can be single and static, or can be dynamically allocated in run time. For example, USB disk driver can create device descriptors dynamically as devices come and go.
Device descriptor is not a required part - there can be driver which does not export devices at all or just starts with empty set of devices. But most drivers will provide at least one device at start.
Driver and/or device properties descriptors. Set of key=value parameters to control driver or device and request meta information. For example, sound driver can set sampling rate with corresponding property.
Properties are optional part of specification.
Driver and device class¶
Device class specifies API this device exports to the kernel.
#define POD_DEV_CLASS_VOID 0 // No API.
#define POD_DEV_CLASS_SPECIAL 1 // Has user defined nonstandard interface
#define POD_DEV_CLASS_VIDEO 2 // Framebuf, bitblt io
#define POD_DEV_CLASS_BLOCK 3 // Disk, cdrom: block io
#define POD_DEV_CLASS_CHARACTER 4 // Tty, serial, byte io
#define POD_DEV_CLASS_NET 5 // Packet IO, has MAC address
#define POD_DEV_CLASS_KEYBD 6 // Key (make/break) events
#define POD_DEV_CLASS_MOUSE 7 // Mouse coordinate events
#define POD_DEV_CLASS_MULTIPLE 0xFF // Driver only, has multiple dev types
Note
Sound driver class is missing.
Sound device is, basically a character (byte stream) device, but separate class can help kernel to classify and connect driver in a special way.
Driver descriptor¶
typedef struct pod_driver
{
uint32_t magic;
uint8_t API_version_major;
uint8_t API_version_minor;
uint8_t arch_major;
uint8_t arch_minor;
uint8_t class_id;
uint8_t pad0;
uint8_t pad1;
uint8_t state_flags;
char *name;
pod_dev_required_f calls;
pod_dev_optional_f *optional;
pod_properties *prop;
void *private_data;
kernel_f *kernel_driver_api;
} pod_driver;
All the fields of this structure will be described in reference part of this document, now we will just note the most interesting ones.
- state_flags
- Used to track driver state, such as if driver found its hardware or not.
- name
- Driver name, printable text for user to ideitify it.
- calls
- Main set of driver entry points, see below.
- private_data
- Private driver’s state. Simple drivers keep here a link to an only device descriptor.
All the other fields can be zeroes or have default predefined values.
Driver methods¶
- pod_construct
- Driver initialization. Driver must allocate structures and do any setup required but do not scan hardware or attempt to export devices. This stage can be empty.
errno_t pod_construct( pod_driver *drv ); // ENOMEM
- pod_destruct
Complete driver destruction. Driver must free all resources, stop threads, release OS objects (such as mutexes and conds). Driver can be unloaded after this call.
It is assumed that
deactivate
was called before this call. Nevertheless if nodeactivate
was called driver must attempt to do its best to deactivate self before desctruction. If it is not possible, this call can fail.
errno_t pod_destruct( pod_driver *drv );
pod_activate
Start driver. Prepare and activate hardware. Inform kernel about all devices this driver exports. See
pod_dev_link
.This call is done before any IO is attempted by kernel. Until this call driver must refuse to do any IO.
Hardware scan must be done before this call.
errno_t pod_activate( pod_driver *drv );
pod_deactivate
Stop driver. Reset and stop hardware. Revoke all exported devices. Seepod_dev_unlink
.
errno_t pod_deactivate( pod_driver *drv );
pod_sense
Look for hardware to exist and be operational. Scan for devices that can be operated by this driver.
Devices must be found but not exported to kernel or started.
It is ok to call kernel entry points to scan hardware, such as PCI enumeration functions.
errno_t pod_sense( pod_driver *drv );
pod_probe
This is a call from kernel with an offer of a device to handle.
Driver must check if this device can be handled and accept or refuse to handle it. As with
pod_sense
accepted device should not be started or exported to kernel during processing of this call.This entry point is not finally defined. Request for comments.
errno_t pod_probe( pod_driver *drv, bus?, dev? ); // ENOMEM, EFAULT
Driver life cycle¶
Defines driver initialization, start, search for hardware, stop and resource release stages.
Basic life cycle:
- Kernel calls
pod_construct
, driver allocates data strutures and resources (mutexes, threads, etc). - Kernel calls
pod_sense
and/orpod_probe
, driver looks for hardware and decides on list of devices that exist and can be served. - Kernel calls
pod_activate
, driver exports to kernel known devices with calls topod_dev_link
. - Driver serves IO requests. If devices come or go driver calls
pod_dev_link
/pod_dev_unlink
to inform kernel. - Kernel calls
pod_deactivate
, driver deregisters all devices. - Kernel calls
pod_destruct
, driver releases all resources. - Kernel can unload driver at this point.
Device descriptor¶
struct pod_device {
uint32_t magic;
uint8_t class;
uint8_t pad0;
uint8_t pad1;
uint8_t flags;
pod_driver *drv;
pod_dev_f *calls;
pod_properties *prop;
void *class_interface;
void *private_data;
// Fields below are used by default framework code
// Request queue, used by pod_dev_q_ functions
pod_q *default_r_q; // default request q
pod_request *curr_rq; // request we do now
pod_thread *rq_run_thread; // thread used to run requests
pod_cond *rq_run_cond; // triggered to run next request
};
Device methods¶
pod_dev_stop
Stop device. Called after all IO is done and all possible users disconnected. In Unix like OS this entry point is called after device was closed by least process. No IO can be done after stop.
pod_dev_start
Start device. Called before first IO attempt on device.
In Unix like OS corresponds to open system call for this device.
errno_t pod_dev_stop( pod_device *dev );
errno_t pod_dev_start( pod_device *dev );
pod_rq_enqueue
Start asyncronous IO on device.
pod_rq_dequeue
Revoke IO request previously enqueued by
pod_rq_enqueue
, if possible. Can fail if IO is already in progress.This entry point is optional.
pod_rq_fence
Make sure that all IO started before this call is finished. Can return instantly or wait for all previous IO is complete. In any case must guarantee that on call to request callback for this request all previously enqueued requests are complete and callbacks for them are finished too.
This entry point is optional.
pod_rq_raise
Change (usually - rise:) request priority.
Optional, but for disk IO it is really good thing to have.
errno_t pod_rq_enqueue( pod_device *dev, pod_request *rq );
errno_t pod_rq_dequeue( pod_device *dev, pod_request *rq );
errno_t pod_rq_fence( pod_device *dev, pod_request *rq );
errno_t pod_rq_raise( pod_device *dev, pod_request *rq, uint32_t io_prio );
Device life cycle¶
- Driver detects device, constructs descriptor and calls
pod_dev_link
for this device. - Kernel connects device according to device class.
- Kernel decides to use device, calls
pod_dev_start
for it. - Kernel calls sync interface methods or sends in IO requests for async IO. Device executes requests. See below for details.
- Kernel decides to cease using device, calls
pod_dev_stop
.
There are two possible interfaces that device can implement.
Asyncronous interface¶
Main entry point for this iterface is pod_rq_enqueue
entry point. Kernel constructs
pod_request
structure and calls pod_rq_enqueue
. Driver adds request to queue and
returns immediately. As soon as request is processed by driver request is filled with
results (status code) and done
function is called. (Pointer to done
function is in
the request structure.)
OpenPOD project library provides helper functions for working with request queue.
There is also a proxy function (pod_default_enqueue
) provided that converts asyncronous
calls to syncronous ones, so for a simple driver just a set of sync entry points can
be provided. We discourage writing drivers this way, but for a really fast tasks or
alpha level drivers this is ok.
Here is an example of making asyncronous request to a video driver. (Well, it is meaningless to do this call in async style, but this example shows that just any call can be done in both sync and async style.)
pod_request *rq = calloc( 1, sizeof(pod_request) + sizeof(struct pod_video_rq_mode) );
if( rq == 0 ) { /* fail */ }
struct pod_video_rq_mode *rq_arg = ((void*)rq) + sizeof(pod_request);
rq->request_class = POD_DEV_CLASS_VIDEO;
rq->operation = pod_video_getmode;
rq->io_prio = 0;
rq->op_arg = rq_arg;
rq->done = req_done_func;
rc = pod_rq_enqueue( dev, rq );
if( rc ) { /* fail */ }
After completing request done
function (req_done_func()
in this example)
will be called and rq_arg
will be filled with requested data.
Warning
Callback can be called before pod_rq_enqueue
return!
Don’t rely on assumption that pod_rq_enqueue
will return first and callback
function called next. For long calls such as (non flash) disk read can beheave
this way repeatedly, but it is never guaranteed.
For example, it is extremely wrong to write something like
rc = pod_rq_enqueue( dev, rq );
pod_kernel_lock_mutex( ... )
pod_kernel_wait_cond( ... )
pod_kernel_unlock_mutex( ... )
It is ok to do it this way:
pod_kernel_lock_mutex( ... )
rc = pod_rq_enqueue( dev, rq );
pod_kernel_wait_cond( ... )
pod_kernel_unlock_mutex( ... )
But signalling cond in done
function must be guarded by the same
mutex too.
Syncronous interface¶
Each device class can provide a set of methods that is specific for this class. An
class_interface
field of device descriptor points to an array of functions, which
implement class specific operations. There are pod_io_<CLASS_NAME>.h
headers
defining class specific interfaces. Currently interfaces for block, network and video
drivers are provided.
These methods are syncronous and return only if operation requested is done or error occured.
It is possible to set up proxy code that converts syncronous calls to asyncronous ones, so driver developer has to provide just one of them.
struct pod_video_rq_mode rq_arg;
rc = pod_dev_method( dev, pod_video_getmode, &rq_arg );
if( rc ) { /* fail */ }
Class specific interfaces¶
Both syncronous and asyncronous entry points implement same set of operations.
For async interface operation is selected by request_class
, operation
pair
of request structure fields. Of course, request_class must be equal to target device’s
class.
Sync interface selects function by calling correct entry point. There is a helper function provided, but user code can do the same directly.
errno_t pod_dev_method( pod_device *dev, int op_id, void *param )
{
if( dev == 0 ) return ENODEV;
if( (op_id < 0) || (dev->class_interface == 0) ) return ENOSYS;
errno_t (*class_func)(pod_device *dev, void *arg);
// TODO check op id > max possible
class_func = dev->class_interface[op_id];
if( class_func == 0 ) return ENOSYS;
return class_func( dev, param );
}
Video IO interface¶
Common types and structues.
typedef enum pod_video_operartions
{
pod_video_nop,
pod_video_getmode, pod_video_setmode, pod_video_getbestmode,
pod_video_clear_all, pod_video_clear,
pod_video_move,
pod_video_write, pod_video_read,
pod_video_write_part, pod_video_read_part
} pod_video_operartions;
// Pixel format, pod_pixel_rgba is preferred
typedef enum pod_pixel_fmt
{
pod_pixel_rgba, // 32 bit RGBA, A byte is ignored by HW
pod_pixel_rgb, // 24 bit RGB
pod_pixel_r5g6b5, // 16 bit, 5-6-5
pod_pixel_r5g5b5, // 16 bit, 5-5-5
} pod_pixel_fmt;
// flags
#define POD_VIDEO_IGNORE_Z (1<<0) // ignore z coordinate (but update z buffer)
#define POD_VIDEO_IGNORE_A (1<<1) // ignore A byte (aplha channel)
Clear:
struct pod_video_rq_sqare
{
uint32_t x, y;
uint32_t x_size, y_size;
};
Move from screen to screen:
struct pod_video_rq_2sqare
{
uint32_t from_x, from_y;
uint32_t from_x_size, from_y_size;
uint32_t to_x, to_y;
uint32_t to_x_size, to_y_size;
};
Put or get complete bitmap:
// write, read
struct pod_video_rq_rw
{
uint32_t x, y;
uint32_t x_size, y_size;
uint32_t z;
uint32_t flags;
char *buf;
pod_pixel_fmt buf_fmt;
};
Put or get partial bitmap:
// write_part, read_part
struct pod_video_rq_rw_part
{
uint32_t from_x, from_y; // point to start in buf
uint32_t from_x_size, from_y_size; // full size of bitmap in buf
uint32_t to_x, to_y; // point to start on screen
uint32_t move_x_size, move_y_size; // size of sqare to move
uint32_t z; // z position
uint32_t flags;
char *buf;
pod_pixel_fmt buf_fmt;
};
Parameters for video mode related ops:
// getmode, setmode, getbestmode
struct pod_video_rq_mode
{
uint32_t x_size, y_size;
pod_pixel_fmt buf_fmt;
physaddr_t vbuf; // может быть 0? тогда всё через методы?
};
Block IO interface¶
There are three possible operations defined for block IO devices.
enum pod_block_operartions
{
pod_block_nop,
pod_block_read, pod_block_write,
pod_block_trim // SSD specific
};
Parameters for trim
operation:
struct pod_block_rq
{
diskaddr_t block_no;
uint32_t block_count;
uint32_t block_size;
};
Parameters for data IO (read, write):
struct pod_block_io_rq
{
diskaddr_t block_no;
uint32_t block_count;
uint32_t block_size;
physaddr_t physmem_addr;
};
Network IO interface¶
Network class specification is incomplete.
enum pod_video_operartions
{
nop, get_mac_addr,
mac_send, mac_recv,
//ip_send, ip_recv,
};
Parameters for get_mac_addr
operation:
#define POD_NET_MAX_MAC 16
// get_mac_addr
struct pod_net_rq_mac
{
uint8_t mac_len;
uint8_t mac[POD_NET_MAX_MAC];
};
Examples¶
Simplest driver skeleton¶
lets review a simplest driver example provided in project repository.
Please see barebones/simple
directory for this example’s source code.
Driver descriptor¶
First we need to provide driver descriptor structure.
pod_driver simple_driver = {
POD_DRIVER_MAGIC,
POD_DRIVER_VERSION,
POD_DRIVER_ARCH_UNKNOWN,
POD_DEV_CLASS_VIDEO, 0, 0, 0,
"simple driver skeleton - RENAME ME!",
{
simple_driver_construct,
simple_driver_destruct,
simple_driver_activate,
simple_driver_deactivate,
simple_driver_sense
},
0, // no optional entry points
0, // no properties
0, // Private state does not exist yet
0, // Statically linked, no need for kernel entry points struct
};
The only notable things here are:
POD_DEV_CLASS_VIDEO
- we declare that this is a video driver. OS might decide to start different drivers sooner or later, or to group drivers start by classes.simple_driver_X
- entry points to start and stop our driver by OS.
Device descriptor¶
Our driver will provide just one and only device, so device descriptor can be static too.
pod_device simple_device =
{
POD_DEVICE_MAGIC, // magic
POD_DEV_CLASS_VIDEO,
0, 0,
0, // flags
// Device name, either static or runtime-generated
"simple driver skeleton device name - RENAME ME!",
&simple_driver,
&dev_func, // dev io entry points
0, // no properties
0, // dev class specific interface
0, // private data
0, // default request q
0, // request we do now
0, // thread used to run requests
0, // triggered to run next request
};
Again, we note that dev class is POD_DEV_CLASS_VIDEO
. It is critical to be
correct - OS will connect device to its users based on this.
We provide device entry points for async interface, but all of them are default ones provided by OpenPOD library.
static pod_dev_f dev_func =
{
pod_default_enqueue,
pod_default_dequeue,
pod_default_fence,
pod_default_raise
};
First of them, pod_default_enqueue
, will just call syncronous entry point of driver,
three other are no-ops.
Driver functions¶
All the driver lifecycle functions are trivial.
Constructor and destructor (simple_driver_construct
and simple_driver_destruct
) are just
empty - we don’t need them for this driver to work.
simple_driver_activate¶
This function is mostly copied from system default implementation.
simple_driver_deactivate¶
Again, function is copied from system default implementation.
Reference¶
This section is not ready yet.