未分类 2026年8月6日 263 分钟

V4L2子系统1-应用编程

非常开心!!!最后一个模块啦!!!! V4L2摄像头编程完全指南 目录 一、V4L2框架概述 1.1 什么是V4L2? ...
本文目录 展开目录

非常开心!!!最后一个模块啦!!!!

V4L2摄像头编程完全指南

目录

  1. V4L2框架概述
  2. 核心数据结构详解
  3. 摄像头编程完整流程
  4. ioctl命令详解
  5. 内存映射机制
  6. 完整代码示例
  7. 常见问题与调试

一、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_fmtdescv4l2_pix_format
用途枚举设备支持的格式列表设置/获取当前正在使用的格式
ioctlVIDIOC_ENUM_FMTVIDIOC_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); // 提高饱和度

注意事项:

  1. 不是所有摄像头都支持所有控制参数,设置前应先用VIDIOC_QUERYCTRL查询
  2. 参数范围因设备而异,需要查询minimum、maximum、step
  3. 详细的控制参数说明参见《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);

目的:

  1. 确认设备是摄像头(而不是输出设备)
  2. 确认支持流式I/O(性能更好)
  3. 获取设备基本信息用于调试

为什么要检查能力?

  • /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);

目的:

  1. 告诉驱动期望的分辨率和像素格式
  2. 驱动协商出实际支持的参数
  3. 获取每帧数据的大小(用于分配缓冲区)

为什么格式可能被调整?

  • 摄像头可能不支持请求的分辨率(如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);

目的:

  1. 让内核驱动分配视频帧缓冲区
  2. 驱动根据硬件能力返回实际分配的数量
  3. 这些缓冲区在内核空间,需要映射到用户空间

为什么需要多个缓冲区?(流水线机制)

单缓冲区(容易丢帧):
时间 → [驱动填充] [应用处理] [驱动填充] ...
↑ 硬件采集新帧但缓冲区被占用 → 丢帧

多缓冲区(流水线):
缓冲区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);
}

目的:

  1. 查询每个缓冲区在内核中的位置(offset)和大小
  2. 通过 mmap 将内核缓冲区映射到用户空间
  3. 应用可以直接访问这块内存,无需数据拷贝

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");

目的:

  1. 将空缓冲区交给驱动
  2. 驱动会在采集到帧数据后填充这些缓冲区
  3. 入队后缓冲区归驱动所有,应用不能访问

缓冲区所有权转移:

入队前(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");

目的:

  1. 通知驱动开始采集视频数据
  2. 摄像头硬件开始工作
  3. 驱动会自动填充已入队的缓冲区

启动流后发生了什么?

硬件层:
摄像头传感器开始曝光、采集
通过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;
}
}

目的:

  1. DQBUF:取出驱动已填充的缓冲区
  2. 应用处理帧数据(保存/显示/编码)
  3. 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");

目的:

  1. 通知驱动停止采集
  2. 摄像头硬件停止工作
  3. 所有已入队的缓冲区变为空闲状态

停止流后发生了什么?

硬件层:
摄像头传感器停止曝光
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");

目的:

  1. munmap:解除用户空间到内核空间的映射
  2. 释放应用层分配的内存
  3. 关闭设备文件

资源释放顺序:

1. 停止视频流(STREAMOFF)
2. 解除映射(munmap)
3. 释放应用内存(free)
4. 关闭设备(close)

注意:关闭设备时,内核会自动释放缓冲区

四、ioctl命令详解

4.1 设备查询类

ioctl命令结构体作用
VIDIOC_QUERYCAPv4l2_capability查询设备能力
VIDIOC_ENUM_FMTv4l2_fmtdesc枚举支持的像素格式
VIDIOC_ENUM_FRAMESIZESv4l2_frmsizeenum枚举支持的分辨率
VIDIOC_ENUM_FRAMEINTERVALSv4l2_frmivalenum枚举支持的帧率
VIDIOC_QUERYCTRLv4l2_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_FMTv4l2_format获取当前格式
VIDIOC_S_FMTv4l2_format设置格式
VIDIOC_TRY_FMTv4l2_format尝试格式(不实际设置)
VIDIOC_G_PARMv4l2_streamparm获取流参数(帧率)
VIDIOC_S_PARMv4l2_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_REQBUFSv4l2_requestbuffers申请缓冲区
VIDIOC_QUERYBUFv4l2_buffer查询缓冲区信息
VIDIOC_QBUFv4l2_buffer缓冲区入队
VIDIOC_DQBUFv4l2_buffer缓冲区出队

4.4 流控制类

ioctl命令参数作用
VIDIOC_STREAMONenum v4l2_buf_type启动视频流
VIDIOC_STREAMOFFenum v4l2_buf_type停止视频流

4.5 控制参数类

ioctl命令结构体作用
VIDIOC_G_CTRLv4l2_control获取控制值
VIDIOC_S_CTRLv4l2_control设置控制值
VIDIOC_G_EXT_CTRLSv4l2_ext_controls批量获取控制
VIDIOC_S_EXT_CTRLSv4l2_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: 可能的原因:

  1. YUYV字节序错误(应该是Y0 U Y1 V)
  2. RGB转换公式错误
  3. 摄像头白平衡未校准

Q2: 为什么帧率达不到30fps?

A: 检查:

  1. USB带宽是否足够(USB 2.0高速模式)
  2. 分辨率是否过高
  3. 应用处理速度是否跟上
  4. 使用压缩格式(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摄像头编程的完整流程:

  1. 核心结构体:v4l2_capability, v4l2_format, v4l2_buffer等
  2. 编程流程:打开→查询→设置→映射→启动→循环读取→停止→清理
  3. ioctl命令:QUERYCAP, S_FMT, REQBUFS, QBUF, DQBUF, STREAMON等
  4. 内存映射:零拷贝的mmap方式实现高性能视频采集
  5. 完整代码:基础版和高级版示例
  6. 调试技巧:v4l2-ctl, dmesg, strace等工具

掌握这些知识后,你可以:

  • 开发摄像头采集程序
  • 实现视频监控系统
  • 进行图像处理和计算机视觉应用
  • 理解V4L2驱动的工作原理

参考资料:

完成! 这是一份完整的V4L2摄像头编程指南。

上一篇 PCIE子系统2-PCIE硬件设备的访问方法之非桥设备

初始化设备选择线

下一篇 uboot 默认配置过程分析