2. 开发环境搭建:交叉编译工具链安装、OpenCV移植、Linux V4L2框架介绍

做嵌入式摄像头开发,第一步就是搭环境。很多人觉得这事简单,照着教程敲命令就行。但我见过太多人卡在这一步——工具链版本不对、OpenCV编译三天三夜、V4L2接口调不通。说白了,环境没搭好,后面全是坑。

这一章,我带你从头捋一遍。咱们的目标很明确:让代码能在开发板上跑起来,能调通摄像头,能显示图像。

2.1 交叉编译工具链:为什么需要它?

你想想看,你的PC是x86架构,开发板是ARM架构。PC上编译的程序,ARM板子根本跑不了。怎么办?交叉编译就是干这个的——在PC上编译出ARM能运行的二进制文件。

核心概念:交叉编译 = 在宿主机(x86)上编译,生成目标机(ARM)的可执行文件。

2.1.1 安装步骤

我个人习惯用Linaro提供的工具链,稳定且社区活跃。以ARM Cortex-A7为例:

# 下载工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到指定目录
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 配置环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin

# 验证安装
arm-linux-gnueabihf-gcc --version

我的经验:环境变量最好写到 ~/.bashrc 里,不然每次开终端都要重新 export。我曾经因为忘了加,浪费了半小时排查编译错误。

2.1.2 测试交叉编译

写个简单的hello world试试:

// hello.c
#include <stdio.h>

int main() {
    printf("Hello, Camera World!\n");
    return 0;
}

// 编译
arm-linux-gnueabihf-gcc hello.c -o hello_arm

// 查看文件信息
file hello_arm
// 输出:ELF 32-bit LSB executable, ARM, version 1 (SYSV)

看到 ARM 字样,说明编译成功了。把这个文件拷到板子上,直接 ./hello_arm 就能跑。

2.2 OpenCV移植:从源码编译到板端运行

OpenCV是摄像头开发的标配。但嵌入式移植是个大工程,我刚开始做的时候,编译一次要等两个小时。后来总结了一套流程,能省不少时间。

2.2.1 准备工作

先确认板子上已经安装了基础依赖。如果没有,用 apt-get 或者手动编译:

依赖库 作用 安装方式
libjpeg-turbo JPEG图像编解码 源码编译
libpng PNG图像编解码 源码编译
libzlib 数据压缩 源码编译
cmake 构建工具 apt-get install cmake

注意:依赖库的版本要和OpenCV匹配。我曾经用了一个太新的libjpeg,结果OpenCV编译时报链接错误。建议用OpenCV源码里 3rdparty 目录下的版本。

2.2.2 编译OpenCV

下载OpenCV源码,我一般用4.5.x版本,稳定且支持V4L2:

# 下载
wget -O opencv-4.5.5.zip https://github.com/opencv/opencv/archive/4.5.5.zip
unzip opencv-4.5.5.zip
cd opencv-4.5.5

# 创建构建目录
mkdir build && cd build

# 配置交叉编译
cmake -DCMAKE_SYSTEM_NAME=Linux \
      -DCMAKE_SYSTEM_PROCESSOR=arm \
      -DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
      -DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++ \
      -DWITH_V4L=ON \
      -DWITH_LIBV4L=ON \
      -DBUILD_opencv_python=OFF \
      -DBUILD_SHARED_LIBS=ON \
      ..

# 编译(用-j4加速)
make -j4

# 安装到指定目录
make install DESTDIR=/opt/opencv-arm

我的习惯:编译时加上 -DWITH_V4L=ON 和 -DWITH_LIBV4L=ON,这样OpenCV能直接通过V4L2接口读取摄像头,省去自己写驱动的麻烦。

2.2.3 移植到板子

编译完成后,把 /opt/opencv-arm 目录下的 lib 和 include 拷到板子上:

# 在PC上打包
tar -czvf opencv-arm.tar.gz -C /opt/opencv-arm .

# 传到板子(假设板子IP是192.168.1.100)
scp opencv-arm.tar.gz root@192.168.1.100:/root/

# 在板子上解压
tar -xzvf opencv-arm.tar.gz -C /usr/local/

然后配置库路径:

echo "/usr/local/lib" > /etc/ld.so.conf.d/opencv.conf
ldconfig

2.3 Linux V4L2框架:摄像头驱动的灵魂

V4L2(Video for Linux 2)是Linux下视频设备的标准接口。说白了,它就是操作系统和摄像头之间的翻译官。你调用 open()、ioctl()、mmap(),V4L2帮你搞定硬件。

2.3.1 V4L2核心架构

我画了一张图,帮你理解V4L2的工作流程:

V4L2 框架核心流程 用户空间应用程序(OpenCV / FFmpeg / 自定义程序) 系统调用接口(open / ioctl / mmap / poll) V4L2 核心框架 设备节点 / 视频缓冲队列 / 格式协商 / 控制参数 摄像头驱动(Sensor 驱动 / ISP 驱动 / 桥接芯片驱动) 摄像头硬件(Sensor / MIPI / USB) 图:V4L2 框架分层结构

2.3.2 关键数据结构

V4L2的核心数据结构就那么几个,记住它们,你就掌握了80%:

结构体 作用 常用字段
struct v4l2_capability 查询设备能力 driver, card, bus_info, capabilities
struct v4l2_format 设置/获取图像格式 type, fmt.pix.width, fmt.pix.height, fmt.pix.pixelformat
struct v4l2_buffer 管理视频缓冲 index, bytesused, length, memory, timestamp
struct v4l2_requestbuffers 申请缓冲数量 count, type, memory

2.3.3 标准操作流程

我总结了一个五步法,每次调摄像头都按这个来:

  1. 打开设备:fd = open("/dev/video0", O_RDWR)
  2. 查询能力:ioctl(fd, VIDIOC_QUERYCAP, &cap),确认设备支持视频捕获
  3. 设置格式:ioctl(fd, VIDIOC_S_FMT, &fmt),指定分辨率、像素格式
  4. 申请缓冲:ioctl(fd, VIDIOC_REQBUFS, &req),然后 mmap 映射到用户空间
  5. 启动流:ioctl(fd, VIDIOC_STREAMON, &type),然后循环 VIDIOC_DQBUF / VIDIOC_QBUF

避坑指南:我曾经在 VIDIOC_S_FMT 之后没有检查实际设置的格式。有些驱动会悄悄修改你传的参数(比如把YUYV改成MJPEG)。一定要用 VIDIOC_G_FMT 读回来确认。

2.3.4 一个完整的V4L2采集示例

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <sys/mman.h>

int main() {
    int fd = open("/dev/video0", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    // 1. 查询能力
    struct v4l2_capability cap;
    ioctl(fd, VIDIOC_QUERYCAP, &cap);
    printf("Driver: %s\n", cap.driver);

    // 2. 设置格式:640x480, YUYV
    struct v4l2_format fmt = {0};
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = 640;
    fmt.fmt.pix.height = 480;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
    ioctl(fd, VIDIOC_S_FMT, &fmt);

    // 3. 申请3个缓冲
    struct v4l2_requestbuffers req = {0};
    req.count = 3;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    ioctl(fd, VIDIOC_REQBUFS, &req);

    // 4. 映射缓冲
    struct v4l2_buffer buf = {0};
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;
    ioctl(fd, VIDIOC_QUERYBUF, &buf);
    void *buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
                        MAP_SHARED, fd, buf.m.offset);

    // 5. 入队并启动流
    ioctl(fd, VIDIOC_QBUF, &buf);
    int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    ioctl(fd, VIDIOC_STREAMON, &type);

    // 6. 采集一帧
    ioctl(fd, VIDIOC_DQBUF, &buf);
    printf("Captured %d bytes\n", buf.bytesused);
    // 处理 buffer 中的数据...

    // 7. 清理
    ioctl(fd, VIDIOC_STREAMOFF, &type);
    close(fd);
    return 0;
}

我的建议:实际项目中,缓冲数量至少设3个。一个被驱动填充,一个被应用处理,一个在队列中等待。这样能避免丢帧。我见过有人只设2个缓冲,结果在高帧率下频繁丢帧。

2.4 本章小结

环境搭建是摄像头开发的基石。工具链让你能编译ARM代码,OpenCV提供了图像处理能力,V4L2则是和硬件对话的桥梁。这三样东西,缺一不可。