Skip to content
Home » Embedded Systems » Embedded C » Using a Config File for Feature Selection in Embedded C

Using a Config File for Feature Selection in Embedded C

Config File for Feature Selection featured image with dark blue background, C FOUNDATIONS badge, #cfg icon in teal circle, and Compile-Time Config in Embedded C subtitle by nerdyelectronics.com
Embedded Systems Learning Path
Part 28 of 129View Full Path →

KEY TAKEAWAYS

  • A configuration header file uses #define flags to enable or disable firmware features at compile time
  • Conditional compilation (#ifdef FEATURE_X) includes only the code for enabled features
  • This approach produces smaller, optimized binaries tailored to specific hardware or product variants
  • A single codebase can serve multiple products by swapping configuration files

What is Feature Selection?

In embedded systems, you often need to build different versions of the same firmware. For example:
  • A product line with different hardware variants (some have Bluetooth, some do not)
  • Debug builds with extra logging vs release builds with smaller code size
  • Different communication protocols enabled based on the target board
  • Regional variants with different sensor configurations
Rather than maintaining separate codebases for each variant, you can use a configuration header file to enable or disable features at compile time. The C preprocessor handles the rest.

The Basic config.h Pattern

Create a single file called config.h that contains all your feature switches:
#ifndef CONFIG_H
#define CONFIG_H

// ============================================
// Hardware Variant Selection
// ============================================
#define BOARD_VARIANT_A    1
#define BOARD_VARIANT_B    2
#define BOARD_VARIANT_C    3

#define CURRENT_BOARD      BOARD_VARIANT_A

// ============================================
// Feature Switches (1 = enabled, 0 = disabled)
// ============================================
#define FEATURE_BLUETOOTH      1
#define FEATURE_WIFI           0
#define FEATURE_SD_CARD        1
#define FEATURE_DISPLAY        1
#define FEATURE_GPS            0

// ============================================
// Debug Options
// ============================================
#define DEBUG_ENABLED          1
#define DEBUG_UART_BAUD        115200
#define LOG_LEVEL_VERBOSE      0
#define LOG_LEVEL_ERROR        1

// ============================================
// System Parameters
// ============================================
#define SENSOR_READ_INTERVAL_MS   1000
#define WATCHDOG_TIMEOUT_MS       5000
#define MAX_SENSOR_COUNT          8

#endif // CONFIG_H

Using the Config File in Your Code

Include config.h in every source file that needs to check feature flags. Use #if directives to conditionally compile code:
#include "config.h"
#include "uart.h"

#if FEATURE_BLUETOOTH
    #include "bluetooth.h"
#endif

#if FEATURE_WIFI
    #include "wifi.h"
#endif

#if FEATURE_DISPLAY
    #include "display.h"
#endif

void system_init(void) {
    uart_init(DEBUG_UART_BAUD);

    #if FEATURE_BLUETOOTH
        bluetooth_init();
        uart_send_string("Bluetooth: ONrn");
    #endif

    #if FEATURE_WIFI
        wifi_init();
        uart_send_string("WiFi: ONrn");
    #endif

    #if FEATURE_DISPLAY
        display_init();
        display_show_text("System Ready");
    #endif

    #if FEATURE_SD_CARD
        sd_card_init();
        uart_send_string("SD Card: ONrn");
    #endif
}
When FEATURE_WIFI is set to 0, the WiFi code is completely removed by the preprocessor. It does not exist in the compiled binary, saving both flash memory and RAM.

Board-Specific Configuration

For product lines with different hardware, use the board variant to auto-select features:
#ifndef CONFIG_H
#define CONFIG_H

#define BOARD_BASIC     1
#define BOARD_PRO       2
#define BOARD_INDUSTRIAL 3

// Change this line to build for different boards
#define TARGET_BOARD    BOARD_PRO

// Auto-configure features based on board
#if (TARGET_BOARD == BOARD_BASIC)
    #define FEATURE_BLUETOOTH      0
    #define FEATURE_DISPLAY        0
    #define FEATURE_SD_CARD        0
    #define SENSOR_COUNT           2
    #define CPU_CLOCK_MHZ          8

#elif (TARGET_BOARD == BOARD_PRO)
    #define FEATURE_BLUETOOTH      1
    #define FEATURE_DISPLAY        1
    #define FEATURE_SD_CARD        1
    #define SENSOR_COUNT           4
    #define CPU_CLOCK_MHZ          48

#elif (TARGET_BOARD == BOARD_INDUSTRIAL)
    #define FEATURE_BLUETOOTH      0
    #define FEATURE_DISPLAY        1
    #define FEATURE_SD_CARD        1
    #define SENSOR_COUNT           8
    #define CPU_CLOCK_MHZ          72

#else
    #error "Unknown TARGET_BOARD! Please define a valid board."

#endif

#endif // CONFIG_H
The #error directive at the end ensures a clear compilation error if someone sets an invalid board type.

Debug and Logging Configuration

A very common use case is controlling debug output:
// In config.h
#define DEBUG_ENABLED   1
#define LOG_LEVEL       2   // 0=none, 1=error, 2=warning, 3=info, 4=verbose

// In debug.h
#ifndef DEBUG_H
#define DEBUG_H

#include "config.h"
#include <stdio.h>

#if DEBUG_ENABLED
    #define LOG_ERROR(fmt, ...)   do { if (LOG_LEVEL >= 1) printf("[ERR] " fmt "rn", ##__VA_ARGS__); } while(0)
    #define LOG_WARN(fmt, ...)    do { if (LOG_LEVEL >= 2) printf("[WRN] " fmt "rn", ##__VA_ARGS__); } while(0)
    #define LOG_INFO(fmt, ...)    do { if (LOG_LEVEL >= 3) printf("[INF] " fmt "rn", ##__VA_ARGS__); } while(0)
    #define LOG_VERBOSE(fmt, ...) do { if (LOG_LEVEL >= 4) printf("[VRB] " fmt "rn", ##__VA_ARGS__); } while(0)
#else
    #define LOG_ERROR(fmt, ...)
    #define LOG_WARN(fmt, ...)
    #define LOG_INFO(fmt, ...)
    #define LOG_VERBOSE(fmt, ...)
#endif

#endif // DEBUG_H
Usage:
#include "debug.h"

void sensor_read(void) {
    LOG_INFO("Reading sensor...");

    int value = read_adc();

    if (value < 0) {
        LOG_ERROR("Sensor read failed: %d", value);
        return;
    }

    LOG_VERBOSE("Raw ADC value: %d", value);
    LOG_INFO("Temperature: %.1f C", value * 0.1);
}
In a release build, set DEBUG_ENABLED to 0 and all log calls compile to nothing, with zero runtime overhead.

Compile-Time Assertions

You can add checks in your code to verify that the configuration is valid:
#include "config.h"

// Ensure at least one communication interface is enabled
#if !FEATURE_BLUETOOTH && !FEATURE_WIFI
    #warning "No wireless communication enabled!"
#endif

// Ensure sensor count is within range
#if SENSOR_COUNT > MAX_SENSOR_COUNT
    #error "SENSOR_COUNT exceeds MAX_SENSOR_COUNT"
#endif

// Ensure buffer sizes make sense
#if (SENSOR_COUNT * sizeof(float)) > 512
    #error "Sensor data exceeds buffer capacity"
#endif

Passing Config from the Build System

Instead of editing config.h every time, you can pass feature flags from your Makefile or build system using the -D compiler flag:
# Makefile
CFLAGS += -DTARGET_BOARD=BOARD_PRO
CFLAGS += -DDEBUG_ENABLED=1

# Build for different board:
# make BOARD=industrial
ifeq ($(BOARD), industrial)
    CFLAGS += -DTARGET_BOARD=BOARD_INDUSTRIAL
else ifeq ($(BOARD), basic)
    CFLAGS += -DTARGET_BOARD=BOARD_BASIC
else
    CFLAGS += -DTARGET_BOARD=BOARD_PRO
endif
Then in config.h, use defaults that can be overridden:
#ifndef TARGET_BOARD
    #define TARGET_BOARD BOARD_PRO  // Default if not set by build system
#endif
This allows you to build different variants without modifying any source files:
make BOARD=basic       # Build for basic board
make BOARD=industrial  # Build for industrial board
make                   # Build with default (pro)

Real-World Example: Complete config.h

#ifndef CONFIG_H
#define CONFIG_H

// ============================================
// Project: Temperature Monitor v2.0
// ============================================

// Board selection (override via -D compiler flag)
#ifndef TARGET_BOARD
    #define TARGET_BOARD    BOARD_PRO
#endif

// Feature flags
#define FEATURE_BLUETOOTH      1
#define FEATURE_SD_LOGGING     1
#define FEATURE_OLED_DISPLAY   1
#define FEATURE_BUZZER_ALARM   1
#define FEATURE_OTA_UPDATE     0

// Sensor configuration
#define SENSOR_TYPE_DHT22      1
#define SENSOR_TYPE_BME280     2
#define ACTIVE_SENSOR          SENSOR_TYPE_BME280
#define SENSOR_POLL_RATE_MS    2000

// Communication
#define UART_BAUD_RATE         115200
#define BT_DEVICE_NAME         "TempMonitor"

// Alarm thresholds
#define TEMP_ALARM_HIGH        40.0f
#define TEMP_ALARM_LOW         -10.0f
#define HUMIDITY_ALARM_HIGH    85.0f

// Debug (set to 0 for release builds)
#ifndef DEBUG_ENABLED
    #define DEBUG_ENABLED      1
#endif
#define LOG_LEVEL              3

// System
#define WATCHDOG_ENABLED       1
#define SLEEP_MODE_ENABLED     0
#define FIRMWARE_VERSION       "2.0.1"

#endif // CONFIG_H

Summary

Using a configuration header file is a simple but powerful technique for managing firmware variants:
  • Centralizes all feature flags and parameters in one file
  • Disabled features are completely removed from the binary (zero overhead)
  • #if / #elif / #else directives control which code gets compiled
  • #error and #warning catch invalid configurations at compile time
  • Build system can override defaults using -D flags
  • Keeps one codebase for multiple product variants
Every professional embedded project uses this pattern. Start with a simple config.h and grow it as your project evolves.

Leave a Reply

Your email address will not be published. Required fields are marked *