简介:面向ARM嵌入式平台的OV9650摄像头驱动与测试程序资源包,参考现有代码移植适配,可完整支持tiny210开发板,也能经少量改动运行于2410、2440、6410、A8等ARM平台及不同版本Linux内核,适合嵌入式驱动开发者、项目集成人员参考复用。包内含驱动核心ov965x.c、平台相关mach-mini210.c以及Kconfig/Makefile构建配置,还提供基于OpenCV和Qt4.7编写的测试程序,便于直接验证图像采集与预览效果;另有PDF/docx形式的设计说明,可帮助理解移植思路。资源共288个文件,压缩包仅2.29MB,其中源码、构建脚本与说明文档为使用重点,其余多为网页脚本辅助内容。目前已有486人浏览学习。作者公开了花费两周完成的项目代码,对学习摄像头驱动框架、Linux下V4L2编程以及ARM平台驱动适配均有实质参考价值。
1. 为什么还要自己写 OV9650 摄像头驱动:老传感器在 2410/2440/A8 上的现实问题
OV9650 是一颗很老的 CMOS 图像传感器,最大支持 SXGA(1280x1024)输出,支持 YUV、RGB 和 RAW Bayer 三种数据格式,到今天仍然大量出现在工业视觉采集板、老式医疗设备和 ARM9 教学平台上。标题里的「自己编写」说明你手里没有完整的 BSP 摄像头驱动,或者厂商给的 demo 只是个黑匣子——寄存器配置不可见,换分辨率要碰运气,换一颗镜头、调一次曝光都要重新猜。这篇笔记适合手里有 2410、2440 或 A8 开发板,想把 OV9650 驱动从零肝出来的工程师。你会需要同时搞定三件事:SCCB 寄存器配置、按帧中断做 DMA 搬运,以及一个能验证图像的测试程序。这三件事在 2410/2440 和 A8 上的做法差别不小,尤其是 DMA 和缓存一致性,下面会逐一讲到。
2. 先把 OV9650 的时序和寄存器逻辑吃透:SCCB 读写的三个关键差异
2.1 OV9650 的关键引脚与上电流程
OV9650 对外接口不算复杂:SCCB 控制口(SIOC/SIOD,兼容 I2C 时序)负责写寄存器,数据口是 D0-D9 十条并行线,配合 PCLK(像素时钟)、VSYNC(帧同步)、HREF(行有效)三个同步信号把图像数据送出来。很多新手上来就写初始化序列,结果是图像出不来或者颜色一团糟,问题往往不在寄存器表,而在上电顺序——这其实就是一块典型的玄学区域。
正确的上电流程是:先把 PWDN 拉低、RESET 拉低,然后给传感器提供稳定的时钟(我一般用外部晶振 24MHz),等电源和时钟稳定至少 50ms 之后,再把 RESET 拉高,再等一个帧周期让内部 PLL 锁住,然后才开始通过 SCCB 写寄存器。如果你把 RESET 拉高的动作放在时钟之前,传感器内部逻辑会处于不确定状态,后面 SCCB 读写经常随机失败,而且失败得很稳定——每次都在同一批寄存器上 NACK。
另一个关键点是 PCLK 的输出频率。OV9650 内部有时钟分频系数,默认配置下 PCLK 可能高达几十 MHz,对 2410 这类 ARM9 来说,如果 DMA 配置跟不上,图像的右半部分会稳定地出现错位或残影。所以硬件设计时最好把 PCLK 引到一个可测的测试点,软件调试时用示波器或逻辑分析仪确认 PCLK 实际频率,而不是只看寄存器手册上的理论值。
2.2 SCCB 读时序和 I2C 的差异:写没问题,读容易翻车
OV9650 的控制总线叫 SCCB,物理层和 I2C 几乎一样,但读时序有个差异:I2C 允许在一条消息里先写寄存器地址再切换为读,而 SCCB 的读操作要求在写完寄存器地址之后发送一个 Stop 条件,再以重复起始的方式发起读。很多平台的 I2C 控制器在做 read 时会自动生成 repeated START,这也能工作,但某些老版本内核的 i2c-algo-bit 实现不会自动做这个,导致读出来的一律是 0xFF。
我一般用 Linux 内核的 i2c_transfer 接口,用两个 i2c_msg 来完成一次读,这样驱动无需关心底层控制器是否自动生成 repeated START:
static int ov9650_sccb_read(struct ov9650_dev *dev, u8 reg, u8 *val) { struct i2c_msg msg[2]; u8 addr_buf = reg; int ret; /* 第一段:写寄存器地址,不带 STOP 条件 */ msg[0].addr = dev->client->addr; msg[0].flags = 0; msg[0].len = 1; msg[0].buf = &addr_buf; /* 第二段:读一个字节,控制器自动产生 repeated START */ msg[1].addr = dev->client->addr; msg[1].flags = I2C_M_RD; msg[1].len = 1; msg[1].buf = val; ret = i2c_transfer(dev->client->adapter, msg, 2); if (ret != 2) return -EIO; return 0; }逻辑说明:两个 i2c_msg 组成一次完整传输,第一段写寄存器地址,第二段读数据,由 I2C 控制器在两条消息之间自动插入 repeated START。这样写出来的 read 函数在内核 i2c 框架下最稳,也方便移植到 2410、2440 各自的 I2C 适配器上。
参数说明:dev->client 来自 i2c_new_device,addr 是 OV9650 的 7 位地址 0x21,因为芯片的 SCCB 地址是 0x42 写、0x43 读,去掉最低位后就是 0x21。如果你是在 A8 平台上用平台设备的 I2C 控制器,这段代码同样适用,只要把 adapter 换成对应的 ic_bus。这里有一个我踩过的坑:read 失败时不要轻易返回错误,很多初始化序列里读寄存器只是为了校验,建议失败时打印寄存器号,然后继续往下写,否则初始化会脆到一抖动就挂。
2.3 2410/2440 与 A8 的采集通道差异:DMA 怎么选
三块处理器的差异主要在 DMA 通道和源地址表示方式。2410 和 2440 的 DMA 支持外设到内存传输,源地址用外设基地址加一个固定的寄存器地址(比如 CAMIF 的输入 FIFO),传输单位可以配置为 32 位,一次搬运 4 字节,刚好和 OV9650 的 YUV422 数据宽度匹配。2440 相比 2410 多了一个 CAMIF 摄像头接口,可以接 ITU-R BT.601/656 格式,但 OV9650 的并口输出方式是老式的同步并行接口,不满足 BT.656 的嵌入同步格式,所以用 2440 时多半还是要绕过 CAMIF,直接把 D0-D9 接到 GPIO 或外部总线,用 GPIO 中断 + DMA 搬运。
A8 处理器(比如 S5PV210 或 i.MX51)的情况又不一样:CPU 主频高,但外设总线的行为差异很大,DMA 控制器大多数支持 scatter-gather,缓冲区不要求物理连续,这对驱动设计是好事。但 A8 引入了一个 2410/2440 时代没这么强烈的问题——CPU cache 与 DMA 的一致性。如果 DMA 把图像数据写到了内存,而 CPU 在读取时命中了 cache 里的旧数据,画面就会随机出现上一帧的碎片。
选 DMA 通道时我的经验是:优先选内存到外设方向不涉及的那个通道,并且把突发长度(burst length)设为 16 个 32 位字,传输宽度设 32 位。突发长度太小,DMA 会频繁被总线仲裁打断,帧率高不起来;突发长度太大,在 A8 上容易触发总线错误,系统直接 oops。这个参数值得花时间调,一块板一个最优值,别照抄。
3. 驱动主体实现:字符设备框架下的 SCCB 配置与帧中断
3.1 先写字符设备驱动,而不是直接上 V4L2
很多教程一上来就让你按 V4L2 框架写摄像头驱动,但 V4L2 要在 struct video_device、vb2_queue、v4l2_event 这些抽象层里绕一圈,对 2410 这种老内核和只想抓帧做图像处理的场景,反而把简单问题复杂化。我的做法是先写一个字符设备驱动,注册成 /dev/ov9650,提供 open、close、ioctl、mmap 四个操作。测试程序配合着写,逻辑透明,出问题好定位。等驱动稳定后,如果产品需要接 GStreamer 或 OpenCV,再封装一层 V4L2 也不迟。
字符设备驱动的核心结构大概如下:open 时做一次传感器初始化,ioctl 里支持设置输出格式和启动/停止采集,mmap 把内核的 DMA 缓冲区映射到用户空间。帧数据用两个缓冲区轮换,中断来的时候通知 DMA 把当前帧搬到另一个缓冲区,DMA 完成中断把 buffer 状态置为 ready,用户程序通过 select/poll 收到可读事件。
3.2 SCCB 读写与初始化序列:一份能点亮传感器的寄存器表
SCCB 写函数比读简单,一条 i2c_msg 就能完成,但有一个细节:OV9650 的寄存器在 0x00-0x7F 之间是直接寻址,0x80 之后有部分寄存器属于扩展页,需要通过寄存器 0xFF 切换页。所以我初始化序列里第一件固定动作是往 0xFF 写 0x00,确保回到默认页,避免上一手配置残留把初始化带偏。
static const struct ov9650_reg_init ov9650_sxga_uyvy[] = { { 0xff, 0x00 }, /* 选择寄存器页 0 */ { 0x12, 0x80 }, /* PCLK 分频,SXGA 分辨率 */ { 0x13, 0xe7 }, /* 自动曝光/自动白平衡开启 */ { 0x1e, 0x04 }, /* 水平窗口起始 */ { 0x3a, 0x04 }, /* 帧率 15fps @ SXGA */ { 0x40, 0xc1 }, /* RGB/YUV 输出格式选择 */ { 0x42, 0x80 }, /* UYVY 输出顺序,Y0 U Y1 V */ { 0x11, 0x80 }, /* 输出窗口 1280x1024 */ };逻辑说明:这是最小初始化子集,真正完整序列一般有七八十个寄存器,但点亮传感器只需要先保证时钟分频、输出格式、窗口大小三个组别正确。0x12 寄存器同时承担 reset 功能,写 0x80 会让芯片执行一次软复位,所以它必须出现在 0xff 之后、其他配置之前,否则后面的配置会被复位清掉。
参数说明:0x40 寄存器决定格式,0xC1 表示选择 RGB/YUV 输出模式;0x42 寄存器控制 YUV 的字节顺序,0x80 代表 UYVY;0x3a 控制帧率分频,SXGA 下最高大概 15fps,想跑 30fps 就得把分辨率降到 VGA。我建议把初始化序列做成数组而不是散落的赋值函数,这样后面接不同分辨率配置时,只需要加一组表项,驱动代码不用动。
初始化完成之后,建议读回几个关键寄存器校验,比如读 0x0a、0x0b(PID/VERSION)确认传感器型号是 0x7F 和 0xA2。如果 PID 读不对,再往后配寄存器都是白费,这时候回去查上电时序和 SCCB 波形,别跟寄存器表较劲。
3.3 帧采集中断与双缓冲:DMA 搬运的组织方式
OV9650 的 VSYNC 是帧同步信号,每输出一帧会产生一个低电平脉冲。驱动里把这个信号接到处理器的外部中断脚(2410/2440 一般接 EINT,A8 接 GPIO 中断),中断触发方式用下降沿,这样一帧开始和结束都能捕获到。中断处理里需要做的不是搬运数据,而是启动 DMA:把传感器的数据口数据灌进当前空闲缓冲区。
static irqreturn_t ov9650_isr(int irq, void *dev_id) { struct ov9650_dev *dev = (struct ov9650_dev *)dev_id; struct ov9650_buf *buf = &dev->bufs[dev->frame_in]; unsigned long flags; spin_lock_irqsave(&dev->dma_lock, flags); if (buf->state == BUF_IDLE) { /* 把 DMA 目标地址指向当前缓冲区,搬运一帧 */ buf->state = BUF_DMA_RUNNING; dma_start(dev->dma_chan, buf->dma_addr, buf->size); dev->frame_in = (dev->frame_in + 1) % OV9650_BUF_NUM; } spin_unlock_irqrestore(&dev->dma_lock, flags); return IRQ_HANDLED; }逻辑说明:VSYNC 中断里只负责切换 DMA 目标,不处理图像数据,这个设计能保证中断处理时间足够短。dma_start 启动一次外设到内存的搬运,搬运完成后由 DMA 的中断回调把缓冲区状态改成 BUF_READY,这时候用户态的 select 才能读到 POLLIN 事件。
参数说明:OV9650_BUF_NUM 我一般设 4,不要只设 2,因为 DMA 搬运一帧的时间接近一帧周期,偶数缓冲容易在帧率波动时出现一帧被覆盖的情况。dma_addr 必须是用 dma_alloc_coherent 申请的物理地址,2410/2440 上内核默认无 cache 问题,但 A8 上必须保证这个内存区域是 uncached 或者按 DMA 层正确处理。
DMA 搬运的字节数等于一帧大小:SXGA 下 UYVY 格式是 1280×1024×2 字节,约 2.5MB。这个尺寸对 2410 的内存带宽已经有点压力,所以帧率上不去 15fps 时,第一件事不是优化 DMA,而是检查是否每一帧都真的搬完了、有没有中断丢失。
4. 测试程序怎么写:从 mmap 取帧到存出一张 BMP
4.1 测试程序的总体流程与 ioctl 设计
测试程序要解决的验证问题有三个:传感器有没有出图、颜色对不对、帧率稳不稳。针对这三个问题,我给 /dev/ov9650 设计了三个 ioctl:OV9650_GET_FMT 查询当前格式、OV9650_SET_FMT 切换分辨率和输出格式、OV9650_STREAM_ON/OFF 控制采集开关。测试程序启动的流程是 open 设备,先 GET_FMT 看默认配置,再 SET_FMT 设成自己想要的 YUV422 UYVY + SXGA,然后 mmap 两块缓冲区,最后 STREAM_ON 开始采集。
这里有个经验:ioctl 的序号定义不要图省事直接用随机数,用内核的 _IOW/_IOR 宏按类型编码。因为测试程序和驱动是两个工程,同一个 ioctl 号在两边不一致时,表现出的现象是 ioctl 调用不报错但参数全部无效,这类问题排查起来特别费时间。
4.2 mmap 双缓冲取帧:测试程序的核心代码
驱动和测试程序之间用 mmap 共享缓冲区,避免了每帧一次 read 拷贝。mmap 的 offset 参数我用来区分第几块缓冲区,驱动里 remap_pfn_range 按 offset 计算对应的物理页。用户态拿到的是一段连续虚拟地址,可以直接按 UYVY 格式解析。
#include <sys/mman.h> #include <fcntl.h> #include <linux/fb.h> #define BUF_SIZE (1280 * 1024 * 2) int main(int argc, char **argv) { int fd = open("/dev/ov9650", O_RDWR); unsigned char *buf0, *buf1; unsigned char *frame[2]; int idx = 0; if (fd < 0) { perror("open /dev/ov9650"); return 1; } /* 两边缓冲区,驱动保证 DMA 完成后才轮到用户读 */ buf0 = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); buf1 = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, BUF_SIZE); frame[0] = buf0; frame[1] = buf1; ioctl(fd, OV9650_STREAM_ON, NULL); for (int f = 0; f < 10; f++) { struct pollfd pfd = { .fd = fd, .events = POLLIN }; poll(&pfd, 1, 3000); /* 先读用户态 index,驱动在 DMA 完成后更新 */ idx ^= 1; save_as_bmp("capture.bmp", frame[idx], 1280, 1024); } ioctl(fd, OV9650_STREAM_OFF, NULL); munmap(buf0, BUF_SIZE); munmap(buf1, BUF_SIZE); close(fd); return 0; }逻辑说明:测试程序在 poll 返回之后直接拿另一块缓冲区的数据,这要求驱动必须保证:用户上次读完的缓冲区不会被下一次 DMA 覆盖。双缓冲时驱动在 VSYNC 中断里交替使用缓冲区,用户态的 idx 翻转必须和驱动保持同一个节拍,稍有误差就会出现两帧重复或一帧撕裂。
参数说明:BUF_SIZE 要和驱动里的 DMA 缓冲区大小严格一致,SXGA + UYVY 就是 1280×1024×2。poll 超时设定为 3000ms,正常情况下帧率 15fps,poll 最多等 70ms 就会返回,等超过 3 秒直接判定采集失败,这样测试程序能自己报错,而不是卡在莫名其妙的地方。save_as_bmp 是像素格式转换函数,下一节展开。
4.3 UYVY 转 BMP:像素格式转换的几个细节
OV9650 输出 UYVY 时,字节顺序是 U、Y0、V、Y1,两个像素共享一对 UV。转 BMP 时要先解出每个像素的 Y、U、V,再按标准 BT.601 公式算 RGB。这里最容易翻车的是缩放系数和截断方式,很多代码直接写浮点公式,效率低还容易在边界上溢出。
static void save_as_bmp(const char *path, const unsigned char *uyvy, int w, int h) { static const int bmp_hdr[14] = { 0x4d, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00 }; FILE *fp = fopen(path, "wb"); int row_size = (24 * w + 31) / 32 * 4; int data_size = row_size * h; unsigned char *rgb = malloc(row_size); /* 写位图文件头和信息头,BGR 顺序存储 */ /* ... 省略文件头填充 ... */ /* 从最后一行开始写,BMP 的像素存储是自底向上 */ for (int y = h - 1; y >= 0; y--) { const unsigned char *src = uyvy + y * w * 2; unsigned char *dst = rgb; for (int x = 0; x < w; x += 2) { int u = src[0] - 128; int y0 = src[1] - 16; int v = src[2] - 128; int y1 = src[3] - 16; /* BT.601 公式,全部用整数运算 */ dst[0] = clamp(298 * y0 + 516 * u + 128); dst[1] = clamp(298 * y0 - 100 * u - 208 * v + 128); dst[2] = clamp(298 * y0 + 409 * v + 128); dst[3] = clamp(298 * y1 + 516 * u + 128); dst[4] = clamp(298 * y1 - 100 * u - 208 * v + 128); dst[5] = clamp(298 * y1 + 409 * v + 128); dst += 6; src += 4; } fwrite(rgb, 1, row_size, fp); } free(rgb); fclose(fp); }逻辑说明:BMP 有 14 字节文件头加 40 字节信息头,像素从图像最后一行开始写,这是 BMP 格式与多数显示设备相反的存储方向,最容易踩坑。转换公式用整数运算,298、516、409 这些系数是 BT.601 的定点化近似,右移 8 位等价于除以 256,这样处理速度能支撑 SXGA 全尺寸转换。
参数说明:row_size 计算了 24 位色深下行字节数按 4 字节对齐,公式是 (24×w+31)/32×4,如果不按这个对齐,转换出来的 BMP 在 Windows 看图软件里会显示斜线或错位。clamp 函数要同时限制下界 0 和上界 255,U/V 减 128 后超出边界时,不做 clamp 的图像颜色会在高光区域明显失真。
5. 移植到 2410/2440 与 A8 的四个血泪坑:从花屏到缓存一致性
5.1 图像花屏或撕裂:先查 PCLK 分频,再查 DMA 对齐
现象:图像能出来,但右侧有规则的错位带,或整帧随机撕裂,像是把两帧图像水平切开了再拼起来。我第一次在 2440 上遇到这个问题时,对着寄存器表调了两天,最后是示波器量了 PCLK 发现频率已经超过 30MHz,处理器那边根本来不及处理。
原因:OV9650 的 PCLK 分频系数没调对,数据速率超过了 DMA 或 GPIO 采样能力;或者 DMA 传输宽度和源数据宽度不匹配,每行结束点对不齐。
解决:先用示波器确认 PCLK 实测频率,把 0x12 寄存器的时钟分频位调大,让 PCLK 降到 20MHz 以下;然后把 DMA 传输宽度设为 32 位,源地址递增方式设 4 字节递增,保证每次突发传输都是整行长度的整数倍。如果 PCLK 已经很低还撕裂,检查 VSYNC 和 HREF 是不是接到了处理器的正确引脚,HREF 丢失会造成行同步错位,但 VSYNC 还能正常触发,画面就表现为固定位置的斜切。
5.2 SCCB 写寄存器 NACK:上电顺序比寄存器值更玄学
现象:初始化序列跑到第 20 个寄存器左右,读写开始返回 NACK,之后配置值写不进去,图像不出来或全黑。
原因:OV9650 内部 PLL 还没锁定时,SCCB 接口会间歇性不响应。很多时候不是寄存器表问题,而是 RESET 拉高的时机早于时钟稳定,或者电源和时钟之间的电源纹波太大。这类问题有个特点:同一块板子上冷启动失败,复位后再跑一遍又变成全通,非常典型的时序竞争。
解决:在驱动 open 里加严格的时序屏障,RESET 拉低后至少延时 10ms,释放 RESET 后再等 50ms 才开始 SCCB 操作。另外在初始化函数开头加一个简单重试机制:如果连续 3 次写同一个寄存器都 NACK,就把整个上电流程重来一遍,而不是继续往下写。这个重试机制在环境温度变化导致晶振起振变慢时,能救回不少板子。
5.3 帧率上不去、画面抖动:VSYNC 中断触发沿
现象:帧率统计只有宣称值的一半,或者画面每隔几秒抖一下,CPU 占用率反而很高。
原因:中断触发方式配置成了上升沿而不是下降沿,或者 GPIO 内部上没有使能下拉。OV9650 的 VSYNC 时序是帧消隐期拉低,水平同步期间维持高电平,如果配置成上升沿触发,中断在每一帧会触发多次,DMA 被反复启动,缓冲区竞争激烈。4096 字节的 FIFO 还没填满,DMA 就被下一次中断打断,帧自然丢。
解决:把外部中断配置为下降沿触发,并且把中断服务函数里的 DMA 启动逻辑加一个 state 判断,只允许 BUF_IDLE 状态下的缓冲区被启动。这个 state 判断在 2410 上尤其重要,因为 2410 的外部中断没有硬件消抖,GPIO 毛刺会直接触发中断,软件层不判断状态就会被毛刺带进错误路径。
5.4 A8 平台上图像偶尔错位:DMA 缓存一致性问题
现象:同一段测试程序,在 2440 上跑 1000 帧不出错,换到 A8 平台跑,每隔几十帧就出一帧花屏,花屏内容看起来像是上一帧的右上角补到当前帧的左下角。
原因:A8 处理器有较大的 L2 cache,DMA 把数据写进内存时,如果那片区域在 CPU cache 中还有旧数据,用户态读到的就是缓存里的残留。2440 的 cache 行为相对简单,这个问题不明显,A8 上就是概率性出现,而且越是连续读同一块缓冲区,命中概率越高。
解决:驱动里不要用 kmalloc 加 virt_to_phys 这种老套组合,直接用 dma_alloc_coherent 申请 DMA 缓冲区,这一接口保证分配的内存与 cache 操作是同步的。如果缓冲区和申请接口都已经正确还没好,检查平台总线的 smp 相关配置,某些 A8 平台需要把总线设为非一致性传输模式。还有一个临时排查技巧:把花屏帧与前一帧做像素差,如果碎块边界和 DMA 突发长度对齐,基本就是缓存问题,不用再查时序。
6. 一个让我少走半年弯路的调试习惯:上板前先做寄存器 dump
6.1 寄存器 dump 工具怎么写
我强烈建议在驱动里加一个 DEBUG ioctl,把 OV9650 全部寄存器读出来打印到内核日志,这也是我接手任何一颗新传感器时做的第一件事。原因很简单:很多所谓「调好的初始化序列」在另一块板子上完全不是默认状态,可能是上一个调试者遗留了半配置状态。寄存器 dump 能告诉你传感器现在到底在什么状态,而不是猜。实现上也简单,在 ioctl 里循环调用 ov9650_sccb_read,把 0x00-0x80 的寄存器值打包到用户缓冲区,测试程序用 hexdump 格式打印。这个工具看起来不起眼,但在排查「为什么同一份配置两块板子效果不一样」这类问题上,回报极高。
6.2 能继续往下走的三个优化方向
第一个方向是帧率优化,把 SXGA 降到 VGA,再调节 0x3a 寄存器把曝光和帧率解耦,15fps 提到 30fps 基本没有成本,但要注意 PCLK 分频也要同步调整,否则输出带宽超限。第二个方向是自动曝光和自动白平衡的参数收敛,OV9650 的 AEC/AGB 收敛算法对场景变化很敏感,可以在驱动里暴露亮度阈值,让应用层根据图像平均亮度动态修改目标值。第三个方向是往后兼容的封装,把字符设备驱动加上 videobuf2 适配层,往 V4L2 框架靠,这样上层可以直接用 OpenCV 打开 /dev/video0,而不用维护私有的测试程序。我自己早年因为贪快一直用私有接口,后来接算法库时吃了不少后悔药,所以现在哪怕是内部项目,也会在驱动稳定后尽快补一层 V4L2 适配。
最后说个习惯:每次调完一块板子,我会把最终能用的寄存器序列单独存成一份头文件,并在文件头写上板名、晶振频率、PCLK 实测值和当时的环境光照条件。后来遇到同型号新板子时,先按这份头文件初始化,大多数时候一次过,省下的时间远超当时写注释的几分钟。这也是这篇驱动笔记希望传达的核心——OV9650 这类老传感器没有高深的技术门槛,真正值钱的是把每次踩坑后的稳定配置沉淀下来。希望帮到你。
本文还有配套的精品资源,点击获取