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.

Context management examples.#
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.

Example stream I/O implementation#
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.

Examples of register access.#
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.

Example post-option callback.#
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().

Example implementation#
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;
}