简介:一套完整移植的OV9650摄像头驱动与配套测试程序,兼容V4L2架构,适配tiny210开发板,对2410、2440、6410及A8等ARM平台只需少量调整即可复用。压缩包共288个文件、约2.29MB,内含C源码、头文件、Makefile、Kconfig等驱动工程文件,也打包了PDF/DOCX说明文档和测试相关图片素材,目录结构便于按模块查阅。测试程序基于OpenCV与Qt4.7编写,覆盖图像采集与显示流程,可结合实际板卡快速验证摄像头工作状态;驱动设计上不直接操作ARM摄像头控制寄存器,降低了对内核版本和硬件的依赖,便于二次开发。内容来自作者实际项目,移植与排错过程约两周,完整保留了V4L2框架接入与调试思路;从平台适配到测试验证的链路清晰,适合有一定嵌入式基础的开发者阅读源码、理解驱动与应用层的协作方式。已有487人学习/下载。
1. 从三块开发板的花屏说起:OV9650摄像头驱动到底做了什么
同一份OV9650摄像头驱动源码,我在2410上调通了,烧到2440上花屏,再挪到A8平台直接黑屏。三块板子的差异不是换个编译工具链就能糊弄过去的,传感器初始化时序、Camera接口寄存器、DMA缓存一致性都得重新过一遍。这个项目提供的就是一整套OV9650驱动加测试程序,覆盖SCCB读写下寄存器、CAMIF采集配置、帧缓冲管理、应用层mmap拉帧到落盘BMP的完整链路。底层是字符设备驱动加私有ioctl的老派做法,跑通之后对理解V4L2和现代摄像头驱动也有直接帮助。适合做嵌入式Linux驱动入门、给老平台设备接摄像头,或者想抄一份能出图的摄像头驱动框架的人。
2. 驱动架构拆解:从SCCB控制到DMA搬帧的完整链路
2.1 硬件连接与平台差异:OV9650怎么挂在三款处理器上
OV9650对外信号分两头:控制侧是SCCB两线,SIO_C当SCL用,SIO_D当SDA用,直接接处理器的I2C总线;数据侧是一根PCLK、一根HREF、一根VSYNC,外加D0到D9十根数据线。工程里实际只用到8根,把D0到D7和CAMIF的YCbCr输入对接,D8、D9悬空就行。PCLK是传感器由XVCLK内部分频产生的,帧率能不能上去,CLKRC寄存器说了算。
SCCB总线在基本读写操作上和I2C兼容,多数驱动直接拿i2c_transfer封装,省得再写一套bit-bang。真正的差异在处理器平台的Camera接口侧:2410的CAMIF是初代设计,寄存器少、只有单DMA通道,跑VGA分辨率够用;2440补了双DMA和更宽的时钟分频范围;A8平台的CAMIF寄存器布局重排,DMA描述符也可能从简单的首地址加大小变成链表式。移植时不能只改寄存器地址,数据通路的描述方式要跟着SoC手册换。
| 平台 | CAMIF形态 | DMA方式 | 时钟来源 |
|---|---|---|---|
| S3C2410 | 初代CAMIF,寄存器少 | 单DMA,地址+大小 | HCLK分频 |
| S3C2440 | 增强CAMIF,双通道 | 双DMA,Y/C分离搬运 | HCLK分频,范围更宽 |
| A8平台 | 按各SoC手册实现 | 描述符链表,可能带UBC | PLL配置,需查手册 |
硬件接线上有一个容易翻车的点:VSYNC、HREF、PCLK三根线的极性不要拍脑袋定。很多板子默认高有效,但OV9650在不同输出模式下极性定义可能不同。我一般会在驱动里把三个极性子段做成参数,先按传感器手册的默认值,调不通就翻转对比一次。这个动作看起来小,实际上花屏问题里一半以上是它引起的。
2.2 驱动骨架:字符设备加私有ioctl,还是包成V4L2子设备
老平台工程很少直接上完整V4L2框架,一方面是内核版本老、V4L2子设备机制不成熟,另一方面是测试程序只需要一个能出图的通道,私有ioctl反而更直白。这个项目走的是字符设备驱动加私有ioctl的风格,file_operations里有open、release、ioctl、mmap、read。好处是把传感器初始化、帧采集、格式切换暴露成几个明确的命令,调试时一个ioctl就能做完整操作;代价是应用层只能针对这个驱动写,换平台换驱动都得改。
我把驱动按职责拆成三块文件来读:ov9650_sensor.c管SCCB读写和寄存器序列,camif_platform.c管CAMIF和DMA的寄存器操作,ov9650_core.c串起file_operations和帧管理。这样的分法跨平台迁移时只需要换camif_platform.c,传感器部分可以原样保留。在2410、2440、A8之间挪洞,这张分层图就是最小的改造边界。
2.3 数据通路:从PCLK到SDRAM帧缓冲的一站式流程
帧数据流的路径是:OV9650按PCLK节奏逐像素输出YCbCr422,CAMIF收到行同步和帧同步信号后把数据推进FIFO,FIFO达到水位就触发DMA搬运到SDRAM中的帧缓冲。驱动初始化时把dma_addr配给CAMIF,之后CPU不参与像素拷贝,只在帧结束中断里切换缓冲归属并更新帧序号。
这里有个老平台经典坑:DMA缓冲如果映射成cacheable,CPU读到的图像会一半新一半旧。所以驱动要保证这块内存走coherent映射或者显式标记uncached,A8平台尤其严格。测试程序直接mmap这块内存读取,省掉read系统调用的拷贝开销,VGA分辨率下CPU占用很低,这也是老工程宁可写私有mmap也不走read读用户态的原因。
3. 核心模块实现:初始化寄存器表、DMA配置与私有ioctl命令集
3.1 传感器初始化:一张写满寄存器的表
OV9650的初始化等价于往几十个寄存器里写值。驱动里维护一张静态表,启动时按顺序刷进去,下面的代码是简化后的关键片段:
static const struct ov9650_reg ov9650_init_table[] = { { 0x12, 0x80 }, /* COM7 软件复位 */ { 0x11, 0x00 }, /* CLKRC 时钟分频 1:1 */ { 0x12, 0x00 }, /* COM7 切换YUV输出 */ { 0x1e, 0x00 }, /* HREF 行参考配置 */ { 0x1f, 0x50 }, /* 输出尺寸相关 */ { 0x20, 0x00 }, { 0x21, 0x00 }, { 0x13, 0x20 }, /* COM8 AGC/AEC使能 */ { 0x14, 0x38 }, /* COM9 增益上限 */ { 0x2d, 0x00 }, /* ALC 自动电平控制 */ { 0x2b, 0x00 }, /* HRef 窗口偏移 */ { 0x17, 0x12 }, /* HSTART 采样窗口起始 */ { 0x18, 0x02 }, /* HSTOP 采样窗口结束 */ { 0x19, 0x02 }, /* VSTART */ { 0x1a, 0x7b }, /* VSTOP */ { 0x2f, 0x40 }, /* COM13 边沿/输出控制 */ };写入函数按SCCB协议逐字节发送:SIO_C拉高,先送7位器件地址加读写位,再送寄存器地址,最后送要写入的值。每个寄存器都要走完整时序。
参数说明:COM7写0x80是软件复位,芯片内部复位需要几十毫秒,复位后必须先等再写0x00切到YUV输出;CLKRC的分频值直接决定PCLK频率,VGA分辨率下常用1:1,SXGA需要预分频否则像素时钟超限。COM8的bit5是AEC使能,bit2是AGC使能,调试时发现画面亮度跳来跳去,多半是这两个位没按预期设置。HSTART、HSTOP、VSTART、VSTOP四个窗口寄存器共同决定有效图像区域,改错会出现采样窗口偏移,表现出来就是画面整体剪切、有斜向断层。
上电时序也是初始化的一部分:供电稳定后先拉复位引脚为低电平至少1ms,再释放,之后通过SCCB写软复位命令。常见问题是复位引脚接了GPIO但GPIO初始化晚于传感器供电,导致芯片在上电窗口内工作在不稳定状态,表现是SCCB能响应命令,但输出信号全是噪点。
3.2 CAMIF与DMA:把像素时钟变成内存地址
camif_start负责接管PCLK、配置源格式和DMA地址,核心代码如下:
static int camif_start(struct ov9650_dev *dev) { void __iomem *base = dev->io_base; /* 写入源图像宽高,低16位宽,高16位高 */ writel(dev->fmt.width | (dev->fmt.height << 16), base + S3C_CISRCFMT); /* 清窗口偏移,保证数据从帧起点写起 */ writel(0x01, base + S3C_CIWDOFST); /* Y分量双缓冲地址,DMA在两者间交替写入 */ writel(dev->dma_addr, base + S3C_CICOYSA1); writel(dev->dma_addr + dev->buf_size, base + S3C_CICOYSA2); /* 时钟极性控制:PCLK/HREF/VSYNC各占一位 */ writel(dev->pclk_polarity | dev->href_polarity | dev->vsync_polarity, base + S3C_CIGCTRL); /* 使能帧结束中断,开始采集 */ writel(0x1, base + S3C_CIINTEN); writel(0x1, base + S3C_CISRCFMT + 0x04); return 0; }逻辑说明:宽高按CAMIF要求的格式写入源格式寄存器,YSA1和YSA2构成双缓冲底衬,一个被DMA写入时,另一个可以安全地被CPU读取。CIGCTRL里三个极性位就是2.1节提到的那根容易翻车的线,配置错会出现横条纹。帧结束中断里做一次缓冲切换,把当前帧标记为ready,应用通过ioctl取走帧序号。
参数说明:dev->dma_addr来自dma_alloc_coherent,这片内存物理连续并且不会走cache,DMA写进去的数据应用层直接就能读到,不会出现缓存陈旧问题。buf_size等于width乘height乘2,YUV422一个像素占两个字节。改分辨率后必须重新分配DMA缓冲,否则DMA写穿到别处,画面会花屏甚至内核崩溃,这是黑匣子问题最常见的来源之一。
3.3 控制接口:私有ioctl命令集与实现
驱动对外暴露四个核心命令,头文件里的定义如下:
#define OV9650_IOC_MAGIC 'O' struct ov9650_fmt { unsigned int width; unsigned int height; unsigned int format; /* 0=YUV422 1=RGB565 */ }; #define OV9650_IOC_INIT _IO(OV9650_IOC_MAGIC, 0) #define OV9650_IOC_SET_FMT _IOW(OV9650_IOC_MAGIC, 1, struct ov9650_fmt) #define OV9650_IOC_START _IO(OV9650_IOC_MAGIC, 2) #define OV9650_IOC_STOP _IO(OV9650_IOC_MAGIC, 3) #define OV9650_IOC_GET_FRAME _IOR(OV9650_IOC_MAGIC, 4, unsigned int)ioctl实现里按switch分派:SET_FMT做三件事,先校验宽高是否支持,再重新分配DMA缓冲,最后把格式配置写进传感器;GET_FRAME不把整帧数据拷给用户,只返回当前帧序号,应用层拿mmap地址直接读,十帧采集只需要十次轻量ioctl,没有大块拷贝。
mmap的实现是标准的三行,关键是关闭cache属性:
static int ov9650_mmap(struct file *file, struct vm_area_struct *vma) { struct ov9650_dev *dev = file->private_data; return remap_pfn_range(vma, vma->vm_start, virt_to_phys(dev->virt_addr) >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot); }vma->vm_page_prot必须带上禁止cache的属性位,否则DMA写完了、CPU读到的还是旧内容。这个细节在A8平台上是必踩的坑,很多从2410迁过来的代码这里没改,表现就是黑屏或者花图。
4. 测试程序实战:命令行拉帧到BMP落盘
4.1 测试程序的结构与命令行参数
测试程序只有三百来行,主流程是经典的五步:打开设备、IOCTL初始化、设置格式、mmap映射、循环取帧。命令行用getopt支持-w、-h、-o、-c四个参数,分别控制宽、高、输出文件名和采集帧数,默认值是640x480、output.bmp、1帧。这样脚本批量测试不同分辨率很方便,换一种曝光参数也不需要重新编译。
4.2 关键代码:打开设备、设置格式与mmap循环取帧
int main(int argc, char *argv[]) { int ret = 0, seq = 0; struct ov9650_fmt fmt; unsigned char *buf; int fd; fd = open("/dev/ov9650", O_RDWR | O_SYNC); if (fd < 0) { perror("open /dev/ov9650"); return -1; } ret = ioctl(fd, OV9650_IOC_INIT); if (ret < 0) { perror("IOC_INIT"); goto out; } fmt.width = 640; fmt.height = 480; fmt.format = OV9650_FMT_YUV422; ret = ioctl(fd, OV9650_IOC_SET_FMT, &fmt); if (ret < 0) { perror("IOC_SET_FMT"); goto out; } buf = mmap(NULL, fmt.width * fmt.height * 2, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (buf == MAP_FAILED) { perror("mmap"); ret = -1; goto out; } ret = ioctl(fd, OV9650_IOC_START); if (ret < 0) { perror("IOC_START"); goto out; } while (seq < 10) { ret = ioctl(fd, OV9650_IOC_GET_FRAME, &seq); if (ret < 0) { perror("GET_FRAME"); break; } save_bmp(buf, fmt.width, fmt.height, seq); printf("frame %d saved, bytes=%d\n", seq, fmt.width * fmt.height * 2); } ioctl(fd, OV9650_IOC_STOP); out: if (buf) munmap(buf, fmt.width * fmt.height * 2); close(fd); return ret; }这是典型的按帧循环读指针模式。open带O_SYNC在老内核上能避免部分驱动里write back缓冲导致mmap数据不可见的问题,属于防患于未然。GET_FRAME返回的seq是驱动侧递增的帧序号,应用拿它刷新BMP文件名,也方便加计时逻辑统计帧间隔。
save_bmp内部把YCbCr422转成RGB888,再用BMP格式落盘。位图头54字节手工填,BMP标准要求每行4字节对齐,宽度不是4的倍数时末尾要补零。这个转换完全放在用户态做,驱动不参与色彩空间转换,调试时哪个环节出问题一眼能定位。
4.3 验证流程与预期输出
| 步骤 | 操作 | 预期现象 |
|---|---|---|
| 1 | insmod ov9650.ko | dmesg出现传感器型号识别信息 |
| 2 | ls -l /dev/ov9650 | 字符设备节点存在,权限正确 |
| 3 | ./test_ov9650 -w 640 -h 480 -c 5 | 终端打印5行frame序号 |
| 4 | 查看生成的out_0.bmp | 画面无斜切、无条纹、色彩正常 |
| 5 | 改变环境光亮度再跑 | AGC/AEC生效,亮度变化平滑 |
验证时先跑一次默认格式,确认出图后再切RGB565。如果画面花屏,优先翻转极性位重测;如果颜色整体偏色,优先查CbCr次序配置,这两个顺序基本能覆盖大部分异常。
5. 避坑手册:OV9650驱动移植调试的五个典型问题
5.1 花屏条纹:百叶窗一样的横条纹
现象:画面出现横向条纹,像拉开一半的百叶窗,每一行错位。
原因:PCLK、HREF、VSYNC三根线的极性和CAMIF配置不匹配,导致行数据错位拼接。传感器默认极性和SoC手册默认极性往往不一致。
解决:往CIGCTRL寄存器写不同极性组合,逐个测试。最有效的办法是给驱动加一个调试ioctl,运行态实时翻转极性位,不用每次改完重新编译重启。
5.2 SCCB读回全0xFF:初始化直接失败
现象:I2C读寄存器返回0xFF,写寄存器没反应,驱动挂在初始化第一步。
原因:OV9650的SCCB地址有7位和8位两种写法,8位地址是0x42,7位地址是0x21,移植代码经常把两者混用,或者适配器层又做了一次左移。
解决:先查驱动传给i2c_transfer的地址到底是几位,统一按7位地址0x21交给内核I2C框架,或者按8位0x42在传感器层写SCCB协议,两种方式选一种并保持一致。
5.3 A8平台黑屏但2410正常
现象:同样的源码在A8平台正常运行,dmesg无任何报错,但图像全黑或者全是噪点。
原因:DMA缓冲的cache一致性问题。2410平台对cache要求宽松,A8平台缓存层次更深,CPU读到的可能是旧数据。
解决:DMA缓冲必须用dma_alloc_coherent分配,不要用kmalloc;mmap时vma->vm_page_prot要清除cacheable标志。这两个位置在A8平台是硬约束。
5.4 颜色整体偏紫或偏绿
现象:画面内容正常、没有花屏,但整幅图偏紫色或者偏绿色。
原因:YCbCr的采样次序配置反了。OV9650输出序列可能是YCbYCr也可能是YCrYCb,CAMIF端解析次序如果不一致,Cb和Cr就交换了。
解决:检查驱动里0x12和0x2f的格式控制位,同时对照CAMIF寄存器里的数据次序字段,两边设置成同一个字节序即可恢复正常。
5.5 帧率远低于标称值
现象:VGA模式标称30帧,实测只有12帧左右,画面看着卡。
原因:CLKRC分频没按实际XVCLK输入频率算,PCLK太低;或者CAMIF打开了scaler缩放通道,缩放会显著拖慢帧率。
解决:按传感器输入时钟和目标帧率倒推分频值,回填0x11;确认驱动里没有开scaler,如果是配置了窗口缩放,改成关闭scaler走全尺寸输出。
6. 二次开发:从字符设备驱动迈向V4L2与双缓冲采集
如果想把这套驱动用在新平台或者新内核上,第一步不是改寄存器,而是把私有ioctl翻译成V4L2标准接口。字符设备里file_operations上的ioctl换成v4l2_file_operations,OV9650_IOC_SET_FMT对应VIDIOC_S_FMT,GET_FRAME对应DQBUF,mmap部分保留但要在video_device里注册queue。这样应用层可以直接用v4l2-ctl测试,不用再依赖自己写的测试程序。
第二步是改缓冲策略。现在的驱动是两块固定DMA缓冲,靠帧中断来回切,够用但不够稳。改成内核里常见的四缓冲队列,driver持有两个、用户持有两个,每次DQBUF后空闲缓冲立刻重新入队,帧间隔能明显收窄。改造时注意把dma_addr的分配从原来的一次性改为按缓冲索引管理,中断里只做指针交换。
第三步可以做曝光控制。OV9650的AGC、AEC寄存器支持粗调,在驱动里新增一个命令周期读取当前增益值和曝光时间,应用层根据环境亮度动态写回。人脸检测这类算法对曝光稳定性很敏感,这一步做完了画面亮度不会忽跳忽暗。
验证方法:先在同一块板上跑旧测试程序确认出图,再用v4l2-ctl --set-fmt-video抓帧做基准对比,最后跑新队列压测,观察连续采集1000帧有没有丢帧和野指针。从那以后我每次做摄像头驱动,都会先把三份手册的时钟和极性参数整理成一页对照表再动代码,剩下的调试基本是查表而不是猜。希望帮到你。
本文还有配套的精品资源,点击获取