[特殊字符] 摄像头模块(七):编写 V4L2 设备框架

📘 摄像头模块(七):编写 V4L2 设备框架

前置:必须理解摄像头模块(六):结构体抽象与设备管理


文章目录

  • [📘 摄像头模块(七):编写 V4L2 设备框架](#📘 摄像头模块(七):编写 V4L2 设备框架)
    • [🎯 本节核心目标](#🎯 本节核心目标)
    • [📂 本节涉及的文件](#📂 本节涉及的文件)
    • [🧱 V4L2 初始化的"六步法"(框架图)](#🧱 V4L2 初始化的“六步法”(框架图))
    • [🔍 逐个步骤详解(包含你碰到的困惑)](#🔍 逐个步骤详解(包含你碰到的困惑))
      • [步骤 0:全局支持的格式列表](#步骤 0:全局支持的格式列表)
      • [步骤 1:打开设备](#步骤 1:打开设备)
      • [步骤 2:查询设备能力(VIDIOC_QUERYCAP)](#步骤 2:查询设备能力(VIDIOC_QUERYCAP))
      • [步骤 3:枚举并选择像素格式(VIDIOC_ENUM_FMT)](#步骤 3:枚举并选择像素格式(VIDIOC_ENUM_FMT))
      • [步骤 4:设置图像格式(VIDIOC_S_FMT)](#步骤 4:设置图像格式(VIDIOC_S_FMT))
      • [步骤 5:申请缓冲区(VIDIOC_REQBUFS)并 mmap](#步骤 5:申请缓冲区(VIDIOC_REQBUFS)并 mmap)
      • [步骤 6:把缓冲区放入驱动队列(VIDIOC_QBUF)--- 仅 streaming 模式](#步骤 6:把缓冲区放入驱动队列(VIDIOC_QBUF)— 仅 streaming 模式)
    • [🧪 其他辅助函数(暂不深究,(九) 会讲)](#🧪 其他辅助函数(暂不深究,(九) 会讲))
    • [📌 最容易犯的错误(我在学习中踩过的坑)](#📌 最容易犯的错误(我在学习中踩过的坑))
    • [🧭 完整代码清单(v4l2.c 骨架,核心部分已注释)](#🧭 完整代码清单(v4l2.c 骨架,核心部分已注释))
    • [✅ 本节自测题(检验是否真的掌握框架)](#✅ 本节自测题(检验是否真的掌握框架))
    • [🖼️ 图解:V4L2 初始化与数据流](#🖼️ 图解:V4L2 初始化与数据流)
    • [💬 总结](#💬 总结)

🎯 本节核心目标

把 📘摄像头模块(六):结构体抽象与设备管理 中定义的 VideoOpr 函数指针全部实现 ,让 V4L2 摄像头能够被我们的统一框架调用。

最终得到一个 g_tV4l2VideoOpr 结构体,并通过 V4l2Init() 注册到全局链表中。

学习重点不是记忆每个 ioctl 的参数,而是掌握 V4L2 设备初始化的"标准六步法" ------ 这是所有 V4L2 摄像头程序的通用骨架。


📂 本节涉及的文件

文件 作用
video/v4l2.c V4L2 操作的具体实现(核心)
include/video_manager.h 定义结构体,(六) 已完成
video/video_manager.c 链表管理,(六) 已完成

你只需要关注 v4l2.c,其他文件是之前已经搭好的框架。


🧱 V4L2 初始化的"六步法"(框架图)

text

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    V4L2 设备初始化流程                        │
└─────────────────────────────────────────────────────────────┘

1. open("/dev/videoX")        → 获得文件描述符 fd
          │
2. VIDIOC_QUERYCAP            → 查询能力,确认是视频捕获设备
          │
3. VIDIOC_ENUM_FMT 循环       → 枚举格式,找到支持的像素格式(YUYV/MJPEG/RGB565)
          │
4. VIDIOC_S_FMT               → 设置分辨率、像素格式(读回实际生效值)
          │
5. VIDIOC_REQBUFS             → 向内核申请若干个缓冲区(NB_BUFFER=4)
          │
   mmap 每个缓冲区             → 映射到用户空间,得到虚拟地址
          │
6. (streaming 模式)           → VIDIOC_QBUF 把所有缓冲区放入驱动队列
          │
   ─────── 至此摄像头已准备好,可调用 VIDIOC_STREAMON 启动采集 ───────

这个六步顺序是固定的,缺一步或顺序错了都会导致无法采集。


🔍 逐个步骤详解(包含你碰到的困惑)

步骤 0:全局支持的格式列表

c

复制代码
static int g_aiSupportedFormats[] = {
    V4L2_PIX_FMT_YUYV,   // YUYV 4:2:2 打包格式
    V4L2_PIX_FMT_MJPEG,  // MJPEG 压缩格式
    V4L2_PIX_FMT_RGB565  // RGB565 格式
};

static int isSupportThisFormat(int iPixelFormat)
{
    int i;
    for (i = 0; i < sizeof(g_aiSupportedFormats)/sizeof(g_aiSupportedFormats[0]); i++)
        if (g_aiSupportedFormats[i] == iPixelFormat)
            return 1;
    return 0;
}

❓ 你为什么困惑sizeof(g_aiSupportedFormats)/sizeof(g_aiSupportedFormats[0]) 是什么意思?
✅ 解释sizeof(数组) 返回整个数组占用的字节数,sizeof(数组[0]) 返回一个元素的字节数。两者相除就是元素个数。

这样当你增加新的格式(比如 V4L2_PIX_FMT_NV12)时,只需在数组中添加一行,循环自动适应,不用手动修改数字 3。

这是 C 语言中遍历静态数组的常用技巧。


步骤 1:打开设备

c

复制代码
iFd = open(strDevName, O_RDWR);
if (iFd < 0) return -1;
ptVideoDevice->iFd = iFd;

目的 :获得文件描述符,后续所有 ioctl 都需要它。
strDevName 通常是 /dev/video0/dev/video1


步骤 2:查询设备能力(VIDIOC_QUERYCAP)

c

复制代码
struct v4l2_capability tV4l2Cap;
ioctl(iFd, VIDIOC_QUERYCAP, &tV4l2Cap);
if (!(tV4l2Cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
    goto err;

目的

  • 确认设备是视频捕获设备(不是 VBI 或输出设备)。
  • 同时可以检查它支持哪种 I/O 方式:V4L2_CAP_STREAMING(内存映射) 或 V4L2_CAP_READWRITE(传统读写)。

❓ 你看到的代码里为什么有两个 ioctl 中间加了一个 memset?

那是原作者的冗余写法,实际只需要一次 ioctl。你可以忽略第二组,理解为"先获取一次,清空后再获取一次"的无害操作。


步骤 3:枚举并选择像素格式(VIDIOC_ENUM_FMT)

c

复制代码
struct v4l2_fmtdesc tFmtDesc;
memset(&tFmtDesc, 0, sizeof(tFmtDesc));
tFmtDesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tFmtDesc.index = 0;
while (ioctl(iFd, VIDIOC_ENUM_FMT, &tFmtDesc) == 0) {
    if (isSupportThisFormat(tFmtDesc.pixelformat)) {
        ptVideoDevice->iPixelFormat = tFmtDesc.pixelformat;
        break;
    }
    tFmtDesc.index++;
}

目的 :摄像头可能支持十几种格式,我们需要找出一种摄像头支持且我们程序也支持 的格式(YUYV、MJPEG 或 RGB565)。
index 从 0 开始递增,每次调用返回一种格式的描述,直到返回非 0(没有更多格式)。

❓ 为什么不直接用 VIDIOC_S_FMT 设置想要的格式?

因为有些摄像头不支持你想要的格式,必须先枚举确认。否则设置失败会导致无法采集。


步骤 4:设置图像格式(VIDIOC_S_FMT)

c

复制代码
struct v4l2_format tV4l2Fmt;
memset(&tV4l2Fmt, 0, sizeof(tV4l2Fmt));
tV4l2Fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tV4l2Fmt.fmt.pix.pixelformat = ptVideoDevice->iPixelFormat;
tV4l2Fmt.fmt.pix.width       = iLcdWidth;   // LCD 的宽度
tV4l2Fmt.fmt.pix.height      = iLcdHeigt;   // LCD 的高度
tV4l2Fmt.fmt.pix.field       = V4L2_FIELD_ANY;
ioctl(iFd, VIDIOC_S_FMT, &tV4l2Fmt);
ptVideoDevice->iWidth  = tV4l2Fmt.fmt.pix.width;
ptVideoDevice->iHeight = tV4l2Fmt.fmt.pix.height;

目的 :告诉驱动我们希望的分辨率和格式。驱动可能会调整(比如摄像头最大只支持 640x480,而 LCD 是 1024x600),它会修改结构体中的 width/height 为实际支持的值。必须读回这些值,后续所有操作都基于实际分辨率。

❓ 为什么设置成 LCD 的分辨率?

为了减少后期缩放的工作量。如果摄像头能直接输出与 LCD 相同的分辨率,就可以省去 PicZoom 步骤,提高效率。

GetDispResolution 是什么?

它从显示模块(08.1 之前注册的 fb 设备)中取出 LCD 的宽度、高度和每像素位数。这个函数在 disp_manager.c 中实现,需要提前调用 SelectAndInitDefaultDispDev("fb")


步骤 5:申请缓冲区(VIDIOC_REQBUFS)并 mmap

c

复制代码
struct v4l2_requestbuffers tV4l2ReqBuffs;
tV4l2ReqBuffs.count = NB_BUFFER;   // 4
tV4l2ReqBuffs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tV4l2ReqBuffs.memory = V4L2_MEMORY_MMAP;
ioctl(iFd, VIDIOC_REQBUFS, &tV4l2ReqBuffs);
ptVideoDevice->iVideoBufCnt = tV4l2ReqBuffs.count;  // 实际可能少于 4

for (i = 0; i < ptVideoDevice->iVideoBufCnt; i++) {
    struct v4l2_buffer tV4l2Buf;
    tV4l2Buf.index = i;
    tV4l2Buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    tV4l2Buf.memory = V4L2_MEMORY_MMAP;
    ioctl(iFd, VIDIOC_QUERYBUF, &tV4l2Buf);   // 获得每个缓冲区的物理偏移和长度
    ptVideoDevice->pucVideBuf[i] = mmap(NULL, tV4l2Buf.length, PROT_READ, MAP_SHARED, iFd, tV4l2Buf.m.offset);
    ptVideoDevice->iVideoBufMaxLen = tV4l2Buf.length;
}

目的

  • REQBUFS:告诉内核我们要使用多少个缓冲区(通常 4 个),内核会分配相应数量的 buffers(可能更少,用返回值更新)。
  • QUERYBUF:获取每个缓冲区的信息(长度、物理偏移)。
  • mmap:将内核空间的缓冲区映射到用户空间,这样程序就可以直接读写这些内存。

❓ 为什么要用多个缓冲区?

实现流水线:驱动正在往 buffer 0 里写数据时,应用程序可以处理 buffer 1 里的前一帧;驱动填满 buffer 0 后切换去填 buffer 1,应用程序则处理 buffer 0。多个缓冲区可以避免丢帧。


步骤 6:把缓冲区放入驱动队列(VIDIOC_QBUF)--- 仅 streaming 模式

c

复制代码
for (i = 0; i < ptVideoDevice->iVideoBufCnt; i++) {
    struct v4l2_buffer tV4l2Buf;
    tV4l2Buf.index = i;
    tV4l2Buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    tV4l2Buf.memory = V4L2_MEMORY_MMAP;
    ioctl(iFd, VIDIOC_QBUF, &tV4l2Buf);
}

目的 :将空闲缓冲区"借给"驱动,驱动采集到一帧后就会填入其中一个缓冲区,并标记为"已满"。

应用程序后面通过 VIDIOC_DQBUF 取出已满的缓冲区,处理完后再次 QBUF 归还。

❓ 如果设备只支持 read/write 模式怎么办?

代码中也有对应的分支:

  • 只分配一个缓冲区(malloc)
  • GetFrame 直接调用 read(fd, buf, size)
  • PutFrame 什么都不做
    这种方式简单但效率低,项目中实际使用 streaming 模式。

🧪 其他辅助函数(暂不深究,(九) 会讲)

函数 作用 详细讲解
V4l2GetFrameForStreaming 等待数据,取出已填满的缓冲区 (九)
V4l2PutFrameForStreaming 使用完后归还缓冲区 (九)
V4l2StartDevice 调用 VIDIOC_STREAMON 启动采集 (九)
V4l2StopDevice 调用 VIDIOC_STREAMOFF 停止采集 (九)
V4l2GetFormat 返回当前像素格式 简单

你现在只需要知道它们存在并知道作用,不需要死记代码。


📌 最容易犯的错误(我在学习中踩过的坑)

  1. 忘记 mmap 之前必须先 REQBUFS
    顺序错了 QUERYBUF 会失败。
  2. 没有读回 VIDIOC_S_FMT 后的实际宽高
    后面 mmap 长度计算错误,导致内存访问越界。
  3. 混淆 bytesused 和缓冲区长度
    MJPEG 格式每帧大小不同,bytesused 是实际数据长度,缓冲区长度是上限。
  4. poll 后忘记检查返回值
    超时或错误会导致无限等待或错误处理。
  5. Streaming 模式下 QBUFDQBUF 不配对
    必须保持 "取出一个,处理完,放回一个" 的循环,否则驱动会耗尽可用缓冲区。

🧭 完整代码清单(v4l2.c 骨架,核心部分已注释)

c

复制代码
#include <config.h>
#include <video_manager.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <sys/mman.h>
#include <poll.h>

static int g_aiSupportedFormats[] = {V4L2_PIX_FMT_YUYV, V4L2_PIX_FMT_MJPEG, V4L2_PIX_FMT_RGB565};

static int isSupportThisFormat(int iPixelFormat) { /* 如上 */ }

static int V4l2InitDevice(char *strDevName, PT_VideoDevice ptVideoDevice) {
    
    // 步骤1: open
    // 打开设备 /dev/video0
iFd = open(strDevName, O_RDWR);
if (iFd < 0)
{
    DBG_PRINTF("can not open %s\n", strDevName);
    return -1;
}
ptVideoDevice->iFd = iFd;  // 把文件描述符存起来


    // 步骤2: VIDIOC_QUERYCAP//查询设备 "身份证 + 能力清单",确认是不是 V4L2 设备、是不是摄像头、支持什么采集模式The Linux Kernel Archives
    memset(&tV4l2Cap, 0, sizeof(struct v4l2_capability));
iError = ioctl(iFd, VIDIOC_QUERYCAP, &tV4l2Cap);
if (iError) {
    DBG_PRINTF("Error opening device %s: unable to query device.\n", strDevName);
    goto err_exit;
}

// 判断是不是摄像头
if (!(tV4l2Cap.capabilities & V4L2_CAP_VIDEO_CAPTURE))
{
    DBG_PRINTF("%s is not a video capture device\n", strDevName);
    goto err_exit;
}

// 判断支不支持高速流(STREAMING)
if (tV4l2Cap.capabilities & V4L2_CAP_STREAMING) {
    DBG_PRINTF("%s supports streaming i/o\n", strDevName);
}

// 判断支不支持低速read(READWRITE)
if (tV4l2Cap.capabilities & V4L2_CAP_READWRITE) {
    DBG_PRINTF("%s supports read i/o\n", strDevName);
}


    // 步骤3: VIDIOC_ENUM_FMT 循环
    memset(&tFmtDesc, 0, sizeof(tFmtDesc));
tFmtDesc.index = 0;                 // 从第0个格式开始问
tFmtDesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

// 循环问:支持格式1?格式2?格式3?
while ((iError = ioctl(iFd, VIDIOC_ENUM_FMT, &tFmtDesc)) == 0)
{
    if (isSupportThisFormat(tFmtDesc.pixelformat))
    {
        ptVideoDevice->iPixelFormat = tFmtDesc.pixelformat;
        break;
    }
    tFmtDesc.index++;
}

if (!ptVideoDevice->iPixelFormat)
{
    DBG_PRINTF("can not support the format of this device\n");
    goto err_exit;
}


    // 步骤4: VIDIOC_S_FMT
    GetDispResolution(&iLcdWidth, &iLcdHeigt, &iLcdBpp);

memset(&tV4l2Fmt, 0, sizeof(struct v4l2_format));
tV4l2Fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tV4l2Fmt.fmt.pix.pixelformat = ptVideoDevice->iPixelFormat;
tV4l2Fmt.fmt.pix.width       = iLcdWidth;
tV4l2Fmt.fmt.pix.height      = iLcdHeigt;
tV4l2Fmt.fmt.pix.field       = V4L2_FIELD_ANY;

iError = ioctl(iFd, VIDIOC_S_FMT, &tV4l2Fmt);
if (iError)
{
    DBG_PRINTF("Unable to set format\n");
    goto err_exit;
}

// 保存最终生效的宽高
ptVideoDevice->iWidth  = tV4l2Fmt.fmt.pix.width;
ptVideoDevice->iHeight = tV4l2Fmt.fmt.pix.height;


    // 步骤5: VIDIOC_REQBUFS + mmap
    memset(&tV4l2ReqBuffs, 0, sizeof(struct v4l2_requestbuffers));
tV4l2ReqBuffs.count = NB_BUFFER;    // 申请4个缓冲区
tV4l2ReqBuffs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
tV4l2ReqBuffs.memory = V4L2_MEMORY_MMAP;

iError = ioctl(iFd, VIDIOC_REQBUFS, &tV4l2ReqBuffs);
if (iError)
{
    DBG_PRINTF("Unable to allocate buffers.\n");
    goto err_exit;
}

// 保存实际得到的缓冲区数量
ptVideoDevice->iVideoBufCnt = tV4l2ReqBuffs.count;


    // 步骤6: (streaming) VIDIOC_QBUF
    for (i = 0; i < ptVideoDevice->iVideoBufCnt; i++)
{
    memset(&tV4l2Buf, 0, sizeof(struct v4l2_buffer));
    tV4l2Buf.index = i;
    tV4l2Buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    tV4l2Buf.memory = V4L2_MEMORY_MMAP;

    // 查询缓冲区信息
    iError = ioctl(iFd, VIDIOC_QUERYBUF, &tV4l2Buf);

    // 映射到用户空间
    ptVideoDevice->pucVideBuf[i] = mmap(..., iFd, tV4l2Buf.m.offset);
}


    ptVideoDevice->ptOPr = &g_tV4l2VideoOpr;
    return 0;
err_exit:
    close(iFd);
    return -1;
}

static int V4l2ExitDevice(PT_VideoDevice ptVideoDevice) {
    for (int i = 0; i < ptVideoDevice->iVideoBufCnt; i++)
        if (ptVideoDevice->pucVideBuf[i])
            munmap(ptVideoDevice->pucVideBuf[i], ptVideoDevice->iVideoBufMaxLen);
    close(ptVideoDevice->iFd);
    return 0;
}

/* GetFrame/PutFrame/Start/Stop 等函数省略,08.4 会详细讲 */

static T_VideoOpr g_tV4l2VideoOpr = {
    .name        = "v4l2",
    .InitDevice  = V4l2InitDevice,
    .ExitDevice  = V4l2ExitDevice,
    .GetFormat   = V4l2GetFormat,
    .GetFrame    = V4l2GetFrameForStreaming,
    .PutFrame    = V4l2PutFrameForStreaming,
    .StartDevice = V4l2StartDevice,
    .StopDevice  = V4l2StopDevice,
};

int V4l2Init(void) {
    return RegisterVideoOpr(&g_tV4l2VideoOpr);
}

✅ 本节自测题(检验是否真的掌握框架)

  1. 请按顺序写出 V4L2 初始化设备的六个主要步骤(用 ioctl 名称或功能描述)。
  2. 为什么需要 VIDIOC_ENUM_FMT 而不是直接设置格式?
  3. VIDIOC_REQBUFSmmap 之间为什么必须调用 VIDIOC_QUERYBUF
  4. 如果摄像头不支持 streaming 模式,代码会怎样处理?
  5. 你如何理解 ptVideoDevice->ptOPr = &g_tV4l2VideoOpr 这行代码的作用?

🖼️ 图解:V4L2 初始化与数据流


💬 总结

摄像头模块(七) 课程的核心就是 V4L2 初始化的六步法

你现在脑中应该有了一张清晰的地图:打开 → 查询能力 → 枚举格式 → 设置格式 → 申请+映射缓冲区 → 入队

至于 GetFrame/PutFrame 等函数,它们是数据流阶段的细节,下一节课(八)会专门讲解初始化函数中每个 ioctl 的参数含义,再下一节(九)才会深入数据传输。

保持这个框架感,后续学习就不会迷路。

🔥 本文档会持续更新,你可以随时回来查阅六步法和常见错误。如果仍有疑问,欢迎在评论区留言。

相关推荐
小李子呢02111 小时前
前端八股网络浏览器---输入 URL 到页面呈现
前端·网络
里晓山2 小时前
SOME/IP协议(上)
网络·网络协议·tcp/ip·车载系统
wangl_922 小时前
Modbus TCP/IP 地址完全解析手册
网络·tcp/ip·php·modbus·kepware·kepserverex
许泽宇的技术分享3 小时前
别再把 AI Agent 当“会聊天的脚本”:Hermes Agent 源码级拆解(架构、框架、实战、趋势,一文吃透)
java·linux·网络
Yupureki3 小时前
《Linux网络编程》9.数据链路层原理
linux·运维·服务器·网络
顶点多余3 小时前
Socket编程实现UDP通信
linux·网络协议·udp
minji...3 小时前
Linux 网络基础(二)HTTP协议,域名,URL,URI,认识HTTP的请求和响应
linux·服务器·网络·网络协议·http·tcp
05候补工程师3 小时前
[408考研笔记] 传输层与网络层核心辨析:从逻辑通信到滑动窗口计算
网络·经验分享·笔记·网络协议·tcp/ip·考研·ip
酿情师3 小时前
网络攻防技术:Windows操作系统的攻防
网络·windows