非常开心!!!最后一个模块啦!!!!
V4L2摄像头编程完全指南
目录
一、V4L2框架概述
1.1 什么是V4L2?
V4L2 (Video for Linux 2) 是Linux内核提供的视频设备驱动框架。
用户空间应用
↓ (open/ioctl/mmap/read/poll)
─────────────────────────────────
V4L2核心层 (videodev.c)
↓ (v4l2_ioctl_ops)
─────────────────────────────────
USB摄像头驱动 (uvcvideo.ko)
↓ (USB URB)
─────────────────────────────────
USB总线层
↓
─────────────────────────────────
摄像头硬件 (UVC设备)
1.2 设备节点
# 摄像头设备节点
/dev/video0 # 第一个摄像头
/dev/video1 # 第二个摄像头
# 查看设备信息
ls -l /dev/video*
v4l2-ctl --list-devices
v4l2-ctl -d /dev/video0 --all
1.3 V4L2编程的核心概念
| 概念 | 说明 | 目的 |
|---|---|---|
| 设备能力 | 摄像头支持的功能 | 查询设备是否支持视频采集 |
| 格式协商 | 像素格式、分辨率、帧率 | 设置期望的视频参数 |
| 缓冲区管理 | 用户态/内核态数据交换 | 高效传输视频帧数据 |
| 流控制 | 启动/停止视频流 | 控制数据采集的时机 |
二、核心数据结构详解
2.1 v4l2_capability – 设备能力结构体
作用:描述摄像头设备的基本信息和支持的功能。
使用时机:程序启动时,通过 VIDIOC_QUERYCAP 查询设备能力。
struct v4l2_capability {
__u8 driver[16]; // 驱动名称,如 "uvcvideo"
__u8 card[32]; // 设备名称,如 "USB Camera"
__u8 bus_info[32]; // 总线信息,如 "usb-0000:00:1d.7-2"
__u32 version; // 驱动版本号
__u32 capabilities; // 设备能力标志位(重要!)
__u32 device_caps; // 当前设备节点的能力
__u32 reserved[3]; // 保留字段
};
capabilities 常用标志位:
V4L2_CAP_VIDEO_CAPTURE // 支持视频采集(必须)
V4L2_CAP_VIDEO_OUTPUT // 支持视频输出
V4L2_CAP_VIDEO_OVERLAY // 支持视频叠加
V4L2_CAP_STREAMING // 支持流式I/O(内存映射/用户指针)
V4L2_CAP_READWRITE // 支持read()/write()
示例代码:
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
perror("查询设备能力失败");
return -1;
}
// 检查是否支持视频采集
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "设备不支持视频采集\n");
return -1;
}
// 检查是否支持流式I/O
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "设备不支持流式I/O\n");
return -1;
}
printf("驱动: %s\n", cap.driver);
printf("设备: %s\n", cap.card);
2.2 v4l2_format – 视频格式结构体
作用:描述视频数据的格式(像素格式、分辨率、字节序)。
使用时机:设置或查询摄像头的视频格式。
struct v4l2_format {
__u32 type; // 缓冲区类型(必须设置)
union {
struct v4l2_pix_format pix; // 像素格式(视频采集用这个)
struct v4l2_window win; // 视频叠加
struct v4l2_vbi_format vbi; // VBI数据
struct v4l2_sliced_vbi_format sliced;
__u8 raw_data[200];
} fmt;
};
type 字段:
V4L2_BUF_TYPE_VIDEO_CAPTURE // 视频采集(最常用)
V4L2_BUF_TYPE_VIDEO_OUTPUT // 视频输出
V4L2_BUF_TYPE_VIDEO_OVERLAY // 视频叠加
v4l2_pix_format 子结构体(最重要):
struct v4l2_pix_format {
__u32 width; // 图像宽度(像素)
__u32 height; // 图像高度(像素)
__u32 pixelformat; // 像素格式(fourcc代码)
__u32 field; // 场模式(隔行/逐行)
__u32 bytesperline; // 每行字节数(stride)
__u32 sizeimage; // 图像总字节数
__u32 colorspace; // 色彩空间
__u32 priv; // 私有数据
};
常用像素格式:
V4L2_PIX_FMT_YUYV // YUV 4:2:2(最常见)
V4L2_PIX_FMT_MJPEG // Motion JPEG(压缩格式)
V4L2_PIX_FMT_H264 // H.264视频流
V4L2_PIX_FMT_RGB24 // RGB888
V4L2_PIX_FMT_BGR24 // BGR888
示例代码:
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
// 1. 设置类型
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
// 2. 设置分辨率和格式
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
// 3. 设置格式(驱动会调整为支持的值)
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
perror("设置格式失败");
return -1;
}
// 4. 读取实际设置的值
printf("实际格式: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
printf("每帧大小: %d 字节\n", fmt.fmt.pix.sizeimage);
2.3 v4l2_requestbuffers – 缓冲区请求结构体
作用:向驱动申请视频缓冲区。
使用时机:设置完格式后,申请内核缓冲区用于存储视频帧。
struct v4l2_requestbuffers {
__u32 count; // 请求的缓冲区数量(输入/输出)
__u32 type; // 缓冲区类型
__u32 memory; // 内存类型(重要!)
__u32 reserved[2];
};
memory 字段(内存映射方式):
V4L2_MEMORY_MMAP // 内存映射(最常用,性能好)
V4L2_MEMORY_USERPTR // 用户指针(应用提供内存)
V4L2_MEMORY_DMABUF // DMA缓冲区(零拷贝)
为什么需要多个缓冲区?
缓冲区数量 说明
───────────────────────────────────────
1个 ✗ 容易丢帧(驱动填充时,应用无法读取)
2个 ○ 最小配置(ping-pong缓冲)
4个 ✓ 推荐(驱动填充、应用处理可并行)
8个+ ✓ 高帧率场景(更平滑)
示例代码:
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4; // 请求4个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP; // 使用内存映射
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
perror("申请缓冲区失败");
return -1;
}
// 驱动可能调整数量
if (req.count < 2) {
fprintf(stderr, "缓冲区不足\n");
return -1;
}
printf("实际分配 %d 个缓冲区\n", req.count);
2.4 v4l2_buffer – 缓冲区操作结构体
作用:查询缓冲区信息、入队、出队操作(核心结构体)。
使用时机:整个视频流采集过程中频繁使用。
struct v4l2_buffer {
__u32 index; // 缓冲区索引(0, 1, 2, ...)
__u32 type; // 缓冲区类型
__u32 bytesused; // 实际使用的字节数(驱动填充)
__u32 flags; // 缓冲区状态标志
__u32 field; // 场模式
struct timeval timestamp;// 时间戳(帧捕获时间)
struct v4l2_timecode timecode;
__u32 sequence; // 帧序号(递增)
__u32 memory; // 内存类型
union {
__u32 offset; // 内存映射时的偏移(重要!)
unsigned long userptr;
struct v4l2_plane *planes;
__s32 fd;
} m;
__u32 length; // 缓冲区大小(字节)
__u32 reserved2;
__u32 reserved;
};
flags 字段(缓冲区状态):
V4L2_BUF_FLAG_MAPPED // 缓冲区已映射到用户空间
V4L2_BUF_FLAG_QUEUED // 缓冲区已入队(等待驱动填充)
V4L2_BUF_FLAG_DONE // 缓冲区已填充完成(可以读取)
V4L2_BUF_FLAG_ERROR // 传输错误
V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC // 时间戳类型
缓冲区状态转换:
IDLE(空闲)
↓ VIDIOC_QBUF(入队)
QUEUED(已入队,驱动持有)
↓ 硬件采集完成
DONE(已完成,可读取)
↓ VIDIOC_DQBUF(出队)
IDLE(应用处理)
↓ 处理完成后再次入队
QUEUED ...
示例代码:
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0; // 查询第0个缓冲区
// 1. 查询缓冲区信息
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
perror("查询缓冲区失败");
return -1;
}
// 2. 映射到用户空间
void *buffer_start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, buf.m.offset); // ← 使用offset字段
// 3. 入队(让驱动填充)
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
perror("入队失败");
return -1;
}
2.5 其他重要结构体
2.5.1 v4l2_fmtdesc – 格式枚举结构体
作用:用于枚举设备支持的所有像素格式,查询”设备能提供哪些格式”。
使用时机:在设置格式之前,先枚举设备支持的格式列表,选择合适的格式。
结构体定义:
struct v4l2_fmtdesc {
__u32 index; // 【输入】格式索引,从0开始递增枚举
__u32 type; // 【输入】缓冲区类型(V4L2_BUF_TYPE_VIDEO_CAPTURE)
__u32 flags; // 【输出】格式标志位
__u8 description[32]; // 【输出】格式的人类可读描述,如"Motion-JPEG"
__u32 pixelformat; // 【输出】像素格式的fourcc代码(4字节)
__u32 reserved[4]; // 保留字段
};
成员详解:
| 成员 | 类型 | 方向 | 说明 |
|---|---|---|---|
index | __u32 | 输入 | 格式索引,从0开始递增。每次调用前设置,失败返回-EINVAL表示枚举结束 |
type | __u32 | 输入 | 缓冲区类型,通常是V4L2_BUF_TYPE_VIDEO_CAPTURE |
flags | __u32 | 输出 | 格式标志: V4L2_FMT_FLAG_COMPRESSED (0x0001) – 压缩格式(MJPEG/H264等) V4L2_FMT_FLAG_EMULATED (0x0002) – 驱动模拟的格式 |
description | __u8[32] | 输出 | 格式的可读描述字符串,如”YUYV 4:2:2″、”Motion-JPEG” |
pixelformat | __u32 | 输出 | 像素格式的fourcc代码(4个ASCII字符),如V4L2_PIX_FMT_YUYV、V4L2_PIX_FMT_MJPEG |
ioctl命令:VIDIOC_ENUM_FMT
典型用法:
/*
* 枚举所有支持的像素格式
*/
void enumerate_formats(int fd)
{
struct v4l2_fmtdesc fmtdesc;
printf("支持的像素格式:\n");
printf("================================================\n");
memset(&fmtdesc, 0, sizeof(fmtdesc));
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmtdesc.index = 0;
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
/* 打印fourcc(4字节转为字符串) */
printf("[%d] %s\n", fmtdesc.index, fmtdesc.description);
printf(" Fourcc: %.4s (0x%08x)\n",
(char *)&fmtdesc.pixelformat,
fmtdesc.pixelformat);
/* 标志 */
if (fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) {
printf(" [压缩格式]\n");
}
if (fmtdesc.flags & V4L2_FMT_FLAG_EMULATED) {
printf(" [模拟格式]\n");
}
fmtdesc.index++;
}
printf("================================================\n");
}
/*
* 检查设备是否支持特定格式
*/
int is_format_supported(int fd, __u32 pixelformat)
{
struct v4l2_fmtdesc fmtdesc;
memset(&fmtdesc, 0, sizeof(fmtdesc));
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmtdesc.index = 0;
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
if (fmtdesc.pixelformat == pixelformat) {
printf("支持格式: %s\n", fmtdesc.description);
return 1;
}
fmtdesc.index++;
}
return 0;
}
/*
* 使用示例
*/
int main(int argc, char **argv)
{
int fd = open("/dev/video0", O_RDWR);
/* 枚举所有格式 */
enumerate_formats(fd);
/* 检查是否支持MJPEG */
if (is_format_supported(fd, V4L2_PIX_FMT_MJPEG)) {
printf("✓ 支持MJPEG格式\n");
} else {
printf("✗ 不支持MJPEG格式\n");
}
close(fd);
return 0;
}
输出示例:
支持的像素格式:
================================================
[0] Motion-JPEG
Fourcc: MJPG (0x47504a4d)
[压缩格式]
[1] YUYV 4:2:2
Fourcc: YUYV (0x56595559)
[2] NV12
Fourcc: NV12 (0x3231564e)
================================================
v4l2_fmtdesc vs v4l2_pix_format 的区别:
| 维度 | v4l2_fmtdesc | v4l2_pix_format |
|---|---|---|
| 用途 | 枚举设备支持的格式列表 | 设置/获取当前正在使用的格式 |
| ioctl | VIDIOC_ENUM_FMT | VIDIOC_S_FMT / VIDIOC_G_FMT |
| 时机 | 设置格式之前,查询”能提供什么” | 设置格式时,指定”要使用什么” |
| 信息 | 格式名称、fourcc、标志(压缩/模拟) | 完整格式参数:宽高、像素格式、字节数、颜色空间等 |
| 方向 | 只读(查询设备能力) | 读写(协商格式) |
| 迭代 | 通过递增index枚举多个格式 | 不迭代,只操作一个当前格式 |
类比理解:
v4l2_fmtdesc(枚举) v4l2_pix_format(设置)
═══════════════════════════════════════════════════════════
餐厅菜单 点菜单
"我们提供哪些菜品?" "我要这道菜,大份"
- 查询能力 - 协商参数
- 只读 - 读写
- 列表(多个格式) - 单个(当前格式)
- 格式名称+fourcc - 完整参数(宽高、字节数等)
流程:
1. VIDIOC_ENUM_FMT "有什么格式?"
→ v4l2_fmtdesc → MJPEG、YUYV、NV12...
2. 选择一个格式
3. VIDIOC_S_FMT "用MJPEG,640x480"
→ v4l2_format → 设置完整参数
└─ v4l2_pix_format
├─ width = 640
├─ height = 480
├─ pixelformat = MJPEG
└─ ...
核心区别总结:
- v4l2_fmtdesc:回答”设备支持哪些格式”(能力查询)
- v4l2_pix_format:指定”正在使用什么格式”(参数设置)
完整工作流程:
/* 步骤1: 枚举格式(v4l2_fmtdesc) */
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
printf("支持: %s (%.4s)\n",
fmtdesc.description,
(char *)&fmtdesc.pixelformat);
if (fmtdesc.pixelformat == V4L2_PIX_FMT_MJPEG) {
printf("✓ 找到MJPEG格式\n");
break;
}
fmtdesc.index++;
}
/* 步骤2: 设置格式(v4l2_pix_format) */
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; // ← 使用枚举到的格式
fmt.fmt.pix.field = V4L2_FIELD_NONE;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
perror("设置格式失败");
return -1;
}
printf("已设置格式: %dx%d, MJPEG\n",
fmt.fmt.pix.width,
fmt.fmt.pix.height);
2.5.2 v4l2_frmsizeenum – 帧尺寸枚举结构体
作用:枚举特定像素格式支持的所有分辨率。
使用时机:选定像素格式后,查询该格式支持哪些分辨率。
结构体定义:
struct v4l2_frmsizeenum {
__u32 index; // 【输入】分辨率索引,从0开始
__u32 pixel_format; // 【输入】像素格式(fourcc)
__u32 type; // 【输出】分辨率类型
union { // 【输出】分辨率值
struct v4l2_frmsize_discrete discrete; // 离散值
struct v4l2_frmsize_stepwise stepwise; // 范围+步进
};
__u32 reserved[2];
};
/* 离散分辨率 */
struct v4l2_frmsize_discrete {
__u32 width; // 宽度(像素)
__u32 height; // 高度(像素)
};
/* 范围分辨率 */
struct v4l2_frmsize_stepwise {
__u32 min_width; // 最小宽度
__u32 max_width; // 最大宽度
__u32 step_width; // 宽度步进
__u32 min_height; // 最小高度
__u32 max_height; // 最大高度
__u32 step_height; // 高度步进
};
/* 分辨率类型 */
enum v4l2_frmsizetypes {
V4L2_FRMSIZE_TYPE_DISCRETE = 1, // 离散值(如640x480, 1280x720)
V4L2_FRMSIZE_TYPE_CONTINUOUS = 2, // 连续范围(任意宽高)
V4L2_FRMSIZE_TYPE_STEPWISE = 3, // 步进范围(有最小/最大/步进限制)
};
ioctl命令:VIDIOC_ENUM_FRAMESIZES
典型用法:
/*
* 枚举特定格式支持的所有分辨率
*/
void enumerate_frame_sizes(int fd, __u32 pixelformat)
{
struct v4l2_frmsizeenum frmsize;
printf("\n格式 %.4s 支持的分辨率:\n", (char *)&pixelformat);
printf("================================================\n");
memset(&frmsize, 0, sizeof(frmsize));
frmsize.pixel_format = pixelformat;
frmsize.index = 0;
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) {
if (frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
/* 离散分辨率 */
printf(" [%d] %dx%d\n",
frmsize.index,
frmsize.discrete.width,
frmsize.discrete.height);
} else if (frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
/* 范围分辨率 */
printf(" [%d] 范围:\n", frmsize.index);
printf(" 宽度: %d ~ %d (步进: %d)\n",
frmsize.stepwise.min_width,
frmsize.stepwise.max_width,
frmsize.stepwise.step_width);
printf(" 高度: %d ~ %d (步进: %d)\n",
frmsize.stepwise.min_height,
frmsize.stepwise.max_height,
frmsize.stepwise.step_height);
}
frmsize.index++;
}
printf("================================================\n");
}
/* 使用示例 */
enumerate_frame_sizes(fd, V4L2_PIX_FMT_MJPEG);
enumerate_frame_sizes(fd, V4L2_PIX_FMT_YUYV);
输出示例:
格式 MJPG 支持的分辨率:
================================================
[0] 640x480
[1] 800x600
[2] 1280x720
[3] 1920x1080
================================================
格式 YUYV 支持的分辨率:
================================================
[0] 范围:
宽度: 160 ~ 1920 (步进: 2)
高度: 120 ~ 1080 (步进: 2)
================================================
2.5.3 v4l2_streamparm – 流参数结构体(帧率)
作用:设置和查询视频流的参数,最常用的是设置帧率。
使用时机:设置完格式后,进一步调整帧率。
结构体定义:
struct v4l2_streamparm {
__u32 type; // 缓冲区类型
union {
struct v4l2_captureparm capture; // 采集参数
struct v4l2_outputparm output; // 输出参数
__u8 raw_data[200];
} parm;
};
struct v4l2_captureparm {
__u32 capability; // 能力标志
__u32 capturemode; // 采集模式
struct v4l2_fract timeperframe; // 【关键】帧间隔
__u32 extendedmode; // 扩展模式
__u32 readbuffers; // 读缓冲区数量
__u32 reserved[4];
};
struct v4l2_fract {
__u32 numerator; // 分子
__u32 denominator; // 分母
};
成员详解:
| 成员 | 说明 |
|---|---|
capability | 能力标志: V4L2_CAP_TIMEPERFRAME – 支持设置帧率 |
capturemode | 采集模式: V4L2_MODE_HIGHQUALITY – 高质量模式 通常设为0(默认模式) |
timeperframe | 帧间隔时间(单位:秒) 例如30fps → timeperframe = 1/30 → numerator=1, denominator=30 例如15fps → timeperframe = 1/15 → numerator=1, denominator=15 |
ioctl命令:
VIDIOC_G_PARM– 获取流参数VIDIOC_S_PARM– 设置流参数
典型用法:
/*
* 设置帧率
*/
int set_frame_rate(int fd, int fps)
{
struct v4l2_streamparm parm;
memset(&parm, 0, sizeof(parm));
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/* 先获取当前参数 */
if (ioctl(fd, VIDIOC_G_PARM, &parm) == -1) {
perror("VIDIOC_G_PARM");
return -1;
}
/* 检查是否支持设置帧率 */
if (!(parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME)) {
printf("设备不支持设置帧率\n");
return -1;
}
/* 设置帧间隔(fps → timeperframe) */
parm.parm.capture.timeperframe.numerator = 1;
parm.parm.capture.timeperframe.denominator = fps;
if (ioctl(fd, VIDIOC_S_PARM, &parm) == -1) {
perror("VIDIOC_S_PARM");
return -1;
}
/* 回读实际设置的帧率(驱动可能调整) */
printf("请求帧率: %d fps\n", fps);
printf("实际帧率: %d/%d = %.2f fps\n",
parm.parm.capture.timeperframe.denominator,
parm.parm.capture.timeperframe.numerator,
(float)parm.parm.capture.timeperframe.denominator /
parm.parm.capture.timeperframe.numerator);
return 0;
}
/*
* 获取当前帧率
*/
float get_frame_rate(int fd)
{
struct v4l2_streamparm parm;
memset(&parm, 0, sizeof(parm));
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_G_PARM, &parm) == -1) {
perror("VIDIOC_G_PARM");
return -1.0;
}
if (parm.parm.capture.timeperframe.numerator == 0) {
return 0.0;
}
return (float)parm.parm.capture.timeperframe.denominator /
parm.parm.capture.timeperframe.numerator;
}
/* 使用示例 */
int main()
{
int fd = open("/dev/video0", O_RDWR);
/* 查询当前帧率 */
float current_fps = get_frame_rate(fd);
printf("当前帧率: %.2f fps\n", current_fps);
/* 设置为30fps */
set_frame_rate(fd, 30);
/* 设置为15fps */
set_frame_rate(fd, 15);
close(fd);
return 0;
}
2.5.4 v4l2_control – 控制参数结构体
作用:调整摄像头的控制参数(亮度、对比度、曝光等)。
使用时机:需要调整图像质量或摄像头行为时。
结构体定义:
struct v4l2_control {
__u32 id; // 控制参数ID
__s32 value; // 控制参数值
};
ioctl命令:
VIDIOC_G_CTRL– 获取控制参数值VIDIOC_S_CTRL– 设置控制参数值
常用控制ID:
/* 图像调整 */
V4L2_CID_BRIGHTNESS // 亮度(0-255)
V4L2_CID_CONTRAST // 对比度(0-255)
V4L2_CID_SATURATION // 饱和度(0-255)
V4L2_CID_HUE // 色调(-128~127)
V4L2_CID_SHARPNESS // 锐度(0-255)
V4L2_CID_GAMMA // 伽马(100-300)
/* 曝光控制 */
V4L2_CID_EXPOSURE_AUTO // 自动曝光模式
V4L2_CID_EXPOSURE_ABSOLUTE // 曝光时间(100μs单位)
V4L2_CID_GAIN // 增益(0-255)
V4L2_CID_AUTOGAIN // 自动增益
/* 白平衡 */
V4L2_CID_AUTO_WHITE_BALANCE // 自动白平衡(0/1)
V4L2_CID_WHITE_BALANCE_TEMPERATURE // 色温(2800-6500K)
/* 对焦 */
V4L2_CID_FOCUS_AUTO // 自动对焦(0/1)
V4L2_CID_FOCUS_ABSOLUTE // 绝对对焦位置
典型用法:
/*
* 设置亮度
*/
int set_brightness(int fd, int value)
{
struct v4l2_control ctrl;
ctrl.id = V4L2_CID_BRIGHTNESS;
ctrl.value = value; // 0-255
if (ioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) {
perror("设置亮度失败");
return -1;
}
printf("亮度已设置为: %d\n", value);
return 0;
}
/*
* 获取当前亮度
*/
int get_brightness(int fd)
{
struct v4l2_control ctrl;
ctrl.id = V4L2_CID_BRIGHTNESS;
if (ioctl(fd, VIDIOC_G_CTRL, &ctrl) == -1) {
perror("获取亮度失败");
return -1;
}
printf("当前亮度: %d\n", ctrl.value);
return ctrl.value;
}
/* 使用示例 */
set_brightness(fd, 150); // 提高亮度
set_contrast(fd, 160); // 提高对比度
set_saturation(fd, 140); // 提高饱和度
注意事项:
- 不是所有摄像头都支持所有控制参数,设置前应先用
VIDIOC_QUERYCTRL查询 - 参数范围因设备而异,需要查询
minimum、maximum、step - 详细的控制参数说明参见《V4L2摄像头控制参数详解.md》
2.5.5 v4l2_frmivalenum – 帧间隔枚举结构体
作用:枚举特定格式和分辨率下支持的帧率(帧间隔)。
使用时机:选定格式和分辨率后,查询支持的帧率列表。
结构体定义:
struct v4l2_frmivalenum {
__u32 index; // 【输入】帧间隔索引,从0开始
__u32 pixel_format; // 【输入】像素格式(fourcc)
__u32 width; // 【输入】帧宽度
__u32 height; // 【输入】帧高度
__u32 type; // 【输出】帧间隔类型
union { // 【输出】帧间隔值
struct v4l2_fract discrete; // 离散值
struct v4l2_frmival_stepwise stepwise; // 范围+步进
};
__u32 reserved[2];
};
/* 帧间隔类型 */
enum v4l2_frmivaltypes {
V4L2_FRMIVAL_TYPE_DISCRETE = 1, // 离散帧率(如30fps, 15fps)
V4L2_FRMIVAL_TYPE_CONTINUOUS = 2, // 连续范围
V4L2_FRMIVAL_TYPE_STEPWISE = 3, // 步进范围
};
/* 步进范围 */
struct v4l2_frmival_stepwise {
struct v4l2_fract min; // 最小帧间隔(最高帧率)
struct v4l2_fract max; // 最大帧间隔(最低帧率)
struct v4l2_fract step; // 步进值
};
帧间隔与帧率的关系:
帧间隔(秒) = 1 / 帧率(fps)
例如:
30fps → 帧间隔 = 1/30秒 → numerator=1, denominator=30
60fps → 帧间隔 = 1/60秒 → numerator=1, denominator=60
15fps → 帧间隔 = 1/15秒 → numerator=1, denominator=15
7.5fps → 帧间隔 = 2/15秒 → numerator=2, denominator=15
ioctl命令:VIDIOC_ENUM_FRAMEINTERVALS
典型用法:
/*
* 枚举特定格式和分辨率下支持的帧率
*/
void enumerate_frame_intervals(int fd, __u32 pixelformat,
__u32 width, __u32 height)
{
struct v4l2_frmivalenum frmival;
printf("\n格式 %.4s @ %dx%d 支持的帧率:\n",
(char *)&pixelformat, width, height);
printf("================================================\n");
memset(&frmival, 0, sizeof(frmival));
frmival.pixel_format = pixelformat;
frmival.width = width;
frmival.height = height;
frmival.index = 0;
while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival) == 0) {
if (frmival.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
/* 离散帧率 */
float fps = (float)frmival.discrete.denominator /
frmival.discrete.numerator;
printf(" [%d] %.2f fps (%d/%d)\n",
frmival.index, fps,
frmival.discrete.denominator,
frmival.discrete.numerator);
} else if (frmival.type == V4L2_FRMIVAL_TYPE_STEPWISE) {
/* 范围帧率 */
float min_fps = (float)frmival.stepwise.min.denominator /
frmival.stepwise.min.numerator;
float max_fps = (float)frmival.stepwise.max.denominator /
frmival.stepwise.max.numerator;
printf(" [%d] 范围: %.2f ~ %.2f fps\n",
frmival.index, max_fps, min_fps);
}
frmival.index++;
}
printf("================================================\n");
}
/* 使用示例 */
enumerate_frame_intervals(fd, V4L2_PIX_FMT_MJPEG, 1920, 1080);
enumerate_frame_intervals(fd, V4L2_PIX_FMT_YUYV, 640, 480);
输出示例:
格式 MJPG @ 1920x1080 支持的帧率:
================================================
[0] 30.00 fps (30/1)
[1] 15.00 fps (15/1)
[2] 7.50 fps (15/2)
================================================
格式 YUYV @ 640x480 支持的帧率:
================================================
[0] 范围: 30.00 ~ 5.00 fps
================================================
2.5.6 v4l2_cropcap – 裁剪能力结构体
作用:查询设备的裁剪能力和默认裁剪区域。
使用时机:需要裁剪视频画面时,先查询设备支持的裁剪区域。
结构体定义:
struct v4l2_cropcap {
__u32 type; // 缓冲区类型
struct v4l2_rect bounds; // 最大可裁剪边界
struct v4l2_rect defrect;// 默认裁剪区域
struct v4l2_fract pixelaspect; // 像素宽高比
};
struct v4l2_rect {
__s32 left; // 左上角X坐标
__s32 top; // 左上角Y坐标
__u32 width; // 宽度
__u32 height; // 高度
};
ioctl命令:VIDIOC_CROPCAP
典型用法:
/*
* 查询裁剪能力
*/
void query_crop_capability(int fd)
{
struct v4l2_cropcap cropcap;
memset(&cropcap, 0, sizeof(cropcap));
cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_CROPCAP, &cropcap) == -1) {
perror("查询裁剪能力失败");
return;
}
printf("\n裁剪能力:\n");
printf("最大边界: (%d, %d) %dx%d\n",
cropcap.bounds.left,
cropcap.bounds.top,
cropcap.bounds.width,
cropcap.bounds.height);
printf("默认区域: (%d, %d) %dx%d\n",
cropcap.defrect.left,
cropcap.defrect.top,
cropcap.defrect.width,
cropcap.defrect.height);
printf("像素宽高比: %d/%d\n",
cropcap.pixelaspect.numerator,
cropcap.pixelaspect.denominator);
}
/*
* 设置裁剪区域
*/
int set_crop(int fd, int left, int top, int width, int height)
{
struct v4l2_crop crop;
memset(&crop, 0, sizeof(crop));
crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
crop.c.left = left;
crop.c.top = top;
crop.c.width = width;
crop.c.height = height;
if (ioctl(fd, VIDIOC_S_CROP, &crop) == -1) {
perror("设置裁剪失败");
return -1;
}
printf("裁剪区域已设置: (%d, %d) %dx%d\n",
left, top, width, height);
return 0;
}
2.5.7 v4l2_input – 视频输入结构体
作用:枚举和选择视频输入源(用于多输入设备,如视频采集卡)。
结构体定义:
struct v4l2_input {
__u32 index; // 输入索引
__u8 name[32]; // 输入名称
__u32 type; // 输入类型
__u32 audioset; // 关联的音频输入
__u32 tuner; // 调谐器类型
v4l2_std_id std; // 支持的视频标准
__u32 status; // 输入状态
__u32 capabilities; // 能力标志
__u32 reserved[3];
};
/* 输入类型 */
#define V4L2_INPUT_TYPE_TUNER 1 // 调谐器输入
#define V4L2_INPUT_TYPE_CAMERA 2 // 摄像头输入
/* 状态标志 */
#define V4L2_IN_ST_NO_POWER 0x00000001 // 设备无电
#define V4L2_IN_ST_NO_SIGNAL 0x00000002 // 无信号
#define V4L2_IN_ST_NO_COLOR 0x00000004 // 无彩色
ioctl命令:
VIDIOC_ENUMINPUT– 枚举输入VIDIOC_G_INPUT– 获取当前输入VIDIOC_S_INPUT– 设置当前输入
典型用法:
/*
* 枚举所有视频输入
*/
void enumerate_inputs(int fd)
{
struct v4l2_input input;
printf("\n可用的视频输入:\n");
printf("================================================\n");
memset(&input, 0, sizeof(input));
input.index = 0;
while (ioctl(fd, VIDIOC_ENUMINPUT, &input) == 0) {
printf("[%d] %s\n", input.index, input.name);
printf(" 类型: %s\n",
input.type == V4L2_INPUT_TYPE_CAMERA ? "摄像头" : "调谐器");
if (input.status & V4L2_IN_ST_NO_SIGNAL) {
printf(" [无信号]\n");
}
input.index++;
}
printf("================================================\n");
}
/*
* 选择视频输入
*/
int select_input(int fd, int index)
{
if (ioctl(fd, VIDIOC_S_INPUT, &index) == -1) {
perror("选择输入失败");
return -1;
}
printf("已选择输入: %d\n", index);
return 0;
}
2.5.8 v4l2_queryctrl – 控制查询结构体
作用:查询控制参数的详细信息(范围、默认值、标志等)。
结构体定义:
struct v4l2_queryctrl {
__u32 id; // 【输入】控制ID
__u32 type; // 【输出】控制类型
__u8 name[32]; // 【输出】控制名称
__s32 minimum; // 【输出】最小值
__s32 maximum; // 【输出】最大值
__s32 step; // 【输出】步进值
__s32 default_value; // 【输出】默认值
__u32 flags; // 【输出】标志位
__u32 reserved[2];
};
/* 控制类型 */
enum v4l2_ctrl_type {
V4L2_CTRL_TYPE_INTEGER = 1, // 整数
V4L2_CTRL_TYPE_BOOLEAN = 2, // 布尔
V4L2_CTRL_TYPE_MENU = 3, // 菜单
V4L2_CTRL_TYPE_BUTTON = 4, // 按钮
V4L2_CTRL_TYPE_INTEGER64 = 5, // 64位整数
V4L2_CTRL_TYPE_STRING = 7, // 字符串
V4L2_CTRL_TYPE_BITMASK = 8, // 位掩码
};
/* 控制标志 */
#define V4L2_CTRL_FLAG_DISABLED 0x0001 // 禁用
#define V4L2_CTRL_FLAG_GRABBED 0x0002 // 被其它应用占用
#define V4L2_CTRL_FLAG_READ_ONLY 0x0004 // 只读
#define V4L2_CTRL_FLAG_UPDATE 0x0008 // 更改时需刷新
#define V4L2_CTRL_FLAG_INACTIVE 0x0010 // 不活动(依赖未满足)
#define V4L2_CTRL_FLAG_SLIDER 0x0020 // 建议用滑块显示
#define V4L2_CTRL_FLAG_WRITE_ONLY 0x0040 // 只写
#define V4L2_CTRL_FLAG_VOLATILE 0x0080 // 值可能自动变化
ioctl命令:VIDIOC_QUERYCTRL
典型用法:
/*
* 查询控制参数详细信息
*/
void query_control(int fd, __u32 control_id)
{
struct v4l2_queryctrl queryctrl;
memset(&queryctrl, 0, sizeof(queryctrl));
queryctrl.id = control_id;
if (ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == -1) {
if (errno == EINVAL) {
printf("控制不支持\n");
} else {
perror("查询控制失败");
}
return;
}
printf("\n控制: %s (ID: 0x%08x)\n", queryctrl.name, queryctrl.id);
/* 类型 */
printf("类型: ");
switch (queryctrl.type) {
case V4L2_CTRL_TYPE_INTEGER:
printf("整数\n");
printf("范围: %d ~ %d (步进: %d)\n",
queryctrl.minimum,
queryctrl.maximum,
queryctrl.step);
break;
case V4L2_CTRL_TYPE_BOOLEAN:
printf("布尔\n");
break;
case V4L2_CTRL_TYPE_MENU:
printf("菜单\n");
break;
case V4L2_CTRL_TYPE_BUTTON:
printf("按钮\n");
break;
default:
printf("其它\n");
break;
}
printf("默认值: %d\n", queryctrl.default_value);
/* 标志 */
if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) {
printf("[禁用]\n");
}
if (queryctrl.flags & V4L2_CTRL_FLAG_READ_ONLY) {
printf("[只读]\n");
}
if (queryctrl.flags & V4L2_CTRL_FLAG_INACTIVE) {
printf("[不活动]\n");
}
}
/*
* 枚举所有控制参数
*/
void enumerate_all_controls(int fd)
{
struct v4l2_queryctrl queryctrl;
printf("\n所有控制参数:\n");
printf("================================================\n");
memset(&queryctrl, 0, sizeof(queryctrl));
queryctrl.id = V4L2_CTRL_FLAG_NEXT_CTRL;
while (ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == 0) {
if (!(queryctrl.flags & V4L2_CTRL_FLAG_DISABLED)) {
printf("[0x%08x] %s\n", queryctrl.id, queryctrl.name);
}
queryctrl.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
}
printf("================================================\n");
}
/* 使用示例 */
query_control(fd, V4L2_CID_BRIGHTNESS);
query_control(fd, V4L2_CID_CONTRAST);
enumerate_all_controls(fd);
2.5.9 v4l2_querymenu – 菜单查询结构体
作用:查询菜单类型控制参数的可选项。
结构体定义:
struct v4l2_querymenu {
__u32 id; // 【输入】控制ID
__u32 index; // 【输入】菜单项索引
union {
__u8 name[32]; // 【输出】菜单项名称
__s64 value; // 【输出】菜单项值(整数菜单)
};
__u32 reserved;
} __attribute__ ((packed));
ioctl命令:VIDIOC_QUERYMENU
典型用法:
/*
* 查询菜单选项
*/
void query_menu_items(int fd, __u32 control_id)
{
struct v4l2_queryctrl queryctrl;
struct v4l2_querymenu querymenu;
/* 先查询控制信息 */
memset(&queryctrl, 0, sizeof(queryctrl));
queryctrl.id = control_id;
if (ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == -1) {
perror("查询控制失败");
return;
}
if (queryctrl.type != V4L2_CTRL_TYPE_MENU) {
printf("不是菜单类型控制\n");
return;
}
printf("\n%s 的菜单选项:\n", queryctrl.name);
printf("================================================\n");
/* 枚举菜单项 */
memset(&querymenu, 0, sizeof(querymenu));
querymenu.id = control_id;
for (querymenu.index = queryctrl.minimum;
querymenu.index <= queryctrl.maximum;
querymenu.index++) {
if (ioctl(fd, VIDIOC_QUERYMENU, &querymenu) == 0) {
printf(" [%d] %s\n", querymenu.index, querymenu.name);
}
}
printf("默认: %d\n", queryctrl.default_value);
printf("================================================\n");
}
/* 使用示例 */
query_menu_items(fd, V4L2_CID_POWER_LINE_FREQUENCY);
query_menu_items(fd, V4L2_CID_EXPOSURE_AUTO);
输出示例:
Power Line Frequency 的菜单选项:
================================================
[0] Disabled
[1] 50 Hz
[2] 60 Hz
[3] Auto
默认: 1
================================================
2.6 结构体总结对比
| 结构体 | 主要用途 | ioctl命令 | 使用时机 |
|---|---|---|---|
v4l2_capability | 查询设备能力 | VIDIOC_QUERYCAP | 打开设备后第一步 |
v4l2_format | 设置/获取视频格式 | VIDIOC_S_FMT / VIDIOC_G_FMT | 查询能力后设置格式 |
v4l2_requestbuffers | 申请缓冲区 | VIDIOC_REQBUFS | 设置格式后申请缓冲 |
v4l2_buffer | 缓冲区操作 | VIDIOC_QBUF / VIDIOC_DQBUF | 缓冲区入队/出队 |
v4l2_fmtdesc | 枚举支持的格式 | VIDIOC_ENUM_FMT | 设置格式之前查询 |
v4l2_frmsizeenum | 枚举支持的分辨率 | VIDIOC_ENUM_FRAMESIZES | 选定格式后查询分辨率 |
v4l2_frmivalenum | 枚举支持的帧率 | VIDIOC_ENUM_FRAMEINTERVALS | 选定格式和分辨率后查询帧率 |
v4l2_streamparm | 设置/获取流参数(帧率) | VIDIOC_S_PARM / VIDIOC_G_PARM | 设置格式后调整帧率 |
v4l2_control | 设置/获取控制参数 | VIDIOC_S_CTRL / VIDIOC_G_CTRL | 需要调整图像质量时 |
v4l2_queryctrl | 查询控制参数信息 | VIDIOC_QUERYCTRL | 设置控制参数之前 |
v4l2_querymenu | 查询菜单选项 | VIDIOC_QUERYMENU | 查询菜单类型控制 |
v4l2_cropcap | 查询裁剪能力 | VIDIOC_CROPCAP | 需要裁剪时 |
v4l2_input | 枚举/选择视频输入 | VIDIOC_ENUMINPUT / VIDIOC_S_INPUT | 多输入设备选择输入源 |
查询型结构体(设置前先查询):
v4l2_fmtdesc → "有什么格式?"
v4l2_frmsizeenum → "这个格式有什么分辨率?"
v4l2_frmivalenum → "这个分辨率有什么帧率?"
v4l2_queryctrl → "这个参数范围是多少?"
v4l2_querymenu → "这个菜单有什么选项?"
v4l2_cropcap → "可以裁剪多大区域?"
v4l2_input → "有哪些输入源?"
设置型结构体(实际使用的参数):
v4l2_format → "用这个格式"
v4l2_streamparm → "用这个帧率"
v4l2_control → "用这个参数值"
v4l2_crop → "裁剪这个区域"
典型工作流程:
/* 1. 查询设备能力 */
v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);
/* 2. 枚举格式 → 选择格式 */
v4l2_fmtdesc fmtdesc; // 枚举:"有MJPEG、YUYV..."
// ... 选择MJPEG
/* 3. 枚举分辨率 → 选择分辨率 */
v4l2_frmsizeenum frmsize; // 枚举:"有640x480、1920x1080..."
// ... 选择1920x1080
/* 4. 枚举帧率 → 选择帧率 */
v4l2_frmivalenum frmival; // 枚举:"有30fps、15fps..."
// ... 选择30fps
/* 5. 设置格式 */
v4l2_format fmt;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
ioctl(fd, VIDIOC_S_FMT, &fmt);
/* 6. 设置帧率 */
v4l2_streamparm parm;
parm.parm.capture.timeperframe.numerator = 1;
parm.parm.capture.timeperframe.denominator = 30;
ioctl(fd, VIDIOC_S_PARM, &parm);
/* 7. 查询控制 → 设置控制 */
v4l2_queryctrl queryctrl; // 查询:"亮度范围0-255"
v4l2_control ctrl; // 设置:"亮度=150"
ctrl.id = V4L2_CID_BRIGHTNESS;
ctrl.value = 150;
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
/* 8. 申请缓冲区... */
v4l2_requestbuffers req;
ioctl(fd, VIDIOC_REQBUFS, &req);
3.1 流程图总览
┌─────────────────────────────────────────────────┐
│ 1. 打开设备 │
│ open("/dev/video0", O_RDWR) │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 2. 查询设备能力 │
│ ioctl(VIDIOC_QUERYCAP) │
│ 目的:确认设备支持视频采集和流式I/O │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 3. 设置视频格式 │
│ ioctl(VIDIOC_S_FMT) │
│ 目的:协商分辨率、像素格式、帧率 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 4. 申请缓冲区 │
│ ioctl(VIDIOC_REQBUFS) │
│ 目的:让内核分配视频帧缓冲区 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 5. 查询并映射缓冲区 │
│ for (i = 0; i < buffer_count; i++) { │
│ ioctl(VIDIOC_QUERYBUF) │
│ mmap() // 映射到用户空间 │
│ } │
│ 目的:建立用户态访问内核缓冲区的通道 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 6. 缓冲区入队 │
│ for (i = 0; i < buffer_count; i++) { │
│ ioctl(VIDIOC_QBUF) │
│ } │
│ 目的:将空缓冲区交给驱动,准备接收数据 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 7. 启动视频流 │
│ ioctl(VIDIOC_STREAMON) │
│ 目的:开始采集视频数据 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 8. 循环读取帧 ←─────────────┐ │
│ ioctl(VIDIOC_DQBUF) │ 重复 │
│ 处理帧数据 │ │
│ ioctl(VIDIOC_QBUF) ─────┘ │
│ 目的:持续获取视频帧 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 9. 停止视频流 │
│ ioctl(VIDIOC_STREAMOFF) │
│ 目的:停止采集 │
└──────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 10. 释放资源 │
│ munmap() // 解除映射 │
│ close() // 关闭设备 │
└─────────────────────────────────────────────────┘
3.2 详细步骤解析
步骤1:打开设备
int fd = open("/dev/video0", O_RDWR);
if (fd == -1) {
perror("无法打开摄像头");
return -1;
}
目的:获取设备文件描述符,后续所有操作都通过这个fd进行。
为什么用 O_RDWR?
- V4L2某些ioctl需要读写权限
- 即使只读取视频,也建议用 O_RDWR
步骤2:查询设备能力
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
perror("查询设备能力失败");
close(fd);
return -1;
}
// 检查必要的能力
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "不是视频采集设备\n");
close(fd);
return -1;
}
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "不支持流式I/O\n");
close(fd);
return -1;
}
printf("驱动: %s\n", cap.driver);
printf("设备: %s\n", cap.card);
目的:
- 确认设备是摄像头(而不是输出设备)
- 确认支持流式I/O(性能更好)
- 获取设备基本信息用于调试
为什么要检查能力?
/dev/video*可能是输出设备(如虚拟摄像头)- 某些设备只支持 read/write,不支持 mmap
步骤3:设置视频格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
perror("设置格式失败");
close(fd);
return -1;
}
// 驱动可能调整参数,读取实际值
printf("实际分辨率: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
printf("像素格式: %.4s\n", (char *)&fmt.fmt.pix.pixelformat);
printf("每帧大小: %d 字节\n", fmt.fmt.pix.sizeimage);
目的:
- 告诉驱动期望的分辨率和像素格式
- 驱动协商出实际支持的参数
- 获取每帧数据的大小(用于分配缓冲区)
为什么格式可能被调整?
- 摄像头可能不支持请求的分辨率(如640×480)
- 驱动会选择最接近的支持分辨率(如640×480 → 640×480或800×600)
- 必须使用驱动返回的实际值
常见像素格式选择:
- YUYV:未压缩,质量好,数据量大
- MJPEG:压缩格式,数据量小,需要解码
- H264:高压缩比,适合网络传输
步骤4:申请缓冲区
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4; // 请求4个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
perror("申请缓冲区失败");
close(fd);
return -1;
}
if (req.count < 2) {
fprintf(stderr, "缓冲区不足\n");
close(fd);
return -1;
}
printf("分配了 %d 个缓冲区\n", req.count);
目的:
- 让内核驱动分配视频帧缓冲区
- 驱动根据硬件能力返回实际分配的数量
- 这些缓冲区在内核空间,需要映射到用户空间
为什么需要多个缓冲区?(流水线机制)
单缓冲区(容易丢帧):
时间 → [驱动填充] [应用处理] [驱动填充] ...
↑ 硬件采集新帧但缓冲区被占用 → 丢帧
多缓冲区(流水线):
缓冲区0: [驱动填充] → [应用处理] → [驱动填充] ...
缓冲区1: [驱动填充] → [应用处理] → ...
缓冲区2: [驱动填充] → [应用处理] ...
缓冲区3: [驱动填充] → ...
驱动和应用并行工作,不会相互阻塞
缓冲区数量建议:
- 最少2个(ping-pong缓冲)
- 推荐4个(兼顾性能和内存)
- 高帧率场景可用8个或更多
步骤5:查询并映射缓冲区
struct buffer {
void *start; // 映射后的用户空间地址
size_t length; // 缓冲区大小
};
struct buffer *buffers = calloc(req.count, sizeof(*buffers));
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
// 查询第i个缓冲区的信息
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
perror("查询缓冲区失败");
return -1;
}
// 映射到用户空间
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, // 让内核选择地址
buf.length, // 映射大小
PROT_READ | PROT_WRITE,
MAP_SHARED, // 与内核共享
fd, // 设备文件描述符
buf.m.offset); // 缓冲区偏移
if (buffers[i].start == MAP_FAILED) {
perror("mmap失败");
return -1;
}
printf("缓冲区 %d: 地址=%p, 大小=%zu\n",
i, buffers[i].start, buffers[i].length);
}
目的:
- 查询每个缓冲区在内核中的位置(offset)和大小
- 通过 mmap 将内核缓冲区映射到用户空间
- 应用可以直接访问这块内存,无需数据拷贝
mmap的工作原理:
内核空间 用户空间
──────────────────────────────────────
[缓冲区0] ←──────mmap──────→ buffers[0].start
[缓冲区1] ←──────mmap──────→ buffers[1].start
[缓冲区2] ←──────mmap──────→ buffers[2].start
[缓冲区3] ←──────mmap──────→ buffers[3].start
应用读取 buffers[0].start → 直接读取内核缓冲区
不需要 read() 系统调用,不需要数据拷贝
为什么用 MAP_SHARED?
- MAP_SHARED:与内核共享同一块物理内存
- MAP_PRIVATE:拷贝一份独立内存(不适合视频流)
步骤6:缓冲区入队
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
// 将缓冲区加入驱动队列
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
perror("缓冲区入队失败");
return -1;
}
}
printf("所有缓冲区已入队\n");
目的:
- 将空缓冲区交给驱动
- 驱动会在采集到帧数据后填充这些缓冲区
- 入队后缓冲区归驱动所有,应用不能访问
缓冲区所有权转移:
入队前(IDLE):
应用拥有 → 可以读写
入队后(QUEUED):
驱动拥有 → 应用不能访问
硬件会往这个缓冲区写入视频数据
出队后(DONE):
应用拥有 → 可以读取数据
步骤7:启动视频流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) {
perror("启动视频流失败");
return -1;
}
printf("视频流已启动\n");
目的:
- 通知驱动开始采集视频数据
- 摄像头硬件开始工作
- 驱动会自动填充已入队的缓冲区
启动流后发生了什么?
硬件层:
摄像头传感器开始曝光、采集
通过USB/CSI总线传输数据
驱动层:
USB URB回调接收数据
将数据写入已入队的缓冲区
缓冲区填充完成后标记为 DONE
应用层:
调用 VIDIOC_DQBUF 取出完成的缓冲区
处理视频帧
重新入队供驱动使用
步骤8:循环读取视频帧
while (running) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
// 1. 从驱动队列中取出已填充的缓冲区
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
if (errno == EAGAIN) {
continue; // 非阻塞模式下没有可用帧
}
perror("出队失败");
break;
}
// 2. 处理视频帧
printf("收到第 %d 帧,大小 %d 字节,时间戳 %ld.%06ld\n",
buf.sequence,
buf.bytesused,
buf.timestamp.tv_sec,
buf.timestamp.tv_usec);
// 访问帧数据
unsigned char *frame_data = (unsigned char *)buffers[buf.index].start;
int frame_size = buf.bytesused;
// 这里可以:
// - 保存为图片:save_as_jpeg(frame_data, frame_size);
// - 显示视频:display_frame(frame_data);
// - 编码压缩:encode_h264(frame_data);
// - 网络传输:send_to_network(frame_data);
process_frame(frame_data, frame_size);
// 3. 处理完成后,将缓冲区重新入队
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
perror("重新入队失败");
break;
}
}
目的:
- DQBUF:取出驱动已填充的缓冲区
- 应用处理帧数据(保存/显示/编码)
- QBUF:将缓冲区重新交给驱动
缓冲区循环流转:
时间线 →
缓冲区0: [QBUF入队] → [驱动填充] → [DQBUF出队] → [应用处理] → [QBUF入队] ...
缓冲区1: [QBUF入队] → [驱动填充] → [DQBUF出队] → [应用处理] ...
缓冲区2: [QBUF入队] → [驱动填充] → [DQBUF出队] ...
缓冲区3: [QBUF入队] → [驱动填充] ...
流水线工作:
- 驱动填充缓冲区1时,应用处理缓冲区0
- 不会相互阻塞,帧率更稳定
阻塞 vs 非阻塞模式:
// 阻塞模式(默认)
fd = open("/dev/video0", O_RDWR);
ioctl(fd, VIDIOC_DQBUF, &buf); // 如果没有可用帧,会一直等待
// 非阻塞模式
fd = open("/dev/video0", O_RDWR | O_NONBLOCK);
ioctl(fd, VIDIOC_DQBUF, &buf); // 立即返回,没有帧时返回EAGAIN
使用 select/poll 等待帧就绪:
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = 2;
tv.tv_usec = 0;
int ret = select(fd + 1, &fds, NULL, NULL, &tv);
if (ret > 0) {
// 有帧可读
ioctl(fd, VIDIOC_DQBUF, &buf);
process_frame(...);
ioctl(fd, VIDIOC_QBUF, &buf);
}
步骤9:停止视频流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMOFF, &type) == -1) {
perror("停止视频流失败");
return -1;
}
printf("视频流已停止\n");
目的:
- 通知驱动停止采集
- 摄像头硬件停止工作
- 所有已入队的缓冲区变为空闲状态
停止流后发生了什么?
硬件层:
摄像头传感器停止曝光
USB传输停止
驱动层:
取消所有待处理的URB
清空缓冲区队列
所有缓冲区变为IDLE状态
应用层:
不能再调用DQBUF(会返回错误)
可以解除映射和关闭设备
步骤10:释放资源
// 1. 解除所有缓冲区的映射
for (int i = 0; i < req.count; i++) {
if (munmap(buffers[i].start, buffers[i].length) == -1) {
perror("munmap失败");
}
}
// 2. 释放缓冲区数组
free(buffers);
// 3. 关闭设备文件
close(fd);
printf("资源已释放\n");
目的:
- munmap:解除用户空间到内核空间的映射
- 释放应用层分配的内存
- 关闭设备文件
资源释放顺序:
1. 停止视频流(STREAMOFF)
2. 解除映射(munmap)
3. 释放应用内存(free)
4. 关闭设备(close)
注意:关闭设备时,内核会自动释放缓冲区
四、ioctl命令详解
4.1 设备查询类
| ioctl命令 | 结构体 | 作用 |
|---|---|---|
| VIDIOC_QUERYCAP | v4l2_capability | 查询设备能力 |
| VIDIOC_ENUM_FMT | v4l2_fmtdesc | 枚举支持的像素格式 |
| VIDIOC_ENUM_FRAMESIZES | v4l2_frmsizeenum | 枚举支持的分辨率 |
| VIDIOC_ENUM_FRAMEINTERVALS | v4l2_frmivalenum | 枚举支持的帧率 |
| VIDIOC_QUERYCTRL | v4l2_queryctrl | 查询控制参数范围 |
示例:枚举所有支持的格式
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
printf("支持的像素格式:\n");
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
printf(" [%d] %s (%.4s)\n",
fmtdesc.index,
fmtdesc.description,
(char *)&fmtdesc.pixelformat);
fmtdesc.index++;
}
示例:枚举支持的分辨率
struct v4l2_frmsizeenum frmsize;
frmsize.index = 0;
frmsize.pixel_format = V4L2_PIX_FMT_YUYV;
printf("YUYV格式支持的分辨率:\n");
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) {
if (frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
printf(" %dx%d\n",
frmsize.discrete.width,
frmsize.discrete.height);
}
frmsize.index++;
}
4.2 格式设置类
| ioctl命令 | 结构体 | 作用 |
|---|---|---|
| VIDIOC_G_FMT | v4l2_format | 获取当前格式 |
| VIDIOC_S_FMT | v4l2_format | 设置格式 |
| VIDIOC_TRY_FMT | v4l2_format | 尝试格式(不实际设置) |
| VIDIOC_G_PARM | v4l2_streamparm | 获取流参数(帧率) |
| VIDIOC_S_PARM | v4l2_streamparm | 设置流参数 |
TRY_FMT vs S_FMT:
// TRY_FMT:测试格式是否支持,不实际设置
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
if (ioctl(fd, VIDIOC_TRY_FMT, &fmt) == 0) {
printf("驱动会将格式调整为: %dx%d\n",
fmt.fmt.pix.width, fmt.fmt.pix.height);
// 此时设备格式未改变
}
// S_FMT:实际设置格式
ioctl(fd, VIDIOC_S_FMT, &fmt); // 真正修改设备格式
4.3 缓冲区管理类
| ioctl命令 | 结构体 | 作用 |
|---|---|---|
| VIDIOC_REQBUFS | v4l2_requestbuffers | 申请缓冲区 |
| VIDIOC_QUERYBUF | v4l2_buffer | 查询缓冲区信息 |
| VIDIOC_QBUF | v4l2_buffer | 缓冲区入队 |
| VIDIOC_DQBUF | v4l2_buffer | 缓冲区出队 |
4.4 流控制类
| ioctl命令 | 参数 | 作用 |
|---|---|---|
| VIDIOC_STREAMON | enum v4l2_buf_type | 启动视频流 |
| VIDIOC_STREAMOFF | enum v4l2_buf_type | 停止视频流 |
4.5 控制参数类
| ioctl命令 | 结构体 | 作用 |
|---|---|---|
| VIDIOC_G_CTRL | v4l2_control | 获取控制值 |
| VIDIOC_S_CTRL | v4l2_control | 设置控制值 |
| VIDIOC_G_EXT_CTRLS | v4l2_ext_controls | 批量获取控制 |
| VIDIOC_S_EXT_CTRLS | v4l2_ext_controls | 批量设置控制 |
示例:调整摄像头参数
// 设置亮度
struct v4l2_control ctrl;
ctrl.id = V4L2_CID_BRIGHTNESS;
ctrl.value = 150;
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
// 启用自动曝光
ctrl.id = V4L2_CID_EXPOSURE_AUTO;
ctrl.value = V4L2_EXPOSURE_AUTO;
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
五、内存映射机制
5.1 三种内存管理方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| read/write | 简单易用 | 性能差(数据拷贝) | 简单测试 |
| mmap (内存映射) | 零拷贝,性能好 | 需要手动管理缓冲区 | 推荐,常用 |
| userptr (用户指针) | 应用控制内存 | 需要物理连续内存 | 嵌入式系统 |
| dmabuf (DMA缓冲) | 零拷贝,多模块共享 | 复杂,需要硬件支持 | 高性能场景 |
5.2 read/write方式(不推荐)
// 打开设备
int fd = open("/dev/video0", O_RDWR);
// 设置格式
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
ioctl(fd, VIDIOC_S_FMT, &fmt);
// 直接读取
unsigned char *buffer = malloc(fmt.fmt.pix.sizeimage);
while (running) {
ssize_t size = read(fd, buffer, fmt.fmt.pix.sizeimage);
if (size > 0) {
process_frame(buffer, size);
}
}
free(buffer);
close(fd);
read/write的问题:
内核空间 用户空间
──────────────────────────────────────
[驱动缓冲区]
↓ memcpy (数据拷贝)
────────→ [应用缓冲区]
每帧都需要拷贝,浪费CPU和内存带宽
640x480 YUYV = 614KB/帧,30fps = 18MB/s 拷贝
5.3 mmap方式(推荐)
零拷贝原理:
物理内存
┌──────────────┐
│ 缓冲区物理页 │
└──────────────┘
↑ ↑
│ │
内核映射 用户映射
│ │
┌─────┴─┐ ┌─┴─────┐
│ 驱动 │ │ 应用 │
└───────┘ └───────┘
内核和应用访问同一块物理内存
无数据拷贝,只有虚拟地址映射
完整代码:
struct buffer {
void *start;
size_t length;
};
struct buffer *buffers;
unsigned int n_buffers;
// 1. 申请缓冲区
struct v4l2_requestbuffers req;
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);
// 2. 映射所有缓冲区
buffers = calloc(req.count, sizeof(*buffers));
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, buf.m.offset);
}
// 3. 入队、启动流
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QBUF, &buf);
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
// 4. 循环处理帧
while (running) {
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf);
// 直接访问映射的内存
process_frame(buffers[buf.index].start, buf.bytesused);
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 5. 清理
ioctl(fd, VIDIOC_STREAMOFF, &type);
for (int i = 0; i < n_buffers; i++) {
munmap(buffers[i].start, buffers[i].length);
}
free(buffers);
close(fd);
5.4 userptr方式
应用场景:应用需要自己管理内存(如使用特殊的内存池)。
// 应用自己分配内存
#define BUFFER_COUNT 4
unsigned char *user_buffers[BUFFER_COUNT];
for (int i = 0; i < BUFFER_COUNT; i++) {
user_buffers[i] = malloc(frame_size);
}
// 申请缓冲区(但不分配内存)
struct v4l2_requestbuffers req;
req.count = BUFFER_COUNT;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_USERPTR;
ioctl(fd, VIDIOC_REQBUFS, &req);
// 入队时传递用户指针
for (int i = 0; i < BUFFER_COUNT; i++) {
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_USERPTR;
buf.index = i;
buf.m.userptr = (unsigned long)user_buffers[i];
buf.length = frame_size;
ioctl(fd, VIDIOC_QBUF, &buf);
}
注意:userptr要求内存是物理连续的,普通malloc可能不满足。
六、完整代码示例
6.1 基础版本(采集并保存JPEG)
/*
* v4l2_capture.c - V4L2摄像头采集示例
*
* 功能:
* 1. 打开摄像头设备
* 2. 设置640x480 YUYV格式
* 3. 使用mmap方式采集30帧
* 4. 将每帧转换为JPEG保存
*
* 编译:gcc v4l2_capture.c -o v4l2_capture -ljpeg
* 运行:./v4l2_capture /dev/video0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <jpeglib.h>
/* 缓冲区结构 */
struct buffer {
void *start; // 映射后的用户空间地址
size_t length; // 缓冲区大小
};
/* 全局变量 */
static int fd = -1; // 设备文件描述符
static struct buffer *buffers = NULL; // 缓冲区数组
static unsigned int n_buffers = 0; // 缓冲区数量
/*
* 错误处理宏
*/
#define CLEAR(x) memset(&(x), 0, sizeof(x))
/*
* 打开设备
*
* 目的:获取设备文件描述符
*/
static int open_device(const char *dev_name)
{
fd = open(dev_name, O_RDWR | O_NONBLOCK, 0);
if (fd == -1) {
fprintf(stderr, "无法打开 %s: %s\n", dev_name, strerror(errno));
return -1;
}
printf("成功打开设备: %s (fd=%d)\n", dev_name, fd);
return 0;
}
/*
* 查询设备能力
*
* 目的:确认设备支持视频采集和流式I/O
*/
static int check_capabilities(void)
{
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
fprintf(stderr, "VIDIOC_QUERYCAP 失败: %s\n", strerror(errno));
return -1;
}
printf("驱动: %s\n", cap.driver);
printf("设备: %s\n", cap.card);
printf("总线: %s\n", cap.bus_info);
printf("版本: %u.%u.%u\n",
(cap.version >> 16) & 0xFF,
(cap.version >> 8) & 0xFF,
cap.version & 0xFF);
/* 检查是否支持视频采集 */
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
fprintf(stderr, "设备不支持视频采集\n");
return -1;
}
/* 检查是否支持流式I/O */
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
fprintf(stderr, "设备不支持流式I/O\n");
return -1;
}
printf("设备能力检查通过\n");
return 0;
}
/*
* 设置视频格式
*
* 目的:协商分辨率和像素格式
*/
static int set_format(void)
{
struct v4l2_format fmt;
CLEAR(fmt);
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
fprintf(stderr, "VIDIOC_S_FMT 失败: %s\n", strerror(errno));
return -1;
}
printf("设置格式:\n");
printf(" 分辨率: %dx%d\n", fmt.fmt.pix.width, fmt.fmt.pix.height);
printf(" 像素格式: %.4s\n", (char *)&fmt.fmt.pix.pixelformat);
printf(" 每行字节: %d\n", fmt.fmt.pix.bytesperline);
printf(" 帧大小: %d 字节\n", fmt.fmt.pix.sizeimage);
return 0;
}
/*
* 初始化内存映射
*
* 目的:
* 1. 申请缓冲区
* 2. 查询缓冲区信息
* 3. 映射到用户空间
*/
static int init_mmap(void)
{
struct v4l2_requestbuffers req;
/* 申请缓冲区 */
CLEAR(req);
req.count = 4; // 请求4个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
fprintf(stderr, "VIDIOC_REQBUFS 失败: %s\n", strerror(errno));
return -1;
}
if (req.count < 2) {
fprintf(stderr, "缓冲区不足: %d\n", req.count);
return -1;
}
printf("分配了 %d 个缓冲区\n", req.count);
/* 分配缓冲区数组 */
buffers = calloc(req.count, sizeof(*buffers));
if (!buffers) {
fprintf(stderr, "内存分配失败\n");
return -1;
}
/* 映射每个缓冲区 */
for (n_buffers = 0; n_buffers < req.count; n_buffers++) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
/* 查询缓冲区信息 */
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
fprintf(stderr, "VIDIOC_QUERYBUF 失败: %s\n", strerror(errno));
return -1;
}
buffers[n_buffers].length = buf.length;
/* 映射到用户空间 */
buffers[n_buffers].start = mmap(NULL, // 让内核选择地址
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
buf.m.offset);
if (buffers[n_buffers].start == MAP_FAILED) {
fprintf(stderr, "mmap 失败: %s\n", strerror(errno));
return -1;
}
printf(" 缓冲区 %d: 地址=%p, 大小=%zu\n",
n_buffers, buffers[n_buffers].start, buffers[n_buffers].length);
}
return 0;
}
/*
* 启动视频流
*
* 目的:
* 1. 将所有缓冲区入队
* 2. 启动采集
*/
static int start_capture(void)
{
unsigned int i;
enum v4l2_buf_type type;
/* 将所有缓冲区入队 */
for (i = 0; i < n_buffers; i++) {
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
fprintf(stderr, "VIDIOC_QBUF 失败: %s\n", strerror(errno));
return -1;
}
}
printf("所有缓冲区已入队\n");
/* 启动视频流 */
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) {
fprintf(stderr, "VIDIOC_STREAMON 失败: %s\n", strerror(errno));
return -1;
}
printf("视频流已启动\n");
return 0;
}
/*
* YUYV转RGB (简化版)
*
* YUYV格式:每2个像素占4字节 [Y0 U Y1 V]
* RGB格式:每像素3字节 [R G B]
*/
static void yuyv_to_rgb(const unsigned char *yuyv,
unsigned char *rgb,
int width, int height)
{
int i, j;
for (i = 0; i < height; i++) {
for (j = 0; j < width; j += 2) {
int y0, u, y1, v;
int r, g, b;
/* 读取YUYV数据 */
y0 = yuyv[0];
u = yuyv[1];
y1 = yuyv[2];
v = yuyv[3];
yuyv += 4;
/* YUV到RGB转换 (简化公式) */
/* 像素0 */
r = y0 + 1.402 * (v - 128);
g = y0 - 0.344 * (u - 128) - 0.714 * (v - 128);
b = y0 + 1.772 * (u - 128);
*rgb++ = (r < 0) ? 0 : (r > 255) ? 255 : r;
*rgb++ = (g < 0) ? 0 : (g > 255) ? 255 : g;
*rgb++ = (b < 0) ? 0 : (b > 255) ? 255 : b;
/* 像素1 */
r = y1 + 1.402 * (v - 128);
g = y1 - 0.344 * (u - 128) - 0.714 * (v - 128);
b = y1 + 1.772 * (u - 128);
*rgb++ = (r < 0) ? 0 : (r > 255) ? 255 : r;
*rgb++ = (g < 0) ? 0 : (g > 255) ? 255 : g;
*rgb++ = (b < 0) ? 0 : (b > 255) ? 255 : b;
}
}
}
/*
* 保存为JPEG文件
*
* 目的:将RGB数据压缩为JPEG格式
*/
static int save_jpeg(const char *filename,
unsigned char *rgb_data,
int width, int height, int quality)
{
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE *outfile;
JSAMPROW row_pointer[1];
int row_stride;
/* 初始化JPEG压缩对象 */
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
/* 打开输出文件 */
if ((outfile = fopen(filename, "wb")) == NULL) {
fprintf(stderr, "无法创建 %s\n", filename);
return -1;
}
jpeg_stdio_dest(&cinfo, outfile);
/* 设置压缩参数 */
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = 3; // RGB 3个分量
cinfo.in_color_space = JCS_RGB;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
/* 开始压缩 */
jpeg_start_compress(&cinfo, TRUE);
row_stride = width * 3;
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &rgb_data[cinfo.next_scanline * row_stride];
jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
/* 完成压缩 */
jpeg_finish_compress(&cinfo);
fclose(outfile);
jpeg_destroy_compress(&cinfo);
return 0;
}
/*
* 处理一帧数据
*
* 目的:
* 1. 从缓冲区取出帧数据
* 2. YUYV转RGB
* 3. 保存为JPEG
*/
static void process_frame(const void *data, int size, int frame_no)
{
static unsigned char *rgb_buffer = NULL;
static int rgb_buffer_size = 0;
char filename[64];
/* 分配RGB缓冲区 */
int rgb_size = 640 * 480 * 3;
if (!rgb_buffer || rgb_buffer_size < rgb_size) {
rgb_buffer = realloc(rgb_buffer, rgb_size);
rgb_buffer_size = rgb_size;
}
/* YUYV转RGB */
yuyv_to_rgb((const unsigned char *)data, rgb_buffer, 640, 480);
/* 保存JPEG */
snprintf(filename, sizeof(filename), "frame_%03d.jpg", frame_no);
save_jpeg(filename, rgb_buffer, 640, 480, 90);
printf("已保存: %s\n", filename);
}
/*
* 主循环:读取帧
*
* 目的:持续采集视频帧
*/
static int capture_frames(int frame_count)
{
int frames_captured = 0;
printf("开始采集 %d 帧...\n", frame_count);
while (frames_captured < frame_count) {
fd_set fds;
struct timeval tv;
int ret;
/* 等待帧就绪 */
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = 2;
tv.tv_usec = 0;
ret = select(fd + 1, &fds, NULL, NULL, &tv);
if (ret == -1) {
if (errno == EINTR) {
continue;
}
fprintf(stderr, "select 失败: %s\n", strerror(errno));
return -1;
}
if (ret == 0) {
fprintf(stderr, "select 超时\n");
return -1;
}
/* 从队列中取出已填充的缓冲区 */
struct v4l2_buffer buf;
CLEAR(buf);
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
if (errno == EAGAIN) {
continue;
}
fprintf(stderr, "VIDIOC_DQBUF 失败: %s\n", strerror(errno));
return -1;
}
/* 处理帧数据 */
printf("帧 %d: 索引=%d, 大小=%d, 时间戳=%ld.%06ld\n",
frames_captured,
buf.index,
buf.bytesused,
buf.timestamp.tv_sec,
buf.timestamp.tv_usec);
process_frame(buffers[buf.index].start, buf.bytesused, frames_captured);
/* 重新入队 */
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
fprintf(stderr, "VIDIOC_QBUF 失败: %s\n", strerror(errno));
return -1;
}
frames_captured++;
}
printf("采集完成!\n");
return 0;
}
/*
* 停止视频流
*/
static int stop_capture(void)
{
enum v4l2_buf_type type;
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMOFF, &type) == -1) {
fprintf(stderr, "VIDIOC_STREAMOFF 失败: %s\n", strerror(errno));
return -1;
}
printf("视频流已停止\n");
return 0;
}
/*
* 释放资源
*/
static void cleanup(void)
{
unsigned int i;
/* 解除映射 */
if (buffers) {
for (i = 0; i < n_buffers; i++) {
if (buffers[i].start != NULL) {
munmap(buffers[i].start, buffers[i].length);
}
}
free(buffers);
}
/* 关闭设备 */
if (fd != -1) {
close(fd);
}
printf("资源已释放\n");
}
/*
* 主函数
*/
int main(int argc, char **argv)
{
const char *dev_name = "/dev/video0";
if (argc > 1) {
dev_name = argv[1];
}
printf("=== V4L2摄像头采集示例 ===\n");
/* 1. 打开设备 */
if (open_device(dev_name) < 0) {
return 1;
}
/* 2. 检查能力 */
if (check_capabilities() < 0) {
cleanup();
return 1;
}
/* 3. 设置格式 */
if (set_format() < 0) {
cleanup();
return 1;
}
/* 4. 初始化内存映射 */
if (init_mmap() < 0) {
cleanup();
return 1;
}
/* 5. 启动视频流 */
if (start_capture() < 0) {
cleanup();
return 1;
}
/* 6. 采集30帧 */
if (capture_frames(30) < 0) {
stop_capture();
cleanup();
return 1;
}
/* 7. 停止视频流 */
stop_capture();
/* 8. 释放资源 */
cleanup();
printf("程序结束\n");
return 0;
}
6.2 高级版本(实时显示 + 控制参数)
/*
* v4l2_advanced.c - V4L2高级示例
*
* 新增功能:
* 1. 枚举设备支持的格式和分辨率
* 2. 设置帧率
* 3. 调整摄像头参数(亮度、对比度)
* 4. 使用poll替代select
* 5. 信号处理(Ctrl+C优雅退出)
*
* 编译:gcc v4l2_advanced.c -o v4l2_advanced -ljpeg
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <poll.h>
#include <linux/videodev2.h>
static int running = 1;
/*
* 信号处理:Ctrl+C优雅退出
*/
static void signal_handler(int sig)
{
printf("\n收到信号 %d,准备退出...\n", sig);
running = 0;
}
/*
* 枚举所有支持的格式
*
* 目的:了解设备支持哪些像素格式
*/
static void enum_formats(int fd)
{
struct v4l2_fmtdesc fmtdesc;
printf("\n支持的像素格式:\n");
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
printf(" [%d] %s (%.4s)%s\n",
fmtdesc.index,
fmtdesc.description,
(char *)&fmtdesc.pixelformat,
(fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) ? " [压缩]" : "");
fmtdesc.index++;
}
}
/*
* 枚举指定格式支持的分辨率
*
* 目的:了解每种格式支持的分辨率列表
*/
static void enum_framesizes(int fd, __u32 pixelformat)
{
struct v4l2_frmsizeenum frmsize;
printf("\n%.4s 格式支持的分辨率:\n", (char *)&pixelformat);
frmsize.index = 0;
frmsize.pixel_format = pixelformat;
while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) == 0) {
if (frmsize.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
printf(" [%d] %dx%d\n",
frmsize.index,
frmsize.discrete.width,
frmsize.discrete.height);
} else if (frmsize.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
printf(" 范围: %dx%d ~ %dx%d (步进: %dx%d)\n",
frmsize.stepwise.min_width,
frmsize.stepwise.min_height,
frmsize.stepwise.max_width,
frmsize.stepwise.max_height,
frmsize.stepwise.step_width,
frmsize.stepwise.step_height);
}
frmsize.index++;
}
}
/*
* 设置帧率
*
* 目的:控制视频采集速度
*/
static int set_framerate(int fd, int fps)
{
struct v4l2_streamparm parm;
memset(&parm, 0, sizeof(parm));
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
/* 先获取当前参数 */
if (ioctl(fd, VIDIOC_G_PARM, &parm) == -1) {
fprintf(stderr, "VIDIOC_G_PARM 失败: %s\n", strerror(errno));
return -1;
}
/* 检查是否支持设置帧率 */
if (!(parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME)) {
printf("设备不支持设置帧率\n");
return -1;
}
/* 设置帧率 */
parm.parm.capture.timeperframe.numerator = 1;
parm.parm.capture.timeperframe.denominator = fps;
if (ioctl(fd, VIDIOC_S_PARM, &parm) == -1) {
fprintf(stderr, "VIDIOC_S_PARM 失败: %s\n", strerror(errno));
return -1;
}
/* 读取实际设置的帧率 */
int actual_fps = parm.parm.capture.timeperframe.denominator /
parm.parm.capture.timeperframe.numerator;
printf("帧率设置为: %d fps (请求: %d fps)\n", actual_fps, fps);
return 0;
}
/*
* 枚举控制参数
*
* 目的:查看设备支持哪些控制参数
*/
static void enum_controls(int fd)
{
struct v4l2_queryctrl queryctrl;
printf("\n支持的控制参数:\n");
/* 枚举所有控制类 */
memset(&queryctrl, 0, sizeof(queryctrl));
queryctrl.id = V4L2_CTRL_FLAG_NEXT_CTRL;
while (ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == 0) {
if (!(queryctrl.flags & V4L2_CTRL_FLAG_DISABLED)) {
printf(" %s (ID: 0x%08x)\n", queryctrl.name, queryctrl.id);
printf(" 类型: %s\n",
queryctrl.type == V4L2_CTRL_TYPE_INTEGER ? "整数" :
queryctrl.type == V4L2_CTRL_TYPE_BOOLEAN ? "布尔" :
queryctrl.type == V4L2_CTRL_TYPE_MENU ? "菜单" : "其它");
if (queryctrl.type == V4L2_CTRL_TYPE_INTEGER) {
printf(" 范围: %d ~ %d (步进: %d, 默认: %d)\n",
queryctrl.minimum,
queryctrl.maximum,
queryctrl.step,
queryctrl.default_value);
}
}
queryctrl.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
}
}
/*
* 设置控制参数
*
* 目的:调整摄像头参数
*/
static int set_control(int fd, __u32 id, __s32 value)
{
struct v4l2_control ctrl;
struct v4l2_queryctrl queryctrl;
/* 查询参数是否支持 */
memset(&queryctrl, 0, sizeof(queryctrl));
queryctrl.id = id;
if (ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == -1) {
fprintf(stderr, "控制参数 0x%08x 不支持\n", id);
return -1;
}
if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) {
fprintf(stderr, "控制参数 %s 已禁用\n", queryctrl.name);
return -1;
}
/* 设置参数 */
ctrl.id = id;
ctrl.value = value;
if (ioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) {
fprintf(stderr, "设置 %s 失败: %s\n", queryctrl.name, strerror(errno));
return -1;
}
printf("设置 %s = %d\n", queryctrl.name, value);
return 0;
}
/*
* 使用poll等待帧就绪
*
* 目的:比select更高效的I/O多路复用
*/
static int wait_frame_ready(int fd, int timeout_ms)
{
struct pollfd pfd;
int ret;
pfd.fd = fd;
pfd.events = POLLIN; // 等待可读事件
ret = poll(&pfd, 1, timeout_ms);
if (ret == -1) {
if (errno == EINTR) {
return 0; // 被信号中断,继续
}
fprintf(stderr, "poll 失败: %s\n", strerror(errno));
return -1;
}
if (ret == 0) {
/* 超时 */
return 0;
}
if (pfd.revents & POLLIN) {
return 1; // 有帧可读
}
return 0;
}
/*
* 主函数(高级版)
*/
int main(int argc, char **argv)
{
const char *dev_name = "/dev/video0";
int fd = -1;
struct buffer *buffers = NULL;
unsigned int n_buffers = 0;
/* 安装信号处理 */
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
printf("=== V4L2摄像头高级示例 ===\n");
/* 1. 打开设备 */
fd = open(dev_name, O_RDWR);
if (fd == -1) {
fprintf(stderr, "无法打开 %s: %s\n", dev_name, strerror(errno));
return 1;
}
printf("设备已打开: %s\n", dev_name);
/* 2. 查询能力 */
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
fprintf(stderr, "查询能力失败\n");
close(fd);
return 1;
}
printf("驱动: %s, 设备: %s\n", cap.driver, cap.card);
/* 3. 枚举格式和分辨率 */
enum_formats(fd);
enum_framesizes(fd, V4L2_PIX_FMT_YUYV);
enum_framesizes(fd, V4L2_PIX_FMT_MJPEG);
/* 4. 枚举控制参数 */
enum_controls(fd);
/* 5. 设置格式 */
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
fprintf(stderr, "设置格式失败\n");
close(fd);
return 1;
}
printf("\n当前格式: %dx%d %.4s\n",
fmt.fmt.pix.width,
fmt.fmt.pix.height,
(char *)&fmt.fmt.pix.pixelformat);
/* 6. 设置帧率 */
set_framerate(fd, 30);
/* 7. 调整摄像头参数 */
set_control(fd, V4L2_CID_BRIGHTNESS, 128);
set_control(fd, V4L2_CID_CONTRAST, 32);
set_control(fd, V4L2_CID_SATURATION, 64);
/* 8. 初始化缓冲区(省略详细代码,同基础版) */
/* ... */
/* 9. 启动视频流 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) {
fprintf(stderr, "启动视频流失败\n");
close(fd);
return 1;
}
printf("\n视频流已启动,按Ctrl+C停止\n");
/* 10. 主循环 */
int frame_count = 0;
while (running) {
/* 使用poll等待 */
int ret = wait_frame_ready(fd, 1000);
if (ret < 0) {
break;
}
if (ret == 0) {
continue; // 超时或中断,继续
}
/* 出队 */
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
if (errno == EAGAIN) {
continue;
}
fprintf(stderr, "出队失败\n");
break;
}
/* 处理帧 */
printf("\r帧 %d: 索引=%d, 大小=%d 字节",
frame_count++, buf.index, buf.bytesused);
fflush(stdout);
/* 这里可以处理帧数据 */
// process_frame(buffers[buf.index].start, buf.bytesused);
/* 重新入队 */
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
fprintf(stderr, "入队失败\n");
break;
}
}
printf("\n\n总共采集 %d 帧\n", frame_count);
/* 11. 停止视频流 */
ioctl(fd, VIDIOC_STREAMOFF, &type);
/* 12. 清理资源 */
if (buffers) {
for (unsigned int i = 0; i < n_buffers; i++) {
munmap(buffers[i].start, buffers[i].length);
}
free(buffers);
}
close(fd);
printf("程序结束\n");
return 0;
}
七、常见问题与调试
7.1 常见错误及解决方案
错误1:Permission denied
# 错误信息
open: Permission denied
# 原因
普通用户无权限访问 /dev/video0
# 解决方案1:添加用户到video组
sudo usermod -aG video $USER
# 重新登录生效
# 解决方案2:临时修改权限
sudo chmod 666 /dev/video0
# 解决方案3:使用sudo运行
sudo ./v4l2_capture
错误2:Device or resource busy
# 错误信息
VIDIOC_STREAMON: Device or resource busy
# 原因
设备已被其它程序占用(如cheese、guvcview、ffmpeg)
# 解决方案:查找并关闭占用进程
lsof /dev/video0
# 或
fuser /dev/video0
# 杀死进程
sudo kill -9 <PID>
错误3:Invalid argument
# 错误信息
VIDIOC_S_FMT: Invalid argument
# 原因
设备不支持请求的格式或分辨率
# 解决方案:使用TRY_FMT测试
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
if (ioctl(fd, VIDIOC_TRY_FMT, &fmt) == 0) {
printf("驱动建议的格式: %dx%d\n",
fmt.fmt.pix.width, fmt.fmt.pix.height);
// 使用驱动返回的值
ioctl(fd, VIDIOC_S_FMT, &fmt);
}
错误4:Out of memory
# 错误信息
VIDIOC_REQBUFS: Cannot allocate memory
# 原因
系统内存不足,或请求的缓冲区数量过多
# 解决方案:减少缓冲区数量
req.count = 4; // 从8减到4
ioctl(fd, VIDIOC_REQBUFS, &req);
// 或检查实际分配的数量
printf("实际分配: %d 个缓冲区\n", req.count);
错误5:Buffer 未映射就访问
// 错误代码
struct v4l2_buffer buf;
ioctl(fd, VIDIOC_DQBUF, &buf);
// 直接访问buf.m.offset → 错误!
// 正确做法:访问映射后的用户空间地址
unsigned char *data = buffers[buf.index].start;
7.2 调试技巧
技巧1:使用v4l2-ctl命令行工具
# 列出所有视频设备
v4l2-ctl --list-devices
# 查询设备所有信息
v4l2-ctl -d /dev/video0 --all
# 列出支持的格式
v4l2-ctl -d /dev/video0 --list-formats-ext
# 设置格式并采集一帧
v4l2-ctl -d /dev/video0 \
--set-fmt-video=width=640,height=480,pixelformat=YUYV \
--stream-mmap \
--stream-count=1 \
--stream-to=frame.raw
# 调整参数
v4l2-ctl -d /dev/video0 --set-ctrl brightness=128
v4l2-ctl -d /dev/video0 --set-ctrl contrast=32
技巧2:查看内核日志
# 实时查看内核日志
sudo dmesg -w
# 插入摄像头后查看
[ 123.456789] usb 1-1: new high-speed USB device number 3 using ehci-pci
[ 123.567890] usb 1-1: New USB device found, idVendor=046d, idProduct=0825
[ 123.678901] uvcvideo: Found UVC 1.00 device USB Camera (046d:0825)
[ 123.789012] input: USB Camera as /devices/pci0000:00/0000:00:1d.0/usb1/1-1/1-1:1.0/input/input15
# 过滤UVC相关信息
dmesg | grep -i uvc
技巧3:使用strace跟踪系统调用
# 跟踪所有系统调用
strace ./v4l2_capture 2>&1 | less
# 只跟踪ioctl调用
strace -e ioctl ./v4l2_capture
# 跟踪并记录到文件
strace -o trace.log ./v4l2_capture
技巧4:检查USB带宽
# 查看USB设备信息
lsusb -v -d 046d:0825
# 查看当前USB速度和带宽
cat /sys/bus/usb/devices/1-1/speed
cat /sys/bus/usb/devices/1-1/bMaxPower
技巧5:代码中添加调试输出
#define DEBUG 1
#ifdef DEBUG
#define DBG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __func__, __LINE__, ##__VA_ARGS__)
#else
#define DBG(fmt, ...) do {} while(0)
#endif
// 使用
DBG("缓冲区 %d: start=%p, length=%zu", i, buffers[i].start, buffers[i].length);
DBG("出队缓冲区: index=%d, bytesused=%d", buf.index, buf.bytesused);
7.3 性能优化建议
优化1:使用足够的缓冲区
// 不够:容易丢帧
req.count = 2;
// 推荐:流水线工作
req.count = 4;
// 高帧率场景
req.count = 8;
优化2:使用压缩格式减少带宽
// YUYV: 640x480 = 614KB/帧, 30fps = 18MB/s
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
// MJPEG: 压缩后约50KB/帧, 30fps = 1.5MB/s
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
// 但需要解码(libjpeg-turbo)
优化3:多线程处理
// 主线程:采集帧
while (running) {
ioctl(fd, VIDIOC_DQBUF, &buf);
// 将帧数据加入队列
queue_push(frame_queue, buffers[buf.index].start, buf.bytesused);
ioctl(fd, VIDIOC_QBUF, &buf);
}
// 工作线程:处理帧
void *worker_thread(void *arg)
{
while (running) {
frame_t *frame = queue_pop(frame_queue);
process_frame(frame);
free(frame);
}
}
优化4:避免不必要的数据拷贝
// 差:拷贝数据
unsigned char *copy = malloc(buf.bytesused);
memcpy(copy, buffers[buf.index].start, buf.bytesused);
process_frame(copy); // 增加延迟
// 好:直接处理映射的内存
process_frame(buffers[buf.index].start, buf.bytesused);
优化5:使用O_DIRECT减少内核缓冲
// 对于高速设备,可以尝试
fd = open("/dev/video0", O_RDWR | O_DIRECT);
7.4 常见问题FAQ
Q1: 为什么采集的图像颜色不对?
A: 可能的原因:
- YUYV字节序错误(应该是Y0 U Y1 V)
- RGB转换公式错误
- 摄像头白平衡未校准
Q2: 为什么帧率达不到30fps?
A: 检查:
- USB带宽是否足够(USB 2.0高速模式)
- 分辨率是否过高
- 应用处理速度是否跟上
- 使用压缩格式(MJPEG)
Q3: 如何知道设备支持哪些分辨率?
A: 使用VIDIOC_ENUM_FRAMESIZES枚举,或:
v4l2-ctl -d /dev/video0 --list-formats-ext
Q4: 可以同时打开多个摄像头吗?
A: 可以,每个摄像头使用独立的fd:
int fd1 = open("/dev/video0", O_RDWR);
int fd2 = open("/dev/video1", O_RDWR);
Q5: MMAP和USERPTR有什么区别?
A:
- MMAP:内核分配内存,应用映射访问(推荐)
- USERPTR:应用分配内存,传递给内核使用
总结
本文详细介绍了V4L2摄像头编程的完整流程:
- 核心结构体:v4l2_capability, v4l2_format, v4l2_buffer等
- 编程流程:打开→查询→设置→映射→启动→循环读取→停止→清理
- ioctl命令:QUERYCAP, S_FMT, REQBUFS, QBUF, DQBUF, STREAMON等
- 内存映射:零拷贝的mmap方式实现高性能视频采集
- 完整代码:基础版和高级版示例
- 调试技巧:v4l2-ctl, dmesg, strace等工具
掌握这些知识后,你可以:
- 开发摄像头采集程序
- 实现视频监控系统
- 进行图像处理和计算机视觉应用
- 理解V4L2驱动的工作原理
参考资料:
完成! 这是一份完整的V4L2摄像头编程指南。
- Linux内核文档:Documentation/video4linux/v4l2-framework.txt
- V4L2 API规范:https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/v4l2.html
- UVC驱动:drivers/media/usb/uvc/