1. 项目概述iniSettings是一个面向嵌入式系统的轻量级配置管理库专为资源受限的 MCU 平台设计核心目标是提供稳定、可移植、线程安全的 INI 格式配置文件读写能力。与通用 PC 端 INI 解析器不同iniSettings不依赖标准 C 库的fopen/fread/fwrite而是直接对接底层文件系统抽象层如 FatFS、LittleFS、SPIFFS支持在 SD 卡、eMMC、QSPI Flash 等物理介质上持久化存储设备运行时参数、用户偏好、校准数据、网络配置等关键设置。该库不包含文件系统实现而是通过一组明确定义的回调函数接口callback interface与宿主文件系统解耦。这种设计使iniSettings具备极强的可移植性开发者只需实现 5 个基础 I/O 回调打开、读、写、关闭、同步即可将库无缝集成至任意支持顺序读写的嵌入式文件系统中。实测表明在 STM32F407 FatFSSD 卡和 ESP32 LittleFS内部 Flash两种典型平台上其内存占用均低于 1.2KBRAM代码体积小于 4.8KBFlash且无动态内存分配malloc/free完全满足 IEC 61508 SIL-2 或 ISO 26262 ASIL-B 等功能安全场景对确定性行为的要求。INI 文件格式因其人类可读性、结构清晰性和编辑便捷性在工业控制、IoT 终端、医疗设备固件升级包、测试仪器配置等领域被广泛采用。iniSettings的工程价值在于它将这一成熟格式引入裸机Bare-Metal或 RTOSFreeRTOS、Zephyr、RT-Thread环境并解决了嵌入式场景下长期存在的三大痛点原子写入缺失传统fputs直接覆盖易导致断电后配置损坏多任务并发冲突多个任务同时读写同一配置节section引发数据竞争路径与编码鲁棒性差缺乏对长路径、特殊字符、BOM 头、DOS/Unix 换行符的统一处理。iniSettings通过“双文件原子提交”机制、细粒度互斥锁Mutex封装及严格的 UTF-8/BOM 自适应解析系统性规避了上述风险。2. 核心架构与设计原理2.1 分层抽象模型iniSettings采用三层架构严格分离关注点层级模块职责可替换性应用层用户代码调用ini_get_string()、ini_set_int()等 API完全独立中间层iniSettings.c/hINI 语法解析、缓存管理、锁机制、原子写入逻辑不可替换核心逻辑驱动层ini_fs_ops_t回调表封装open()、read()、write()等底层 I/O完全可替换适配任意 FS该模型确保当从 FatFS 迁移至 LittleFS 时仅需重写 5 个回调函数上层业务代码零修改。2.2 原子写入机制详解嵌入式设备断电是常态。若配置更新采用“先删后写”或“原地覆盖”断电瞬间极易产生半截文件truncated file导致下次启动解析失败。iniSettings采用Write-Ahead Logging (WAL) Rename Atomicity双保险策略临时文件写入所有ini_save()操作首先将完整新配置写入临时文件如settings.ini.tmp同步刷盘调用fs_sync()强制将临时文件数据及元数据写入物理介质原子重命名调用fs_rename(settings.ini.tmp, settings.ini)—— 此操作在 FatFS/LittleFS 中均为原子操作POSIXrename()语义旧文件清理仅在重命名成功后才尝试删除原settings.ini失败亦无害。此流程保证任意时刻settings.ini要么是旧版完整文件要么是新版完整文件绝不存在中间态损坏文件。关键代码逻辑如下// iniSettings.c 片段原子保存核心流程 static ini_err_t ini_save_atomic(const char* filename, const ini_section_t* sections) { char tmp_name[INI_MAX_PATH_LEN]; snprintf(tmp_name, sizeof(tmp_name), %s.tmp, filename); // 步骤1写入临时文件 ini_err_t err ini_write_to_file(tmp_name, sections); if (err ! INI_OK) return err; // 步骤2强制同步到介质 if (!g_fs_ops.sync || !g_fs_ops.sync(tmp_name)) { g_fs_ops.remove(tmp_name); // 清理临时文件 return INI_FS_ERROR; } // 步骤3原子重命名关键 if (!g_fs_ops.rename(tmp_name, filename)) { g_fs_ops.remove(tmp_name); return INI_FS_ERROR; } // 步骤4清理可能残留的旧备份非必需增强健壮性 char bak_name[INI_MAX_PATH_LEN]; snprintf(bak_name, sizeof(bak_name), %s.bak, filename); g_fs_ops.remove(bak_name); return INI_OK; }工程提示g_fs_ops.sync()的实现必须确保数据块 文件系统元数据FAT 表、目录项均落盘。FatFS 中需调用f_sync(fil)LittleFS 中需确保lfs_file_sync()返回成功。忽略此步将使原子性失效。2.3 线程安全模型在 FreeRTOS 或 Zephyr 等多任务环境中多个任务可能并发访问配置任务 A 读取[network] ssid任务 B 同时写入[sensor] calib_offset任务 C 触发ini_save()持久化。iniSettings提供两级锁机制全局配置锁Global Mutex保护整个配置树ini_section_t*链表的读写互斥。所有ini_get_*()、ini_set_*()、ini_save()必须持有此锁。节级读写锁Section RWLock在全局锁内对单个 section如[wifi]进一步加读锁允许多读单写或写锁独占。此设计提升高并发读场景性能。FreeRTOS 下锁实现示例// 初始化通常在系统启动时调用一次 static StaticSemaphore_t g_ini_mutex_buffer; static SemaphoreHandle_t g_ini_mutex NULL; void ini_init(void) { g_ini_mutex xSemaphoreCreateMutexStatic(g_ini_mutex_buffer); // ... 其他初始化 } // 获取配置值自动加锁 const char* ini_get_string(const char* section, const char* key, const char* def) { if (xSemaphoreTake(g_ini_mutex, portMAX_DELAY) ! pdTRUE) { return def; // 锁获取失败返回默认值 } const char* val ini_get_string_unlocked(section, key, def); xSemaphoreGive(g_ini_mutex); return val; }关键约束iniSettings不负责创建或管理任务/线程锁的生命周期完全由用户控制。在裸机系统中可使用__disable_irq()/__enable_irq()替代 Mutex。3. API 接口规范与使用详解3.1 文件系统回调接口ini_fs_ops_t这是集成iniSettings的唯一必填接口必须在调用任何其他 API 前完成注册typedef struct { // 打开文件返回非NULL句柄表示成功NULL表示失败 void* (*open)(const char* path, const char* mode); // mode: r, w, a // 读取数据返回实际读取字节数0表示EOF或错误 size_t (*read)(void* handle, void* buf, size_t size); // 写入数据返回实际写入字节数0表示错误 size_t (*write)(void* handle, const void* buf, size_t size); // 关闭文件成功返回true失败返回false bool (*close)(void* handle); // 同步文件强制刷盘成功返回true bool (*sync)(const char* path); // 【可选】重命名用于原子提交若不支持可设为NULL降级为非原子写 bool (*rename)(const char* oldpath, const char* newpath); // 【可选】删除文件用于清理临时文件若不支持可设为NULL bool (*remove)(const char* path); } ini_fs_ops_t; // 注册回调必须在首次使用前调用 void ini_register_fs_ops(const ini_fs_ops_t* ops);FatFS 典型实现ff_ini_adapter.c#include ff.h static FIL g_ini_fil; static void* fatfs_open(const char* path, const char* mode) { BYTE fa (mode[0] w) ? FA_WRITE | FA_CREATE_ALWAYS : FA_READ; FRESULT res f_open(g_ini_fil, path, fa); return (res FR_OK) ? g_ini_fil : NULL; } static size_t fatfs_read(void* handle, void* buf, size_t size) { UINT br; f_read((FIL*)handle, buf, size, br); return br; } static size_t fatfs_write(void* handle, const void* buf, size_t size) { UINT bw; f_write((FIL*)handle, buf, size, bw); return bw; } static bool fatfs_close(void* handle) { return f_close((FIL*)handle) FR_OK; } static bool fatfs_sync(const char* path) { // FatFS 中 sync 即 f_sync() FIL fil; if (f_open(fil, path, FA_READ) ! FR_OK) return false; bool ok (f_sync(fil) FR_OK); f_close(fil); return ok; } // 注册到 iniSettings static const ini_fs_ops_t g_fatfs_ops { .open fatfs_open, .read fatfs_read, .write fatfs_write, .close fatfs_close, .sync fatfs_sync, .rename fatfs_rename, // 需自行实现 f_rename() 封装 .remove fatfs_remove, // 需自行实现 f_unlink() 封装 }; void app_ini_init(void) { ini_register_fs_ops(g_fatfs_ops); }3.2 配置操作 API所有 API 均以ini_为前缀语义清晰参数安全函数签名功能参数说明返回值void ini_init(void)初始化库注册默认回调、清空缓存无无ini_err_t ini_load(const char* filename)从文件加载配置到内存缓存filename: INI 文件路径如 /cfg/settings.iniINI_OK或错误码ini_err_t ini_save(const char* filename)将内存缓存保存至文件原子写入filename: 目标文件路径INI_OK或错误码const char* ini_get_string(const char* sec, const char* key, const char* def)获取字符串值sec: 节名如 wifikey: 键名如 ssiddef: 未找到时的默认值字符串指针指向缓存内内存int ini_get_int(const char* sec, const char* key, int def)获取整数支持十进制、十六进制0x同上整数值bool ini_get_bool(const char* sec, const char* key, bool def)获取布尔值支持true/false,1/0,on/off,yes/no同上true/falseini_err_t ini_set_string(const char* sec, const char* key, const char* val)设置字符串值自动创建节val: 值字符串可为 NULL表示删除该键INI_OK或错误码ini_err_t ini_set_int(const char* sec, const char* key, int val)设置整数值val: 整数值INI_OK或错误码ini_err_t ini_set_bool(const char* sec, const char* key, bool val)设置布尔值val:true/falseINI_OK或错误码ini_err_t ini_remove_section(const char* sec)删除整个节及其所有键sec: 节名INI_OK或错误码关键行为说明ini_get_*()返回的字符串指向内部缓存其生命周期与ini_load()加载的配置一致。若需长期持有必须strdup()ini_set_*()若指定节sec不存在则自动创建ini_set_string(sec, key, NULL)等价于删除该key所有ini_set_*()操作仅修改内存缓存必须显式调用ini_save()才会持久化。3.3 错误码定义ini_err_ttypedef enum { INI_OK 0, // 成功 INI_INVALID_ARG -1, // 无效参数如 NULL 指针 INI_NOT_FOUND -2, // 键或节未找到 INI_PARSE_ERROR -3, // INI 文件语法错误如缺少 INI_FS_ERROR -4, // 文件系统操作失败IO 错误 INI_NO_MEMORY -5, // 内存不足仅当启用动态分配时 INI_LOCK_ERROR -6, // 获取锁失败RTOS 环境 } ini_err_t;4. 典型应用场景与代码示例4.1 场景一Wi-Fi 配置管理ESP32 LittleFS设备需存储 SSID、密码、AP/STA 模式并在启动时自动连接// 1. LittleFS 回调实现简化 static lfs_file_t g_lfs_file; static const ini_fs_ops_t g_lfs_ops { .open lfs_open_wrapper, .read lfs_read_wrapper, .write lfs_write_wrapper, .close lfs_close_wrapper, .sync lfs_sync_wrapper, .rename lfs_rename_wrapper, .remove lfs_remove_wrapper, }; // 2. 初始化与加载 void wifi_config_init(void) { ini_register_fs_ops(g_lfs_ops); ini_init(); // 加载配置若文件不存在则使用默认值 if (ini_load(/wifi.cfg) ! INI_OK) { // 首次运行设置默认 AP 模式 ini_set_string(wifi, mode, ap); ini_set_string(wifi, ssid, MyDevice_AP); ini_set_string(wifi, password, 12345678); ini_save(/wifi.cfg); // 持久化 } } // 3. 运行时读取并连接 void wifi_connect(void) { const char* mode ini_get_string(wifi, mode, sta); if (strcmp(mode, ap) 0) { esp_netif_create_default_wifi_ap(); wifi_config_t ap_cfg { .ap { .ssid (char*)ini_get_string(wifi, ssid, ESP32_AP), .password (char*)ini_get_string(wifi, password, ), .max_connection 4, } }; esp_wifi_set_config(WIFI_IF_AP, ap_cfg); } else { // STA 模式连接... } }4.2 场景二传感器校准参数存储STM32H7 FatFS SD 卡工业传感器需定期校准校准值偏移、增益必须掉电保存// 定义校准结构体与 INI 键名映射 typedef struct { float temp_offset; // [calibration] temp_offset float temp_gain; // [calibration] temp_gain float humi_offset; // [calibration] humi_offset } sensor_calib_t; // 从 INI 加载校准参数 sensor_calib_t load_calibration(void) { sensor_calib_t calib {0}; calib.temp_offset ini_get_float(calibration, temp_offset, 0.0f); calib.temp_gain ini_get_float(calibration, temp_gain, 1.0f); calib.humi_offset ini_get_float(calibration, humi_offset, 0.0f); return calib; } // 保存校准参数在用户触发校准后 void save_calibration(const sensor_calib_t* calib) { ini_set_float(calibration, temp_offset, calib-temp_offset); ini_set_float(calibration, temp_gain, calib-temp_gain); ini_set_float(calibration, humi_offset, calib-humi_offset); ini_save(/sd/calib.ini); // SD 卡根目录 } // 在 FreeRTOS 任务中安全调用 void calibration_task(void* pvParameters) { for(;;) { if (user_pressed_calibrate_btn()) { sensor_calib_t new_calib run_sensor_calibration(); save_calibration(new_calib); printf(Calibration saved!\n); } vTaskDelay(pdMS_TO_TICKS(100)); } }4.3 场景三多任务并发配置访问Zephyr RTOS三个任务协同工作需安全共享配置// 定义配置节 // [system] log_level3, auto_updatetrue // [network] ip192.168.1.100, port8080 // 任务1日志服务只读 void log_task(void* p) { for(;;) { int level ini_get_int(system, log_level, 2); if (level 3) { LOG_INF(Debug message...); } k_msleep(1000); } } // 任务2网络服务读写 void net_task(void* p) { for(;;) { const char* ip ini_get_string(network, ip, 127.0.0.1); uint16_t port ini_get_int(network, port, 80); // 尝试连接失败则重试并记录 if (!try_connect(ip, port)) { ini_set_int(network, retry_count, ini_get_int(network, retry_count, 0) 1); ini_save(/cfg/network.ini); } k_msleep(5000); } } // 任务3OTA 更新服务写入 void ota_task(void* p) { if (ota_update_available()) { ini_set_string(system, fw_version, v2.1.0); ini_set_bool(system, auto_update, true); ini_save(/cfg/system.ini); // 原子写入不影响其他任务读取 } }5. 高级配置与性能调优5.1 内存模型配置iniSettings默认使用静态内存池避免malloc。关键宏定义在ini_settings.h中宏定义默认值说明调整建议INI_MAX_SECTIONS16最大支持节section数量根据实际配置节数量设定每节约占用 24 字节 RAMINI_MAX_KEYS_PER_SECTION32每节最大键key数量每键占用约 16 字节含字符串指针INI_MAX_KEY_LEN64键名最大长度含 \0超过则截断建议 ≤128INI_MAX_VALUE_LEN256值字符串最大长度含 \0大文本如证书需外部存储此处仅存路径INI_MAX_PATH_LEN128文件路径最大长度SD 卡路径通常足够QSPI Flash 可设小些内存占用计算示例默认配置节描述符16 × 24 384 字节键描述符16 × 32 × 16 8192 字节字符串缓冲区16 × 32 × (64256) 163840 字节此为最大理论值实际按需分配实际 RAM 占用 ≈ 2.1KB典型配置下重要INI_MAX_VALUE_LEN限制的是解析时的值缓冲区并非存储限制。若值超长ini_get_string()仍可返回其地址只要文件系统能读取但ini_set_string()会截断。5.2 解析性能优化对于频繁读取的配置如 PID 控制参数可启用缓存预热// 启动时预加载关键节到 RAM避免每次解析 void preload_critical_sections(void) { ini_load(/cfg/control.ini); // 立即解析 [pid] 节后续 get 操作无需再扫描全文 ini_get_float(pid, kp, 1.0f); ini_get_float(pid, ki, 0.1f); ini_get_float(pid, kd, 0.05f); }5.3 安全加固建议配置文件权限在 FatFS 中通过f_chmod()设置AM_RDO只读位防止意外覆盖完整性校验在ini_save()后计算settings.ini的 CRC32 并存入独立文件/cfg/settings.crc加载时校验敏感信息加密iniSettings不内置加密但可在ini_fs_ops.write回调中对写入内容 AES-ECB 加密需注意 ECB 模式弱点推荐 CBC/GCM防暴力修改在ini_get_string()返回前对关键键如password进行白名单检查非法访问记录日志。6. 故障排查与调试技巧6.1 常见错误与解决方案现象可能原因诊断方法解决方案ini_load()返回INI_PARSE_ERRORINI 文件存在语法错误如keyvalue缺少或注释符#位置错误用 PC 端文本编辑器打开文件检查第 N 行错误码含行号使用ini_dump()打印解析过程或启用INI_DEBUG宏输出详细日志ini_get_*()总是返回默认值节名/键名拼写错误文件未成功ini_load()文件路径错误检查ini_load()返回值用ini_dump()查看已加载的节和键确保ini_load()在ini_get_*()前调用打印ini_get_string(NULL, NULL, TEST)验证库是否初始化ini_save()后文件内容为空或乱码fs_write()回调未正确处理换行符\nfs_sync()未实现或失败在回调中添加printf(WRITE %d bytes: %.*s\n, size, size, (char*)buf)确保回调按size字节数精确写入验证fs_sync()是否真正刷盘多任务下配置值错乱未启用线程安全裸机未关中断RTOS 锁未正确初始化在ini_get_string()开头添加printf(GET %s.%s\n, sec, key)观察交叉输出调用ini_init()确保锁创建检查xSemaphoreTake()返回值6.2 调试辅助函数启用INI_DEBUG宏在ini_settings.h中取消注释可激活以下函数void ini_dump(void)打印当前内存中所有节、键、值到printf用于快速验证加载结果void ini_dump_section(const char* sec)仅打印指定节的内容void ini_print_stats(void)输出内存使用统计已用节/键数量、最大占用等。// 调试示例 ini_load(/cfg/test.ini); ini_dump(); // 输出类似 // [wifi] // ssid MyNetwork // password secret123 // [system] // log_level 37. 与主流嵌入式生态的集成7.1 STM32CubeMX HAL FatFS在 CubeMX 中启用 FatFSSDIO 或 SPI 模式将iniSettings源码加入Core/Src实现fatfs_ini_adapter.c如前文所示在main.c的MX_FATFS_Init()后调用app_ini_init()关键确保 FatFS 的USER_diskio.c中disk_ioctl()正确实现CTRL_SYNC否则fs_sync()失效。7.2 ESP-IDF LittleFS在sdkconfig中启用LittleFSCONFIG_LITTLEFS创建components/ini-settings/目录放入源码编写lfs_ini_adapter.c利用lfs_file_open()等 API在app_main()中调用ini_register_fs_ops()和ini_init()优势LittleFS 原生支持磨损均衡和掉电安全与iniSettings的原子写入形成双重保障。7.3 Zephyr RTOS NFFS/JFFS2Zephyr 的 NFFSNOR Flash File System或 JFFS2 驱动需提供struct fs_file_t抽象。适配要点open()返回struct fs_file_t*read()/write()调用fs_read()/fs_write()sync()调用fs_sync()Zephyr 2.7 支持注意 Zephyr 的Kconfig需启用CONFIG_FILE_SYSTEM和对应 FS。iniSettings的设计哲学是不做文件系统只做好配置。它不试图替代 FatFS 或 LittleFS而是作为其上一层简洁、可靠的配置管理层让嵌入式工程师能像在 Linux 上编辑/etc/配置一样安全、高效地管理 MCU 的“大脑”。
嵌入式INI配置库:轻量、原子写入与线程安全设计
1. 项目概述iniSettings是一个面向嵌入式系统的轻量级配置管理库专为资源受限的 MCU 平台设计核心目标是提供稳定、可移植、线程安全的 INI 格式配置文件读写能力。与通用 PC 端 INI 解析器不同iniSettings不依赖标准 C 库的fopen/fread/fwrite而是直接对接底层文件系统抽象层如 FatFS、LittleFS、SPIFFS支持在 SD 卡、eMMC、QSPI Flash 等物理介质上持久化存储设备运行时参数、用户偏好、校准数据、网络配置等关键设置。该库不包含文件系统实现而是通过一组明确定义的回调函数接口callback interface与宿主文件系统解耦。这种设计使iniSettings具备极强的可移植性开发者只需实现 5 个基础 I/O 回调打开、读、写、关闭、同步即可将库无缝集成至任意支持顺序读写的嵌入式文件系统中。实测表明在 STM32F407 FatFSSD 卡和 ESP32 LittleFS内部 Flash两种典型平台上其内存占用均低于 1.2KBRAM代码体积小于 4.8KBFlash且无动态内存分配malloc/free完全满足 IEC 61508 SIL-2 或 ISO 26262 ASIL-B 等功能安全场景对确定性行为的要求。INI 文件格式因其人类可读性、结构清晰性和编辑便捷性在工业控制、IoT 终端、医疗设备固件升级包、测试仪器配置等领域被广泛采用。iniSettings的工程价值在于它将这一成熟格式引入裸机Bare-Metal或 RTOSFreeRTOS、Zephyr、RT-Thread环境并解决了嵌入式场景下长期存在的三大痛点原子写入缺失传统fputs直接覆盖易导致断电后配置损坏多任务并发冲突多个任务同时读写同一配置节section引发数据竞争路径与编码鲁棒性差缺乏对长路径、特殊字符、BOM 头、DOS/Unix 换行符的统一处理。iniSettings通过“双文件原子提交”机制、细粒度互斥锁Mutex封装及严格的 UTF-8/BOM 自适应解析系统性规避了上述风险。2. 核心架构与设计原理2.1 分层抽象模型iniSettings采用三层架构严格分离关注点层级模块职责可替换性应用层用户代码调用ini_get_string()、ini_set_int()等 API完全独立中间层iniSettings.c/hINI 语法解析、缓存管理、锁机制、原子写入逻辑不可替换核心逻辑驱动层ini_fs_ops_t回调表封装open()、read()、write()等底层 I/O完全可替换适配任意 FS该模型确保当从 FatFS 迁移至 LittleFS 时仅需重写 5 个回调函数上层业务代码零修改。2.2 原子写入机制详解嵌入式设备断电是常态。若配置更新采用“先删后写”或“原地覆盖”断电瞬间极易产生半截文件truncated file导致下次启动解析失败。iniSettings采用Write-Ahead Logging (WAL) Rename Atomicity双保险策略临时文件写入所有ini_save()操作首先将完整新配置写入临时文件如settings.ini.tmp同步刷盘调用fs_sync()强制将临时文件数据及元数据写入物理介质原子重命名调用fs_rename(settings.ini.tmp, settings.ini)—— 此操作在 FatFS/LittleFS 中均为原子操作POSIXrename()语义旧文件清理仅在重命名成功后才尝试删除原settings.ini失败亦无害。此流程保证任意时刻settings.ini要么是旧版完整文件要么是新版完整文件绝不存在中间态损坏文件。关键代码逻辑如下// iniSettings.c 片段原子保存核心流程 static ini_err_t ini_save_atomic(const char* filename, const ini_section_t* sections) { char tmp_name[INI_MAX_PATH_LEN]; snprintf(tmp_name, sizeof(tmp_name), %s.tmp, filename); // 步骤1写入临时文件 ini_err_t err ini_write_to_file(tmp_name, sections); if (err ! INI_OK) return err; // 步骤2强制同步到介质 if (!g_fs_ops.sync || !g_fs_ops.sync(tmp_name)) { g_fs_ops.remove(tmp_name); // 清理临时文件 return INI_FS_ERROR; } // 步骤3原子重命名关键 if (!g_fs_ops.rename(tmp_name, filename)) { g_fs_ops.remove(tmp_name); return INI_FS_ERROR; } // 步骤4清理可能残留的旧备份非必需增强健壮性 char bak_name[INI_MAX_PATH_LEN]; snprintf(bak_name, sizeof(bak_name), %s.bak, filename); g_fs_ops.remove(bak_name); return INI_OK; }工程提示g_fs_ops.sync()的实现必须确保数据块 文件系统元数据FAT 表、目录项均落盘。FatFS 中需调用f_sync(fil)LittleFS 中需确保lfs_file_sync()返回成功。忽略此步将使原子性失效。2.3 线程安全模型在 FreeRTOS 或 Zephyr 等多任务环境中多个任务可能并发访问配置任务 A 读取[network] ssid任务 B 同时写入[sensor] calib_offset任务 C 触发ini_save()持久化。iniSettings提供两级锁机制全局配置锁Global Mutex保护整个配置树ini_section_t*链表的读写互斥。所有ini_get_*()、ini_set_*()、ini_save()必须持有此锁。节级读写锁Section RWLock在全局锁内对单个 section如[wifi]进一步加读锁允许多读单写或写锁独占。此设计提升高并发读场景性能。FreeRTOS 下锁实现示例// 初始化通常在系统启动时调用一次 static StaticSemaphore_t g_ini_mutex_buffer; static SemaphoreHandle_t g_ini_mutex NULL; void ini_init(void) { g_ini_mutex xSemaphoreCreateMutexStatic(g_ini_mutex_buffer); // ... 其他初始化 } // 获取配置值自动加锁 const char* ini_get_string(const char* section, const char* key, const char* def) { if (xSemaphoreTake(g_ini_mutex, portMAX_DELAY) ! pdTRUE) { return def; // 锁获取失败返回默认值 } const char* val ini_get_string_unlocked(section, key, def); xSemaphoreGive(g_ini_mutex); return val; }关键约束iniSettings不负责创建或管理任务/线程锁的生命周期完全由用户控制。在裸机系统中可使用__disable_irq()/__enable_irq()替代 Mutex。3. API 接口规范与使用详解3.1 文件系统回调接口ini_fs_ops_t这是集成iniSettings的唯一必填接口必须在调用任何其他 API 前完成注册typedef struct { // 打开文件返回非NULL句柄表示成功NULL表示失败 void* (*open)(const char* path, const char* mode); // mode: r, w, a // 读取数据返回实际读取字节数0表示EOF或错误 size_t (*read)(void* handle, void* buf, size_t size); // 写入数据返回实际写入字节数0表示错误 size_t (*write)(void* handle, const void* buf, size_t size); // 关闭文件成功返回true失败返回false bool (*close)(void* handle); // 同步文件强制刷盘成功返回true bool (*sync)(const char* path); // 【可选】重命名用于原子提交若不支持可设为NULL降级为非原子写 bool (*rename)(const char* oldpath, const char* newpath); // 【可选】删除文件用于清理临时文件若不支持可设为NULL bool (*remove)(const char* path); } ini_fs_ops_t; // 注册回调必须在首次使用前调用 void ini_register_fs_ops(const ini_fs_ops_t* ops);FatFS 典型实现ff_ini_adapter.c#include ff.h static FIL g_ini_fil; static void* fatfs_open(const char* path, const char* mode) { BYTE fa (mode[0] w) ? FA_WRITE | FA_CREATE_ALWAYS : FA_READ; FRESULT res f_open(g_ini_fil, path, fa); return (res FR_OK) ? g_ini_fil : NULL; } static size_t fatfs_read(void* handle, void* buf, size_t size) { UINT br; f_read((FIL*)handle, buf, size, br); return br; } static size_t fatfs_write(void* handle, const void* buf, size_t size) { UINT bw; f_write((FIL*)handle, buf, size, bw); return bw; } static bool fatfs_close(void* handle) { return f_close((FIL*)handle) FR_OK; } static bool fatfs_sync(const char* path) { // FatFS 中 sync 即 f_sync() FIL fil; if (f_open(fil, path, FA_READ) ! FR_OK) return false; bool ok (f_sync(fil) FR_OK); f_close(fil); return ok; } // 注册到 iniSettings static const ini_fs_ops_t g_fatfs_ops { .open fatfs_open, .read fatfs_read, .write fatfs_write, .close fatfs_close, .sync fatfs_sync, .rename fatfs_rename, // 需自行实现 f_rename() 封装 .remove fatfs_remove, // 需自行实现 f_unlink() 封装 }; void app_ini_init(void) { ini_register_fs_ops(g_fatfs_ops); }3.2 配置操作 API所有 API 均以ini_为前缀语义清晰参数安全函数签名功能参数说明返回值void ini_init(void)初始化库注册默认回调、清空缓存无无ini_err_t ini_load(const char* filename)从文件加载配置到内存缓存filename: INI 文件路径如 /cfg/settings.iniINI_OK或错误码ini_err_t ini_save(const char* filename)将内存缓存保存至文件原子写入filename: 目标文件路径INI_OK或错误码const char* ini_get_string(const char* sec, const char* key, const char* def)获取字符串值sec: 节名如 wifikey: 键名如 ssiddef: 未找到时的默认值字符串指针指向缓存内内存int ini_get_int(const char* sec, const char* key, int def)获取整数支持十进制、十六进制0x同上整数值bool ini_get_bool(const char* sec, const char* key, bool def)获取布尔值支持true/false,1/0,on/off,yes/no同上true/falseini_err_t ini_set_string(const char* sec, const char* key, const char* val)设置字符串值自动创建节val: 值字符串可为 NULL表示删除该键INI_OK或错误码ini_err_t ini_set_int(const char* sec, const char* key, int val)设置整数值val: 整数值INI_OK或错误码ini_err_t ini_set_bool(const char* sec, const char* key, bool val)设置布尔值val:true/falseINI_OK或错误码ini_err_t ini_remove_section(const char* sec)删除整个节及其所有键sec: 节名INI_OK或错误码关键行为说明ini_get_*()返回的字符串指向内部缓存其生命周期与ini_load()加载的配置一致。若需长期持有必须strdup()ini_set_*()若指定节sec不存在则自动创建ini_set_string(sec, key, NULL)等价于删除该key所有ini_set_*()操作仅修改内存缓存必须显式调用ini_save()才会持久化。3.3 错误码定义ini_err_ttypedef enum { INI_OK 0, // 成功 INI_INVALID_ARG -1, // 无效参数如 NULL 指针 INI_NOT_FOUND -2, // 键或节未找到 INI_PARSE_ERROR -3, // INI 文件语法错误如缺少 INI_FS_ERROR -4, // 文件系统操作失败IO 错误 INI_NO_MEMORY -5, // 内存不足仅当启用动态分配时 INI_LOCK_ERROR -6, // 获取锁失败RTOS 环境 } ini_err_t;4. 典型应用场景与代码示例4.1 场景一Wi-Fi 配置管理ESP32 LittleFS设备需存储 SSID、密码、AP/STA 模式并在启动时自动连接// 1. LittleFS 回调实现简化 static lfs_file_t g_lfs_file; static const ini_fs_ops_t g_lfs_ops { .open lfs_open_wrapper, .read lfs_read_wrapper, .write lfs_write_wrapper, .close lfs_close_wrapper, .sync lfs_sync_wrapper, .rename lfs_rename_wrapper, .remove lfs_remove_wrapper, }; // 2. 初始化与加载 void wifi_config_init(void) { ini_register_fs_ops(g_lfs_ops); ini_init(); // 加载配置若文件不存在则使用默认值 if (ini_load(/wifi.cfg) ! INI_OK) { // 首次运行设置默认 AP 模式 ini_set_string(wifi, mode, ap); ini_set_string(wifi, ssid, MyDevice_AP); ini_set_string(wifi, password, 12345678); ini_save(/wifi.cfg); // 持久化 } } // 3. 运行时读取并连接 void wifi_connect(void) { const char* mode ini_get_string(wifi, mode, sta); if (strcmp(mode, ap) 0) { esp_netif_create_default_wifi_ap(); wifi_config_t ap_cfg { .ap { .ssid (char*)ini_get_string(wifi, ssid, ESP32_AP), .password (char*)ini_get_string(wifi, password, ), .max_connection 4, } }; esp_wifi_set_config(WIFI_IF_AP, ap_cfg); } else { // STA 模式连接... } }4.2 场景二传感器校准参数存储STM32H7 FatFS SD 卡工业传感器需定期校准校准值偏移、增益必须掉电保存// 定义校准结构体与 INI 键名映射 typedef struct { float temp_offset; // [calibration] temp_offset float temp_gain; // [calibration] temp_gain float humi_offset; // [calibration] humi_offset } sensor_calib_t; // 从 INI 加载校准参数 sensor_calib_t load_calibration(void) { sensor_calib_t calib {0}; calib.temp_offset ini_get_float(calibration, temp_offset, 0.0f); calib.temp_gain ini_get_float(calibration, temp_gain, 1.0f); calib.humi_offset ini_get_float(calibration, humi_offset, 0.0f); return calib; } // 保存校准参数在用户触发校准后 void save_calibration(const sensor_calib_t* calib) { ini_set_float(calibration, temp_offset, calib-temp_offset); ini_set_float(calibration, temp_gain, calib-temp_gain); ini_set_float(calibration, humi_offset, calib-humi_offset); ini_save(/sd/calib.ini); // SD 卡根目录 } // 在 FreeRTOS 任务中安全调用 void calibration_task(void* pvParameters) { for(;;) { if (user_pressed_calibrate_btn()) { sensor_calib_t new_calib run_sensor_calibration(); save_calibration(new_calib); printf(Calibration saved!\n); } vTaskDelay(pdMS_TO_TICKS(100)); } }4.3 场景三多任务并发配置访问Zephyr RTOS三个任务协同工作需安全共享配置// 定义配置节 // [system] log_level3, auto_updatetrue // [network] ip192.168.1.100, port8080 // 任务1日志服务只读 void log_task(void* p) { for(;;) { int level ini_get_int(system, log_level, 2); if (level 3) { LOG_INF(Debug message...); } k_msleep(1000); } } // 任务2网络服务读写 void net_task(void* p) { for(;;) { const char* ip ini_get_string(network, ip, 127.0.0.1); uint16_t port ini_get_int(network, port, 80); // 尝试连接失败则重试并记录 if (!try_connect(ip, port)) { ini_set_int(network, retry_count, ini_get_int(network, retry_count, 0) 1); ini_save(/cfg/network.ini); } k_msleep(5000); } } // 任务3OTA 更新服务写入 void ota_task(void* p) { if (ota_update_available()) { ini_set_string(system, fw_version, v2.1.0); ini_set_bool(system, auto_update, true); ini_save(/cfg/system.ini); // 原子写入不影响其他任务读取 } }5. 高级配置与性能调优5.1 内存模型配置iniSettings默认使用静态内存池避免malloc。关键宏定义在ini_settings.h中宏定义默认值说明调整建议INI_MAX_SECTIONS16最大支持节section数量根据实际配置节数量设定每节约占用 24 字节 RAMINI_MAX_KEYS_PER_SECTION32每节最大键key数量每键占用约 16 字节含字符串指针INI_MAX_KEY_LEN64键名最大长度含 \0超过则截断建议 ≤128INI_MAX_VALUE_LEN256值字符串最大长度含 \0大文本如证书需外部存储此处仅存路径INI_MAX_PATH_LEN128文件路径最大长度SD 卡路径通常足够QSPI Flash 可设小些内存占用计算示例默认配置节描述符16 × 24 384 字节键描述符16 × 32 × 16 8192 字节字符串缓冲区16 × 32 × (64256) 163840 字节此为最大理论值实际按需分配实际 RAM 占用 ≈ 2.1KB典型配置下重要INI_MAX_VALUE_LEN限制的是解析时的值缓冲区并非存储限制。若值超长ini_get_string()仍可返回其地址只要文件系统能读取但ini_set_string()会截断。5.2 解析性能优化对于频繁读取的配置如 PID 控制参数可启用缓存预热// 启动时预加载关键节到 RAM避免每次解析 void preload_critical_sections(void) { ini_load(/cfg/control.ini); // 立即解析 [pid] 节后续 get 操作无需再扫描全文 ini_get_float(pid, kp, 1.0f); ini_get_float(pid, ki, 0.1f); ini_get_float(pid, kd, 0.05f); }5.3 安全加固建议配置文件权限在 FatFS 中通过f_chmod()设置AM_RDO只读位防止意外覆盖完整性校验在ini_save()后计算settings.ini的 CRC32 并存入独立文件/cfg/settings.crc加载时校验敏感信息加密iniSettings不内置加密但可在ini_fs_ops.write回调中对写入内容 AES-ECB 加密需注意 ECB 模式弱点推荐 CBC/GCM防暴力修改在ini_get_string()返回前对关键键如password进行白名单检查非法访问记录日志。6. 故障排查与调试技巧6.1 常见错误与解决方案现象可能原因诊断方法解决方案ini_load()返回INI_PARSE_ERRORINI 文件存在语法错误如keyvalue缺少或注释符#位置错误用 PC 端文本编辑器打开文件检查第 N 行错误码含行号使用ini_dump()打印解析过程或启用INI_DEBUG宏输出详细日志ini_get_*()总是返回默认值节名/键名拼写错误文件未成功ini_load()文件路径错误检查ini_load()返回值用ini_dump()查看已加载的节和键确保ini_load()在ini_get_*()前调用打印ini_get_string(NULL, NULL, TEST)验证库是否初始化ini_save()后文件内容为空或乱码fs_write()回调未正确处理换行符\nfs_sync()未实现或失败在回调中添加printf(WRITE %d bytes: %.*s\n, size, size, (char*)buf)确保回调按size字节数精确写入验证fs_sync()是否真正刷盘多任务下配置值错乱未启用线程安全裸机未关中断RTOS 锁未正确初始化在ini_get_string()开头添加printf(GET %s.%s\n, sec, key)观察交叉输出调用ini_init()确保锁创建检查xSemaphoreTake()返回值6.2 调试辅助函数启用INI_DEBUG宏在ini_settings.h中取消注释可激活以下函数void ini_dump(void)打印当前内存中所有节、键、值到printf用于快速验证加载结果void ini_dump_section(const char* sec)仅打印指定节的内容void ini_print_stats(void)输出内存使用统计已用节/键数量、最大占用等。// 调试示例 ini_load(/cfg/test.ini); ini_dump(); // 输出类似 // [wifi] // ssid MyNetwork // password secret123 // [system] // log_level 37. 与主流嵌入式生态的集成7.1 STM32CubeMX HAL FatFS在 CubeMX 中启用 FatFSSDIO 或 SPI 模式将iniSettings源码加入Core/Src实现fatfs_ini_adapter.c如前文所示在main.c的MX_FATFS_Init()后调用app_ini_init()关键确保 FatFS 的USER_diskio.c中disk_ioctl()正确实现CTRL_SYNC否则fs_sync()失效。7.2 ESP-IDF LittleFS在sdkconfig中启用LittleFSCONFIG_LITTLEFS创建components/ini-settings/目录放入源码编写lfs_ini_adapter.c利用lfs_file_open()等 API在app_main()中调用ini_register_fs_ops()和ini_init()优势LittleFS 原生支持磨损均衡和掉电安全与iniSettings的原子写入形成双重保障。7.3 Zephyr RTOS NFFS/JFFS2Zephyr 的 NFFSNOR Flash File System或 JFFS2 驱动需提供struct fs_file_t抽象。适配要点open()返回struct fs_file_t*read()/write()调用fs_read()/fs_write()sync()调用fs_sync()Zephyr 2.7 支持注意 Zephyr 的Kconfig需启用CONFIG_FILE_SYSTEM和对应 FS。iniSettings的设计哲学是不做文件系统只做好配置。它不试图替代 FatFS 或 LittleFS而是作为其上一层简洁、可靠的配置管理层让嵌入式工程师能像在 Linux 上编辑/etc/配置一样安全、高效地管理 MCU 的“大脑”。