ARTICLE DETAIL

资讯详情

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

OV9650摄像头驱动开发:2440到A8平台移植与排错实战

OV9650摄像头驱动开发:2440到A8平台移植与排错实战 简介一套基于 V4L2 架构自行移植的 ov9650 摄像头驱动及配套测试程序面向嵌入式 Linux 驱动开发者和 ARM 平台项目二次开发人员。驱动参考已有代码完成移植可完整支持 tiny210 开发板也能通过少量修改适配 2410、6410 等平台及不同版本内核硬件连接方式一致的其他厂商 210 板卡同样可用由于基本不涉及摄像头控制寄存器操作平台间迁移难度被明显降低。资源共 288 个文件rar 压缩包约 2.29MB核心包括 .c/.h/Makefile/Kconfig 等驱动源码与编译配置另有测试程序相关脚本、图片以及较多 js/css/html 等辅助或缓存文件。测试程序需要 OpenCV 与 Qt 4.7 支持可直接验证摄像头采集、显示等主要流程。已有 486 人学习适合正在处理 ARM Linux 摄像头驱动移植、V4L2 应用集成或不同内核版本适配的开发人员参考项目中的实测调试经验也能帮助减少常见排错时间。1. 为什么在2440和A8上还要自己写OV9650摄像头驱动能把OV9650这颗130万像素CMOS传感器在2410、2440、Cortex-A8上稳定出图不是插上摄像头、选个分辨率就完事。常见现象是2440开发板上一模一样的模组能出图换成A8平台后整帧是绿色或者内核自带的驱动能跑起来但一抓帧全是坏行。原因大多出在OV9650的SCCB时序、初始化寄存器窗口以及三个平台之间CAMIF和FIMC主机接口的差异。这篇笔记围绕我自己写的OV9650摄像头驱动加配套测试程序展开从sensor初始化、V4L2抓帧到平台移植和排错代码可以直接抄到你的工程里适合正在调2440或Cortex-A8板卡、又被摄像头折腾到怀疑人生的嵌入式工程师。2. OV9650驱动的SCCB初始化从寄存器表写到V4L2子设备OV9650的寄存器控制接口叫SCCB英文全称是Serial Camera Control Bus本质上是I2C的一种变体。很多人在2440上直接用Linux的i2c子系统去读写发现偶尔读回0xFF以为是模组坏了。其实SCCB的读时序和标准I2C不完全一样OV9650在第9个时钟周期里只给一个dont care位不产生真正的ACK某些i2c控制器在这种半握手状态会直接罢休。我习惯的做法是GPIO模拟SCCB代码量不大而且换到A8平台后不依赖具体i2c控制器出问题好排查。2.1 SCCB读写时序为什么不能用普通I2C读寄存器OV9650的SCCB设备地址是0x308位地址写地址0x30读地址0x31。两线模式下SIO_C是时钟SIO_D是数据。读一个寄存器的过程是START、发写地址、发寄存器号、STOP、START、发读地址、读8位数据、第9个时钟、STOP。关键就在第9个时钟OV9650在这一拍释放SIO_D主控要把它当成无效位跳过如果复用标准i2c-core的读操作很多驱动会在这一拍等待ACK然后报错重试。下面是我在驱动里实际使用的GPIO模拟读写函数读操作特别处理了那第9个时钟周期static void sccb_start(struct ov9650_sccb *bus) { gpio_set_value(bus-sio_d, 1); gpio_set_value(bus-sio_c, 1); sccb_delay(); gpio_set_value(bus-sio_d, 0); sccb_delay(); gpio_set_value(bus-sio_c, 0); sccb_delay(); } static void sccb_stop(struct ov9650_sccb *bus) { gpio_set_value(bus-sio_d, 0); gpio_set_value(bus-sio_c, 1); sccb_delay(); gpio_set_value(bus-sio_d, 1); sccb_delay(); } static u8 sccb_read_byte(struct ov9650_sccb *bus) { int i; u8 val 0; gpio_direction_input(bus-sio_d); for (i 7; i 0; i--) { gpio_set_value(bus-sio_c, 1); sccb_delay(); val | (gpio_get_value(bus-sio_d) ? 1 : 0) i; gpio_set_value(bus-sio_c, 0); sccb_delay(); } /* 第9个时钟OV9650不会拉低ACK只把总线释放主控读到一个无效位 */ gpio_set_value(bus-sio_c, 1); sccb_delay(); gpio_set_value(bus-sio_c, 0); sccb_delay(); gpio_direction_output(bus-sio_d, 1); return val; }这段代码里sccb_delay()的取值很关键。2440的GPIO翻转速度没A8快同样的延时函数在2440上跑出来的SCCB时钟可能是120kHz换到A8上可能飙到400kHz以上。我一般会先量一下SIO_C的实际频率把delay调到时钟频率稳定在100kHz到200kHz之间OV9650对这个范围内都能接受。读和写共用一个模拟层写操作不需要第9个周期但读操作漏掉那一拍就会读到0xFF或者一个不稳定值这是最常见的第一处坑。2.2 上电时序和初始化寄存器表OV9650对供电时序不敏感但对时钟和复位顺序有要求。最稳的顺序是先给XCLK输入时钟再拉低PWDN延时1ms然后给RESET一个低脉冲拉高后等5ms再开始写寄存器。如果反过来先复位后给时钟寄存器写进去也可能不生效表现出来就是软件复位后读0x12还是0xFF。static int ov9650_power_on(struct ov9650_dev *sensor) { /* 先有XCLK再拉低PWDN最后复位 */ sensor-pdata-ops-enable_xclk(sensor, sensor-pdata-xclk_freq); gpio_set_value(sensor-pdata-gpio_pwdn, 0); mdelay(1); gpio_set_value(sensor-pdata-gpio_reset, 0); mdelay(1); gpio_set_value(sensor-pdata-gpio_reset, 1); mdelay(5); return 0; }初始化寄存器我维护成一张表驱动probe时逐条写入。下面是SXGA输出、YUV422格式的公共配置具体窗口值要按模组手册里的HSTART、HSTOP、VSTART、VSTOP微调寄存器作用常用值说明0x12COM7软复位/输出格式0x80复位0x00恢复复位后要重写0x11CLKRC内部时钟分频0x80决定PCLK频率0x32HREF/VREF窗口使能0xB6多数模组默认0x17-0x1A水平/垂直采样窗口按模组手册出图像块时先查这里0x2A-0x2D输出分辨率缩放0x20/0x00等SXGA下关闭缩放把这表写进代码里就是这样static const struct ov9650_regval ov9650_sxga_yuv422[] { {0x12, 0x80}, /* 软复位 */ {0x12, 0x00}, /* 恢复运行YUV422输出 */ {0x11, 0x80}, /* 使能内部PLL输入24MHzPCLK按手册配置 */ {0x32, 0xB6}, /* HREF和VREF窗口开启 */ {0x17, 0x26}, {0x18, 0xA6}, /* 水平窗口以模组手册为准 */ {0x19, 0x07}, {0x1A, 0xF3}, /* 垂直窗口以模组手册为准 */ };写表时注意一个细节0x120x80软复位之后OV9650会有一小段内部自复位时间至少要延时5ms再继续写。如果批量写入时不停顿后面的寄存器可能丢第一个表现是画面有图但颜色错乱。我在s_stream回调里每次启动都执行完整初始化表而不是只在probe时初始化一次因为有些2440休眠唤醒后sensor配置会被清掉重新采集前必须再写一遍。3. 把驱动从2410/2440搬上A8分离主机接口与平台差异OV9650本身是标准并行接口的sensor输出8位YUV或RGB数据。但2410、2440、A8三个平台的主机接口不一样2410的CAMIF只支持BT.6012440的CAMIF支持BT.601和BT.656A8平台用的是FIMC支持格式更多、DMA能力更强。写普通字符设备驱动可以跑通一个平台但要一套代码全跑通必须把平台相关的东西从sensor驱动里剥出去。3.1 三个平台到底差在哪CAMIF和FIMC对外的接口都是ITU-R BT.601那样的8位并行总线接OV9650时引脚基本通用差别在三个点。第一是XCLK来源2410和2440通常从系统时钟分频出来A8平台则是单独一个时钟域。第二是DMA传输对齐2440的CAMIF对缓冲地址要求低一些FIMC对地址和宽度对齐要求更严格地址没对齐时出来的图会整帧绿色。第三是接口控制器的寄存器不同2440写在regs-camif.h里A8的FIMC寄存器完全是另一套想在驱动里用一堆#ifdef包住两个平台的寄存器操作代码会很难维护。我的做法是定义一组主机操作钩子sensor驱动只关心“主机接口要开始收了”这件事具体怎么配CAMIF还是FIMC由各自平台代码去实现。这样OV9650的初始化、SCCB、开机自检逻辑全部复用平台差异被关在接口后面。struct ov9650_host_ops { int (*enable_xclk)(struct ov9650_dev *sensor, int freq); void (*disable_xclk)(struct ov9650_dev *sensor); int (*start_capture)(struct ov9650_dev *sensor); void (*stop_capture)(struct ov9650_dev *sensor); }; struct ov9650_platform_data { struct ov9650_sccb sccb; unsigned int gpio_pwdn; unsigned int gpio_reset; int xclk_freq; bool output_yuv422; struct ov9650_host_ops *ops; };sensor驱动里只调用pdata-ops比如在s_stream里static int ov9650_s_stream(struct v4l2_subdev *sd, int enable) { struct ov9650_dev *sensor to_ov9650(sd); if (!enable) return sensor-pdata-ops-stop_capture(sensor); ov9650_power_on(sensor); ov9650_write_regs(sensor, ov9650_sxga_yuv422, ARRAY_SIZE(ov9650_sxga_yuv422)); return sensor-pdata-ops-start_capture(sensor); }这里start_capture对2440来说要做三件事配置CAMIF的源格式为YCbCr422、8位输入设置DMA两个缓冲区的地址打开CAMIF中断。对A8的FIMC来说除了同样的DMA设置还要配置FIMC的输入源为摄像头接口并且把scaler关闭或设成1:1因为OV9650已经输出SXGA不需要FIMC再缩放。3.2 2440用platform_dataA8设备树接法2440上跑的内核版本通常比较老不支持设备树驱动注册用platform_device结构体。A8平台的内核一般都有完整设备树支持驱动要同时兼容两套注册方式。我的做法是在probe函数里先用解析设备树的方式拿GPIO和时钟如果拿不到就退回platform_datastatic int ov9650_probe(struct platform_device *pdev) { struct ov9650_platform_data *pdata dev_get_platdata(pdev-dev); if (!pdata) { pdata devm_kzalloc(pdev-dev, sizeof(*pdata), GFP_KERNEL); pdata-gpio_pwdn of_get_named_gpio(pdev-dev.of_node, pwdn-gpio, 0); pdata-gpio_reset of_get_named_gpio(pdev-dev.of_node, reset-gpio, 0); pdata-xclk_freq 24000000; pdata-ops fimc_a8_ops; } ... }设备树里自然就是这样ov9650: sensor30 { compatible ovti,ov9650; reg 0x30; reset-gpio gpk0 0 GPIO_ACTIVE_LOW; pwdn-gpio gpk0 1 GPIO_ACTIVE_HIGH; };有个细节值得注意设备树里reg地址0x30对2440的老式platform_device却只认dev.platform_data里的struct两者经常同时存在。我在驱动里特意让设备树解析优先因为同一个OV9650驱动挂到subdev前必须先把irq和时钟准备好否则后面CAMIF中断来了驱动还在拿platform_data里的NULL指针。4. 测试程序这么写V4L2抓帧与YUV422验证的最小实现驱动写完只是第一步真正能证明“这个驱动能用”的是配套测试程序。我在2440和A8上都用同一套V4L2程序逻辑非常简单打开video设备、设置格式YUYV、申请mmap缓冲区、streamon、DQBUF取一帧、处理完后QBUF还回去。整个过程不依赖任何第三方库就一个C文件交叉编译后扔到板子上就能跑。4.1 V4L2抓帧主流程老内核版本上V4L2 API基本稳定下面这套在2.6.28到4.19都能编译通过。打开设备后先查询能力然后设置1280x1024、YUYV格式。这里有个参数要注意OV9650的YCbCr输出顺序是Y0 Cb Y1 Cr对应V4L2里的V4L2_PIX_FMT_YUYV不要写成UYVY否则图像整帧偏色且看起来像坏道。struct v4l2_format fmt {0}; fd open(/dev/video0, O_RDWR); if (fd 0) { perror(open /dev/video0); return -1; } fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width 1280; fmt.fmt.pix.height 1024; fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field V4L2_FIELD_NONE; if (ioctl(fd, VIDIOC_S_FMT, fmt) 0) { perror(VIDIOC_S_FMT); close(fd); return -1; }设置完格式后申请4个mmap缓冲区。为什么用4个而不是2个驱动里的DMA队列在CAMIF或FIMC双缓冲机制下至少需要两个缓冲区轮转但4个能让DQBUF不阻塞得太明显丢帧率更低。申请和映射代码如下struct v4l2_requestbuffers req {0}; req.count 4; req.type V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_REQBUFS, req) 0) { perror(VIDIOC_REQBUFS); return -1; } for (i 0; i 4; i) { struct v4l2_buffer buf {0}; buf.type req.type; buf.memory V4L2_MEMORY_MMAP; buf.index i; if (ioctl(fd, VIDIOC_QUERYBUF, buf) 0) return -1; buffers[i].length buf.length; buffers[i].start mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start MAP_FAILED) return -1; ioctl(fd, VIDIOC_QBUF, buf); }映射完成后把4个缓冲区全部QBUF入队然后streamon。这一套动作里最容易出问题的不是代码而是2440上某些内核版本把CAMIF的crop区域默认设置成了320x240S_FMT改到1280x1024后DQBUF拿到的bytesused可能还是640x480的数据长度。所以在测试程序开头打印一下V4L2实际返回的width和height如果和设置值不一致多半是CAMIF向上报告格式没同步需要检查驱动里的resizer配置。4.2 采集一帧并保存成RAW文件拿到一帧数据后不要急着转RGB。调试驱动阶段最好先直接保存原始YUYV数据用PC上的YUV播放器打开。很多图像问题在YUYV里一眼就能看出来花屏会有规则的竖条信号弱会出现模糊的亮带。如果要判断有没有正常曝光只需要统计Y分量平均值static void process_frame(void *buf, int len) { unsigned char *p buf; unsigned long y_sum 0; int i; for (i 0; i len; i 2) y_sum p[i]; printf(frame len%d, avgY%lu\n, len, y_sum / (len / 2)); }把一帧存成文件时注意不要直接存DQBUF拿到的缓冲因为驱动可能把DMA缓冲区凑成4096的整数倍尾部带了填充字节。保存时用bytesused截取并且跳过第一帧。第一帧往往只有半帧数据因为sensor刚从寄存器配置切过来VSYNC还没对齐。我在测试程序里会循环DQBUF两次以上第一帧丢弃从第二帧开始统计和落盘for (i 0; i 2; i) { struct v4l2_buffer buf {0}; buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory V4L2_MEMORY_MMAP; select(fd 1, fds, NULL, NULL, tv); if (ioctl(fd, VIDIOC_DQBUF, buf) 0) break; if (i 1) { process_frame(buffers[buf.index].start, buf.bytesused); write(raw_fd, buffers[buf.index].start, buf.bytesused); } ioctl(fd, VIDIOC_QBUF, buf); }打印出来的avgY值特别管用。纯黑画面时avgY在16左右正常室内光照下拍摄一张白纸或桌面avgY在100到160之间。如果avgY接近255说明曝光过度sensor的AEC没有收敛如果avgY接近0多半是PCLK配置不对CAMIF没有真正采到有效像素。这个指标比肉眼看LCD可靠得多尤其是2440板子没有显示输出的时候。5. OV9650摄像头驱动避坑五个让画面翻车的现场记录这个驱动我前后在2440、S5PV210上各调过一轮每次都是从“能出图”到“出图稳定”之间反复横跳。下面的问题都是亲自踩过的有两条还想过放弃。5.1 寄存器读回全是0xFF上电顺序没按SCCB规矩来现象驱动里读OV9650的Dev ID连续读十次全是0xFF。一开始以为是GPIO模拟时序不对用示波器量SIO_C和SIO_D波形正常地址也发了但sensor就是不回应。原因板子上电后先给sensor的RESET拉了高电平之后才初始化GPIO。OV9650在复位期间SCCB接口是无效的等驱动开始第一条SCCB事务时sensor还停在复位状态。解决在probe开始处强制把PWDN拉低、RESET拉低延时1ms再拉高RESET。并且把上电顺序代码放进驱动而不是依赖bootloader初始化GPIO。修完后再读Dev ID一次就拿到0x96。5.2 整屏绿色RGB通道对不上现象2440板子上出图了但画面是完全的绿色看不到实际景物偶尔还有红色和蓝色的微条纹。原因CAMIF把OV9650输出的YCbCr顺序理解成了另一种422排列。OV9650输出顺序是Y/Cb/Y/Cr如果CAMIF寄存器配置成UYVY画面就是绿色或者偏色的。解决检查CAMIF相关寄存器里源格式的设置0440上通常要设置成YCbCr422标准同时确保OV9650那边没有开BT.656输出。两个地方一旦有一处不对就很容易出现这种“能采到数据但颜色全错”的怪画面。5.3 中断每帧都来但缓冲里全是零现象CAMIF中断每33ms触发一次DMA搬完数据的回调也正常但通过mmap拿到的缓冲区内容全是0。开始怀疑是sensor没出数据拿示波器量PCLK发现频率稳定。原因2440的CAMIF对DMA地址有对齐要求我把用户态的mmap缓冲地址当成了物理地址填给DMA但很多内核配置下mmap返回的是虚拟地址物理地址需要通过virt_to_phys或sg_table取。地址错位后DMA要么没启动要么被异常终止中断虽然到数据没进内存。解决在s_stream里不要直接拿mmap的虚拟地址设置DMA改用VIDIOC_REQBUFS得到的物理地址或总线地址。A8的FIMC对16字节对齐更敏感22440对4字节对齐就行统一按16字节对齐处理最稳。5.4 画面有斜纹和向右偏移的图像块现象图像大部分区域是正常的但上部有一条大约几十像素高的斜纹带颜色明显拖影像sensor窗口没对齐。原因寄存器表里的HSTART、HSTOP、VSTART、VSTOP值是从网上抄的不是这个具体模组的参数。OV9650的感光窗口各模组因为镜头尺寸不一样默认值并不完全相同霍尔区出现偏移时sensor采到的起始行不对。解决按模组商提供的寄存器配置重新填写区间。没有模组手册时先把窗口寄存器设成数据手册里最大范围确认出图后按偏移量逐步修正直到斜纹消失。这个只能慢慢试没有捷径。5.5 2440上正常A8上帧率直接砍半现象同一块OV9650模组2440上15fps稳定S5PV210上只能跑到7fps左右而且图像偶尔出现上下滚动的水平条纹。原因两个平台的XCLK都是24MHz但A8平台FIMC的时钟链路里多一级分频导致实际送到sensor的PCLK比2440上更低。看起来fps变低是DMA问题实际上是sensor内部PLL配置没有随主机时钟链路变化。解决在A8平台把XCLK调到24MHz然后重新检查OV9650的0x11寄存器。如果sensor手册里允许更快的扫描频率就把内部PLL倍率调高一档让PCLK靠近30MHz。改完后帧率恢复到14fps水平条纹也消失了。6. 进阶校验用时间戳算实际帧率用直方图找坏点测试程序能抓出RAW图之后很多人就停了剩下交给眼睛看。我觉得还差一步把“看得到图”变成“能证明图是对的”。我在测试程序里加了两个小工具一个是帧率统计一个是Y分量直方图都是不到30行代码的事却能在A8平台上快速定位驱动和sensor之间的不匹配。帧率统计不靠sleep估算而是记录DQBUF之间的系统时间差。V4L2驱动会在每个缓冲里打上时间戳但老内核对这个时间戳的支持时好时坏我直接自己测更踏实struct timespec t1, t2; double diff, fps; clock_gettime(CLOCK_MONOTONIC, t1); /* 连续DQBUF 30帧 */ clock_gettime(CLOCK_MONOTONIC, t2); diff (t2.tv_sec - t1.tv_sec) (t2.tv_nsec - t1.tv_nsec) / 1e9; fps 29.0 / diff; printf(average fps %.2f\n, fps);OV9650配置成SXGA时如果寄存器设置正确帧率应该稳定在15fps左右。测出来的值如果是7fps基本就是PCLK配置问题或FIMC丢帧不是测试程序慢。Y分量直方图用来抓“花屏”和“坏点”。一个正常画面的直方图是连续分布过曝画面会在255附近出现一个峰坏点固定像素会在某个灰阶上持续出现尖峰unsigned int hist[256] {0}; unsigned char *p buf; for (i 0; i len; i 2) hist[p[i]]; printf(hist Y0%u Y16%u Y128%u Y255%u\n, hist[0], hist[16], hist[128], hist[255]);Y0如果高得离谱说明画面被钳黑多半是VSYNC没对齐Y255高说明过曝需要查AEC配置。这两个指标配合可以快速判断是sensor问题还是DMA问题。我的习惯是拿到一块新板子先不开显示通道用这套测试程序把帧率、平均亮度、直方图统计打出来看到数值合理了再往上叠显示。这套习惯让我在A8上减少了至少半天查错时间希望帮到你。本文还有配套的精品资源点击获取
返回列表