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的工作流程:
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 标准操作流程
我总结了一个五步法,每次调摄像头都按这个来:
- 打开设备:
fd = open("/dev/video0", O_RDWR) - 查询能力:
ioctl(fd, VIDIOC_QUERYCAP, &cap),确认设备支持视频捕获 - 设置格式:
ioctl(fd, VIDIOC_S_FMT, &fmt),指定分辨率、像素格式 - 申请缓冲:
ioctl(fd, VIDIOC_REQBUFS, &req),然后mmap映射到用户空间 - 启动流:
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则是和硬件对话的桥梁。这三样东西,缺一不可。