简介:OV7725是OmniVision公司生产的高分辨率、低功耗CMOS图像传感器,广泛用于数字摄像头与手机成像模组。这份Linux驱动源码包面向嵌入式Linux开发者与驱动调试工程师,解决向系统接入OV7725摄像头并纳入V4L2视频框架的常见需求,也适合作为学习视频设备驱动的参考样例。压缩包体积仅7KB,包含2个文件:ov7725.c为驱动主体,实现设备探测(probe)、视频流初始化、I2C寄存器读写以及YUV/RGB图像格式配置;ov7725.h提供数据结构定义与函数原型,方便快速移植到不同内核或硬件平台。目前已有358人学习/下载,通过这份精简源码,读者可以系统理解CMOS传感器驱动在V4L2框架下的注册、控制和数据采集流程,掌握曝光、增益、帧率等关键参数调节方法,为后续自研摄像头驱动或图像质量优化提供起点,同时代码量不大,适合逐行分析并结合实际硬件验证,对从零开始编写类似传感器驱动具有直接参考价值。
1. 一个 ov7725.rar 引发的老 sensor 新平台移植问题
做嵌入式 Linux 摄像头项目时,你很容易在某个 BSP 压缩包里翻到ov7725.rar,里面是一颗 30 万像素 CMOS 传感器 OV7725 的驱动源码。这颗传感器本身不新,却因为成本低、功耗小,还在很多工业检测、教育机器人、门禁和医疗设备里服役。问题在于:老 BSP 给的驱动多半是基于某个 3.x 内核写的,拿到的同学往往直接往里拷贝,结果编译不过、probe 失败、/dev/video0出不来,最后连图像流都看不见。
这篇文章就是解决这条落地路径的。我会从 V4L2 驱动框架讲起,说清 OV7725 在框架里到底是 subdev 还是 video_device,然后给出把源码包塞进内核、配设备树、用 V4L2 采图的具体步骤,最后把 DVP 花屏、I2C 不通、老框架迁移这些常见坑写透。适合正在做 Linux 驱动开发、手里有 OV7725 模组、却不知道源码该往哪放的工程师;也适合想快速验证一颗老 sensor 还能不能在新的内核上出图的硬件/软件同事。
2. 在 Linux 驱动栈里找 OV7725 的位置:SCCB 之外还有 V4L2 subdev
别急着编译源码。OV7725 是颗典型的并行 DVP 摄像头,和现在常见的 MIPI-CSI 模组完全不同;它在 V4L2 框架里也不是一个独立/dev/videoX,而是挂在 camera host 下游的 subdev。先把这个位置想清楚,后面的 Kconfig、DTS、media-ctl 才不会配错。
2.1 硬件侧:OV7725 是 DVP 并行 CMOS sensor 不是 MIPI
OV7725 的分辨率是 640x480,输出格式常见为 YUV422、RGB565 和 RAW Bayer,通过一组 8 位并行数据线 D0-D7 送出。它没有 MIPI CSI-2 的差分 lane,硬件接口是 PCLK、VSYNC、HREF 加上 8 根数据线,属于 DVP(Digital Video Port)并行接口。主控 SoC 那边必须有 parallel CSI 控制器,或者把普通 GPIO 复用在 CSI 功能上,否则驱动写得再好也没有数据通路。
Configuration 总线是 SCCB,也就是 OmniVision 自己的串行控制协议,引脚叫 SIO_C 和 SIO_D,电气上和 I2C 高度相似。多数 Linux I2C controller 可以直接拿来操作,默认 7 位地址是 0x21。板上通常还会拉出 reset 和 pwdn(power down)两个 GPIO,用来做上电时序。
如果项目里已经有 ov7725 驱动源码,先打开头文件看寄存器表,你会看到大量像COM3、COM7、COM8这样的小写寄存器定义。COM7 决定输出格式,COM8 控制自动曝光和自动增益,这些寄存器最终会通过 SCCB 写入。
2.2 软件侧:v4l2 驱动框架里,OV7725 是 subdev 而不是 video_device
很多新手以为摄像头驱动就是写个字符设备驱动框架,注册file_operations,让 app 能 read。实际上现代 Linux 摄像头走了 V4L2 media controller 那套:OV7725 驱动注册的是v4l2_subdev,它只负责管理 sensor 本身;/dev/video0这个video_device由 SoC 的 CSI/ISP 驱动创建。用户空间通过open("/dev/video0")拿到帧,而 video0 的 driver 在启动 stream 时,会回调到 subdev 的s_stream,让 OV7725 真正开始输出图像。
这也是标题里 “ov7725 v4l2 驱动” 的真正含义:你需要的不是一套 V4L2 主设备驱动,而是一个能被 async subdev 框架找到的 sensor 驱动。典型的 subdev ops 长这样:
static const struct v4l2_subdev_core_ops ov7725_core_ops = { .s_power = ov7725_s_power, }; static const struct v4l2_subdev_video_ops ov7725_video_ops = { .s_stream = ov7725_s_stream, .g_frame_interval = ov7725_g_frame_interval, }; static const struct v4l2_subdev_pad_ops ov7725_pad_ops = { .init_cfg = ov7725_init_cfg, .get_fmt = ov7725_get_fmt, .set_fmt = ov7725_set_fmt, }; static const struct v4l2_subdev_ops ov7725_subdev_ops = { .core = &ov7725_core_ops, .video = &ov7725_video_ops, .pad = &ov7725_pad_ops, };这套回调里,s_power控制 sensor 上电/掉电,s_stream是拉流开关,set_fmt设置输出到 CSI 的 bus format。老内核里没有pad_ops,而是video_ops里的s_fmt和g_fmt;从 4.x 开始 media controller 重构后,基本都改走 pad ops。如果你手里的 rar 包还在用soc_camera那套soc_camera_link,就是要做框架迁移的重点对象。
2.3 先用 i2cget 读 PID/VID:SCCB 通没通一测便知
软件栈再复杂,第一步永远是确认 SCCB 能读到 sensor 的芯片 ID。OV7725 的 PID 在寄存器 0x0b,版本号在 0x0c,如果读出来 PID 是 0x77,说明 sensor 活着,驱动只是匹配或框架的问题。先用 linux 常用命令验证:
i2cdetect -y -r 1 i2cget -y 1 0x21 0x0b i2cget -y 1 0x21 0x0c-y表示跳过交互确认,-r让工具强制用 read 方式探测,兼容 SCCB;地址 1 表示是哪条 I2C 总线,具体要看你 DTS 里 sensor 挂在哪。读不到 0x77 时,不要怀疑驱动,先查上电、reset GPIO、XCLK 时钟有没有到,这步是后面所有调试的地基。
3. 把 ov7725.rar 放进内核编译:Kconfig、Makefile 和 DTS 一个都不能少
源码包解压出来以后,最忌讳的就是把整个目录拖到drivers/media/i2c/下面然后直接 make。老平台的内核目录结构、Kconfig 依赖和 DTS 语法都变过,得一步一步来。
3.1 解包后先判断驱动是哪个框架
在 Linux 下解压ov7725.rar,常用的是unrar x,如果没有装可以先装 unrar,再执行:
mkdir -p ov7725 unrar x ov7725.rar -d ov7725 grep -rn "soc_camera\|v4l2_subdev\|v4l2_async" ov7725 | head -50grep的结果能很快告诉你源码属于哪个时代。如果搜到soc_camera和soc_camera_device,这是老内核的平台驱动写法,不建议直接编进 4.19 以上的内核;如果搜到v4l2_subdev_init、v4l2_async_register_subdev,说明驱动已经接近现代框架,移植工作量小很多。
我一般还会看它有没有of_match_table。有of_match_table的驱动支持设备树匹配,没有的话通常要 i2c_board_info 或 platform_data 创建设备,这种驱动放到 DTS 平台上会很别扭,宁可改成 device tree compatible 匹配。
3.2 Kconfig 和 Makefile:让内核菜单里能选中 OV7725
确认驱动源文件是ov7725.c或ov772x.c后,把它放到当前内核的drivers/media/i2c/目录,再给 Kconfig 和 Makefile 加条目。Kconfig 片段大概是这样的:
config VIDEO_OV7725 tristate "OmniVision OV7725/OV7720 sensor support" depends on I2C && VIDEO_DEV && MEDIA_CONTROLLER help V4L2 sub-device driver for the OmniVision OV7725 CMOS image sensor. Supports 640x480 and lower resolutions via parallel DVP interface.Makefile 里加一行:
obj-$(CONFIG_VIDEO_OV7725) += ov7725.o注意,ov7725.o的拼写必须和实际.c文件名一致。如果 rar 包里的文件名是ov772x.c,这里的 object 就是ov772x.o。depends on I2C不能删,因为 sensor 驱动全靠 SCCB/I2C 读写寄存器;VIDEO_DEV是 V4L2 框架基础。老的 soc_camera 驱动还会依赖VIDEO_SOC_CAMERA,这个在 4.19 之后基本被移除,碰到就说明要迁移。
3.3 DTS:让 I2C controller 找到这颗 sensor
现代内核里,subdev 驱动和设备树节点是一一对应的。OV7725 挂在 SoC 的 I2C 总线上,同时在 CSI 端口上要把并行时序写对。常见 DTS 写法如下:
&i2c1 { ov7725: ov7725@21 { compatible = "ovti,ov7725"; reg = <0x21>; clocks = <&clk24m>; clock-names = "xclk"; assigned-clocks = <&clk24m>; assigned-clock-rates = <24000000>; pwdn-gpios = <&pio 4 13 GPIO_ACTIVE_HIGH>; reset-gpios = <&pio 4 15 GPIO_ACTIVE_LOW>; port { ov7725_ep: endpoint { remote-endpoint = <&csi0_ep>; bus-width = <8>; hsync-active = <1>; vsync-active = <1>; pclk-sample = <1>; }; }; }; }; &csi0 { status = "okay"; port { csi0_ep: endpoint { remote-endpoint = <&ov7725_ep>; }; }; };reg = <0x21>是 sensor 的 7 位 SCCB 地址。clocks给 sensor 提供 master clock OV7725 一般用 24MHz,assigned-clock-rates最好显式写上,否则 bootloader 可能把时钟设成 25MHz 或默认值,导致出图帧率不对。bus-width、hsync-active、vsync-active、pclk-sample是 DVP 时序极性,这四个参数一旦和 SoC CSI controller 不匹配,图像就会花屏。
注意:
compatible必须以源码里of_match_table的字符串为准,不同内核版本可能是ovti,ov7725,也可能是ovti,ov772x。配 DTS 前先grep -rn "compatible" drivers/media/i2c/ov7725.c确认。
如果平台没有设备树,老方式是在 board.c 里用i2c_board_info注册:
static struct i2c_board_info __initdata ov7725_info = { .type = "ov7725", .addr = 0x21, }; i2c_register_board_info(1, &ov7725_info, 1);这种写法在支持 DTS 的新内核里也能用,但会和你板子的其他 I2C 设备管理方式不一致,我建议优先 DTS,只有实在是老 BSP 才用 board_info。
4. 编译、加载和采集:把 OV7725 真正变成 /dev/videoX
Kconfig、Makefile、DTS 都改完后,进入验证闭环。先编译,再加载,用 V4L2 工具确认传感器能被 media controller 识别,最后写个最简采集程序抓一帧 YUYV 图像下来。这一章是把黑匣子变成可见数据的关键。
4.1 编译与加载:模块化比较好调试
第一次移植不要直接编进内核,先编成模块,方便改参数后单独重编。交叉编译环境下执行:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig # Device Drivers -> Multimedia support -> Sensors -> OmniVision OV7725 # 先选 <M> make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules dtbs编译通过后,把ov7725.ko拷到板子的/lib/modules/$(uname -r)/extra/,执行depmod -a,再modprobe ov7725。如果用insmod,记得按依赖顺序手动加载videodev、v4l2-async等模块,比较麻烦。
加载完看 dmesg 是最直接的:
dmesg | grep -i ov7725如果驱动 probe 成功,你会看到类似 “ov7725 1-0021: chip found ...” 或 “ov7725: probing ...” 的日志。如果看到-EPROBE_DEFER,多半是 DTS 的时钟、电源或 media link 还没准备好,先不急着骂驱动。
4.2 media-ctl 管线配置:不打通 link 是不会有图像的
现代内核里,即使 subdev 注册成功,video0 也不一定出图。你要先把 media controller 的 pipeline link 起来。先看拓扑:
media-ctl -d /dev/media0 -p输出里会列出 sensor 实体、CSI 实体和它们之间的 pad。通常 sensor 是ov7725 1-0021,pad 0 输出;CSI 端有一个 sink pad。打通 link 并设置 bus format 的命令如下:
media-ctl -d /dev/media0 -r media-ctl -d /dev/media0 -l "'ov7725 1-0021':0 -> 'csi0':0 [1]" media-ctl -d /dev/media0 -V "'ov7725 1-0021':0[fmt:UYVY8_2X8/640x480]"这里的fmt:UYVY8_2X8/640x480是 media bus format,前面的UYVY8_2X8表示 8 位并行总线,每像素 16bit,UYVY 顺序。OV7725 如果配置成 YUYV 输出,这里也可能是YUYV8_2X8,以驱动set_fmt支持的数组为准。顺序不对时,图像会出现 R/B 通道互换,这在应用层很容易误判成白平衡问题。
然后可以用 v4l2-ctl 设置主设备视频格式:
v4l2-ctl -d /dev/video0 --set-fmt-video=width=640,height=480,pixelformat=YUYV v4l2-ctl -d /dev/video0 --list-formats-extpixelformat=YUYV是用户在/dev/video0上看到的四字符码,和 media bus format 是两个维度,经常有人把这里写错。--list-formats-ext能看到驱动实际支持的像素格式和分辨率。
4.3 一个最少 mmap 采集程序:一帧 640x480 YUYV 落盘
用命令行工具验证完格式,还得确认真正能取到帧。写个最小 C 程序,用 MMAP 方式抓一帧保存成frame.yuv:
#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #include <linux/videodev2.h> int main(void) { int fd = open("/dev/video0", O_RDWR); if (fd < 0) { perror("open /dev/video0"); return -1; } struct v4l2_format fmt; memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 640; fmt.fmt.pix.height = 480; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("VIDIOC_S_FMT"); return -1; } struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); 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; } void *map[4] = {0}; for (int i = 0; i < 4; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; ioctl(fd, VIDIOC_QUERYBUF, &buf); map[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); ioctl(fd, VIDIOC_QBUF, &buf); } int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd, VIDIOC_STREAMON, &type); struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { perror("VIDIOC_DQBUF"); return -1; } FILE *fp = fopen("frame.yuv", "wb"); fwrite(map[buf.index], 1, buf.bytesused, fp); fclose(fp); ioctl(fd, VIDIOC_QBUF, &buf); ioctl(fd, VIDIOC_STREAMOFF, &type); close(fd); return 0; }这个程序有几个关键点:VIDIOC_S_FMT必须在VIDIOC_REQBUFS之前,V4L2 驱动大多在申请 buffer 时锁定格式;所有 buffer 都要VIDIOC_QBUF放回队列,再VIDIOC_STREAMON,否则驱动找不到可用的 buffer 会一直等。VIDIOC_DQBUF是阻塞调用,如果 OV7725 的 media link 没打通、CSI 没有时钟,它会一直卡在那里,这不是程序 bug,是驱动链路问题。
抓下来的frame.yuv是裸 YUYV 数据,可以用 7yuv、ffplay 或 Python + numpy 打开。看到画面颜色不对,先回 media-ctl 检查 bus format;看到完全花屏,去查 DTS 里的 hsync/vsync/pclk 极性。
5. 驱动移植与调不到的坑:I2C 不通、花屏和框架变换
OV7725 驱动本身不复杂,但老 sensor 移植到新内核的路上坑不少。下面几条是我实际踩过、也帮别人排查过的典型问题,按“现象 -> 原因 -> 解决”写出来,能省你半天时间。
5.1 i2cdetect 看到 0x21,驱动却报 chip_id 错误
现象:i2cdetect明明在 0x21 上看到了设备,但modprobe后 dmesg 显示chip found failed,或者read pid error。
原因:SCCB 读时序和标准 I2C 有细微差别,很多 I2C controller 在快速读写时会少一个 ACK 周期;另外如果 reset GPIO 极性写反,sensor 根本没从复位状态走出来,只能响应部分寄存器。最容易被忽略的是 XCLK:OV7725 的 SCCB 能响应不代表内部逻辑已起来,没有主时钟时读 ID 会随机失败。
解决:先在应用层反复读 PID 确认稳定性:
for i in $(seq 1 10); do i2cget -y 1 0x21 0x0b; done如果结果忽有忽无,优先查 24MHz 时钟和复位时序。驱动里常见做法是先拉低 reset,延时 20ms,再拉高,再延时 20ms:
gpiod_set_value_cansleep(ov7725->reset_gpio, 0); usleep_range(20000, 50000); gpiod_set_value_cansleep(ov7725->reset_gpio, 1); usleep_range(20000, 50000);有些模组把 PWDN 引脚默认拉高,sensor 一直处于 power down,也会出现“probe 时失败、但 i2cdetect 有设备”的假象。查硬件原理图时,把 reset 和 pwdn 两个 GPIO 的状态都量一遍。
5.2 MCLK 没起来或时钟频率不对:probe 返回 -EPROBE_DEFER
现象:驱动日志没有任何 ov7725 的错误,只有probe of ov7725 failed with error -517,设备树里 sensor 节点看起来没问题。
原因:-517就是-EPROBE_DEFER,通常是驱动请求xclk时钟时,时钟 provider 还没就绪,或者assigned-clock-rates没生效。OV7725 对 master clock 宽容度还行,但频率突变会直接影响帧率。
解决:在 DTS 里显式固定频率:
assigned-clocks = <&clk24m>; assigned-clock-rates = <24000000>;上板后查看时钟是否真的跑起来:
cat /sys/kernel/debug/clk/clk_summary | grep -E "clk24m|xclk"没有 debugfs 时,就用示波器量 sensor XCLK 引脚,看到波形再继续。这个坑很像玄学,但九成是 bootloader 把时钟树重配了。
5.3 图像花屏、绿屏、左右错位:DVP 极性参数是第一嫌疑
现象:v4l2-ctl 能列出格式,采集程序也能 DQBUF 出帧,但打开 YUYV 文件是绿紫相间的斜条纹,或者图像像被水平切割错位。
原因:CSI controller 和 OV7725 在 PCLK 采样沿、HSYNC/VSYNC 有效电平上不一致。DVP 并行接口没有自动协商机制,双方必须约定同一套极性。pclk-sample = <1>表示在 PCLK 上升沿采样,= <0>表示下降沿;hsync-active和vsync-active同理。
解决:改 DTS endpoint 后重新编译 dtbs,重启板子再试。常见组合参考:
| 参数 | 值 | 说明 |
|---|---|---|
| bus-width | 8 | OV7725 输出 8 位并行数据 |
| pclk-sample | 1 / 0 | 数据在 PCLK 上升沿/下降沿有效 |
| hsync-active | 1 / 0 | HSYNC 高电平/低电平有效 |
| vsync-active | 1 / 0 | VSYNC 高电平/低电平有效 |
如果极性看起来都对了还是花屏,再把 sensor 输出格式从 YUV422 临时切成 RGB565 验证一下。颜色空间转换错误和极性错误在画面上表现不一样,前者是固定色偏,后者是水平撕裂或雪花状条纹。
5.4 老源码在新内核编译不过:soc_camera 到 v4l2_subdev 的迁移
现象:编译时报soc_camera.h: No such file or directory,或者unknown type name 'v4l2_subdev_ops';有些还会报implicit declaration of function 'v4l2_async_register_subdev'。
原因:内核删掉了旧的 soc_camera 平台框架,subdev API 也改过参数。比如早期驱动注册 subdev 时直接v4l2_i2c_subdev_init(&sd, client, ops),新内核要求v4l2_subdev_init之后填充sd->dev,最后必须调v4l2_async_register_subdev。
解决:做最小迁移,把 probe 尾部改成:
v4l2_i2c_subdev_init(&ov7725->sd, client, &ov7725_subdev_ops); v4l2_async_register_subdev(&ov7725->sd);remove 函数里加上v4l2_async_unregister_subdev(&ov7725->sd)。同时把struct soc_camera_link对应的 platform_data 改到 DTS/device property 里。老的s_fmt/g_fmt如果报错,搬到 pad ops 的set_fmt/get_fmt下。这一步没有统一模板,必须逐个看编译错误,但方向是明确的:让驱动变成一个“能在 async subdev 框架里被 probe 的 I2C client”。
5.5 VIDIOC_DQBUF 一直阻塞:link 没打通或 buffer 没排满
现象:上面给的采集程序运行后卡在VIDIOC_DQBUF,按 Ctrl+C 才退出。media-ctl -p看实体都在,但不出帧。
原因:三个原因按概率排序:media link 没有 enable、sensor 的s_stream没有被调用、buffer 队列里有 buffer 没 QBUF。其中 link 没 enable 最常见,因为很多 SoC 上 CSI 和 sensor 是两个独立 driver,video0 是 video_device 但不清楚下游 link。
解决:确认 link 状态:
media-ctl -d /dev/media0 -p看ov7725 1-0021的 pad 到 csi0 的 link 是否带[1]。然后查看有没有 sensor 启动日志:
dmesg | tail -50如果驱动里有s_stream的 debug 打印,能看到stream on。另外检查采集程序是否把所有 buffer 都 QBUF 了,少了任何一个,DMA 环都可能停住。
6. 让 OV7725 长期稳定出图:用 100 帧压力测试和帧时间戳说话
驱动能抓到一帧只是开始,真正要投入生产,得做稳定性验证。我最常用的验证方式是连续抓 100 帧裸数据,检查文件大小和帧时间戳是否正常。
v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=100 --stream-to=stress.yuv ls -al stress.yuv如果格式是 640x480 YUYV,每帧 614400 字节,100 帧文件大小正好 61440000 字节。少任何一截都说明有丢帧,丢帧基本来自 CSI 带宽、DMA 优先级或电源抖动。不要只看文件大小,还要看v4l2_buffer.sequence和timestamp。在采集程序里打印这两项:
printf("seq=%u ts=%lld.%09ld\n", buf.sequence, (long long)buf.timestamp.tv_sec, buf.timestamp.tv_nsec);连续帧的 timestamp 间隔应该和你设置的帧率一致,比如 30fps 就是约 33.33ms。如果序列跳号,说明有帧没被 DQBUF 到应用层;如果时间戳抖动超过 10%,多半是 sensor 的 24MHz 时钟不稳,或者上游摄像头用 GPIO 做了非精确的帧同步。
我自己的习惯是:无论从哪里拿到 ov7725.rar,先花十分钟做三件事——确认它是不是 v4l2_subdev 驱动、确认 DTS 时钟和极性、确认 i2cget 能读到 0x77。这三件事不翻车,后面编译、采集、压力测试都能按部就班;翻车了就直接往电源、时钟、GPIO 复位这三处找问题。OV7725 老,但不难伺候,只要把 DVP 时序和 V4L2 框架位置摆正,它能在新内核上再稳定跑很多年。希望帮到你。
本文还有配套的精品资源,点击获取