运行时参数框架 (Runtime Config Framework)

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


一、参数描述表驱动读写 (Descriptor-Driven Access)

运行时参数框架采用参数描述表(rcfg_param_descriptor_t)驱动所有参数的读写操作。每个参数通过唯一的 ID 进行索引,描述表记录了参数在结构体中的偏移量(offset)、位宽(width)、取值范围(min/max)以及默认值。读写接口通过 ID → descriptor → struct offset 的映射实现 O(1) 访问,避免了冗长的 switch-case 或 if-else 链。

flowchart LR
    subgraph DESC[参数描述表]
        D1[ID / 偏移 / 位宽]
        D2[取值范围 / 默认值]
    end
    subgraph RW[读写接口]
        R1[rcfg_read]
        R2[rcfg_write]
    end
    subgraph STORE[存储后端]
        S1[RAM 运行时]
        S2[EEPROM 持久化]
    end
    D1 --> RW --> STORE
    style DESC fill:transparent,stroke:#8dc7ff,color:#eaf4ff
    style RW fill:transparent,stroke:#8dc7ff,color:#eaf4ff
    style STORE fill:transparent,stroke:#8dc7ff,color:#eaf4ff

核心数据结构:

typedef struct {
    uint16_t id;          // 参数 ID
    uint16_t offset;      // 在 packer_runtime_config_t 中的字节偏移
    uint8_t  width;       // 位宽(0 表示已废弃/只读)
    int32_t  min;         // 最小值
    int32_t  max;         // 最大值
    int32_t  def;         // 默认值
    uint8_t  flags;       // 属性标志位
} rcfg_param_descriptor_t;

所有与 HMI 或协议层的交互均通过统一的 rcfg_get_param() / rcfg_set_param() 接口完成,读写逻辑与具体业务解耦。

二、EEPROM 持久化与写保护策略

参数持久化采用 I²C EEPROM(24LC64 或兼容型号),存储布局包含魔法数、版本号、CRC16 校验和及参数数据块。写入操作采用"手动保存"策略——调用 rcfg_set_param() 仅修改内存中的缓存,必须显式调用 rcfg_save_to_eeprom() 才会触发 EEPROM 写入,从而避免频繁写入导致 EEPROM 寿命耗尽(典型 endurance 为 1,000,000 次)。

EEPROM 存储布局:

// EEPROM 布局(地址从 0 开始)
// +--------+---------+-------+----------+------+
// | Magic  | Version | CRC16 | Reserved | Data |
// | (2 B)  |  (2 B)  | (2 B) | (2 B)    |  N B |
// +--------+---------+-------+----------+------+
// Magic   = 0x5AA5
// Version = 0x000D (当前版本)
// CRC16   = CRC16-Modbus over data area only
// Reserved= 0x0000

加载流程的三重校验:读取后依次校验 Magic → Version → CRC16,任一校验失败则回退到编译期默认值(不自动回写 EEPROM,避免写入损坏数据)。


三、描述表设计:O(1) 参数访问

rcfg_param_descriptor_t 结构体是参数访问的核心元数据。每个活跃参数在编译期生成一个描述符条目,构成全局描述符表。40+ 个活跃参数通过 ID 线性索引描述表,每条记录直接映射到 packer_runtime_config_t 结构体的内存偏移,实现 O(1) 随机访问。

// 描述符结构体原型
typedef struct {
    uint16_t id;          // 参数 ID,0..RCFG_PARAM_COUNT-1
    uint16_t offset;      // packer_runtime_config_t 中的字节偏移
    uint8_t  width;       // 位宽(字节);width=0 标识已移除或废弃的参数
    int32_t  min;         // 最小值(含)
    int32_t  max;         // 最大值(含)
    int32_t  def;         // 出厂默认值
    uint8_t  flags;       // 位标志:只读、隐藏、需重启生效等
} rcfg_param_descriptor_t;

// 访问流程(伪代码)
int32_t rcfg_get_param(uint16_t id) {
    const rcfg_param_descriptor_t *desc = &s_param_descriptors[id];
    if (desc->width == 0) return 0;  // 已废弃参数
    uint8_t *base = (uint8_t *)&s_runtime_config;
    int32_t val = 0;
    memcpy(&val, base + desc->offset, desc->width);
    return val;
}

已移除/废弃参数处理:当某参数因功能迭代被移除时,其描述符条目保留在表中但 width 设为 0。get 操作返回 0,set 操作返回错误码 RCFG_ERR_OBSOLETE。这样既保证 ID 不重排(避免协议层兼容性问题),又明确标识废弃状态。

四、EEPROM 持久化三重校验

EEPROM 存储区布局:

// EEPROM 地址映射
// 0x0000: Magic         (uint16_t) = 0x5AA5
// 0x0002: Version        (uint16_t) = 0x000D
// 0x0004: CRC16          (uint16_t) = CRC16-Modbus of Data
// 0x0006: Reserved       (uint16_t) = 0x0000
// 0x0008: Data           (N bytes)  = packer_runtime_config_t

三重校验加载流程

rcfg_load_status_t rcfg_load_from_eeprom(void) {
    // 第一重:Magic 校验
    uint16_t magic = eeprom_read16(EEPROM_ADDR_MAGIC);
    if (magic != EEPROM_MAGIC_0x5AA5) {
        LOG_WRN("EEPROM magic mismatch: 0x%04X", magic);
        return RCFG_LOAD_FALLBACK_DEFAULTS;
    }
    // 第二重:Version 校验
    uint16_t version = eeprom_read16(EEPROM_ADDR_VERSION);
    if (version != EEPROM_CURRENT_VERSION) {
        LOG_WRN("EEPROM version mismatch: 0x%04X", version);
        return rcfg_migrate_from_version(version);
    }
    // 第三重:CRC16 校验
    uint16_t stored_crc = eeprom_read16(EEPROM_ADDR_CRC);
    uint16_t calc_crc = crc16_modbus(
        eeprom_data_ptr(EEPROM_ADDR_DATA),
        sizeof(packer_runtime_config_t)
    );
    if (stored_crc != calc_crc) {
        LOG_WRN("EEPROM CRC mismatch: stored=0x%04X calc=0x%04X",
                stored_crc, calc_crc);
        return RCFG_LOAD_FALLBACK_DEFAULTS;
    }
    // 全部通过 → 加载数据
    eeprom_read(EEPROM_ADDR_DATA, &s_runtime_config,
                sizeof(packer_runtime_config_t));
    return RCFG_LOAD_OK;
}

任何一重校验失败都回退到编译期默认值,不写回 EEPROM(避免将损坏数据覆盖回存储介质)。CRC16-Modbus 仅覆盖 Data 区域(不含 Magic、Version、CRC 自身及 Reserved 字段)。

五、旧版本兼容迁移

当 EEPROM 中存储的版本号低于当前版本时,rcfg_migrate_from_version() 负责将旧版数据升级到新版格式。迁移按版本号顺序链式执行。

0x000B → 0x000C:heater_pwm_duty 精度提升

heater_pwm_duty 从 0-100(百分比,整数)迁移到 0-200(千分比 ×10,即 0.0‰–200.0‰)。

static void migrate_0x000B_to_0x000C(packer_runtime_config_t *cfg) {
    // 旧版:heater_pwm_duty = 0..100 (percent)
    // 新版:heater_pwm_duty = 0..200 (per-mille * 10, i.e. 0.0‰..200.0‰)
    int32_t old_val = cfg->legacy.heater_pwm_duty;  // 0..100
    cfg->heater_pwm_duty = old_val * 2;              // 0..200
    LOG_INF("Migrated heater_pwm_duty: %d → %d", old_val, cfg->heater_pwm_duty);
}

0x000C → 0x000D:tear_off_len_mm_x1000 字段补充

0x000C 版本缺少 tear_off_len_mm_x1000 字段。迁移时通过旧结构体镜像(legacy struct mirror)读取旧布局,将缺失字段设为默认值后重新保存。

// 旧版结构体镜像(0x000C 布局)
typedef struct __attribute__((packed)) {
    // ... 所有 0x000C 版本的字段 ...
    uint8_t  leg_puller_duty;       // 旧版同位置字段
    // 注:tear_off_len_mm_x1000 在 0x000C 中不存在
    uint16_t heater_pwm_duty;       // 已迁移
    // ...
} packer_runtime_config_0x000C_t;

static void migrate_0x000C_to_0x000D(packer_runtime_config_t *cfg) {
    packer_runtime_config_0x000C_t old;
    memcpy(&old, cfg, sizeof(old));  // 从当前结构体读取旧版数据

    // 填充缺失字段的默认值
    cfg->tear_off_len_mm_x1000 = TEAR_OFF_LEN_DEFAULT_MM_X1000;
    LOG_INF("Migrated tear_off_len_mm_x1000 ← default (%d)",
            cfg->tear_off_len_mm_x1000);
}

迁移模式总结:每个版本升级对应一个独立的迁移函数,按版本号顺序依次调用。迁移仅操作内存中的数据,最终由上层决定是否写回 EEPROM。

六、参数清洗与范围钳位

每次加载完成或初始化后,调用 rcfg_sanitize_config() 遍历所有活跃参数,将超出 [min, max] 范围的值钳位到边界值。每个钳位操作记录一条 WRN 级别日志,便于调试和审计。

void rcfg_sanitize_config(void) {
    for (int i = 0; i < RCFG_PARAM_ACTIVE_COUNT; i++) {
        const rcfg_param_descriptor_t *desc = &s_param_descriptors[i];
        if (desc->width == 0) continue;  // 跳过废弃参数

        int32_t val = rcfg_get_param(desc->id);
        int32_t clamped = val;

        if (val < desc->min) {
            clamped = desc->min;
            LOG_WRN("Param[%d] %s clamped: %d → %d (min=%d)",
                    desc->id, desc->name, val, clamped, desc->min);
        } else if (val > desc->max) {
            clamped = desc->max;
            LOG_WRN("Param[%d] %s clamped: %d → %d (max=%d)",
                    desc->id, desc->name, val, clamped, desc->max);
        }

        if (clamped != val) {
            rcfg_set_param(desc->id, clamped);
        }
    }
}

rcfg_sanitize_config() 在以下时机被调用:

  • 系统上电初始化(从 EEPROM 加载后)
  • EEPROM 校验失败回退到默认值后
  • 版本迁移完成后
  • 外部协议批量设置参数后

七、惰性派生缓存刷新

为避免每次读取派生值都重新计算,框架采用脏标志(dirty flag)机制实现惰性刷新。

// ===== 脏标志管理 =====
static bool s_derived_dirty = true;

void packer_runtime_config_mark_derived_dirty(void) {
    s_derived_dirty = true;
}

void packer_runtime_config_refresh_derived_if_needed(void) {
    if (!s_derived_dirty) return;

    // 重新初始化派生模块
    bag_runtime_config_init(&s_bag_config, &s_runtime_config);
    press_runtime_config_init(&s_press_config, &s_runtime_config);

    s_derived_dirty = false;
}

// ===== 应用入口 =====
// 每次写入基本参数时标记脏标志
rcfg_status_t rcfg_set_param(uint16_t id, int32_t value) {
    // ... 写入逻辑 ...
    packer_runtime_config_mark_derived_dirty();
    return RCFG_OK;
}

// 每次读取派生参数前检查是否需要刷新
int32_t rcfg_get_bag_param(uint16_t param_id) {
    packer_runtime_config_refresh_derived_if_needed();
    return bag_runtime_config_get(&s_bag_config, param_id);
}

触发 dirty 的场景:任何基本参数(袋长、速度、压杆延时等)通过 rcfg_set_param() 写入时,自动调用 mark_derived_dirty()。下次读取派生参数时触发一次性刷新,避免冗余重复计算。

八、自描述元数据表

框架维护两套自描述元数据表,使 HMI 能够动态渲染参数页面,无需 HMI 侧硬编码参数列表。

// 分组元数据(7 组)
static const rcfg_group_catalog_t s_group_catalog[7] = {
    { .id = 0, .name_zh = "基础参数",   .name_en = "Basic" },
    { .id = 1, .name_zh = "袋长参数",   .name_en = "Bag Length" },
    { .id = 2, .name_zh = "压杆参数",   .name_en = "Press Bar" },
    { .id = 3, .name_zh = "加热参数",   .name_en = "Heating" },
    { .id = 4, .name_zh = "送膜参数",   .name_en = "Film Feed" },
    { .id = 5, .name_zh = "拉链参数",   .name_en = "Zipper" },
    { .id = 6, .name_zh = "系统参数",   .name_en = "System" },
};

// 参数元数据(38 个)
static const rcfg_param_catalog_t s_param_catalog[38] = {
    { .id = 0,  .group = 0, .name_zh = "运行速度",     .name_en = "Speed",
      .exp = 2,  .min = 0,    .max = 5000,  .step = 1,   .def = 1000 },
    { .id = 1,  .group = 0, .name_zh = "加速度",       .name_en = "Accel",
      .exp = 0,  .min = 1,    .max = 100,   .step = 1,   .def = 20 },
    // ... 其余 36 个参数类似 ...
};

元数据字段说明:

  • exp (scaling exponent):十进制幂指数,实际值 = 整数值 / 10^exp。例如 exp=2 时存储值 12345 表示 123.45。
  • min / max:存储值范围。
  • step:HMI 步进调整的增量。
  • def:出厂默认值。

HMI 通过 HMIS 协议请求参数目录,框架返回 s_group_catalogs_param_catalog,HMI 据此动态构建参数编辑页面,无需固件升级即可适配新增参数。

九、派生模块:袋长换算

袋长换算模块将用户设置的袋长(mm)转换为伺服电机所需的脉冲数,同时将运行速度(bags/min)转换为电机频率(Hz)。

核心换算公式

// ===== 编译期常量 =====
#define PULLEY_DRIVEN_TEETH   48   // 从动轮齿数
#define PULLEY_DRIVE_TEETH    24   // 主动轮齿数
#define ROLLER_DIAMETER_MM    40   // 辊筒直径 (mm)
#define PPR                  4096  // 伺服编码器线数

// ===== 脉冲数换算 =====
// 袋长(mm)→ 伺服脉冲数
// pulses = len × ppr × driven_teeth / (roller_diameter × drive_teeth × π)
static int32_t calc_bag_len_pulses(int32_t len_mm) {
    // 使用定点数运算避免浮点
    // pulses = len * PPR * PULLEY_DRIVEN_TEETH * 1000
    //        / (ROLLER_DIAMETER_MM * PULLEY_DRIVE_TEETH * 3141)
    // (π ≈ 3.141,放大 1000 倍为 3141)
    int64_t numerator   = (int64_t)len_mm * PPR * PULLEY_DRIVEN_TEETH * 1000;
    int64_t denominator = (int64_t)ROLLER_DIAMETER_MM * PULLEY_DRIVE_TEETH * 3141;
    return (int32_t)(numerator / denominator);
}

// ===== 速度换算 =====
// 速度(bags/min)→ 电机频率(Hz)
// speed_hz = speed × pulses / 60
static int32_t calc_speed_hz(int32_t speed_bpm, int32_t pulses_per_bag) {
    return (speed_bpm * pulses_per_bag) / 60;
}

袋长模块通过 bag_runtime_config_refresh() 在派生缓存刷新时重新计算脉冲数和频率,供运动控制模块直接使用。

十、派生模块:压杆脉冲换算

压杆模块沿用与袋长相同的换算内核,但使用同步压轮节距(sync_pulley_pd)替代辊筒直径。

// ===== 压杆专用常量 =====
#define SYNC_PULLEY_PD_MM     30   // 同步压轮节距 (mm)
#define PRESS_PULLEY_DRIVEN   24   // 压杆从动轮齿数
#define PRESS_PULLEY_DRIVE    12   // 压杆主动轮齿数

// ===== 压杆脉冲换算 =====
// 与袋长公式相同结构,仅 roller_diameter → sync_pulley_pd
static int32_t press_len_to_pulses(int32_t len_mm) {
    int64_t numerator   = (int64_t)len_mm * PPR * PRESS_PULLEY_DRIVEN * 1000;
    int64_t denominator = (int64_t)SYNC_PULLEY_PD_MM * PRESS_PULLEY_DRIVE * 3141;
    return (int32_t)(numerator / denominator);
}

// ===== 便利封装 =====
int32_t calc_retract_pulses(void) {
    // 压杆回缩脉冲 = 压杆回缩长度 → 脉冲
    int32_t retract_len = bag_runtime_config_get_retract_len();
    return press_len_to_pulses(retract_len);
}

int32_t calc_open_return_pulses(void) {
    // 压杆张开返回脉冲 = 压杆张开长度 → 脉冲
    int32_t open_return_len = bag_runtime_config_get_open_return_len();
    return press_len_to_pulses(open_return_len);
}

便利封装函数屏蔽了内部换算细节,运动控制模块只需调用 calc_retract_pulses()calc_open_return_pulses() 即可获得脉冲目标值。

十一、全模块数据流

下图展示了运行时参数框架的数据流:基本参数(由 HMI/协议写入)经过派生缓存层,在脏标志控制下按需刷新,最终供各执行模块使用。

┌──────────────────────────┐
                    │     HMI / HMIS 协议       │
                    │  (rcfg_set_param)         │
                    └───────────┬──────────────┘
                                │
                                ▼
                    ┌──────────────────────────┐
                    │   packer_runtime_config_t │
                    │   (核心基本参数结构体)      │
                    │   offset/width 描述表驱动  │
                    └───────────┬──────────────┘
                                │ rcfg_set_param()
                                │ mark_derived_dirty()
                                ▼
                    ┌──────────────────────────┐
                    │   脏标志检查门控           │
                    │  is_derived_dirty?        │
                    └───────┬─────────┬────────┘
                            │ YES     │ NO
                            ▼         ▼
                    ┌──────────┐  ┌──────────┐
                    │ 刷新派生  │  │ 直接返回  │
                    │ 缓存      │  │ 缓存值    │
                    └────┬─────┘  └──────────┘
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
    ┌──────────────────┐  ┌──────────────────┐
    │ bag_runtime_config│  │press_runtime_cfg │
    │ - pulses_per_bag │  │ - retract_pulses │
    │ - speed_hz       │  │ - open_pulses    │
    │ - ...            │  │ - ...            │
    └────────┬─────────┘  └────────┬─────────┘
             │                     │
             ▼                     ▼
    ┌─────────────────────────────────────────┐
    │     运动控制 / 执行模块                   │
    │  (伺服驱动、加热控制、送膜、拉链等)        │
    └─────────────────────────────────────────┘

数据流说明

  1. HMI 或通信协议通过 rcfg_set_param() 写入基本参数,写入后自动标记脏标志。
  2. 执行模块读取派生参数时,脏标志门控检查是否需要刷新缓存。
  3. 需要刷新时,bag_runtime_config_refresh()press_runtime_config_refresh() 从核心参数重新计算所有派生值。
  4. 无需刷新时,直接从缓存读取,避免重复计算开销。
此作者没有提供个人介绍。
最后更新于 2026-05-30