☰
MIPI/DVP CSI摄像头驱动移植
2026/10/21 5:55:36 网站建设 项目流程

1、原理图

1.1 dcmi

port:代表硬件模块上的视频接口
DCMI 的 port:DCMI 控制器的接收口(SINK)
OV5640 的 port:摄像头 sensor 的发送口(SOURCE)
endpoint:port 上面的连接端子,用来标记这根连线。
remote-endpoint:互相指向对方,告诉内核 media graph:这两个端点连在一起。

1.2 i2c

ov5640

1.3 framefmt‌

‌framefmt‌ 是 Linux V4L2 框架里的核心结构体 struct v4l2_mbus_framefmt,用于描述媒体总线上的图像格式,比如像素格式、分辨率、色彩空间这些。‌‌

1.4 DCMI

DCMI 属于 Linux Media 子系统,v4l2 框架,最终应用通过 /dev/videoX 捕获图像
stm32-dcmi 驱动只负责并行数据接收;OV5640 的寄存器配置是另一个 i2c sensor 驱动(ov5640.c)
两个节点通过 remote-endpoint 完成链路绑定,内核会构建 media graph

1.5为什么两边 endpoint 都要写总线参数?
看这段代码:
ov5640_probe 读取sensor 端 endpoint参数,用来配置 sensor 输出时序;
dcmi_probe 读取dcmi 端 endpoint参数,用来配置 DCMI 采样时序;

ov5640_core_ops(核心操作)

函数作用谁调用何时调用
.s_power=ov5640_s_power打开/关闭传感器电源(上下电、复位)主控驱动(DCMI)或应用层通过VIDIOC_S_POWERioctl应用 open/close 设备时;或 host 驱动 probe 阶段需要上电做初始化时
.log_status打印传感器当前状态(用于 debug)V4L2 core,通过VIDIOC_LOG_STATUS用户手动触发ioctl(fd, VIDIOC_LOG_STATUS)
.subscribe_event/.unsubscribe_event订阅/退订事件(如帧率变化、控制值变化通知)V4L2 控件子系统 (v4l2-ctrls)应用调用VIDIOC_SUBSCRIBE_EVENT时

最常用的是 s_power,它的典型调用链:

应用 open("/dev/videoX") → dcmi_open() → v4l2_fh_open() → pm_runtime_get() → 如果之前是关闭的,host 驱动会调用: v4l2_subdev_call(subdev, core, s_power, 1) ← 上电 → ov5640_s_power(1) → regulator_enable(DOVDD) → gpiod_set_value(PWDN, 0) // 退出 powerdown → gpiod_set_value(RESET, 1) // 拉高复位 → clk_prepare_enable(xclk) // 给 MCLK → ov5640_identify_chip() // 读 chip_id 确认在位

ov5640_video_ops(视频流操作)

函数作用谁调用何时调用
.s_stream=ov5640_s_stream启动/停止传感器输出像素流主控驱动(DCMI)在 STREAMON/OFF 时调用整个驱动里最重要的函数——应用VIDIOC_STREAMON时触发
.g_frame_interval读取当前帧率(如 30fps →{numerator=1, denominator=30})V4L2 core / 应用通过VIDIOC_G_PARM应用查询帧率时
.s_frame_interval设置帧率应用通过VIDIOC_S_PARM应用修改帧率时(如切换到 15fps)

s_stream 的调用链是最核心的一条线:

应用: ioctl(fd, VIDIOC_STREAMON, &type) → V4L2 core: video_ioctl2() → vb2_core_streamon() → vb2_ops.start_streaming (即 dcmi_start_streaming) → dcmi_pipeline_s_stream(1) // 遍历 media 管线 → v4l2_subdev_call(subdev, video, s_stream, 1) ← 关键一跳 → ov5640_s_stream(enable=1) → if (pending_mode_change) ov5640_set_mode() // 写分辨率/时序寄存器 → if (pending_fmt_change) ov5640_set_framefmt() // 写格式寄存器 → ov5640_set_stream_dvp(sensor, 1) // 打开 DVP 输出使能位 → ov5640_write_reg(sensor, OV5640_REG_SYS_CTRL1, 0x01) // start

ov5640_pad_ops(媒体管线/格式操作)

这是格式协商阶段用到的表,所有函数都在应用调用 VIDIOC_S_FMT 等 ioctl 时被触发:

函数作用谁调用何时调用
.enum_mbus_code枚举传感器支持的像素格式编码(如 YUYV、JPEG、RGB565)V4L2 core,通过VIDIOC_ENUM_FMT应用查询"支持哪些格式"时
.get_fmt=ov5640_get_fmt读取当前配置的格式(mbus_code、宽×高)V4L2 core / host 驱动,通过VIDIOC_G_FMT或管线遍历时应用查询当前格式;host 在dcmi_graph_notify_complete初始化默认格式时
.set_fmt=ov5640_set_fmt设置目标格式(只记录,真正写寄存器留到 STREAMON)V4L2 core / host 驱动,通过VIDIOC_S_FMT应用ioctl(fd, VIDIOC_S_FMT, &fmt)时
.enum_frame_size枚举支持的分辨率(如 640×480、1080P)应用通过VIDIOC_ENUM_FRAMESIZES应用查询"支持哪些分辨率"时
.enum_frame_interval枚举某分辨率下支持的帧率应用通过VIDIOC_ENUM_FRAMEINTERVALS应用查询"这个分辨率下有哪些帧率可选"时

v4l2_async_register_subdev_sensor_common(&sensor->sd):把这个 subdev 交给异步框架。

整体链路逻辑:

1. DCMI probe 执行:
解析DTS endpoint → 读取data-shift/bus-width
media_entity_pads_init 创建SINK pad
vb2_queue_init 初始化buffer队列
dcmi_graph_init 创建async notifier,等待OV5640
2. OV5640 i2c驱动 probe执行:
media_entity_pads_init 创建SOURCE pad
v4l2_async_register_subdev_sensor_common 注册notifier,等待DCMI
3. 两端全部probe成功 → V4L2异步框架匹配成功 → media_create_link,graph连通
4. 应用层 open /dev/video0 → VIDIOC_S_FMT 设置分辨率格式 → VIDIOC_REQBUFS申请buffer → VIDIOC_QBUF入队buffer → VIDIOC_STREAMON
5. STREAMON调用链路:dcmi的streamon → 调用subdev s_stream → ov5640_s_stream(就是第一张截图)开启sensor输出DVP信号
6. DCMI硬件接收PCLK/VSYNC/HSYNC,DMA搬运图像到vb2 buffer,中断通知上层

2.流程逻辑分析:

2.1 DCMI 控制器端:

a.

先判断设备是否匹配,检查设备树中是否有与此驱动匹配的兼容性字符串
创建DCMI私有数据结构体

b.

获取复位控制器(用于后续硬复位)

解析设备树中的 endpoint(端点),读取摄像头与 DCMI 之间的物理连接属性。

(关键点:代码明确检查了 ep.bus_type,如果发现是 MIPI CSI-2 接口,直接报错退出,因为此 DCMI 驱动只支持并口(Parallel)。如果你的 OV5640 是 MIPI 接口,这个驱动不会生效。)

保存并口的配置:总线宽度(如 8/10/12 位)、数据偏移等。

c.

interrupts=GIC_SPI 78 → platform_get_irq + request_threaded_irq (L1910,1926)
reg=0x4c006000 → platform_get_resource + ioremap (L1914,1920)
clocks/clock-names="mclk" → devm_clk_get("mclk") (L1934)
dmas/dma-names="tx" → dma_request_chan("tx") (L1941)
获取资源

d.初始化并发控制与链表

irqlock:自旋锁,中断上下文使用
lock/dma_lock:互斥锁,保护 V4L2 ioctl、DMA 操作
complete:等待 DMA 采集完成的同步对象
buffers:维护正在使用的 vb2 buffer 链表

e.

media_device_init:初始化 media_device(整个 media graph 的顶层管理对象,OV5640 subdev 也注册到这个 mdev)
v4l2_device_register:注册 V4L2 顶层设备,一个 media 设备对应一个 v4l2_device,可以包含多个 subdev + video 节点

框架骨架:
v4l2_device_register → video_device_alloc → vb2_queue 初始化 → media_entity_pads_init

f.

dcmi_graph_init内部:
初始化v4l2_async_notifier异步通知器
解析 DTS endpoint 的remote-endpoint,等待远端 OV5640 sensor subdev 就绪
(和 OV5640 那边的v4l2_async_register_subdev_sensor_common配对:
不管是 OV5640 先 probe 还是 DCMI 先 probe,等两端都 probe 成功,V4L2 异步框架自动调用匹配回调,执行media_create_link,把 OV5640 SOURCE pad ↔ DCMI SINK pad 建立 link,media graph 连通。)

dcmi硬件复位

platform_set_drvdata:把 dcmi 私有结构体挂载到 platform_device,后面 ioctl、中断里可以通过dev_get_drvdata拿到 dcmi 指针
pm_runtime_enable:开启电源管理

【物理世界】 ┌──────────────────┐ I2C控制 ┌──────────────┐ │ SoC 内 DCMI 控制器 │ ──────────────────────→ │ OV5640 传感器 │ │ (并行采集硬件) │ (发寄存器配置命令) │ (数据源) │ └────────┬─────────┘ └──────┬───────┘ │ DVP 并行总线(DATA/PCLK/HSYNC/VSYNC) │ └───────────────────────────────────────────┘ 【V4L2 框架层】 ┌─────────────────────────────────────────────────────────┐ │ v4l2_device (注册中心/根节点) │ ← 不属于任何物理设备,纯粹是框架的"管理层" │ ├── video_device (→ /dev/video0) │ ← 对应 DCMI 的"用户接口面" │ │ └── vb2_queue (DMA 缓冲队列管理) │ │ └── v4l2_subdev (OV5640) │ ← 对应 OV5640 传感器 │ └── ov5640_subdev_ops (.core/.video/.pad) │ └──────────────────────────────────────────────────────────┘

v4l2_device 不是物理设备,它是 V4L2 框架的"管理层/注册中心";video_device 是 DCMI 的"用户接口面"(生成 /dev/videoX);v4l2_subdev 是 OV5640 的"标准契约外壳"(让 DCMI 能用统一接口操作任意传感器)。物理上只有两块芯片,但 V4L2 把"管理"、"接口"、"子设备"拆成三个结构体,目的是让 host 驱动和 sensor 驱动彻底解耦。

struct ini dcmi_graph_init相关函数解析:

// dcmi_graph_parse() 里: struct device_node *ep, *remote; struct v4l2_async_subdev *asd; ep = of_graph_get_next_endpoint(np, NULL); // 拿到 dcmi 自己的 endpoint remote = of_graph_get_remote_port_parent(ep); // 顺着 remote-endpoint 拿到 OV5640 的节点 asd = kzalloc(sizeof(*asd), GFP_KERNEL); asd->match_type = V4L2_ASYNC_MATCH_FWNODE; // ← 告诉框架:用 fwnode 匹配 asd->match.fwnode = of_fwnode_handle(remote); // ← 填入 OV5640 节点的 fwnode 指针 v4l2_async_notifier_add_subdev(&dcmi->notifier, asd);

此时 DCMI 并没有"主动去找 OV5640",它只是向异步框架交了一张"寻人启事"——"我需要的子设备,它的 fwnode 是这个指针,出现了通知我"

时序图:

时间轴 ─────────────────────────────────────────────► [DCMI 驱动加载] [OV5640 驱动加载] [结果] │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ dcmi_probe │ │ ov5640_probe│ │ 绑定完成 │ │ │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ │ │ dcmi_graph_ │ │ 初始化 sd │ │ │ │ _init() │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ 解析设备树 │ │ v4l2_async_ │ │ │ │ 找到 ov5640 │ │ register_ │ │ │ │ 节点引用 │ │ subdev_ │ │ │ │ │ │ │ sensor_ │ │ │ │ ▼ │ │ common() │ │ │ │ 注册 notifier│◄──────│ │ │ │ │ │ 到全局列表 │ │ ▼ │ │ │ │ │ │ │ 框架检查 │ │ │ │ │ │ │ 匹配条件 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ └───────┴────────┤ 匹配成功! │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ 调用 DCMI │ │ │ │ │ 的 bound() │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ 建立 media │ │ │ │ │ pad link │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ 调用 │ │ │ │ │ complete() │ │ │ │ │ 注册 │ │ │ │ │ /dev/video0 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘

2.2 ov5640传感器端

a.

MEDIA_BUS_FMT_UYVY8_2X8:就是 8bit 并行 DVP 总线,UYVY,每个像素拆成 2 个 8bit 样本在数据线传输;
这是 subdev 的 pad 输出格式,是 sensor 往 DCMI 发的数据格式。

v4l2_mbus_framefmt 描述的是 media pad 上传输的数据格式,不是用户态 buffer 格式,是 media graph 链路协商的核心结构体。

b.

读 DTS 根节点的rotation = <180>属性,属于sensor 本体属性,不是 endpoint 总线属性。

c.

fwnode_graph_get_next_endpoint:从 ov5640 这个设备节点,找到它 port 下的 endpoint(ov5640_0)
v4l2_fwnode_endpoint_parse:解析 endpoint,填充 sensor->ep(struct v4l2_fwnode_endpoint),里面就包含 ep.bus.parallel,拿到 bus-width/data-shift/pclk-sample/hsync/vsync-active/pclk-max-frequency。
(sensor 驱动拿到这组总线参数,用来配置 OV5640 内部输出时序,让 sensor 硬件输出符合约定的 DVP 信号。
DCMI 驱动拿到它自己 endpoint 的参数,用来配置 DCMI 采样时序。)

d.

ov5640_subdev_ops:subdev 的回调函数集合,里面包含 get_fmt/set_fmt、stream_on/stream_off 等子设备接口;
subdev 就是 media_entity,对应 media graph 里的 camera-sensor 实体。

MEDIA_PAD_FL_SOURCE:这个 pad 是输出源 pad(数据从 sensor 往外发);
media_entity_pads_init:初始化 media entity 的 pad;
(DCMI 那边的 pad 是 MEDIA_PAD_FL_SINK(接收)。
media graph link 就是把 SOURCE pad(ov5640) ↔ SINK pad(dcmi)连接起来。)

2C 读取 OV5640 芯片 ID,确认硬件在线,这是 sensor 驱动经典第一步硬件校验。

初始化 v4l2_ctrl:曝光、增益、白平衡等控制项,用户态可以通过 subdev 节点去调整 sensor 寄存器。

异步注册 subdev,这是 media graph 摄像头非常关键的机制!
v4l2_async_register_subdev_sensor_common 属于异步子设备注册:
sensor(i2c 子设备)先注册,但此时DCMI 主 video 设备可能还没 probe 完成。内核 media 框架会等待两边全部就绪,自动匹配 remote-endpoint,创建 media link。
(这就是为什么 DTS 里只需要互相写 remote-endpoint,不用手动在代码里创建 link。
老式非 async 驱动,必须保证 probe 顺序,很麻烦;async 框架解决多实体 probe 先后顺序问题。)

行号函数调用干了什么
1138v4l2_async_notifier_init(notifier)初始化一个 notifier 对象
1140v4l2_async_notifier_parse_fwnode_sensor_common(...)解析设备树,根据port/endpoint自动生成 asd 匹配规则(相当于传感器的"反向寻人启事")
1145v4l2_async_subdev_notifier_register(sd, notifier)把这个 notifier 挂到全局链表(和 DCMI 的 notifier 一样)
1149v4l2_async_register_subdev(sd)← 就是这一步!把 subdev 挂到全局链表 + 遍历所有 notifier 逐个比对

3.重要函数

3.1获取端点建立连接

of_graph_get_next_endpoint

3.2匹配类型选择

enum 保证了代码的可读性和类型安全(通过 switch-case 可以防止访问错误的联合体成员)。 union 保证了所有可能的匹配参数共享一块内存,不会因为定义了一个庞大的结构体而浪费内核内存。

3.3notifier.ops 三个回调(bound/complete/unbind)的注册

bound回调时机

3.4核心匹配机制代码体现

a.

// dcmi_graph_parse() 里: struct device_node *ep, *remote; struct v4l2_async_subdev *asd; ep = of_graph_get_next_endpoint(np, NULL); // 拿到 dcmi 自己的 endpoint remote = of_graph_get_remote_port_parent(ep); // 顺着 remote-endpoint 拿到 OV5640 的节点 asd = kzalloc(sizeof(*asd), GFP_KERNEL); asd->match_type = V4L2_ASYNC_MATCH_FWNODE; // ← 告诉框架:用 fwnode 匹配 asd->match.fwnode = of_fwnode_handle(remote); // ← 填入 OV5640 节点的 fwnode 指针 v4l2_async_notifier_add_subdev(&dcmi->notifier, asd);

关键理解:此时 DCMI 并没有"主动去找 OV5640",它只是向异步框架交了一张"寻人启事"——"我需要的子设备,它的 fwnode 是这个指针,出现了通知我"

b.

V4L2 异步核心把"寻人启事"登记到全局链表

// v4l2_async_notifier_register() 内部: list_add_tail(&notifier->list, &notifier_list); // 把 dcmi 的 notifier 挂到全局链表上,框架会在每次有新 subdev 注册时遍历这个链表

c.

OV5640 probe 完成,向框架"报到"

// ov5640_probe() 末尾: v4l2_async_register_subdev_sensor_common(&sensor->sd); // → 内部调用 v4l2_async_register_subdev(&sensor->sd) // → 把 sensor->sd 挂到全局 subdev 链表 // → 遍历所有已注册的 notifier,逐个比对

v4l2_async_register_subdev

一旦匹配成功,立即调用v4l2_async_match_notify。这个函数是真正建立连接的地方,它会创建 V4L2 子设备节点、在 Media Controller 中建立 pad 与 pad 的链接(Link),让你的 OV5640 和 DCMI 真正连通。

接着调用 v4l2_async_notifier_try_complete。因为一个主设备可能等待多个子设备(比如等完 OV5640 还要等一个 MIPI 接收器),这个函数检查主设备等待的所有子设备是不是都到齐了。如果到齐了,就触发主设备的 complete 回调,完成整个管线的初始化。

极度重要的一点:绑定成功后,直接 goto out_unlock 跳出循环。一个子设备只能绑定一个主设备。既然已经配对了,就不能再去遍历后面的主设备了。

d.异步核心执行比对&匹配核心

这段 switch-case 完美地用到了3.2提到的 enum 配合 union 的设计:

通过 asd->match_type(枚举标签),代码决定去哪里、拿什么数据,以及用哪个函数去比对。 如果是V4L2_ASYNC_MATCH_FWNODE,就选择 match_fwnode。这个函数会去比较 sd 的节点和 asd 里存的那个节点是否一致。

如果返回 true,说明找到了!直接返回当前的 asd(代表配对成功,主设备知道这个子设备就是自己要找的)。 如果返回 false,说明当前 asd 条件不满足,继续 for 循环看下一个。 如果循环结束都没找到,返回 NULL,表示这个子设备不是主设备在等的(或者还没准备好)。

sd->dev 是 OV5640 的 struct device(由 I2C core 在 probe 前根据 camera@3c 节点创建的)。 dev_fwnode(sd->dev) 取出这个 device 关联的 fwnode 指针——它就是设备树里 camera@3c 那个节点的 fwnode。

asd->match.fwnode 是 DCMI 之前存的——它是顺着 remote-endpoint 找到的 camera@3c 节点的 fwnode。

两者指向同一个设备树节点,所以指针相等 → 匹配成功!

3.5 数据流生命周期

苏醒 -> 启动引擎 -> 搬运数据 -> 刹车回收

3.6用户层时序

应用发 STREAMON,V4L2 走 ioctl_ops 找到 DCMI,DCMI 通过 v4l2_subdev_call 远程摇醒 OV5640 的 s_stream 寄存器开流;硬件像素经 DVP 进 DCMI,DMA 自动写内存;应用通过 DQBUF 从内存零拷贝取帧。

4.1 sensor端的函数

ov5640_set_power:是总指挥(入口),负责统筹上下电逻辑,并处理 MIPI 接口的专属初始化。 ov5640_set_power_on / off:是执行者,专门负责底层硬件(时钟、供电、复位引脚)的开与关。

4.2总时序

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询