We have a battery manager implementation which uses a fuel gauge driver and a charger driver. Currently both of these drivers are hard-references to the fuel gauge and charger driver we currently support.

Now for new projects, another fuel gauge and another charger have been selected. We would like to keep the logic of the battery manager, but that the drivers can be chosen dynamically. The drivers all provide similar functionality for reading the important values and such.

The drivers are all part of our own library, we don't expect that we will have to support third party drivers. Also, we don't expect multiple fuel gauges or chargers to be present simultaneously.

Currently I see three options:

1. Give a filled-in API struct with function pointers to the Battery Manager

  • Description: Initialize the Battery Manager with a struct of function pointers to e.g. the Fuel Gauge and Charger driver's their initialization and sensor reading functions
  • Pros: Flexible
  • Cons: The Battery Manager initialization caller has to know about the drivers

2. Let the Battery Manager find available fuel gauge and charger drivers from a shared list in which the fuel gauge and charger drivers register themselves

  • Description: Setup a list that the is accessible by the drivers and the modules that use those drivers. The drivers register themselves in this list, the modules look for drivers that match their type
  • Pros: The module initialization callers do not need to know about the drivers
  • Cons: More programming overhead

3. Let the fuel gauge and charger drivers all implement the same interface (shared header) and let the Battery Manager just reference that

  • Description: Have one .h file per driver type define the interface, and have drivers implement that interface so that the Battery Manager can just call e.g. FuelGauge_ReadVoltage() everywhere
  • Pros: Easy to implement
  • Cons: Feels hackish

I would very much appreciate your insights! Did I miss options? Did I miss pro's and/or cons? What would be the best choice?

  • 1
    What are your priorities with respect to the pros and cons you have identified? As someone with no mentionable experience in C i'd say #3 is totally fine until you need something more flexible. #1 sounds like throwing typechecks out the window; but again, i have no clue on C.
    – marstato
    Apr 13, 2021 at 14:07

3 Answers 3


Both options 1 and 3 I have seen being used in C code bases.

Option 2 has the disadvantage that there might be a timing issue. For example, the Battery Manager could try to retrieve a Charger from the central repository before the corresponding driver has registered itself. Or the driver might be absent completely. Handling those scenarios will increase the complexity of your code.

Option 3 basically makes your build system responsible for choosing the correct driver implementations, where option 1 makes it possible to make that choice at runtime.

Option 1 is also similar to modern DI practices and may be more easily understood by others for that reason.

For that reason, option 1 is preferred. The con that the initialization caller of Battery Monitor also needs to have knowledge of the drivers is a very minor con to me. Most likely, those drivers also need to be initialized and it is fairly common that all such initializations are done from a single component, so then it already knows about the drivers anyway.

  • Thank you for your detailed response, and also for taking the time and care to rewrite it. At first I was leaning towards Option 3, but I just thought of another con. If we were to package our code as a library, this would mean that we have to recompile the library for every combination of drivers. On the other hand though, we would likely only provide 1 type of each driver per customer project, and compile the entire library specifically for their embedded target.
    – Kodiak
    Apr 13, 2021 at 14:19

Drivers like this are often implemented as shared libraries (.so or .DLL) with agreed-upon interfaces. They serve to "abstract away" how the device actually works. Then, all of these libraries are managed by a single layer of software – your so-called "Battery Manager" – which abstracts-away the fact that multiple drivers may exist and how they are selected. When any other piece of code wants to do anything having to do with a Battery, it calls upon the Battery Manager to do it and doesn't care what happens next.

By the way, operating systems such as Linux very routinely use this exact same architecture idea. In that system, "loadable kernel modules" can be selected at pre-boot time to drive any particular device. ("DEC token-ring interface card," anyone?) All of them expose the same agreed-upon API for use by the kernel, and none are called directly.

  • Thank you for your feedback! So if I understand correctly, this would mean option 1, the API way?
    – Kodiak
    Apr 13, 2021 at 14:31
  • 1
    @Kodiak, No, it is more a dynamic load variant of option 3. Each shared library implements the same header. Apr 14, 2021 at 5:44

Judging from your question, this is about software for some sort of microcontroller.

Personally, I'd lean towards option 1 - you would create a separate part of code which deals with initializing the hardware, with all the knowledge required. It would then, among others, initialize the battery manager with the struct with appropriate function. The structs with functions should be part of the driver, the initialization just picking the right one.

Should the situation warrant it, you could write a generator, which would create a table where hardware initialization could look up the struct by device name. While I'm no expert, as far as I know this is more or less how Linux' handling of device trees works. It could also be worth it to take a look at pre device tree board initialization files in Linux (pre 3.4).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.