V4L2应用程序开发实战:枚举摄像头所有支持的格式和分辨率
V4L2应用程序开发实战:枚举摄像头所有支持的格式和分辨率
这节课我们只做一件事:用手把手的方式,从零写出一个完整的 V4L2 程序,它能列出你的摄像头设备所有支持的像素格式(比如 YUYV、MJPEG)以及每种格式下的所有分辨率(比如 640x480、1280x720)。
我会带你一步步写代码、编译、传到开发板并运行,同时讲解每个关键知识点。你不需要有多个摄像头,一个就够。写完这个程序,你就掌握了 V4L2 中最常用的查询接口,以后写任何摄像头应用都心里有底。
🎯 这节课你将收获什么?
| 技能点 | 重要性 | 说明 |
|---|---|---|
| 编写完整的 V4L2 枚举程序 | ⭐⭐⭐ 必须掌握 | 你以后开发摄像头应用,第一步几乎都是先查摄像头能力 |
理解VIDIOC_ENUM_FMT和VIDIOC_ENUM_FRAMESIZES | ⭐⭐⭐ 必须掌握 | 面试常问,实际项目必备 |
| 交叉编译(主机 → ARM 开发板) | ⭐⭐⭐ 必须掌握 | 嵌入式开发的标配流程 |
| 使用 ADB 上传并运行程序 | ⭐⭐ 重点掌握 | 调试嵌入式程序最快的方法 |
| 调试常见错误(权限、设备节点、工具链) | ⭐⭐ 重点掌握 | 实际开发中一定会遇到 |
文章目录
- V4L2应用程序开发实战:枚举摄像头所有支持的格式和分辨率
- 🎯 这节课你将收获什么?
- 一、准备工作
- 1.1 你需要什么?
- 1.2 验证摄像头设备
- 二、编写完整代码(可直接复制)
- 三、在 Ubuntu 主机上先本地测试(可选)
- 四、交叉编译:生成开发板能运行的程序
- 4.1 设置工具链路径
- 4.2 交叉编译
- 五、将程序传到开发板并运行
- 5.1 连接 ADB
- 5.2 上传程序
- 5.3 进入开发板 shell 并运行
- 5.4 查看输出
- 六、逐行讲解代码中的关键点(面试/考试重点)
- 6.1 为什么需要 `memset` 清零结构体?
- 6.2 `VIDIOC_ENUM_FMT` 的 index 从 0 开始递增
- 6.3 `VIDIOC_ENUM_FRAMESIZES` 需要传入 `pixel_format`
- 6.4 `fsenum.type` 的作用
- 七、常见错误与解决方法
- 八、必须搞懂的 5 个核心问题
- 问题1:为什么每次循环都要用 `memset` 清零?
- 问题2:`VIDIOC_ENUM_FMT` 的 `index` 为什么从 0 开始递增?它何时停止?
- 问题3:为什么 `VIDIOC_ENUM_FRAMESIZES` 必须放在内层循环?
- 问题4:分辨率的“离散”和“连续”是什么意思?分别怎么处理?
- 问题5:如果枚举过程中 ioctl 返回 -1 且 `errno` 不是 `EINVAL`,代表什么?
- 九、面试官提问环节(必背)
- 第1问:V4L2 中如何枚举摄像头支持的像素格式?请说明使用的 ioctl 和关键数据结构。
- 第2问:在枚举完一种格式后,如何知道该格式支持哪些分辨率?需要用到什么 ioctl?为什么这个 ioctl 必须放在格式枚举的循环内部?
- 第3问:`v4l2_frmsizeenum` 结构体中的 `type` 字段有哪些取值?分别代表什么?
- 第4问:枚举过程中,`ioctl` 返回 -1 是否一定表示错误?如何正确判断枚举结束?
- 第5问:为什么要先用 `memset` 清零 V4L2 结构体?不清零会有什么后果?
- 第6问:请画出或描述枚举所有格式及分辨率的程序流程图。
- 十、总结
一、准备工作
1.1 你需要什么?
- 一台Ubuntu 虚拟机(或主机),装有交叉编译工具链(我们使用百问网 T113 开发板的工具链)。
- 一块6ull 开发板(已烧录 Tina Linux 系统),并通过 USB OTG 线连接到电脑,ADB 能识别。
- 一个USB 摄像头(或开发板自带的摄像头接口),插入开发板后会出现
/dev/video0(或/dev/video1)。
如果你还没有交叉编译工具链,不用担心,我会给出路径和设置方法。
1.2 验证摄像头设备
在开发板上(通过adb shell或串口)执行:
bash
ls /dev/video*如果显示/dev/video0或/dev/video1,说明摄像头已经被驱动识别。
二、编写完整代码(可直接复制)
下面是一个完整的 C 程序,它会打开指定的视频设备,枚举所有格式和分辨率,然后打印出来。代码里写了详尽的注释,即使你没学过 V4L2,也能看懂每一步在做什么。
创建一个文件video_enum.c:
c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <linux/videodev2.h> int main(int argc, char **argv) { int fd; struct v4l2_fmtdesc fmtdesc; struct v4l2_frmsizeenum fsenum; int fmt_index = 0; int frame_index; /* 1. 检查命令行参数 */ if (argc != 2) { fprintf(stderr, "Usage: %s <video_device>\n", argv[0]); fprintf(stderr, "Example: %s /dev/video0\n", argv[0]); return 1; } /* 2. 打开设备 */ fd = open(argv[1], O_RDWR); if (fd < 0) { perror("open"); return 1; } printf("Device: %s\n", argv[1]); printf("========================================\n"); /* 3. 循环枚举所有像素格式 */ while (1) { memset(&fmtdesc, 0, sizeof(fmtdesc)); fmtdesc.index = fmt_index; fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) < 0) { break; // 枚举结束 } /* 打印格式信息 */ printf("Format %d: %s (fourcc=0x%08x)\n", fmt_index, fmtdesc.description, fmtdesc.pixelformat); /* 4. 对于当前格式,枚举所有支持的分辨率 */ frame_index = 0; while (1) { memset(&fsenum, 0, sizeof(fsenum)); fsenum.index = frame_index; fsenum.pixel_format = fmtdesc.pixelformat; if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) < 0) { break; // 该格式的分辨率枚举结束 } /* 只处理离散分辨率(绝大多数摄像头都是离散的) */ if (fsenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) { printf(" %d x %d\n", fsenum.discrete.width, fsenum.discrete.height); } else if (fsenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) { /* 连续范围(极少见),简单打印范围 */ printf(" continuous: %d..%d x %d..%d\n", fsenum.stepwise.min_width, fsenum.stepwise.max_width, fsenum.stepwise.min_height, fsenum.stepwise.max_height); } frame_index++; } fmt_index++; printf("\n"); // 格式之间空一行 } close(fd); return 0; }代码核心逻辑:
- 打开设备文件。
- 用
VIDIOC_ENUM_FMT依次查询每种像素格式,直到返回错误。 - 对每一种格式,用
VIDIOC_ENUM_FRAMESIZES查询它支持的分辨率,直到返回错误。 - 打印出每种格式的名称、fourcc 码以及所有分辨率。
三、在 Ubuntu 主机上先本地测试(可选)
为了确保代码逻辑正确,你可以在 Ubuntu 主机上插一个 USB 摄像头(或者使用虚拟机里的虚拟摄像头),先用本地 gcc 编译测试。
bash
sudo apt install gcc # 如果没有安装 gcc -o video_enum video_enum.c ./video_enum /dev/video0如果主机上有摄像头,你会看到类似输出:
text
Device: /dev/video0 ======================================== Format 0: YUYV 4:2:2 (fourcc=0x56595559) 640 x 480 800 x 600 1280 x 720 Format 1: MJPEG (fourcc=0x47504a4d) 640 x 480 1280 x 720如果主机没有摄像头,可以跳过这一步,直接交叉编译到开发板上运行。
四、交叉编译:生成开发板能运行的程序
我们使用 T113 开发板的交叉编译工具链。工具链在 Tina-SDK 的prebuilt目录下。
4.1 设置工具链路径
假设你的 Tina-SDK 放在/home/ubuntu/tina-d1-h,则执行:
bash
export PATH=$PATH:/home/ubuntu/tina-d1-h/prebuilt/gcc/linux-x86/arm/toolchain-sunxi-musl/toolchain/bin为了确认是否设置成功,输入:
bash
arm-buildroot-linux-gnueabihf-gcc --version应该能看到版本信息。如果没有,请检查路径是否正确(你的用户名和 SDK 目录可能不同)。
4.2 交叉编译
bash
arm-buildroot-linux-gnueabihf-gcc -o video_enum video_enum.c编译成功后,用file命令查看:
bash
file video_enum输出应该包含ELF 32-bit LSB executable, ARM,说明是 ARM 架构的可执行文件。
五、将程序传到开发板并运行
5.1 连接 ADB
- 用 USB 线连接开发板的OTG 口到电脑。
- 在 Ubuntu 终端输入:
bash
adb devices如果看到类似20080411 device的设备,说明连接成功。
如果显示no permissions,可以尝试sudo adb kill-server后重新插拔。
5.2 上传程序
bash
adb push video_enum /root/5.3 进入开发板 shell 并运行
bash
adb shell cd /root chmod +x video_enum # 如果没有可执行权限 ./video_enum /dev/video0如果摄像头是video1,就改成/dev/video1。
5.4 查看输出
你应该会看到类似下面的输出(根据你的摄像头支持情况可能不同):
text
Device: /dev/video0 ======================================== Format 0: YUYV 4:2:2 (fourcc=0x56595559) 640 x 480 160 x 120 320 x 240 352 x 288 800 x 600 1280 x 720 1280 x 1024 Format 1: Motion-JPEG (fourcc=0x47504a4d) 640 x 480 160 x 120 320 x 240 352 x 288 800 x 600 1280 x 720 1280 x 1024恭喜!你已经成功列出了摄像头的所有能力。
六、逐行讲解代码中的关键点(面试/考试重点)
6.1 为什么需要memset清零结构体?
c
memset(&fmtdesc, 0, sizeof(fmtdesc));V4L2 的结构体中有很多保留字段,内核要求应用程序把它们初始化为 0,否则 ioctl 可能失败或行为异常。这是一个好习惯。
6.2VIDIOC_ENUM_FMT的 index 从 0 开始递增
内核维护了一个格式列表,你每次传入一个 index,它返回第 index 个格式的信息。当传入的 index 超过最后一个时,ioctl 返回 -1,我们跳出循环。
6.3VIDIOC_ENUM_FRAMESIZES需要传入pixel_format
只有先知道格式,才能查询该格式的分辨率。这也是为什么要在内层循环中的原因。
6.4fsenum.type的作用
V4L2_FRMSIZE_TYPE_DISCRETE:离散分辨率,直接读取discrete.width/height。V4L2_FRMSIZE_TYPE_STEPWISE:连续范围(如最小 32x32 到最大 1920x1080,步长 8)。这种摄像头非常少见,我们简单打印范围即可。
七、常见错误与解决方法
| 错误现象 | 原因 | 解决办法 |
|---|---|---|
open: Permission denied | 普通用户无权限访问设备 | 开发板上用 root 用户(adb shell 默认 root),或chmod 666 /dev/video0 |
VIDIOC_ENUM_FMT: Invalid argument | 传入了错误的type值 | 确保type = V4L2_BUF_TYPE_VIDEO_CAPTURE |
交叉编译时command not found | PATH 未设置 | 重新执行export PATH=...,并确认路径正确 |
| ADB 推文件失败(device offline) | USB 线松动或驱动没加载 | 拔插 USB 线,重新运行adb devices |
| 开发板执行后没有任何输出 | 摄像头设备节点可能不是 video0 | 先用ls /dev/video*查看,用正确的节点名 |
八、必须搞懂的 5 个核心问题
问题1:为什么每次循环都要用memset清零?
答:V4L2 的结构体中包含reserved保留字段。内核要求这些字段必须为 0,否则可能被视为要求启用尚未定义的功能,导致 ioctl 失败或行为异常。清零是一个好习惯,避免栈上的随机值污染。
问题2:VIDIOC_ENUM_FMT的index为什么从 0 开始递增?它何时停止?
答:内核内部维护一个格式的列表,index就是数组下标。从 0 开始,每成功一次就加 1,直到内核返回 -1(且errno为EINVAL)。这表示已经枚举完所有格式。这种模式在 V4L2 中非常普遍(枚举帧率、控制项等也类似)。
问题3:为什么VIDIOC_ENUM_FRAMESIZES必须放在内层循环?
答:该 ioctl 需要输入参数pixel_format,即具体的格式 fourcc。这个 fourcc 只能从外层VIDIOC_ENUM_FMT获得。没有外层枚举,你无法知道要查哪种格式的分辨率。因此,它必须嵌套在格式循环内部。
问题4:分辨率的“离散”和“连续”是什么意思?分别怎么处理?
答:
- 离散:摄像头只支持固定的几个分辨率,如 640x480、1280x720。直接读取
fsenum.discrete.width/height。 - 连续:摄像头支持一段范围内的任何分辨率,并给出步长。例如最小 16x16,最大 1920x1080,步长 8x8。应用程序可以选择任意满足步长倍数的分辨率。此类摄像头极少见,通常出现在工业领域。
问题5:如果枚举过程中 ioctl 返回 -1 且errno不是EINVAL,代表什么?
答:真正的硬件错误或驱动异常。例如EIO(I/O 错误)、ENOMEM(内存不足)。在这种情况下,不应继续循环,应打印错误并退出。
九、面试官提问环节(必背)
第1问:V4L2 中如何枚举摄像头支持的像素格式?请说明使用的 ioctl 和关键数据结构。
参考答案:
使用VIDIOC_ENUM_FMT命令,配合结构体struct v4l2_fmtdesc。设置fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE,然后从index = 0开始递增调用ioctl,直到返回 -1。每次成功时,fmtdesc.pixelformat是格式的 FourCC 码,fmtdesc.description是可读字符串。
第2问:在枚举完一种格式后,如何知道该格式支持哪些分辨率?需要用到什么 ioctl?为什么这个 ioctl 必须放在格式枚举的循环内部?
参考答案:
使用VIDIOC_ENUM_FRAMESIZES,结构体为struct v4l2_frmsizeenum。必须传入fsenum.pixel_format(即从格式枚举得到的 FourCC),所以必须在格式枚举的内层循环中调用。内核根据该 FourCC 返回该格式支持的分辨率列表。
第3问:v4l2_frmsizeenum结构体中的type字段有哪些取值?分别代表什么?
参考答案:
V4L2_FRMSIZE_TYPE_DISCRETE:摄像头支持离散的固定分辨率,分辨率信息在fsenum.discrete中。V4L2_FRMSIZE_TYPE_STEPWISE:摄像头支持连续范围分辨率,信息在fsenum.stepwise中,包括最小、最大和步长。
绝大多数消费级摄像头都是前一种。
第4问:枚举过程中,ioctl返回 -1 是否一定表示错误?如何正确判断枚举结束?
参考答案:
不是。当index超过最后一个条目时,ioctl返回 -1 并且errno被设置为EINVAL,这是正常的枚举结束。真正的错误应检查errno是否为其他值(如EIO、ENOMEM)。简单示例中通常只判断返回值 < 0 就退出,严谨的程序应区分EINVAL和真实错误。
第5问:为什么要先用memset清零 V4L2 结构体?不清零会有什么后果?
参考答案:
V4L2 结构体中有reserved保留字段,内核要求这些字段必须为 0。如果不清零,栈上的随机值会被内核读取,可能被解释为尚未定义的扩展参数,导致 ioctl 失败或产生不可预知的行为。清零是一种防御性编程,确保兼容性。
第6问:请画出或描述枚举所有格式及分辨率的程序流程图。
参考答案:
text
开始 │ ├─ 打开 /dev/videoX │ ├─ fmt_index = 0 │ ├─ while (1) { │ └─ 调用 ioctl( VIDIOC_ENUM_FMT ) │ ├─ 失败 → 跳出外层循环 │ └─ 成功 → 打印格式信息 │ frame_index = 0 │ while (1) { │ 调用 ioctl( VIDIOC_ENUM_FRAMESIZES ) │ ├─ 失败 → 跳出内层循环 │ └─ 成功 → 打印分辨率 │ frame_index++ │ } │ fmt_index++ │ } │ └─ 关闭设备十、总结
这一节我们完成了一个非常实用的 V4L2 程序,它不采集图像,却能帮你彻底摸清摄像头的“底细”。在真实项目中,你会经常先运行类似程序来确认摄像头能力,然后再写采集代码。
你必须掌握的核心:
VIDIOC_ENUM_FMT和VIDIOC_ENUM_FRAMESIZES的用法。- 交叉编译的完整流程(设置 PATH、编译、file 验证)。
- ADB 部署与运行。
下次当你拿到一个新的摄像头时,别忘了先运行video_enum /dev/videoX,看看它到底支持什么。
🚀 你已经迈出了 V4L2 应用开发的一大步!下一个目标:写一个可以实时显示摄像头画面的程序(结合 LCD 显示)。
