使用V4L2 API捕获MIPI CSI相机图像时程序卡顿问题咨询
我帮你梳理下这个问题——你的V4L2代码在USB相机上正常工作,但换到MIPI CSI相机就冻结,确实是因为两类相机在V4L2配置上有不少差异,必须针对性调整代码才行。下面我分点说明要改的核心点,再给你一个无GStreamer依赖的完整示例:
一、必须修改的代码要点
1. 确认相机支持的像素格式与分辨率
MIPI CSI相机的格式支持和USB相机差异很大:很多USB相机默认支持MJPEG,但多数MIPI CSI相机(比如树莓派的IMX219、Jetson的OV5647)原生只支持YUYV、NV12或者RAW格式,并不支持MJPEG压缩输出。如果你的代码硬编码了MJPEG格式,相机无法响应就会导致程序冻结。
解决方法:
- 先通过
VIDIOC_ENUM_FMT枚举相机支持的所有格式,选择相机支持的格式(比如YUYV) - 用
VIDIOC_ENUM_FRAMESIZES查询对应格式下支持的分辨率,不要硬编码不兼容的分辨率
2. 调整缓冲区分配方式
USB相机常用MMAP或USERPTR缓冲区,但MIPI CSI相机在嵌入式平台上通常需要使用DMABUF(直接内存访问缓冲区),因为MIPI数据是通过硬件DMA直接传输到内核缓冲区的,用户空间直接映射可能有权限或兼容性问题。如果你的代码用的是MMAP,可以先尝试保留,但如果还是冻结,就需要切换到DMABUF模式。
3. 严格配置帧率与曝光参数
MIPI CSI相机对帧率配置的要求更严格:
- 必须先通过
VIDIOC_S_PARM设置帧率,有些相机需要先将曝光模式设为自动(VIDIOC_S_CTRL设置V4L2_CID_EXPOSURE_AUTO),再启动流 - 避免设置相机不支持的帧率,建议先通过
VIDIOC_ENUM_FRAMEINTERVALS查询支持的帧率区间
4. 确认设备节点与驱动加载
MIPI CSI相机的设备节点通常是/dev/video0或/dev/video1,但要确保你打开的是CSI相机设备(可以用v4l2-ctl --list-devices在终端确认)。另外,部分平台需要先加载对应的相机驱动模块(比如树莓派的bcm2835-v4l2),否则相机无法被V4L2识别。
二、无GStreamer依赖的MIPI CSI相机V4L2示例代码
下面是一个简化的、针对MIPI CSI相机的V4L2捕获示例,支持YUYV格式,用MMAP缓冲区(如果你的相机不支持MMAP,可以替换为DMABUF逻辑):
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <linux/videodev2.h> #define DEVICE_PATH "/dev/video0" #define WIDTH 1920 #define HEIGHT 1080 #define BUFFER_COUNT 4 struct buffer { void *start; size_t length; }; struct buffer *buffers; int fd = -1; // 打开设备并初始化 int init_camera() { fd = open(DEVICE_PATH, O_RDWR | O_NONBLOCK, 0); if (fd == -1) { perror("Failed to open device"); return -1; } // 查询设备能力 struct v4l2_capability cap; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) { perror("Failed to query capability"); return -1; } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { fprintf(stderr, "Device is not a video capture device\n"); return -1; } if (!(cap.capabilities & V4L2_CAP_STREAMING)) { fprintf(stderr, "Device does not support streaming\n"); return -1; } // 设置格式为YUYV struct v4l2_format fmt; memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = WIDTH; fmt.fmt.pix.height = HEIGHT; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) { perror("Failed to set format"); return -1; } // 确认实际设置的格式(相机可能调整分辨率) if (ioctl(fd, VIDIOC_G_FMT, &fmt) == -1) { perror("Failed to get format"); return -1; } printf("Actual format: %dx%d, format: %c%c%c%c\n", fmt.fmt.pix.width, fmt.fmt.pix.height, (fmt.fmt.pix.pixelformat >> 0) & 0xFF, (fmt.fmt.pix.pixelformat >> 8) & 0xFF, (fmt.fmt.pix.pixelformat >> 16) & 0xFF, (fmt.fmt.pix.pixelformat >> 24) & 0xFF); // 请求缓冲区 struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); req.count = BUFFER_COUNT; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) { perror("Failed to request buffers"); return -1; } // 分配缓冲区并映射 buffers = calloc(req.count, sizeof(struct buffer)); for (int i = 0; i < req.count; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) { perror("Failed to query buffer"); return -1; } buffers[i].length = buf.length; buffers[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start == MAP_FAILED) { perror("Failed to mmap buffer"); return -1; } } // 入队缓冲区 for (int i = 0; i < req.count; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) { perror("Failed to queue buffer"); return -1; } } // 启动流 enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) { perror("Failed to start stream"); return -1; } return 0; } // 捕获一帧图像 int capture_frame() { 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) == -1) { if (errno == EAGAIN) { // 没有可用帧,重试 return 0; } perror("Failed to dequeue buffer"); return -1; } // 这里可以处理图像数据,比如保存为文件 printf("Captured frame %d, size: %zu bytes\n", buf.index, buf.bytesused); // 重新入队缓冲区 if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) { perror("Failed to requeue buffer"); return -1; } return 1; } // 清理资源 void cleanup() { enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMOFF, &type); for (int i = 0; i < BUFFER_COUNT; i++) { munmap(buffers[i].start, buffers[i].length); } free(buffers); close(fd); } int main() { if (init_camera() == -1) { cleanup(); return EXIT_FAILURE; } // 捕获10帧后退出 int count = 0; while (count < 10) { int ret = capture_frame(); if (ret == 1) { count++; } usleep(10000); // 10ms延迟 } cleanup(); return EXIT_SUCCESS; }
这个示例做了这些适配MIPI CSI相机的工作:
- 明确使用YUYV格式(多数MIPI相机支持)
- 用MMAP缓冲区(如果你的相机不支持,可以修改为DMABUF,需要额外处理
V4L2_MEMORY_DMABUF逻辑) - 严格遵循V4L2流启动流程:请求缓冲区→映射→入队→启动流
- 处理
EAGAIN错误(非阻塞模式下无帧可用时的正常情况)
三、额外调试技巧
如果还是冻结,可以用这些方法排查:
- 用
v4l2-ctl --all查看相机的当前配置,确认格式、帧率是否正确 - 用
v4l2-ctl --stream-mmap --stream-count=10测试相机是否能正常捕获帧,如果这个命令也冻结,说明是驱动或硬件问题,不是代码问题 - 检查程序的权限,确保当前用户有访问
/dev/videoX的权限(可以加入video用户组)
内容的提问来源于stack exchange,提问作者Avis

