拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OV9650摄像头驱动开发实战:从SCCB时序到CAMIF采集

OV9650摄像头驱动开发实战:从SCCB时序到CAMIF采集 简介一套OV9650摄像头驱动及测试程序源码面向ARM平台嵌入式开发者基于V4L2架构编写参考他人代码移植而来可运行于Tiny210开发板也适用于2410、6410、A8等平台仅需根据Linux内核版本做少量调整硬件连接方式相同即可直接复用。压缩包共288个文件体积2.29MB主要包含C驱动源码、头文件、Makefile/Kconfig构建配置、测试程序相关脚本另有一批HTML/CSS/JS、图片及PDF/DOCX文档便于查看说明与工程文档目录结构清晰。资源在CSDN已有485人学习属于实际项目产出作者花费两周完成并开源适合学习V4L2驱动移植和摄像头应用调试的开发者参考。测试程序依赖OpenCV与Qt 4.7可直接用于图像采集实验或二次开发。1. 自己写的 OV9650 驱动到底难在哪如果你正在一块 2410 或 2440 开发板上做摄像头采集OV9650 驱动可能不是整块板子上最难的部分但一定是最值得亲手写一遍的东西网上能直接跑的 sensor 补丁往往和本机 CAMIF 版本对不上V4L2 框架在内核 3.x、4.x 之间变化又大A8 平台虽然外设更强但传感器寄存器映射不会因此少一个。自己写驱动加测试程序等于把 SCCB 时序、CAMIF 中断、DMA 缓冲、用户态 mmap 这条链完整过一遍回头再去看任何标准驱动都会快很多。这篇文章适合正在调板子、评估 sensor、或者想拿 Linux 字符设备驱动框架练手的人。2. 动手之前先拆清楚SCCB、CAMIF、输出格式各管一段OV9650 驱动不是“写一个初始化函数”就完事。它至少要拆成三段SCCB 负责读写寄存器CAMIF/ISP 负责把 sensor 的并行数据收进 DDR字符设备驱动则负责把缓冲区送到应用层。三段里任何一段没接对最后都是黑屏或者花屏。2.1 SCCB 不是 I2C但可以用 GPIO 模拟OV9650 的寄存器接口叫 SCCB两根线 SIO_C 和 SIO_D时序和 I2C 很像起始条件是 SIO_C 高电平时 SIO_D 产生下降沿停止条件是 SIO_C 高电平时 SIO_D 产生上升沿。对大多数平台来说直接复用 I2C 控制器也能写寄存器但很多 2410/2440 底板上这两个引脚接在普通 GPIO 上或者和触摸屏、EEPROM 的 I2C 总线共用一旦总线上有设备拉死就要拆设备排查。我一般会先用 GPIO 模拟把时序主动权握在自己手里转速也方便调。#include linux/gpio/consumer.h #include linux/delay.h struct ov9650_dev { struct device *dev; struct gpio_desc *sio_c; struct gpio_desc *sio_d; unsigned int sccb_delay_ns; }; static void sccb_delay(struct ov9650_dev *ov) { ndelay(ov-sccb_delay_ns); } static void sccb_start(struct ov9650_dev *ov) { gpiod_set_value_cansleep(ov-sio_c, 1); gpiod_set_value_cansleep(ov-sio_d, 1); sccb_delay(ov); gpiod_set_value_cansleep(ov-sio_d, 0); sccb_delay(ov); gpiod_set_value_cansleep(ov-sio_c, 0); sccb_delay(ov); } static void sccb_stop(struct ov9650_dev *ov) { gpiod_set_value_cansleep(ov-sio_d, 0); sccb_delay(ov); gpiod_set_value_cansleep(ov-sio_c, 1); sccb_delay(ov); gpiod_set_value_cansleep(ov-sio_d, 1); sccb_delay(ov); } static int sccb_write_byte(struct ov9650_dev *ov, u8 val) { int i; int ack; for (i 7; i 0; i--) { gpiod_set_value_cansleep(ov-sio_d, (val i) 1); sccb_delay(ov); gpiod_set_value_cansleep(ov-sio_c, 1); sccb_delay(ov); gpiod_set_value_cansleep(ov-sio_c, 0); sccb_delay(ov); } /* 第 9 个时钟是应答位此时 SIO_D 由 OV9650 控制 */ gpiod_direction_input(ov-sio_d); gpiod_set_value_cansleep(ov-sio_c, 1); sccb_delay(ov); ack gpiod_get_value(ov-sio_d) ? -EIO : 0; gpiod_set_value_cansleep(ov-sio_c, 0); gpiod_direction_output(ov-sio_d, 1); sccb_delay(ov); return ack; } static int sccb_write_reg(struct ov9650_dev *ov, u8 reg, u8 val) { int ret; sccb_start(ov); ret sccb_write_byte(ov, OV9650_SCCB_ADDR_WR); if (ret 0) goto out; ret sccb_write_byte(ov, reg); if (ret 0) goto out; ret sccb_write_byte(ov, val); out: sccb_stop(ov); return ret; }这里OV9650_SCCB_ADDR_WR我一般定义为0x60对应 7 位地址0x30。如果你的板卡原理图把 sensor 的 ID 引脚拉高或者拉低实际地址会偏移需要按照模组手册确认。sccb_delay_ns一开始不要追求速度快先给到 1000 也就是 1us读到 ID 后再慢慢往下压。GPIO 模拟 SCCB 的坑通常不是逻辑不对而是初始时 SIO_D 没有设成开漏输出导致应答位读不到低电平。OV9650 读寄存器不能像 I2C 那样连续写完地址直接读SCCB 的标准做法是先写寄存器地址停止再发读地址static u8 sccb_read_reg(struct ov9650_dev *ov, u8 reg) { u8 val 0; sccb_start(ov); sccb_write_byte(ov, OV9650_SCCB_ADDR_WR); sccb_write_byte(ov, reg); sccb_stop(ov); sccb_start(ov); sccb_write_byte(ov, OV9650_SCCB_ADDR_RD); val sccb_read_byte(ov, 1); sccb_stop(ov); return val; }读字节的返回值要区分最后一次字节和中间字节读最后一字节时可以发 NACK告诉 sensor 不需要继续送了。多数驱动为了省事读完一字节就停止OV9650 也能接受但遇到某些批次模组时会产生错位。2.2 OV9650 输出什么格式CAMIF 就按什么格式拆OV9650 最大能输出 SXGA 分辨率实际项目里用的最多的是 VGA 640x480。输出格式主要有 YUV 4:2:2、RGB565、Raw RGB、JPEG 几种。2410/2440 的 CAMIF 对 YUV 4:2:2 支持最顺DMA 通道按 YUYV/YVYU 顺序收数据DRAM 里就是连续两字节一个像素。如果第一次点屏我强烈建议先把 sensor 配置成 YUV 4:2:2不要上来就用 RGB565因为 CAMIF 内部还可能涉及字节交换RGB565 一旦字节序反了图上颜色会非常诡异。YUV 4:2:2 下一行 640 像素一帧数据量就是 640x480x2。用户态拿到的是Y0 U0 Y1 V0 Y2 U2 Y3 V3这样的连续流。想快速预览可以用 ffplay 直接播放ffplay -f rawvideo -pixel_format yuyv422 -video_size 640x480 frame.yuv先把这条命令跑通再去做 RGB 转换。如果画面能看出轮廓但颜色不对优先怀疑 UV 顺序如果全是雪花优先怀疑 PCLK 极性和 HREF 时序。2.3 用字符设备驱动框架而不是 V4L2不是炫技自己写 OV9650 驱动不是否定 V4L2。V4L2 是标准接口后续要接 GStreamer、OpenCV 或视频编码器最终还是要往 V4L2 上靠。但如果你手里是一块老内核的 2440 板或者 A8 平台要快速验证 sensor 通路V4L2 的videobuf、v4l2_subdev在不同内核版本间差异很大调个框架半天就过去了。字符设备驱动框架轻得多注册一个miscdevice实现 open/release/ioctl/mmap/poll数据通路自己控制。调试 sensor、抓原始寄存器、测帧率都比在 V4L2 里翻日志快。3. 把 SCCB 和寄存器初始化写进驱动这一章是把上一章的思路落到代码上。字符设备驱动框架不需要很多函数但每个函数的边界要清楚open 只负责打开设备read/mmap 负责把帧数据交给用户态poll 让用户态能等到“新的帧到了”ioctl 用来查格式、触发采集。3.1 file_operations 里该实现哪几个接口static const struct file_operations ov9650_fops { .owner THIS_MODULE, .open ov9650_open, .release ov9650_release, .read ov9650_read, .mmap ov9650_mmap, .poll ov9650_poll, .unlocked_ioctl ov9650_ioctl, };open 里我不做重活只把mutex_lock加上防止两个进程同时打开设备。真正初始化 sensor 放在platform_probe里做因为 open 可能被应用反复调用每次都初始化一遍寄存器太慢。read 接口提供一种傻瓜式用法用户态调用read(fd, buf, len)驱动阻塞到新帧到来后拷贝一帧。这种拷贝过程多一次内存搬运但适合先把通路验证了。poll 的实现核心是一个等待队列static unsigned int ov9650_poll(struct file *file, poll_table *wait) { struct ov9650_dev *ov file-private_data; poll_wait(file, ov-waitq, wait); if (atomic_read(ov-frame_ready)) return POLLIN | POLLRDNORM; return 0; }frame_ready必须在硬件中断里置位而不是在用户态ioctl后再去读寄存器。摄像头帧中断是硬件产生的中断里要做的事越少越好。3.2 寄存器初始化序列先验证 ID再写参数表OV9650 的寄存器有上百个真正决定能不能出图的没有那么多。关键路径是软复位、等待 sensor 稳定、读产品 ID、设置输出格式、设置窗口、设置 PCLK 分频最后开自动曝光/白平衡。下面这段代码不追求把每个寄存器都列全而是展示驱动里初始化函数该有的骨架#define OV9650_REG_PID 0x0a #define OV9650_EXPECT_PID 0x96 static int ov9650_init(struct ov9650_dev *ov) { u8 pid; int ret; int i; ret sccb_write_reg(ov, 0x12, 0x80); /* 软复位 */ if (ret 0) return -EIO; msleep(20); pid sccb_read_reg(ov, OV9650_REG_PID); if (pid ! OV9650_EXPECT_PID) { dev_err(ov-dev, sensor id mismatch: 0x%02x\n, pid); return -ENODEV; } for (i 0; i ov-reg_num; i) { ret sccb_write_reg(ov, ov-regs[i].reg, ov-regs[i].val); if (ret 0) { dev_err(ov-dev, write reg 0x%02x failed\n, ov-regs[i].reg); return -EIO; } usleep_range(200, 300); } return 0; }0x12是 COM70x80是软复位位。软复位后立刻读寄存器很多批次会读到 0 或者旧值所以msleep(20)不能省。ID 写入0x0A是产品 ID 寄存器OV9650 正常返回0x96记下这个值后面排查 SCCB 电气问题会省很多时间。参数表我习惯单独放在一个数组里驱动逻辑不关心每个寄存器含义struct ov9650_reg { u8 reg; u8 val; }; static const struct ov9650_reg ov9650_vga_yuv422[] { {0x12, 0x00}, /* 退出复位选择 YUV 输出 */ {0x11, 0x40}, /* PCLK 分频实际值由 XCLK 和目标帧率算 */ {0x0e, 0x61}, /* COM5UV 顺序相关 */ {0x3a, 0x04}, /* 颜色矩阵先按模组默认值 */ };这块是 OV9650 驱动里最容易被网上资料误导的地方。不同模组厂给的初始化表可能都不一样尤其在 2440 和 A8 上XCLK 频率不同PCLK 分频就不同镜头不同自动曝光目标值也不同。正确做法是根据你自己的模组手册或供应商驱动提取一张表再通过测试程序一张一张验证。驱动框架只需要一个“按表写寄存器”的入口。3.3 2410/2440 和 A8 的 CAMIF 配置差异同样是 OV96502410/2440 和 A8 平台的接入方式有两点要额外注意一个是时钟一个是 DMA 地址。2440 的 CAMIF 需要 HCLK 分频出 CAMCLK 给 sensor 做 XCLKOV9650 的 XCLK 一般给 12MHz 到 24MHz。你需要在板级初始化里把摄像头时钟算对而不是只在驱动里配分频。A8 平台如果用的是片上 camera controller通常也要在设备树里把时钟和 GPIO 引线配好ov9650: ov965030 { compatible ovti,ov9650; reg 0x30; pinctrl-names default; pinctrl-0 cam_pins; clocks clk_cam; reset-gpios gpio 0x10 GPIO_ACTIVE_LOW; pwdn-gpios gpio 0x11 GPIO_ACTIVE_HIGH; status okay; };2440 年代的内核没有设备树大多数用platform_device和板级注册表搞定A8 上我更建议直接用设备树把 CAMIF 中断、DMA 通道、时钟全部交给内核解析。你写的字符设备驱动代码在两个平台上是同一套差异收敛在 DMA buffer 分配和 mmap 映射上。4. 测试程序从 /dev/ov9650 拿一帧 YUV422驱动写完后应用层测试程序要能回答三个问题能不能出帧、格式对不对、帧率够不够。用 mmap 映射 DMA 缓冲区比每次 read 拷贝更适合连续采集。4.1 最小采集程序mmap 加 poll这个测试程序不依赖 V4L2直接操作自定义字符设备#include stdio.h #include fcntl.h #include sys/ioctl.h #include sys/mman.h #include poll.h #include unistd.h #include ov9650_ioctl.h int main(void) { int fd; int i; unsigned char *buf; struct ov9650_format fmt; struct pollfd pfd; FILE *out; fd open(/dev/ov9650, O_RDWR); if (fd 0) { perror(open); return 1; } if (ioctl(fd, OV9650_IOC_G_FMT, fmt) 0) { perror(ioctl fmt); return 1; } buf mmap(NULL, fmt.width * fmt.height * 2, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (buf MAP_FAILED) { perror(mmap); return 1; } pfd.fd fd; pfd.events POLLIN; for (i 0; i 30; i) { if (poll(pfd, 1, 500) 0) { ioctl(fd, OV9650_IOC_DQ_FRAME, i); } } out fopen(frame.yuv, wb); fwrite(buf, 1, fmt.width * fmt.height * 2, out); fclose(out); munmap(buf, fmt.width * fmt.height * 2); close(fd); return 0; }这里OV9650_IOC_G_FMT只负责查询当前格式驱动返回 640x480OV9650_IOC_DQ_FRAME表示用户态要取最新的一帧驱动在中断里已经把帧准备好这个 ioctl 主要用来做缓冲切换。记住一点mmap 映射的是内核 DMA buffer不是普通用户态 malloc 内存所以驱动在 mmap 回调里要用pgprot_noncached或者直接映射物理连续内存否则 A8 的 cache 会把数据挡住。4.2 帧率验证不要只看打印很多人在测试程序里 atodelay 一下算一秒能读多少帧结果是 30fps 还是 8fps 完全没准。更可靠的做法是记录每帧的单调时钟#include time.h static long long now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return ts.tv_sec * 1000LL ts.tv_nsec / 1000000; }每 poll 到一帧就调用now_ms()打印与上一帧的差值。正常 640x480 YUV422 在 24MHz 的 XCLK 下能跑到 30fps如果间隔普遍超过 40ms先查 PCLK 分频和 DMA burst 配置不要怀疑 sensor 能力。4.3 用原始帧检查像素格式是否被 CAMIF 拆错把frame.yuv拖回 PC 用 ffplay 看过之后如果画面方向、亮度和颜色都正常说明 sensor 输出格式和 CAMIF 配置已经对齐。如果画面像万花筒一样说明窗口时序不对如果是绿紫色人脸基本是 U/V 顺序反了。对 OV9650 来说改 UV 顺序有两个入口sensor 寄存器里的输出格式位以及 CAMIF 里的字节交换位。我调试时会固定住 sensor 那侧只改 CAMIF 的 byte swap这样一旦画质正常能确定是哪一个环节造成的。5. OV9650 驱动从黑屏到花屏五条避坑记录驱动能出图不代表能交付下面这几个问题我都踩过。每一条按现象、原因、解决来写遇到相同症状可以直接对照。5.1 读不到 sensor ID返回 0x00 或 0xff现象ov9650_init里读0x0A返回值不是0x96日志里报sensor id mismatch。原因最常见是 sensor 没真正上电或者 SCCB 总线上拉电阻缺失。GPIO 模拟读应答位时SIO_D 切到输入后悬空读到高电平驱动把高电平当作 NACK整个事务失败。还有可能是 SCCB 时钟太快OV9650 跟不上。解决先用万用表量 SIO_D 上拉到 VDD 的电阻确认 PWDN 和 RESET 引脚不是悬空再把sccb_delay_ns改成 1000 以上把 SCCB 时钟压到 100kHz 以下重试。如果还不行用逻辑分析仪抓 SIO_C 和 SIO_D重点看起始条件和 ACK 位位置九成问题一眼就能看出来。5.2 能读到 ID但 CAMIF 一直收不到 VSYNC现象驱动初始化成功内核没有报错但 poll 始终超时调试 GPIO 接的帧中断也一直没有触发。原因寄存器表里没有把 sensor 从测试模式或者待机模式切出来。很多网上抄的初始化表最后几行会把 sensor 设成输出彩条或者让 PCLK 停在高电平看起来像“初始化成功”实际没进正常采集模式。解决用示波器或逻辑分析仪直接量 OV9650 的 VSYNC 和 HREF 引脚。如果 PCLK 有信号但 VSYNC 不动问题在 sensor 窗口设置如果 PCLK 都没有问题在时钟或者 power down。不要一上来就怀疑 DMA 配置。5.3 画面整体偏绿或偏紫轮廓是清楚的现象能出帧但人脸和桌面颜色完全不对绿色和紫色占满画面。原因YUV422 的 U/V 顺序反了。OV9650 可以输出 YUYV也可以输出 YVYUCAMIF 的 DMA 通道对应关系如果和 sensor 不一致颜色就会乱。解决先固定 sensor 寄存器里的 UV 输出顺序然后改 CAMIF 的 byte swap 控制位。改完重新抓一帧用 ffplay 看同一块颜色区域正常情况下肤色能回来。如果换寄存器没用检查是不是把 YUV422 数据当成了 RGB565 处理两者每像素字节数一样但含义完全不同。5.4 DMA 中断有但 mmap 出来的内存一直是旧数据现象poll 每次都返回用户态也调用了 DQ_FRAME但读出来的 30 帧内容一模一样或者前两帧会动后面全卡住。原因DMA buffer 的 cache 一致性没有处理。DMA 把数据写进物理内存CPU 如果读到 cache 里的旧数据看到的就是“帧没更新”。在老内核 2440 上尤其常见因为 CAMIF 的 DMA 地址是物理地址而用户态 mmap 映射的是内核虚拟地址中间缺一次 cache invalidate。解决不要在驱动里手工把 buffer 地址传到 DMA 后就不再管。用dma_alloc_coherent分配采集缓冲区或者每次帧完成后对 buffer 做dma_map_single加dma_unmap_single。这个钱不能省。5.5 连续采集几分钟后系统卡死控制台报 paging request现象单帧采集没问题循环跑一会儿后unable to handle kernel paging request或者 DMA 中断风暴。原因中断处理函数里做了不允许睡觉的事比如msleep或者直接访问 SCCB又或者 DMA buffer 被用户态越界写坏。OV9650 初始化写寄存器时常用usleep_range如果这段代码被放到硬件中断上下文系统挂掉只是时间问题。解决中断里只置frame_ready标志唤醒等待队列所有寄存器操作挪到 ioctl 或者工作队列里。DMA buffer 要把VMA的访问范围限制死不让应用随便越界写。建议在中断处理函数里加一个自旋锁保护frame_ready防止并发访问。6. 从单帧采集到连续采集中断、超时和缓存一致性驱动过了“能出一帧”这个阶段接下来的问题是连续采集会不会漏帧、会不会卡死。这里有一个我每次必做的改造给等待帧加超时同时把中断处理精简到最小。static irqreturn_t ov9650_irq(int irq, void *data) { struct ov9650_dev *ov data; atomic_set(ov-frame_ready, 1); wake_up_interruptible(ov-waitq); return IRQ_HANDLED; } static int ov9650_wait_frame(struct ov9650_dev *ov) { int ret; atomic_set(ov-frame_ready, 0); ret wait_event_interruptible_timeout(ov-waitq, atomic_read(ov-frame_ready), 2 * HZ); if (ret 0) { dev_warn(ov-dev, capture timeout\n); return -ETIMEDOUT; } return 0; }wait_event_interruptible_timeout是连续采集的后悔药DMA 或帧中断偶尔会丢一次如果驱动傻等应用线程就卡死了。超时返回后驱动要主动做一次 CAMIF 软复位或者重新启动采集把采集机拉回正轨。我现在的习惯是每 1000 帧打印一次超时计数而不是每帧中断都打印否则日志刷得比数据还多。DMA buffer 用dma_alloc_coherent分配mmap 时保持 cache 一致性属性这比在中断里手工 flush 可靠得多。最后一条提醒不要在中断里调printk也不要调sccb_read_reg。把中断处理压缩到“置位、唤醒、返回”这是字符设备驱动框架下最不容易翻车的写法。希望帮到你。本文还有配套的精品资源点击获取
返回列表