从 #define 到 EEPROM:嵌入式参数持久化的迁移实战

Babel36acl 嵌入式实战 无~ 12 次阅读 预计阅读时间: 8 分钟 发布于 1 天前 最后更新于 1 小时前 1757 字


嵌入式实战 配置管理

从 #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

第四步:增量替换旧宏

不是一次性替换全部代码。逐个宏替换的流程:

  1. 在 runtime_config 结构体中加入新字段,默认值等于旧宏
  2. 所有代码改为通过 runtime_config_get()->xxx 读取
  3. grep -rn 'OLD_MACRO_NAME' --include='*.c' --include='*.h' 确认零引用
  4. 删除旧宏定义
  5. 同步清理 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 布局,就不能运行时改。区分清楚再决定归属。

整个迁移过程中没有出现"旧宏已删、新字段未上线"的断档期。加字段 → 切换读取点 → 确认零引用 → 删宏。每一步都可回退。

此作者没有提供个人介绍。
最后更新于 2026-05-30