views:

133

answers:

3

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:

  • use a global stack of arguments, and all functions are unsigned char Func(void);
  • use a helper function to each implementation that lets me switch on the right assignment to make/call;
  • use JMP / LCALL assembly calls (which of course is horrible), potentially causing major problems on the call stack.

Neither is elegant, or composed... what's your approach/advise?

A: 

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.

jpalecek
Sorry. I edited it to make it more C.
jpalecek
C structs can have members of any type a variable can have, including function pointers. (If you don't believe it, just try it, or look into Linux source code.) What you say about C++ struct/class is almost true, but is totally irrelevant to your question AND my answer - not sure why you mention it.
jpalecek
Sorry, I misread your code, it seamed you had a function, not a function-pointer, in that struct. How is a called function going to know about the additional arguments that you've encapsulated on that second struct?Your point about common interfaces being a requirement for a non-changed implementation is correct. So, what I'm trying here is to find a way to step outside the "normal" C signature for a function (return, number of args and their types) and come up with something more flexible (but probably less obvious)
jpinto3912
How is a called function gonna know about the arguments: it will know about the arguments, because it is tightly coupled with a specific struct with the arguments (ie. one type of struct - one function)
jpalecek
+2  A: 

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.

kgiannakakis
That architecture is what we all use. The disadvantage you mentioned is also true at compile time... there has to be manual intervention to change the implementation, and that is what we want to avoid. It's very tedious to have the same basic piece of FW having to be compiled 100s of times per each part with different options. This is not a PC, this a FW to go with HW with many different possible implements (and that FW should be as constant as possible for gazillions of reasons)
jpinto3912
If you carefully configure the build system, you only need to compile each part once and link 100s of times, which is faster. I understand that you want to have a single executable for all different hardware configurations. That is indeed a difficult problem to solve. I believe that if all driver implementations follow a common interface, finding an elegant solution would be easier.
kgiannakakis
A: 

The correct way is to use a helper function. Sure, the unsigned char ucReadCh2_ADC(unsigned char *); may look like it stores the result as a value [0,255]. But who says the actual range is [0,255] ? And even if it did, what would those values represent?

On the other hand, if you'd typedef unsigned long milliKelvin, the typedef unsigned char (*EvalTemperature)(milliKelvin *out); is a lot clearer. And for every function, it becomes clear how it should be wrapped - quite often a trivial function.

Note that I removed the "uc" prefix from the typedef since the function didn't return an unsigned char anyway. It returns a boolean, OK-ness. (Colbert fans may want to use a float to indicate truthiness ;-) )

MSalters
Spent some time on it, actually. I considered writing _Bool, which is a C type, before picking boolean. Exactly because it isn't a C type, but has unambiguous, correct semantics. // As for the Colbert reference, he's a type on the "Daily Show with Jon Stewart" who famously introduced "truthiness" when covering the McCain/Obama elections to cover all the half-truths told in there.
MSalters
Oh, THAT Colbert! I thought you'd might be pointing at him, but because I don't see the show for over an year now, didn't get that specific terminology. I miss my cracked sat-TV...
jpinto3912