In embedded C is quite natural to have some fixed/generic algorithm but more than one possible implementation. This is due to several product presentations, sometimes options, other times its just part of product roadmap strategies, such as optional RAM, different IP-set MCU, uprated frequency, etc.
In most of my projects I deal with that by decoupling the core stuff, the algorithm and the logic architecture, from the actual functions that implement outside state evaluation, time keeping, memory storage, etc.
Naturally, I use the C function pointers mechanism, and I use a set of meaningful names for those pointers. E.G.
unsigned char (*ucEvalTemperature)(int *);
That one stores temperature in an int, and the return is OKness.
Now imagine that for a specific configuration of the product I have a
unsigned char ucReadI2C_TMP75(int *);
a function that reads temperature on the I2C bus from the TMP75 device and
unsigned char ucReadCh2_ADC(unsigned char *);
a function that reads a diode voltage drop, read by an ADC, which is a way to evaluate temperature in very broad strokes.
It's the same basic functionality but on different option-set products.
In some configs I'll have ucEvalTemperature setup to ucReadI2C_TMP75, while on other I'll have ucReadCh2_ADC. In this mild case, to avoid problems, I should change the argument type to int, because the pointer is always the same size, but the function signature isn't and the compiler will complaint. Ok... that's not a killer.
The issue becomes apparent on functions that may need to have different set of arguments. The signatures won't ever be right, and the compiler will not be able to resolve my Fpointers.
So, I have three ways:
Neither is elegant, or composed... what's your approach/advise?
I usually prefer to have a layered architecture. The communication with the hardware is achieved with "drivers". The algorithms layers call functions (readTemp), which are implemented by the driver. The key point is that an interface needs to defined and that must be honoured by all driver implementations.
The higher layer should know nothing about how the temperature is read (It doesn't matter if you use TMP75 or an ADC). The disadvantage of the drivers architecture is that you generally can't switch a driver at runtime. For most embedded projects this is not a problem. If you want to do it, define function pointers to the functions exposed by the driver (which follow the common interface) and not to the implementation functions.
If you can, use struct "interfaces":
struct Device {
int (*read_temp)(int*, Device*);
} *dev;
call it:
dev->read_temp(&ret, dev);
If you need additional arguments, pack them inside Device
struct COMDevice {
struct Device d;
int port_nr;
};
and when you use this, just downcast.
Then, you'll create functions for your devices:
int foo_read_temp(int* ret, struct Device*)
{
*ret = 100;
return 0;
}
int com_device_read_temp(int* ret, struct Device* dev)
{
struct COMDevice* cdev = (struct COMDevice*)dev; /* could be made as a macro */
... communicate with device on com port cdev->port_nr ...
*ret = ... what you got ...
return error;
}
and create the devices like this:
Device* foo_device= { .read_temp = foo_read_temp };
COMDevice* com_device_1= { .d = { .read_temp = com_read_temp },
.port_nr = 0x3e8
};
COMDevice* com_device_1= { .d = { .read_temp = com_read_temp },
.port_nr = 0x3f8
};
You'll pass the Device structure around to function that need to read the temperature.
This (or something similar) is used in the Linux kernel, exceptthat they don't put the function pointers inside the structs, but create a special static struct for it and stor pointer to this struct inside the Device struct. This is almost exactly how object oriented lingages like C++ implement polymorphism.
If you put the functions in a separate compilation unit, including the Device struct that refer to them, you can still save space and leave them out while linking.
If you need different types of arguments, or fewer arguments, just forget it. This means you cannot design a common interface (in any sense) for things you want to change, but without a common interface, no changeable implementation is possible. You can use compile-time polymorphism (eg. typedefs & separate compilation units for different implementations, one of which would be linked in your binary), but it still has to be at least source-compatible, that is, be called in the same way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With