2. 摄像头采集与预览

任务目标

在嵌入式Linux设备上,通过V4L2接口从USB摄像头采集原始视频帧,并将帧数据直接写入本地LCD framebuffer实现实时预览。本案例聚焦最小可运行闭环:V4L2采集→帧缓冲→LCD显示,不涉及格式转换、编码或网络传输。

环境与依赖

  • 硬件:USB摄像头(支持YUYV格式)、带LCD屏的嵌入式板卡(如RV1126/全志V3s)
  • 内核:启用V4L2驱动(CONFIG_VIDEO_DEV)、LCD framebuffer驱动(CONFIG_FB)
  • 设备节点:/dev/video0(摄像头)、/dev/fb0(LCD)
  • 交叉编译工具链(如arm-linux-gnueabihf-gcc)

实现原理

程序通过V4L2的VIDIOC_S_FMT将摄像头输出格式固定为YUYV(4:2:2打包格式),分辨率与LCD一致(如800×480)。采集到的YUYV数据无法直接显示在RGB565的LCD上,因此需要将每个像素的YUV分量转换为RGB565。转换后通过mmap映射写入framebuffer,实现单帧显示;循环采集即形成实时预览。

关键完整代码

// preview.c - V4L2 USB摄像头采集 + LCD framebuffer实时预览
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/fb.h>

#define WIDTH   800
#define HEIGHT  480
#define FB_SIZE (WIDTH * HEIGHT * 2)  // RGB565

// YUV(YUYV) 转 RGB565
static unsigned short yuyv_to_rgb565(unsigned char y, unsigned char u, unsigned char v) {
    int c = y - 16, d = u - 128, e = v - 128;
    int r = (298 * c + 409 * e + 128) >> 8;
    int g = (298 * c - 100 * d - 208 * e + 128) >> 8;
    int b = (298 * c + 516 * d + 128) >> 8;
    r = r < 0 ? 0 : (r > 255 ? 255 : r);
    g = g < 0 ? 0 : (g > 255 ? 255 : g);
    b = b < 0 ? 0 : (b > 255 ? 255 : b);
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}

int main() {
    // ---- 1. 打开LCD framebuffer ----
    int fbfd = open("/dev/fb0", O_RDWR);
    if (fbfd < 0) { perror("open fb0"); return 1; }
    unsigned short *fb = mmap(NULL, FB_SIZE, PROT_READ | PROT_WRITE,
                              MAP_SHARED, fbfd, 0);
    if (fb == MAP_FAILED) { perror("mmap fb"); return 1; }

    // ---- 2. 打开摄像头并设置格式 ----
    int vfd = open("/dev/video0", O_RDWR);
    if (vfd < 0) { perror("open video0"); return 1; }

    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(vfd, VIDIOC_S_FMT, &fmt) < 0) { perror("set format"); return 1; }

    // ---- 3. 请求缓冲区并mmap ----
    struct v4l2_requestbuffers req;
    memset(&req, 0, sizeof(req));
    req.count = 4;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(vfd, VIDIOC_REQBUFS, &req) < 0) { perror("reqbufs"); return 1; }

    struct v4l2_buffer buf;
    unsigned char *buffers[4];
    for (int i = 0; i < 4; i++) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        if (ioctl(vfd, VIDIOC_QUERYBUF, &buf) < 0) { perror("querybuf"); return 1; }
        buffers[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
                          MAP_SHARED, vfd, buf.m.offset);
        if (buffers[i] == MAP_FAILED) { perror("mmap buf"); return 1; }
        if (ioctl(vfd, VIDIOC_QBUF, &buf) < 0) { perror("qbuf"); return 1; }
    }

    // ---- 4. 启动采集流 ----
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(vfd, VIDIOC_STREAMON, &type) < 0) { perror("streamon"); return 1; }

    // ---- 5. 采集循环:取帧→转换→写LCD ----
    for (int frame = 0; frame < 300; frame++) {  // 约5秒@60fps
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        if (ioctl(vfd, VIDIOC_DQBUF, &buf) < 0) { perror("dqbuf"); break; }

        unsigned char *yuyv = buffers[buf.index];
        for (int i = 0, p = 0; i < WIDTH * HEIGHT * 2; i += 4, p += 2) {
            // 每4字节含2个像素: Y0 U Y1 V
            fb[p]     = yuyv_to_rgb565(yuyv[i], yuyv[i+1], yuyv[i+3]);
            fb[p + 1] = yuyv_to_rgb565(yuyv[i+2], yuyv[i+1], yuyv[i+3]);
        }

        if (ioctl(vfd, VIDIOC_QBUF, &buf) < 0) { perror("qbuf"); break; }
        usleep(16000);  // 约60fps
    }

    // ---- 6. 清理 ----
    ioctl(vfd, VIDIOC_STREAMOFF, &type);
    for (int i = 0; i < 4; i++) munmap(buffers[i], 0);
    munmap(fb, FB_SIZE);
    close(vfd);
    close(fbfd);
    return 0;
}

编译与运行

# 交叉编译(示例)
arm-linux-gnueabihf-gcc -o preview preview.c

# 拷贝到目标板后运行
./preview

验证方法

  1. 运行程序前,确认/dev/video0和/dev/fb0均存在且有读写权限。
  2. 执行./preview后,观察LCD屏幕是否出现摄像头拍摄的实时画面。
  3. 用手在摄像头前晃动,画面应同步变化,无明显撕裂或卡顿。
  4. 程序运行约5秒后自动退出,终端无报错信息即表示采集与显示链路正常。

通过条件:LCD上呈现清晰、连续、方向正确的实时视频画面,且程序正常退出无错误输出。

常见问题

  • 画面颜色异常:检查LCD实际像素格式是否为RGB565,若为RGB888需调整FB_SIZE和写入方式。
  • ioctl报错:确认摄像头支持YUYV格式且分辨率不超过其最大能力,可用v4l2-ctl --list-formats-ext查询。
  • 画面闪烁:尝试增加usleep时间降低帧率,或检查LCD刷新率是否匹配。