当前位置: 首页 > news >正文

[特殊字符] 全项目架构与代码运转流程(十三)

📘 全项目架构与代码运转流程(十三)

前置知识:已完成 06~12 全部代码学习,理解每个模块的具体实现。
本文定位:跳出代码细节,从架构高度俯瞰整个项目的设计思想和运转流程。


🎯 本文核心目标

  1. 理解项目的分层架构模块职责
  2. 掌握main 函数完整执行流程(初始化 + 主循环)
  3. 理解C 语言模拟面向对象的设计模式
  4. 画出数据流图控制流图
  5. 理解每个模块之间的调用关系

一、项目架构总览

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/VideoOpr+VideoDeviceGetFrame/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.c

2.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 │ │ (当前画面) │ └───────┬────────┘ │ ┌─────────┴─────────┐ │ 画面比屏幕大? │ │ width>lcdWidth │ │ height>lcdHeight │ └──────┬──────┬─────┘ 否 │ │ 是 ▼ │ ▼ ┌─────────┐ ┌──────────────────┐ │ 不缩放 │ │ 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.width=iLcdWidth;// 让摄像头输出和屏幕一样大tV4l2Fmt.fmt.pix.height=iLcdHeigt;

如果显示没初始化,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/RGB32yuv2rgb
MJPEGRGB565/RGB32mjpeg2rgb
RGB565RGB32rgb2rgb
RGB565RGB565不转换(格式相同)

7.2 不支持的格式组合会怎样?

如果摄像头输出NV12格式(也在 V4L2 中常见),而我们的程序不支持:

GetVideoConvertForFormats(V4L2_PIX_FMT_NV12,RGB565)// 遍历三个转换器,isSupport 全部返回 0// 返回 NULL

在 main.c 中:

ptVideoConvert=GetVideoConvertForFormats(...);if(NULL==ptVideoConvert){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 -ljpeg

9.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 显示的管道,中间用模块化的转换器、缩放器做数据处理,所有模块通过链表注册的方式解耦。

http://www.jsqmd.com/news/779639/

相关文章:

  • cursorrules:自动生成AI编码规范,提升开发效率
  • 文本匹配任务
  • 【深度解析】自主机器学习工程师 Neo:从 Agent 工作流到聊天内容审核 Pipeline 落地
  • UAE与Prism Hypothesis:统一语义与像素的隐空间方法
  • 从零搭建私有化AI智能体平台:基于Coze-Studio的架构解析与实战部署
  • 马拦过河卒
  • 离网型风力发电机储能系统充放电控制技术
  • 四川盛世钢联国际贸易有限公司型钢频道 -H型钢|工字钢|槽钢|角钢 - 四川盛世钢联营销中心
  • 基于MCP协议为AI智能体构建可插拔技能库:Semgrep与Comby实战
  • 洛谷刷题自动化提效工具:用户脚本与本地服务集成实践
  • 我花一周测了10个降AI工具,这个是性价比最高的降AI软件! - 我要发一区
  • 视频素材太多找不到?分镜标签+语义检索,让素材管理效率提升10倍
  • Cortex-A720性能监控与嵌入式跟踪技术解析
  • Java 集合遍历时删除元素的安全写法是什么?
  • 【LSF集群搭建】9-配置远程桌面服务
  • 低频信号处理在生成式AI中的关键作用与UAE架构实践
  • 跟随教程使用Taotoken快速创建一个AI对话机器人原型
  • 【技术干货】OpenManus 智能体框架深度解析:从 Agent Loop 到本地可控 AI 工作流实战
  • OpenAI 发布三款新语音模型;Plaud 获头部大厂投资,估值达 20 亿美元;阶跃星辰将完成近 25 亿美元融资丨日报
  • 角色扮演大语言模型:从核心原理到多智能体架构的实践指南
  • 嵌入式RTOS实战:从OpenFelix内核解析到物联网数据采集系统设计
  • 海思HI3516 MIPI屏幕时序参数详解:如何用计算器搞定HBP、VFP与像素时钟
  • kagi-skills:聚合Kagi AI API的Windows桌面工具集,提升工作效率
  • HPC集群构架手册:计算节点的搭建密码
  • ARM7TDMI调试接口架构与实战技巧
  • 镇江电加热器厂家哪家好?2026年05月选购指南来袭,电加热器/翅片加热管/风道式电加热器,电加热器源头厂家有哪些 - 品牌推荐师
  • CBJQ_Unlock使用教程
  • JAVA-实战8 Redis实战项目—雷神点评(10)附近商铺
  • 内存标准演进:如何平衡性能、功耗与尺寸,塑造消费电子体验
  • 基于注意力机制的时间序列异常检测实践与优化