1. 项目缘起与整体设计思路
1.1 为什么需要多路MIPI视频聚合
做过嵌入式视觉项目的朋友大概率都遇到过这样的场景:手头有好几路MIPI摄像头或者MIPI视频源,每一路都是独立的CSI-2输出,但后端主控的MIPI CSI接口数量有限,通常只有一到两路。这时候要么换主控,要么加桥接芯片,要么就得上FPGA做聚合。
我最早接触这个需求是在一个多目视觉项目里,四路MIPI摄像头需要同步采集,后端只有一路CSI输入。市面上能买到的MIPI聚合芯片选择不多,而且灵活性差,分辨率、帧率、路数稍微一变就得重新选型。用FPGA来做这件事,虽然前期开发投入大一些,但胜在完全可控,后期扩展也方便。
所谓多路视频聚合,本质上就是把多路独立的MIPI视频流,经过缓冲、同步、复用之后,合并成一路高带宽的视频流输出。这个“合并”可以是空间上的拼接(比如四宫格画面),也可以是时间上的轮询(分时复用),还可以是通道上的复用(把多路视频打包进一个更大的数据流)。具体选哪种,取决于后端怎么解析。
1.2 方案选型的几个关键考量
在动手之前,有几个问题必须先想清楚,否则后面会反复返工。
第一,MIPI输入路数和速率。MIPI D-PHY的每条lane速率决定了你能接多少路。比如常见的1080p60视频,每路大概需要1.5Gbps左右的带宽(取决于像素格式和blanking)。如果FPGA的MIPI硬核支持4条lane、每lane 2.5Gbps,那总输入带宽就是10Gbps,理论上能接6路左右。但实际要考虑协议开销和突发,一般留30%余量。
第二,聚合后的输出形式。如果后端是标准CSI接收器,那输出也得是标准MIPI CSI-2格式,只是把多路虚拟通道(Virtual Channel)映射到同一个物理链路上。MIPI CSI-2协议本身支持VC(Virtual Channel)概念,最多16个虚拟通道,这给聚合提供了天然便利。如果后端是自定义协议或者走其他接口(比如LVDS、以太网),那输出形式可以更灵活。
第三,帧同步问题。多路视频如果要做拼接或者融合,帧同步是绕不开的。不同摄像头的曝光时刻可能差几毫秒甚至几十毫秒,直接拼接会出现运动物体错位。解决方案要么是硬件同步信号(比如外部触发),要么是在FPGA里做帧对齐缓冲。我一般推荐前者,软件对齐的延迟和复杂度都更高。
第四,DDR带宽和延迟。多路视频聚合必然需要帧缓冲,DDR的带宽和访问延迟直接影响系统能跑多高的分辨率。以四路1080p60为例,每路每秒约124MB的原始数据(RGB888),四路就是500MB/s的写入带宽,加上读出,DDR至少需要1GB/s以上的有效带宽。DDR3-1600理论带宽12.8GB/s,实际有效带宽大概60%-70%,也就是7-8GB/s,看起来够用,但要注意多端口竞争和刷新开销。
1.3 整体架构设计
基于上面的考量,我最终确定的架构是这样的:
- 输入侧:FPGA的MIPI D-PHY硬核接收多路MIPI信号,每路独立解包CSI-2协议,提取出像素数据和时序信息。
- 缓冲侧:每路视频写入DDR中独立的帧缓冲区,采用乒乓缓冲或者多帧缓冲策略。
- 聚合侧:从DDR中按需读出各路视频数据,根据聚合模式(拼接/轮询/复用)进行重组。
- 输出侧:将聚合后的视频流重新打包成MIPI CSI-2格式,通过D-PHY发送出去。
这个架构的核心在于DDR控制器的多端口仲裁和MIPI协议的精确解析。前者决定了系统能不能跑满带宽,后者决定了数据能不能正确提取。
提示:如果FPGA没有MIPI硬核,也可以用LVDS或者普通IO配合外部电阻网络来接收MIPI信号,但速率和稳定性会打折扣。建议优先选带MIPI D-PHY硬核的FPGA,比如Xilinx的Zynq UltraScale+系列或者紫光同创的Logos系列。
2. MIPI协议解析与FPGA实现细节
2.1 MIPI CSI-2协议栈的拆解
MIPI CSI-2的协议栈从下到上大致分三层:物理层(D-PHY)、协议层(CSI-2)、应用层(像素数据)。FPGA要做的,主要是物理层的接收和协议层的解析。
物理层(D-PHY)负责把差分信号转换成数字比特流。D-PHY有HS(高速)和LP(低功耗)两种模式,视频传输时工作在HS模式。HS模式下,时钟lane提供DDR时钟,数据lane在时钟的上下沿都采样数据。FPGA的MIPI硬核会自动处理时钟恢复和串并转换,输出的是并行数据加同步信号。
协议层(CSI-2)的数据包结构是这样的:每个数据包以SoT(Start of Transmission)开始,然后是包头(包含虚拟通道号、数据类型、数据长度等),接着是有效载荷(像素数据),最后是CRC校验和EoT(End of Transmission)。FPGA需要解析包头,提取出虚拟通道和数据类型,然后把有效载荷写入对应的缓冲区。
这里有个容易踩坑的地方:CSI-2的包头长度和字节序。包头通常是4个字节,但不同厂商的IP核可能对字节序的处理不一样。我遇到过有的IP核输出的是大端序,有的是小端序,如果不注意,解析出来的虚拟通道号会完全错乱。建议在调试阶段先把包头抓出来,用逻辑分析仪或者ILA核对一下。
2.2 多路MIPI接收的同步与去偏斜
多路MIPI接收时,各路之间的偏斜(skew)是个大问题。虽然每路内部有D-PHY的deskew校准,但路与路之间的到达时间可能差很多。如果后端要做像素级拼接,这个偏斜必须处理。
我的做法是在每路的CSI-2解析模块后面加一个帧同步FIFO,用统一的帧起始信号(比如第一路的SoF)作为基准,其他路的数据先缓存,等到基准帧到来后再一起释放。FIFO的深度取决于最大偏斜量,一般几行像素的缓冲就够了。
注意:MIPI D-PHY的deskew校准是在物理层做的,主要解决lane与lane之间的偏斜。路与路之间的偏斜属于系统级问题,需要在协议层或者应用层解决。
2.3 虚拟通道映射与数据提取
CSI-2协议支持虚拟通道(VC),这给多路聚合提供了便利。每路摄像头可以配置成不同的VC号,FPGA在解析时根据VC号把数据分发到不同的缓冲区。这样即使多路视频复用在同一个物理链路上,也能正确分离。
但实际项目中,很多摄像头不支持VC配置,或者VC号是固定的。这时候就需要FPGA主动做VC重映射。具体做法是:在CSI-2解析模块里,忽略包头中的VC号,而是根据接收端口号来分配VC。比如端口0的数据统一映射到VC0,端口1映射到VC1,以此类推。
数据提取时要注意像素格式的转换。MIPI CSI-2支持多种像素格式,RAW8/10/12、RGB565/888、YUV422等。FPGA内部一般统一成RGB888或者RAW16来处理,输出时再转回目标格式。转换过程中要注意位对齐和饱和处理,否则会出现颜色偏差。
2.4 输出侧的CSI-2打包
输出侧的CSI-2打包是接收侧的逆过程。需要根据聚合后的视频流,重新生成包头、计算CRC、插入SoT/EoT。这里的关键是时序控制:MIPI D-PHY对时序要求很严,SoT和EoT之间的间隔、包头和载荷之间的间隔都有明确规定,违反了会导致接收端解析失败。
我一般会用状态机来实现打包过程:IDLE状态等待帧起始,然后依次发送包头、载荷、CRC,最后发EoT。每个状态的持续时间根据D-PHY的时序参数来计算。比如包头通常是4个字节,在HS模式下每个字节需要的时间是固定的,状态机只需要按节拍推进就行。
3. DDR多端口读写与带宽优化
3.1 多端口DDR控制器的设计
多路视频聚合对DDR控制器的要求很高:多个写入端口(每路视频一个)、多个读出端口(聚合模块一个或多个)、还要保证实时性。常见的做法是用Xilinx的MIG或者紫光同创的DDR控制器IP,然后在外面包一层仲裁逻辑。
仲裁策略我试过几种:轮询仲裁最简单,但实时性差;优先级仲裁能保证关键端口,但低优先级端口可能饿死;基于信用额的仲裁比较复杂,但效果最好。最终我选的是加权轮询,给每个端口分配一个权重,权重根据视频分辨率和帧率来定。比如1080p的端口权重是720p的两倍。
DDR的访问粒度也很重要。MIPI视频数据是流式的,但DDR是突发访问的。如果每次只写几个字节,DDR的效率会极低。我的做法是在写入侧加一个异步FIFO,积累到一定长度(比如64字节或者128字节)再触发一次DDR突发写。这样能把DDR的效率从30%提升到70%以上。
3.2 帧缓冲区的地址规划
帧缓冲区的地址规划直接影响DDR的访问效率。我的经验是:
- 每路视频分配独立的地址区间,避免交叉访问导致的bank冲突。
- 地址区间按DDR的bank和row对齐,尽量让连续访问落在同一个row里。
- 乒乓缓冲的两块地址要错开bank,避免同时访问同一bank。
以DDR3为例,假设每路1080p视频需要4MB的帧缓冲(RGB888,1920x1080x3约6MB,实际按8MB算),四路就是32MB。加上乒乓,64MB。DDR3的容量一般够用,但地址映射要仔细规划。
3.3 带宽计算与余量评估
带宽计算是方案设计阶段必须做的。以四路1080p60、RGB888为例:
- 每路像素时钟:1920x1080x60 ≈ 124.4M像素/秒
- 每像素3字节,每路数据率:124.4M x 3 ≈ 373MB/s
- 四路总写入带宽:373 x 4 ≈ 1.5GB/s
- 读出带宽(假设聚合后输出1080p60):373MB/s
- 总带宽需求:约1.9GB/s
DDR3-1600的有效带宽大概7-8GB/s,看起来余量很大。但实际要考虑:
- 刷新开销:约5%-10%
- 多端口竞争:效率下降20%-30%
- 读写切换开销:约10%
所以实际可用带宽大概4-5GB/s,1.9GB/s的需求是安全的。但如果分辨率提到4K,或者路数增加到8路,就要重新评估了。
提示:带宽计算时一定要留余量,我一般按理论值的50%来规划。宁可DDR跑不满,也不要让视频出现丢帧。
3.4 读写冲突的规避技巧
DDR的读写切换是有开销的,频繁切换会严重拉低效率。我的做法是批量读写:写入侧积累到一定量再写,读出侧也按行或者按块读。这样读写切换的频率大大降低。
另外,读写分离也是个好办法。如果DDR控制器支持多个bank,可以把写入和读出分配到不同的bank,减少冲突。具体做法是在地址映射时,把写入地址和读出地址映射到不同的bank组。
4. 实操过程与关键环节实现
4.1 硬件选型与连接
我用的FPGA是Xilinx Zynq UltraScale+ ZCU104,带MIPI D-PHY硬核,支持4条lane、每lane 2.5Gbps。摄像头是四路IMX219,通过MIPI CSI-2输出1080p30。DDR是板载的DDR4-2400,容量2GB。
连接上,四路摄像头的MIPI信号分别接到FPGA的四个MIPI接收bank。注意MIPI的差分对要走等长,阻抗控制100欧姆。如果走线太长或者阻抗不匹配,眼图会闭合,导致接收失败。
4.2 MIPI接收模块的配置
Xilinx的MIPI D-PHY IP核配置如下:
- Lane数:2条(IMX219是2-lane)
- 数据速率:每lane 1.5Gbps(1080p30的典型值)
- 参考时钟:200MHz
- 连续时钟模式:使能
配置完成后,IP核会输出并行数据和同步信号。接下来是CSI-2解析模块,我用的是自己写的状态机,解析包头、提取VC和数据类型、计算CRC。
// CSI-2包头解析状态机(简化版) always @(posedge clk) begin case (state) IDLE: if (sot) state <= HEADER; HEADER: begin // 解析4字节包头 if (byte_cnt == 3) state <= PAYLOAD; end PAYLOAD: begin // 提取像素数据 if (byte_cnt == data_len) state <= CRC; end CRC: state <= IDLE; endcase end4.3 DDR控制器的多端口仲裁实现
DDR控制器我用的是MIG IP,配置成4个AXI端口。仲裁逻辑用加权轮询:
// 加权轮询仲裁(简化版) always @(posedge clk) begin if (req[0] && weight[0] > 0) begin grant <= 0; weight[0] <= weight[0] - 1; end else if (req[1] && weight[1] > 0) begin grant <= 1; weight[1] <= weight[1] - 1; end // ... 其他端口 if (weight[0] == 0 && weight[1] == 0) begin // 重新加载权重 weight[0] <= 4; weight[1] <= 2; end end权重根据视频分辨率来定:1080p的端口权重4,720p的权重2,480p的权重1。
4.4 聚合模块的实现
聚合模块从DDR中读出各路视频数据,按拼接模式重组。以四宫格为例,输出分辨率是3840x2160(四路1080p拼成2x2)。读出时按行扫描,每行从四路中各取一段,拼成一行输出。
// 四宫格拼接(简化版) always @(posedge clk) begin if (out_x < 1920) begin // 左上角 data_out <= buf0[out_y][out_x]; end else if (out_x < 3840) begin // 右上角 data_out <= buf1[out_y][out_x - 1920]; end // ... 下半部分类似 end4.5 输出侧MIPI发送的配置
输出侧用同样的MIPI D-PHY IP核,配置成发送模式。CSI-2打包模块生成包头和CRC,然后通过D-PHY发送。注意输出速率要和接收端匹配,否则接收端会解析失败。
5. 常见问题与排查技巧实录
5.1 MIPI接收无信号或信号不稳定
这是最常见的问题。排查思路:
- 检查物理连接:差分对是否接反、阻抗是否匹配、走线是否等长。
- 检查时钟:MIPI的参考时钟是否正常,频率是否正确。
- 检查D-PHY配置:lane数、数据速率是否和摄像头匹配。
- 检查LP模式:有些摄像头需要先进入LP模式再切HS,如果FPGA没有正确处理LP状态,会一直收不到数据。
我遇到过最坑的一次是差分对接反了,眼图完全闭合,查了两天才发现。所以硬件调试时一定要先确认物理层没问题。
5.2 DDR带宽不足导致丢帧
表现是视频偶尔卡顿或者花屏。排查方法:
- 用ILA抓DDR的读写信号,看是否有长时间等待。
- 计算实际带宽需求,对比DDR的有效带宽。
- 检查仲裁权重是否合理,低优先级端口是否被饿死。
优化手段:
- 增大FIFO深度,减少DDR访问频率。
- 调整仲裁权重,给高带宽端口更多机会。
- 优化地址映射,减少bank冲突。
5.3 聚合后画面错位或撕裂
这通常是帧同步没做好。排查:
- 检查各路视频的帧起始信号是否对齐。
- 检查FIFO的深度是否足够容纳最大偏斜。
- 检查读出逻辑是否按正确的顺序读取。
我的经验是,帧同步FIFO的深度至少要能容纳两行像素的偏斜。如果偏斜更大,就要考虑用外部同步信号了。
5.4 输出MIPI接收端解析失败
表现是后端完全收不到画面,或者画面全是雪花。排查:
- 检查输出D-PHY的时序参数是否满足MIPI规范。
- 检查CSI-2包头是否正确,CRC是否计算正确。
- 检查输出速率是否和接收端匹配。
我遇到过CRC计算错误导致接收端丢弃所有包的情况。后来发现是CRC的多项式用错了,改成标准的多项式后正常。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| MIPI无信号 | 物理连接问题 | 查差分对、阻抗、走线 | 重新焊接或调整走线 |
| 信号不稳定 | 时钟或配置问题 | 查参考时钟、D-PHY配置 | 调整配置参数 |
| DDR丢帧 | 带宽不足 | 计算带宽、抓ILA | 优化仲裁、增大FIFO |
| 画面错位 | 帧同步问题 | 查帧起始信号、FIFO深度 | 增加同步逻辑 |
| 输出解析失败 | 时序或CRC问题 | 查时序参数、CRC计算 | 调整时序、修正CRC |
提示:调试MIPI时,逻辑分析仪和ILA是必备工具。没有它们,基本靠猜,效率极低。
6. 几个容易被忽略的细节
6.1 MIPI同层挖空的处理
PCB设计时,MIPI差分对下面的参考平面要完整,不能有挖空或者跨分割。如果参考平面不完整,阻抗会突变,导致信号反射。我见过一个项目因为MIPI走线跨了电源分割,眼图完全闭合,后来加了缝合电容才解决。
6.2 摄像头配置的坑
不同摄像头的MIPI配置寄存器不一样,有的需要配置lane数、数据速率、VC号等。IMX219的配置就比较典型,需要先写一堆寄存器才能输出正确的MIPI信号。建议先用官方的配置脚本,再根据实际需求微调。
6.3 温度对DDR的影响
DDR的时序参数会随温度变化,高温下可能需要降频或者调整刷新率。工业级应用要特别注意这一点。我一般会在DDR控制器里使能温度补偿功能,或者留出足够的时序余量。
6.4 电源噪声对MIPI的影响
MIPI的差分信号幅度很小,电源噪声很容易耦合进去。建议MIPI的电源单独用LDO供电,并且加足够的去耦电容。我试过用DC-DC直接供电,结果误码率很高,换成LDO后立刻改善。
7. 方案扩展与个人体会
这套方案目前跑四路1080p30很稳,DDR带宽占用大概40%,还有很大余量。如果要扩展到八路,或者提升到4K,需要做几件事:换更大容量的DDR、增加MIPI lane数、优化仲裁算法。
我个人在实际操作中的体会是,FPGA做MIPI聚合,难点不在写代码,而在调试。物理层的信号完整性、协议层的时序匹配、系统层的带宽分配,任何一个环节出问题都会导致整个系统跑不起来。所以前期规划一定要做足,尤其是带宽计算和地址映射,后面改起来很麻烦。
另外,MIPI的调试工具很重要。一个能抓协议包的分析仪,能省掉大量猜测时间。我用的是一款支持CSI-2解码的分析仪,虽然贵,但值得。
最后再分享一个小技巧:如果DDR带宽实在不够,可以考虑在FPGA内部做行缓存而不是帧缓存。行缓存只需要几KB的BRAM,带宽压力小很多。但行缓存只能做行级的拼接,不能做帧级的同步。具体用哪种,看你的应用需求。