ISP模块参数统一对外接口插值逻辑(三)

在上一篇博文中ISP模块参数统一对外接口插值逻辑(一)重点讲解了整个ISP Pipeline 中使用统一接口进行插值的原理。这篇博文主要讲解其中的一些细节。

在 如下代码段中,这样设置是否有问题 为什么不用struct?

cpp 复制代码
typedef union {
    AK_ISP_LSC_ATTR        lsc;
    AK_ISP_DRC_REG         drc;
    AK_ISP_RGB_GAMMA       rgb_gamma;
    AK_ISP_YUV_GAMMA_LUT   yuv_gamma_lut;
    AK_ISP_DPC_ATTR        dpc;
} ISP_INTERP_UNION_BUF_T;
extern ISP_INTERP_UNION_BUF_T g_interp_global_buf;

一、联合体 union VS 结构体 struct 核心差异(内存布局)

1. union(共用体)内存规则

联合体所有成员共享同一块内存起始地址 ,分配的总内存 = 联合体内部最大成员的字节大小

cpp 复制代码
typedef union {
    ISP_LSC_ATTR        lsc;    // 假设 800Byte
    ISP_DRC_REG         drc;    // 假设 120Byte
    ISP_RGB_GAMMA       rgb_gamma;// 300Byte
    ISP_YUV_GAMMA_LUT   yuv_gamma_lut;// 1024Byte 最大
    ISP_DPC_ATTR        dpc;    // 64Byte
} ISP_INTERP_UNION_BUF_T;

内存占用 = sizeof(ISP_YUV_GAMMA_LUT) = 1024Byte

  • g_interp_global_buf.lscg_interp_global_buf.drcg_interp_global_buf.yuv_gamma_lut 全部从地址 &g_interp_global_buf 开始;
  • 同一时间只能安全使用其中一个成员,写入 lsc 后立刻读 drc 会读到脏数据。

2. struct(结构体)内存规则

结构体成员按顺序连续排布,内存叠加,总内存 = 所有成员大小之和 + 对齐填充。 如果改成 struct:

cpp 复制代码
typedef struct {
    ISP_LSC_ATTR        lsc;
    ISP_DRC_REG         drc;
    ISP_RGB_GAMMA       rgb_gamma;
    ISP_YUV_GAMMA_LUT   yuv_gamma_lut;
    ISP_DPC_ATTR        dpc;
} ISP_INTERP_STRUCT_BUF_T;

总内存 = 800+120+300+1024+64 ≈ 2308Byte,是 union 的 2 倍多。

三、为什么这里必须用 union,不能用 struct?

场景需求:全局单块插值缓存,同一时刻只需要一种模块参数

业务逻辑:AE 线程每次只插值单个 ISP 模块,不会同时生成 LSC+DRC+Gamma 三套参数:

cpp 复制代码
// 第一步:插值LSC,只用 .lsc 成员
isp_get_unified_interp_param(..., &g_interp_global_buf.lsc, sizeof(g_interp_global_buf.lsc));
libisp_set(..., &g_interp_global_buf.lsc);

// 第二步:插值DRC,覆盖同一块内存,只用 .drc 成员
isp_get_unified_interp_param(..., &g_interp_global_buf.drc, sizeof(g_interp_global_buf.drc));
libisp_set(..., &g_interp_global_buf.drc);

用 struct 的致命问题

  1. 内存翻倍浪费 嵌入式 MCU RAM 资源紧张,struct 会永久占用 2KB + 静态内存;union 只占用最大结构体尺寸,RAM 占用直接减半。
  2. 无业务收益 业务不会同时读写多个模块参数,struct 同时保留多套参数完全多余;
  3. 上层调用时传参麻烦 如果是 struct,下发 LSC 要写 &g_buf.lsc 没问题,但内存开销完全无意义。

union 的适配优势

  1. RAM 最小化 只分配所有 ISP 参数中最大那一块内存,静态全局内存占用最低;
  2. 天然类型封装 不需要手动强转 void*,.lsc/.drc 直接是对应结构体类型,代码可读性强;
  3. 全局单缓存无碎片 仅一块静态全局内存,全程无 malloc/free,规避嵌入式内存碎片、栈溢出。

四、当前 union 写法存在的唯一小问题

问题 1:多模块连续插值时,前一次数据脏残留(逻辑风险,非崩溃 BUG)

示例流程:

  1. 插值 YUV_GAMMA_LUT(占用 1024 字节大缓存)写入完整 LUT 表;
  2. 紧接着插值 DPC(仅 64 字节小结构体)写入.dpc
  3. 此时缓存内存前 64 字节是合法 DPC 数据,但 64~1024 字节还残留上一轮 Gamma LUT 脏数据; 虽然业务不会读取脏区域,但如果代码失误越界读取会出现异常。

规避方案:每次插值前手动 memset 清空当前使用的成员内存:

cpp 复制代码
// 插值DPC前清空DPC结构体内存
memset(&g_interp_global_buf.dpc, 0, sizeof(g_interp_global_buf.dpc));
isp_get_unified_interp_param(LIB_ALGSET_DPC_ATTR, cur_gain, &g_interp_global_buf.dpc, sizeof(g_interp_global_buf.dpc));

问题 2:编译期无互斥使用检查(C 语言原生限制)

C 编译器不会限制你同时读写.lsc.drc,如果写出如下错误代码,会直接参数错乱:

复制代码
// 错误示例:同时使用两个union成员,内存互相覆盖
isp_get_unified_interp_param(LIB_ALGSET_LSC_ATTR, gain, &g_interp_global_buf.lsc, ...);
isp_get_unified_interp_param(LIB_ALGSET_DRC_ATTR, gain, &g_interp_global_buf.drc, ...);
libisp_set(..., &g_interp_global_buf.lsc); // lsc内存已经被drc覆盖,参数损坏

规避方案:业务代码规范约束 ------ 单次插值 - 下发流程只操作一个 union 成员,下发完成后再进行下一个模块插值。

问题 3:sizeof 传参依赖开发者手动匹配

调用插值接口时需要传入当前成员的sizeof(xxx),如果传错尺寸会截断 / 越界拷贝:

cpp 复制代码
// 风险:把drc的size写成lsc的size,拷贝越界脏数据
isp_get_unified_interp_param(..., &g_interp_global_buf.drc, sizeof(g_interp_global_buf.lsc));

优化方案:封装宏简化调用,强制绑定 cmd 与联合体成员尺寸,杜绝传参错误。

五、补充:什么场景才需要用 struct?

只有业务需要同时保存多套 ISP 参数时才用 struct,例如:

  1. 批量缓存 LSC+DRC+Gamma 全套参数,一次性批量下发;
  2. 多线程同时独立读写不同模块参数(多线程并行插值); 当前代码是单线程 AE 轮询、逐个模块插值下发,完全不需要 struct。

六、总结

  1. 现有 union 写法无致命 BUG,架构适配业务,是嵌入式 RAM 最优方案
  2. 禁止替换为 struct,会造成静态 RAM 大量浪费,无任何业务收益;
  3. union 仅存在 2 个工程规范类隐患,通过简单代码规范 + memset 即可完全规避;
  4. 核心设计逻辑:复用同一块内存缓冲区,同一时刻仅存储一种 ISP 模块插值参数,最大化节约片内 RAM。

拓展优化建议(消除 union 短板)

封装一层宏,统一调用,避免手动写 sizeof、重复 memset:

cpp 复制代码
#define ISP_INTERP_RUN(cmd, gain, member) do{ \
    memset(&g_interp_global_buf.member, 0, sizeof(g_interp_global_buf.member)); \
    isp_get_unified_interp_param(cmd, gain, &g_interp_global_buf.member, sizeof(g_interp_global_buf.member)); \
    libisp_set(g_isp_handle, cmd, &g_interp_global_buf.member); \
}while(0)

// 上层调用极简,不会传错尺寸、自动清缓存
ISP_INTERP_RUN(LIB_ALGSET_LSC_ATTR, cur_total_gain, lsc);
ISP_INTERP_RUN(LIB_ALGSET_DRC_ATTR, cur_total_gain, drc);