Enable different drivers with API - Page 1
Forum section: electronics / general

author: lorenrus
author_rank: Regular Contributor
timestamp: February 19, 2021, 08:52:31 am
content: 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

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 19, 2021, 10:26:19 am
content: 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 measurementready()- to return whether a new measurement is readyread()- to return the measurementDriverfoodefinesfoo_trigger(),foo_ready(), andfoo_read().Declare a common function pointer structure visible to all drivers implementing a similar module. Here,Code: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: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:const struct temperature_sensor *temperature_sensor;void temperature_sensor_init(void) {#ifdef FOO_INCLUDEDif ((temperature_sensor = foo_probe()))return;#endif#ifdef BAR_INCLUDEDif ((temperature_sensor = bar_probe()))return;#endif#ifdef BAZ_INCLUDEDif ((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:static inline int temperature_sensor_trigger(void) {if (temperature_sensor)return temperature_sensor->trigger();elsereturn NOT_AVAILABLE;}static inline int temperature_sensor_ready(void) {if (temperature_sensor)return temperature_sensor->ready();elsereturn NEVER_READY;}static inline int temperature_sensor_read(void) {if (temperature_sensor)return temperature_sensor->read();elsereturn 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 thattemperature_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 thesectionattribute 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.

author: lorenrus
author_rank: Regular Contributor
timestamp: February 19, 2021, 03:27:44 pm
content: 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

author: lorenrus
author_rank: Regular Contributor
timestamp: February 19, 2021, 03:40:49 pm
content: 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

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 19, 2021, 05:12:38 pm
content: The function names must be unique.

author: lorenrus
author_rank: Regular Contributor
timestamp: February 19, 2021, 08:04:41 pm
content: so don't my case can't do it ?thank you

author: Nominal Animal
author_rank: Super Contributor
timestamp: February 20, 2021, 04:07:18 am
content: 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 othersstatic.You rename these functionsltc6811_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, incommoncode, 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 sayCode: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.Thestruct ltc681xstructure 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 acommoninterface for all other code to acceseither 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).