:结构体抽象与管理)
转换模块(十)结构体抽象与管理前置必须理解 链表注册和V4L2 数据采集本文档将彻底搞懂「转换模块」的框架设计即使零基础也能看懂文章目录 转换模块(十)结构体抽象与管理 本节核心目标 本节涉及的文件 核心结构体对比转换模块 vs 摄像头模块摄像头操作结构体08.1转换操作结构体09.1相似点至少 4 处 注册流程图一步一步画 每个函数的详细解释含代码1. 结构体定义convert_manager.h2. 注册函数convert_manager.c3. 自动查找函数convert_manager.c4. 初始化所有转换模块convert_manager.c 具体模块实现示例yuv2rgb.c 骨架❗ 你曾经犯过的错误及纠正错误1认为 ConvertExit 是“停止转换”或自动调用错误2增加新转换时去修改 g_aiSupportedFormats 数组✅ 自测题一问一答Q1: 转换模块中 isSupport 函数的作用是什么为什么需要两个参数Q2: GetVideoConvertForFormats 和 GetVideoOpr 的查找方式有何不同Q3: 为什么 Convert 函数中有时需要动态分配内存而 ConvertExit 负责释放Q4: 如果我想支持一种新的输入格式 V4L2_PIX_FMT_NV12输出 RGB565需要修改哪些文件Q5: 画出注册三个转换模块后的链表结构图用文字描述或用 ASCII 画出来。 总结 本节核心目标用“面向对象”的思想抽象出所有格式转换操作的通用接口实现多种转换算法YUV→RGB、MJPEG→RGB、RGB→RGB的统一管理按需自动匹配转换器输入格式 输出格式 → 找到对应模块与摄像头模块完全解耦新增转换无需修改主程序 本节涉及的文件文件作用convert/convert_manager.h定义转换器结构体T_VideoConvert和全局函数convert/convert_manager.c链表管理、注册、查找、初始化convert/yuv2rgb.cYUV → RGB 转换的具体实现注册、转换函数convert/mjpeg2rgb.cMJPEG → RGB 转换的具体实现convert/rgb2rgb.cRGB565 → RGB32 等转换的具体实现学习顺序先看.h理解框架再看convert_manager.c理解注册和查找最后看任意一个*2rgb.c理解如何挂接。 核心结构体对比转换模块 vs 摄像头模块为了让你一眼看透设计模式我直接把VideoOpr摄像头操作和VideoConvert转换操作的结构体并排列出摄像头操作结构体08.1cstruct VideoOpr { char *name; int (*InitDevice)(char *strDevName, PT_VideoDevice ptVideoDevice); int (*ExitDevice)(PT_VideoDevice ptVideoDevice); int (*GetFrame)(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf); int (*GetFormat)(PT_VideoDevice ptVideoDevice); int (*PutFrame)(PT_VideoDevice ptVideoDevice, PT_VideoBuf ptVideoBuf); int (*StartDevice)(PT_VideoDevice ptVideoDevice); int (*StopDevice)(PT_VideoDevice ptVideoDevice); struct VideoOpr *ptNext; };转换操作结构体09.1ctypedef struct VideoConvert { char *name; int (*isSupport)(int iPixelFormatIn, int iPixelFormatOut); int (*Convert)(PT_VideoBuf ptVideoBufIn, PT_VideoBuf ptVideoBufOut); int (*ConvertExit)(PT_VideoBuf ptVideoBufOut); struct VideoConvert *ptNext; } T_VideoConvert;相似点至少 4 处特征摄像头模块转换模块名字标识char *namechar *name链表指针ptNextptNext注册函数RegisterVideoOprRegisterVideoConvert全局链表头static PT_VideoOpr g_ptVideoOprHeadstatic PT_VideoConvert g_ptVideoConvertHead查找方式按名字查找GetVideoOpr按格式能力查找GetVideoConvertForFormats批量初始化VideoInit→V4l2InitVideoConvertInit→Yuv2RgbInit/Mjpeg2RgbInit/Rgb2RgbInit设计模式总结两个模块都使用了「注册链表 函数指针」的策略模式主程序只需依赖抽象接口具体实现可以动态替换或扩展。 注册流程图一步一步画注册一个转换模块例如yuv2rgb的完整过程text开始 │ ▼ 调用 VideoConvertInit() │ ├── 调用 Yuv2RgbInit() │ │ │ ▼ │ 定义静态结构体 g_tYuv2RgbConvert { │ .name yuv2rgb, │ .isSupport isSupportYuv2Rgb, │ .Convert Yuv2RgbConvert, │ .ConvertExit Yuv2RgbConvertExit, │ .ptNext NULL │ } │ │ │ ▼ │ 调用 RegisterVideoConvert(g_tYuv2RgbConvert) │ │ │ ▼ │ 链表操作 │ if (头指针 NULL) │ 头指针 新节点 │ else │ 找到末尾节点末尾-next 新节点 │ 新节点-next NULL │ ├── 调用 Mjpeg2RgbInit() → 同样注册 g_tMjpeg2RgbConvert │ └── 调用 Rgb2RgbInit() → 同样注册 g_tRgb2RgbConvert 结束此时全局链表中有三个节点链表最终结构textg_ptVideoConvertHead │ ▼ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │ yuv2rgb │ │ mjpeg2rgb │ │ rgb2rgb │ │ isSupport │──▶│ isSupport │──▶│ isSupport │──▶ NULL │ Convert │ │ Convert │ │ Convert │ │ ConvertExit │ │ ConvertExit │ │ ConvertExit │ └──────────────┘ └────────────────┘ └─────────────┘ 每个函数的详细解释含代码1. 结构体定义convert_manager.hctypedef struct VideoConvert { char *name; // 模块名字如 yuv2rgb int (*isSupport)(int iPixelFormatIn, int iPixelFormatOut); int (*Convert)(PT_VideoBuf ptVideoBufIn, PT_VideoBuf ptVideoBufOut); int (*ConvertExit)(PT_VideoBuf ptVideoBufOut); struct VideoConvert *ptNext; } T_VideoConvert, *PT_VideoConvert;name调试用ShowVideoConvert会打印所有模块的名字。isSupport判断本模块是否能处理输入格式 → 输出格式这一对。例如yuv2rgb只支持YUYV → RGB565和YUYV → RGB32。Convert执行真正的转换。输入是ptVideoBufIn原始帧数据如 YUYV输出是ptVideoBufOut转换后的 RGB 数据。输出缓冲区的内存可能由Convert内部malloc也可能由调用者预先分配看具体实现。ConvertExit清理Convert分配的资源比如释放ptVideoBufOut-aucPixelDatas。不是自动调用的需要上层在不再需要转换器时主动调用。ptNext链表指针用来串联多个转换模块。2. 注册函数convert_manager.ccint RegisterVideoConvert(PT_VideoConvert ptVideoConvert) { PT_VideoConvert ptTmp; if (!g_ptVideoConvertHead) { g_ptVideoConvertHead ptVideoConvert; ptVideoConvert-ptNext NULL; } else { ptTmp g_ptVideoConvertHead; while (ptTmp-ptNext) ptTmp ptTmp-ptNext; ptTmp-ptNext ptVideoConvert; ptVideoConvert-ptNext NULL; } return 0; }与摄像头模块完全相同的尾插法不再赘述。3. 自动查找函数convert_manager.ccPT_VideoConvert GetVideoConvertForFormats(int iPixelFormatIn, int iPixelFormatOut) { PT_VideoConvert ptTmp g_ptVideoConvertHead; while (ptTmp) { if (ptTmp-isSupport(iPixelFormatIn, iPixelFormatOut)) return ptTmp; ptTmp ptTmp-ptNext; } return NULL; }作用给定输入格式和输出格式遍历链表返回第一个支持的转换模块。为什么不用名字查找因为主程序main只知道摄像头输出的格式和显示屏需要的格式不知道具体该用yuv2rgb还是mjpeg2rgb还是rgb2rgb。名字查找无法适应这种情况。4. 初始化所有转换模块convert_manager.ccint VideoConvertInit(void) { int iError; iError Yuv2RgbInit(); iError | Mjpeg2RgbInit(); iError | Rgb2RgbInit(); return iError; }每个*Init函数内部都会调用RegisterVideoConvert把自己注册到全局链表。使用|只要有一个失败最终返回值非 0表示至少有一个模块注册失败不过一般情况下都会成功。 具体模块实现示例yuv2rgb.c骨架c#include convert_manager.h /* 判断是否支持输入必须是 YUYV输出必须是 RGB565 或 RGB32 */ static int isSupportYuv2Rgb(int iPixelFormatIn, int iPixelFormatOut) { if (iPixelFormatIn ! V4L2_PIX_FMT_YUYV) return 0; if (iPixelFormatOut V4L2_PIX_FMT_RGB565 || iPixelFormatOut V4L2_PIX_FMT_RGB32) return 1; return 0; } /* 实际转换函数算法细节在 09.2 */ static int Yuv2RgbConvert(PT_VideoBuf ptVideoBufIn, PT_VideoBuf ptVideoBufOut) { // 这里调用 color.c 中的查表函数逐像素转换 return 0; } /* 清理函数 */ static int Yuv2RgbConvertExit(PT_VideoBuf ptVideoBufOut) { if (ptVideoBufOut-tPixelDatas.aucPixelDatas) { free(ptVideoBufOut-tPixelDatas.aucPixelDatas); ptVideoBufOut-tPixelDatas.aucPixelDatas NULL; } return 0; } /* 定义模块 */ static T_VideoConvert g_tYuv2RgbConvert { .name yuv2rgb, .isSupport isSupportYuv2Rgb, .Convert Yuv2RgbConvert, .ConvertExit Yuv2RgbConvertExit, }; /* 注册模块 */ int Yuv2RgbInit(void) { return RegisterVideoConvert(g_tYuv2RgbConvert); }mjpeg2rgb.c和rgb2rgb.c结构完全一致只是isSupport的条件和Convert的实现不同。❗ 你曾经犯过的错误及纠正错误1认为ConvertExit是“停止转换”或自动调用❌ 错误理解ConvertExit在转换完成后自动调用。✅ 正确理解ConvertExit用于释放Convert中动态分配的资源如输出缓冲区必须由上层代码主动调用。在当前main.c中程序无限循环没有机会调用ConvertExit但框架预留了此接口以便将来需要重新初始化转换器时使用。错误2增加新转换时去修改g_aiSupportedFormats数组❌ 错误操作在摄像头模块的g_aiSupportedFormats中添加 NV12。✅ 正确做法创建nv12torgb.c实现isSupportNV12ToRgb检查输入V4L2_PIX_FMT_NV12输出RGB565或RGB32实现NV12ToRgbConvert和NV12ToRgbConvertExit定义静态结构体g_tNV12ToRgbConvert实现int NV12ToRgbInit(void) { return RegisterVideoConvert(g_tNV12ToRgbConvert); }在VideoConvertInit中添加iError | NV12ToRgbInit();不需要修改convert_manager.c的任何其他代码这就是框架的扩展性。✅ 自测题一问一答Q1: 转换模块中isSupport函数的作用是什么为什么需要两个参数A1:isSupport判断本模块能否将一种像素格式转换成另一种像素格式。两个参数分别是输入格式摄像头输出的原始格式和输出格式显示设备需要的格式。只给一个参数无法判断转换关系。Q2:GetVideoConvertForFormats和GetVideoOpr的查找方式有何不同A2:GetVideoOpr按模块名字精确查找例如v4l2。GetVideoConvertForFormats遍历链表依次调用每个模块的isSupport找到第一个支持(输入格式, 输出格式)的模块。主程序不知道转换模块的具体名字所以必须用后者。Q3: 为什么Convert函数中有时需要动态分配内存而ConvertExit负责释放A3:某些转换如 MJPEG→RGB解码后的 RGB 数据大小是固定的宽×高×3但输入缓冲区MJPEG 压缩数据的大小不一定。Convert需要在输出结构体ptVideoBufOut中malloc一块内存来存放 RGB 数据。当应用程序不再需要这个转换器时必须调用ConvertExit来free这块内存否则会造成内存泄漏。Q4: 如果我想支持一种新的输入格式V4L2_PIX_FMT_NV12输出RGB565需要修改哪些文件A4:新建nv12torgb.c实现isSupportNV12ToRgb、NV12ToRgbConvert、NV12ToRgbConvertExit。定义静态T_VideoConvert g_tNV12ToRgbConvert。实现int NV12ToRgbInit(void)并调用RegisterVideoConvert。在VideoConvertInit中添加一行iError | NV12ToRgbInit();。不需要修改convert_manager.c的链表管理逻辑。Q5: 画出注册三个转换模块后的链表结构图用文字描述或用 ASCII 画出来。A5:textg_ptVideoConvertHead │ ▼ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │ yuv2rgb │ │ mjpeg2rgb │ │ rgb2rgb │ │ isSupport │──▶│ isSupport │──▶│ isSupport │──▶ NULL │ Convert │ │ Convert │ │ Convert │ │ ConvertExit │ │ ConvertExit │ │ ConvertExit │ └──────────────┘ └────────────────┘ └─────────────┘ 总结09.1 课程是转换模块的框架设计与摄像头模块的架构如出一辙。你只需要记住链表注册 函数指针是这类驱动框架的标配。isSupport是实现自动匹配的关键。增加新转换只需新增一个*2rgb.c文件在VideoConvertInit中调用其Init完全符合开闭原则。掌握了这个框架接下来的 09.2YUV 转 RGB、09.3MJPEG 转 RGB、09.4RGB 转 RGB都只是填充不同的Convert函数而已。 本文档将作为你专栏的基石反复阅读直到闭眼能画出链表结构。遇到问题随时回来对照自测题。