Different Ways of Software Configuration

Most of the time software needs some way to configure things: depending on the settings, the software will do different things. For example the software running on the microcontroller on top of the Raspberry might have the OLED LCD available or not:

Raspberry Pi and tinK22 with OLED LCD

Raspberry Pi and tinyK22 (NXP Kinetis K22FN512) with OLED LCD

How can I deal with this in my application code?

Variable

For example I could check in my application a variable if there is a certain feature available at runtime:

extern bool configHas_LCD = true;
...
if (configHas_LCD) {
  showStatusOnLCD();
}

That approach would decide this at runtime. It needs more memory and takes longer to execute.

File

Having a file system available, it can make sense to store the configuration in a file. There is the very useful  .ini utility (see Minini). With this configuration settings can be stored in [sections] in a text file:

[Configuration]
LCD = Yes

Then I can use it like this:

configHas_LCD = MINI1_ini_bool("Configuration", "LCD", false, "config.ini");
if (configHas_LCD) {
  showStatusOnLCD();
}
}

With that approach I can configure the application from the ‘outside’: very flexible, but of course comes with some overhead.

Constant

A better approach would be to use a constant if that configuration does not change:

const bool configHas_LCD = true; 
...
if (configHas_LCD) {
  showStatusOnLCD();
}

Better, and hopefully the compiler will do ‘constant-folding’ and no extra compare code will be present in my application.

#define

What I prefer is to use some configuration macro defines:

#define CONFIG_HAS_LCD   (1)
  /*!< 1: we have the LCD available. 0: no LCD available */

...
#if CONFIG_HAS_LCD
 showStatusOnLCD();
#endif

It makes sense to have these configuration macros in a dedicated header file:

/* config.h */
#define CONFIG_HAS_LCD (1) 
  /*!< 1: we have the LCD available. 0: no LCD available */ 

and then include that header file in the application.

Compiler -D Option

Another way is to use the compiler -D (Define) option.

-DCONFIG_HAS_LCD=1

has the same effect as having

#define CONFIG_HAS_LCD   1

in the sources.

In Eclipse (screenshot from MCUXpresso IDE) the defines can be added in the project settings:

Compiler -D Option

Compiler -D Option

I don’t prefer that way because that way the settings are ‘buried’ in the project settings. With using a version control system, it might be hard to see changes that way.

-include Compiler Option

What I prefer is to use the -include compiler option: using that option I can include a header file for each source file I compile.

That option is described in https://gcc.gnu.org/onlinedocs/gcc-4.3.2/gcc/Preprocessor-Options.html#Preprocessor-Options:

-include file:
process file as if #include "file" appeared as the first line of the primary source file. However, the first directory searched for file is the preprocessor’s working directory instead of the directory containing the main source file. If not found there, it is searched for in the remainder of the #include "..." search chain as normal.

I can use that option in make files or set it in the IDE:

-Include Option

-Include Option

Please note that I’m using a fully qualified path to the header file:

"${ProjDirPath}/source/config.h"

Just using ‘config.h’ would be fine for the compiler. But there is a long outstanding bug in Eclipse CDT how the Eclipse Indexer is dealing with the -include option. Without an expanded path, Eclipse CDT shows annoying warnings about ‘unresolved inclusion’:

Unresolved Inclusion

Unresolved Inclusion

💡 There is yet another glitch in Eclipse CDT: the option description suggests that there should be a space between -include and the file name, while Eclipse CDT does not issue a space. The gcc compilers accepts the option without a space too, at least in the current version.

Multi-Level Configuration

Of course there are many ways to implement configurations. Here is my preferred way:

Each driver or module has an external configuration file. For example LCD.c and LCD.h, and the configuration of the LCD is in configLCD.h

/* configLCD.h */
#ifndef CONFIGLCD_H_
#define CONFIGLCD_H_

#ifndef CONFIG_HAS_LCD
  #define CONFIG_HAS_LCD (0)
#endif

#endif

I can configure the driver with the settings in that configuration header file if I need to.This configuration header file gets included in the LCD driver:

/* LCD.c */
#include "configLCD.h
#include "LCD.h"
...
#if CONFIG_HAS_LCD
  showStatusOnLCD();
#endif
...

Note that the configuration #defines in the configuration header file are setting a default value in case the macro is *not* already defined (#ifndef). So if don’t want to touch the file I can use the -D compiler option to change the setting.

Or better: use the -include to include a header file like below:

/* config.h */
#define CONFIG_HAS_LCD (1) /* enable LCD support */

With this I can overwrite or configure a driver without touching the driver configuration header file itself. This is especially useful if I have many drivers or have them shared by multiple projects: the drivers can be shared together with their default configuration files, while I set and overwrite things with the -include file.

Summary

There are many ways to configure software at compile or runtime. I prefer using #define macros in combination with the -include option for most of my applications.

I hope this is useful for you and you might use one or the other way to make your software and drivers more versatile.

Happy configuring 🙂

Links

15 thoughts on “Different Ways of Software Configuration

  1. Good summary Erich, thanks.
    We use the following model for the one codeline and many models with different settings defaults.
    In build config we set “MODEL_TYPE=modeltypeid”
    Then in a common #include file use
    #if MODEL_TYPE=modeltypeid
    # include “models/modeltypeinclude.h”
    Each of these files can contain many #defines for default values etc, including model name strings, IDs, features to include or not, etc., specific to each model.
    Your -Include tip circumvents the first part of this method, so that’s possibly an improvement we can make. Thanks for that 🙂
    One advantage of having a settings file for each configuration compared with having them in project settings is that the files can be easily diffed and cloned to product new model configurations.

    Like

    • I have used build configurations with -D defines as well (and still do to some extend). But as you say: it is not very transparent where the defines are located, and there is no way to have comments added for the settings in the project settings. So I prefer to have the settings in an external file: that way the settings are not burried or hidden in the project settings, and it is easier to migrate that project to another compiler/toolchain too.

      Like

  2. IMHO, using #if for switching configurations leads to dormant code. I prefer a #define XYZ, plus if(XYZ).

    When the switched-off code portion is not used for a long time, it never gets compiled while the rest of the source code is being developed. Then, the switched-off code is turned on again and it often is out-of-sync with the rest of the code (e.g., compiler generates errors). It’s not easy to catch up with all changes.

    When a if() is used, compilers parses even the unused code and generates errors while the rest of the code is being developed. It’s usually easier to adapted the switched-off code step-by-step.

    Like

    • Yes, this is indeed a problem which I have faced too. I tried to address this with a build system which does build all the (active/valid) configurations.
      But you are right, there might be some dormant code.
      I have used that if() approach too. The compiler might produces some warnings about ‘condition always is true’ or similar.

      Like

  3. I’ve mostly scaled back my usage of -D to distinguish debug and production builds. For compile-time inclusion of drivers and modules, I’ll often use my AUTO_START macro that builds a list of init functions to be called from the main module, and then drivers can make the calls they need to register themselves or hook events. CodeWarrior has the convenient checkbox next to each file to exclude it from the build (I wish they’d do that in MCUX) and this makes it really easy to add or remove optional modules – assuming it’s a system large enough to justify the overhead of some function pointers.

    For runtime user configuration, I’d be interested to get your feedback on the framework I’m working on. My more complex projects have lots of options that might be stored as strings, integers, or bitfields loaded in RAM and stored in internal or external flash, or they could be in emulated EEPROM, and they need .ini file parsing and/or shell set and show commands. My framework aims to make that all table-driven with some standard integer range and string length validation built in, plus optional help text.

    So far it’s working well. The bitfield options are kind of inefficient in terms of code space, because the macro has to create a new accessor and mutator function for each one. Still, where space allows I think it’s worth it. I can enter “show wifi*” and the shell will iterate through the whole list of parameters and show all that start with “wifi”, and I only have to write input validation code for special cases.

    I’m sure something like this must exist already, but I couldn’t find it.

    Like

    • I don’t make a distinction between a ‘debug’ and a ‘release’ build: I want to use and debug what will end up in the device.
      I’m doing a similar way as your ‘AUTO_START’: I call a PL_Init() (PL for Platform) which calls drivers and which register callback, setup tasks, etc. What gets called in PL_Init() is controlled by #defines which are set or not set.
      That way I easily can add/remove modules too.
      Modules can publish extra macros in header files telling about their capabilities (e.g. that they provide a command line interface). The command line handler ‘sees’ them and calls the handlers.
      I was thinking to add wildcards to the shell interface too, but did not had the time or pressing need.
      Yes, I think as well that this kind of framework must exist (maybe?), but did not find a suitable one.

      Like

      • “I don’t make a distinction between a ‘debug’ and a ‘release’ build: I want to use and debug what will end up in the device.”
        I like this, and am heading this way myself, if there is the available flash to accommodate the debug code.
        If not (which was the case for my previous platform unfortunately) then the distinction definitely does have merit.

        Like

        • Agreed, it always depends. But I had one case where an issue escapted because it was only present in the release build and not in the debug one. Yes, the release build needs full testing coverage, but it could be that some subtle situations only occur in one build and you might miss it.

          Like

  4. Pingback: Running FreeRTOS on the VEGA RISC-V Board | MCU on Eclipse

  5. Pingback: FatFS, MinIni, Shell and FreeRTOS for the NXP K22FN512 | MCU on Eclipse

  6. Pingback: Tutorial: Adding FreeRTOS to where there is no FreeRTOS | MCU on Eclipse

  7. Pingback: How to get Data off an Embedded System: FatFS with USB MSD Host and FreeRTOS Direct Task Notification | MCU on Eclipse

  8. Pingback: Visual Studio Code for C/C++ with ARM Cortex-M: Part 6 | MCU on Eclipse

  9. Pingback: Key-Value pairs in FLASH Memory: file-system-less minINI | MCU on Eclipse

  10. Pingback: LoRaWAN with NXP LPC55S16 and ARM Cortex-M33 | MCU on Eclipse

What do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.