嵌入式Linux C应用编程——V4L2摄像头应用编程

V4L2 摄像头应用编程

概述

V4L2(Video for Linux Two) 是 Linux 内核中视频类设备的一套驱动框架,为视频类设备驱动开发和应用层提供了一套统一的接口规范。最典型的视频类设备就是视频采集设备(各种摄像头),当然还包括其他类型视频类设备。

使用 V4L2 设备驱动框架注册的设备会在 Linux 系统的 /dev/ 目录下生成对应的设备节点文件,名称通常为 videoX(X 为数字编号,0、1、2、3……),每一个 videoX 设备文件就代表一个视频类设备。应用程序通过对 videoX 设备文件进行 I/O 操作来配置、使用该设备。

开发板配套摄像头:正点原子 ov5640(500W 像素)、ov2640(200W 像素)、ov7725(不带 FIFO、30W 像素),也可以直接使用 USB 摄像头(插入开发板 USB HOST 接口即可)。

摄像头应用编程流程

V4L2 设备驱动框架向应用层提供了一套统一、标准的接口规范,摄像头设备的编程模式如下:

  1. 打开摄像头设备;
  2. 查询设备的属性或功能;
  3. 设置设备的参数(像素格式、帧大小、帧率);
  4. 申请帧缓冲、内存映射;
  5. 帧缓冲入队;
  6. 开启视频采集;
  7. 帧缓冲出队、对采集的数据进行处理;
  8. 处理完后,再次将帧缓冲入队,往复循环;
  9. 结束采集。
flowchart TD A[打开摄像头 open] --> B[查询设备属性 VIDIOC_QUERYCAP] B --> C[设置参数: 格式/分辨率/帧率<br>VIDIOC_S_FMT / VIDIOC_S_PARM] C --> D[申请帧缓冲 VIDIOC_REQBUFS] D --> E[查询帧缓冲+内存映射<br>VIDIOC_QUERYBUF + mmap] E --> F[帧缓冲入队 VIDIOC_QBUF] F --> G[开启视频采集 VIDIOC_STREAMON] G --> H[帧缓冲出队 VIDIOC_DQBUF] H --> I[处理采集到的数据] I --> J[帧缓冲入队 VIDIOC_QBUF] J --> H G -.结束.-> K[关闭视频采集 VIDIOC_STREAMOFF]

img

从流程图中可以看到,几乎对摄像头的所有操作都是通过 ioctl() 完成,搭配不同的 V4L2 指令(request 参数)请求不同的操作。这些指令定义在头文件 linux/videodev2.h 中,应用代码中需要包含该头文件。

videodev2.h 中,定义了很多 ioctl() 指令(以宏定义形式提供,VIDIOC_XXX),这些宏由 _IO/_IOR/_IOW/_IOWR 四个基础宏封装生成:

  • _IO:无数据参数传递;
  • _IOR:从设备读取数据(驱动 → 应用,Read);
  • _IOW:向设备写入数据(应用 → 驱动,Write);
  • _IOWR:既可读又可写(双向,Read + Write)。

每个读写宏后面都携带了一个 struct 数据结构体(如 struct v4l2_capabilitystruct v4l2_fmtdesc),这就是调用 ioctl() 时需要传入的第三个参数的类型。摄像头编程中几个典型的宏定义如下:

/* I O C T L   C O D E S   F O R   V I D E O   D E V I C E S */
#define VIDIOC_QUERYCAP    _IOR('V',  0, struct v4l2_capability)     /* 查询设备能力 */
#define VIDIOC_ENUM_FMT    _IOWR('V',  2, struct v4l2_fmtdesc)      /* 枚举像素格式 */
#define VIDIOC_G_FMT       _IOWR('V',  4, struct v4l2_format)       /* 获取帧格式 */
#define VIDIOC_S_FMT       _IOWR('V',  5, struct v4l2_format)       /* 设置帧格式 */
#define VIDIOC_REQBUFS     _IOWR('V',  8, struct v4l2_requestbuffers) /* 申请帧缓冲 */
#define VIDIOC_QUERYBUF    _IOWR('V',  9, struct v4l2_buffer)       /* 查询帧缓冲 */
#define VIDIOC_QBUF        _IOWR('V', 15, struct v4l2_buffer)       /* 帧缓冲入队 */
#define VIDIOC_DQBUF       _IOWR('V', 17, struct v4l2_buffer)       /* 帧缓冲出队 */
#define VIDIOC_STREAMON    _IOW('V', 18, int)                       /* 开启视频采集 */
#define VIDIOC_STREAMOFF   _IOW('V', 19, int)                       /* 关闭视频采集 */

_IO/_IOR/_IOW/_IOWR 分类有实际作用吗?
有,但作用在"命令编码"层面,应用调用层面看不出差别。ioctl() 的第二个参数本质上只是一个 32 位整数命令号,这几个宏会真正参与计算出这个整数(底层由 _IOC(dir, type, nr, size) 宏合成),其中:

  • 方向位 dir:决定内核如何搬运数据。驱动通过 _IOC_DIR(cmd) 判断——_IOW/_IOWR 会先把参数 copy_from_user 拷进内核;_IOR/_IOWR 在驱动处理完之后再 copy_to_user 把结果拷回用户空间。所以 VIDIOC_S_FMT_IOWR)传入的 &fmt 在调用返回后能读到驱动实际生效的宽高/像素格式(驱动可能改写了结构体再拷回);VIDIOC_QUERYCAP_IOR)驱动填好结构体后由内核拷回 cap
  • size 位:告诉内核参数结构体的大小,用于拷贝字节数及参数校验。
  • type('V')+ nr:保证命令号全局唯一,不同设备、不同指令不会冲突。

所以应用调用方式永远一样(ioctl(fd, cmd, &arg)),这些位是给内核驱动层的 ioctl 分发逻辑用的;用户态看不到变化,恰恰说明它正常生效了。

调用 ioctl() 前,定义一个宏所携带的数据结构体变量,调用时将变量的指针作为第三个参数传入:

struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);

常用 ioctl 指令

V4L2 指令 描述
VIDIOC_QUERYCAP 查询设备的属性/能力/功能
VIDIOC_ENUM_FMT 枚举设备支持的像素格式
VIDIOC_G_FMT 获取设备当前的帧格式信息
VIDIOC_S_FMT 设置帧格式信息
VIDIOC_REQBUFS 申请帧缓冲
VIDIOC_QUERYBUF 查询帧缓冲
VIDIOC_QBUF 帧缓冲入队操作
VIDIOC_DQBUF 帧缓冲出队操作
VIDIOC_STREAMON 开启视频采集
VIDIOC_STREAMOFF 关闭视频采集
VIDIOC_G_PARM 获取设备的一些参数
VIDIOC_S_PARM 设置参数
VIDIOC_TRY_FMT 尝试设置帧格式、用于判断设备是否支持该格式
VIDIOC_ENUM_FRAMESIZES 枚举设备支持的视频采集分辨率
VIDIOC_ENUM_FRAMEINTERVALS 枚举设备支持的视频采集帧率

1. 打开摄像头

视频类设备对应的设备节点为 /dev/videoX,X 为数字编号,通常从 0 开始。摄像头应用编程的第一步便是打开设备,调用 open() 得到文件描述符 fd,打开时需要使用 O_RDWR 指定读写权限:

int fd = -1;

/* 打开摄像头 */
fd = open("/dev/video0", O_RDWR);
if (0 > fd) {
    fprintf(stderr, "open error: %s: %s\n", "/dev/video0", strerror(errno));
    return -1;
}

ioctl() 对于设备文件来说是一个非常重要的系统调用,凡是涉及配置设备、获取设备配置等操作都会使用 ioctl 完成;但对于普通文件来说,ioctl() 几乎没什么用。

2. 查询设备的属性/能力/功能

查询设备属性使用 VIDIOC_QUERYCAP 指令,将获取到一个 struct v4l2_capability 类型数据,该结构体描述了设备的一些属性:

struct v4l2_capability {
    __u8    driver [16];     /* 驱动的名字 */
    __u8    card [32];       /* 设备的名字 */
    __u8    bus_info [32];   /* 总线的名字 */
    __u32   version;         /* 版本信息 */
    __u32   capabilities;    /* 设备拥有的能力 */
    __u32   device_caps;
    __u32   reserved [3];    /* 保留字段 */
};

重点关注 capabilities 字段,它描述了设备拥有的能力,取值如下(可以是以下任意一个值或多个值的位或关系):

#define V4L2_CAP_VIDEO_CAPTURE        0x00000001  /* 视频采集设备 */
#define V4L2_CAP_VIDEO_OUTPUT         0x00000002  /* 视频输出设备 */
#define V4L2_CAP_VIDEO_OVERLAY        0x00000004  /* 视频叠加 */
#define V4L2_CAP_VBI_CAPTURE          0x00000010  /* 原始 VBI 采集设备 */
#define V4L2_CAP_VBI_OUTPUT           0x00000020  /* 原始 VBI 输出设备 */
#define V4L2_CAP_SLICED_VBI_CAPTURE   0x00000040
#define V4L2_CAP_SLICED_VBI_OUTPUT    0x00000080
#define V4L2_CAP_VIDEO_CAPTURE_MPLANE 0x00001000  /* 支持 multiplanar 的视频采集 */
#define V4L2_CAP_VIDEO_OUTPUT_MPLANE  0x00002000
#define V4L2_CAP_VIDEO_M2M_MPLANE     0x00004000
#define V4L2_CAP_VIDEO_M2M            0x00008000  /* mem-to-mem 设备 */
#define V4L2_CAP_TUNER                0x00010000  /* 有 tuner */
#define V4L2_CAP_AUDIO                0x00020000  /* 有音频支持 */
#define V4L2_CAP_RADIO                0x00040000  /* 收音机设备 */
#define V4L2_CAP_MODULATOR            0x00080000
#define V4L2_CAP_READWRITE            0x01000000  /* 支持 read/write 系统调用 */
#define V4L2_CAP_ASYNCIO              0x02000000  /* 支持异步 I/O */
#define V4L2_CAP_STREAMING            0x04000000  /* 支持 streaming I/O ioctls */

对于摄像头设备来说,其 capabilities 字段必须包含 V4L2_CAP_VIDEO_CAPTURE,表示它支持视频采集功能。所以可以通过判断该字段是否包含 V4L2_CAP_VIDEO_CAPTURE 来确定它是否是一个摄像头设备:

/* 查询设备功能 */
ioctl(fd, VIDIOC_QUERYCAP, &vcap);

/* 判断是否是视频采集设备 */
if (!(V4L2_CAP_VIDEO_CAPTURE & vcap.capabilities)) {
    fprintf(stderr, "Error: No capture video device!\n");
    return -1;
}

3. 设置帧格式、帧率

一个摄像头通常会支持多种不同的像素格式(如 RGB、YUYV、压缩格式 MJPEG 等),支持多种不同的采集分辨率(如 640*480、320*240、1280*720 等),同一分辨率还可能支持多种采集帧率(15fps、30fps)。所以进行视频采集之前,通常需要在应用程序中设置这些参数。

a) 枚举摄像头支持的所有像素格式:VIDIOC_ENUM_FMT

使用 VIDIOC_ENUM_FMT 可以枚举出设备支持的所有像素格式,传入一个 struct v4l2_fmtdesc * 指针,ioctl() 会将获取到的数据写入其中:

struct v4l2_fmtdesc {
    __u32   index;          /* 格式编号 */
    __u32   type;           /* enum v4l2_buf_type */
    __u32   flags;
    __u8    description [32]; /* 描述字符串 */
    __u32   pixelformat;    /* 格式 fourcc */
    __u32   reserved [4];
};
  • index:编号。枚举前需将其设置为 0,每次 ioctl() 调用之后加 1。一次 ioctl() 调用只能得到一种像素格式的信息,若设备支持多种格式,需循环调用多次,直到 ioctl() 调用失败(表示所有格式都已枚举出来)。
  • description:简单的描述性字符串。
  • pixelformat:像素格式编号,无符号 32 位数据,每一种像素格式用一个 u32 数据表示(通过 v4l2_fourcc 宏合成)。

常见像素格式宏(定义在 videodev2.h):

/* RGB formats */
#define V4L2_PIX_FMT_RGB332   v4l2_fourcc('R', 'G', 'B', '1')  /* 8   RGB-3-3-2  */
#define V4L2_PIX_FMT_RGB565   v4l2_fourcc('R', 'G', 'B', 'P')  /* 16  RGB-5-6-5  */
#define V4L2_PIX_FMT_RGB444   v4l2_fourcc('R', '4', '4', '4')
#define V4L2_PIX_FMT_RGB555   v4l2_fourcc('R', 'G', 'B', 'O')  /* 16  RGB-5-5-5  */

/* Grey formats */
#define V4L2_PIX_FMT_GREY     v4l2_fourcc('G', 'R', 'E', 'Y')  /* 8   灰度 */

/* Luminance+Chrominance formats */
#define V4L2_PIX_FMT_YUYV     v4l2_fourcc('Y', 'U', 'Y', 'V')  /* 16  YUV 4:2:2 */
#define V4L2_PIX_FMT_UYVY     v4l2_fourcc('U', 'Y', 'V', 'Y')  /* 16  YUV 4:2:2 */
#define V4L2_PIX_FMT_YVYU     v4l2_fourcc('Y', 'V', 'Y', 'U')
#define V4L2_PIX_FMT_YYUV     v4l2_fourcc('Y', 'Y', 'U', 'V')

/* compressed formats */
#define V4L2_PIX_FMT_MJPEG    v4l2_fourcc('M', 'J', 'P', 'G')  /* Motion-JPEG */
#define V4L2_PIX_FMT_JPEG     v4l2_fourcc('J', 'P', 'E', 'G')  /* JFIF JPEG */
#define V4L2_PIX_FMT_MPEG     v4l2_fourcc('M', 'P', 'E', 'G')  /* MPEG-1/2/4 */
  • type:指定要获取设备的哪种功能对应的像素格式(因为有些设备既支持视频采集、又支持视频输出等功能),取值如下。对于摄像头,需将 type 字段设置为 V4L2_BUF_TYPE_VIDEO_CAPTURE
enum v4l2_buf_type {
    V4L2_BUF_TYPE_VIDEO_CAPTURE          = 1,  /* 视频采集 */
    V4L2_BUF_TYPE_VIDEO_OUTPUT           = 2,  /* 视频输出 */
    V4L2_BUF_TYPE_VIDEO_OVERLAY          = 3,
    V4L2_BUF_TYPE_VBI_CAPTURE            = 4,
    V4L2_BUF_TYPE_VBI_OUTPUT             = 5,
    V4L2_BUF_TYPE_SLICED_VBI_CAPTURE     = 6,
    V4L2_BUF_TYPE_SLICED_VBI_OUTPUT      = 7,
    V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY   = 8,
    V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE   = 9,
    V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE    = 10,
    V4L2_BUF_TYPE_SDR_CAPTURE            = 11,
    V4L2_BUF_TYPE_SDR_OUTPUT             = 12,
    V4L2_BUF_TYPE_META_CAPTURE           = 13,
    V4L2_BUF_TYPE_PRIVATE                = 0x80, /* 废弃,勿用 */
};

使用示例:

struct v4l2_fmtdesc fmtdesc;

/* 枚举出摄像头所支持的所有像素格式以及描述信息 */
fmtdesc.index = 0;
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
while (0 == ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc)) {
    printf("fmt: %s <0x%x>\n", fmtdesc.description, fmtdesc.pixelformat);
    fmtdesc.index++;
}

b) 枚举摄像头支持的所有视频采集分辨率:VIDIOC_ENUM_FRAMESIZES

使用 VIDIOC_ENUM_FRAMESIZES 指令枚举设备支持的所有采集分辨率,传入 struct v4l2_frmsizeenum * 指针:

struct v4l2_frmsizeenum {
    __u32   index;          /* 帧大小编号 */
    __u32   pixel_format;   /* 像素格式 */
    __u32   type;           /* type */

    union {                 /* Frame size */
        struct v4l2_frmsize_discrete    discrete;
        struct v4l2_frmsize_stepwise    stepwise;
    };

    __u32   reserved [2];
};

struct v4l2_frmsize_discrete {
    __u32   width;          /* 帧宽度 [像素] */
    __u32   height;         /* 帧高度 [像素] */
};
  • indexv4l2_fmtdesc 的 index 意义相同(循环递增直到 ioctl 失败)。
  • 调用 ioctl() 之前,需要先设置 type 字段与 pixel_format 字段,确定要枚举的是:设备的哪种功能、哪种像素格式支持的视频帧大小。
  • union 共用体中,type = V4L2_BUF_TYPE_VIDEO_CAPTUREdiscrete 生效,描述了视频帧的宽度和高度(即采集分辨率)。

示例:枚举 RGB565 像素格式支持的所有视频帧大小:

struct v4l2_frmsizeenum frmsize;

frmsize.index = 0;
frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmsize.pixel_format = V4L2_PIX_FMT_RGB565;
while (0 == ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {
    printf("frame_size<%d*%d>\n", frmsize.discrete.width, frmsize.discrete.height);
    frmsize.index++;
}

c) 枚举摄像头支持的所有视频采集帧率:VIDIOC_ENUM_FRAMEINTERVALS

同一种视频帧大小,摄像头可能支持多种帧率(15fps、30fps、45fps、60fps 等)。使用 VIDIOC_ENUM_FRAMEINTERVALS 指令枚举,传入 struct v4l2_frmivalenum * 指针:

struct v4l2_frmivalenum {
    __u32   index;          /* 帧率编号 */
    __u32   pixel_format;   /* 像素格式 */
    __u32   width;          /* 帧宽度 */
    __u32   height;         /* 帧高度 */
    __u32   type;           /* type */

    union {                 /* Frame interval */
        struct v4l2_fract          discrete;
        struct v4l2_frmival_stepwise stepwise;
    };

    __u32   reserved [2];
};

struct v4l2_fract {
    __u32   numerator;      /* 分子 */
    __u32   denominator;    /* 分母 */
};
  • indextype 意义与前面相同;widthheight 指定帧大小,pixel_format 指定像素格式,这些字段都需要在调用 ioctl() 之前设置
  • union 共用体中,type = V4L2_BUF_TYPE_VIDEO_CAPTUREdiscrete 生效,是 struct v4l2_fract 类型变量,描述视频帧率信息:numerator / denominator 表示图像采集的周期(采集一幅图像需要多少秒),所以视频帧率 = denominator / numerator

示例:枚举 RGB565、640*480 帧大小支持的所有帧率:

struct v4l2_frmivalenum frmival;

frmival.index = 0;
frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
frmival.pixel_format = V4L2_PIX_FMT_RGB565;
frmival.width = 640;
frmival.height = 480;

while (0 == ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {
    printf("Frame interval<%ffps> ", frmival.discrete.denominator / frmival.discrete.numerator);
    frmival.index++;
}

d) 查看或设置当前的格式:VIDIOC_G_FMTVIDIOC_S_FMT

  • VIDIOC_G_FMT:查看设备当前的格式,ioctl() 将获取到的数据写入 fmt 指针所指对象;
  • VIDIOC_S_FMT:设置设备格式,ioctl() 使用 fmt 所指对象的数据去设置设备的格式。

struct v4l2_format 结构体描述格式相关信息:

struct v4l2_format {
    __u32   type;
    union {
        struct v4l2_pix_format       pix;     /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
        struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
        struct v4l2_window           win;     /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
        struct v4l2_vbi_format       vbi;     /* V4L2_BUF_TYPE_VBI_CAPTURE */
        struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
        struct v4l2_sdr_format       sdr;     /* V4L2_BUF_TYPE_SDR_CAPTURE */
        struct v4l2_meta_format      meta;    /* V4L2_BUF_TYPE_META_CAPTURE */
        __u8   raw_data [200];                /* user-defined */
    } fmt;
};

type 字段需要在调用 ioctl() 之前设置。当 type = V4L2_BUF_TYPE_VIDEO_CAPTURE 时,union 共用体中 pix 变量生效,它是 struct v4l2_pix_format 类型变量,记录视频帧格式相关信息:

struct v4l2_pix_format {
    __u32   width;          /* 视频帧宽度(像素) */
    __u32   height;         /* 视频帧高度(像素) */
    __u32   pixelformat;    /* 像素格式 */
    __u32   field;          /* enum v4l2_field */
    __u32   bytesperline;   /* for padding, zero if unused */
    __u32   sizeimage;
    __u32   colorspace;     /* enum v4l2_colorspace */
    __u32   priv;
    __u32   flags;
    union {
        __u32   ycbcr_enc;  /* enum v4l2_ycbcr_encoding */
        __u32   hsv_enc;    /* enum v4l2_hsv_encoding */
    };
    __u32   quantization;   /* enum v4l2_quantization */
    __u32   xfer_func;      /* enum v4l2_xfer_func */
};

colorspace 字段描述颜色空间,常用的取值:

enum v4l2_colorspace {
    V4L2_COLORSPACE_DEFAULT    = 0,  /* 让驱动自己决定 */
    V4L2_COLORSPACE_SMPTE170M  = 1,  /* 广播 NTSC/PAL SDTV */
    V4L2_COLORSPACE_SMPTE240M  = 2,
    V4L2_COLORSPACE_REC709     = 3,  /* HDTV */
    V4L2_COLORSPACE_BT878      = 4,  /* 废弃 */
    V4L2_COLORSPACE_470_SYSTEM_M  = 5,
    V4L2_COLORSPACE_470_SYSTEM_BG = 6,
    V4L2_COLORSPACE_JPEG       = 7,  /* (Motion-)JPEG */
    V4L2_COLORSPACE_SRGB       = 8,  /* 大多数 webcam 产生的 RGB */
    V4L2_COLORSPACE_ADOBERGB   = 9,
    V4L2_COLORSPACE_BT2020     = 10, /* UHDTV */
    V4L2_COLORSPACE_RAW        = 11, /* 原始未处理图像 */
    V4L2_COLORSPACE_DCI_P3     = 12,
};

使用 VIDIOC_S_FMT 设置格式时,通常不需要用户指定 colorspace,底层驱动会根据像素格式 pixelformat 来确定对应的 colorspace。

获取格式 + 设置格式示例:

struct v4l2_format fmt;

fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(fd, VIDIOC_G_FMT, &fmt)) {  // 获取格式信息
    perror("ioctl error");
    return -1;
}

printf("width:%d, height:%d format:%d\n",
       fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat);

fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) {  // 设置格式
    perror("ioctl error");
    return -1;
}

重要:使用 VIDIOC_S_FMT 设置格式时,实际设置的参数并不一定等于我们指定的参数(如摄像头不支持 800*480,驱动可能会将其修改为 640*480)。所以当 ioctl() 调用返回后,还需要检查返回的 struct v4l2_format 变量,以确定指定的参数是否已经生效

struct v4l2_format fmt;

fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 800;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(fd, VIDIOC_S_FMT, &fmt)) {  // 设置格式
    perror("ioctl error");
    return -1;
}

if (800 != fmt.fmt.pix.width || 480 != fmt.fmt.pix.height) {
    do_something();
}
if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
    do_something();
}

e) 设置或获取流类型相关参数:VIDIOC_G_PARMVIDIOC_S_PARM

  • VIDIOC_G_PARM:获取设备的流类型相关参数(Stream type-dependent parameters);
  • VIDIOC_S_PARM:设置设备的流类型相关参数。

什么是"流类型"? "流"指数据流(data stream)——设备上流动的一路视频数据。一个 V4L2 设备可能同时支持多条数据流:视频采集流(V4L2_BUF_TYPE_VIDEO_CAPTURE)、视频输出流(V4L2_BUF_TYPE_VIDEO_OUTPUT)、VBI 采集/输出流、overlay 流、SDR 流等。所以"流类型"= enum v4l2_buf_type 的取值,它标识这次 ioctl 操作针对的是哪一条数据流。这就是为什么几乎每个 V4L2 结构体(v4l2_fmtdescv4l2_formatv4l2_streamparmv4l2_requestbuffersv4l2_buffer 等)都带一个 type 字段,且都需在调用 ioctl 前先设置——不告诉内核操作哪条流,驱动就无法确定参数含义。而"流类型相关参数"就是指针对某条数据流的参数(如采集流的帧率),struct v4l2_streamparm 里的 union 正是根据 type 选对应参数结构体:采集用 capture,视频输出用 output。对摄像头采集编程,type 一律设为 V4L2_BUF_TYPE_VIDEO_CAPTURE

struct v4l2_streamparm 结构体描述流类型相关信息:

struct v4l2_streamparm {
    __u32   type;           /* enum v4l2_buf_type */
    union {
        struct v4l2_captureparm  capture;
        struct v4l2_outputparm   output;
        __u8   raw_data [200];   /* user-defined */
    } parm;
};

struct v4l2_captureparm {
    __u32           capability;     /* 支持的模式 */
    __u32           capturemode;    /* 当前模式 */
    struct v4l2_fract timeperframe; /* 每帧时间(秒) */
    __u32           extendedmode;   /* 驱动特定扩展 */
    __u32           readbuffers;    /* 用于 read 的缓冲区数量 */
    __u32           reserved[4];
};

当 type = V4L2_BUF_TYPE_VIDEO_CAPTURE 时,union 中 capture 变量生效,它描述了摄像头采集相关参数(如采集帧率)。

  • capability:设备支持的模式,取值(位或关系):
    • V4L2_MODE_HIGHQUALITY (0x0001):高品质成像模式
    • V4L2_CAP_TIMEPERFRAME (0x1000):支持设置 timeperframe 字段
  • capturemode:当前模式,与 capability 取值相同。
  • timeperframestruct v4l2_fract 类型,描述视频采集周期。使用 VIDIOC_S_PARM 可以设置采集周期(即采集帧率),但很多设备并不支持应用层设置 timeperframe 字段,只有当 capability 字段包含 V4L2_CAP_TIMEPERFRAME 时才表示设备支持,应用层才可以设置采集帧率。

所以在设置之前,先通过 VIDIOC_G_PARM 获取参数、判断是否支持帧率设置:

struct v4l2_streamparm streamparm;

streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

/** 判断是否支持帧率设置 **/
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
    streamparm.parm.capture.timeperframe.numerator = 1;
    streamparm.parm.capture.timeperframe.denominator = 30;  // 30fps

    if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {  // 设置参数
        fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
        return -1;
    }
}
else
    fprintf(stderr, "不支持帧率设置");

4. 申请帧缓冲、内存映射

读取摄像头数据有两种方式:

  1. read 方式:直接通过 read() 系统调用读取摄像头采集到的数据(需 capabilities 包含 V4L2_CAP_READWRITE);
  2. streaming 方式:需向设备申请帧缓冲、并将帧缓冲映射到应用程序进程地址空间(需 capabilities 包含 V4L2_CAP_STREAMING)。绝大部分设备都支持 streaming I/O 方式

申请帧缓冲:VIDIOC_REQBUFS

帧缓冲就是用于存储一帧图像数据的缓冲区。使用 VIDIOC_REQBUFS 指令申请帧缓冲,传入 struct v4l2_requestbuffers * 指针:

struct v4l2_requestbuffers {
    __u32   count;          /* 申请帧缓冲的数量 */
    __u32   type;           /* enum v4l2_buf_type */
    __u32   memory;         /* enum v4l2_memory */
    __u32   reserved [2];
};
  • type:同前,调用前需先设置;
  • count:申请帧缓冲的数量;
  • memory:取值如下,通常设置为 V4L2_MEMORY_MMAP
enum v4l2_memory {
    V4L2_MEMORY_MMAP    = 1,  /* 内存映射方式 */
    V4L2_MEMORY_USERPTR = 2,  /* 用户指针方式 */
    V4L2_MEMORY_OVERLAY = 3,
    V4L2_MEMORY_DMABUF  = 4,
};

使用示例:

struct v4l2_requestbuffers reqbuf;

reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3;                    // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;

if (0 > ioctl(fd, VIDIOC_REQBUFS, &reqbuf)) {
    fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
    return -1;
}

队列原理(入队 / 出队)

streaming I/O 方式会在内核空间中维护一个帧缓冲队列

  • 驱动程序将从摄像头读取的一帧数据写入队列中的一个帧缓冲,接着将下一帧数据写入下一个帧缓冲;
  • 应用程序需要读取一帧数据时,从队列中取出一个装满一帧数据的帧缓冲,这个取出过程叫出队(Dequeue)
  • 应用程序处理完这一帧数据后,再把该帧缓冲加入内核的帧缓冲队列,这个过程叫入队(Queue)。注意:这里"加回去"的是缓冲(容器)本身,而不是帧数据——处理完的旧数据已无价值、直接被丢弃,驱动下一次往这块缓冲里写的是新的一帧,旧数据会被覆盖。

所以读取图像数据的过程就是一个不断出队和入队的过程。

为什么要"入队"(处理完后再还回去)?
因为帧缓冲队列是一个数量固定的循环复用池(申请几个就几个,如 3 个),并不是无限供给的:

  • 摄像头硬件是持续不断地产生帧数据的(按帧率),驱动每采到一帧,就需要一个"空的、已入队"的帧缓冲来存放它;
  • 缓冲被 DQBUF 出队后,就归应用程序所有("满"状态),驱动不会再往里面写(否则会破坏应用正在读的数据);
  • 如果应用处理完不把它 QBUF 还回去,内核里"空的可用缓冲"就会越来越少;一旦申请的缓冲全被应用占用,驱动就没有缓冲可写,新帧只能被丢弃,整个采集流程就"卡住"了。

所以入队 = 把用过的缓冲归还到循环队列,让驱动能再次写入下一帧,形成一个环形流水线:3 个缓冲让驱动在应用处理第 1 帧的同时,还能继续往第 2、3 帧写;但前提是应用必须及时归还。

队列不是还存在吗?直接丢弃数据不行吗?
不行,因为队列本身并不存放数据,它只是"哪些缓冲当前可用"的引用列表。真正存放帧数据的是你申请并 mmap 的那 N 个缓冲,队列里并没有数据的副本:

  • DQBUF 的作用是把某个缓冲从"可用列表"中移除、交给应用——数据仍然留在那块缓冲内存里,队列并不会因此多出一份拷贝;
  • "丢弃数据"只是应用自己忽略读到的内容,但缓冲这块内存依然存在,且此时它处于"归应用所有"的状态——驱动不知道你什么时候读完,所以不敢再往里写(写了会破坏你正在读的数据);
  • QBUF 就是所有权交接:应用主动告诉驱动"这块缓冲我读完了,你可以再往里面写了",并把它加回可用列表。没有这个握手信号,驱动无法判断缓冲能否复用。

所以"丢弃数据"和"归还缓冲"是两件事:内容可以扔掉,但缓冲容器必须还回去(否则 3 个缓冲很快全被应用占住,驱动没有可用缓冲,只能丢帧)。

将帧缓冲映射到进程地址空间:VIDIOC_QUERYBUF + mmap()

申请到的帧缓冲实质上由内核维护,应用程序不能直接读取该缓冲区的数据,需要将其映射到用户空间。映射前需要查询帧缓冲的信息(长度、偏移量等),使用 VIDIOC_QUERYBUF 指令,传入 struct v4l2_buffer * 指针:

struct v4l2_buffer {
    __u32           index;      /* buffer 的编号 */
    __u32           type;       /* type */
    __u32           bytesused;
    __u32           flags;
    __u32           field;
    struct timeval       timestamp;
    struct v4l2_timecode timecode;
    __u32           sequence;

    __u32           memory;     /* memory location */
    union {
        __u32         offset;   /* 偏移量 */
        unsigned long userptr;
        struct v4l2_plane  *planes;
        __s32         fd;
    } m;
    __u32           length;     /* buffer 的长度 */
    __u32           reserved2;
    __u32           reserved;
};
  • index:编号,从 0 开始。一次 ioctl() 调用只能获取指定编号对应帧缓冲的信息,要获取多个需重复调用,每调用一次 index 加 1。
  • typememory:与前面相同,调用前需设置。
  • length:帧缓冲的长度;m.offset:帧缓冲的偏移量。因为申请帧缓冲时,内核会向操作系统申请一块内存空间作为帧缓冲区(大小 = 帧缓冲数量 × 单个帧缓冲大小),每个帧缓冲对应这块内存的某一段,所以它们都有一个地址偏移量。

帧缓冲的数量不要太多,尤其在一些内存比较吃紧的嵌入式系统中,帧缓冲太多会占用太多系统内存。

申请帧缓冲后、调用 mmap() 将帧缓冲映射到用户地址空间:

struct v4l2_requestbuffers reqbuf;
struct v4l2_buffer buf;
void *frm_base[3];

reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.count = 3;                     // 申请 3 个帧缓冲
reqbuf.memory = V4L2_MEMORY_MMAP;

/* 申请 3 个帧缓冲 */
if (0 > ioctl(fd, VIDIOC_REQBUFS, &reqbuf)) {
    fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
    return -1;
}

/* 建立内存映射 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < 3; buf.index++) {

    ioctl(fd, VIDIOC_QUERYBUF, &buf);
    frm_base[buf.index] = mmap(NULL, buf.length,
                               PROT_READ | PROT_WRITE, MAP_SHARED,
                               fd, buf.m.offset);
    if (MAP_FAILED == frm_base[buf.index]) {
        perror("mmap error");
        return -1;
    }
}

将三个帧缓冲映射到用户空间,并把每个帧缓冲对应映射区的起始地址保存在 frm_base 数组中,后面读取摄像头采集的数据时,直接读取映射区即可。

5. 帧缓冲入队:VIDIOC_QBUF

使用 VIDIOC_QBUF 指令将帧缓冲放入内核的帧缓冲队列(入队操作)。调用前需设置 struct v4l2_buffermemorytype 字段:

struct v4l2_buffer buf;

/* 入队操作 */
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < 3; buf.index++) {

    if (0 > ioctl(fd, VIDIOC_QBUF, &buf)) {
        perror("ioctl error");
        return -1;
    }
}

6. 开启视频采集:VIDIOC_STREAMON

将三个帧缓冲放入队列后,便可以打开摄像头、开启图像采集。type 实际是一个 enum v4l2_buf_type * 指针:

ioctl(int fd, VIDIOC_STREAMON, int *type);   // 开启视频采集
ioctl(int fd, VIDIOC_STREAMOFF, int *type);  // 停止视频采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

if (0 > ioctl(fd, VIDIOC_STREAMON, &type)) {
    perror("ioctl error");
    return -1;
}

7. 读取数据、对数据进行处理:VIDIOC_DQBUF

开启视频采集后,直接读取每个帧缓冲在用户空间的映射区即可读取到每一帧图像数据。读取数据之前,需要将帧缓冲从内核的帧缓冲队列中取出(出队),使用 VIDIOC_DQBUF 指令:

ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *buf);

帧缓冲出队之后读取数据、进行处理(如将图像显示到 LCD 屏);数据处理完成后再将帧缓冲入队,然后将队列中的下一个帧缓冲出队,如此往复操作:

struct v4l2_buffer buf;

buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for ( ; ; ) {

    for (buf.index = 0; buf.index < 3; buf.index++) {

        ioctl(fd, VIDIOC_DQBUF, &buf);   // 出队

        // 读取帧缓冲的映射区、获取一帧数据
        // 处理这一帧数据
        do_something();

        // 数据处理完之后、将当前帧缓冲入队、接着读取下一帧数据
        ioctl(fd, VIDIOC_QBUF, &buf);
    }
}

8. 结束视频采集:VIDIOC_STREAMOFF

enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

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

9. V4L2 摄像头应用编程实战

摄像头应用编程并不难,基本按照:打开设备 → 查询设备 → 设置格式 → 申请帧缓冲 → 内存映射 → 入队 → 开启视频采集 → 出队 → 处理数据,每一步基本都是通过 ioctl() 搭配不同请求指令实现。

本实验使用通用 USB 摄像头(UVC 协议),将摄像头采集的图像数据显示到开发板 LCD 屏上。与正点原子配套摄像头(ov5640/ov2640/ov7725)相比,有一个关键差异:

USB 摄像头不支持 RGB565 输出。正点原子摄像头支持 RGB565,采集的数据可以直接拷贝到 LCD 显存;而通用 USB 摄像头(UVC)通常只支持 YUYV(未压缩 YUV 4:2:2)和 MJPEG(压缩格式)。因此示例程序做了三处适配:

  1. 自动选择 YUYV(备选 UYVY)像素格式,软件转换为 RGB565 后再上屏;
  2. LCD 位深自适应:通过 bits_per_pixel 判断,支持 16bpp(RGB565) 直拷与 32bpp(ARGB8888) 扩展两种显存格式;
  3. 按 Ctrl+C 干净退出(STREAMOFF + 关闭设备、解除映射),避免摄像头被异常占用。

程序启动时会枚举并打印摄像头支持的所有像素格式、分辨率、帧率(可借此确认摄像头是否支持 YUYV),然后自动设置 640×480、YUYV 格式开始采集。

/***************************************************************
 * 文件名 : v4l2_camera.c
 * 描述   : V4L2 摄像头应用编程实战(USB 摄像头版)
 *          参考:正点原子 I.MX6U 嵌入式 Linux C 应用编程指南 V1.6
 *                第 25 章 V4L2 摄像头应用编程
 *
 * 与课程示例(ov5640/ov2640/ov7725)的差异:
 *   1. 正点原子摄像头支持 RGB565 输出,采集到的数据可直接拷贝到 LCD;
 *      通用 USB 摄像头(UVC)通常只支持 YUYV / MJPEG,不支持 RGB565,
 *      因此本程序自动选择 YUYV(其次 UYVY),软件转换为 RGB565 后再上屏。
 *   2. LCD 位深自适应:支持 16bpp(RGB565) 与 32bpp(ARGB8888) 帧缓冲。
 *   3. 按 Ctrl+C 干净退出(STREAMOFF + 关闭设备、解除映射)。
 ***************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>

#define FB_DEV              "/dev/fb0"      //LCD 设备节点
#define FRAMEBUFFER_COUNT   3               //帧缓冲数量

#define DEFAULT_WIDTH       640             //默认请求的视频帧宽度(USB 摄像头通用分辨率)
#define DEFAULT_HEIGHT      480             //默认请求的视频帧高度

/*** 摄像头像素格式及其描述信息 ***/
typedef struct camera_format {
    unsigned char description[32];  //字符串描述信息
    unsigned int pixelformat;       //像素格式
} cam_fmt;

/*** 描述一个帧缓冲的信息 ***/
typedef struct cam_buf_info {
    unsigned char *start;       //帧缓冲起始地址
    unsigned long length;       //帧缓冲长度
} cam_buf_info;

static int width;                       //LCD 宽度
static int height;                      //LCD 高度
static int fb_bpp;                      //LCD 位深(16 或 32)
static unsigned long fb_line_length;    //LCD 一行显存的字节数
static unsigned long fb_screen_size;    //LCD 显存总大小
static unsigned short *screen_base = NULL;//LCD 显存基地址
static int fb_fd = -1;                  //LCD 设备文件描述符
static int v4l2_fd = -1;                //摄像头设备文件描述符
static cam_buf_info buf_infos[FRAMEBUFFER_COUNT];
static cam_fmt cam_fmts[10];            //摄像头支持的像素格式数组
static int frm_width, frm_height;       //视频帧宽度和高度
static int frm_bytesperline;            //视频帧一行数据的字节数
static unsigned int pixel_format;       //实际选用的像素格式(YUYV / UYVY)
static volatile sig_atomic_t stop_flag = 0; //Ctrl+C 退出标志

static int fb_dev_init(void)
{
    struct fb_var_screeninfo fb_var = {0};
    struct fb_fix_screeninfo fb_fix = {0};

    /* 打开 framebuffer 设备 */
    fb_fd = open(FB_DEV, O_RDWR);
    if (0 > fb_fd) {
        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
        return -1;
    }

    /* 获取 framebuffer 设备信息 */
    ioctl(fb_fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &fb_fix);

    fb_screen_size = fb_fix.line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;
    fb_bpp = fb_var.bits_per_pixel;
    fb_line_length = fb_fix.line_length;

    /* 内存映射 */
    screen_base = mmap(NULL, fb_screen_size, PROT_READ | PROT_WRITE,
                       MAP_SHARED, fb_fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fb_fd);
        return -1;
    }

    /* LCD 背景刷白(16bpp/32bpp 下白色的字节均为 0xFF) */
    memset(screen_base, 0xFF, fb_screen_size);
    return 0;
}

static int v4l2_dev_init(const char *device)
{
    struct v4l2_capability cap = {0};

    /* 打开摄像头 */
    v4l2_fd = open(device, O_RDWR);
    if (0 > v4l2_fd) {
        fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
        return -1;
    }

    /* 查询设备功能 */
    ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap);

    /* 判断是否是视频采集设备 */
    if (!(V4L2_CAP_VIDEO_CAPTURE & cap.capabilities)) {
        fprintf(stderr, "Error: %s: No capture video device!\n", device);
        close(v4l2_fd);
        return -1;
    }

    return 0;
}

static void v4l2_enum_formats(void)
{
    struct v4l2_fmtdesc fmtdesc = {0};

    /* 枚举摄像头所支持的所有像素格式以及描述信息 */
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    while (fmtdesc.index < 10 && 0 == ioctl(v4l2_fd, VIDIOC_ENUM_FMT, &fmtdesc)) {

        // 将枚举出来的格式以及描述信息存放在数组中
        cam_fmts[fmtdesc.index].pixelformat = fmtdesc.pixelformat;
        strcpy((char *)cam_fmts[fmtdesc.index].description,
               (const char *)fmtdesc.description);
        fmtdesc.index++;
    }
}

static void v4l2_print_formats(void)
{
    struct v4l2_frmsizeenum frmsize = {0};
    struct v4l2_frmivalenum frmival = {0};
    int i;

    frmsize.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    frmival.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    for (i = 0; cam_fmts[i].pixelformat; i++) {

        printf("format<0x%x>, description<%s>\n", cam_fmts[i].pixelformat,
               cam_fmts[i].description);

        /* 枚举出摄像头所支持的所有视频采集分辨率 */
        frmsize.index = 0;
        frmsize.pixel_format = cam_fmts[i].pixelformat;
        /* 限制枚举次数:USB 摄像头驱动可能返回 stepwise/continuous 类型,
           始终返回成功,防止死循环 */
        while (frmsize.index < 16 && 0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMESIZES, &frmsize)) {

            if (V4L2_FRMSIZE_TYPE_DISCRETE == frmsize.type) {
                printf("size<%d*%d> ", frmsize.discrete.width, frmsize.discrete.height);
            } else if (V4L2_FRMSIZE_TYPE_STEPWISE == frmsize.type) {
                printf("size<%d*%d~%d*%d> ",
                       frmsize.stepwise.min_width, frmsize.stepwise.min_height,
                       frmsize.stepwise.max_width, frmsize.stepwise.max_height);
            }
            frmsize.index++;

            /* 获取摄像头视频采集帧率 */
            frmival.index = 0;
            frmival.width = frmsize.discrete.width;
            frmival.height = frmsize.discrete.height;
            while (frmival.index < 8 && 0 == ioctl(v4l2_fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmival)) {

                if (V4L2_FRMIVAL_TYPE_DISCRETE == frmival.type) {
                    printf("<%dfps>", frmival.discrete.denominator / frmival.discrete.numerator);
                } else if (V4L2_FRMIVAL_TYPE_CONTINUOUS == frmival.type) {
                    printf("<continuous>");
                } else if (V4L2_FRMIVAL_TYPE_STEPWISE == frmival.type) {
                    printf("<stepwise>");
                }
                frmival.index++;
            }
            printf("\n");
        }
        printf("\n");
    }
}

static int v4l2_set_format(void)
{
    struct v4l2_format fmt = {0};
    struct v4l2_streamparm streamparm = {0};
    int i;

    /* USB 摄像头通常不支持 RGB565,优先选择 YUYV,其次 UYVY */
    for (i = 0; cam_fmts[i].pixelformat; i++) {
        if (V4L2_PIX_FMT_YUYV == cam_fmts[i].pixelformat) {
            pixel_format = V4L2_PIX_FMT_YUYV;
            break;
        }
    }
    if (!pixel_format) {
        for (i = 0; cam_fmts[i].pixelformat; i++) {
            if (V4L2_PIX_FMT_UYVY == cam_fmts[i].pixelformat) {
                pixel_format = V4L2_PIX_FMT_UYVY;
                break;
            }
        }
    }
    if (!pixel_format) {
        fprintf(stderr, "Error: the device does not support YUYV/UYVY format!\n"
                "       请查看上面打印的格式清单,确认摄像头支持的格式\n");
        return -1;
    }

    /* 设置帧格式 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;    //type 类型
    fmt.fmt.pix.width = DEFAULT_WIDTH;          //视频帧宽度
    fmt.fmt.pix.height = DEFAULT_HEIGHT;        //视频帧高度
    fmt.fmt.pix.pixelformat = pixel_format;     //像素格式
    if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
        fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
        return -1;
    }

    /*** 判断是否已经设置为我们要求的像素格式
        如果没有设置成功表示该设备不支持该像素格式 */
    if (pixel_format != fmt.fmt.pix.pixelformat) {
        fprintf(stderr, "Error: set format failed!\n");
        return -1;
    }

    frm_width = fmt.fmt.pix.width;      //获取实际的帧宽度
    frm_height = fmt.fmt.pix.height;    //获取实际的帧高度
    frm_bytesperline = fmt.fmt.pix.bytesperline;
    if (!frm_bytesperline)              //某些驱动不填充该字段,按每像素 2 字节计算
        frm_bytesperline = frm_width * 2;
    printf("视频帧大小<%d * %d>, 像素格式<0x%x>\n", frm_width, frm_height, pixel_format);

    /* 获取 streamparm */
    streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);

    /** 判断是否支持帧率设置(USB 摄像头大多不支持,失败不致命) **/
    if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
        streamparm.parm.capture.timeperframe.numerator = 1;
        streamparm.parm.capture.timeperframe.denominator = 30;  //30fps
        if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
            fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
        }
    }

    return 0;
}

static int v4l2_init_buffer(void)
{
    struct v4l2_requestbuffers reqbuf = {0};
    struct v4l2_buffer buf = {0};

    /* 申请帧缓冲 */
    reqbuf.count = FRAMEBUFFER_COUNT;        //帧缓冲的数量
    reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    reqbuf.memory = V4L2_MEMORY_MMAP;
    if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {
        fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
        return -1;
    }

    /* 建立内存映射 */
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

        ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
        buf_infos[buf.index].length = buf.length;
        buf_infos[buf.index].start = mmap(NULL, buf.length,
                                          PROT_READ | PROT_WRITE, MAP_SHARED,
                                          v4l2_fd, buf.m.offset);
        if (MAP_FAILED == (void *)buf_infos[buf.index].start) {
            perror("mmap error");
            return -1;
        }
    }

    /* 入队 */
    for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {

        if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
            fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
            return -1;
        }
    }

    return 0;
}

static int v4l2_stream_on(void)
{
    /* 打开摄像头、摄像头开始采集数据 */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMON, &type)) {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMON: %s\n", strerror(errno));
        return -1;
    }

    return 0;
}

static int v4l2_stream_off(void)
{
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    if (0 > ioctl(v4l2_fd, VIDIOC_STREAMOFF, &type)) {
        fprintf(stderr, "ioctl error: VIDIOC_STREAMOFF: %s\n", strerror(errno));
        return -1;
    }

    return 0;
}

/***************************************************************
 * 单个像素 YUV(4:2:2) -> RGB565
 * 使用 BT.601 limited-range 整数公式(linux 内核/ffmpeg 常用算法):
 *   c = Y - 16, d = U - 128, e = V - 128
 *   R = (298*c + 409*e + 128) >> 8
 *   G = (298*c - 100*d - 208*e + 128) >> 8
 *   B = (298*c + 516*d + 128) >> 8
 ***************************************************************/
static inline unsigned short yuv_to_rgb565(int y, int u, int v)
{
    int c = y - 16;
    int d = u - 128;
    int e = v - 128;
    int r, g, b;
    unsigned short rgb565;

    r = (298 * c + 409 * e + 128) >> 8;
    g = (298 * c - 100 * d - 208 * e + 128) >> 8;
    b = (298 * c + 516 * d + 128) >> 8;

    /* 裁剪到 0~255 */
    if (r < 0) r = 0;
    if (r > 255) r = 255;
    if (g < 0) g = 0;
    if (g > 255) g = 255;
    if (b < 0) b = 0;
    if (b > 255) b = 255;

    /* RGB888 -> RGB565 */
    rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
    return rgb565;
}

/***************************************************************
 * 将一行 YUYV / UYVY 数据转换为 RGB565
 * YUYV 每 4 个字节表示 2 个像素: [Y0 U Y1 V]
 * UYVY 每 4 个字节表示 2 个像素: [U Y0 V Y1]
 ***************************************************************/
static void yuv_to_rgb565_row(const unsigned char *src,
                              unsigned short *dst, int n_pixels)
{
    int i;

    for (i = 0; i < n_pixels / 2; i++) {
        unsigned char y0, u, y1, v;

        if (V4L2_PIX_FMT_YUYV == pixel_format) {
            y0 = src[0]; u = src[1]; y1 = src[2]; v = src[3];
        } else {    //V4L2_PIX_FMT_UYVY
            u = src[0]; y0 = src[1]; v = src[2]; y1 = src[3];
        }

        dst[0] = yuv_to_rgb565(y0, u, v);
        dst[1] = yuv_to_rgb565(y1, u, v);

        src += 4;
        dst += 2;
    }
}

/***************************************************************
 * RGB565 -> ARGB8888(用于 32bpp LCD 帧缓冲)
 ***************************************************************/
static void rgb565_to_argb8888(const unsigned short *src,
                               unsigned int *dst, int n_pixels)
{
    int i;

    for (i = 0; i < n_pixels; i++) {
        unsigned short p = src[i];
        unsigned int r, g, b;

        r = (p >> 11) & 0x1F;
        g = (p >> 5) & 0x3F;
        b = p & 0x1F;

        r = (r << 3) | (r >> 2);    //5bit -> 8bit
        g = (g << 2) | (g >> 4);    //6bit -> 8bit
        b = (b << 3) | (b >> 2);    //5bit -> 8bit

        dst[i] = 0xFF000000 | (r << 16) | (g << 8) | b;
    }
}

static void v4l2_read_data(void)
{
    struct v4l2_buffer buf = {0};
    unsigned char *base;
    unsigned short *line_buf;   //一行 RGB565 转换缓冲
    unsigned char *row_dst;     //LCD 当前行写入位置
    int min_w, min_h;
    int j;

    if (width > frm_width)
        min_w = frm_width;
    else
        min_w = width;
    if (height > frm_height)
        min_h = frm_height;
    else
        min_h = height;

    /* 分配一行 RGB565 数据缓冲(用于 YUYV -> RGB565 转换) */
    line_buf = malloc(min_w * 2);
    if (NULL == line_buf) {
        perror("malloc error");
        return;
    }

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    while (!stop_flag) {

        /* 出队:从内核帧缓冲队列取出一个装满一帧数据的缓冲 */
        if (0 > ioctl(v4l2_fd, VIDIOC_DQBUF, &buf))
            continue;

        for (j = 0, base = buf_infos[buf.index].start;
             j < min_h; j++) {

            /* 将一行 YUYV 数据转换为 RGB565 */
            yuv_to_rgb565_row(base, line_buf, min_w);

            /* 写入 LCD 当前行(按 LCD 实际位深) */
            row_dst = (unsigned char *)screen_base + j * fb_line_length;
            if (16 == fb_bpp)
                memcpy(row_dst, line_buf, min_w * 2);   //16bpp 直接拷贝
            else
                rgb565_to_argb8888(line_buf, (unsigned int *)row_dst, min_w);

            base += frm_bytesperline;   //指向视频帧下一行数据
        }

        // 数据处理完之后、再入队、往复
        ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
    }

    free(line_buf);
}

static void sigint_handler(int sig)
{
    (void)sig;
    stop_flag = 1;
}

int main(int argc, char *argv[])
{
    if (2 != argc) {
        fprintf(stderr, "Usage: %s <video_dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 注册 SIGINT 处理函数:按 Ctrl+C 时干净退出 */
    signal(SIGINT, sigint_handler);

    /* 初始化 LCD */
    if (fb_dev_init())
        exit(EXIT_FAILURE);

    /* 初始化摄像头 */
    if (v4l2_dev_init(argv[1]))
        exit(EXIT_FAILURE);

    /* 枚举所有格式并打印摄像头支持的分辨率及帧率 */
    v4l2_enum_formats();
    v4l2_print_formats();

    /* 设置格式 */
    if (v4l2_set_format())
        exit(EXIT_FAILURE);

    /* 初始化帧缓冲:申请、内存映射、入队 */
    if (v4l2_init_buffer())
        exit(EXIT_FAILURE);

    /* 开启视频采集 */
    if (v4l2_stream_on())
        exit(EXIT_FAILURE);

    /* 读取数据:出队 -> 转换 -> 上屏 -> 入队 */
    printf("开始采集,按 Ctrl+C 退出...\n");
    v4l2_read_data();

    /* 结束采集、释放资源 */
    v4l2_stream_off();
    close(v4l2_fd);
    munmap(screen_base, fb_screen_size);
    close(fb_fd);

    exit(EXIT_SUCCESS);
}

代码中的 yuv_to_rgb565() 是 YUYV→RGB565 转换的核心:使用 BT.601 limited-range 整数公式(R = (298*c + 409*e + 128) >> 8 等,c = Y-16、d = U-128、e = V-128),一个 YUYV 单元(4 字节 = 2 像素)转换为 2 个 RGB565 像素,再裁剪到 0~255。该公式是 Linux 内核/ffmpeg 处理 YUV 数据时的常用算法。

编译与测试

  1. 关闭出厂系统的 Qt 进程。出厂系统开机自启的 Qt 界面直接写 /dev/fb0 显存,若不关闭:Qt 空闲时不刷屏,摄像头画面看似正常,但一旦触摸屏幕,Qt 收到事件就会重绘自己的界面,覆盖摄像头画面并响应原本的交互动作。Qt 进程名通常不含 "qt" 字样(ps -ef | grep -i qt 找不到),需要精准定位"谁打开了 /dev/fb0":
# 找出打开 /dev/fb0 的所有进程(纯 shell 遍历 /proc,busybox 即可运行)
for fd in /proc/[0-9]*/fd/*; do t=$(readlink $fd 2>/dev/null); if [ "$t" = "/dev/fb0" ]; then pid=$(echo $fd | cut -d/ -f3); echo "PID=$pid 进程名=$(cat /proc/$pid/comm) 完整命令=$(tr '\0' ' ' < /proc/$pid/cmdline)"; fi; done

# 杀掉它(用上面输出的 PID)
kill -9 <PID>

验证是否杀干净:触摸屏幕不再有任何界面交互反应;重跑上面命令无输出也说明 fb0 已释放。注意重启开发板后 Qt 会再次自启,每次实验前都要重复本步骤(想彻底关闭自启可禁用 /etc/init.d/ 下对应的启动脚本)。

  1. 编译gcc -O2 -Wall -o v4l2_camera v4l2_camera.c,得到可执行文件后拷贝到开发板 Linux 系统的用户家目录下(PC 上交叉编译用 arm-linux-gnueabihf-gcc,如 make CC=arm-linux-gnueabihf-gcc);
  2. 硬件准备:USB 摄像头插入开发板 USB HOST 接口(可运行状态下直接插入),ls /dev/video* 查看设备节点(不同板子编号不同,本实验板子上 USB 摄像头对应 /dev/video2);
  3. 运行:传入摄像头对应的设备节点参数:
./v4l2_camera /dev/video2

程序运行后,首先打印摄像头支持的格式清单,然后 LCD 屏上实时显示摄像头采集到的图像,按 Ctrl+C 结束。实际运行输出示例:

format<0x56595559>, description<YUYV 4:2:2>
size<1920*1080> <5fps>
size<1280*720> <10fps>
size<640*480> <30fps>
size<320*240> <30fps>

format<0x47504a4d>, description<MJPG Motion-JPEG>
size<4000*3000> <15fps>
size<3840*2160> <30fps>
...
视频帧大小<640 * 480>, 像素格式<0x56595559>
开始采集,按 Ctrl+C 退出...

如何查看 USB 摄像头支持哪些格式?

  1. 运行本程序,启动时自动枚举并打印所有格式/分辨率/帧率;
  2. v4l2-ctl --list-formats-ext -d /dev/videoX(需安装 v4l-utils);
  3. cat /sys/class/video4linux/videoX/formats(仅较新内核 5.x/6.x 提供该文件,正点原子出厂系统内核 4.1.15 下不存在,属正常现象)。

只要摄像头支持 YUYV 即可运行本程序(绝大多数 USB 摄像头都支持);0x56595559 即 fourcc 'Y''U''Y''V' 的十六进制值。

常见问题:

  • /dev/videoX 打开失败或提示 No capture video device!:设备节点不对,试其他编号(ls /dev/video* 查看有哪些节点);
  • 提示 the device does not support YUYV/UYVY format!:摄像头只支持 MJPEG 等压缩格式(极少见),需引入 libjpeg 解码;
  • 摄像头画面正常,但触摸屏幕出现 Qt 界面/交互动作:Qt 进程未关闭,重新执行第 1 步;
  • 画面只占 LCD 左上角一部分:LCD 分辨率大于 640×480,本实验只显示最小区域、不做缩放,属正常现象。

总结

V4L2 摄像头应用编程的核心就是一套固定的流程,几乎全部通过 ioctl() 搭配不同的 VIDIOC_XXX 指令完成:

步骤 指令 关键数据结构
打开设备 open() -
查询能力 VIDIOC_QUERYCAP struct v4l2_capability
枚举像素格式 VIDIOC_ENUM_FMT struct v4l2_fmtdesc
枚举分辨率 VIDIOC_ENUM_FRAMESIZES struct v4l2_frmsizeenum
枚举帧率 VIDIOC_ENUM_FRAMEINTERVALS struct v4l2_frmivalenum
获取/设置格式 VIDIOC_G_FMT / VIDIOC_S_FMT struct v4l2_format
获取/设置流参数 VIDIOC_G_PARM / VIDIOC_S_PARM struct v4l2_streamparm
申请帧缓冲 VIDIOC_REQBUFS struct v4l2_requestbuffers
查询帧缓冲 VIDIOC_QUERYBUF struct v4l2_buffer
内存映射 mmap() -
入队 VIDIOC_QBUF struct v4l2_buffer
开启采集 VIDIOC_STREAMON enum v4l2_buf_type
出队 VIDIOC_DQBUF struct v4l2_buffer
结束采集 VIDIOC_STREAMOFF enum v4l2_buf_type
posted @ 2026-08-13 16:42  Javenwww  阅读(3)  评论(0)    收藏  举报