Writing Driver Translators#
A device driver translator (also called onidriver
) is a small piece of
software that sits between the public liboni API and the low-level
libraries or kernel drivers handling actual hardware, taking care of all the
implementation details. This allows the API to remain hardware-agnostic and
work with a wide variety of host devices.
Writing a device driver translator requires detailed knowledge of the target hardware. However, from the API perspective, it is as simple as defining a driver context and implementing all functions defined in onidriver.h.
Driver Context#
A driver context is a data structure containing all state information about a
particular instance of the driver. An instance of this structure will be
created during liboni context creation phase and passed to the main API as an
opaque pointer of type oni_driver_ctx
and stored in a field of
oni_ctx
during its lifetime.
Tip
The driver context contents are completely opaque to liboni, so its contents are completely up to the developer and the requirements of the driver translator itself.
Warning
All state information must be contained in this context, instanced
in oni_driver_create_ctx()
and accessed through the oni_driver_ctx
parameter present in most function calls. No static variables can be used to
keep any state information. This is necessary for multiple instances of the driver
to exist simultaneously, accessing different devices of the same type.
An example of such a context for a simple, file-descriptor accessed device, could be
struct simple_driver_ctx_impl {
int fd_in; // ONI data input stream
int fd_out; // ONI data output stream
int fd_sig; // ONI signal input stream
int fd_reg; // ONI register IO interface
};
typedef simple_driver_ctx_impl* simple_driver_ctx;
Driver Functions#
When liboni operation requires hardware access, it calls a set of functions present on the driver translator, which take care of the appropriate low-level calls. See onidriver.h for a complete description of these functions and its parameters.
Warning
All functions present in onidriver.h must be implemented, even if they are not actively used.
Note
All examples shown in this page, are only orientating and lack elements, such as state checks, that are required in real development.
Tip
Most functions have the same return scheme, 0, or ONI_ESUCCESS
on successful operation, or any of the Error Codes on failure.
Commonly, this error is passed up to the public API and used as return value
of the function called by the user. The specific
error value is up to the driver developer.
Tip
Since most functions receive a Driver Context parameter
in the form of a oni_driver_ctx
opaque pointer, a cast to the
appropriate structure pointer is required. It is handy to define a macro
to take care of this, instead of manually typing the cast in
every function. For example:
#define CTX_CAST const simple_driver_ctx ctx = (simple_driver_ctx)driver_ctx;
This macro will be used in all the examples on this page.
Driver translator functions can be organized in the following categories:
Context Management#
The three functions responsible for context management are
oni_driver_create_ctx()
, oni_driver_init()
and
oni_driver_destroy_ctx()
.
oni_driver_create_ctx()
is responsible for creating the context
instance and allocating all required resources. No hardware access should be
performed in this function, only internal memory allocations as required.
oni_driver_init()
is where actual hardware initialization is done. This
function opens the relevant hardware channels and prepares the driver for normal
operation.
oni_destroy_ctx()
must close any open hardware connection and release
all allocated resources.
oni_driver_ctx oni_driver_create_ctx()
{
simple_driver_ctx ctx = calloc(1,sizeof(simple_driver_ctx_impl));
return ctx;
}
int oni_driver_init(oni_driver_ctx driver_ctx, int host_idx)
{
CTX_CAST;
ctx->fd_in = open("/dev/instr",O_RDONLY);
ctx->fs_out = open("/dev/outstr",O_WRONLY);
...
return ONI_ESUCCESS;
}
int oni_driver_destroy_ctx(oni_driver_ctx)
{
CTX_CAST;
close(ctx->fd_in);
close(ctx->fd_out);
...
free(ctx);
return ONI_ESUCCESS;
}
Stream I/O#
Functions oni_driver_read_stream()
and
oni_driver_write_stream()
are where access to the ONI-defined hardware data streams is performed. Read
operations can be done on the input and signal streams and write operations
on the output streams.
Specific low-level stream access is completely dependent on the hardware interface used.
int oni_driver_read_stream(oni_driver_ctx driver_ctx, oni_read_stream_t stream, void *data, size_t size)
{
CTX_CAST;
if (stream == ONI_READ_STREAM_DATA) return read(ctx->fd_in, data, size);
else if (stream == ONI_READ_STREAM_SIGNAL) return read(ctx->fd_sig, data, size);
else return ONI_EPATHINVALID
}
int oni_driver_write_stream(oni_driver_ctx driver_ctx, oni_write_stream_t stream, const char *data, size_t size)
{
CTX_CAST;
if (stream == ONI_WRITE_STREAM_DATA) return write(ctx->fs_out, data, size);
else return ONI_EPATHINVALID;
}
Note
Read operations must return the same number of bytes as requested, or it will be treated as an error.
Register access#
Access to the register interface described on the ONI specification
is done through the functions oni_driver_read_config()
and oni_driver_write_config()
Again, the specifics on how to access such registers are dependent on the hardware interface.
Tip
These functions can be used to perform additional actions when the API accesses specific registers. An example would be a device that requires some additional low-level actions, besides the usual register trigger, when performing a reset or starting/stopping acquisition.
int oni_driver_read_config(oni_driver_ctx driver_ctx, oni_config_t config, oni_reg_val_t *value)
{
CTX_CAST;
lseek(ctx->fd_reg,reg_to_address(config));
read(ctx->fd_reg,value,sizeof(oni_reg_val_t));
return ONI_ESUCCESS;
}
int oni_driver_write_config(oni_driver_ctx driver_ctx, oni_config_t config, oni_reg_val_t value)
{
//Example of using this function to perform additional low-level actions
if (config == ONI_CONFIG_RESET && value != 0) ioctl(ctx->fd_reg, CUSTOM_IOCTL_RESET);
lseek(ctx->fd_reg,reg_to_address(config));
write(ctx->fd_reg,&value,sizeof(oni_reg_val_t));
}
Option Callback#
While some options set by oni_set_opt()
translate to hardware register
access (and thus oni_driver_write_config()
or
oni_driver_read_config()
calls), not all of them do, with some setting
some internal software parameters in liboni. However, there might be
cases where the hardware or the driver translator might need to be aware of
these settings. An example of this could be hardware requiring knowledge of the
block read size (ONI_OPT_BLOCKREADSIZE
) to optimize internal
buffering parameters.
To solve this, the driver translator function
oni_driver_set_opt_callback()
gets called at the end of any successful
oni_set_opt()
call, with its same parameters. This allows the driver
translator to act accordingly. The result of this function will be returned as
the result of oni_set_opt()
.
If the driver translator does not require any action on any option, this
function can simply return ONI_ESUCCESS
.
Tip
Even for options that perform register access, this function is different from
adding extra actions in register I/O calls because it is called after any
library operations are also performed. For example, if
oni_driver_set_opt_callback()
were to react to
ONI_OPT_RESET
, it would act after the new device table
has been loaded, while an extra action in oni_driver_write_config()
would act before, during low-level register access.
int oni_driver_set_opt_callback(oni_driver_ctx driver_ctx, int oni_option, const void *value, size_t option_len)
{
//Example of a device reacting to changes in block read size
CTX_CAST;
if (oni_option == ONI_OPT_BLOCKREADSIZE)
{
oni_size_t size = *(oni_size_t*)value;
ioctl(ctx->fd_in, CUSTOM_IOCTL_BLOCKREAD, size);
}
return ONI_ESUCCESS;
}
Driver Options#
While it is recommended that all internal settings for the driver translator
and its underlying hardware are derived from standard ONI options, there might
be cases when special options need to be passed to the driver translator.
oni_driver_set_opt()
and oni_driver_get_opt()
are used for
this. This two functions are transparently called from the public liboni
API functions oni_set_driver_opt()
and oni_get_driver_opt()
.
In most cases these functions will simply return ONI_EINVALOPT
.
Driver Information#
A driver translator must be able to report its name and version information.
This is done using the Driver Information structure, which contains
information following the Semantic Versioning
specification. This structure should be a constant, and a pointer to it should
be returned by oni_get_driver_info()
.
const oni_driver_info_t oni_driver_info = {
.name = "simple",
.major = 1,
.minor = 0,
.patch = 0,
.pre_release = NULL
};
const oni_driver_info_t* oni_driver_info()
{
return &driverInfo;
}