ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OV9650摄像头驱动与V4L2测试程序:打通S3C2410/2440/A8平台

OV9650摄像头驱动与V4L2测试程序:打通S3C2410/2440/A8平台

简介:这份资源是面向嵌入式Linux开发者的OV9650摄像头驱动移植与测试工程,针对S3C2410、S3C2440、S5PV210(A8)等ARM平台,解决摄像头驱动适配与图像采集验证的问题。驱动完全兼容V4L2架构,基本不涉及ARM摄像头控制寄存器的直接操作,在tiny210开发板上可完美运行,其他厂商的210板卡只要硬件连接方式相同也能直接使用;对于2410、6410等平台或内核版本不同的环境,稍作修改即可迁移,适合有一定Linux驱动基础、正在做项目或课程设计的开发者参考。压缩包共288个文件,约2.29MB,核心为ov965x.c、mach-mini210.c等C源码与头文件、Kconfig和Makefile构建脚本,另含测试程序所需的工程文件及若干说明文档,测试程序依赖OpenCV与Qt4.7。目前已有490人学习,可帮助读者快速获得可运行的驱动框架、板级适配思路与采集测试方案,省去从零调试的时间。

1. 一块老摄像头模组,为什么值得单独写一份驱动

手上有一块 OV9650 摄像头模组,接上 S3C2410、S3C2440 或者 A8 这类老平台,第一反应往往是去找现成驱动,结果翻遍内核源码树只找到零星几个 sensor 的注册代码,寄存器初始化表还得自己补。这份资源就是干这件事的:一份自己编写的 OV9650 摄像头驱动,配一个能直接跑起来的测试程序,覆盖 2410、2440、A8 三类处理器平台。它适合两类人——手里有老开发板、想把摄像头链路打通的人,以及想搞清楚 V4L2 驱动从 sensor 上电到出图完整链路的人。OV9650 摄像头驱动这类代码网上零散片段多、能编译能出图的少,这份资源的价值在于驱动和测试程序是配套的,不用自己猜上层怎么调。

2. 驱动架构拆解:从 I2C 配置到 V4L2 出图的完整链路

OV9650 是一颗 130 万像素的 CMOS sensor,输出 RGB565/YUV 格式,通过 SCCB(本质是 I2C 时序)配置寄存器,通过并行接口把像素数据送到主控的摄像头控制器。这份驱动的结构基本遵循 Linux 摄像头驱动的经典分层:sensor 层负责上电时序和寄存器配置,host 层负责 DMA 搬运和中断,V4L2 层负责把 buffer 暴露给用户态。下面按这个链路拆。

2.1 目录结构与模块划分

拿到源码先看目录,这类驱动的文件组织通常是这样:

ov9650_driver/ ├── ov9650.c # sensor 驱动主体:probe、寄存器表、上电时序 ├── ov9650.h # 寄存器地址宏、分辨率与格式定义 ├── ov9650_cfg.c # 各分辨率下的寄存器初始化数组 ├── s3c_camif.c # host 控制器:DMA、中断、buffer 管理 ├── s3c_camif.h ├── Makefile └── test/ ├── capture.c # V4L2 测试程序:open/ioctl/mmap/取帧 └── Makefile

这个划分的关键点是 sensor 和 host 解耦。ov9650.c 里不出现任何 S3C 的寄存器地址,s3c_camif.c 里也不关心 OV9650 具体某个寄存器写什么值,两边通过 platform_data 和 v4l2_subdev 的 ops 对接。换平台时理论上只改 host 层,sensor 层能复用——这也是为什么这份代码敢标 2410、2440、A8 三个平台,它们的摄像头控制器寄存器布局不同,但 OV9650 的配置序列是一样的。

Makefile 里一般会区分内核模块编译和测试程序编译两条路径,前者用内核的 kbuild 体系,后者用交叉编译器直接编。看 Makefile 时重点确认ARCH和CROSS_COMPILE两个变量,老平台交叉工具链前缀差异很大,arm-linux- 和 arm-none-linux-gnueabi- 混用是最常见的编译失败原因。

2.2 sensor 层:上电时序与寄存器初始化

OV9650 对时序敏感,上电顺序错了 probe 阶段读到的 chip id 就是 0xFF 或者 0x00。驱动里这段通常写成这样:

static int ov9650_power_on(struct ov9650_priv *priv) { int ret; /* 1. 先给模拟电源,再给 IO 电源,间隔至少 1ms */ regulator_enable(priv->avdd); mdelay(2); regulator_enable(priv->dovdd); mdelay(2); /* 2. 拉低复位,保持至少 1ms 后释放 */ gpio_set_value(priv->rst_gpio, 0); mdelay(2); gpio_set_value(priv->rst_gpio, 1); mdelay(5); /* 3. 提供 MCLK,OV9650 需要 12MHz~27MHz */ clk_prepare_enable(priv->mclk); mdelay(10); /* 4. 读 chip id 验证通信,OV9650 的 PID 寄存器是 0x0A */ ret = ov9650_read_reg(priv, 0x0A); if (ret != 0x96) { dev_err(priv->dev, "chip id mismatch: 0x%02x\n", ret); return -ENODEV; } return 0; }

这段代码的逻辑顺序不能调换:电源稳定之前拉复位没有意义,MCLK 没起来之前 I2C 读写会失败。参数上,mdelay的毫秒数是保守值,实际 datasheet 要求的最小间隔更短,但老平台上电源爬升慢,多等几毫秒比偶发 probe 失败划算。chip id 读 0x0A 寄存器返回 0x96 是 OV9650 的标识,读不到就说明要么 I2C 地址错了(OV9650 是 0x30/0x60 两种写法,取决于驱动里是否左移一位),要么上电时序没走对。

寄存器初始化表在 ov9650_cfg.c 里,按分辨率分数组,比如ov9650_vga[]、ov9650_svga[]。每个数组是一串{reg, val}对,最后以{0xFF, 0xFF}之类的哨兵结尾。写的时候用循环下发:

static int ov9650_write_array(struct ov9650_priv *priv, const struct regval *regs) { int i, ret; for (i = 0; regs[i].reg != 0xFF; i++) { ret = ov9650_write_reg(priv, regs[i].reg, regs[i].val); if (ret < 0) return ret; } return 0; }

这里有个容易忽略的点:OV9650 部分寄存器写入后需要延时才能生效,尤其是 PLL 和时钟分频相关的寄存器。如果初始化表里没有内嵌延时,出图会花屏或者偏色。常见做法是在表里插入特殊标记,比如{0xFE, 0x00}表示延时,写函数里判断到就mdelay。这份代码如果没做这个处理,调试时就得手动在关键寄存器后加延时试。

2.3 host 层:DMA 搬运与中断处理

host 层是平台相关的部分,2410/2440 的摄像头控制器和 A8 的差异主要在寄存器偏移和 FIFO 深度。核心逻辑是配置 DMA 目标地址、设置帧大小、使能中断,然后在中断里切换 buffer:

static irqreturn_t s3c_camif_irq(int irq, void *dev_id) { struct s3c_camif *camif = dev_id; struct camif_buffer *buf; /* 帧结束中断:当前 buffer 填满,换下一个 */ if (camif->irq_status & CAMIF_FRAME_END) { buf = list_first_entry(&camif->active, struct camif_buffer, list); list_move_tail(&buf->list, &camif->done); buf = list_first_entry_or_null(&camif->pending, struct camif_buffer, list); if (buf) { list_move_tail(&buf->list, &camif->active); camif_set_dma_addr(camif, buf->dma_addr); } else { /* 没有新 buffer,停掉采集避免覆盖 */ camif_stop_dma(camif); } wake_up(&camif->wait); } return IRQ_HANDLED; }

这段是驱动能不能稳定出图的关键。三个链表 active/pending/done 分别表示正在写的、排队等写的、已经写满待用户态取走的。中断里只做链表搬移和地址切换,不做耗时操作。参数上buf->dma_addr必须是物理地址,用户态 mmap 拿到的虚拟地址在驱动里要通过dma_map_single或者平台特定的转换拿到物理地址,老平台上没有 IOMMU,这一步错了就是 DMA 写到野地址,表现是系统直接挂死或者花屏。

DMA 地址切换的时机也有讲究:必须在帧结束中断里切,不能在帧中间切,否则一帧图像上半部分是旧 buffer、下半部分是新 buffer,出图撕裂。如果调试时看到画面上下错位,先查这里。

2.4 V4L2 接口:用户态看到的那几个 ioctl

驱动对上层暴露的是标准 V4L2 接口,测试程序里用到的 ioctl 序列基本固定:

ioctl作用关键参数
VIDIOC_QUERYCAP查询设备能力确认有 VIDEO_CAPTURE 和 STREAMING
VIDIOC_S_FMT设置分辨率与像素格式width/height/pixelformat
VIDIOC_REQBUFS申请 buffer 数量count 一般 3~4
VIDIOC_QUERYBUF查 buffer 长度与偏移用于 mmap
VIDIOC_QBUFbuffer 入队index
VIDIOC_STREAMON启动采集类型选 VIDEO_CAPTURE
VIDIOC_DQBUF取一帧阻塞等待

驱动侧对应实现v4l2_ioctl_ops,其中vidioc_reqbufs里分配 DMA 内存,vidioc_qbuf把 buffer 挂到 pending 链表,vidioc_dqbuf从 done 链表取。这里常见的实现差异是 buffer 内存用dma_alloc_coherent还是videobuf2框架,老平台内核版本低,很多是自己管理,那就得自己处理 cache 一致性——DMA 写完之后要dma_sync_single_for_cpu,否则用户态读到的是 cache 里的旧数据,表现是画面静止不动或者隔几帧才更新一次。

3. 跑起来:交叉编译、加载模块与测试程序验证

驱动能不能跑通,一半看代码,一半看环境。老平台的坑集中在工具链、内核版本匹配和设备节点权限上,下面按实际操作顺序走一遍。

3.1 编译驱动模块

先确认内核源码树路径和配置文件。驱动编译依赖内核头文件里的include/media、include/linux/videodev2.h等,内核版本不对会直接报结构体成员不存在。

# 进入驱动目录,指定内核源码路径和交叉编译器 make -C /path/to/kernel-source \ M=$PWD \ ARCH=arm \ CROSS_COMPILE=arm-linux-gnueabi- \ modules

参数说明:-C切到内核源码目录执行内核的顶层 Makefile,M=$PWD告诉 kbuild 外部模块在当前目录,ARCH=arm和CROSS_COMPILE必须和编译内核时用的一致,否则生成的目标文件 ABI 不匹配,insmod 时报 "invalid module format"。编译产物是ov9650.ko和s3c_camif.ko(如果 host 层也编成模块)。

如果报No such file or directory: asm/arch/regs-camif.h这类错,说明内核源码树没配好或者头文件路径变了,老内核里asm/arch是符号链接,新一点的版本改成了mach/,需要手动改 include 路径或者建软链接。

3.2 加载模块与确认设备节点

模块加载顺序是先 host 后 sensor,因为 sensor 的 probe 依赖 host 注册的 subdev 适配器:

# 先加载 host 控制器驱动 insmod s3c_camif.ko # 再加载 sensor 驱动,i2c 地址通过模块参数传 insmod ov9650.ko i2c_addr=0x30 # 确认 video 设备节点生成 ls -l /dev/video*

加载后用dmesg | tail -30看 probe 日志,正常应该看到 chip id 匹配、分辨率表加载、video 设备注册三条信息。如果只看到 host 注册没有 sensor,检查 i2c_addr 参数——有些板子上 OV9650 的地址是 0x30,有些是 0x60,取决于硬件上 SIO_C/SIO_D 的上拉和地址引脚接法。设备节点没生成的话,确认内核配置里CONFIG_VIDEO_DEV和CONFIG_VIDEO_V4L2是打开的。

3.3 编译并运行测试程序

测试程序是纯用户态代码,只依赖 V4L2 头文件:

cd test arm-linux-gnueabi-gcc capture.c -o capture -lpthread # 拷到板子上运行,参数是设备节点、分辨率、输出文件名 ./capture /dev/video0 640 480 frame.raw

capture.c 的主循环逻辑是:打开设备、设置格式、申请并 mmap 四个 buffer、全部入队、STREAMON、循环 DQBUF 取帧写文件再 QBUF 放回。取够帧数后 STREAMOFF 并 munmap。输出的是 raw 数据,RGB565 的话可以用 ffmpeg 转成图片验证:

# RGB565 raw 转 png,宽高按实际设置填 ffmpeg -f rawvideo -pix_fmt rgb565 -s 640x480 -i frame.raw frame.png

如果转出来的图是绿的或者全黑,先确认 pixelformat 设置的是 V4L2_PIX_FMT_RGB565 还是 YUYV,OV9650 两种都支持,但驱动里默认输出格式和测试程序请求的格式必须一致,否则驱动会做一次格式转换或者直接拒绝 S_FMT。

4. 避坑与排查:五个让驱动跑不起来的典型问题

4.1 probe 失败,dmesg 里 chip id 读到 0x00 或 0xFF

现象是 insmod 之后没有 video 节点,dmesg 打印 chip id mismatch。原因通常是 I2C 通信根本没建立:要么上电时序里 MCLK 没起来,OV9650 内部逻辑没工作;要么 I2C 地址写错,0x30 和 0x60 混淆;要么板子上 SIO_C/SIO_D 没有上拉电阻,信号浮空。解决顺序是先量 MCLK 有没有波形,再用 i2c-tools 的i2cdetect扫总线看设备在不在,最后对照原理图确认地址引脚。

4.2 出图花屏或颜色错乱

现象是能取到帧但画面是彩色噪点或者颜色整体偏。原因多半是寄存器初始化表里缺少延时,PLL 还没锁定就写了后续寄存器;或者输出格式和测试程序请求的不一致,驱动按 YUYV 配的寄存器但程序按 RGB565 解析。解决方法是先在初始化表的关键位置插mdelay(1)试,再确认 S_FMT 返回的 pixelformat 和预期一致。

4.3 取几帧后卡死或系统挂掉

现象是 capture 程序跑几秒后不再输出,严重时内核 panic。原因是 DMA 地址用了虚拟地址,或者 buffer 释放后 DMA 还在写。检查vidioc_qbuf里地址转换是否走了virt_to_phys或平台 DMA 映射接口,以及 STREAMOFF 之后是否等 DMA 真正停止再释放内存。老平台上没有 IOMMU 兜底,这类错误是直接挂死,没有后悔药。

4.4 画面静止不动,DQBUF 一直返回同一帧内容

现象是程序在跑、帧数在涨,但每帧数据一模一样。原因是 cache 一致性没处理,DMA 写进内存的数据还在 cache 里没刷到用户态可见的位置。解决是在 DQBUF 之前调用dma_sync_single_for_cpu,QBUF 时调用dma_sync_single_for_device。如果驱动用的是dma_alloc_coherent分配的内存则不需要,但很多老代码是kmalloc加手动映射,就必须补这一步。

4.5 换到另一个平台编译报寄存器未定义

现象是在 2440 上编过的代码拿到 A8 平台,报一堆regs-camif.h里的宏找不到。原因是 host 层直接 include 了平台头文件,换平台头文件路径和宏名都变了。解决是把平台相关的寄存器操作抽成一组函数指针或者条件编译,sensor 层完全不动,只替换 host 层的实现文件。这也是这份代码标三个平台时最需要验证的地方——sensor 部分应该零修改,host 部分按平台选编译。

5. 二次开发:换 sensor、加格式、提帧率的改造路径

拿到这份代码之后,最常见的改造需求有三个:换一颗别的 sensor、增加一种输出格式、把帧率提上去。三个方向的改动位置完全不同,分开说。

换 sensor 是最省事的,因为分层做对了的话只动 sensor 层。新建一个xxx.c,照着 ov9650.c 实现v4l2_subdev_ops里的s_power、s_stream、get_fmt、set_fmt几个回调,寄存器表换成新 sensor 的初始化序列,chip id 校验改掉,host 层一行不用动。验证方法是先只加载 host 和新 sensor 模块,用v4l2-ctl --list-formats看格式枚举对不对,再跑 capture 取一帧。这里有个技巧:新 sensor 的初始化表不要一次全抄 datasheet,先只配最小集合(时钟、输出格式、分辨率),能出图之后再逐段加自动曝光、白平衡这些,出问题容易定位是哪一段配置引起的。

加输出格式要同时改两层。sensor 层在set_fmt里根据请求的 pixelformat 选不同的寄存器表,host 层在 DMA 配置里改每像素字节数和帧大小。比如从 RGB565 加一路 YUYV,帧大小从w*h*2变成w*h*2(YUYV 也是两字节),但 DMA 的 burst 长度和 FIFO 阈值可能要调。改完用v4l2-ctl --set-fmt-video=width=640,height=480,pixelformat=YUYV配合 capture 验证,重点看颜色对不对——YUYV 的 Y 和 UV 顺序搞反了画面会偏绿或偏紫。

提帧率是最麻烦的,瓶颈通常在三个地方:sensor 的 PLL 配置、host 的 DMA 带宽、buffer 数量。先确认 sensor 寄存器表里帧率相关的分频值,OV9650 的 CLKRC 和 COM 系列寄存器决定 PCLK 和帧时序;再看 host 的 DMA 是否用了 burst 模式,单次传输长度够不够;最后把 buffer 数量从 3 加到 4 或 5,减少 DQBUF 等待时的丢帧。我一般的做法是先不改代码,用v4l2-ctl --stream-mmap --stream-count=100测当前实际帧率,确认瓶颈在 sensor 还是 host 再动手,避免改了一圈发现是测试程序自己处理慢。

改造方向改动文件验证命令易错点
换 sensor新增 sensor .c + 改 Makefilev4l2-ctl --list-formatschip id 和上电时序
加格式sensor set_fmt + host DMA 配置v4l2-ctl --set-fmt-videoYUV 分量顺序
提帧率sensor PLL 表 + buffer 数量v4l2-ctl --stream-count先测瓶颈再改

最后说个习惯:每次改完 sensor 寄存器表,我都会先用最小分辨率(比如 160x120)跑一遍 capture,确认链路通了再上目标分辨率。小分辨率下 DMA 压力小、出问题容易复现,等大分辨率花屏了再回头查,变量太多根本分不清是时序问题还是带宽问题。这套流程走下来,换 sensor 和调格式基本能在一两天内出图,比对着 datasheet 盲调快得多。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表