EEVblog Electronics Community Forum

Products => Computers => Programming => Topic started by: lorenrus on February 19, 2021, 08:52:31 am

Title: Enable different drivers with API
Post by: lorenrus on February 19, 2021, 08:52:31 am
Hi everyone,

in my project, in C, I have different drivers for some devices I am using. I have made a recognition of the device that is connected to my logic board. Instead of merging the drivers into a single file and using #ifdef I was wondering if it was possible to put all the drivers in different files and through an API select a driver file I need.

Best regards
Title: Re: Enable different drivers with API
Post by: Nominal Animal on February 19, 2021, 10:26:19 am
The most common way is to have the drivers implement the same interface, but use different function names to implement them, and then use function pointers to select which one is being used.

For example, let's say you have a number of different temperature sensor modules, each with their own driver.  They all implement the same interface:
    trigger() - to trigger a new measurement
    ready() - to return whether a new measurement is ready
    read() - to return the measurement
Driver foo defines foo_trigger(), foo_ready(), and foo_read().

Declare a common function pointer structure visible to all drivers implementing a similar module.  Here,
Code: [Select]
struct temperature_sensor {
    int  (*trigger)(void);
    int  (*ready)(void);
    int  (*read)(void);
};

Have the probe function for each driver return the structure, if it detects it is installed, and NULL otherwise:
Code: [Select]
const struct temperature_sensor   foo_sensor = {
    .trigger = foo_trigger,
    .ready = foo_ready,
    .read = foo_read,
};

const struct temperature_sensor  *foo_probe(void)
{
    if (this module is detected) {
        return foo_sensor;
    } else {
        return NULL;
    }
}

At probe time, the common code calls each probe function for each module interface.  In this case,
Code: [Select]
const struct temperature_sensor  *temperature_sensor;

void temperature_sensor_init(void) {

#ifdef  FOO_INCLUDED
    if ((temperature_sensor = foo_probe()))
        return;
#endif

#ifdef  BAR_INCLUDED
    if ((temperature_sensor = bar_probe()))
        return;
#endif

#ifdef  BAZ_INCLUDED
    if ((temperature_sensor = baz_probe()))
        return;
#edif

    /* No temperature sensor module found. */
    temperature_sensor = NULL;
}
and at run time, the function pointers in the structure are used.  You can even create wrapper functions to hide the complexity:
Code: [Select]
static inline int  temperature_sensor_trigger(void) {
    if (temperature_sensor)
        return temperature_sensor->trigger();
    else
        return NOT_AVAILABLE;
}

static inline int  temperature_sensor_ready(void) {
    if (temperature_sensor)
        return temperature_sensor->ready();
    else
        return NEVER_READY;
}

static inline int  temperature_sensor_read(void) {
    if (temperature_sensor)
        return temperature_sensor->read();
    else
        return NOT_AVAILABLE;
}

Because I personally use GNU toolchain (avr-gcc or arm-gcc), I can use macros to declare members into an array (of known modules) in different files, so that temperature_sensor_init() becomes a loop over an array, and it does not need to know anything about the actual drivers; the driver implementations "slot in" automagically.   This is based on the section (https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#section) attribute supported by GCC, and the linker providing symbols the code can use to treat the section as an array.  (Essentially, with GCC toolchain, you can create arrays whose elements are declared in different files, in different compilation units.)

Obviously, the exact definition of the structure containing the function pointers is CRITICAL.  Personally, I like to include a string (a pointer to a string in ROM/Flash) declaring the name of the module.

If the structure containing the function pointers needs runtime data, say a variable identifying the bus the module is attached to, then the structure must be in RAM instead.  It is not a big issue, since it's just a few bytes.  Then, the _init function does not return a pointer, but populates the structure it is given a pointer to.
Title: Re: Enable different drivers with API
Post by: lorenrus on February 19, 2021, 03:27:44 pm
Thanks for the answer, now I read it well. In any case I have different drivers as the devices are different.

As soon as I have read it thoroughly I give you some feedback. Thank you
Title: Re: Enable different drivers with API
Post by: lorenrus on February 19, 2021, 03:40:49 pm
Ok.

I now have only one driver file where inside I enable the parts of interest with #ifdef.

I would like to have two separate driver files, one for LTC6811 and one for LTC6813, and enable / enable their functions with an API.

My case, if I have not misunderstood, differs from the example you reported in the fact that you have that the function names are the same, right?

Thank you
Title: Re: Enable different drivers with API
Post by: Nominal Animal on February 19, 2021, 05:12:38 pm
The function names must be unique.
Title: Re: Enable different drivers with API
Post by: lorenrus on February 19, 2021, 08:04:41 pm
so don't my case can't do it ?

thank you
Title: Re: Enable different drivers with API
Post by: Nominal Animal on February 20, 2021, 04:07:18 am
I mean, you do need to rename the functions, so that they won't clash.

Let's say you implement the drivers in files ltc6811.c and ltc6813.c, and right now both of them implement functions probe(), setmode(), trigger(), completed(), and voltages().  All other functions in the files are local, not used outside the files, so you declare all others static.

You rename these functions ltc6811_probe(), ltc6813_probe(), ltc6811_setmode(), ltc6813_setmode(), and so on.  This is necessary, because C does not have namespaces, and the code outside these files must be able to differentiate between the two.

Then, in common code, if you only have these two alternate drivers, you can do the switch-case approach.  You create a third file, say ltc681x-common.c, containing say
Code: [Select]
enum {
    NONE = 0,
    LTC6811,
    LTC6813,
};

struct ltc681x {
    SPI *spi;
    int  type;
};

int ltc681x_probe(SPI *spi, struct ltc681x *dev)
{
    if (ltc6811_probe(spi) == FOUND) {
        dev->spi = spi;
        dev->type = LTC6811;
        return LTC6811;
    }
    if (ltc6813_probe(spi) == FOUND) {
        dev->spi = spi;
        dev->type = LTC6813;
        return LTC6813;
    }
    return NONE;
}

int ltc681x_setmode(struct ltc681x *dev, int mode)
{
    switch (dev->type) {
    case LTC6811: return ltc6811_setmode(dev->spi, mode);
    case LTC6813: return ltc6813_setmode(dev->spi, mode);
    default: return FAILURE;
    }
}

int ltc681x_trigger(struct ltc681x *dev)
{
    switch (dev->type) {
    case LTC6811: return ltc6811_trigger(dev->spi);
    case LTC6813: return ltc6813_trigger(dev->spi);
    default: return FAILURE;
    }
}

int ltc681x_completed(struct ltc681x *dev)
{
    switch (dev->type) {
    case LTC6811: return ltc6811_completed(dev->spi);
    case LTC6813: return ltc6813_completed(dev->spi);
    default: return FAILURE;
    }
}

int ltc681x_completed(struct ltc681x *dev, float voltages[], int count)
{
    switch (dev->type) {
    case LTC6811: return ltc6811_completed(dev->spi, voltages, count);
    case LTC6813: return ltc6813_completed(dev->spi, voltages, count);
    default: return FAILURE;
    }
}
Your main code always calls ltc681x_ functions, not the ltc6811_ or ltc6813_ directly.

The struct ltc681x structure is used by main code to keep the details of the device; I am assuming you have more than one SPI bus, so the structure contains a reference to the particular SPI bus used to talk to a device.  Other function parameters can be passed as-is, as long as the ltc6811_ and ltc6813_ functions agree on what the parameters mean.

Essentially, the ltc681x.c provides a common interface for all other code to acces either type of device – the exact purpose of having a "driver" in the first place.  your ltc6811.c and ltc6813.c implement one driver each, and the ltc681x.c the logic to access both (either one, without worrying which one one is accessing).