
简介这是一份面向嵌入式Linux开发者的OV9650摄像头驱动移植与测试资源适合正在学习V4L2摄像头驱动架构、需要在ARM平台上完成图像采集项目的工程师与学生。驱动完全兼容V4L2框架可完美支持tiny210开发板对2410、2440、6410等平台及不同版本内核稍作修改即可复用基本不涉及ARM摄像头控制寄存器的底层操作硬件连接方式相同的其他厂家210开发板同样适用。压缩包共288个文件约2.29MB包含C与H驱动源码、Kconfig与Makefile构建文件、PDF说明文档以及测试程序所需的JS、CSS、HTML等前端与配置资源。测试程序依赖OpenCV与Qt4.7环境。该资源已有485人学习作者基于项目实践耗时两周完成移植并开源读者可从中获得完整的驱动代码结构、V4L2注册与采集流程参考以及可直接编译验证的测试工程便于快速搭建摄像头采集环境并理解驱动与应用的对接方式。1. 手里有块 OV9650 却点不亮聊聊这份能在 2410/2440 和 A8 上跑的驱动翻出几块吃灰的 S3C2410、S3C2440 开发板摄像头接口上插着 OV9650上电后/dev下什么都没有或者有节点但一读就花屏、绿屏、卡死——这大概是很多做嵌入式图像采集的老哥都遇到过的场景。这份自己编写的 OV9650 摄像头驱动加测试程序就是冲着这个场景来的它把 SCCB 寄存器配置、CMM 摄像头控制器初始化、DMA 搬运和帧缓冲注册串成一条线目标平台覆盖 2410、2440 以及 A8 系列处理器。适合谁适合手里有这些老平台、需要快速验证摄像头通路、又不想从零啃数据手册的嵌入式工程师。它解决的不是“拍出多好看的照片”而是“先把图像流稳定地采上来”。2. 先搞懂 OV9650 在 2410/2440 上的数据通路SCCB、CMM 与 DMA 怎么串OV9650 是一颗 130 万像素的 CMOS 图像传感器输出格式支持 RAW、RGB565、YUV422 等通过 SCCB 总线配置内部寄存器通过并行数据线输出像素。在 2410/2440 这类 ARM9 平台上摄像头数据不是直接进内存的中间要经过芯片内部的 Camera InterfaceCMM 模块。理解这条通路是后面改驱动、调参数的前提。2.1 SCCB 配置通道为什么它像 I2C 但又不是 I2COV9650 的寄存器配置走 SCCB时序和 I2C 非常接近很多驱动直接复用 I2C 控制器来发。但要注意SCCB 在应答位和停止位上和标准 I2C 有细微差别有些平台用硬件 I2C 发会偶发失败常见做法是用 GPIO 模拟 SCCB 时序稳定可控。配置流程一般是先读产品 ID 确认通信正常再写时钟分频、输出格式、分辨率、曝光、增益等寄存器。下面是一段典型的 SCCB 写寄存器逻辑用 GPIO 模拟/* GPIO 模拟 SCCB 写一个字节sda/scl 为对应引脚 */ static void sccb_write_byte(unsigned char data) { int i; for (i 0; i 8; i) { sccb_scl_low(); if (data 0x80) sccb_sda_high(); else sccb_sda_low(); data 1; sccb_delay(); sccb_scl_high(); /* 上升沿采样 */ sccb_delay(); } sccb_scl_low(); sccb_sda_high(); /* 释放 SDA准备读应答 */ sccb_delay(); sccb_scl_high(); sccb_delay(); /* 此处可读 SDA 判断从机应答 */ sccb_scl_low(); }逻辑说明每个 bit 在 SCL 低电平期间准备 SDA高电平期间保持稳定接收方在上升沿采样。参数上sccb_delay()的延时长度决定了总线速率太快会导致 OV9650 来不及应答一般调到几十微秒级别比较稳。写寄存器时先发从机地址OV9650 通常为 0x60 写、0x61 读再发寄存器地址再发数据。2.2 CMM 控制器初始化2410/2440 的关键寄存器S3C2410/2440 的 Camera Interface 有一组寄存器需要按顺序配置包括CIGCTRL全局控制、CIPRCLRSA1~4DMA 目标地址、CIPRTRGFMT目标格式、CIPRCTRL采集控制、CIPRSCCTRL源格式和缩放等。配置顺序错了常见现象是 DMA 不启动或者采到全黑。典型初始化步骤关闭摄像头接口清中断挂起位设置 DMA 目标地址为帧缓冲物理地址设置目标格式为 RGB565源格式与 OV9650 输出一致设置缩放参数如果不需要缩放就设成 1:1使能采集等待帧结束中断。/* 2440 CMM 初始化片段base 为 CMM 寄存器基址 */ writel(0, base CIGCTRL); /* 先关闭 */ writel(fb_phys, base CIPRCLRSA1); /* 帧缓冲物理地址 */ writel((height 16) | width, base CIPRTRGFMT); /* 目标宽高 */ writel(0x1, base CIPRCTRL); /* 使能采集 */参数说明fb_phys必须是物理地址且要和 MMU 映射一致否则 DMA 搬过去的是错位数据。CIPRTRGFMT里的宽高要和 OV9650 实际输出分辨率匹配比如 QVGA 是 320x240VGA 是 640x480。如果这里填错图像会拉伸或截断。2.3 DMA 与帧缓冲图像数据怎么落到内存CMM 采集到的数据通过 DMA 写入你指定的帧缓冲。2410/2440 支持多帧缓冲切换但这份驱动里通常用单缓冲加中断通知的方式简单直接。帧结束中断触发后应用程序就可以读取这块内存。要注意缓存一致性问题如果开了 D-CacheDMA 写入的内存区域对 CPU 可能不可见常见做法是把帧缓冲区域设为非缓存或者在读取前 invalidate 对应 cache 行。/* 帧结束中断处理里 invalidate cache保证 CPU 读到 DMA 新数据 */ dma_inv_range(fb_virt, fb_virt fb_size);这段代码的作用是让 CPU 侧看到的帧缓冲内容和 DMA 写入的一致。参数fb_virt是帧缓冲的虚拟地址fb_size是一帧的字节数比如 RGB565 的 QVGA 一帧是 3202402 153600 字节。3. 把驱动挂进内核并跑通测试程序从编译到出图驱动源码拿到手下一步是让它跑起来。这一章按实际操作顺序走改 Makefile、编译、加载、跑测试程序、看输出。中间会说明每个环节的参数怎么对应到你的板子。3.1 编译驱动Makefile 里要改哪几个变量这份驱动通常以模块形式提供编译前要确认内核源码路径和交叉编译器。Makefile 里关键变量是KERNELDIR和CROSS_COMPILE。如果你的内核是 2.6.32 或更早的版本注意struct file_operations里.ioctl的签名差异新内核用unlocked_ioctl老内核用ioctl编译报错时先看这里。KERNELDIR : /home/you/linux-2.6.32 CROSS_COMPILE : arm-linux- obj-m : ov9650.o逻辑说明obj-m表示编译成可加载模块ov9650.o对应驱动源文件。编译命令是make -C $(KERNELDIR) M$(PWD) modules。如果报“找不到内核头文件”检查KERNELDIR是否指向已经编译过的内核源码目录而不是只有头文件的目录。3.2 加载模块与设备节点insmod 之后看什么编译出ov9650.ko后通过 NFS 或串口传到板子上执行insmod ov9650.ko。加载成功后驱动一般会注册字符设备你需要用mknod创建设备节点或者驱动里已经通过class_create自动创建了/dev/ov9650。insmod ov9650.ko ls /dev/ov9650 # 确认节点存在 dmesg | tail -20 # 看驱动打印的初始化信息dmesg里应该能看到类似“OV9650 ID read success”或“camera init done”的输出。如果看到“SCCB read id failed”说明 SCCB 通信没通先查引脚复用和上拉电阻。如果看到“DMA buffer alloc failed”说明帧缓冲分配失败检查内存是否够或者分配标志是否正确。3.3 测试程序怎么用采集一帧并保存成文件测试程序通常是一个简单的用户态程序打开设备节点通过read或ioctl获取一帧数据然后写成文件。下面是一个最小示例int fd open(/dev/ov9650, O_RDWR); unsigned char *buf malloc(320 * 240 * 2); read(fd, buf, 320 * 240 * 2); /* 阻塞直到一帧采集完成 */ FILE *fp fopen(frame.raw, wb); fwrite(buf, 1, 320 * 240 * 2, fp); fclose(fp); close(fd);逻辑说明read会阻塞到帧结束中断触发驱动把帧缓冲数据拷到用户空间。参数320*240*2对应 QVGA RGB565 一帧大小。保存的frame.raw可以用工具转成 BMP 查看比如用 Python 的 PIL 或 ImageMagick 指定宽高和格式。如果图像颜色不对先确认 RGB565 的字节序有些平台需要交换高低字节。4. 避坑与排查OV9650 驱动最容易翻车的五个地方这一章记录的是实际调试中最常遇到的五个问题每个都按“现象 → 原因 → 解决”写。这些坑不挑平台2410、2440、A8 上都可能碰到。4.1 现象SCCB 读 ID 一直失败dmesg 报 0x00 或 0xFF原因SCCB 引脚复用没配对或者上拉电阻缺失。OV9650 的 SDA/SCL 需要 4.7K 左右上拉到 2.8V 或 3.3V有些板子没焊。另外2410/2440 的摄像头引脚和 LCD 引脚可能复用如果 LCD 驱动先占用了SCCB 就发不出去。解决先量 SDA/SCL 静态电平正常应该是高。如果为低查上拉和复用配置。确认GPH或GPC等控制寄存器里对应位设成了 SCCB 功能而不是 GPIO 输入。如果复用被 LCD 占用需要在 LCD 驱动里释放或者改引脚。4.2 现象能读到 ID但采集出来全黑或全绿原因CMM 的源格式和目标格式不匹配。OV9650 上电默认输出可能是 RAW 或 YUV而 CMM 配成了 RGB565导致解析错位。全绿通常是 YUV 数据被当成 RGB 解析。解决先通过 SCCB 把 OV9650 的COM7等寄存器配成 RGB565 输出再确认 CMM 的CIPRSCCTRL里源格式也设成 RGB565。两边一致后再看图像。如果还是黑检查曝光寄存器室内光线暗时默认曝光可能不够。4.3 现象图像有但撕裂、错位或者只显示一部分原因DMA 目标地址没对齐或者帧缓冲大小和分辨率不匹配。2410/2440 的 CMM DMA 对地址有对齐要求通常 4 字节或 8 字节对齐。另外如果CIPRTRGFMT里的宽高和实际输出不一致DMA 会按错误长度搬运。解决确保帧缓冲物理地址按 4 字节对齐malloc出来的内存不一定对齐最好用memalign或驱动里用dma_alloc_coherent。核对 OV9650 输出分辨率和 CMM 目标分辨率QVGA 就填 320x240VGA 就填 640x480不要混。4.4 现象读一帧后程序卡死或者第二次 read 返回旧数据原因中断处理里没有清中断标志导致中断只触发一次。或者帧缓冲没有双缓冲机制第二次 read 时 DMA 还没写完就被读走。解决在帧结束中断处理里写CIPRSTATUS清对应位。如果驱动是单缓冲read 要等中断完成再拷贝如果应用层读得太快可以在驱动里加等待队列确保一帧完整后再唤醒。常见做法是用两个帧缓冲交替一个在采集时另一个可读。4.5 现象加载模块后系统变慢或死机原因帧缓冲分配太大或者 cache 操作范围越界。有些驱动在中断里做dma_inv_range如果范围算错会 invalidate 到内核代码区直接死机。解决检查fb_size计算是否正确RGB565 一帧字节数是宽×高×2。dma_inv_range的起止地址要落在帧缓冲范围内不要多算一页。如果内存紧张把分辨率降到 QVGA 甚至 QQVGA 先跑通再往上加。5. 进阶技巧用双缓冲和 mmap 把采集帧率提上去跑通单帧之后下一步通常是提高采集效率。单缓冲加 read 拷贝的方式每帧都要从内核拷到用户空间CPU 占用高帧率上不去。这里说两个我常用的改进驱动侧双缓冲加用户态 mmap。5.1 驱动侧双缓冲让 DMA 和读取互不打架思路是分配两块帧缓冲DMA 当前写 A 时应用可以读 B帧结束中断里切换 DMA 目标地址并唤醒等待的读操作。这样采集和读取并行不会互相阻塞。/* 中断里切换 DMA 目标地址 */ static irqreturn_t cam_isr(int irq, void *dev_id) { struct ov9650_dev *dev dev_id; writel(0, dev-base CIGCTRL); /* 暂停采集 */ dev-cur_buf ^ 1; /* 切换缓冲 */ writel(dev-fb_phys[dev-cur_buf], dev-base CIPRCLRSA1); writel(0x1, dev-base CIGCTRL); /* 重新使能 */ wake_up_interruptible(dev-wq); /* 唤醒读等待 */ writel(0x1, dev-base CIPRSTATUS); /* 清中断 */ return IRQ_HANDLED; }参数说明dev-fb_phys[2]是两块帧缓冲的物理地址cur_buf在 0 和 1 之间翻转。注意暂停和重新使能采集之间要有短暂延时否则可能丢帧。wake_up_interruptible唤醒在 read 里等待的进程read 根据当前可读的缓冲索引拷贝数据。5.2 用户态 mmap省掉一次内存拷贝如果不想在 read 里做拷贝可以在驱动里实现mmap把帧缓冲直接映射到用户空间。应用层拿到指针后直接读省掉内核到用户空间的拷贝开销。代价是要处理好同步确保读的时候 DMA 没在写同一块。/* 用户态 mmap 示例 */ int fd open(/dev/ov9650, O_RDWR); unsigned char *fb mmap(NULL, 320*240*2*2, PROT_READ, MAP_SHARED, fd, 0); /* fb 指向两块帧缓冲根据驱动提供的索引读取当前帧 */逻辑说明mmap的长度是两块缓冲的总大小偏移 0 表示从第一块开始。驱动里mmap实现要把物理地址对应的页映射到用户空间通常用remap_pfn_range。参数上PROT_READ表示只读映射MAP_SHARED保证驱动更新对用户可见。读的时候要配合驱动的 ioctl 获取当前可读缓冲索引避免读到正在被 DMA 写的块。5.3 验证帧率用时间戳算实际 FPS改完之后怎么确认有效在应用层加时间戳连续采集 100 帧算平均帧间隔。struct timeval tv1, tv2; gettimeofday(tv1, NULL); for (i 0; i 100; i) { ioctl(fd, OV9650_GET_FRAME, idx); /* 等待一帧 */ /* 处理 fb[idx] */ } gettimeofday(tv2, NULL); double fps 100.0 / ((tv2.tv_sec - tv1.tv_sec) (tv2.tv_usec - tv1.tv_usec)/1000000.0); printf(fps %.2f\n, fps);参数说明OV9650_GET_FRAME是自定义 ioctl阻塞到新帧可用并返回缓冲索引。算出来的 FPS 受 OV9650 本身输出帧率限制QVGA RGB565 在 2410/2440 上一般能到 15~30 帧具体看主频和总线带宽。如果明显偏低先查 SCCB 配置里的时钟分频和 PLL 倍频再查 DMA 是否频繁暂停。从那以后我每次调摄像头驱动都强制先量 SCCB 引脚电平、再读 ID、再单帧、最后才上双缓冲顺序不乱跳。希望帮到你。本文还有配套的精品资源点击获取