嵌入式实战 配置管理
从 #define 到 EEPROM:嵌入式参数持久化的迁移实战
项目初期所有参数都是 #define BAG_SPEED_HZ 10504。每次改速度都要重新编译、烧录、重启。现场调试时工程师提了一个需求:"能不能在串口屏上改完直接生效?"
答案是可以,但要走一条安全的迁移路径。
迁移四步走
第一步:定义参数 ID 枚举
每个参数一个全局唯一 ID,与 20B 协议的 Y1 字段对应:
typedef enum {
RCFG_ID_SPEED_BAG_OUT_HZ = 0x10,
RCFG_ID_SPEED_TEAR_OFF_HZ = 0x11,
RCFG_ID_SPEED_SEAL_MOVE_HZ = 0x12,
RCFG_ID_SPEED_SEAL_RETURN_HZ = 0x13,
RCFG_ID_STEPPER_START_HZ = 0x14,
RCFG_ID_STEPPER_ACCEL_HZ_PER_S = 0x15,
RCFG_ID_POWER_ON_DELAY_MS = 0x20,
RCFG_ID_SELF_CHECK_DELAY_MS = 0x21,
// ... 共约 30+ 个参数
} rcfg_id_t;
第二步:集中数据结构
typedef struct {
uint32_t version; // 结构体版本,用于兼容性检查
uint32_t speed_bag_out_hz;
uint32_t speed_tear_off_hz;
uint32_t speed_seal_move_hz;
uint32_t stepper_start_hz;
uint32_t stepper_accel_hz_per_s;
uint32_t power_on_delay_ms;
uint32_t self_check_delay_ms;
// ... 所有运行时参数
uint16_t crc; // 全结构体 CRC16,校验完整性
} packer_runtime_config_t;
第三步:四函数接口
// 1. 上电初始化 — 从 EEPROM 加载,加载失败回退默认值
void packer_runtime_config_init(void);
// 2. 只读访问 — 返回 const 指针,各模块直接读字段
const packer_runtime_config_t* packer_runtime_config_get(void);
// 3. 单参数写入(仅 RAM)
uint8_t packer_runtime_config_write(rcfg_id_t id, uint32_t value);
// 4. 显式保存到 EEPROM
uint8_t packer_runtime_config_save(void);
默认值集中在一个头文件中:
// app_runtime_config_defaults.h
#define RCFG_DEFAULT_SPEED_BAG_OUT_HZ 10504u
#define RCFG_DEFAULT_SPEED_TEAR_OFF_HZ 10504u
#define RCFG_DEFAULT_STEPPER_START_HZ 2000u
第四步:增量替换旧宏
不是一次性替换全部代码。逐个宏替换的流程:
- 在 runtime_config 结构体中加入新字段,默认值等于旧宏
- 所有代码改为通过
runtime_config_get()->xxx读取 grep -rn 'OLD_MACRO_NAME' --include='*.c' --include='*.h'确认零引用- 删除旧宏定义
- 同步清理 Doxygen
@name空组
整个迁移历时数个重构迭代,没有任何一个版本同时出现"新架构没写完、旧宏已删除"的中间态。
EEPROM 存储策略
// AT24C02: 256 字节,软件 I2C (PA11/PA12)
// 结构体放在 EEPROM 起始地址
#define RCFG_EEPROM_ADDR 0x00
// 保存流程:
// 1. 计算整个结构体的 CRC16
// 2. 写入 crc 字段
// 3. bsp_eeprom_write(addr, &config, sizeof(config))
// 加载流程:
// 1. bsp_eeprom_read(addr, &config, sizeof(config))
// 2. 重新计算 CRC16 与存储的 crc 比较
// 3. 匹配 → 使用;不匹配 → 回退默认值
串口调参链路
DGUS 串口屏 (USART1) → packer_dbus_frame → 解析 5A A5 帧
→ 识别写参数命令 → packer_runtime_config_write(id, value)
→ packer_runtime_config_save() ─→ EEPROM 持久化
HMI 上位机 (USART3) → 20B 协议帧 → 协议分发
→ 识别调参功能码 → packer_runtime_config_write(id, value)
→ packer_runtime_config_save() ─→ EEPROM 持久化
参数分类策略
| 分类 | 例子 | 管理方式 |
|---|---|---|
| 运行时调参 | 速度、加减速、延时、脉冲 | runtime_config + EEPROM |
| 固件定死 | 硬件引脚、节点地址、报警码 | 保留 #define |
| 换算常量 | 脉冲换算中间值 | 局部 static const |
核心经验:不要把"能在运行时调"和"应该运行时调"混为一谈。任务栈大小影响 .bss 布局,就不能运行时改。区分清楚再决定归属。
整个迁移过程中没有出现"旧宏已删、新字段未上线"的断档期。加字段 → 切换读取点 → 确认零引用 → 删宏。每一步都可回退。
Comments NOTHING