📘 转换模块(十二):实现 RGB 转 RGB + 项目整合与上机实验
前置知识:
- 必须先理解 [10-转换模块 结构体抽象与管理](10-转换模块 结构体抽象与管理.md) 中定义的
T_VideoConvert框架- 已掌握 11-MJPEG 转 RGB 的完整流程
- 理解像素格式:RGB565(16 位)、RGB32(32 位)
本文是本系列最后一篇代码实现篇,完成后整个 video2lcd 项目全部打通。
🎯 本节核心目标
- 理解 RGB 转 RGB 模块的作用:摄像头输出 RGB565 但 LCD 需要 RGB32 时,进行位扩展转换
- 掌握 RGB565 → RGB32 的转换算法(移位、左移、拼装)
- 理解相同格式时不会调用转换模块(main 逻辑决定)
- 完整运行整个 video2lcd 项目:编译 → 传到开发板 → 执行
- 排查常见问题(设备节点错误、格式不支持等)
📂 本节涉及的文件
| 文件 | 作用 |
|---|---|
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 显示可能配置为 RGB32 (0x00RRGGBB,每像素 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 或驱动异常 | 确保 GetFrame 和 PutFrame 成对调用 |
编译报错 undefined reference to jpeg_* |
未链接 libjpeg | 确认 Makefile 中 LDFLAGS 包含 -ljpeg |
编译报错 arm-linux-gcc: not found |
交叉编译器未安装 | 安装交叉编译工具链,检查 $PATH |
✅ 六、全系列知识点自查清单
以下内容覆盖 06~12 所有必须掌握的点。请逐条自问是否理解:
摄像头模块(06~09)
结构体与框架(06):
-
struct VideoOpr和struct 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_VideoConvert与T_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 永远等待。检查 GetFrame 和 PutFrame 是否配对调用。
Q8 : 缩放模块 PicZoom 使用的是什么算法?有什么优缺点?
A8 : 使用最近邻插值(Nearest Neighbor)。优点:速度快、无浮点运算;缺点:缩放后图像可能有锯齿。
Q9 : mmap 的 MAP_SHARED 和 MAP_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-全项目架构与代码运转流程 将从架构高度俯瞰整个项目的设计思路和数据流转。