第13章:配置模块的设计:配置文件解析(INI/JSON)、配置项的集中管理、实战:设计一个可扩展的配置模块
配置模块,说白了就是给程序一个「变通」的能力。你写死的代码是骨架,配置就是血肉。我见过太多项目,一开始图省事把参数硬编码,结果后期每次改参数都要重新编译、烧录,那叫一个痛苦。
这一章,我们就来聊聊怎么设计一个靠谱的配置模块。我会从最基础的配置文件解析讲起,再到如何集中管理配置项,最后带大家手撸一个可扩展的配置模块。
13.1 为什么需要配置模块?
你想想看,一个嵌入式产品,可能面对不同的客户、不同的应用场景。比如一个温控器,国内用摄氏度,国外用华氏度。如果没有配置模块,你就得维护两套代码。有了配置模块,改个配置文件就行。
我个人习惯,只要项目里出现超过3个需要「根据环境变化」的参数,我就会上配置模块。别等到项目做大了再重构,那成本太高了。
13.2 配置文件格式的选择:INI vs JSON
嵌入式领域,最常见的两种配置文件格式就是INI和JSON。它们各有千秋,我分别说说。
13.2.1 INI格式
INI格式简单、直观,非常适合资源受限的MCU。它的结构就是「节(Section)」+「键值对(Key=Value)」。
// config.ini 示例
[system]
baudrate=115200
log_level=3
[sensor]
temp_offset=2.5
humidity_calibration=1.02
解析INI文件,说白了就是逐行读取,遇到 [section] 就切换当前节,遇到 key=value 就存起来。我早期做的一个项目,用了一个不到200行的INI解析器,跑在Cortex-M0上,稳得很。
13.2.2 JSON格式
JSON表达能力更强,支持嵌套结构、数组、多种数据类型。但代价是解析器更复杂,内存开销也更大。
// config.json 示例
{
"system": {
"baudrate": 115200,
"log_level": 3
},
"sensor": {
"temp_offset": 2.5,
"humidity_calibration": 1.02
}
}
如果你的MCU有几百KB的RAM,用JSON完全没问题。但如果是几KB的RAM,我建议还是老老实实用INI。
- RAM < 32KB:用INI,解析简单,内存占用小
- RAM > 128KB:可以用JSON,灵活性更高
- 中间地带:看团队习惯,我个人偏向INI
13.3 配置项的集中管理
配置文件解析出来之后,不能散落在各个模块里。你需要一个「配置中心」,统一管理所有配置项。
我常用的做法是定义一个配置结构体,把所有配置项都放在里面。然后提供一个全局的配置句柄,各个模块通过接口来读写。
// config_manager.h
typedef struct {
// system section
uint32_t baudrate;
uint8_t log_level;
// sensor section
float temp_offset;
float humidity_calibration;
// 预留扩展字段
void* ext_data;
} config_t;
// 初始化配置模块
int config_init(const char* filepath);
// 获取配置项(只读)
const config_t* config_get(void);
// 更新配置项(带校验)
int config_set_baudrate(uint32_t baudrate);
int config_set_log_level(uint8_t level);
为什么要用接口而不是直接暴露结构体?嗯,这里要注意:直接暴露结构体,别人就能随意修改,容易出问题。通过接口,你可以在设置值时做校验,比如波特率不能是0,日志级别不能超过5。
void* ext_data 指针,用于扩展。这样以后加新配置项,不用改结构体定义,只需要在外部定义自己的扩展结构体,然后挂到这个指针上。
13.4 实战:设计一个可扩展的配置模块
好,理论说完了,我们来动手。我会设计一个支持INI和JSON双格式的配置模块,并且预留扩展接口。
13.4.1 整体架构
先画个图,让大家对整体结构有个概念。
13.4.2 核心代码实现
先定义解析器的接口。这样不管底层是INI还是JSON,上层调用都一样。
// parser_interface.h
typedef struct {
int (*parse)(const char* filepath, config_t* cfg);
int (*save)(const char* filepath, const config_t* cfg);
} parser_ops_t;
// 注册解析器
int config_register_parser(parser_ops_t* ops);
然后实现INI解析器。这里我只展示核心逻辑,完整的解析器代码量比较大,但思路就是逐行扫描。
// ini_parser.c
#include "parser_interface.h"
#include
#include
static int ini_parse(const char* filepath, config_t* cfg) {
FILE* fp = fopen(filepath, "r");
if (!fp) return -1;
char line[128];
char current_section[32] = {0};
while (fgets(line, sizeof(line), fp)) {
// 去掉换行符
line[strcspn(line, "\n")] = 0;
// 跳过空行和注释
if (line[0] == '\0' || line[0] == ';' || line[0] == '#') continue;
// 检查是否是节
if (line[0] == '[') {
sscanf(line, "[%[^]]]", current_section);
continue;
}
// 解析键值对
char key[32], value[64];
if (sscanf(line, "%[^=]=%s", key, value) == 2) {
// 根据 current_section 和 key 设置 cfg 的对应字段
if (strcmp(current_section, "system") == 0) {
if (strcmp(key, "baudrate") == 0) cfg->baudrate = atoi(value);
else if (strcmp(key, "log_level") == 0) cfg->log_level = atoi(value);
} else if (strcmp(current_section, "sensor") == 0) {
if (strcmp(key, "temp_offset") == 0) cfg->temp_offset = atof(value);
else if (strcmp(key, "humidity_calibration") == 0) cfg->humidity_calibration = atof(value);
}
}
}
fclose(fp);
return 0;
}
static parser_ops_t ini_ops = {
.parse = ini_parse,
.save = NULL // 保存功能类似,这里省略
};
13.4.3 配置管理器的实现
配置管理器负责协调解析器和上层应用。
// config_manager.c
#include "config_manager.h"
static config_t g_config;
static parser_ops_t* g_parser = NULL;
int config_init(const char* filepath) {
// 设置默认值
memset(&g_config, 0, sizeof(g_config));
g_config.baudrate = 9600; // 默认波特率
g_config.log_level = 2; // 默认日志级别
g_config.temp_offset = 0.0f;
g_config.humidity_calibration = 1.0f;
if (!g_parser) return -1; // 没有注册解析器
return g_parser->parse(filepath, &g_config);
}
int config_register_parser(parser_ops_t* ops) {
if (!ops || !ops->parse) return -1;
g_parser = ops;
return 0;
}
const config_t* config_get(void) {
return &g_config;
}
int config_set_baudrate(uint32_t baudrate) {
// 校验:波特率必须是标准值
if (baudrate != 9600 && baudrate != 19200 &&
baudrate != 38400 && baudrate != 115200) {
return -1; // 非法波特率
}
g_config.baudrate = baudrate;
return 0;
}
13.4.4 如何使用
在main函数里,使用起来非常简单:
// main.c
#include "config_manager.h"
#include "ini_parser.h" // 或者 json_parser.h
int main(void) {
// 注册解析器
config_register_parser(&ini_ops);
// 初始化配置
if (config_init("config.ini") != 0) {
printf("配置加载失败,使用默认配置\n");
}
// 读取配置
const config_t* cfg = config_get();
printf("当前波特率: %d\n", cfg->baudrate);
// 修改配置(带校验)
if (config_set_baudrate(115200) == 0) {
printf("波特率修改成功\n");
}
// ... 其他业务逻辑
}
13.5 可扩展性设计
这个模块的可扩展性体现在哪里?我总结了几点:
- 解析器可替换: 通过函数指针,INI和JSON解析器可以无缝切换。以后想支持YAML?写个YAML解析器,注册进来就行。
- 配置项可扩展: 结构体里的
ext_data指针,可以指向任意扩展结构。比如某个模块需要自己的配置,不用改核心代码。 - 校验逻辑可定制: 每个配置项的设置接口都可以独立加校验,不影响其他项。
13.6 小结
这一章我们聊了配置模块的设计思路。从INI和JSON的选型,到配置项的集中管理,再到一个可扩展的配置模块实战。核心就三点:解析器接口化、配置结构体统一管理、设置接口带校验。
配置模块看起来简单,但设计得好,能省很多后期维护的功夫。我见过太多项目,配置散落在各个.c文件里,改一个参数要找半天。嗯,希望这一章能帮你避免这种坑。
公众号:蓝海资料掘金营,微信deep3321