19、宏与配置管理:编译时配置开关,特性标志(feature flag),模块化配置宏,实战:产品线差异化

说到宏,很多人第一反应就是「定义个常量」或者「写个简单的函数替代」。但在我十几年的嵌入式开发经历里,宏最强大的用法,其实是做配置管理

你想想看,一个产品往往有多个变种——标准版、高配版、低配版、国内版、海外版。如果每个版本都维护一套代码,那简直是噩梦。宏就是解决这个问题的利器。

编译时配置开关

配置开关,说白了就是用一个宏来决定某段代码要不要编译进去。最常见的做法就是 #ifdef#ifndef

#define ENABLE_BLUETOOTH  1

#if ENABLE_BLUETOOTH
void bluetooth_init(void) {
    // 蓝牙初始化代码
}
#endif

这里有个坑,我踩过好几次。有些人喜欢用 #ifdef ENABLE_BLUETOOTH,但如果你只写了 #define ENABLE_BLUETOOTH 没给值,它也是被定义的。我个人习惯用 #if ENABLE_BLUETOOTH,配合 0/1 来控制,这样更明确。

注意:不要用 #define ENABLE_BLUETOOTH 然后指望它等于 0 来关闭功能。没给值的宏,在 #if 里会被当作 0,但 #ifdef 里它仍然是「已定义」状态。这两种行为不一致,容易出 bug。

特性标志(feature flag)

特性标志是配置开关的升级版。它不只是开或关,还可以携带更多信息。比如某个功能在硬件版本 2.0 以上才支持,你可以这样写:

#define HW_VERSION  200

#if HW_VERSION >= 200
    #define FEATURE_GPS_ENABLED     1
    #define FEATURE_4G_ENABLED      1
#else
    #define FEATURE_GPS_ENABLED     0
    #define FEATURE_4G_ENABLED      0
#endif

我在做一款车载定位终端时,就用过这种方案。不同批次的硬件,GPS 模块和 4G 模块的型号不一样,驱动代码也不同。通过特性标志,一套代码编译出三个固件,分别对应三个硬件版本。测试的时候,只需要改一行 #define HW_VERSION 就行。

模块化配置宏

当项目大了,配置开关会散落在各个源文件里。这时候就需要模块化配置宏——把相关的配置集中到一个头文件里管理。

我一般会建一个 config.h,里面放所有模块的开关:

// config.h
#ifndef CONFIG_H
#define CONFIG_H

// 通信模块配置
#define MODULE_UART_ENABLED     1
#define MODULE_SPI_ENABLED      1
#define MODULE_I2C_ENABLED      0

// 传感器模块配置
#define MODULE_TEMP_SENSOR      1
#define MODULE_HUMID_SENSOR     1
#define MODULE_PRESSURE_SENSOR  0

// 存储模块配置
#define MODULE_FLASH_ENABLED    1
#define MODULE_EEPROM_ENABLED   0

#endif

然后在各个模块的源文件里,统一引用 config.h

// uart.c
#include "config.h"

#if MODULE_UART_ENABLED
void uart_init(void) {
    // 初始化 UART
}
#endif

这样做的好处是,你只需要打开一个文件,就能看到整个项目的功能开关。新同事接手项目,第一件事就是看 config.h,马上就知道这个固件支持哪些功能。

小技巧:我习惯在 config.h 里加一段编译时检查,防止配置冲突。比如蓝牙和 Wi-Fi 共用同一个天线,就不能同时开启:

#if MODULE_BLUETOOTH_ENABLED && MODULE_WIFI_ENABLED
    #error "Bluetooth and Wi-Fi cannot be enabled at the same time!"
#endif

实战:产品线差异化

讲个真实案例吧。我之前做的一款智能家居网关,有三个产品型号:

型号 功能 目标市场
GW-100 Wi-Fi + 蓝牙 国内家庭
GW-200 Wi-Fi + Zigbee 海外家庭
GW-300 Wi-Fi + 蓝牙 + Zigbee + 4G 工业场景

如果每个型号单独维护一套代码,那光是同步 bug 修复就能让人崩溃。我的做法是:

  1. config.h 里定义产品型号宏:#define PRODUCT_GW_100#define PRODUCT_GW_200#define PRODUCT_GW_300
  2. 在编译脚本里,通过 -D 参数传入当前编译的是哪个型号
  3. config.h 里根据型号自动开启或关闭对应的模块
// config.h
#if defined(PRODUCT_GW_100)
    #define MODULE_BLUETOOTH_ENABLED    1
    #define MODULE_ZIGBEE_ENABLED       0
    #define MODULE_4G_ENABLED           0

#elif defined(PRODUCT_GW_200)
    #define MODULE_BLUETOOTH_ENABLED    0
    #define MODULE_ZIGBEE_ENABLED       1
    #define MODULE_4G_ENABLED           0

#elif defined(PRODUCT_GW_300)
    #define MODULE_BLUETOOTH_ENABLED    1
    #define MODULE_ZIGBEE_ENABLED       1
    #define MODULE_4G_ENABLED           1

#else
    #error "Unknown product model!"
#endif

这样一来,三个型号共用 95% 的代码,只有 5% 的差异化代码通过宏来控制。维护成本直线下降。

核心思路:用宏把「变」和「不变」分离。不变的逻辑写在公共代码里,变的配置写在宏开关里。编译时,宏开关决定了最终生成哪个版本的固件。

知识体系图

下面这张图总结了宏在配置管理中的核心逻辑:

宏与配置管理知识体系 编译时配置开关 #if / #ifdef / #ifndef 特性标志 FEATURE_XXX_ENABLED 模块化配置宏 config.h 集中管理 实战:产品线差异化 GW-100 / GW-200 / GW-300 三型号共用代码 核心原则 「变」与「不变」分离 | 编译时决策 | 集中配置 | 编译时冲突检查

嗯,这套方法我用了很多年,从单片机到 Linux 嵌入式,从几百行的小项目到几十万行的大项目,都适用。说白了,宏就是 C 语言里最灵活的「配置引擎」,用好了,能让你的代码适应各种产品变体,而且几乎不增加运行时开销。

下次当你面对一堆相似但又不同的产品时,不妨试试用宏来做配置管理。相信我,你会爱上这种「改一行宏,编译出不同固件」的爽快感。


公众号:蓝海资料掘金营,微信deep3321