一、项目说明书1.1 项目概述本项目基于 100ASK IMX6ULL Pro 嵌入式开发板实现了USB UVC 摄像头数据采集 → YUYV 4:2:2 格式转 ARGB8888 格式 → 1024×600 LCD 实时居中显示的完整功能。项目严格遵循 Linux V4L2 视频采集框架和 Framebuffer 显示框架解决了嵌入式开发中常见的系统 GUI 抢占、双画面、紫绿偏色、显存寻址错误等经典问题最终实现了流畅、清晰、无失真的实时视频显示效果。1.2 硬件环境表格设备型号 / 参数开发板100ASK IMX6ULL ProARM Cortex-A7 内核摄像头USB UVC 免驱摄像头支持 YUYV 4:2:2 格式LCD 屏幕1024×600 分辨率32 位色深ARGB8888 格式摄像头采集分辨率640×48015fps传输接口USB OTGADB 文件传输1.3 软件环境表格软件版本开发板操作系统Buildroot Linux 4.9.88交叉编译器arm-buildroot-linux-gnueabihf-gcc 7.5.0开发主机Ubuntu 18.04 LTS传输工具ADB 1.0.41视频验证工具ffplay 4.2.31.4 功能目标✅ 自动检测并初始化 V4L2 摄像头设置 640×480 YUYV 采集格式✅ 设置摄像头采集帧率为 15fps✅ 申请 4 个内核缓冲区并映射到用户空间实现零拷贝采集✅ 实现标准 BT.601 YUYV 到 ARGB8888 的颜色空间转换✅ 自动计算居中偏移量将摄像头画面居中显示在 LCD 上✅ 支持程序优雅退出自动释放摄像头和 LCD 硬件资源✅ 解决系统 GUI 抢占、双画面、偏色等常见问题二、逐步实现流程含完整代码2.1 项目文件结构camorama_project_LCD/ ├── v4l2.h # V4L2摄像头接口声明 ├── v4l2.c # V4L2摄像头完整实现含帧率设置 ├── lcd.h # LCD显示接口声明 ├── lcd.c # LCD显示完整实现32位ARGB8888 ├── main.c # 主程序入口 └── Makefile # 交叉编译脚本模式规则2.2 环境准备配置交叉编译器环境变量export PATH$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin确认 ADB 连接正常adb devices # 输出List of devices attached # xxxxxxxx device关闭开发板默认 GUI释放 LCD 控制权必须执行# 立即停止当前运行的GUI /etc/init.d/S99myirhmi2 stop # 永久禁止GUI开机自启 mv /etc/init.d/*hmi* /root mv /etc/init.d/*lvgl* /root # 禁止LCD自动黑屏 echo -e \033[9;0] /dev/tty0 # 彻底清空LCD显存残留 cat /dev/zero /dev/fb02.3 V4L2 摄像头驱动实现2.3.1 v4l2.h接口声明#ifndef __V4L2_H #define __V4L2_H #include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include sys/mman.h #include linux/videodev2.h #define CAM_W 640 #define CAM_H 480 #define BUF_COUNT 4 // 帧缓冲结构体 struct camera_buf { unsigned char *start; size_t length; }; // 函数声明 int v4l2_init(const char *dev); int v4l2_grab_frame(struct camera_buf *buf); void v4l2_close(void); #endif2.3.2 v4l2.c完整实现含帧率设置#include v4l2.h static int cam_fd; static unsigned char *mptr[BUF_COUNT]; static unsigned int size[BUF_COUNT]; // 教程标准初始化流程 int v4l2_init(const char *dev) { int ret, i; struct v4l2_format vfmt; struct v4l2_streamparm stream_parm; struct v4l2_requestbuffers reqbuf; struct v4l2_buffer mapbuf; // 1. 打开设备 cam_fd open(dev, O_RDWR); if (cam_fd 0) return -1; // 2. 设置格式YUYV 640x480 memset(vfmt, 0, sizeof(vfmt)); vfmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; vfmt.fmt.pix.width CAM_W; vfmt.fmt.pix.height CAM_H; vfmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; ret ioctl(cam_fd, VIDIOC_S_FMT, vfmt); if (ret 0) return -2; // 3. 设置帧率 15fps教程新增 memset(stream_parm, 0, sizeof(stream_parm)); stream_parm.type V4L2_BUF_TYPE_VIDEO_CAPTURE; stream_parm.parm.capture.timeperframe.numerator 1; stream_parm.parm.capture.timeperframe.denominator 15; ioctl(cam_fd, VIDIOC_S_PARM, stream_parm); // 4. 申请内核缓冲区 memset(reqbuf, 0, sizeof(reqbuf)); reqbuf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; reqbuf.count BUF_COUNT; reqbuf.memory V4L2_MEMORY_MMAP; ret ioctl(cam_fd, VIDIOC_REQBUFS, reqbuf); if (ret 0) return -3; // 5. 内存映射 入队 memset(mapbuf, 0, sizeof(mapbuf)); mapbuf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; for (i 0; i BUF_COUNT; i) { mapbuf.index i; ioctl(cam_fd, VIDIOC_QUERYBUF, mapbuf); mptr[i] mmap(NULL, mapbuf.length, PROT_READ|PROT_WRITE, MAP_SHARED, cam_fd, mapbuf.m.offset); size[i] mapbuf.length; ioctl(cam_fd, VIDIOC_QBUF, mapbuf); } // 6. 开启采集 int type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(cam_fd, VIDIOC_STREAMON, type); printf(v4l2 init success\n); return 0; } // 教程原版采集一帧 int v4l2_grab_frame(struct camera_buf *buf) { int ret; struct v4l2_buffer readbuf; memset(readbuf, 0, sizeof(readbuf)); readbuf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; readbuf.memory V4L2_MEMORY_MMAP; ret ioctl(cam_fd, VIDIOC_DQBUF, readbuf); if (ret 0) return -1; buf-start mptr[readbuf.index]; buf-length readbuf.length; ioctl(cam_fd, VIDIOC_QBUF, readbuf); return 0; } // 教程原版释放资源 void v4l2_close(void) { int type V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(cam_fd, VIDIOC_STREAMOFF, type); for (int i 0; i BUF_COUNT; i) munmap(mptr[i], size[i]); close(cam_fd); }2.4 LCD 显示驱动实现2.4.1 lcd.h接口声明#ifndef __LCD_H #define __LCD_H #include v4l2.h #define LCD_W 1024 #define LCD_H 600 // 教程原版转换函数 void yuyv_to_rgb(unsigned char *yuyvdata, unsigned char *rgbdata, int w, int h); int lcd_init(void); // 严格匹配main.c调用名 void lcd_draw_frame(struct camera_buf buf); void lcd_close(void); #endif2.4.2 lcd.c完整实现32 位 ARGB8888#include lcd.h // 包含自定义的LCD接口头文件 #include linux/fb.h // 包含Linux Framebuffer标准头文件定义了fb_var_screeninfo等结构体 // 静态全局变量定义 // static关键字表示这些变量仅在本文件内可见避免全局命名冲突 static int lcd_fd; // LCD设备文件描述符类似文件句柄 static unsigned int *fb_base; // LCD显存映射后的用户空间起始地址32位指针对应ARGB8888 static int lcd_w; // LCD实际宽度像素自动从硬件读取 static int lcd_h; // LCD实际高度像素自动从硬件读取 static int screen_size; // LCD显存总大小字节 /** * brief 初始化LCD Framebuffer * return 成功返回0失败返回负值 */ int lcd_init(void) { struct fb_var_screeninfo var; // LCD可变参数结构体用于保存分辨率、色深等信息 // 1. 打开Framebuffer设备 // /dev/fb0是Linux系统中第一个Framebuffer设备的标准节点 // O_RDWR以读写模式打开 lcd_fd open(/dev/fb0, O_RDWR); if (lcd_fd 0) { perror(open lcd failed); // 打印系统错误信息 return -1; } // 2. 读取LCD硬件参数 // FBIOGET_VSCREENINFOFramebuffer的IOCTL命令用于获取可变屏幕信息 // 第二个参数是输出参数保存读取到的信息 ioctl(lcd_fd, FBIOGET_VSCREENINFO, var); // 从var结构体中提取分辨率信息 lcd_w var.xres; // 宽度单位像素 lcd_h var.yres; // 高度单位像素 // 打印LCD信息方便调试 // var.bits_per_pixel色深本项目为32位即ARGB8888 printf(LCD: %dx%d, bpp: %d\n, lcd_w, lcd_h, var.bits_per_pixel); // 3. 计算显存大小并映射 // 32位色深 4字节/像素A:8位 R:8位 G:8位 B:8位 // 所以总显存大小 宽度 × 高度 × 4 screen_size lcd_w * lcd_h * 4; // mmap将内核空间的LCD显存映射到用户空间 // 参数1NULL表示让内核自动选择映射地址 // 参数2映射的长度即显存总大小 // 参数3PROT_READ|PROT_WRITE映射区域可读可写 // 参数4MAP_SHARED共享映射对映射内存的修改会同步到硬件 // 参数5LCD设备文件描述符 // 参数60从文件开头开始映射 fb_base (unsigned int *)mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0); // 检查mmap是否成功 if (fb_base MAP_FAILED) { perror(mmap failed); // 打印映射失败的原因 close(lcd_fd); // 映射失败先关闭已打开的设备 return -1; } // 4. 清屏为黑色 // memset将一段内存区域设置为指定值 // 参数1显存起始地址 // 参数20黑色 // 参数3要设置的长度整个显存 memset(fb_base, 0, screen_size); return 0; // 初始化成功 } /** * brief 标准BT.601 YUYV转ARGB888832位 * param Y 亮度分量0-255 * param U 蓝色色差分量0-255 * param V 红色色差分量0-255 * return 32位ARGB8888颜色值A255表示完全不透明 * * static inline * - static仅本文件内可见其他文件无法调用 * - inline内联函数编译器会将函数体直接展开到调用处减少函数调用开销 */ static inline unsigned int yuv2argb(unsigned char Y, unsigned char U, unsigned char V) { // 标准BT.601转换公式 // Y亮度Luminance // U蓝色色差CbV红色色差Cr // 公式来源ITU-R BT.601标准用于标清电视 int r Y 1.402 * (V - 128); int g Y - 0.344 * (U - 128) - 0.714 * (V - 128); int b Y 1.772 * (U - 128); // 颜色限幅 // 确保RGB分量在0-255之间防止计算溢出导致颜色异常 // 例如如果r255就设为255如果r0就设为0 r r 255 ? 255 : (r 0 ? 0 : r); g g 255 ? 255 : (g 0 ? 0 : g); b b 255 ? 255 : (b 0 ? 0 : b); // 组合成32位ARGB8888 // 位操作说明 // - (0xFF 24)Alpha通道不透明度0xFF255表示完全不透明 // - (r 16)红色分量左移16位到第16-23位 // - (g 8)绿色分量左移8位到第8-15位 // - b蓝色分量在第0-7位 return (0xFF 24) | (r 16) | (g 8) | b; } /** * brief 在LCD上居中显示一帧摄像头数据 * param buf 摄像头采集到的YUYV格式帧数据 */ void lcd_draw_frame(struct camera_buf buf) { unsigned char *yuv (unsigned char *)buf.start; // YUYV数据指针 int x, y; // 计算居中偏移量 // 水平偏移(LCD宽度 - 摄像头宽度) / 2 // 垂直偏移(LCD高度 - 摄像头高度) / 2 // 这样640x480的画面就会居中显示在1024x600的LCD上 int x_off (lcd_w - 640) / 2; int y_off (lcd_h - 480) / 2; // 逐行处理并显示 // 外层循环遍历每一行y从0到479 for (y 0; y 480; y) { // 计算当前行的显存起始地址 // fb_base显存起始地址 // (y y_off)当前行在LCD上的实际行号 // lcd_wLCD宽度每行的像素数 // 因为是32位指针所以加法自动按4字节1个像素计算 unsigned int *line fb_base (y y_off) * lcd_w; // 处理当前行的所有像素 // 内层循环遍历每一列x从0到639步长2 // 步长2的原因YUYV格式中4字节表示2个像素Y0 U0 Y1 V0 for (x 0; x 640; x 2) { // 读取YUYV数据 // YUYV数据排列顺序Y0第1个像素的亮度 // U0两个像素共享的蓝色色差 // Y1第2个像素的亮度 // V0两个像素共享的红色色差 unsigned char Y0 *yuv; // 读取Y0指针后移1字节 unsigned char U0 *yuv; // 读取U0指针后移1字节 unsigned char Y1 *yuv; // 读取Y1指针后移1字节 unsigned char V0 *yuv; // 读取V0指针后移1字节 // 颜色转换并写入显存 // line[x x_off]第1个像素在显存中的位置 // line[x x_off 1]第2个像素在显存中的位置 line[x x_off] yuv2argb(Y0, U0, V0); // 第1个像素 line[x x_off 1] yuv2argb(Y1, U0, V0); // 第2个像素 } } } /** * brief 关闭LCD释放所有资源 */ void lcd_close(void) { // 1. 取消显存映射 // munmap解除mmap建立的映射关系 // 参数1映射的起始地址 // 参数2映射的长度 if (fb_base) { // 先检查指针是否有效 munmap(fb_base, screen_size); } // 2. 关闭LCD设备 if (lcd_fd 0) { // 先检查文件描述符是否有效 close(lcd_fd); } }2.5 主程序实现main.c#include v4l2.h #include lcd.h #include unistd.h int main(void) { // ✅ 修复正确定义 frame 变量 struct camera_buf frame; if (lcd_init() 0 || v4l2_init(/dev/video1) 0) { return -1; } while (1) { // ✅ 修复参数类型完全匹配 if (v4l2_grab_frame(frame) 0) { // ✅ 修复参数类型完全匹配 lcd_draw_frame(frame); } usleep(30000); } v4l2_close(); lcd_close(); return 0; }2.6 编译脚本Makefile模式规则CC arm-buildroot-linux-gnueabihf-gcc CFLAGS -Wall -O2 TARGET video2lcd OBJS main.o v4l2.o lcd.o all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) -lm %.o: %.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f *.o $(TARGET)2.7 编译运行步骤在 Ubuntu 终端编译make clean make传输可执行文件到开发板adb push video2lcd /home/nfs在开发板终端运行# 先关闭系统GUI如果还没执行 /etc/init.d/S99myirhmi2 stop cat /dev/zero /dev/fb0 # 赋予执行权限并运行 chmod x video2lcd ./video2lcd三、错误梳理与解决方案本项目开发过程中遇到的所有问题及解决方案汇总错误现象根本原因解决方案函数未定义引用头文件声明与源文件实现的函数名不一致统一函数名确保头文件和源文件完全匹配变量类型不匹配main.c 中 frame 变量定义为 void*应为 struct camera_buf修正变量类型定义双画面 / 画面重叠系统默认 GUImyirhmi2与用户程序同时写入 LCD 显存执行/etc/init.d/S99myirhmi2 stop关闭 GUI紫绿偏色1. 浮点转换公式在 ARM 上精度丢失2. U/V 分量顺序错误使用标准 BT.601 公式确认摄像头输出格式画面花屏 / 错位LCD 是 32 位色深代码按 16 位 RGB565 处理改用unsigned int *指针显存大小按 4 字节 / 像素计算LCD 自动黑屏Linux 控制台默认 10 分钟无操作自动黑屏执行echo -e \033[9;0] /dev/tty0禁止黑屏ADB 传输失败在开发板终端执行 ADB 命令ADB 命令必须在 Ubuntu 主机终端执行摄像头数据正常但 LCD 无显示显存映射失败或指针类型错误检查mmap返回值确认色深与指针类型匹配四、面试官角度问答4.1 基础概念类Q1什么是 V4L2 框架它解决了什么问题AV4L2Video for Linux 2是 Linux 内核中标准化的视频设备驱动框架。它为应用层提供了统一的 API 接口使得应用程序可以用相同的代码操作不同厂商、不同型号的视频设备如摄像头、电视卡、采集卡等无需关心底层硬件的具体实现细节解决了视频设备驱动碎片化的问题。Q2什么是 Framebuffer它的工作原理是什么结合代码说明。AFramebuffer帧缓冲是 Linux 内核提供的一种抽象层将显示设备的显存抽象为一个连续的内存区域。应用程序可以通过mmap将这段内存映射到用户空间直接读写映射后的内存即可实现对屏幕的绘制内核会自动将内存中的数据同步到显示设备上。结合代码lcd.c我们通过open(/dev/fb0, O_RDWR)打开 Framebuffer 设备通过ioctl(lcd_fd, FBIOGET_VSCREENINFO, var)读取硬件参数通过mmap(NULL, screen_size, PROT_READ|PROT_WRITE, MAP_SHARED, lcd_fd, 0)将显存映射到用户空间直接操作fb_base指针即可绘制画面Q3为什么摄像头常用 YUYV 4:2:2 格式而不是 RGB 格式AYUYV 4:2:2 是一种亮度 - 色差颜色空间格式其中 Y 表示亮度U 和 V 表示色度。人眼对亮度的敏感度远高于色度因此可以通过减少色度分量的采样率来压缩数据量。YUYV 4:2:2 每 4 个 Y 分量对应 2 个 U 和 2 个 V 分量数据量仅为 RGB888 的 2/3在保证图像质量的同时大大降低了带宽和存储需求因此被摄像头广泛采用。4.2 LCD 驱动实现类新增结合 lcd.cQ4结合代码说明 LCD 初始化的完整流程。A结合lcd.c的lcd_init函数流程如下打开 Framebuffer 设备open(/dev/fb0, O_RDWR)获取文件描述符读取硬件参数ioctl(lcd_fd, FBIOGET_VSCREENINFO, var)读取分辨率var.xres/var.yres和色深var.bits_per_pixel计算显存大小screen_size lcd_w * lcd_h * 432 位色深 4 字节 / 像素映射显存mmap(NULL, screen_size, PROT_READ|PROT_WRITE, MAP_SHARED, lcd_fd, 0)将内核显存映射到用户空间清屏memset(fb_base, 0, screen_size)将屏幕初始化为黑色Q5代码中为什么用unsigned int *作为显存指针而不是unsigned char *A这是由 LCD 的色深决定的我们的 LCD 是32 位色深ARGB8888每个像素占4 字节unsigned int在 ARM 平台上正好是4 字节使用unsigned int *指针一次读写正好是一个完整的像素效率最高如果用unsigned char *需要读写 4 次才能完成一个像素效率低且代码复杂Q6结合代码详细说明mmap函数每个参数的含义。A结合lcd.c中的mmap调用c运行fb_base (unsigned int *)mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, lcd_fd, 0);各参数含义NULL让内核自动选择映射地址推荐方式screen_size映射的长度即整个显存的大小lcd_w * lcd_h * 4PROT_READ | PROT_WRITE映射区域的权限可读可写MAP_SHARED共享映射对映射内存的修改会同步到硬件关键如果用MAP_PRIVATE画面不会显示lcd_fdFramebuffer 设备文件描述符0从文件开头开始映射显存起始位置Q7yuv2argb函数为什么用static inline修饰Astatic inline有两个关键作用static函数仅在本文件lcd.c内可见其他文件无法调用避免与其他文件的同名函数冲突inline内联函数编译器会将函数体直接展开到调用处减少函数调用开销颜色转换是高频操作每秒处理几十万次内联可以显著提高性能。Q8结合代码说明 YUYV 数据是如何转换成 ARGB8888 并写入显存的。A结合lcd.c的lcd_draw_frame函数流程如下计算居中偏移量x_off (lcd_w - 640) / 2y_off (lcd_h - 480) / 2逐行处理外层循环遍历每一行y从 0 到 479定位行指针unsigned int *line fb_base (y y_off) * lcd_w指向当前行的显存起始地址读取 YUYV 数据Y0 *yuvU0 *yuvY1 *yuvV0 *yuv4 字节表示 2 个像素颜色转换调用yuv2argb(Y0, U0, V0)和yuv2argb(Y1, U0, V0)写入显存line[x x_off] ...line[x x_off 1] ...Q9颜色转换函数中为什么要做 “颜色限幅”A颜色限幅的代码是c运行r r 255 ? 255 : (r 0 ? 0 : r); g g 255 ? 255 : (g 0 ? 0 : g); b b 255 ? 255 : (b 0 ? 0 : b);原因YUV 转 RGB 的公式使用了浮点运算结果可能会超出 0-255 的范围如果直接使用溢出的值会导致颜色异常比如全白、全黑或彩色噪点限幅确保 RGB 分量始终在有效范围内保证颜色显示正确Q10结合代码说明 32 位 ARGB8888 颜色值是如何通过位操作组合的。A组合代码是c运行return (0xFF 24) | (r 16) | (g 8) | b;位操作详解(0xFF 24)Alpha 通道不透明度0xFF255左移 24 位占据第 24-31 位最高 8 位(r 16)红色分量左移 16 位占据第 16-23 位(g 8)绿色分量左移 8 位占据第 8-15 位b蓝色分量不移动占据第 0-7 位最低 8 位|按位或操作将四个分量组合成一个 32 位整数Q11为什么要逐行处理使用line指针而不是直接计算每个像素的地址A逐行处理有两个关键优点提高缓存命中率CPU 缓存是按行缓存的逐行处理可以充分利用 CPU 缓存减少缓存失效提高性能代码更清晰行指针line指向当前行的起始地址列偏移x x_off更容易理解和调试如果直接计算每个像素的地址fb_base[(y y_off) * lcd_w x x_off]代码会更复杂且重复计算行地址效率更低。Q12结合lcd_close函数说明资源释放的顺序和原因。Alcd_close函数的释放顺序是先取消显存映射munmap(fb_base, screen_size)再关闭设备文件close(lcd_fd)原因必须先取消映射再关闭设备文件如果先关闭设备文件再取消映射可能会导致段错误Segmentation Fault因为设备文件关闭后映射的内存区域可能已经无效4.3 流程实现类Q13V4L2 内存映射mmap方式采集视频的完整流程是什么A完整流程分为 9 步打开视频设备文件/dev/videoX设置视频采集格式VIDIOC_S_FMT设置采集帧率VIDIOC_S_PARM可选申请内核缓冲区VIDIOC_REQBUFS查询每个缓冲区的信息VIDIOC_QUERYBUF将内核缓冲区映射到用户空间mmap将所有缓冲区放入采集队列VIDIOC_QBUF开启视频流VIDIOC_STREAMON循环从队列取出帧数据VIDIOC_DQBUF→ 处理 → 重新入队停止视频流 → 取消映射 → 关闭设备Q14为什么要使用多个内核缓冲区只用一个缓冲区可以吗A使用多个缓冲区是为了实现流水线操作提高采集效率。当应用程序处理第 N 个缓冲区的数据时摄像头可以同时向第 N1 个缓冲区写入数据避免了单缓冲区时的等待时间。如果只用一个缓冲区摄像头必须等待应用程序处理完数据后才能继续写入会导致帧率下降和画面卡顿。Q15Makefile 中的模式规则%.o: %.c有什么优点A模式规则是 Makefile 的一种高级特性它的优点有简洁性不需要为每个.c 文件单独写一条编译规则一条模式规则即可处理所有文件可维护性如果需要修改编译选项只需要修改一处即可扩展性新增.c 文件时不需要修改 Makefile自动适用模式规则4.4 问题排查类Q16如果运行程序后 LCD 显示双画面你会如何一步步排查A我会按照以下顺序排查关闭系统 GUI这是最常见的原因执行/etc/init.d/S99myirhmi2 stop后重新运行程序检查 LCD 色深执行cat /sys/class/graphics/fb0/bits_per_pixel确认色深确保代码中指针类型和显存大小计算正确验证摄像头数据将原始 YUYV 数据保存到文件用 ffplay 在电脑上播放排除摄像头采集问题检查显存寻址逐行打印显存地址确认行指针计算是否正确Q17如果画面出现严重的紫绿偏色可能是什么原因结合代码说明。A紫绿偏色几乎都是颜色空间转换错误导致的结合lcd.c代码常见原因有转换公式错误特别是 U 和 V 分量的系数不正确我们用的是标准 BT.601 公式U/V 分量顺序错误摄像头实际输出格式是 YVYU 而不是 YUYV需要调换 U 和 V 的顺序浮点运算精度丢失在 ARM 平台上建议改用整数转换公式LCD 输出格式是 BGR需要调换 R 和 B 分量的顺序Q18如何验证摄像头采集的数据是否正确A最可靠的方法是保存原始数据并在电脑上验证在代码中添加保存原始 YUYV 数据的功能fwrite(frame.start, 1, 640*480*2, fp)将保存的文件传到电脑用 ffplay 播放ffplay -f rawvideo -pixel_format yuyv422 -video_size 640x480 test.yuyv如果电脑上显示正常说明摄像头采集没问题问题出在 LCD 显示代码如果电脑上也异常说明摄像头格式设置错误。4.5 优化与扩展类Q19如何提高视频显示的帧率结合 LCD 代码说明。A可以从以下几个方面优化优化颜色转换使用 ARM NEON 指令集加速yuv2argb函数可将转换速度提高 3-5 倍减少内存拷贝直接在映射后的显存上进行转换避免中间缓存我们的代码已经是这样做的调整摄像头帧率通过VIDIOC_S_PARM将摄像头采集帧率提高到 30fps使用双缓冲避免画面撕裂提高显示流畅度降低分辨率如果硬件性能有限可将摄像头分辨率降低到 320×240Q20如果要将这个项目扩展为视频录制功能需要做哪些修改A需要添加以下功能视频编码使用 libjpeg 将 YUYV 帧编码为 JPEG 图片或使用 libx264 编码为 H.264 视频文件写入将编码后的数据写入文件系统录制控制添加按键或网络控制实现开始 / 停止录制时间戳为每个视频帧添加时间戳方便后续回放Q21如何实现程序的优雅退出A通过注册信号处理函数实现定义一个全局的 volatile 退出标志注册 SIGINT 信号CtrlC的处理函数在信号处理函数中设置退出标志主循环检测退出标志检测到后执行资源释放操作停止采集、取消映射、关闭设备避免直接在信号处理函数中执行复杂操作或释放资源