
在上一篇博文中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.lsc、g_interp_global_buf.drc、g_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 的致命问题
- 内存翻倍浪费 嵌入式 MCU RAM 资源紧张,struct 会永久占用 2KB + 静态内存;union 只占用最大结构体尺寸,RAM 占用直接减半。
- 无业务收益 业务不会同时读写多个模块参数,struct 同时保留多套参数完全多余;
- 上层调用时传参麻烦 如果是 struct,下发 LSC 要写
&g_buf.lsc没问题,但内存开销完全无意义。
union 的适配优势
- RAM 最小化 只分配所有 ISP 参数中最大那一块内存,静态全局内存占用最低;
- 天然类型封装 不需要手动强转 void*,
.lsc/.drc直接是对应结构体类型,代码可读性强; - 全局单缓存无碎片 仅一块静态全局内存,全程无 malloc/free,规避嵌入式内存碎片、栈溢出。
四、当前 union 写法存在的唯一小问题
问题 1:多模块连续插值时,前一次数据脏残留(逻辑风险,非崩溃 BUG)
示例流程:
- 插值 YUV_GAMMA_LUT(占用 1024 字节大缓存)写入完整 LUT 表;
- 紧接着插值 DPC(仅 64 字节小结构体)写入
.dpc; - 此时缓存内存前 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,例如:
- 批量缓存 LSC+DRC+Gamma 全套参数,一次性批量下发;
- 多线程同时独立读写不同模块参数(多线程并行插值); 当前代码是单线程 AE 轮询、逐个模块插值下发,完全不需要 struct。
六、总结
- 现有 union 写法无致命 BUG,架构适配业务,是嵌入式 RAM 最优方案;
- 禁止替换为 struct,会造成静态 RAM 大量浪费,无任何业务收益;
- union 仅存在 2 个工程规范类隐患,通过简单代码规范 + memset 即可完全规避;
- 核心设计逻辑:复用同一块内存缓冲区,同一时刻仅存储一种 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);