嵌入式 Linux V4L2 摄像头采集编程(五):MMAP + 亮度实时控制(附完整代码与面试题)

嵌入式 Linux V4L2 摄像头采集编程(五):MMAP + 亮度实时控制(附完整代码与面试题)

适用硬件:IMX6ULL / 各类支持 V4L2 的嵌入式板卡

功能:采集 MJPEG 图片并保存为 .jpg,同时支持键盘 u/d 实时调节摄像头亮度

日期:2026-05-06


文章目录

  • [嵌入式 Linux V4L2 摄像头采集编程(五):MMAP + 亮度实时控制(附完整代码与面试题)](#嵌入式 Linux V4L2 摄像头采集编程(五):MMAP + 亮度实时控制(附完整代码与面试题))
    • [1. 引言](#1. 引言)
    • [2. 完整代码(video_test.c)](#2. 完整代码(video_test.c))
    • [3. 核心知识点深度解析](#3. 核心知识点深度解析)
      • [3.1 V4L2 控制接口(亮度调节)](#3.1 V4L2 控制接口(亮度调节))
      • [3.2 多线程参数传递技巧](#3.2 多线程参数传递技巧)
      • [3.3 V4L2 常见宏与结构体](#3.3 V4L2 常见宏与结构体)
      • [3.4 为什么需要检查 `revents & POLLIN`?](#3.4 为什么需要检查 revents & POLLIN?)
    • [4. 程序框架图(拆分三图,截图清晰)](#4. 程序框架图(拆分三图,截图清晰))
    • [5. 编译与运行](#5. 编译与运行)
      • [5.1 交叉编译](#5.1 交叉编译)
      • [5.2 开发板运行](#5.2 开发板运行)
      • [5.3 查看采集的图片](#5.3 查看采集的图片)
    • [6. 常见错误与纠正](#6. 常见错误与纠正)
    • [7. 面试自测题(一问一答)](#7. 面试自测题(一问一答))
    • [8. 扩展练习](#8. 扩展练习)
    • [9. 总结](#9. 总结)

1. 引言

在实际项目中,摄像头采集往往还需要动态调节画面参数(亮度、对比度等)。本文在前一篇 V4L2 采集框架的基础上,增加了多线程亮度控制功能,演示如何通过 V4L2 控制接口实时修改摄像头属性。内容涵盖:

  • V4L2 设备操作完整流程
  • 内存映射(MMAP)方式的缓冲区管理
  • 控制接口(VIDIOC_QUERYCTRL / G_CTRL / S_CTRL)的使用
  • POSIX 线程创建与参数传递
  • 常见宏、结构体深度解析
  • 面试自测题与排错指南

2. 完整代码(video_test.c)

编译命令:arm-buildroot-linux-gnueabihf-gcc -o video_test video_test.c -pthread

c

复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <linux/videodev2.h>   // V4L2 核心头文件
#include <poll.h>
#include <sys/mman.h>
#include <pthread.h>

// 亮度控制线程函数
static void *thread_brightness_control(void *args)
{
    int fd = (int)(long)args;          // 获取设备文件描述符

    // 1. 查询亮度控制的范围
    struct v4l2_queryctrl qctrl;
    memset(&qctrl, 0, sizeof(qctrl));
    qctrl.id = V4L2_CID_BRIGHTNESS;    // 控制ID:亮度

    if (ioctl(fd, VIDIOC_QUERYCTRL, &qctrl) < 0) {
        printf("can not query brightness\n");
        return NULL;
    }

    printf("brightness min = %d, max = %d\n", qctrl.minimum, qctrl.maximum);
    int delta = (qctrl.maximum - qctrl.minimum) / 10;   // 每次按键变化 1/10 范围

    // 2. 获取当前亮度值
    struct v4l2_control ctl;
    ctl.id = V4L2_CID_BRIGHTNESS;
    ioctl(fd, VIDIOC_G_CTRL, &ctl);    // ctl.value 被填充为当前亮度

    // 3. 循环处理键盘输入
    while (1) {
        unsigned char c = getchar();
        if (c == 'u' || c == 'U')
            ctl.value += delta;
        else if (c == 'd' || c == 'D')
            ctl.value -= delta;
        else
            continue;   // 其他按键忽略

        // 钳位到硬件允许的范围
        if (ctl.value > qctrl.maximum) ctl.value = qctrl.maximum;
        if (ctl.value < qctrl.minimum) ctl.value = qctrl.minimum;

        // 设置新的亮度值
        if (ioctl(fd, VIDIOC_S_CTRL, &ctl) == 0)
            printf("brightness set to %d\n", ctl.value);
        else
            perror("VIDIOC_S_CTRL");
    }
    return NULL;
}

int main(int argc, char **argv)
{
    int fd;
    struct v4l2_fmtdesc fmtdesc;
    struct v4l2_frmsizeenum fsenum;
    int fmt_index = 0, frame_index = 0, i;
    void *bufs[32];
    int buf_cnt;
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    struct pollfd fds[1];
    char filename[32];
    int file_cnt = 0;

    if (argc != 2) {
        printf("Usage: %s </dev/videoX>\n", argv[0]);
        return -1;
    }

    /* ---------- 1. 打开设备 ---------- */
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    /* ---------- 2. 查询能力 ---------- */
    struct v4l2_capability cap;
    memset(&cap, 0, sizeof(cap));
    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
        if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
            fprintf(stderr, "Error: %s: video capture not supported.\n", argv[1]);
            return -1;
        }
        if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
            fprintf(stderr, "%s does not support streaming i/o\n", argv[1]);
            return -1;
        }
    } else {
        printf("can not get capability\n");
        return -1;
    }

    /* ---------- 3. 枚举格式与分辨率(调试用) ---------- */
    while (1) {
        fmtdesc.index = fmt_index;
        fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        if (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) != 0)
            break;

        frame_index = 0;
        while (1) {
            memset(&fsenum, 0, sizeof(fsenum));
            fsenum.pixel_format = fmtdesc.pixelformat;
            fsenum.index = frame_index;
            if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fsenum) == 0) {
                printf("format %s, fourcc=0x%x, framesize %d: %d x %d\n",
                       fmtdesc.description, fmtdesc.pixelformat, frame_index,
                       fsenum.discrete.width, fsenum.discrete.height);
                frame_index++;
            } else {
                break;
            }
        }
        fmt_index++;
    }

    /* ---------- 4. 设置格式 ---------- */
    struct v4l2_format fmt;
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 1280;                // 期望宽度
    fmt.fmt.pix.height = 720;                // 期望高度
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;  // 期望 MJPEG
    fmt.fmt.pix.field = V4L2_FIELD_ANY;      // 场序任意

    if (ioctl(fd, VIDIOC_S_FMT, &fmt) == 0) {
        printf("set format ok: %d x %d, fourcc=0x%x\n",
               fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.pixelformat);
    } else {
        perror("VIDIOC_S_FMT");
        return -1;
    }

    /* ---------- 5. 申请缓冲区 ---------- */
    struct v4l2_requestbuffers rb;
    memset(&rb, 0, sizeof(rb));
    rb.count = 32;
    rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    rb.memory = V4L2_MEMORY_MMAP;

    if (ioctl(fd, VIDIOC_REQBUFS, &rb) != 0) {
        perror("VIDIOC_REQBUFS");
        return -1;
    }
    buf_cnt = rb.count;      // 实际分配到的 buffer 数量(可能少于32)
    printf("got %d buffers\n", buf_cnt);

    /* ---------- 6. 查询并 mmap 每个 buffer ---------- */
    for (i = 0; i < buf_cnt; i++) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.index = i;
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        if (ioctl(fd, VIDIOC_QUERYBUF, &buf) != 0) {
            perror("VIDIOC_QUERYBUF");
            return -1;
        }
        bufs[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
                       MAP_SHARED, fd, buf.m.offset);
        if (bufs[i] == MAP_FAILED) {
            perror("mmap");
            return -1;
        }
        printf("buffer %d mapped, length=%d\n", i, buf.length);
    }

    /* ---------- 7. 将所有 buffer 放入输入队列 ---------- */
    for (i = 0; i < buf_cnt; i++) {
        struct v4l2_buffer buf;
        memset(&buf, 0, sizeof(buf));
        buf.index = i;
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        if (ioctl(fd, VIDIOC_QBUF, &buf) != 0) {
            perror("VIDIOC_QBUF");
            return -1;
        }
    }
    printf("all buffers queued\n");

    /* ---------- 8. 启动视频流 ---------- */
    if (ioctl(fd, VIDIOC_STREAMON, &type) != 0) {
        perror("VIDIOC_STREAMON");
        return -1;
    }
    printf("stream started\n");

    /* ---------- 9. 创建亮度控制线程 ---------- */
    pthread_t tid;
    pthread_create(&tid, NULL, thread_brightness_control, (void *)(long)fd);

    /* ---------- 10. 主循环:采集图片 ---------- */
    while (1) {
        fds[0].fd = fd;
        fds[0].events = POLLIN;
        if (poll(fds, 1, -1) == 1 && (fds[0].revents & POLLIN)) {
            struct v4l2_buffer buf;
            memset(&buf, 0, sizeof(buf));
            buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
            buf.memory = V4L2_MEMORY_MMAP;

            if (ioctl(fd, VIDIOC_DQBUF, &buf) != 0) {
                perror("VIDIOC_DQBUF");
                break;
            }

            sprintf(filename, "video_raw_data_%04d.jpg", file_cnt++);
            int fd_file = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (fd_file >= 0) {
                write(fd_file, bufs[buf.index], buf.bytesused);
                close(fd_file);
                printf("captured %s, size=%d\n", filename, buf.bytesused);
            } else {
                perror("create file");
            }

            if (ioctl(fd, VIDIOC_QBUF, &buf) != 0) {
                perror("VIDIOC_QBUF");
                break;
            }
        }
    }

    /* ---------- 11. 清理(正常不会执行到,仅示范) ---------- */
    ioctl(fd, VIDIOC_STREAMOFF, &type);
    for (i = 0; i < buf_cnt; i++) {
        // 实际需要保存每个 buffer 的 length,此处省略
        // munmap(bufs[i], length);
    }
    close(fd);
    return 0;
}

3. 核心知识点深度解析

3.1 V4L2 控制接口(亮度调节)

V4L2 使用 control ID 来标识可调节的参数,如亮度、对比度、饱和度等。所有控制 ID 定义在 <linux/videodev2.h> 中。

结构体 / ioctl 作用
struct v4l2_queryctrl 查询某个控制的属性(最小值、最大值、步长、默认值)
VIDIOC_QUERYCTRL 执行查询
struct v4l2_control 存储控制 ID 和具体数值
VIDIOC_G_CTRL 获取当前值 (G = Get)
VIDIOC_S_CTRL 设置新值 (S = Set)

常见控制 ID

  • V4L2_CID_BRIGHTNESS:亮度
  • V4L2_CID_CONTRAST:对比度
  • V4L2_CID_SATURATION:饱和度
  • V4L2_CID_HUE:色调
  • V4L2_CID_AUTO_WHITE_BALANCE:自动白平衡

代码示例

c

复制代码
// 查询亮度范围
struct v4l2_queryctrl qctrl = { .id = V4L2_CID_BRIGHTNESS };
ioctl(fd, VIDIOC_QUERYCTRL, &qctrl);
printf("min=%d, max=%d\n", qctrl.minimum, qctrl.maximum);

// 获取当前亮度
struct v4l2_control ctl = { .id = V4L2_CID_BRIGHTNESS };
ioctl(fd, VIDIOC_G_CTRL, &ctl);

// 设置新亮度
ctl.value = 128;
ioctl(fd, VIDIOC_S_CTRL, &ctl);

3.2 多线程参数传递技巧

在线程创建时,需要把 fd (int) 作为参数传给线程函数。由于 pthread_create 的第四个参数是 void*,直接强转 (void*)fd 会触发"整型到指针转换"警告。正确的做法是两次类型转换

c

复制代码
pthread_create(&tid, NULL, thread_func, (void*)(long)fd);

在线程函数内再转换回来:

c

复制代码
int fd = (int)(long)args;

3.3 V4L2 常见宏与结构体

宏 / 结构体 含义
V4L2_BUF_TYPE_VIDEO_CAPTURE 缓冲区类型:视频捕获
V4L2_MEMORY_MMAP 内存类型:内存映射
V4L2_FIELD_ANY 场序:任意(通常摄像头是逐行)
V4L2_PIX_FMT_MJPEG 像素格式:Motion-JPEG
struct v4l2_fmtdesc 格式描述符,用于枚举格式
struct v4l2_frmsizeenum 帧大小枚举,用于查询分辨率
struct v4l2_buffer 单个缓冲区信息(长度、偏移、实际数据长度)
struct pollfd poll 监听的文件描述符和事件

3.4 为什么需要检查 revents & POLLIN

poll() 返回正数表示有事件发生,但事件可能是 POLLERR(设备错误)或 POLLHUP(挂断)。如果不检查 POLLIN 就调用 DQBUF,可能在异常状态时导致 ioctl 失败或程序崩溃。正确写法:

c

复制代码
if (poll(fds, 1, -1) == 1 && (fds[0].revents & POLLIN)) {
    // 安全地 DQBUF
}

4. 程序框架图(拆分三图,截图清晰)

图1:主线程初始化流程

图2:主线程采集循环

图3:亮度控制子线程

以上 Mermaid 代码可粘贴到支持渲染的编辑器或截图使用。


5. 编译与运行

5.1 交叉编译

bash

复制代码
arm-buildroot-linux-gnueabihf-gcc -o video_test video_test.c -pthread
  • -pthread:必须添加,否则链接时找不到 pthread_create

5.2 开发板运行

bash

复制代码
adb push video_test /root/
adb shell
cd /root
./video_test /dev/video1

预期输出):


注意:要手动按u/U和d/D+回车

此时按键盘的 u 增加亮度,按 d 减少亮度,终端会打印新亮度值,后续采集的图片亮度随之变化。

5.3 查看采集的图片

由于 ADB 可能不稳定,推荐使用 U 盘拷贝

bash

复制代码
# 在开发板上
mount /dev/sda1 /mnt/usb
cp /root/video_raw_data_*.jpg /mnt/usb/
syn
umount /mnt/usb

然后 U 盘插到电脑上直接打开图片。


6. 常见错误与纠正

错误理解 正确解释
"ioctl 返回不等于 0 表示有错误" ioctl 成功返回 0 ,失败返回 -1 。所以判断失败应使用 if (ioctl(...) < 0)
"检查 cap.capabilities 是否等于 0 来判断不支持" capabilities 是位掩码,要用 & 检查特定位,如 cap.capabilities & V4L2_CAP_VIDEO_CAPTURE
"枚举格式时 ioctl 返回不为 0 就是函数没执行" 返回 -1 且 errno == EINVAL 是枚举结束的正常现象,不是错误。
"设置格式成功就一定是我请求的分辨率" 驱动可能调整分辨率,必须读取返回的 fmt.fmt.pix.width/height 以实际值为准。
"poll 返回 1 就可以 DQBUF" 必须检查 revents & POLLIN,否则可能因错误事件导致崩溃。
"创建线程直接传 (void*)fd" 会引发编译警告,应该用 (void*)(long)fd,线程内再转回 (int)(long)arg
"亮度线程中 while(1) 循环退出是正常的" 线程只有出错才会 return NULL,正常情况无限循环。

7. 面试自测题(一问一答)

Q1:VIDIOC_QUERYCTRL 的作用是什么?如何判断摄像头是否支持某个控制?

A:查询指定控制 ID 的属性。如果 ioctl 返回 0 且 qctrl.flags 没有 V4L2_CTRL_FLAG_DISABLED,则说明支持。

Q2:为什么设置亮度前要调用 VIDIOC_G_CTRL 读取当前值?

A:为了获得当前亮度作为基准,然后在此基础上增减。也可以不读直接设绝对值,但读取当前值后增量调节更符合用户预期(按 u 亮一点,按 d 暗一点)。

Q3:两个线程共用同一个 fd 是否安全?

A:V4L2 驱动内部对 ioctl 有串行化(互斥锁),所以同时调用是安全的。但应避免两个线程同时操作同一个控制项(本例中子线程只操作亮度,主线程只做采集,不冲突)。

Q4:mmapoffset 参数从哪里来?为什么不能填 0?

A:offset 来自 VIDIOC_QUERYBUF 返回的 buf.m.offset。不同的 buffer 有不同的物理偏移,填 0 会导致所有 buffer 映射到同一块内存,数据错乱。

Q5:VIDIOC_REQBUFS 请求 32 个 buffer,但实际 rb.count 可能小于 32,为什么?

A:驱动可能因内存不足或硬件限制无法分配那么多。必须使用返回后的 rb.count 作为实际 buffer 数量,否则访问越界会出错。

Q6:如何优雅地终止程序并释放资源?

A:捕获 SIGINT 信号,设置全局退出标志,主线程跳出循环,等待子线程结束(pthread_join),然后停止流、munmapclose

Q7:如果摄像头不支持 MJPEG,VIDIOC_S_FMT 会怎样?

A:可能返回 -1 失败,也可能返回 0 但将 fmt.fmt.pix.pixelformat 改为 YUYV。因此必须检查实际格式,若不是 MJPEG 则应保存为 .yuv 或进行格式转换。

Q8:poll 的超时参数设为 -1 有什么风险?

A:-1 表示无限等待,程序会一直阻塞直到有数据或错误。在产品代码中应设置超时(如 2000 毫秒),避免因驱动异常导致永久卡死。


8. 扩展练习

  1. 增加对比度调节 :在亮度线程中增加对 V4L2_CID_CONTRAST 的支持,用 c/x 键调节。
  2. 支持保存为 YUV 文件 :当摄像头只支持 YUYV 时,自动保存为 .yuv 原始文件。
  3. 增加帧率统计:每秒打印一次采集帧数。
  4. 信号处理 :实现 Ctrl+C 优雅退出,释放所有资源。

9. 总结

本文从一个完整的、可运行的 V4L2 采集程序出发,详细讲解了:

  • 设备打开、能力查询、格式枚举与设置
  • 缓冲区申请、映射、入队、出队
  • 视频流启动、轮询采集、数据保存
  • 控制接口的使用(亮度实时调节)
  • 多线程编程与参数传递
  • 常见错误与面试题

掌握这些内容,你就能够独立开发嵌入式 Linux 下的摄像头采集应用,并灵活扩展其他功能。


相关推荐
2301_789015622 小时前
Linux基础指令(一)
linux·运维·服务器·c语言·开发语言·c++·linux指令
晚风予卿云月2 小时前
【linux】进程优先级
linux·运维·服务器
一拳一个娘娘腔2 小时前
从sudo配置到Root Shell:Linux Sudo提权全景深度解析与防御指南
linux·网络·安全
万法若空2 小时前
Cortex-A7的运行模式
linux·arm开发
YJlio2 小时前
用女娲蒸馏 Mark Russinovich 排障思维:打造 Windows 桌面运维专家 Skill
运维·windows·飞书·ai办公·多维表格·飞书v7.63·飞书问卷
yyuuuzz2 小时前
aws注册过程中的常见问题梳理
运维·服务器·网络·云计算·github·aws
德迅云安全-小潘2 小时前
手游架设全攻略:服务器选型、配置与部署一站式指南
运维·服务器
zhangrelay3 小时前
三分钟云课实践速通--C/C++程序设计--
linux·c语言·c++·笔记·学习·ubuntu
Hical_W3 小时前
从 io_context 出发,掌握 C++20 协程式异步 I/O,学会 TCP 服务器、定时器和多线程模型,结合 Hical 框架实战解读
服务器·tcp/ip·开源·c++20