V4L2驱动框架

1. V4L2 (Video4Linux2)

1.1 简介

V4L2 (Video4Linux2) 是 Linux 内核提供的标准视频设备接口,作为访问摄像头最底层的原生方式。该接口通过设备文件(如 /dev/video0、/dev/video1)与内核驱动通信,实现对视频设备的全面控制。

1.1.1 发展历史
V4L2的发展历程展现了Linux视频子系统的逐步完善:

1998年 - V4L问世:Bill Dirks开发了最初的Video4Linux(V4L),为Linux系统提供了基础视频设备支持功能

2002年 - V4L2推出:为克服V4L的局限性,Video4Linux2(V4L2)被引入Linux 2.5内核,带来更强大灵活的API

2003年 - 正式纳入内核:V4L2成为Linux 2.6内核的标准视频接口

持续优化:伴随硬件技术进步,V4L2持续扩展功能,支持更多设备类型(如摄像头、TV调谐器、SDR等)和先进特性(包括多平面格式、M2M设备等)

1.1.2 技术定位

V4L2在Linux视频生态系统中的核心定位体现在以下三个方面:

1. 内核层接口

  • 作为Linux内核视频子系统的核心接口
  • 运行于内核空间,直接与硬件驱动进行交互
  • 通过统一的设备抽象层屏蔽底层硬件差异

2. 底层基础架构

  • 是Linux系统访问视频设备的最低层标准接口
  • 为OpenCV、GStreamer、FFmpeg等高级库提供底层支持
  • 向上层应用提供稳定的硬件抽象服务

3. 标准化规范

  • 制定统一的API标准,确保各类硬件使用相同接口
  • 兼容多种视频设备:包括USB摄像头、MIPI CSI摄像头、TV调谐器和SDR设备等
  • 具备跨平台特性,所有主流Linux发行版均原生支持

4.总体架构

V4L2 驱动框架分为三层,从上到下依次为:

cpp 复制代码
┌─────────────────────────────────────────┐
│           用户空间应用程序               │
│  (打开 /dev/videoX, ioctl, read/write)  │
└─────────────────────────────────────────┘
                    ↕ (系统调用)
┌─────────────────────────────────────────┐
│            V4L2 核心层                  │
│  - 设备节点管理                         │
│  - ioctl 派发                           │
│  - 缓冲区管理(videobuf2)              │
│  - 媒体控制器辅助                       │
└─────────────────────────────────────────┘
                    ↕ (回调 / 注册)
┌─────────────────────────────────────────┐
│          V4L2 设备驱动层                │
│  - 实现具体的硬件操作                    │
│  - 注册 video_device 结构体              │
│  - 提供 v4l2_ioctl_ops / v4l2_file_ops │
│  - 使用 vb2_queue 管理视频缓冲           │
└─────────────────────────────────────────┘
                    ↕ (硬件操作)
┌─────────────────────────────────────────┐
│              硬件设备                    │
│  (摄像头、HDMI采集卡、CODEC等)          │
└─────────────────────────────────────────┘

1.2 原理

1.2.1 Linux架构原理

Linux内核架构概述:

Linux 内核架构说明:

1.用户空间(User Space)

  • 应用程序:用户态运行的程序
  • 系统库:封装系统调用的库函数(如 libc、libpthread)
  • 特点:受限访问,必须通过系统调用进入内核

2.内核空间(Kernel Space) a) 系统调用接口层

  • 功能:用户/内核空间交互桥梁
  • 特点:触发 CPU 特权级切换
  • 接口:提供标准系统调用(open/read/write/ioctl)

b) 虚拟文件系统层(VFS)

  • 作用:统一文件操作接口
  • 功能:路由操作到具体文件系统
  • 支持:ext4、procfs、sysfs 等文件系统

c) 内核核心子系统

  • 进程管理:调度与同步
  • 内存管理:虚拟内存与分配
  • 中断处理:硬件中断管理
  • 定时器:系统时间维护

d) 设备子系统框架

  • V4L2:视频设备接口
  • ALSA:音频设备接口
  • GPIO:通用 IO 管理
  • I2C/USB:总线管理

e) 驱动层

  • 功能:直接硬件操作
  • 类型:字符/块/网络设备驱动
  • 特点:硬件专属实现

3.硬件层

  • 物理设备:各类硬件控制器
  • 交互方式:寄存器操作与中断处理

设计特点:

  1. 分层抽象:各层职责明确
  2. 统一接口:标准化设备访问
  3. 模块化:子系统独立可扩展
  4. 安全性:强制内核态硬件访问

对比: 单片机开发直接操作硬件寄存器


Linux用户态与内核态开发框架(以GPIO控制LED为例):
Linux GPIO 调用流程说明:

  1. 用户态调用

    应用程序通过文件操作命令 **echo 1 > /sys/class/gpio/gpio18/value**发起调用

  2. 系统调用

    触发**write()**系统调用,CPU 切换至内核态

  3. VFS 层处理

    虚拟文件系统 (VFS) 作为统一抽象层,提供以下功能:

    • 统一接口:为应用程序提供标准文件操作接口 (open/read/write 等)
    • 文件系统识别:根据路径 (如**/sys/**) 识别对应的文件系统类型 (sysfs/ext4/proc 等)
    • 路由分发:将文件操作请求路由至对应的文件系统实现
    • 抽象隔离:屏蔽底层文件系统实现细节,提高可移植性
  4. Sysfs 层处理

    sysfs 子系统解析路径,定位目标 GPIO 设备

  5. GPIO 子系统

    处理 GPIO 操作请求

  6. GPIO 驱动

    具体驱动操作硬件寄存器

  7. 硬件执行

    GPIO 硬件输出高电平,点亮 LED 灯

关键特性对比:

特性 单片机 Linux系统
代码复杂度 简单,直接寄存器操作 复杂,需经过多层抽象
执行路径 程序→寄存器→硬件 程序→系统调用→VFS→子系统→驱动→硬件
安全性 无保护机制,可直接访问硬件 内核保护机制,用户态无法直接访问硬件
多任务支持 仅支持单任务或简单RTOS 完整支持多进程、多线程,需资源管理
可移植性 硬件相关,移植困难 通过驱动抽象,移植性强
开发效率 简单任务开发快速 复杂系统管理便捷

Linux采用复杂架构主要基于以下关键考量:

  1. 安全隔离机制:通过内核层隔离用户程序与硬件交互,有效防止误操作导致的系统崩溃
  2. 并发控制需求:协调多个进程对共享硬件的并发访问,确保系统稳定运行
  3. 硬件抽象层:提供统一系统调用接口,使应用程序摆脱硬件依赖,提升可移植性
  4. 集中式资源调度:由内核统一分配硬件资源,避免使用冲突和资源争用
  5. 模块化扩展设计:采用驱动框架支持新硬件接入,保持应用程序兼容性

Linux分层架构对比(文件、音频、视频):
文件系统架构(以磁盘文件为例):

音频系统架构(以ALSA为例):

V4L2采用类似的分层架构:


分层架构说明:
应用层(浅灰色):用户空间程序,通过系统调用访问设备
Linux内核框架(暗蓝色):系统调用接口、VFS、字符设备框架,提供内核基础服务
V4L2核心层(暗黄色):V4L2 API、核心逻辑、缓冲区管理,提供统一的视频设备接口
驱动层(深蓝灰色):具体的硬件驱动(UVC驱动、MIPI CSI驱动、V4L2子设备驱动等)
硬件层(深灰色):物理摄像头设备(USB摄像头、MIPI摄像头等)

1.2.2 工作流程

  1. 设备发现:内核检测到摄像头并创建/dev/video*设备节点
  2. 设备打开:应用程序调用open()函数打开设备文件
  3. 能力查询:使用VIDIOC_QUERYCAP指令查询设备功能支持
  4. 格式配置:通过VIDIOC_S_FMT设置视频格式参数(包括分辨率与像素格式)
  5. 缓冲区申请:调用VIDIOC_REQBUFS指令申请视频缓冲区
  6. 内存映射:使用VIDIOC_QUERYBUF和mmap()将缓冲区映射至用户空间
  7. 数据采集:通过VIDIOC_QBUF将缓冲区加入队列,使用VIDIOC_DQBUF获取填充数据
  8. 流控制:执行VIDIOC_STREAMON启动采集,调用VIDIOC_STREAMOFF停止采集

1.2.3 I/O方式

V4L2 支持三种 I/O 方式:

  1. read/write:实现简单但性能较低,适合处理小数据量
  2. mmap(内存映射):采用零拷贝技术,性能最优,是推荐的首选方案

1.3 开发流程

1.3.1 基本开发步骤

  1. 设备检查:确认**/dev/video0** 设备存在并具备访问权限
  2. 设备开启:通过**open()** 函数打开设备文件
  3. 能力查询:调用 **VIDIOC_QUERYCAP**获取设备基本信息
  4. 格式枚举:使用 VIDIOC_ENUM_FMT 列出设备支持的视频格式
  5. 格式配置:通过 **VIDIOC_S_FMT**设置所需的视频格式参数
  6. 缓冲申请:执行**VIDIOC_REQBUFS** 请求视频缓冲区
  7. 内存映射:使用**mmap()** 将缓冲区映射到用户空间
  8. 采集启动:发送 VIDIOC_STREAMON 指令开始视频采集
  9. 帧循环处理:交替调用 **VIDIOC_QBUFVIDIOC_DQBUF**进行帧数据获取
  10. 采集停止:发送 VIDIOC_STREAMOFF 指令终止视频采集
  11. 资源释放:解除缓冲区映射并关闭设备文件

1.3.2 错误处理

务必检查所有 ioctl() 调用的返回值 通过 errno 获取具体的错误信息 出现错误时需妥善释放已分配资源

1.4 核心API函数说明

案例中使用的API汇总表:
V4L2 API 函数说明

配置流程

函数/命令 功能说明 关键结构体 使用频率
open() 打开设备文件,获取文件描述符 - 必需
ioctl(fd, VIDIOC_QUERYCAP, ...) 查询设备能力,检查支持的功能 v4l2_capability 必需
ioctl(fd, VIDIOC_ENUM_FMT, ...) 枚举设备支持的像素格式 v4l2_fmtdesc 可选
ioctl(fd, VIDIOC_S_FMT, ...) 设置视频格式(分辨率、像素格式等) v4l2_format 必需
ioctl(fd, VIDIOC_REQBUFS, ...) 申请指定数量的视频缓冲区 v4l2_requestbuffers 必需

缓冲区流程

函数/命令 功能说明 关键结构体 使用频率
ioctl(fd, VIDIOC_QUERYBUF, ...) 查询缓冲区信息(偏移量、长度) v4l2_buffer 必需
mmap() 将内核缓冲区映射到用户空间 - 必需

采集流程

函数/命令 功能说明 关键结构体 使用频率
ioctl(fd, VIDIOC_QBUF, ...) 将缓冲区加入采集队列 v4l2_buffer 必需
ioctl(fd, VIDIOC_STREAMON, ...) 启动视频流,开始采集 - 必需
ioctl(fd, VIDIOC_DQBUF, ...) 从队列取出已填充的缓冲区(循环使用) v4l2_buffer 必需
ioctl(fd, VIDIOC_STREAMOFF, ...) 停止视频流 - 必需
munmap() 取消内存映射,释放缓冲区 - 必需
close() 关闭设备文件 - 必需

说明:
所属流程 :配置流程、缓冲区流程、采集流程
使用频率

  • 必需:必须调用
  • 可选:可选调用
  • 循环:在循环中多次调用
    关键结构体:每个API需要配合使用的V4L2结构体

ioctl函数原型及说明:

c 复制代码
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

功能说明: ioctl(Input/Output Control)是Linux系统中用于设备控制的系统调用。

参数说明:

  • fd:文件描述符,通过open()函数打开设备文件获取
  • request:控制命令,在V4L2中对应VIDIOC_*系列宏定义
  • ...:可变参数,通常为结构体指针,具体类型根据request命令而定

返回值:

  • 成功:返回0
  • 失败:返回-1,并设置errno指示具体错误

应用场景: 在V4L2框架中,ioctl广泛用于设备操作,包括:

  • 设备能力查询
  • 视频格式设置
  • 缓冲区管理等核心功能

常用V4L2 ioctl命令:

markdown 复制代码
| ioctl命令          | 功能               | 输入结构体          | 输出结构体          | 说明                     |
|--------------------|--------------------|---------------------|---------------------|--------------------------|
| VIDIOC_QUERYCAP    | 查询设备能力       | -                   | v4l2_capability     | 获取设备信息和支持的功能 |
| VIDIOC_ENUM_FMT    | 枚举像素格式       | v4l2_fmtdesc        | v4l2_fmtdesc        | 查询设备支持的像素格式   |
| VIDIOC_S_FMT       | 设置视频格式       | v4l2_format         | v4l2_format         | 设置分辨率、像素格式等   |
| VIDIOC_G_FMT       | 获取当前格式       | -                   | v4l2_format         | 获取当前设置的视频格式   |
| VIDIOC_REQBUFS     | 申请缓冲区         | v4l2_requestbuffers | v4l2_requestbuffers | 申请指定数量的缓冲区     |
| VIDIOC_QUERYBUF    | 查询缓冲区信息     | -                   | v4l2_buffer         | 获取缓冲区的偏移量等信息 |
| VIDIOC_QBUF        | 缓冲区入队         | v4l2_buffer         | v4l2_buffer         | 将缓冲区加入采集队列     |
| VIDIOC_DQBUF       | 缓冲区出队         | -                   | v4l2_buffer         | 从队列取出已填充缓冲区   |
| VIDIOC_STREAMON    | 启动视频流         | enum v4l2_buf_type  | -                   | 开始视频采集             |
| VIDIOC_STREAMOFF   | 停止视频流         | enum v4l2_buf_type  | -                   | 停止视频采集             |
| VIDIOC_QUERYCTRL   | 查询控制参数       | v4l2_queryctrl      | v4l2_queryctrl      | 查询控制参数的范围和属性 |
| VIDIOC_G_CTRL      | 获取控制参数值     | v4l2_control        | v4l2_control        | 获取控制参数的当前值     |
| VIDIOC_S_CTRL      | 设置控制参数值     | v4l2_control        | v4l2_control        | 设置控制参数的值         |
 

使用示例:

cpp 复制代码
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <errno.h>
int fd; // 设备文件描述符
struct v4l2_capability cap;
// 示例1:查询设备能力
memset(&cap, 0, sizeof(cap));
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
 perror("VIDIOC_QUERYCAP failed");
 return -1;
}
// 示例2:设置视频格式
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;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
 perror("VIDIOC_S_FMT failed");
 return -1;
}
// 示例3:申请缓冲区
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
 perror("VIDIOC_REQBUFS failed");
 return -1;
}
// 示例4:启动视频流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
 perror("VIDIOC_STREAMON failed");
 return -1;
}

错误处理:

cpp 复制代码
// 所有ioctl调用都应该检查返回值
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
 // 检查errno获取详细错误信息
 if (errno == EINVAL) {
 fprintf(stderr, "Invalid argument\n");
 } else if (errno == ENODEV) {
 fprintf(stderr, "Device not found\n");
 } else if (errno == EBUSY) {
 fprintf(stderr, "Device is busy\n");
 } else {
 perror("ioctl failed");
 }
 return -1;
}

使用ioctl时的注意事项:

  1. 结构体初始化:调用ioctl前必须用memset()清零结构体,防止未初始化数据引发错误
  2. 输入输出参数:部分ioctl命令(如VIDIOC_S_FMT)使用同一结构体作为输入和输出参数,调用后需验证实际设置值
  3. 错误处理:每次ioctl调用都应检查返回值,失败时通过errno获取具体错误原因
  4. 阻塞特性:某些ioctl操作(如VIDIOC_DQBUF)在阻塞模式下会等待操作完成
  5. 线程安全:多线程环境中使用ioctl时,需确保对同一设备的访问进行同步控制

1. 配置流程


流程详细说明:

  1. 设备打开(open)概念说明:

    • 设备文件:在Linux系统中,硬件设备被抽象为文件形式进行管理。常见的摄像头设备文件通常命名为/dev/video0、/dev/video1等。

    • 文件描述符(fd):open()函数调用后会返回一个整数值,该值作为已打开设备的唯一标识符,后续所有设备操作都需通过此文件描述符进行。

    • 打开模式

      • O_RDWR:表示以可读写模式打开设备
  • O_NONBLOCK:表示采用非阻塞模式打开(可选参数)
    伪代码:
cpp 复制代码
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
// 1. 定义设备路径
const char *device = "/dev/video0";
// 2. 打开设备文件
// open() 参数说明:
// - device: 设备文件路径
// - O_RDWR: 可读写模式
// - O_NONBLOCK: 非阻塞模式(可选,如果使用阻塞模式则设为0)
// 返回值: 文件描述符(fd),失败返回-1
int fd = open(device, O_RDWR | O_NONBLOCK, 0);
if (fd < 0) {
 // 错误处理:设备不存在、权限不足、被占用等
 fprintf(stderr, "Cannot open device %s: %s\n", device, strerror(errno));
 return -1;
}
// 3. fd 用于后续所有V4L2操作
// 后续使用:ioctl(fd, ...)
printf("Device opened successfully, fd=%d\n", fd);

开发要点:

  1. 设备检测

    • 检查设备节点:ls /dev/video*
    • 权限验证:确保当前用户具有访问权限,必要时使用sudo
  2. 设备访问模式

    • 独占访问:同一时刻仅允许一个程序访问设备
    • 操作模式选择:
      • 阻塞模式(O_RDWR):VIDIOC_DQBUF将阻塞直至数据可用
      • 非阻塞模式(O_RDWR | O_NONBLOCK):需配合select()使用
  3. 设备能力查询(VIDIOC_QUERYCAP)

    • 功能支持检测:验证设备支持的视频功能
    • 数据结构说明:
      • v4l2_capability:包含驱动名称、设备标识、总线信息及能力标志
    • 关键能力标志:
      • V4L2_CAP_VIDEO_CAPTURE:视频采集支持
      • V4L2_CAP_STREAMING:流式I/O支持(mmap必需)
      • V4L2_CAP_READWRITE:read/write操作支持

伪代码实现:

cpp 复制代码
#include <linux/videodev2.h>
#include <sys/ioctl.h>
// 1. 定义并初始化结构体
struct v4l2_capability cap; // 设备能力结构体
memset(&cap, 0, sizeof(cap)); // 清零,避免未初始化数据
// 2. 调用ioctl查询能力
// ioctl() 参数说明:
// - fd: 设备文件描述符
// - VIDIOC_QUERYCAP: 查询设备能力的命令
// - &cap: 指向v4l2_capability结构体的指针
// 返回值: 成功返回0,失败返回-1
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
 perror("VIDIOC_QUERYCAP failed");
 return -1;
}
// 3. 打印设备信息(调试用)
printf("Driver: %s\n", cap.driver); // 驱动名称
printf("Card: %s\n", cap.card); // 设备名称
printf("Bus: %s\n", cap.bus_info); // 总线信息
printf("Version: %d.%d.%d\n",
 (cap.version >> 16) & 0xFF, // 主版本号
 (cap.version >> 8) & 0xFF, // 次版本号
 cap.version & 0xFF); // 修订版本号
// 4. 检查必需的能力标志
// V4L2_CAP_VIDEO_CAPTURE: 设备必须支持视频采集
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
 fprintf(stderr, "Device does not support video capture\n");
 return -1;
}
// V4L2_CAP_STREAMING: 如果使用mmap方式,必须支持流式I/O
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
 fprintf(stderr, "Device does not support streaming I/O\n");
 return -1;
}
// V4L2_CAP_READWRITE: 如果使用read/write方式,需要检查此标志
if (cap.capabilities & V4L2_CAP_READWRITE) {
 printf("Device supports read/write I/O\n");
}
printf("Device capabilities verified\n");

开发要点:

  1. 枚举格式(可选但推荐):

    • 帮助识别设备支持的视频格式
    • 常用格式:YUYV(未压缩)或MJPEG(压缩高质量)
    • 格式码为四字符标识符(如"YUYV"、"MJPG")
  2. 枚举结果判断:

    • 当errno==EINVAL时表示枚举正常结束
  3. 设置视频格式(VIDIOC_S_FMT):

    • 关键参数配置:
      • 分辨率(如640x480、1280x720)
      • 像素格式(需选择设备支持的格式)
      • 场模式(推荐V4L2_FIELD_INTERLACED或V4L2_FIELD_NONE)
    • 使用v4l2_format结构体:
      • 包含缓冲区类型和格式信息
  • 用于指定采集参数
    伪代码:
cpp 复制代码
// 1. 定义并初始化结构体
struct v4l2_format fmt; // 视频格式结构体
memset(&fmt, 0, sizeof(fmt));
// 2. 设置格式参数
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; // 像素格式(YUYV未压缩格式)
// 或者使用MJPEG格式:
// fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; // 场模式
// 3. 设置格式(驱动可能会调整参数)
// ioctl() 参数说明:
// - fd: 设备文件描述符
// - VIDIOC_S_FMT: 设置格式的命令
// - &fmt: 指向v4l2_format结构体的指针(输入输出参数)
// 返回值: 成功返回0,失败返回-1
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
 perror("VIDIOC_S_FMT failed");
 return -1;
}
// 4. 检查实际设置的格式(驱动可能调整了分辨率)
printf("Requested format: 640x480, YUYV\n");
printf("Actual format: %dx%d, pixelformat: %.4s\n",
 fmt.fmt.pix.width,
 fmt.fmt.pix.height,
 (char*)&fmt.fmt.pix.pixelformat);
// 5. 检查格式是否被调整
if (fmt.fmt.pix.width != 640 || fmt.fmt.pix.height != 480) {
 printf("Warning: Resolution adjusted by driver\n");
}
// 6. 获取图像大小(用于后续缓冲区分配)
unsigned int image_size = fmt.fmt.pix.sizeimage; // 一帧图像的实际大小(字节)
unsigned int bytesperline = fmt.fmt.pix.bytesperline; // 每行的字节数(可能有对齐)
printf("Image size: %u bytes\n", image_size);
printf("Bytes per line: %u\n", bytesperline);
// 7. 计算实际需要的缓冲区大小
// 注意:某些格式(如MJPEG)的图像大小是变化的
// 建议使用驱动返回的sizeimage值

开发注意事项:

  1. 驱动程序可能会自动调整分辨率,设置后需验证实际获取的值
  2. sizeimage字段表示单帧图像的实际存储大小(单位:字节)
  3. bytesperline字段表示每行数据的字节数(可能存在内存对齐填充)
  4. 部分压缩格式(如MJPEG)的图像大小会随内容变化
  5. 当驱动不支持请求的格式时,将返回最接近的可用格式

完整配置流程示例:

cpp 复制代码
// 完整的设备配置函数
static int configure_device(const char *device_path, int width, int height) {
 int fd;
 struct v4l2_capability cap;
 struct v4l2_format fmt;
 
 // 1. 打开设备
 fd = open(device_path, O_RDWR | O_NONBLOCK, 0);
 if (fd < 0) {
 fprintf(stderr, "Cannot open device %s: %s\n", 
 device_path, strerror(errno));
 return -1;
 }
 
 // 2. 查询设备能力
 memset(&cap, 0, sizeof(cap));
 if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
 perror("VIDIOC_QUERYCAP failed");
 close(fd);
 return -1;
 }
 
 // 检查能力
 if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
 fprintf(stderr, "Device does not support video capture\n");
 close(fd);
 return -1;
 }
 
 if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
 fprintf(stderr, "Device does not support streaming I/O\n");
 close(fd);
 return -1;
 }
 
 // 3. 枚举格式(可选)
 struct v4l2_fmtdesc fmt_desc;
 memset(&fmt_desc, 0, sizeof(fmt_desc));
 fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 fmt_desc.index = 0;
 
 printf("Supported formats:\n");
 while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
 printf(" [%d] %s (%.4s)\n", 
 fmt_desc.index,
 fmt_desc.description,
 (char*)&fmt_desc.pixelformat);
 fmt_desc.index++;
 }
 
 // 4. 设置格式
 memset(&fmt, 0, sizeof(fmt));
 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 fmt.fmt.pix.width = width;
 fmt.fmt.pix.height = height;
 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
 fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
 
 if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
 perror("VIDIOC_S_FMT failed");
 close(fd);
 return -1;
 }
 
 printf("Format set: %dx%d, pixelformat: %.4s\n",
 fmt.fmt.pix.width,
 fmt.fmt.pix.height,
 (char*)&fmt.fmt.pix.pixelformat);
 
 return fd; // 返回文件描述符,供后续使用
}

2. 缓冲区流程


操作流程说明:

  1. 缓冲区申请(VIDIOC_REQBUFS)

核心概念:

  • 缓冲区申请:向内核申请指定数量的视频缓冲区,用于存储采集的视频帧数据
  • v4l2_requestbuffers结构体:包含缓冲区数量、类型和内存类型等关键参数

内存类型详解:

  • V4L2_MEMORY_MMAP(推荐):采用内存映射方式,由内核分配缓冲区,用户空间通过mmap访问(实现零拷贝)
  • V4L2_MEMORY_USERPTR:用户指针方式,缓冲区由用户空间自行分配
  • V4L2_MEMORY_DMABUF:DMA缓冲区方式,专为零拷贝传输设计

最佳实践: 建议申请4个缓冲区,通过流水线处理机制提升系统效率
伪代码:

cpp 复制代码
#include <linux/videodev2.h>
// 1. 定义用户空间缓冲区结构体(用于存储映射后的地址)
struct buffer {
 void *start; // 映射后的内存地址
 size_t length; // 缓冲区长度
};

#define BUFFER_COUNT 4 // 申请4个缓冲区
struct buffer *buffers = NULL; // 用户空间缓冲区数组
unsigned int n_buffers = 0; // 实际分配的缓冲区数量

// 2. 定义并初始化v4l2_requestbuffers结构体
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));

// 3. 设置申请参数
req.count = BUFFER_COUNT; // 申请4个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
req.memory = V4L2_MEMORY_MMAP; // 内存映射方式

// 4. 调用ioctl申请缓冲区
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
 perror("VIDIOC_REQBUFS failed");
 return -1;
}

// 5. 检查实际分配的缓冲区数量(驱动可能会调整)
if (req.count < 2) {
 fprintf(stderr, "Insufficient buffer memory (got %d, need at least 2)\n", req.count);
 return -1;
}
n_buffers = req.count; // 保存实际分配的缓冲区数量
printf("Allocated %d buffers\n", n_buffers);

// 6. 分配用户空间缓冲区数组(用于存储映射后的地址)
buffers = calloc(n_buffers, sizeof(*buffers));
if (!buffers) {
 fprintf(stderr, "Out of memory\n");
 return -1;
}

开发要点:

  1. 缓冲区配置建议:

    • 数量设置为4个为宜,过少可能导致丢帧,过多则浪费内存资源
    • 必须验证实际分配的缓冲区数量,驱动程序可能自动调整
  2. 内存优化方案:

    • 采用V4L2_MEMORY_MMAP方式实现零拷贝机制,可获得最佳性能表现
  3. 缓冲区信息查询(VIDIOC_QUERYBUF):

    • 功能:获取每个缓冲区的详细参数(偏移量、长度等),为内存映射做准备
    • 数据结构:v4l2_buffer结构体包含以下关键字段:
      • 缓冲区索引
      • 缓冲区类型
      • 内存类型
      • 偏移量(设备文件中的位置,用于mmap映射)
      • 长度(缓冲区字节大小,通常等于单帧图像尺寸)
      • 时间戳信息

伪代码:

cpp 复制代码
// 循环查询每个缓冲区的信息
for (unsigned int i = 0; i < n_buffers; ++i) {
 struct v4l2_buffer buf; // 缓冲区信息结构体
 
 // 1. 初始化结构体
 memset(&buf, 0, sizeof(buf));
 
 // 2. 设置查询参数
 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 缓冲区类型
 buf.memory = V4L2_MEMORY_MMAP; // 内存类型
 buf.index = i; // 缓冲区索引(0, 1, 2, 3...)
 
 // 3. 调用ioctl查询缓冲区信息
 if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
 perror("VIDIOC_QUERYBUF failed");
 return -1;
 }
 
 // 4. 保存缓冲区信息(用于后续mmap)
 buffers[i].length = buf.length; // 缓冲区长度
 
 // 5. 打印调试信息(可选)
 printf("Buffer %d: offset=0x%x, length=%u\n", 
 i, buf.m.offset, buf.length);
 
 // 注意:buf.m.offset 是偏移量,将在mmap时使用
 // 注意:buf.length 是缓冲区长度,将在mmap时使用
}

开发要点:

  1. 每个缓冲区都需要调用VIDIOC_QUERYBUF获取相关信息
  2. buf.m.offset表示内核缓冲区在设备文件中的偏移地址,该值将用于mmap映射
  3. buf.length指定缓冲区大小,既用于mmap操作也影响后续数据访问

内存映射(mmap)核心概念:

  1. 通过mmap将内核缓冲区直接映射到用户空间,实现零拷贝访问
  2. mmap系统调用可将设备文件的指定区域映射为用户空间内存
  3. 零拷贝机制允许用户程序直接操作内核缓冲区,无需数据复制,性能最优
  4. 映射关系:用户空间buffers[i].start指针直接对应内核缓冲区地址

伪代码:

cpp 复制代码
#include <sys/mman.h> // mmap需要此头文件
// 循环映射每个缓冲区
for (unsigned int i = 0; i < n_buffers; ++i) {
 struct v4l2_buffer buf; // 需要再次查询,或保存之前查询的结果
 
 // 1. 查询缓冲区信息(如果之前没有保存)
 memset(&buf, 0, sizeof(buf));
 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 buf.memory = V4L2_MEMORY_MMAP;
 buf.index = i;
 
 if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
 perror("VIDIOC_QUERYBUF failed");
 return -1;
 }
 
 // 2. 使用mmap映射缓冲区到用户空间
 // mmap参数说明:
 // - NULL: 让系统自动选择映射地址
 // - buf.length: 映射长度(缓冲区大小)
 // - PROT_READ | PROT_WRITE: 可读可写权限
 // - MAP_SHARED: 共享映射(与内核共享内存)
 // - fd: 设备文件描述符
 // - buf.m.offset: 缓冲区在设备文件中的偏移量
 buffers[i].start = mmap(NULL, buf.length,
 PROT_READ | PROT_WRITE,
 MAP_SHARED, fd, buf.m.offset);
 
 // 3. 检查映射是否成功
 if (buffers[i].start == MAP_FAILED) {
 perror("mmap failed");
 return -1;
 }
 
 // 4. 保存缓冲区长度(已保存,这里只是确认)
 buffers[i].length = buf.length;
 
 printf("Buffer %d mapped at %p, length=%zu\n", 
 i, buffers[i].start, buffers[i].length);
}
// 映射完成后,可以通过 buffers[i].start 直接访问帧数据
// 例如:访问第0个缓冲区的数据
// unsigned char *frame_data = (unsigned char *)buffers[0].start;
// frame_data[0] 是第一个字节的数据

开发要点:

  • 调用mmap()会返回映射后的用户空间地址,若失败则返回MAP_FAILED
  • 映射后的内存可直接读写,无需通过系统调用访问
  • 使用MAP_SHARED标志保证内存区域与内核共享
  • 映射成功后,buffers[i].start直接指向内核缓冲区数据

概念知识:
启动视频流:通知驱动开始采集视频数据
VIDIOC_STREAMON:启动指定类型的视频流
缓冲区队列:在启动前,需要先将缓冲区加入队列(VIDIOC_QBUF)
流类型:通常使用V4L2_BUF_TYPE_VIDEO_CAPTURE表示视频采集流
伪代码:

cpp 复制代码
// 1. 先将所有缓冲区加入队列(VIDIOC_QBUF)
// 这一步很重要:驱动需要知道哪些缓冲区可以用来填充数据
for (unsigned int i = 0; i < n_buffers; ++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) < 0) {
 perror("VIDIOC_QBUF failed");
 return -1;
 }
 
 printf("Buffer %d queued\n", i);
}

// 2. 启动视频流
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
 perror("VIDIOC_STREAMON failed");
 return -1;
}

printf("Stream started\n");
// 3. 此时驱动开始采集数据,填充到队列中的缓冲区
// 应用程序可以通过 VIDIOC_DQBUF 取出已填充的缓冲区

开发注意事项:

  1. 初始化阶段需调用VIDIOC_QBUF将所有缓冲区加入队列
  2. 调用VIDIOC_STREAMON后,驱动程序将自动开始采集数据
  3. 运行过程中,可通过VIDIOC_DQBUF获取已采集完成的帧数据

完整缓冲区流程函数封装:

cpp 复制代码
// 完整的缓冲区初始化并启动采集函数
// 功能:整合申请缓冲区、查询缓冲区、映射缓冲区、加入队列、启动采集
// 参数:
// - buffer_count: 申请的缓冲区数量(建议4个)
// 返回值:成功返回0,失败返回-1
static int init_buffers_and_start(int buffer_count) {
 struct v4l2_requestbuffers req;
 unsigned int i;
 enum v4l2_buf_type type;
 
 // ========== 步骤1:申请缓冲区 ==========
 memset(&req, 0, sizeof(req));
 req.count = buffer_count; // 申请指定数量的缓冲区
 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
 req.memory = V4L2_MEMORY_MMAP; // 内存映射方式
 
 if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
 perror("VIDIOC_REQBUFS failed");
 return -1;
 }
 
 // 检查实际分配的缓冲区数量
 if (req.count < 2) {
 fprintf(stderr, "Insufficient buffer memory (got %d, need at least 2)\n",
req.count);
 return -1;
 }
 
 printf("Allocated %d buffers (requested %d)\n", req.count, buffer_count);
 n_buffers = req.count; // 保存实际分配的缓冲区数量
 
 // 分配用户空间缓冲区数组
 buffers = calloc(n_buffers, sizeof(*buffers));
 if (!buffers) {
 fprintf(stderr, "Out of memory\n");
 return -1;
 }
 
 // ========== 步骤2:查询并映射每个缓冲区 ==========
 for (i = 0; i < n_buffers; ++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; // 设置要查询的缓冲区索引
 
 // ioctl是同步系统调用:设置buf.index作为输入,驱动填充buf的其他字段作为输出
 // 调用完成后,buf中已经包含了驱动返回的完整信息(length, offset等)
 if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
 perror("VIDIOC_QUERYBUF failed");
 // 清理已分配的缓冲区
 for (unsigned int j = 0; j < i; ++j) {
 if (buffers[j].start != MAP_FAILED) {
 munmap(buffers[j].start, buffers[j].length);
 }
 }
 free(buffers);
 buffers = NULL;
 return -1;
 }
 
 // 重要:立即将buf中的信息保存到全局数组buffers[i]中
 // 因为buf是局部变量,循环结束后会被销毁,但数据已经保存到buffers[i]了
 buffers[i].length = buf.length; // 保存缓冲区长度
 
 // 映射缓冲区到用户空间(使用buf.m.offset,这是驱动返回的偏移量)
 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 failed");
 // 清理已分配的缓冲区
 for (unsigned int j = 0; j < i; ++j) {
 if (buffers[j].start != MAP_FAILED) {
 munmap(buffers[j].start, buffers[j].length);
 }
 }
 free(buffers);
 buffers = NULL;
 return -1;
 }
 
 printf("Buffer %d: mapped at %p, length=%zu, offset=0x%x\n",
 i, buffers[i].start, buffers[i].length, buf.m.offset);
 
 // 注意:这里buf是局部变量,循环结束后会自动销毁
 // 但数据已经保存到buffers[i]中,所以完全没问题
 }
 
 // ========== 步骤3:将所有缓冲区加入队列 ==========
 for (i = 0; i < n_buffers; ++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; // 指定要加入队列的缓冲区索引
 
 // ioctl是同步系统调用:设置buf.index作为输入,驱动读取这个值
 // 驱动内部会记录:缓冲区i已经加入队列
 // 调用完成后,buf的内容不再需要,因为驱动已经记住了这个缓冲区
 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
 perror("VIDIOC_QBUF failed");
 // 清理资源
 for (unsigned int j = 0; j < n_buffers; ++j) {
 if (buffers[j].start != MAP_FAILED) {
 munmap(buffers[j].start, buffers[j].length);
 }
 }
 free(buffers);
 buffers = NULL;
 return -1;
 }
 
 printf("Buffer %d queued\n", i);
 
 // 注意:这里buf是局部变量,循环结束后会自动销毁
 // 但驱动已经记住了缓冲区i在队列中,所以完全没问题
 }
 
 // ========== 步骤4:启动视频流 ==========
 type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
 perror("VIDIOC_STREAMON failed");
 // 清理资源
 for (i = 0; i < n_buffers; ++i) {
 if (buffers[i].start != MAP_FAILED) {
 munmap(buffers[i].start, buffers[i].length);
 }
 }
 free(buffers);
 buffers = NULL;
 return -1;
 }
 
 printf("Stream started, ready for capture\n");
 return 0;
}
// 使用示例
int main(int argc, char **argv) {
 const char *dev_name = "/dev/video0";
 int buffer_count = 4; // 申请4个缓冲区
 
 // ... 打开设备和设置格式的代码 ...
 
 // 使用完整函数初始化缓冲区并启动采集
 if (init_buffers_and_start(buffer_count) < 0) {
 fprintf(stderr, "Failed to initialize buffers\n");
 close_device();
 return 1;
 }
 
 // 现在可以开始采集了
 // ... 采集循环代码 ...
 
 // 清理资源
 stop_capturing();
 uninit_mmap();
 close_device();
 
 return 0;
}

函数特点:
完整性:整合了缓冲区流程的所有步骤(申请、查询、映射、入队、启动)
错误处理:每个步骤都有错误检查,失败时正确清理已分配的资源
资源管理:确保在任何步骤失败时都能正确释放已分配的资源
易于使用:一个函数调用完成所有缓冲区初始化工作
调试信息:打印每个步骤的执行情况,便于调试
关于局部变量 struct v4l2_buffer buf 的使用说明:

许多初学者对循环中使用局部变量 struct v4l2_buffer buf 存在疑虑,实际上这是完全正确且推荐的做法,原因如下:

1. ioctl是同步系统调用

  • ioctl() 是同步系统调用,操作会立即完成并返回
  • 无需维持 buf 的生命周期,调用结束后即可释放

2.VIDIOC_QUERYBUF的工作机制

c 复制代码
// 输入:设置 buf.index = i(指定查询的缓冲区)
// 输出:驱动填充 buf.length, buf.m.offset 等字段
ioctl(fd, VIDIOC_QUERYBUF, &buf);
// 调用完成后立即保存数据到全局数组
buffers[i].length = buf.length;  // 数据已持久化
buffers[i].start = mmap(..., buf.m.offset);  // 立即建立映射
  • 驱动仅在 ioctl 调用期间填充 buf 字段
  • 数据已及时保存至 buffers[i],局部变量销毁不影响

3. VIDIOC_QBUF的工作机制

c 复制代码
// 输入:设置 buf.index = i(指定入队的缓冲区)
// 驱动:读取 buf.index 并记录"缓冲区i已入队"
ioctl(fd, VIDIOC_QBUF, &buf);
// 调用完成后驱动已保存队列信息
  • 驱动在调用期间读取 buf.index
  • 队列状态由驱动内部维护,与 buf 生命周期无关

4. 为什么不用全局变量?

  • 独立性:每次循环都是独立操作,局部变量完全满足需求
  • 安全性:避免变量重用导致的数据污染
  • 可读性:每次循环重新初始化,代码逻辑更清晰
  • 性能:局部变量在栈上分配,内存效率更高

5. 对比:什么时候需要全局变量?

c 复制代码
// 采集循环中,buf需要保存到循环外使用
struct v4l2_buffer buf; // 在循环外定义或使用全局变量
while (1) {
    ioctl(fd, VIDIOC_DQBUF, &buf);
    // 使用 buf.index 访问 buffers[buf.index].start
    
    // 处理完数据后需要再次使用同一个buf调用 VIDIOC_QBUF
    ioctl(fd, VIDIOC_QBUF, &buf);
}

但在初始化阶段,每次循环都是独立操作,局部变量完全够用。

总结:

✅ 初始化阶段:使用局部变量 struct v4l2_buffer buf; 完全正确

✅ ioctl 是同步的:调用完成后数据已保存或驱动已记录

✅ 局部变量优势:避免变量重用,代码更清晰

❌ 不需要全局变量:除非需要在多个函数间共享同一个buf

3. 采集流程

前提条件:已完成缓冲区流程设置,视频流已启动(已调用VIDIOC_STREAMON),所有缓冲区均已加入队列并开始采集数据。


流程说明:

前置条件: 缓冲区初始化已完成以下步骤:

  1. 申请缓冲区(VIDIOC_REQBUFS)
  2. 查询并映射缓冲区(VIDIOC_QUERYBUF + mmap)
  3. 将所有缓冲区加入队列(VIDIOC_QBUF)
  4. 启动视频流(VIDIOC_STREAMON)

此时驱动已开始采集数据,缓冲区中存在待处理的帧数据。

采集流程: 循环执行以下操作:

  1. 取出已填充的缓冲区(VIDIOC_DQBUF)
  2. 处理缓冲区数据
  3. 将缓冲区重新加入队列

关键概念:

  • 缓冲区出队:从队列中获取包含有效数据的缓冲区
  • VIDIOC_DQBUF:用于取出缓冲区的ioctl命令
  • 阻塞模式:若无可用数据,将等待至数据就绪
  • 非阻塞模式:若无可用数据,立即返回EAGAIN错误
  • 返回信息:v4l2_buffer结构体包含缓冲区索引、数据长度、时间戳等关键信息

1.取出缓冲区
伪代码:

cpp 复制代码
// 从队列取出已填充的缓冲区
int dequeue_buffer(struct v4l2_buffer *buf) {
 // 1. 初始化结构体
 memset(buf, 0, sizeof(*buf));
 
 // 2. 设置缓冲区类型和内存类型
 buf->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 buf->memory = V4L2_MEMORY_MMAP;
 // 注意:不需要设置index,驱动会返回已填充的缓冲区索引
 
 // 3. 从队列取出已填充的缓冲区
 if (ioctl(fd, VIDIOC_DQBUF, buf) < 0) {
 if (errno == EAGAIN) {
 // 非阻塞模式:没有数据可用
 return 0; // 返回0表示没有数据,但不算错误
 }
 perror("VIDIOC_DQBUF failed");
 return -1;
 }
 
 return 1; // 返回1表示成功获取一帧数据
}

开发要点:

  1. 必须通过返回的buf.index访问对应的缓冲区数据
  2. 阻塞模式下,DQBUF操作会持续等待直到获取可用数据
  3. 非阻塞模式下,需结合select()或轮询机制使用
  4. 数据处理完成后,必须将缓冲区重新加入队列

2. 访问用户空间缓冲区
概念知识:
用户空间缓冲区:通过mmap映射的内核缓冲区,可以直接在用户空间访问
缓冲区访问:使用buffers[buf.index].start获取数据指针
数据长度:使用buf.length获取实际数据长度
重要:必须使用buf.index,不要使用循环变量或其他索引
伪代码:

cpp 复制代码
// 访问用户空间缓冲区
void access_buffer(struct v4l2_buffer *buf) {
 // 1. 获取缓冲区索引(由驱动返回)
 unsigned int buffer_index = buf->index;
 
 // 2. 访问用户空间缓冲区
 // 重要:必须使用 buf->index,不要使用循环变量
 void *frame_data = buffers[buffer_index].start; // 数据指针
 size_t frame_size = buf->length; // 数据长度
 
 // 3. 验证缓冲区有效性
 if (frame_data == MAP_FAILED || frame_data == NULL) {
 fprintf(stderr, "Invalid buffer at index %d\n", buffer_index);
 return;
 }
}

开发要点:

  1. 必须通过buf.index访问缓冲区,该索引由驱动提供且保证正确
  2. 缓冲区数据位于用户空间,可直接访问,无需进行额外拷贝操作
  3. 注意数据长度可能动态变化,这在压缩格式(如MJPEG)中尤为常见

3. 打印帧数据信息
概念知识:
帧信息:包括缓冲区索引、数据长度、时间戳等
调试信息:打印帧信息有助于调试和监控采集状态
时间戳:使用buf.timestamp获取帧采集时间
伪代码:

cpp 复制代码
// 打印帧数据信息
void print_frame_info(struct v4l2_buffer *buf, int frame_count) {
 // 1. 打印基本帧信息
 printf("========================================\n");
 printf("Frame #%d\n", frame_count);
 printf(" Buffer Index: %d\n", buf->index);
 printf(" Data Length: %u bytes (%.2f KB)\n", 
 buf->length, buf->length / 1024.0);
 
 // 2. 打印时间戳
 struct timeval *ts = &buf->timestamp;
 printf(" Timestamp: %ld.%06ld seconds\n", 
 ts->tv_sec, ts->tv_usec);
 
 // 3. 打印缓冲区地址(用于调试)
 void *frame_data = buffers[buf->index].start;
 printf(" Buffer Address: %p\n", frame_data);
 
 // 4. 打印数据格式信息(如果之前保存了)
 if (current_pixelformat == V4L2_PIX_FMT_YUYV) {
 printf(" Format: YUYV (YUV 4:2:2)\n");
 printf(" Resolution: %dx%d\n", current_width, current_height);
 } else if (current_pixelformat == V4L2_PIX_FMT_MJPEG) {
 printf(" Format: MJPEG (Compressed)\n");
 printf(" Resolution: %dx%d\n", current_width, current_height);
 }
 
 printf("========================================\n");
}

开发要点:

  1. 打印帧信息便于调试和实时监控
  2. 利用时间戳计算帧率指标
  3. 通过数据长度校验确保数据完整性

4. 保存图片到本地
概念知识:
图像保存:根据像素格式选择不同的保存方式
YUYV格式:可以保存为原始数据,或转换为RGB后保存为JPEG
MJPEG格式:可以直接保存为JPEG文件
文件操作:使用标准C库函数进行文件读写
完整代码示例:

cpp 复制代码
// 全局变量:保存当前格式信息(在set_format时设置)
static unsigned int current_pixelformat = 0;
static unsigned int current_width = 0;
static unsigned int current_height = 0;
static int frame_count = 0;

// 保存MJPEG数据为JPEG文件
static int save_mjpeg_frame(const char *filename, void *data, size_t size) {
 FILE *fp = fopen(filename, "wb");
 if (!fp) {
 fprintf(stderr, "Cannot open file %s: %s\n", filename, strerror(errno));
 return -1;
 }
 
 size_t written = fwrite(data, 1, size, fp);
 fclose(fp);
 
 if (written != size) {
 fprintf(stderr, "Failed to write all data to %s\n", filename);
 return -1;
 }
 
 printf("Saved MJPEG frame to %s (%zu bytes)\n", filename, size);
 return 0;
}
// 完整的采集和处理函数
static int capture_and_save_frame(void) {
 struct v4l2_buffer buf;
 char filename[256];
 
 // 1. 从队列取出已填充的缓冲区
 memset(&buf, 0, sizeof(buf));
 buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 buf.memory = V4L2_MEMORY_MMAP;
 
 if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
 if (errno == EAGAIN) {
 return 0; // 没有数据,非阻塞返回
 }
 fprintf(stderr, "VIDIOC_DQBUF failed: %s\n", strerror(errno));
 return -1;
 }
 
 // 2. 访问用户空间缓冲区
 // 重要:必须使用 buf.index 来访问对应的缓冲区
 void *frame_data = buffers[buf.index].start;
 size_t frame_size = buf.length;
 
 // 3. 打印帧数据信息
 printf("========================================\n");
 printf("Frame #%d\n", frame_count);
 printf(" Buffer Index: %d\n", buf.index);
 printf(" Data Length: %zu bytes (%.2f KB)\n", 
 frame_size, frame_size / 1024.0);
 printf(" Buffer Address: %p\n", frame_data);
 printf(" Timestamp: %ld.%06ld seconds\n", 
 buf.timestamp.tv_sec, buf.timestamp.tv_usec);
 
 // 4. 根据像素格式保存图像
 if (current_pixelformat == V4L2_PIX_FMT_MJPEG) {
 // MJPEG格式:直接保存为JPEG文件
 snprintf(filename, sizeof(filename), "frame_%03d.jpg", frame_count);
 save_mjpeg_frame(filename, frame_data, frame_size);
 printf(" Format: MJPEG -> Saved as %s\n", filename);
 } else if (current_pixelformat == V4L2_PIX_FMT_YUYV) {
 // YUYV格式:保存原始数据
 snprintf(filename, sizeof(filename), "frame_%03d.yuyv", frame_count);
 save_raw_frame(filename, frame_data, frame_size);
 printf(" Format: YUYV -> Saved as %s\n", filename);
 printf(" Resolution: %dx%d\n", current_width, current_height);
 
 // 可选:转换为JPEG保存(需要libjpeg库)
 // snprintf(filename, sizeof(filename), "frame_%03d.jpg", frame_count);
 // save_yuyv_as_jpeg(filename, (unsigned char *)frame_data, 
 // current_width, current_height, 90);
 } else {
 // 其他格式:保存原始数据
 snprintf(filename, sizeof(filename), "frame_%03d.raw", frame_count);
 save_raw_frame(filename, frame_data, frame_size);
 printf(" Format: Unknown -> Saved as %s\n", filename);
 }
 
 printf("========================================\n");
 frame_count++;
 
 // 5. 将缓冲区重新加入队列,继续采集
 if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
 fprintf(stderr, "VIDIOC_QBUF failed: %s\n", strerror(errno));
 return -1;
 }
 
 return 1; // 返回1表示成功处理一帧
}

// 采集循环主函数
static int capture_loop(int max_frames) {
 printf("Start capturing %d frames...\n", max_frames);
 frame_count = 0;
 
 while (frame_count < max_frames) {
 int ret = capture_and_save_frame();
 if (ret < 0) {
 fprintf(stderr, "Capture failed\n");
 return -1;
 }
 // ret == 0 表示非阻塞模式下没有数据,继续等待
 }
 
 printf("Capture finished, saved %d frames\n", frame_count);
 return 0;
}

开发要点:

  1. 缓冲区访问必须通过buf.index实现,禁止使用循环变量或其他索引方式
  2. 需根据像素格式差异采用对应的数据保存方案
  3. 数据处理完成后务必及时将缓冲区重新加入队列
  4. 需特别注意数据长度可能发生变化(压缩格式下尤为明显)
    5. 停止采集(VIDIOC_STREAMOFF)
    概念知识:
    停止视频流:通知驱动停止采集数据
    VIDIOC_STREAMOFF:停止指定类型视频流的ioctl命令
    流类型:使用V4L2_BUF_TYPE_VIDEO_CAPTURE表示视频采集流
    停止后:驱动不再填充新的数据到缓冲区
    伪代码:
cpp 复制代码
// 停止视频流
int stop_streaming(void) {
 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
 
 // ioctl() 参数说明:
 // - fd: 设备文件描述符
 // - VIDIOC_STREAMOFF: 停止视频流的命令
 // - &type: 指向流类型的指针
 // 返回值: 成功返回0,失败返回-1
 if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
 perror("VIDIOC_STREAMOFF failed");
 return -1;
 }
 
 printf("Stream stopped\n");
 return 0;
}
// 在采集循环结束后调用
stop_streaming();

开发要点:

  1. 停止采集后,驱动将停止填充新数据
  2. 停止操作前,需确保所有缓冲区处理完毕
  3. 停止操作完成后,可安全释放缓冲区

6. 释放缓冲区(munmap)
概念知识:
取消内存映射:释放通过mmap映射的内存
munmap系统调用:取消之前mmap建立的映射关系
释放顺序:先停止采集,再释放缓冲区
释放所有缓冲区:需要为每个映射的缓冲区调用munmap
伪代码:

cpp 复制代码
#include <sys/mman.h> // munmap需要此头文件
// 释放所有缓冲区

void unmap_buffers(void) {
 unsigned int i;
 
 // 循环释放每个缓冲区
 for (i = 0; i < n_buffers; ++i) {
 // 检查缓冲区是否已映射
 if (buffers[i].start != MAP_FAILED && buffers[i].start != NULL) {
 // munmap() 参数说明:
 // - buffers[i].start: 映射的内存地址
 // - buffers[i].length: 映射的长度
 // 返回值: 成功返回0,失败返回-1
 if (munmap(buffers[i].start, buffers[i].length) < 0) {
 perror("munmap failed");
 } else {
 printf("Buffer %d unmapped\n", i);
 }
 
 // 清空指针,避免重复释放
 buffers[i].start = NULL;
 buffers[i].length = 0;
 }
 }
 
 // 释放缓冲区数组
 if (buffers) {
 free(buffers);
 buffers = NULL;
 }
 
 n_buffers = 0;
 printf("All buffers released\n");
}

// 在停止采集后调用
stop_streaming();
unmap_buffers();

开发注意事项:

  1. 必须为每个映射的缓冲区执行munmap操作
  2. 操作前需验证指针有效性,防止重复释放
  3. 释放内存后应及时置空指针,避免产生野指针
  4. 操作顺序要求:先停止数据采集,再进行缓冲区释放

7. 关闭设备(close)
概念知识:
关闭设备文件:释放文件描述符
close系统调用:关闭打开的设备文件
资源清理:关闭后,设备可以被其他程序使用
清理顺序:先停止采集,再释放缓冲区,最后关闭设备
伪代码:

cpp 复制代码
#include <unistd.h> // close需要此头文件
// 关闭设备
void close_device(void) {
 if (fd >= 0) {
 // close() 参数说明:
 // - fd: 设备文件描述符
 // 返回值: 成功返回0,失败返回-1
 if (close(fd) < 0) {
 perror("close device failed");
 } else {
 printf("Device closed\n");
 }
 
 fd = -1; // 清空文件描述符,避免重复关闭
 }
}

// 完整的资源清理流程
void cleanup(void) {
 // 1. 停止采集
 stop_streaming();
 
 // 2. 释放缓冲区
 unmap_buffers();
 
 // 3. 关闭设备
 close_device();
 
 printf("Cleanup completed\n");
}

开发要点:

  1. 设备关闭后,文件描述符将自动失效
  2. 关闭操作完成后应及时清空fd,防止出现重复关闭的情况
  3. 资源清理必须遵循以下顺序:
    • 首先停止数据采集
    • 然后释放缓冲区资源
    • 最后关闭设备

1.5 完整示例代码

1.5.1 采集示例

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <stdlib.h>
#include <linux/videodev2.h>
#include <errno.h>
#include <unistd.h>
#include <sys/mman.h>
#define CAM_PATH "/dev/video0"
// 1. 定义用户空间缓冲区结构体(用于存储映射后的地址)
struct buffer
{
    void *start;   // 映射后的内存地址
    size_t length; // 缓冲区长度
};

#define BUFFER_COUNT 4         // 申请4个缓冲区
struct buffer *buffers = NULL; // 用户空间缓冲区数组
unsigned int n_buffers = 0;    // 实际分配的缓冲区数量
int fd;

// 完整的设备配置函数
static int configure_device(int width, int height)
{
    struct v4l2_capability cap;
    struct v4l2_format fmt;
    // 1. 打开设备
    // fd = open(CAM_PATH, O_RDWR | O_NONBLOCK, 0);
    fd = open(CAM_PATH, O_RDWR, 0);
    if (fd < 0)
    {
        fprintf(stderr, "Cannot open device %s: %s\n",
                CAM_PATH, strerror(errno));
        return -1;
    }
    // 2. 查询设备能力
    memset(&cap, 0, sizeof(cap));
    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0)
    {
        perror("VIDIOC_QUERYCAP failed");
        close(fd);
        return -1;
    }
    // 检查能力
    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
    {
        fprintf(stderr, "Device does not support video capture\n");
        close(fd);
        return -1;
    }
    if (!(cap.capabilities & V4L2_CAP_STREAMING))
    {
        fprintf(stderr, "Device does not support streaming I/O\n");
        close(fd);
        return -1;
    }
    // 3. 枚举格式(可选)
    struct v4l2_fmtdesc fmt_desc;
    memset(&fmt_desc, 0, sizeof(fmt_desc));
    fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt_desc.index = 0;
    printf("Supported formats:\n");
    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0)
    {
        printf(" [%d] %s (%.4s)\n",
               fmt_desc.index,
               fmt_desc.description,
               (char *)&fmt_desc.pixelformat);
        fmt_desc.index++;
    }
    // 4. 设置格式
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = width;
    fmt.fmt.pix.height = height;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
    if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0)
    {
        perror("VIDIOC_S_FMT failed");
        close(fd);
        return -1;
    }
    printf("Format set: %dx%d, pixelformat: %.4s\n",
           fmt.fmt.pix.width,
           fmt.fmt.pix.height,
           (char *)&fmt.fmt.pix.pixelformat);
    return fd; // 返回文件描述符,供后续使用
}

// 完整的缓冲区初始化并启动采集函数
// 功能:整合申请缓冲区、查询缓冲区、映射缓冲区、加入队列、启动采集
// 参数:
// - buffer_count: 申请的缓冲区数量(建议4个)
// 返回值:成功返回0,失败返回-1
static int init_buffers_and_start(int buffer_count)
{
    struct v4l2_requestbuffers req;
    unsigned int i;
    enum v4l2_buf_type type;
    // ========== 步骤1:申请缓冲区 ==========
    memset(&req, 0, sizeof(req));
    req.count = buffer_count;               // 申请指定数量的缓冲区
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 视频采集类型
    req.memory = V4L2_MEMORY_MMAP;          // 内存映射方式
    if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)
    {
        perror("VIDIOC_REQBUFS failed");
        return -1;
    }
    // 检查实际分配的缓冲区数量
    if (req.count < 2)
    {
        fprintf(stderr, "Insufficient buffer memory (got %d, need at least 2)\n",
                req.count);
        return -1;
    }
    printf("Allocated %d buffers (requested %d)\n", req.count, buffer_count);
    n_buffers = req.count; // 保存实际分配的缓冲区数量
    // 分配用户空间缓冲区数组
    buffers = calloc(n_buffers, sizeof(*buffers));
    if (!buffers)
    {
        fprintf(stderr, "Out of memory\n");
        return -1;
    }
    // ========== 步骤2:查询并映射每个缓冲区 ==========
    for (i = 0; i < n_buffers; ++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; // 设置要查询的缓冲区索引
        // ioctl是同步系统调用:设置buf.index作为输入,驱动填充buf的其他字段作为输出
        // 调用完成后,buf中已经包含了驱动返回的完整信息(length, offset等)
        if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0)
        {
            perror("VIDIOC_QUERYBUF failed");
            // 清理已分配的缓冲区
            for (unsigned int j = 0; j < i; ++j)
            {
                if (buffers[j].start != MAP_FAILED)
                {
                    munmap(buffers[j].start, buffers[j].length);
                }
            }
            free(buffers);
            buffers = NULL;
            return -1;
        }
        // 重要:立即将buf中的信息保存到全局数组buffers[i]中
        // 因为buf是局部变量,循环结束后会被销毁,但数据已经保存到buffers[i]了
        buffers[i].length = buf.length; // 保存缓冲区长度
        // 映射缓冲区到用户空间(使用buf.m.offset,这是驱动返回的偏移量)
        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 failed");
            // 清理已分配的缓冲区
            for (unsigned int j = 0; j < i; ++j)
            {
                if (buffers[j].start != MAP_FAILED)
                {
                    munmap(buffers[j].start, buffers[j].length);
                }
            }
            free(buffers);
            buffers = NULL;
            return -1;
        }
        printf("Buffer %d: mapped at %p, length=%zu, offset=0x%x\n",
               i, buffers[i].start, buffers[i].length, buf.m.offset);
        // 注意:这里buf是局部变量,循环结束后会自动销毁
        // 但数据已经保存到buffers[i]中,所以完全没问题
    }
    // ========== 步骤3:将所有缓冲区加入队列 ==========
    for (i = 0; i < n_buffers; ++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; // 指定要加入队列的缓冲区索引
        // ioctl是同步系统调用:设置buf.index作为输入,驱动读取这个值
        // 驱动内部会记录:缓冲区i已经加入队列
        // 调用完成后,buf的内容不再需要,因为驱动已经记住了这个缓冲区
        if (ioctl(fd, VIDIOC_QBUF, &buf) < 0)
        {
            perror("VIDIOC_QBUF failed");
            // 清理资源
            for (unsigned int j = 0; j < n_buffers; ++j)
            {
                if (buffers[j].start != MAP_FAILED)
                {
                    munmap(buffers[j].start, buffers[j].length);
                }
            }
            free(buffers);
            buffers = NULL;
            return -1;
        }
        printf("Buffer %d queued\n", i);
        // 注意:这里buf是局部变量,循环结束后会自动销毁
        // 但驱动已经记住了缓冲区i在队列中,所以完全没问题
    }
    // ========== 步骤4:启动视频流 ==========
    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(fd, VIDIOC_STREAMON, &type) < 0)
    {
        perror("VIDIOC_STREAMON failed");
        // 清理资源
        for (i = 0; i < n_buffers; ++i)
        {
            if (buffers[i].start != MAP_FAILED)
            {
                munmap(buffers[i].start, buffers[i].length);
            }
        }
        free(buffers);
        buffers = NULL;
        return -1;
    }
    printf("Stream started, ready for capture\n");
    return 0;
}

int start_capture(int cnt)
{
    struct v4l2_buffer buf;
    char filename[12];
    int i = 0;

    while (cnt--)
    {
        memset(&buf, 0, sizeof(buf));
        memset(filename, 0, sizeof(filename));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0)
        {
            if (errno == EAGAIN)
            {
                // 非阻塞模式:没有数据可用
                continue; // 继续等待下一帧
            }
            perror("VIDIOC_DQBUF failed");
            return -1;
        }
        printf("index=%d,dataLength=%d\n", buf.index, buf.length);
        int index = buf.index;
        void *buffer_frame = buffers[buf.index].start;
        int size = buf.length;

        // 保存一张图片
        sprintf(filename, "%d_.jpg", ++i);
        FILE *fp = fopen(filename, "wb");
        if (fp == NULL)
        {
            fprintf(stderr, "Cannot open file %s: %s\n", filename, strerror(errno));
            // 即使保存失败,也要将缓冲区重新加入队列
        }
        else
        {
            size_t written = fwrite(buffer_frame, 1, size, fp);
            fclose(fp);
            if (written != size)
            {
                fprintf(stderr, "Failed to write all data to %s\n", filename);
            }
            else
            {
                printf("Saved frame to %s (%zu bytes)\n", filename, written);
            }
        }

        // 重新让内核缓存入队,以备下一轮使用
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = index;
        if (ioctl(fd, VIDIOC_QBUF, &buf) < 0)
        {
            perror("VIDIOC_QBUF failed");
            return -1;
        }

        // 这里是虚假sleep,用户空间每隔2s从底层拿到数据,本质上底层是没有sleep
        sleep(2);
    }

    printf("Capture completed, saved %d frames\n", i);
    return 0;
}

// 停止视频流
static int stop_streaming(void)
{
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0)
    {
        perror("VIDIOC_STREAMOFF failed");
        return -1;
    }

    printf("Stream stopped\n");
    return 0;
}

// 释放所有缓冲区
static void unmap_buffers(void)
{
    unsigned int i;
    if (buffers == NULL)
    {
        return;
    }
    // 循环释放每个缓冲区
    for (i = 0; i < n_buffers; ++i)
    {
        // 检查缓冲区是否已映射
        if (buffers[i].start != MAP_FAILED && buffers[i].start != NULL)
        {
            // 取消内存映射
            if (munmap(buffers[i].start, buffers[i].length) < 0)
            {
                perror("munmap failed");
            }
            else
            {
                printf("Buffer %d unmapped\n", i);
            }

            // 清空指针,避免重复释放
            buffers[i].start = NULL;
            buffers[i].length = 0;
        }
    }
    // 释放缓冲区数组
    free(buffers);
    buffers = NULL;
    n_buffers = 0;
    printf("All buffers released\n");
}

// 关闭设备
static void close_device(void)
{
    if (fd >= 0)
    {
        if (close(fd) < 0)
        {
            perror("close device failed");
        }
        else
        {
            printf("Device closed\n");
        }

        fd = -1; // 清空文件描述符,避免重复关闭
    }
}

// 完整的资源清理函数
static void cleanup(void)
{
    // 1. 停止采集
    stop_streaming();

    // 2. 释放缓冲区
    unmap_buffers();

    // 3. 关闭设备
    close_device();

    printf("Cleanup completed\n");
}

int main()
{
    int ret = 0;
    // 1. 配置摄像头图像参数,分辨率,格式
    fd = configure_device(640, 480);
    if (fd < 0)
    {
        fprintf(stderr, "Failed to configure device\n");
        return 1;
    }
    // 2. 申请缓冲区并完成用户控件缓存的映射,把内核缓冲区入队,启动视频数据采集
    if (init_buffers_and_start(4) < 0)
    {
        fprintf(stderr, "Failed to initialize buffers\n");
        close_device();
        return 1;
    }
    // 3. 获得参数张图片
    if (start_capture(5) < 0)
    {
        fprintf(stderr, "Failed to capture frames\n");
        cleanup();
        return 1;
    }
    // 4. 清理资源(按照正确顺序:停止采集 -> 释放缓冲区 -> 关闭设备)
    cleanup();
    return 0;
}
相关推荐
巨大八爪鱼6 天前
瑞芯微RV1106通过MIPI CSI-2 D-PHY接口驱动OV5640摄像头并拍摄照片
linux·ov5640·mipi·v4l2·csi2-dphy
STCNXPARM3 个月前
Linux camera之Media子系统
linux·camera·v4l2·media子系统
好多渔鱼好多4 个月前
【IPC】V4L2 数据结构详解
ipc·v4l2·video for linux·ipc摄像头
赖small强5 个月前
【音视频开发】Linux V4L2 零拷贝 (Zero-Copy) 机制深度解析
v4l2·dma-buf·zero-copy·dma-fd
赖small强5 个月前
【音视频开发】Linux UVC (USB Video Class) 驱动框架深度解析
linux·音视频·v4l2·uvc
赖small强5 个月前
【音视频开发】Linux V4L2 (Video for Linux 2) 驱动框架深度解析白皮书
linux·音视频·v4l2·设备节点管理·视频缓冲队列·videobuf2
赖small强5 个月前
【音视频开发】CMOS Sensor图像采集原理及Linux主控ISP处理流程
linux·音视频·cmos·isp·v4l2
chen_zn956 个月前
V4L2框架介绍
linux·usb·摄像头·v4l2·视频设备
小柯博客6 个月前
STM32MP1 没有硬件编解码,如何用 CPU 实现 H.264 编码支持 WebRTC?
c语言·stm32·嵌入式硬件·webrtc·h.264·h264·v4l2