OV9650摄像头驱动开发实战:从2410到A8的移植与调试

发布时间:2026/10/21 8:14:04
OV9650摄像头驱动开发实战:从2410到A8的移植与调试 简介一套基于V4L2框架的ov9650摄像头驱动及配套测试程序由作者在项目实战中移植自参考代码可稳定运行于tiny210开发板并兼容2410、2440、A8等主流ARM处理器。驱动层已将摄像头控制逻辑与具体平台解耦基本不涉及ARM摄像头控制寄存器操作换用不同Linux内核或其他厂家的210开发板时只要硬件连接一致稍作修改即可使用。压缩包内共288个文件整体仅2.29MB核心为C驱动源码、Makefile及Kconfig配置另有少量HTML/JS辅助页面、PDF说明和图片素材测试程序依赖OpenCV与Qt4.7可直接编译进行图像采集验证。源码中可见mach-mini210.c等平台适配文件与ov965x.c主驱动文件目录结构清晰便于对照不同硬件进行移植。目前已有487人学习下载这份移植资料完整、稀缺能帮助嵌入式开发者快速理解V4L2框架下摄像头驱动的搭建与调试过程对于需要接入ov9650模组的项目有直接参考价值。1. 自己写的 OV9650 摄像头驱动从 2410 到 A8一块传感器三个平台的折腾记录如果你手头有一块 OV9650 摄像头模组想接到 2410、2440 或者 Cortex-A8 开发板上出图像最省事的做法是找个现成的驱动源码移植。这篇文章要聊的这套驱动就是我当时为某项目自己从零写的 OV9650 驱动顺带配套了一个独立的测试程序可以直接抓帧、存成 BMP 文件。整套代码按 Linux 字符设备驱动框架组织核心逻辑在 I2C 配置和寄存器初始化上只要你的内核能提供基本的 I2C 控制器和 GPIO 操作接口稍作修改就能跑起来。这套驱动平时主要用在两类场景一类是工业视觉的入门验证想快速确认模组能不能出图另一类是嵌入式课程里的摄像头驱动实验用来理解 I2C 时序、V4L2 框架和 DMA 传输。适用人群是有点 C 语言和 Linux 驱动基础、但不想从零啃传感器手册的开发者。对我这种常年跟嵌入式底板打交道的人来说OV9650 属于那种不复杂但必须细心的传感器它的 PCLK 极性、HSYNC/VSYNC 的时序配合以及寄存器表的数组顺序稍微弄错一丁点图像就花给你看。下面把整套东西怎么组织、每一段代码干什么、踩过哪些坑一次讲清楚。2. 整体架构与平台关联I2C 控制寄存器V4L2 接口2.1 OV9650 传感器接口速览OV9650 是一个 130 万像素的 CMOS 图像传感器输出格式支持 Bayer RGB、YUV 和 RGB 系列最大 SXGA1280x1024也可以通过寄存器配成 VGA、QVGA 等更小的分辨率。它的控制总线是两线式 SCCB 接口本质上兼容 I2C。我们从处理器这边看需要打交道的信号其实就几根SIO_C 和 SIO_D 是控制总线Y0~Y9 是 10 位并行数据输出PCLK 是像素时钟HREF 和 VSYNC 分别是行有效和帧同步信号。这三个平台的差异主要落在处理器侧S3C2410 和 S3C2440 都自带 I2C 控制器寄存器地址和中断号略有不同但驱动模型几乎一样Cortex-A8 的 I2C 控制器则更接近通用的 OMAP 或类似设计寄存器映射方式完全不同。我在写驱动的时候把 I2C 底层访问做成了三个平台的三套函数但上层寄存器配置逻辑保持同一份这样后续切换平台只需要替换底层读写函数。2.2 驱动框架选型直接字符设备还是 V4L2很多人拿到摄像头第一反应是套 V4L2 框架这样 v4l2-ctl、GStreamer 这些工具可以直接用。但 V4L2 对 OV9650 这种老传感器来说需要处理 subdev、media controller、video_device 等多层抽象对只想出图的场景有点重。我当时选择了更简单的字符设备方案驱动注册成 miscdevice暴露 open、release、ioctl、mmap 等接口应用层用配套的 test 程序来操作。这不是偷懒而是实用主义选择。字符设备驱动的代码量大概只有 V4L2 方案的三分之一而且所有流程都在自己的控制下调试 I2C 时序和寄存器配置时非常直接。如果你后续需要兼容标准应用也可以在这个基础上包一层 V4L2 的 video_device核心的 sensor 配置函数不需要重新写。下面是一个典型的文件布局ov9650_driver/ ├── s3c_i2c_2410.c // 2410/2440 I2C 底层 ├── s3c_i2c_a8.c // A8 平台 I2C 底层 ├── ov9650_core.c // 寄存器配置、上电、初始化 ├── ov9650_drv.c // 字符设备接口 ├── ov9650_regs.h // 寄存器地址宏 ├── test_ov9650.c // 测试程序采集并保存 bmp └── Makefile2.3 平台差异的边界条件先说 2410 和 2440。它们的内核版本老通常跑的是 Linux 2.6 或者 3.x 早期版本。I2C 控制器寄存器基址在 0x540000002410和 0x540000002440附近多数驱动直接把 IICCON、IICSTAT 这些寄存器做内存映射操作很少用内核的 i2c_adapter因为要适配旧内核里的 platform 设备机制。Cortex-A8 这边情况不同。常见开发板上的 I2C 控制器可能挂在不同的 L4 互连总线下物理基址通常在 0x44E0B000 这一类位置访问方式依然是把物理地址映射到内核虚拟地址空间。为了避免平台相关的寄存器位定义散落各处我把底层的 i2c_write_reg 和 i2c_read_reg 做成函数指针在设备树或平台初始化时调用对应实现。3. 核心模块拆解寄存器配置表里的每一字节3.1 I2C 读写函数的实现逻辑OV9650 的 SCCB 协议和标准 I2C 的写序列非常接近起始位、设备地址 写位、寄存器地址、数据字节、停止位。读序列则比标准 I2C 多一个步骤需要先发送寄存器地址再重新发起起始位和设备地址 读位才能把数据读回来。很多第一次写这个驱动的人会在读时序上翻车因为传感器手册里的时序图画得像是要单独发一个读指令。我自己实现的读函数长这样按 2440 平台示例说明static int ov9650_i2c_read(u8 reg, u8 *val) { unsigned int tmp; // 写寄存器地址 tmp (dev_addr 1) | 0; // 写方向 i2c_start(); i2c_write_byte(tmp); i2c_wait_ack(); i2c_write_byte(reg); i2c_wait_ack(); i2c_stop(); // 重新发起读操作 tmp (dev_addr 1) | 1; // 读方向 i2c_start(); i2c_write_byte(tmp); i2c_wait_ack(); *val i2c_read_byte(); i2c_send_nack(); i2c_stop(); return 0; }这段代码里的 i2c_start 和 i2c_write_byte 在不同平台下有不同的实现。比如 2440 上IICCON 的位 4 控制中断使能IICSTAT 的位 5 是数据输出使能A8 平台的寄存器则完全不同。我自己封装时把这两个函数改成从某个结构体里取函数指针这样寄存器表层面的代码一行都不用改。3.2 寄存器配置表的组织方式OV9650 寄存器大概有以下几大类时钟分频和输出格式控制、曝光和增益、窗口裁剪、测试图案、PLL 倍频。直接对每个寄存器写一行赋值语句会让代码变成几百行平铺的序列不好维护。我用的方式是构造一张配置表每一行是{寄存器地址值}然后循环写入。例如下面是部分 VGA640x480YUV422 输出的配置片段static const struct ov9650_reg reg_init_vga_yuv422[] { { 0x12, 0x80 }, // COM7: reset { 0x11, 0x01 }, // CLKRC: 外部时钟不分频 { 0x1e, 0x00 }, // 关闭镜像 { 0x17, 0x26 }, // HSTART 高字节 { 0x18, 0x40 }, // HSTART 低字节 { 0x19, 0x00 }, // VSTART 高字节 { 0x1a, 0x60 }, // VSTART 低字节 { 0x3a, 0x7c }, // TSYNC1 { 0x3d, 0x48 }, // TSYNC2 { 0x3e, 0x00 }, // 窗口设置 { 0x3f, 0x80 }, { 0x40, 0x00 }, { 0x41, 0x00 }, { 0x42, 0x50 }, { 0x43, 0x00 }, { 0x44, 0x80 }, { 0x45, 0x00 }, { 0x46, 0x00 }, { 0x47, 0x00 }, { 0x48, 0x00 }, { 0x49, 0x00 }, { 0x4a, 0x00 }, { 0x4b, 0x00 }, { 0x4c, 0x00 }, { 0x4d, 0x00 }, { 0x4e, 0x00 }, { 0x4f, 0x00 }, { 0x50, 0x7c }, { 0x51, 0x00 }, };写这段配置表的时候最需要注意的是寄存器之间的依赖关系。比如 COM70x12的最高位是软件复位一般要在最前面写一次 0x80然后再配置别的寄存器否则很多寄存器写不进去。此外HSTART/VSTART 这些窗口坐标寄存器与目标分辨率不是简单的相等关系它和 HREF、VREF 的边界、以及内部裁减规则有关。这些值最好参考传感器手册中的recommended settings表格哪怕原始推荐值对应的分辨率是 SXGA也要先照抄再微调不要自己凭感觉推公式。3.3 初始化流程和时序控制初始化函数不只是逐条写寄存器还需要考虑上电时序。OV9650 的复位引脚 XCLR 必须保持低电平至少一段时间再拉高然后延时等待内部 PLL 稳定。我在驱动里用通用 GPIO 操作实现这个逻辑void ov9650_power_up(void) { // XCLR 拉低保持复位 gpio_set_value(xclr_gpio, 0); udelay(1000); // 上电后拉高释放复位 gpio_set_value(xclr_gpio, 1); msleep(20); // 写复位寄存器等待传感器内部自动恢复 ov9650_write_reg(0x12, 0x80); msleep(50); // 逐条写入配置表 for (i 0; reg_table[i].addr ! 0xff; i) { ov9650_write_reg(reg_table[i].addr, reg_table[i].val); // 个别寄存器需要微小延时这里可以根据实际增加 } }延时参数是玄学。不同模组的晶振频率不同外部时钟 24MHz 和 12MHz 的稳定时间不一样。我在 2440 上用的 20ms 和 50ms 是试出来的在 A8 平台上因为主频更高、I2C 线缆寄生电容不同可能要让释放复位后多等 30ms。如果上电后读寄存器一直是 0xff或者读到的 ID 不对优先怀疑延时不够。4. 测试程序实战从打开设备到保存一帧 BMP4.1 测试程序的设计思路配套的 test_ov9650 程序承担两个职责验证驱动的正确性、演示应用层怎么和字符设备交互。它采用的流程非常直接open 设备文件然后 ioctl 命令让驱动完成初始化再 mmap 驱动里预分配的帧缓冲等待一帧完成后把数据写到本地文件。这套方式绕开了复杂的 V4L2 缓冲队列适合刚开始调试传感器的人。程序支持几个命令行参数包括输出文件名、分辨率、采集帧数。如果没有参数默认输出当前目录下的 capture.bmp。这样在开发板上挂载根文件系统后直接执行./test_ov9650 -f /tmp/img.bmp就能看到图像。4.2 ioctl 命令和帧缓冲驱动把帧缓冲用dma_alloc_coherent分配这样避免 cache 一致性问题。测试程序对应使用mmap把这块物理内存映射到用户空间struct ov9650_frame { void *virt_addr; dma_addr_t phys_addr; unsigned int size; unsigned int width; unsigned int height; };ioctl 接口只设计了两条命令OV9650_IOC_INIT触发传感器初始化和预备 DMAOV9650_IOC_CAPTURE启动一次帧采集并阻塞等待 VSYNC。用户空间代码大致如下int fd open(/dev/ov9650, O_RDWR); struct ov9650_buffer_info info; ioctl(fd, OV9650_IOC_INIT, width, height); // 获取帧缓冲信息并 mmap ioctl(fd, OV9650_IOC_GET_BUF, info); unsigned char *buf mmap(NULL, info.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // 触发采集阻塞等待一帧完成 ioctl(fd, OV9650_IOC_CAPTURE, NULL); // 此时 buf 里的数据已经是完整的 YUV422 帧需要注意mmap映射的是 DMA 缓冲区而不是设备文件偏移。驱动要做的是在ov9650_mmap里调用remap_pfn_range或dma_mmap_coherent。很多从网络抄来的驱动映射完发现数据全零就是缓存一致性问题导致的。4.3 YUV422 转 BMP 的坑OV9650 输出 YUV422 时数据顺序是按 UYVY 还是 YUYV 排列取决于寄存器配置。BMP 保存的是 BGR 格式所以测试程序里必须做颜色空间转换。这一转换本身不难难在 YUV 到 RGB 的系数选择和字节序处理上。我用的转换代码是固定公式static void yuv422_to_rgb24(unsigned char *yuv, unsigned char *rgb, int w, int h) { int i, j; for (i 0; i h; i) { for (j 0; j w; j 2) { int y0 yuv[i * w * 2 j * 2]; int u yuv[i * w * 2 j * 2 1]; int y1 yuv[i * w * 2 j * 2 2]; int v yuv[i * w * 2 j * 2 3]; rgb[i * w * 3 j * 3 0] clamp(y0 1.772 * (u - 128)); rgb[i * w * 3 j * 3 1] clamp(y0 - 0.344 * (u - 128) - 0.714 * (v - 128)); rgb[i * w * 3 j * 3 2] clamp(y0 1.402 * (v - 128)); rgb[i * w * 3 j * 3 3] clamp(y1 1.772 * (u - 128)); rgb[i * w * 3 j * 3 4] clamp(y1 - 0.344 * (u - 128) - 0.714 * (v - 128)); rgb[i * w * 3 j * 3 5] clamp(y1 1.402 * (v - 128)); } } }这里的clamp不难理解就是限制输出范围到 0~255。如果图像出来整体偏绿把 U 和 V 的取值顺序调换一下通常就能解决。图像整体偏蓝或偏红则可能是 YUV 排列是 YUYV上面的代码里取 U/V 的位置要换。这类问题属于典型的字节序玄学我会直接在测试程序里加一个#define YUYV_ORDER开关跑一次看颜色是否正确再固化下来。5. 避坑指南OV9650 驱动开发中常见的四个坑5.1 现象图像完全花屏像雪花原因PCLK 极性不对是最常见的原因。OV9650 默认在 PCLK 上升沿输出数据但如果你在驱动配置里把PCLK_POL位设反了或者 CAMIF 采样边沿选错采集到的数据就是乱码。另外HREF 和 VSYNC 的有效电平也可能与 CAMIF 期望的不一致。解决先用示波器看 PCLK、HREF、VSYNC 三根线的波形确认每个信号空闲时是高还是低。然后对照处理器摄像头接口的时序说明在驱动里调整采样边沿。2440 的 CAMIF 寄存器里有个CISRCFMT和CIDCTRL的字段控制边沿和极性A8 平台的 ISP 接口则是另外一套寄存器。我当时在 2440 上花了整整两天最后发现是CIDCTRL里的HREF_LEVEL和VSYNC_LEVEL设和传感器输出反了。5.2 现象I2C 通信时好时坏偶尔写寄存器失败原因SIO_C 和 SIO_D 上缺少上拉电阻是常见情况。很多开发板为了省面积把感光元件排线做得很短不用上拉但某些扩展板通过排线连接时线缆一长就出现信号质量问题。还有一种情况是驱动里 I2C 起始/停止时序没有加足够的延时过于贴近 I2C 最小保持时间。解决给两根信号线各加一个 4.7kΩ 上拉电阻到 VDD 即可。如果没法改硬件可以在 I2C 读写函数中每个时钟周期翻转间加几次空操作。因为 S3C2440 的 I2C 控制器可以自动产生时钟时序问题多发在 GPIO 模拟 I2C 的平台上。我自己在 A8 平台上使用的是寄存器控制 I2C还是会在初始化时连续写同一个寄存器并验证回读值确保稳定。5.3 现象从 SXGA 切到 VGA 后图像只有上半部分有内容原因窗口寄存器没有跟着分辨率同步更新。OV9650 的 HSTART/VSTART、HSIZE/VSIZE 这些值配置不当会导致有效像素窗口偏移。特别要注意的是切分辨率必须重新写整个寄存器表不能只改COM7一个寄存器里的分辨率位因为传感器内部的裁剪通道也要跟随变化。解决维护三张完整的寄存器表分别对应 SXGA、VGA、QVGA。每次切换先写复位寄存器 0x12 为 0x80再延时然后重新写表。另外测试程序里的帧宽高参数也要和寄存器表匹配否则 DMA 缓冲区大小和 CAMIF如果用了计算的行长度不一致也会截断图像。5.4 现象帧率远低于预期只有每秒几帧原因PCLK 分频配置不合理。OV9650 的 CLKRC 寄存器0x11控制内部 PCLK 倍频和分频如果外部晶振是 24MHz而 CLKRC 配置成 1/2 分频PCLK 只有 12MHzVGA 模式下理想帧率可能只有 15fps 甚至更低。调试时如果没有示波器可以通过读 CLKRC 回读值确认实际生效分频。解决根据当前分辨率计算需要的 PCLK 范围VGA 输出大概需要 12MHz 到 24MHz。如果主控端 CAMIF 支持片内分频可以把传感器 PCLK 输出固定到较高值由主控端再分频采样。这样切换分辨率时传感器这边不用频繁改 CLKRC稳定性会更好一些。6. 二次开发把驱动移植到新平台并扩展其他传感器6.1 从 2440 移植到 A8 的改动清单我经历过一次从 2440 到 A8 的移植整体改动量比我预期小因为核心的寄存器配置表几乎没动。需要替换的主要是底层访问接口、中断注册和 DMA 分配方式。下表列出了典型的映射关系方便你对照检查自己的移植功能点2440 做法A8 平台做法I2C 控制器访问直接写物理寄存器通过 ioremap 映射控制寄存器GPIO 控制复位操作 GPIO 配置寄存器使用通用 gpio_set_valueDMA 缓冲区分配dma_alloc_coherentdma_alloc_coherent 或 dma_alloc_coherent 参数变化中断注册request_irq 平台中断号使用设备树中的中断号解析时钟使能手动打开外设时钟通过 clk_get 和 clk_enable移植时最容易忽略的是时钟使能。2440 上外设时钟默认开启但在 A8 平台上很多外设模块默认处于关电状态I2C 控制器不使能时钟所有寄存器访问都会是无效的。我一般在初始化代码最前面加一段clk_prepare_enable并在退出时释放。6.2 调试技巧用后门寄存器定位问题除了常规的 dev_dbg 打印我更推荐在驱动里加一个调试用 proc 接口直接读写 OV9650 寄存器。这样在测试程序跑不通时可以直接在 shell 命令行里执行echo 0x12 0x80 /proc/ov9650/dbg cat /proc/ov9650/dbg这个接口的实现很简单在 procfs 里注册一组 read/write 回调read 时解析命令然后调用驱动内部的ov9650_read_reg把结果打印出来。它能帮你快速确认寄存器表是否真的写进入而不需要反复改内核打印。当时我在怀疑某个寄存器表项没生效时靠这个接口逐字节对比了 OV9650 的参考配置最后发现是一处地址写错导致窗口偏移。6.3 扩展到 OV5640 等更大分辨率传感器如果你想在这个驱动基础上支持 OV5640可以直接复用大部分框架。OV5640 是 500 万像素传感器控制总线同样是 I2C但寄存器空间更大部分寄存器地址是 16 位宽度。你需要把ov9650_write_reg改成可以传 16 位地址的形式同时更新寄存器表为 OV5640 的初始化序列。比较好的做法是把传感器相关代码抽象成 ops 结构体struct camera_sensor_ops { int (*power_up)(void); int (*init)(u32 width, u32 height); int (*read_reg)(u16 addr, u8 *val); int (*write_reg)(u16 addr, u8 val); };原来的 OV9650 驱动实现一套 opsOV5640 再实现一套。字符设备层、DMA 缓冲管理和测试程序完全复用。这样面对不同项目时只需要替换传感器 ops 的指针整套采集链路就能跑起来。从那以后我每次接到新的摄像头模组都强制先做这一步抽象再根据手册填寄存器表省下的调试时间远超重构成本。希望这篇拆解能帮你把自己的 OV9650 驱动尽快点亮少走几段弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询