Table of Contents
KEY TAKEAWAYS
- A configuration header file uses
#defineflags 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
The Basic config.h Pattern
Create a single file calledconfig.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
Includeconfig.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_HThe #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_HUsage:#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"
#endifPassing Config from the Build System
Instead of editingconfig.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
endifThen in config.h, use defaults that can be overridden:#ifndef TARGET_BOARD
#define TARGET_BOARD BOARD_PRO // Default if not set by build system
#endifThis 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_HSummary
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/#elsedirectives control which code gets compiled#errorand#warningcatch invalid configurations at compile time- Build system can override defaults using
-Dflags - Keeps one codebase for multiple product variants
config.h and grow it as your project evolves.
Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.






