1. 项目概述
在嵌入式Linux开发中,摄像头驱动移植是一个常见但颇具挑战性的任务。本文将详细介绍如何在正点原子IMX6ULL开发板上移植OV5640摄像头模块的全过程。这个项目基于NXP官方提供的Linux内核源码进行修改,而非直接使用正点原子出厂自带的源码,这为我们提供了更深入理解底层驱动机制的机会。
OV5640是一款500万像素的CMOS图像传感器,支持多种输出格式和分辨率,在嵌入式视觉应用中非常流行。IMX6ULL则是NXP推出的一款高性能、低功耗的ARM Cortex-A7处理器,广泛应用于工业控制、智能家居等领域。将这两者结合,可以构建一个功能完善的嵌入式视觉系统。
2. 硬件准备与环境搭建
2.1 所需硬件清单
- 正点原子IMX6ULL开发板(EMMC版本)
- 正点原子OV5640摄像头模块
- 正点原子1024×600 RGB LCD屏幕
- 配套的电源适配器和数据线
- 调试串口线(USB转TTL)
2.2 开发环境配置
在开始移植前,需要确保主机开发环境已正确配置:
- 安装交叉编译工具链:
bash复制sudo apt-get install gcc-arm-linux-gnueabihf
-
准备Linux内核源码:
建议使用NXP官方提供的Linux 4.1.15内核源码,这是IMX6ULL最稳定的内核版本之一。 -
配置TFTP和NFS服务:
这将极大方便后续的内核镜像和设备树文件的传输与测试。
提示:建议使用Ubuntu 18.04 LTS作为开发主机系统,这个版本与IMX6ULL的工具链兼容性最佳。
3. 设备树文件修改详解
3.1 CSI接口配置
CSI(CMOS Sensor Interface)是IMX6ULL与摄像头连接的核心接口。在设备树中需要确保CSI节点正确配置:
dts复制&csi {
status = "okay";
port {
csi1_ep: endpoint {
remote-endpoint = <&ov5640_ep>;
};
};
};
这个配置启用了CSI接口,并定义了与OV5640的连接端点。status = "okay"表示启用该接口,remote-endpoint指定了与摄像头模块的连接关系。
3.2 OV5640节点配置
OV5640通过I2C接口进行控制,通过CSI接口传输图像数据。以下是完整的OV5640节点配置:
dts复制ov5640: ov5640@3c {
compatible = "ovti,ov5640";
reg = <0x3c>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_csi1
&csi_pwn_rst>;
clocks = <&clks IMX6UL_CLK_CSI>;
clock-names = "csi_mclk";
pwn-gpios = <&gpio1 4 1>;
rst-gpios = <&gpio1 2 0>;
csi_id = <0>;
mclk = <24000000>;
mclk_source = <0>;
status = "okay";
port {
ov5640_ep: endpoint {
remote-endpoint = <&csi1_ep>;
};
};
};
关键参数说明:
compatible: 驱动匹配字符串,必须与驱动代码中的定义一致reg: I2C设备地址,OV5640的默认地址是0x3cpwn-gpios和rst-gpios: 分别控制摄像头的电源和复位引脚mclk: 摄像头主时钟频率,24MHz是OV5640的标准工作频率
3.3 引脚复用配置
IMX6ULL的引脚功能需要通过IOMUXC控制器进行配置。以下是CSI接口和电源控制引脚的配置:
dts复制pinctrl_csi1: csi1grp {
fsl,pins = <
MX6UL_PAD_CSI_MCLK__CSI_MCLK 0x1b008
MX6UL_PAD_CSI_PIXCLK__CSI_PIXCLK 0x1b008
MX6UL_PAD_CSI_VSYNC__CSI_VSYNC 0x1b008
MX6UL_PAD_CSI_HSYNC__CSI_HSYNC 0x1b008
MX6UL_PAD_CSI_DATA00__CSI_DATA02 0x1b008
MX6UL_PAD_CSI_DATA01__CSI_DATA03 0x1b008
MX6UL_PAD_CSI_DATA02__CSI_DATA04 0x1b008
MX6UL_PAD_CSI_DATA03__CSI_DATA05 0x1b008
MX6UL_PAD_CSI_DATA04__CSI_DATA06 0x1b008
MX6UL_PAD_CSI_DATA05__CSI_DATA07 0x1b008
MX6UL_PAD_CSI_DATA06__CSI_DATA08 0x1b008
MX6UL_PAD_CSI_DATA07__CSI_DATA09 0x1b008
>;
};
csi_pwn_rst: csi_pwn_rstgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0x10b0
MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x10b0
>;
};
引脚配置中的十六进制数(如0x1b008)是引脚电气特性配置,包括驱动强度、上下拉等参数。这些值需要参考IMX6ULL的数据手册进行设置。
3.4 设备树编译与更新
完成修改后,需要编译设备树并更新到开发板:
bash复制make dtbs
cp ./arch/arm/boot/dts/imx6ull-beam-emmc.dtb /home/beam/linux/tftpboot/ -f
注意:设备树文件名可能因开发板型号不同而有所差异,请根据实际情况调整。
4. OV5640驱动移植
4.1 驱动源码获取
NXP官方提供的OV5640驱动可能无法完全兼容正点原子的摄像头模块,因此我们需要使用正点原子修改过的驱动代码。这些代码通常位于:
code复制正点原子提供的Linux源码/drivers/media/platform/mxc/
需要将整个mxc目录替换到我们的内核源码中对应位置。这个目录不仅包含OV5640驱动,还包括其他相关的V4L2平台驱动和Makefile/Kconfig配置。
4.2 驱动配置与编译
通过menuconfig配置内核,启用OV5640驱动:
bash复制make menuconfig
导航路径:
code复制Device Drivers
→ Multimedia support
→ V4L platform devices
确保以下选项被启用:
- MX6UL/ULL Camera Sensor Interface (CSI)
- Omnivision OV5640 camera support
重要提示:IMX6ULL不支持MIPI接口,因此不要启用任何MIPI相关的驱动选项,否则会导致编译错误。
4.3 内核编译与更新
配置完成后,编译内核并更新:
bash复制make -j16
cp ./arch/arm/boot/zImage /home/beam/linux/tftpboot/ -f
编译成功后,启动开发板时应该能在串口日志中看到类似以下信息,表明OV5640驱动已成功加载:
code复制ov5640 1-003c: ov5640_read_reg: error: reg=300a
ov5640 1-003c: ov5640_read_reg: error: reg=300a
ov5640 1-003c: ov5640_read_reg: error: reg=300a
ov5640 1-003c: ov5640: Camera ID=0x5640
这些日志表明驱动已成功检测到OV5640摄像头,ID为0x5640。
5. 应用程序开发与测试
5.1 V4L2框架概述
Video4Linux2(V4L2)是Linux内核中视频设备的通用框架。我们的应用程序将通过V4L2接口与OV5640驱动交互,主要流程包括:
- 打开视频设备
- 查询设备能力
- 设置视频格式
- 申请帧缓冲区
- 开始视频采集
- 循环读取帧数据
- 停止采集并释放资源
5.2 关键代码解析
5.2.1 像素格式转换
由于OV5640输出RGB565格式,而LCD需要ARGB8888格式,我们需要进行转换:
c复制static inline unsigned int rgb565_to_argb8888(unsigned short rgb565)
{
unsigned int r, g, b;
r = (rgb565 >> 11) & 0x1F; // 5bit
g = (rgb565 >> 5) & 0x3F; // 6bit
b = rgb565 & 0x1F; // 5bit
// 扩展到8bit
r = (r << 3) | (r >> 2);
g = (g << 2) | (g >> 4);
b = (b << 3) | (b >> 2);
return (0xFF << 24) | (r << 16) | (g << 8) | b; // ARGB8888
}
这个函数将16位的RGB565像素转换为32位的ARGB8888格式。注意这种转换会消耗一定的CPU资源,可能影响帧率。
5.2.2 视频格式设置
设置视频采集格式的关键代码:
c复制static int v4l2_set_format(void)
{
struct v4l2_format fmt = {0};
struct v4l2_streamparm streamparm = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = width; // LCD宽度
fmt.fmt.pix.height = height;// LCD高度
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565; //像素格式
if (0 > ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt)) {
fprintf(stderr, "ioctl error: VIDIOC_S_FMT: %s\n", strerror(errno));
return -1;
}
// 检查是否设置成功
if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) {
fprintf(stderr, "Error: the device does not support RGB565 format!\n");
return -1;
}
frm_width = fmt.fmt.pix.width; //获取实际的帧宽度
frm_height = fmt.fmt.pix.height;//获取实际的帧高度
printf("视频帧大小<%d * %d>\n", frm_width, frm_height);
// 设置帧率
streamparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_G_PARM, &streamparm);
if (V4L2_CAP_TIMEPERFRAME & streamparm.parm.capture.capability) {
streamparm.parm.capture.timeperframe.numerator = 1;
streamparm.parm.capture.timeperframe.denominator = 30;//30fps
if (0 > ioctl(v4l2_fd, VIDIOC_S_PARM, &streamparm)) {
fprintf(stderr, "ioctl error: VIDIOC_S_PARM: %s\n", strerror(errno));
return -1;
}
}
return 0;
}
5.2.3 帧缓冲区管理
使用内存映射方式管理帧缓冲区:
c复制static int v4l2_init_buffer(void)
{
struct v4l2_requestbuffers reqbuf = {0};
struct v4l2_buffer buf = {0};
// 申请帧缓冲
reqbuf.count = FRAMEBUFFER_COUNT;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(v4l2_fd, VIDIOC_REQBUFS, &reqbuf)) {
fprintf(stderr, "ioctl error: VIDIOC_REQBUFS: %s\n", strerror(errno));
return -1;
}
// 内存映射
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf);
buf_infos[buf.index].length = buf.length;
buf_infos[buf.index].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
v4l2_fd, buf.m.offset);
if (MAP_FAILED == buf_infos[buf.index].start) {
perror("mmap error");
return -1;
}
}
// 缓冲区入队
for (buf.index = 0; buf.index < FRAMEBUFFER_COUNT; buf.index++) {
if (0 > ioctl(v4l2_fd, VIDIOC_QBUF, &buf)) {
fprintf(stderr, "ioctl error: VIDIOC_QBUF: %s\n", strerror(errno));
return -1;
}
}
return 0;
}
5.3 编译与测试
交叉编译应用程序:
bash复制arm-linux-gnueabihf-gcc v4l2_camera.c -o v4l2_camera
将生成的可执行文件拷贝到开发板并运行:
bash复制./v4l2_camera /dev/video0
如果一切正常,应该能在LCD屏幕上看到摄像头采集的实时画面。
6. 常见问题与解决方案
6.1 摄像头无法识别
现象:系统启动后没有OV5640相关的日志输出。
可能原因及解决方案:
-
I2C通信失败
- 检查设备树中的I2C地址是否正确(应为0x3c)
- 使用i2c-tools工具包测试I2C通信是否正常
bash复制
i2cdetect -y 1 -
电源和复位信号问题
- 检查设备树中pwn-gpios和rst-gpios的配置
- 使用万用表测量摄像头模块的电源电压(应为3.3V)
-
时钟信号问题
- 检查设备树中mclk配置(应为24000000)
- 使用示波器测量摄像头MCLK引脚是否有24MHz时钟信号
6.2 图像显示异常
现象:LCD上显示的图像出现颜色错乱、条纹或部分缺失。
可能原因及解决方案:
-
像素格式不匹配
- 确保应用程序中设置的像素格式(V4L2_PIX_FMT_RGB565)与摄像头输出格式一致
- 检查像素格式转换函数是否正确
-
分辨率不匹配
- 确保应用程序中设置的分辨率与摄像头支持的分辨率一致
- 可以在应用程序中打印摄像头支持的所有格式和分辨率进行验证
-
内存对齐问题
- 检查帧缓冲区的内存地址是否对齐
- 尝试调整缓冲区大小或数量
6.3 帧率过低
现象:视频显示卡顿,明显不流畅。
可能原因及解决方案:
-
像素格式转换开销
- RGB565到ARGB8888的转换会消耗大量CPU资源
- 考虑使用硬件加速或直接使用RGB565格式显示(如果LCD支持)
-
分辨率设置过高
- 尝试降低采集分辨率
- OV5640在不同分辨率下支持的帧率不同,参考数据手册选择合适的分辨率
-
系统负载过高
- 使用top命令查看系统资源使用情况
- 优化应用程序,减少不必要的计算
7. 性能优化建议
7.1 使用DMA缓冲区
当前实现使用CPU进行内存拷贝和格式转换,效率较低。可以考虑:
- 使用DMA缓冲区直接传输图像数据
- 配置CSI接口的DMA引擎,减少CPU干预
7.2 硬件加速像素格式转换
IMX6ULL的IPU(Image Processing Unit)可以硬件加速像素格式转换:
- 配置IPU进行RGB565到ARGB8888的转换
- 通过framebuffer直接显示转换后的图像
7.3 多线程处理
将图像采集和显示分离到不同线程:
- 一个线程专门负责从摄像头采集数据
- 另一个线程负责格式转换和显示
- 使用双缓冲或三缓冲机制减少等待时间
7.4 降低分辨率
根据实际需求选择合适的分辨率:
- 对于监控应用,640x480可能已经足够
- 降低分辨率可以显著提高帧率
- 可以通过设备树的ov5640节点配置不同分辨率
8. 扩展功能实现
8.1 添加JPEG编码支持
OV5640支持直接输出JPEG格式,可以减轻CPU负担:
- 修改设备树,配置摄像头输出JPEG格式
- 在应用程序中设置V4L2_PIX_FMT_JPEG格式
- 使用libjpeg库解码并显示
8.2 实现视频录制
基于当前框架,可以轻松扩展视频录制功能:
- 使用FFmpeg库编码视频
- 将采集的帧数据写入视频文件
- 添加开始/停止录制控制
8.3 添加网络传输功能
将视频流通过网络传输:
- 使用RTP/RTSP协议传输视频流
- 实现简单的Web服务器,通过浏览器查看视频
- 考虑使用MJPG-streamer等开源项目
在实际项目中,根据具体需求选择合适的优化和扩展方向。对于简单的监控应用,当前实现已经足够;对于更复杂的应用,可以考虑上述优化建议。