当前位置: 首页 > news >正文

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

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

适用硬件:IMX6ULL / 各类支持 V4L2 的嵌入式板卡
功能:采集 MJPEG 图片并保存为.jpg,同时支持键盘u/d实时调节摄像头亮度
日期:2026-05-06


文章目录

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

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 下的摄像头采集应用,并灵活扩展其他功能。


http://www.jsqmd.com/news/761808/

相关文章:

  • 基于开源项目构建可编程任务管理系统:从全栈架构到个性化工作流
  • Clawup:基于管道模型的Go语言文件抓取与处理工具实战
  • 【通信】MC-CDMA系统Matlab仿真
  • 3步掌握DistroAV:NDI网络视频传输的终极指南
  • 基于Claude API的AI应用开发框架:everything-claude核心功能与实战解析
  • DeTikZify:基于多模态大模型的草图转TikZ代码工具详解
  • AI编程助手上下文工程实战:从瞎猜到精准生成的模板化指南
  • 多用户AI助手系统架构设计:从会话隔离到生产部署全解析
  • 人机界面的DOS分析:自感痕迹论的范式贡献
  • 【2026社工】初级社会工作者历年真题及答案解析PDF电子版(2010-2025年)
  • Java 篇-项目实战-天机学堂(从0到1)-day12
  • SBOM自动化工具minefield:像扫雷一样排查软件供应链安全漏洞
  • 【OC】多界面传值总结
  • 别让高功率激光烧坏你的镜头!一文搞懂LIDT(激光损伤阈值)怎么选
  • 如何一劳永逸解决Windows和Office激活问题?KMS智能激活终极指南
  • 开源鼠标增强工具MousePal:手势识别与按键重映射实战指南
  • 2026年甘肃亮化工程权威厂家TOP5:兰州路灯工程/兰州道路照明工程/兰州发光字/兰州商业街区泛光照明/兰州太阳能路灯/选择指南 - 优质品牌商家
  • 为什么你的PHP订单系统总在凌晨三点告警?资深架构师亲授6步根因定位法
  • MAXQ JTAG板固件更新全流程与常见问题解析
  • AI 系统上线后模型列表空白的稳定性治理:从缓存失效到分层兜底的工程实践
  • 拆解Simulink导弹模型:自动驾驶仪设计、导引头建模与Stateflow制导逻辑详解
  • 智能体编排框架agents-flex:构建复杂AI系统的柔性骨架
  • TPFanCtrl2终极指南:如何彻底掌控ThinkPad风扇,打造静音高效的散热系统
  • 嵌入式实时调度算法与分区技术解析
  • R 4.5量化回测黄金标准白皮书(2024 Q2更新):涵盖IS/OS划分规范、滚动窗口长度最优解(基于信息熵最小化)、及监管沙盒验证模板
  • 别再截图了!用Mathpix API+Python脚本,5分钟批量识别100份数学试卷公式
  • 3步解锁你的Switch:TegraRcmGUI完整免费教程
  • Yume1.5:基于文本控制的3D世界生成技术解析
  • Scikit-LLM:将大语言模型无缝集成到Scikit-learn工作流
  • 高中数学教资面试教案设计:用这个万能模板套用《函数单调性》等高频课题