转换模块(十二):实现 RGB 转 RGB + 项目整合与上机实验

📘 转换模块(十二):实现 RGB 转 RGB + 项目整合与上机实验

前置知识

  • 必须先理解 [10-转换模块 结构体抽象与管理](10-转换模块 结构体抽象与管理.md) 中定义的 T_VideoConvert 框架
  • 已掌握 11-MJPEG 转 RGB 的完整流程
  • 理解像素格式:RGB565(16 位)、RGB32(32 位)

本文是本系列最后一篇代码实现篇,完成后整个 video2lcd 项目全部打通。


🎯 本节核心目标

  1. 理解 RGB 转 RGB 模块的作用:摄像头输出 RGB565 但 LCD 需要 RGB32 时,进行位扩展转换
  2. 掌握 RGB565 → RGB32 的转换算法(移位、左移、拼装)
  3. 理解相同格式时不会调用转换模块(main 逻辑决定)
  4. 完整运行整个 video2lcd 项目:编译 → 传到开发板 → 执行
  5. 排查常见问题(设备节点错误、格式不支持等)

📂 本节涉及的文件

文件 作用
convert/rgb2rgb.c RGB565 → RGB32 转换实现(本节核心)
main.c 主循环:采集 → 转换 → 缩放 → 合并 → 显示
Makefile, Makefile.build 编译系统
include/config.h 调试开关,设备路径
video/v4l2.c 摄像头采集
display/fb.c Framebuffer 显示
render/operation/zoom.c 画面缩放
render/operation/merge.c 画面合并
netprint_client.c 网络打印调试工具(可选)

其余模块(video 框架、display 框架、yuv2rgb、mjpeg2rgb)均已在 06~11 中掌握。


🧠 一、为什么需要 RGB 转 RGB?

1.1 场景分析

摄像头可能直接输出 RGB565 格式(例如某些 OV 系列传感器),但 LCD 显示可能配置为 RGB320x00RRGGBB,每像素 4 字节)。

场景 摄像头格式 LCD 格式 是否需要转换
1 RGB565 RGB565 ❌ 不需要(格式相同)
2 RGB565 RGB32 ✅ 需要转换
3 RGB32 RGB32 ❌ 不需要

注意 :rgb2rgb 模块也支持输入输出均为 RGB565 的情况(直接 memcpy),但 main.c 中已经判断格式相同就跳过,所以实际不会调用。

1.2 为什么不直接支持 RGB32 格式?

摄像头直接输出 RGB32 的场景很少见,因为 RGB32 占用带宽太大(640×480 下每帧 1.2MB),USB 带宽扛不住。大多数摄像头输出 MJPEG(压缩)或 YUYV(原始)或 RGB565(原始)。


🔧 二、RGB565 → RGB32 转换详解

2.1 RGB565 与 RGB32 的位宽差异

格式 红色(R) 绿色(G) 蓝色(B) 总位数 每像素字节
RGB565 5 位 6 位 5 位 16 位 2 字节
RGB32 8 位 8 位 8 位 32 位 4 字节

内存布局对比:

复制代码
RGB565 (2字节): [R4 R3 R2 R1 R0 G5 G4 G3 G2 G1 G0 B4 B3 B2 B1 B0]
                  ↑ 5位红     ↑ 6位绿         ↑ 5位蓝

RGB32  (4字节): [00000000 R7~R0 G7~G0 B7~B0]
                  ↑ 空(8位) ↑ 8位红  ↑ 8位绿  ↑ 8位蓝

2.2 转换步骤

从 16 位 RGB565 像素中提取 R、G、B 分量,分别扩展到 8 位,再拼成 32 位整数:

复制代码
步骤1: 从 RGB565 提取分量
    r = (color >> 11)          → 取出高 5 位(红)
    g = (color >> 5) & 0x3f    → 取出中间 6 位(绿)
    b = color & 0x1f           → 取出低 5 位(蓝)

步骤2: 扩展到 8 位
    R8 = r << 3    ← 5 位 → 8 位,左移 3 位
    G8 = g << 2    ← 6 位 → 8 位,左移 2 位
    B8 = b << 3    ← 5 位 → 8 位,左移 3 位

步骤3: 拼成 32 位
    color32 = (R8 << 16) | (G8 << 8) | B8

图示

复制代码
RGB565 16位:  RRRR RGGG GGGG BBBBB
               │      │        │
               ▼      ▼        ▼
               │      │        │
          左移3位  左移2位   左移3位
               │      │        │
               ▼      ▼        ▼
RGB32  32位:  00000000 RRRRR000 GGGGGG00 BBBBB000
              (高8位=0) (红8位)  (绿8位)  (蓝8位)

2.3 为什么绿色移位不同?

分量 原始位数 目标位数 丢失信息 左移量 解释
红® 5 位 8 位 丢失 3 位 3 r << 3 低位补 0
绿(G) 6 位 8 位 丢失 2 位 2 g << 2 低位补 0
蓝(B) 5 位 8 位 丢失 3 位 3 b << 3 低位补 0

因为绿色原始有 6 位 ,比红色/蓝色多 1 位,丢失的信息少,所以只需要左移 2 位 就能铺满 8 位;而红/蓝丢失 3 位,需要左移 3 位

💡 记忆口诀:红蓝移 3,绿色移 2;因为绿色多 1 位,少补 1 个 0。

2.4 完整转换代码

c 复制代码
for (y = 0; y < ptPixelDatasOut->iHeight; y++) {
    for (x = 0; x < ptPixelDatasOut->iWidth; x++) {
        unsigned short color = *pwSrc++;            // 读一个 RGB565 像素

        // 1) 提取分量
        unsigned int r = color >> 11;                // 高5位 → 红
        unsigned int g = (color >> 5) & 0x3f;        // 中间6位 → 绿
        unsigned int b = color & 0x1f;               // 低5位 → 蓝

        // 2) 位扩展并拼成 32 位
        unsigned int rgb32 = ((r << 19) | (g << 10) | (b << 3));
        // 等价写法:((r<<3)<<16) | ((g<<2)<<8) | (b<<3)

        *pdwDest++ = rgb32;                          // 写入目标缓冲区
    }
}

🧩 三、模块注册与框架

3.1 T_VideoConvert 结构体

rgb2rgb.c 中定义的转换器实例:

c 复制代码
static T_VideoConvert g_tRgb2RgbConvert = {
    .name        = "rgb2rgb",
    .isSupport   = isSupportRgb2Rgb,
    .Convert     = Rgb2RgbConvert,
    .ConvertExit = Rgb2RgbConvertExit,
};

3.2 isSupport 判断逻辑

c 复制代码
static int isSupportRgb2Rgb(int iPixelFormatIn, int iPixelFormatOut)
{
    if (iPixelFormatIn != V4L2_PIX_FMT_RGB565)
        return 0;

    // 输出支持 RGB565 或 RGB32
    if ((iPixelFormatOut == V4L2_PIX_FMT_RGB565) ||
        (iPixelFormatOut == V4L2_PIX_FMT_RGB32))
        return 1;

    return 0;
}

3.3 Convert 函数分支处理

c 复制代码
static int Rgb2RgbConvert(PT_VideoBuf ptVideoBufIn, PT_VideoBuf ptVideoBufOut)
{
    if (ptVideoBufOut->iPixelFormat == V4L2_PIX_FMT_RGB565)
    {
        // 场景:输出 RGB565(同格式复制)
        memcpy(ptPixelDatasOut->aucPixelDatas,
               ptPixelDatasIn->aucPixelDatas,
               ptPixelDatasOut->iTotalBytes);
        return 0;
    }
    else if (ptVideoBufOut->iPixelFormat == V4L2_PIX_FMT_RGB32)
    {
        // 场景:输出 RGB32(位扩展转换)
        // ... 上面 2.4 节的循环代码 ...
        return 0;
    }
    return -1;
}

3.4 注册到管理器

c 复制代码
int Rgb2RgbInit(void)
{
    return RegisterVideoConvert(&g_tRgb2RgbConvert);
}

注册后,VideoConvertInit() 会将其挂入全局链表,供 GetVideoConvertForFormats() 按格式匹配。


🚀 四、上机实验

4.1 实验目的

在 ARM 开发板上完整运行 video2lcd 项目,验证从摄像头采集到 LCD 显示的整条链路。

4.2 编译项目

在 Ubuntu 主机上,进入 video2lcd 目录,执行 make:

bash 复制代码
book@100ask:~/02_video2/video2lcd/video2lcd$ make

你会看到每个 .c 文件被 arm-linux-gcc 编译,最后链接成 video2lcd 可执行文件。

编译失败排查

  • 确保已安装交叉编译工具链:arm-linux-gcc -v
  • 确保已安装 libjpeg 的交叉编译版本(用于 MJPEG 解码)
  • 报错 undefined reference to jpeg_* → 缺少 -ljpeg

4.3 上传到开发板

bash 复制代码
# 方法1:adb(推荐)
book@100ask:~/02_video2/video2lcd/video2lcd$ adb push video2lcd /root/

# 方法2:scp(网络)
book@100ask:~/02_video2/video2lcd/video2lcd$ scp video2lcd root@192.168.1.100:/root/

# 方法3:nfs(网络文件系统)
book@100ask:~/02_video2/video2lcd/video2lcd$ cp video2lcd /nfs_root/

4.4 运行实验

4.4.1 实验记录
复制代码
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 实验记录(请填写)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

开发板型号:___100ask_IMX6ULL(或你的具体型号)___
LCD 分辨率:___程序自动获取(例如 800x480)___
摄像头型号:___USB 摄像头(支持 YUYV 格式)___
摄像头节点:___/dev/video1___

命令:
$ ./video2lcd /dev/video1

程序输出日志:
──────────────────────────────────────────────────
/dev/video1 supports streaming i/o
Convert yuv2rgb, ret = 0
Convert yuv2rgb, ret = 0
Convert yuv2rgb, ret = 0
...(后续每帧均打印 Convert yuv2rgb, ret = 0)
──────────────────────────────────────────────────

摄像头实际输出格式:___V4L2_PIX_FMT_YUYV___
匹配的转换模块:___yuv2rgb___
画面显示效果:____正常显示 _______

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
摄像头
实验结果展示

linux相机

4.4.2 参考:成功输出
text 复制代码
/dev/video1 supports streaming i/o
Convert yuv2rgb, ret = 0
Convert yuv2rgb, ret = 0
...

说明摄像头输入格式是 YUYV,自动匹配了 yuv2rgb 模块,LCD 显示正常。

4.4.3 参考:失败输出
text 复制代码
/dev/video0 is not a video capture device
VideoDeviceInit for /dev/video0 error!

这是因为 /dev/video0 可能不是摄像头设备(如板载 ISP 或其他),需要换到 /dev/video1

提示/dev/video1 通常是 USB 摄像头节点,/dev/video0 可能是板载摄像头或其他设备。

4.5 观察现象

  • 屏幕上应实时显示摄像头画面
  • 画面在屏幕中央,等比例显示
  • Ctrl+C 终止程序

🔍 五、常见问题排查

现象 可能原因 解决方法
no devices/emulators found adb 服务未连接 重新插拔 USB,检查 adb devices
is not a video capture device 设备节点错误 ls /dev/video* 查看可用节点,换一个试试
Convert ... ret = -1 格式不匹配或解码失败 检查摄像头输出格式,是否支持 YUYV/MJPEG/RGB565
画面花屏或颜色错误 转换公式或字节序错误 检查 RGB565 打包顺序(小端/大端)
程序卡死、poll 超时 忘记 QBUF 或驱动异常 确保 GetFramePutFrame 成对调用
编译报错 undefined reference to jpeg_* 未链接 libjpeg 确认 Makefile 中 LDFLAGS 包含 -ljpeg
编译报错 arm-linux-gcc: not found 交叉编译器未安装 安装交叉编译工具链,检查 $PATH

✅ 六、全系列知识点自查清单

以下内容覆盖 06~12 所有必须掌握的点。请逐条自问是否理解:

摄像头模块(06~09)

结构体与框架(06):

  • struct VideoOprstruct VideoDevice 的作用和区别
  • 链表注册 RegisterVideoOpr 的尾插法逻辑
  • VideoDeviceInit 如何遍历链表并调用 InitDevice
  • 为什么需要统一的设备管理框架(解耦、可扩展

V4L2 初始化(07~08):

  • 6 步初始化流程:open → QUERYCAP → ENUM_FMT → S_FMT → REQBUFS → mmap → QBUF
  • VIDIOC_S_FMT 后必须读回实际宽高的原因(驱动可能调整)
  • VIDIOC_REQBUFS 的 count 可能小于请求值,程序要兼容
  • mmap 参数 PROT_READ, MAP_SHARED 的含义
  • VIDIOC_QBUF 的作用及必须在 STREAMON 之前完成

数据传输(09):

  • poll 等待数据就绪的必要性(避免忙等)
  • VIDIOC_DQBUF 取出已填满缓冲区,保存 index
  • VIDIOC_QBUF 归还缓冲区,形成循环
  • VIDIOC_STREAMON / STREAMOFF 控制流启停
  • GetFrame 返回的 aucPixelDatas 指向 mmap 内存(零拷贝)
  • PutFrame 必须在数据处理之后调用

转换模块(10~12)

转换框架(10):

  • T_VideoConvertT_VideoOpr 的相似设计模式
  • RegisterVideoConvert 链表注册
  • GetVideoConvertForFormats 按格式匹配
  • VideoConvertInit 批量注册 yuv2rgb, mjpeg2rgb, rgb2rgb

YUV 转 RGB(未单独成篇,但在 10 中有涉及):

  • YUYV 的 4 字节 → 2 像素排列(U/V 共享)
  • 查表法原理(预计算整数表,避免浮点)
  • RGB565 打包:红5绿6蓝5
  • 小端存储时的两字节写入顺序
  • 内存管理:输出缓冲区第一次 malloc,之后复用

MJPEG 转 RGB(11):

  • libjpeg 解码的 8 步固定顺序
  • 为什么必须自定义错误处理(setjmp/longjmp)
  • 为什么必须用内存数据源(jpeg_mem_src_tj)
  • 解码后 RGB24 → RGB565/RGB32 的再次转换

RGB 转 RGB(12,本文):

  • RGB565 → RGB32 的位扩展公式
  • 为什么绿色移位 2 位,红蓝移位 3 位
  • 相同格式时 main 不会调用转换模块

主程序整合

  • main 函数流程:显示初始化 → 摄像头初始化 → 格式匹配 → 启动流 → 循环取帧 → 转换 → 缩放 → 合并 → 刷新
  • 缩放居中算法(保持比例)
  • 停止流并释放资源(ExitDevice, ConvertExit 在程序退出时调用)

编译与调试

  • 使用 make 编译跨平台 ARM 程序
  • 使用 adb push 上传到开发板
  • 使用 adb shell 进入板子运行
  • 观察打印信息判断错误点

❓ 七、最终自测题(一问一答)

Q1 : 如果摄像头输出 RGB565,LCD 也是 RGB565,程序会调用 rgb2rgb 模块吗?为什么?
A1 : 不会。因为 main 中先判断 iPixelFormatOfVideo == iPixelFormatOfDisp,相等时直接使用原始数据,不进入转换分支。

Q2 : 在 RGB565 → RGB32 转换中,为什么绿色要左移 2 位而红蓝左移 3 位?
A2: 因为绿色原始占 6 位,需要补 2 位到 8 位;红蓝原始占 5 位,需要补 3 位。左移的位数等于需要补充的低位零的个数。

Q3 : 使用 libjpeg 解码 MJPEG 时,如果不设置自定义错误处理,当遇到损坏的 JPEG 帧时会发生什么?
A3 : libjpeg 默认调用 exit(),整个程序会崩溃退出

Q4 : VIDIOC_DQBUF 返回后,为什么要保存 index 到 ptVideoDevice->iVideoBufCurIndex
A4 : 因为后续 PutFrame 必须归还同一个缓冲区,需要知道是哪个索引。

Q5 : 为什么 yuv2rgb 模块中要用查表法而不是直接用浮点公式?
A5: 浮点运算在嵌入式设备上很慢,查表法用整数数组预计算,速度快几十倍。

Q6 : 执行 ./video2lcd /dev/video1 后,屏幕无图像,打印 Convert ret = -1,可能是什么原因?
A6 : 可能原因:摄像头输出格式不是程序支持的三种格式之一(YUYV/MJPEG/RGB565),或者 GetVideoConvertForFormats 找不到匹配的转换模块。

Q7 : 程序运行几秒后卡死,不再打印新帧,最可能是什么问题?
A7 : 可能是在循环中忘记调用 PutFrame ,导致所有缓冲区都被 DQBUF 借出,驱动没有可用缓冲区,poll 永远等待。检查 GetFramePutFrame 是否配对调用。

Q8 : 缩放模块 PicZoom 使用的是什么算法?有什么优缺点?
A8 : 使用最近邻插值(Nearest Neighbor)。优点:速度快、无浮点运算;缺点:缩放后图像可能有锯齿。

Q9 : mmapMAP_SHAREDMAP_PRIVATE 有什么区别?
A9 : MAP_SHARED 对映射内存的修改会写回设备(帧缓冲场景必须用);MAP_PRIVATE 是写时复制,修改不影响原设备。

Q10 : 如果 LCD 屏幕是 800×480,摄像头输出是 640×480,程序会进行缩放吗?
A10 : 不会。因为 640 < 800 且 480 == 480,画面不比屏幕大,不需要缩放,直接居中显示。


🧭 八、完整代码结构回顾

复制代码
video2lcd/
├── main.c                     # 主程序(主循环)
├── Makefile                   # 顶层编译
├── Makefile.build             # 递归编译模板
│
├── include/                   # 头文件(接口定义)
│   ├── config.h               # 全局宏(FB设备名、调试打印开关)
│   ├── disp_manager.h         # 显示设备接口
│   ├── video_manager.h        # 摄像头设备接口
│   ├── convert_manager.h      # 格式转换接口
│   ├── pic_operation.h        # 像素数据结构 T_PixelDatas
│   └── render.h               # 缩放/合并函数声明
│
├── video/                     # 摄像头模块
│   ├── video_manager.c        # 链表管理(注册、查找、初始化)
│   └── v4l2.c                 # V4L2 具体实现
│
├── display/                   # 显示模块
│   ├── disp_manager.c         # 显示设备管理器
│   └── fb.c                   # Framebuffer 操作
│
├── convert/                   # 格式转换模块
│   ├── convert_manager.c      # 转换管理器
│   ├── yuv2rgb.c              # YUYV → RGB
│   ├── color.c + color.h      # YUV↔RGB 查找表
│   ├── mjpeg2rgb.c            # MJPEG → RGB
│   ├── jdatasrc-tj.c          # libjpeg 内存数据源
│   ├── rgb2rgb.c              # RGB565 → RGB32
│   ├── jpeglib.h / jerror.h / jinclude.h  # libjpeg 头文件
│
├── render/                    # 渲染模块
│   └── operation/
│       ├── zoom.c             # 图片缩放
│       └── merge.c            # 图片合并
│
└── netprint_client.c          # 网络打印调试(可选)

🏁 总结

通过本系列博客(06~12),你已经完成了一个完整的嵌入式 Linux 相机项目,掌握了:

能力 掌握内容
V4L2 设备框架 打开、设置、采集、释放完整流程
模块化设计 链表注册 + 函数指针多态 + 策略模式
图像格式转换 YUV→RGB、MJPEG→RGB、RGB→RGB 三种
图像渲染 最近邻缩放 + 区域合并 + 居中显示
嵌入式编译 交叉编译、adb 部署、开发板运行
调试能力 日志分析、设备节点排查、常见问题定位

现在你应该能够独立编写类似的视频采集程序,甚至扩展支持更多格式或特效。

🔥 下一节预告13-全项目架构与代码运转流程 将从架构高度俯瞰整个项目的设计思路和数据流转。

相关推荐
唐诺1 小时前
IOS学习路线计划
ios
研究点啥好呢1 小时前
凯捷 自动化测试(Java+Selenium)面试题精选:10道高频考题+答案解析
java·开发语言·python·selenium·测试工具·求职招聘
ghie90901 小时前
基于遗传算法的配电网重构
开发语言·重构
for_ever_love__1 小时前
UI学习:无限轮播视图
学习·ui·ios·objective-c
SilentSamsara1 小时前
生成器进阶:`yield from`、协程历史与双向通信
开发语言·python·青少年编程·pycharm
kyle~1 小时前
ROS2---消息过滤
开发语言·c++·机器人·ros2
xieliyu.1 小时前
Java手搓二叉树:基础遍历与核心操作全解析
java·开发语言·数据结构·学习
雪度娃娃1 小时前
C++异步日志系统
开发语言·c++
xyq20242 小时前
SVN 提交操作详解
开发语言