嵌入式 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:mmap 的 offset 参数从哪里来?为什么不能填 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),然后停止流、munmap、close。
Q7:如果摄像头不支持 MJPEG,VIDIOC_S_FMT 会怎样?
A:可能返回 -1 失败,也可能返回 0 但将 fmt.fmt.pix.pixelformat 改为 YUYV。因此必须检查实际格式,若不是 MJPEG 则应保存为 .yuv 或进行格式转换。
Q8:poll 的超时参数设为 -1 有什么风险?
A:-1 表示无限等待,程序会一直阻塞直到有数据或错误。在产品代码中应设置超时(如 2000 毫秒),避免因驱动异常导致永久卡死。
8. 扩展练习
- 增加对比度调节 :在亮度线程中增加对
V4L2_CID_CONTRAST的支持,用c/x键调节。 - 支持保存为 YUV 文件 :当摄像头只支持 YUYV 时,自动保存为
.yuv原始文件。 - 增加帧率统计:每秒打印一次采集帧数。
- 信号处理 :实现
Ctrl+C优雅退出,释放所有资源。
9. 总结
本文从一个完整的、可运行的 V4L2 采集程序出发,详细讲解了:
- 设备打开、能力查询、格式枚举与设置
- 缓冲区申请、映射、入队、出队
- 视频流启动、轮询采集、数据保存
- 控制接口的使用(亮度实时调节)
- 多线程编程与参数传递
- 常见错误与面试题
掌握这些内容,你就能够独立开发嵌入式 Linux 下的摄像头采集应用,并灵活扩展其他功能。