[特殊字符] 全项目架构与代码运转流程(十三)
全项目架构与代码运转流程十三前置知识已完成 06~12 全部代码学习理解每个模块的具体实现。本文定位跳出代码细节从架构高度俯瞰整个项目的设计思想和运转流程。 本文核心目标理解项目的分层架构和模块职责掌握main 函数完整执行流程初始化 主循环理解C 语言模拟面向对象的设计模式画出数据流图和控制流图理解每个模块之间的调用关系一、项目架构总览1.1 三层架构整个项目采用三层架构设计┌──────────────────────────────────────────────────────────────────┐ │ 应用层Application │ │ main.c │ │ 负责编排流程、控制循环 │ ├──────────────────────────────────────────────────────────────────┤ │ 框架层Framework │ │ video_manager.c │ disp_manager.c │ convert_manager.c │ │ 负责注册链表、按名称/格式匹配、统一管理 │ ├──────────────────────────────────────────────────────────────────┤ │ 实现层Implementation │ │ v4l2.c │ fb.c │ yuv2rgb.c │ mjpeg2rgb.c │ rgb2rgb.c │ │ zoom.c │ merge.c │ │ 负责具体硬件操作、具体算法实现 │ └──────────────────────────────────────────────────────────────────┘为什么要分层层级变更影响举例应用层修改流程/逻辑改为拍照模式而不是实时显示框架层修改管理方式从链表改为数组管理实现层替换具体驱动从 V4L2 改为自定义摄像头驱动层与层之间单向依赖应用层 → 框架层 → 实现层实现层不反向依赖。1.2 模块职责矩阵模块目录核心结构体核心函数职责一句话视频采集video/VideoOprVideoDeviceGetFrame/PutFrame从摄像头取一帧数据显示输出display/DispOprShowPage/ShowPixel把像素数据显示到 LCD格式转换convert/VideoConvertisSupport/Convert把一种像素格式转为另一种图像缩放render/无纯函数PicZoom把图像缩放到目标尺寸图像合并render/无纯函数PicMerge把小图贴到大图指定位置二、设计模式C 语言如何实现面向对象2.1 三个步骤步骤一定义抽象接口头文件中的结构体 用函数指针定义能做什么 步骤二实现具体功能.c 文件 填充函数指针写具体逻辑 步骤三注册到链表Init 函数 把实现挂到全局链表供框架查找2.2 三个模块的对比视频显示转换抽象接口VideoOprDispOprVideoConvert实例数据VideoDeviceT_DispOpr本身T_VideoConvert本身注册函数RegisterVideoOpr()RegisterDispOpr()RegisterVideoConvert()查找函数GetVideoOpr()GetDispOpr()GetVideoConvertForFormats()统一初始化VideoInit()DisplayInit()VideoConvertInit()实现文件v4l2.cfb.cyuv2rgb.c/mjpeg2rgb.c/rgb2rgb.c2.3 链表注册机制详解链表初始状态NULL RegisterVideoOpr(opr_v4l2): g_ptVideoOprHead → [opr_v4l2 | NULL] RegisterXXX(opr_xxx): (以后再扩展) g_ptVideoOprHead → [opr_v4l2 | *] → [opr_xxx | NULL] 遍历查找如 VideoDeviceInit: ptTmp g_ptVideoOprHead while (ptTmp ! NULL) { if (ptTmp-InitDevice(devName, dev) 0) return 0; // 找到能用的返回成功 ptTmp ptTmp-ptNext; } return -1; // 都不行返回失败这种设计的好处新增一个驱动只需要写一个.c文件 一个 Init 函数不需要修改任何现有代码开闭原则程序启动时自动注册运行时自动匹配三、main 函数完整执行流程3.1 初始化阶段main() 开始 │ ├─ 1. DisplayInit() 注册显示驱动fb │ └─ FBInit() → RegisterDispOpr(g_tFBOpr) │ ├─ 2. SelectAndInitDefaultDispDev(fb) 选择并初始化 │ └─ g_ptDefaultDispOpr-DeviceInit() │ └─ FBDeviceInit() │ ├─ open(/dev/fb0) │ ├─ ioctl(FBIOGET_VSCREENINFO) 获取屏幕信息 │ ├─ ioctl(FBIOGET_FSCREENINFO) │ ├─ mmap() → pucDispMem 映射显存 │ └─ CleanScreen(0) 清屏为黑色 │ ├─ 3. GetDispResolution(w, h, bpp) 读取屏幕分辨率 │ ├─ 4. GetVideoBufForDisplay(tFrameBuf) 获取帧缓冲 │ └─ tFrameBuf.tPixelDatas.aucPixelDatas pucDispMem │ ↑ 关键直接指向显存零拷贝 │ ├─ 5. VideoInit() 注册摄像头驱动 │ └─ V4l2Init() → RegisterVideoOpr(g_tV4l2VideoOpr) │ ├─ 6. VideoDeviceInit(argv[1], tVideoDevice) 打开并初始化摄像头 │ └─ 遍历视频链表 → V4l2InitDevice() │ ├─ open(argv[1]) 打开 /dev/videoX │ ├─ ioctl(QUERYCAP) 查询摄像头能力 │ ├─ ioctl(ENUM_FMT) 枚举格式选一个支持的 │ ├─ ioctl(S_FMT) 设置格式和分辨率 │ ├─ ioctl(REQBUFS) 申请 4 个缓冲区 │ ├─ ioctl(QUERYBUF) mmap 映射缓冲区 │ └─ ioctl(QBUF) 将缓冲区入队 │ ├─ 7. ptVideoOpr-GetFormat() 获取摄像头格式 │ ├─ 8. VideoConvertInit() 注册三个转换器 │ ├─ Yuv2RgbInit() → RegisterVideoConvert(g_tYuv2RgbConvert) │ ├─ Mjpeg2RgbInit() → RegisterVideoConvert(g_tMjpeg2RgbConvert) │ └─ Rgb2RgbInit() → RegisterVideoConvert(g_tRgb2RgbConvert) │ ├─ 9. GetVideoConvertForFormats(视频格式, 屏幕格式) 匹配转换器 │ └─ 遍历转换器链表 → 返回匹配的 VideoConvert 指针 │ └─ 10. StartDevice() 启动摄像头 └─ ioctl(VIDIOC_STREAMON)3.2 主循环阶段while (1) 主循环开始 │ ├─ (1) GetFrame(tVideoDevice, tVideoBuf) │ └─ V4l2GetFrameForStreaming() │ ├─ poll(fd, POLLIN) 等待摄像头有数据 │ ├─ ioctl(DQBUF) 取出已填好的缓冲区 │ ├─ tVideoBuf.tPixelDatas.aucPixelDatas mmap 地址 │ └─ tVideoBuf.tPixelDatas.iTotalBytes 数据大小 │ ├─ (2) 检查格式是否需要转换 │ if (摄像头格式 LCD 格式) → 跳过直接用原始数据 │ if (摄像头格式 ! LCD 格式) → Convert() 转换 │ ptVideoConvert-Convert(原始帧, 转换后帧) │ 转换后的 ptVideoBufCur tConvertBuf │ ├─ (3) 检查画面是否需要缩放 │ if (画面宽 屏幕宽 || 画面高 屏幕高) │ ├─ 计算等比例缩放后的宽高 │ ├─ PicZoom(当前画面, 缩放后画面) │ └─ ptVideoBufCur tZoomBuf │ ├─ (4) 计算居中位置 │ iTopLeftX (屏幕宽 - 画面宽) / 2 │ iTopLeftY (屏幕高 - 画面高) / 2 │ ├─ (5) PicMerge(偏移X, 偏移Y, 当前画面, tFrameBuf) │ └─ 将画面逐行复制到 tFrameBuf即显存 │ ├─ (6) FlushPixelDatasToDev(tFrameBuf.tPixelDatas) │ └─ FBShowPage() │ └─ 此处 tFrameBuf 已在显存memcpy 被跳过 │ └─ (7) PutFrame(tVideoDevice, tVideoBuf) ← 必须否则缓冲区耗尽 └─ ioctl(QBUF) 归还缓冲区供驱动继续使用3.3 初始化 vs 循环 对比阶段调用次数特点初始化1 次打开设备、分配资源、注册驱动主循环无限次取帧→处理→显示→还帧30 帧/秒四、数据流与控制流4.1 完整数据流向图┌──────────┐ 原始数据(YUYV/MJPEG/RGB565) ┌──────────┐ │ USB │ ─────────────────────────────► │ V4L2 │ │ 摄像头 │ 通过 USB 传输到内核驱动 │ 缓冲区 │ └──────────┘ │ (mmap) │ └────┬─────┘ │ GetFrame() ▼ ┌────────────────┐ │ tVideoBuf │ │ aucPixelDatas │ │ mmap 地址 │ └───────┬────────┘ │ ┌───────────┴───────────┐ │ 格式相同? │ │ iPixelFormatOfVideo │ │ iPixelFormatOfDisp│ └───────┬───────┬───────┘ 是 │ │ 否 ▼ │ ▼ ┌─────────┐ ┌──────────────────┐ │ 不转换 │ │ Convert() │ │ 直接用 │ │ YUV→RGB │ │ 原始数据 │ │ MJPEG→RGB │ └────┬─────┘ │ RGB→RGB │ │ └───────┬──────────┘ │ │ └──────┬──────────┘ │ ▼ ┌────────────────┐ │ ptVideoBufCur │ │ (当前画面) │ └───────┬────────┘ │ ┌─────────┴─────────┐ │ 画面比屏幕大? │ │ widthlcdWidth │ │ heightlcdHeight │ └──────┬──────┬─────┘ 否 │ │ 是 ▼ │ ▼ ┌─────────┐ ┌──────────────────┐ │ 不缩放 │ │ PicZoom() │ │ 直接使用 │ │ 等比例缩小 │ └────┬─────┘ └───────┬──────────┘ │ │ └──────┬──────────┘ │ ▼ ┌────────────────┐ │ PicMerge() │ │ 居中合并到 │ │ tFrameBuf │ │ (直接写显存) │ └───────┬────────┘ │ ▼ ┌────────────────┐ │ FlushPixel │ │ DatasToDev() │ │ (刷新屏幕) │ └───────┬────────┘ │ ▼ ┌────────────────┐ │ PutFrame() │ │ QBUF 归还 │ │ 继续循环 │ └────────────────┘4.2 控制流函数调用链以一次主循环为例main() ├─ tVideoDevice.ptOPr-GetFrame() │ └─ v4l2.c: V4l2GetFrameForStreaming() │ ├─ poll() │ └─ ioctl(VIDIOC_DQBUF) │ ├─ ptVideoConvert-Convert() │ ├─ yuv2rgb.c: Yuv2RgbConvert() │ │ ├─ malloc() 首次分配输出缓冲 │ │ ├─ Pyuv422torgb565() 或 Pyuv422torgb32() │ │ └─ color.c: 查表法 YUV→RGB │ │ │ ├─ 或 mjpeg2rgb.c: Mjpeg2RgbConvert() │ │ ├─ jpeg_create_decompress() │ │ ├─ jpeg_mem_src_tj() │ │ ├─ jpeg_read_header() │ │ ├─ jpeg_start_decompress() │ │ ├─ jpeg_read_scanlines() 循环 │ │ ├─ CovertOneLine() RGB24→RGB565/32 │ │ └─ jpeg_finish_decompress() │ │ │ └─ 或 rgb2rgb.c: Rgb2RgbConvert() │ └─ RGB565→RGB32 位扩展循环 │ ├─ PicZoom() render/zoom.c │ └─ 最近邻插值算法 │ ├─ PicMerge() render/merge.c │ └─ 逐行 memcpy │ ├─ FlushPixelDatasToDev() │ └─ disp_manager.c → fb.c: FBShowPage() │ └─ tVideoDevice.ptOPr-PutFrame() └─ v4l2.c: V4l2PutFrameForStreaming() └─ ioctl(VIDIOC_QBUF)五、核心数据结构关系5.1 主要结构体依赖图T_PixelDatas最基础的数据结构 ↑ 被所有模块引用 │ ├── T_VideoBuf视频帧 │ └── 被 VideoOpr.GetFrame 返回 │ 被 VideoConvert.Convert 转换 │ ├── T_VideoDevice摄像头设备实例 │ ├── 包含 iFd, iWidth, iHeight, buffers │ └── 包含 ptOPr → T_VideoOpr操作函数 │ ├── T_DispOpr显示设备操作 │ ├── 包含 pucDispMem显存地址 │ └── 包含 ShowPage, ShowPixel 等函数 │ └── T_VideoConvert格式转换器 ├── 包含 isSupport → 判断是否支持 └── 包含 Convert → 执行转换5.2 关键设计决策为什么 VideoDevice 和 VideoOpr 要分开structVideoDevice{// 实例数据每个设备不同intiFd;// 不同的设备有不同的 fdintiWidth;// 不同的分辨率unsignedchar*pucVideBuf[4];// 不同的缓冲区地址PT_VideoOpr ptOPr;// 共享同一套操作函数};structVideoOpr{// 操作方法同类设备共享char*name;int(*InitDevice)(...);// 函数指针int(*GetFrame)(...);// ...};这样设计的原因如果你插了两个同样的 USB 摄像头它们共享同一套VideoOpr函数代码只有一份但它们各自有自己的VideoDevice不同的 fd、缓冲区节省内存逻辑清晰六、各模块初始化顺序模块初始化有严格的顺序依赖画成时间线时间 → │ ├─ ① DisplayInit() │ 必须先初始化显示因为后面摄像头设置格式时需要读取屏幕分辨率 │ ├─ ② SelectAndInitDefaultDispDev(fb) │ 初始化 fb获取屏幕宽高和 bpp │ ├─ ③ VideoInit() │ 注册 v4l2 驱动只是注册到链表还没打开摄像头 │ ├─ ④ VideoDeviceInit(/dev/videoX) │ 真正打开摄像头设置格式分辨率设为屏幕大小 │ ├─ ⑤ VideoConvertInit() │ 注册三个转换器只是注册还没使用 │ ├─ ⑥ GetVideoConvertForFormats() │ 根据摄像头格式和屏幕格式匹配转换器 │ └─ ⑦ StartDevice() 启动摄像头流为什么显示必须最先初始化因为第④步V4l2InitDevice()中设置了摄像头分辨率为 LCD 分辨率// v4l2.c 中设置摄像头格式GetDispResolution(iLcdWidth,iLcdHeigt,iLcdBpp);tV4l2Fmt.fmt.pix.widthiLcdWidth;// 让摄像头输出和屏幕一样大tV4l2Fmt.fmt.pix.heightiLcdHeigt;如果显示没初始化GetDispResolution()返回 0摄像头分辨率会设错。七、数据格式的匹配逻辑7.1 main.c 中的格式判断树摄像头格式: iPixelFormatOfVideo (如 V4L2_PIX_FMT_YUYV) LCD 格式: iPixelFormatOfDisp (如 V4L2_PIX_FMT_RGB565) 判断 ① 格式相同吗 是 → 不转换直接使用摄像头原始数据 否 → 进入转换逻辑 ② GetVideoConvertForFormats(摄像头格式, LCD格式) 遍历转换器链表 yuv2rgb: isSupport(YUYV, RGB565) → ✅ 匹配 mjpeg2rgb: isSupport(YUYV, RGB565) → ❌ 继续 rgb2rgb: isSupport(YUYV, RGB565) → ❌ 继续 返回 yuv2rgb 转换器 ③ 调用 ptVideoConvert-Convert()常见的匹配组合摄像头格式LCD 格式匹配的转换器YUYVRGB565/RGB32yuv2rgbMJPEGRGB565/RGB32mjpeg2rgbRGB565RGB32rgb2rgbRGB565RGB565不转换格式相同7.2 不支持的格式组合会怎样如果摄像头输出NV12格式也在 V4L2 中常见而我们的程序不支持GetVideoConvertForFormats(V4L2_PIX_FMT_NV12,RGB565)// 遍历三个转换器isSupport 全部返回 0// 返回 NULL在 main.c 中ptVideoConvertGetVideoConvertForFormats(...);if(NULLptVideoConvert){DBG_PRINTF(can not support this format convert\n);return-1;// 程序退出}解决方案新增一个nv122rgb.c实现 NV12→RGB 的转换注册到链表中。八、模块扩展指南如果未来要增加新功能按以下步骤操作8.1 新增一种摄像头驱动1. 新建文件driver/uvc.c 2. 实现 VideoOpr 接口 InitDevice, GetFrame, PutFrame, ... 3. 定义全局结构体static T_VideoOpr g_tUvcOpr {...} 4. 实现注册函数int UvcInit(void) { return RegisterVideoOpr(g_tUvcOpr); } 5. 在 VideoInit() 中调用 UvcInit()8.2 新增一种格式转换1. 新建文件convert/nv122rgb.c 2. 实现 VideoConvert 接口isSupport, Convert, ConvertExit 3. 定义全局结构体static T_VideoConvert g_tNv122RgbConvert {...} 4. 实现注册函数int Nv122RgbInit(void) { return RegisterVideoConvert(...); } 5. 在 VideoConvertInit() 中追加调用 Nv122RgbInit()8.3 新增图像特效1. 在 render/ 下新建文件effect.c 2. 实现函数int PicGrayscale(PT_PixelDatas ptPic) 3. 在 main.c 主循环中PicMerge 之前调用九、Makefile 构建流程9.1 递归编译过程make └─ make -f Makefile.build │ ├─ 进入 video/ 目录 │ └─ 编译 v4l2.c → v4l2.o │ └─ 编译 video_manager.c → video_manager.o │ └─ ld -r → video/built-in.o │ ├─ 进入 display/ 目录 │ └─ 编译 fb.c → fb.o │ └─ 编译 disp_manager.c → disp_manager.o │ └─ ld -r → display/built-in.o │ ├─ 进入 convert/ 目录 │ └─ 编译 ... → convert/built-in.o │ ├─ 进入 render/operation/ 目录 │ └─ 编译 zoom.c, merge.c → render/operation/built-in.o │ └─ 返回 render/ → render/built-in.o │ ├─ 编译 main.c → main.o │ └─ ld -r → built-in.o包含所有子模块的 built-in.o │ └─ arm-linux-gcc -o video2lcd built-in.o -lm -ljpeg9.2 Makefile.build 核心逻辑# 1. 从 Makefile 读取 obj-y哪些 .o 和子目录 # 2. 分离出 当前目录的 .o 和 子目录/ # 3. 递归进入子目录编译生成子目录的 built-in.o # 4. 用 ld -r 把所有 .o 合并为当前目录的 built-in.o # 5. 顶层用 gcc 链接 built-in.o 和库文件生成最终可执行文件十、项目总览图┌──────────────────────────────────────────────────────────────────────┐ │ video2lcd 项目全景 │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ main.c 主循环 │ │ │ │ 初始化 → [取帧 → 转换 → 缩放 → 合并 → 显示 → 还帧]∞ │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ ┌─────────┴──┐ ┌──────┴──────┐ └──────┬──────────┐ │ │ │ video/ │ │ convert/ │ │ render/ │ │ │ │ 摄像头采集 │ │ 格式转换 │ │ 渲染处理 │ │ │ │ │ │ │ │ │ │ │ │ v4l2.c │ │ yuv2rgb.c │ │ zoom.c │ │ │ │ ┌──────┐ │ │ mjpeg2rgb.c│ │ merge.c │ │ │ │ │DQBUF │ │ │ rgb2rgb.c │ │ │ │ │ │ │QBUF │ │ │ color.c │ │ │ │ │ │ │mmap │ │ │ jdatasrc │ │ │ │ │ │ └──────┘ │ └────────────┘ └───────────┘ │ │ └───────────┘ │ │ │ │ │ └──────────────────┬─────────────────┐ │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ display/ │ │ include/ │ │ │ │ LCD 显示 │ │ 接口定义 │ │ │ │ │ │ │ │ │ │ fb.c │ │ config.h │ │ │ │ ┌────────┐ │ │ video_man.h │ │ │ │ │mmap │ │ │ disp_man.h │ │ │ │ │memcpy │ │ │ convert_man.h│ │ │ │ └────────┘ │ │ pic_op.h │ │ │ │ │ │ render.h │ │ │ └─────────────┘ └──────────────┘ │ └──────────────────────────────────────────────────────────────────┘ 总结一张图记住整个项目摄像头 → 缓冲区 → 转格式 → 缩放 → 合并 → 显存 → LCD open DQBUF isSupport PicZoom PicMerge mmap 显示 S_FMT QBUF Convert FB mmap poll一句话总结这个项目就是一个V4L2 采集 Framebuffer 显示的管道中间用模块化的转换器、缩放器做数据处理所有模块通过链表注册的方式解耦。