最近这半年,来问我“FPGA怎么接MIPI摄像头/屏幕”的人明显变多了。甭管是做工业视觉、医疗内窥镜,还是给学生做毕设,大家一开始都会想当然觉得“不就是个高速串行接口嘛,FPGA不是啥都能干吗”。等你真把MIPI DPHY、CPHY的协议手册翻完,再对着Xilinx 7系列的引脚手册一看,就知道这事没那么简单。
先说个最扎心的结论:Xilinx 7系列所有型号,都没有原生支持MIPI DPHY/CPHY的收发器。7系列的普通I/O既不满足MIPI的电气电平标准,也没有MIPI协议里规定的LP/HS两态切换能力,GTP/GTX高速收发器也跟MIPI的源同步架构八字不合。但这不代表用FPGA做MIPI接口就是死路一条,实战中还是有三条路可以走:外接桥接芯片、LVDS直连妥协方案、以及靠高速收发器做协议转换。这篇文章就把这几条路全部摊开,从协议底层原理讲到Vivado里的XDC约束,再配上可直接参考的Verilog代码和调试手法,给想动手做MIPI接口的FPGA工程师一份能直接落地的参考指南。
1. 先搞懂MIPI DPHY和CPHY到底是个啥
在动手写代码之前,建议先把物理层的底子摸清楚。很多人拿着传感器的数据手册上来就搜“MIPI_LANE”引脚定义,然后就开始写逻辑,结果波形一测对不上,都不知道问题出在哪。所以我先花点篇幅,把DPHY和CPHY这两套物理层的核心差异讲透。
1.1 DPHY:一对时钟加多对数据线,源同步传数
DPHY是MIPI联盟最早推出来的一套物理层规范,CSI-2摄像头接口和DSI显示屏接口用的基本都是它。它的结构很直观:一组差分时钟线,加上一组或多组差分数据线,时钟线负责给数据线提供采样时钟,数据在时钟的上下沿各传一个bit,所以本质上是一个源同步的DDR接口。
DPHY定义了两种工作状态:LP(Low Power,低功耗)状态和HS(High Speed,高速)状态。LP状态其实就是普通的单端1.2V电平,用来传一些控制信息,比如SoT/SoF握手包、进入高速模式的Request信号、帧同步包等等。HS状态才是真正的高速数据搬运状态,两条线上呈现的是约200mV摆幅的差分信号,数据速率从80Mbps一路能到2.5Gbps每条lane。
你可以这么理解:LP模式是小区门口的保安,负责登记、开关门、吹哨子,走的是慢车道;HS模式是高速公路上飙车的卡车车队,只管猛跑,啥也不管。FPGA要接DPHY,最关键的一点就是既要在LP模式下识别那套1.2V单端电平,又要在HS模式下能锁住200mV的差分信号。
1.2 CPHY:三线一组没有时钟,编码复杂但带宽更高
CPHY是后来提出的另一种物理层规范,目的是在同样的引脚数量下跑出比DPHY更高的带宽。它的做法是砍掉独立的时钟lane,把三根线划成一组,称为一个trio(也就是一条lane),数据传输完全靠三根线之间的电压关系来表达。
CPHY的编码方式很有意思:三根线上每个时刻各有一个电平状态(0、1、2),三个状态组合起来代表一个符号。它每传一个符号,在线里传输的其实是三进制信息,再结合三根线状态的轮换规则,能从三个符号里编码出5个bit的信息。因为不需要一路时钟线,同样数量的引脚能腾出更多lane来走数据,总带宽自然就上来了。以0.5Gbps的CPHY lane为例,它的有效数据吞吐量大约等效于1.5Gbps的DPHY lane。
但代价就是解码逻辑极其繁琐。硬件上得同时采样三根线,恢复出符号序列,再靠解析符号序列来恢复时钟和数据对齐。FPGA想直接怼CPHY得自己搞定三进制采样、符号恢复、时钟恢复这一整套,这已经不是普通的I/O逻辑能搞定的事了。所以实战中如果非要接CPHY接口的摄像头或屏幕,我建议放弃幻想,直接找一颗支持的桥接芯片,让专门做这个的ASIC去兜底。
2. Xilinx 7系列为什么不能直接怼MIPI
很多人一开始不理解,说FPGA不是号称万能接口芯片吗,为什么连个摄像头接口都直连不了?这真不是FPGA菜,而是MIPI的电气规范实在太“不讲武德”了。
2.1 电气标准不兼容的根本原因
MIPI DPHY在HS模式下的差分信号,单端摆幅大概只有100mV到300mV,典型共模电压在200mV附近。而Xilinx 7系列普通I/O能支持的LVDS标准,共模电压通常得在1.2V上下。这两套电平标准焊接到同一组引脚上,硬件没法同时满足。
再来看LP模式。LP模式是0到1.2V的单端电平,这个其实7系列的bank电压设在1.2V或者1.5V的时候还能勉强玩一玩。可问题是,MIPI收发器需要在同一对引脚上快速切换“LP单端发送”和“HS差分接收”两种模式,中间还夹着复杂的单向/双向切换时序。7系列的普通I/O虽然可以配置成各种电平标准,但它不会自动帮你做这种物理层的模式切换。
这就意味着,直接把传感器的MIPI输出接到7系列FPGA的HP bank上,即使你能用LVDS接收器把HS数据采进来,LP状态的握手信号大概率是废掉的。结果就是传感器认为链路一直没建立起来,压根不会往高速模式下切。
2.2 7系列I/O的三种替代路线对比
既然物理层硬怼不行,那就绕路。实战里我见过而且试过靠谱的主要有三条路线,各有取舍:
| 实现路线 | 成本 | 开发难度 | 可靠度 | 适用场景 |
|---|---|---|---|---|
| 外接MIPI桥接芯片 | 高(芯片十几到几十元) | 低,FPGA端拿通用接口就行 | 高,量产首选 | 摄像头接入、屏幕驱动、产品化 |
| LVDS直连特殊处理 | 低(几乎零额外成本) | 高,要做LP模拟和协议处理 | 中,只适合原型验证和低速场景 | 实验室调算法、快速原型 |
| GTP/GTX高速收发器 | 中 | 很高,CDR和协议适配成本高 | 低,需要额外芯片配合 | 视频流转接,非DTY直连场景 |
外接桥接芯片的核心思路是把MIPI物理层和协议层交给专用芯片处理,FPGA只跟桥接芯片的并行接口或者LVDS接口打交道。这条路动手做起来最省心,因为桥接芯片已经把LP/HS切换、数据对齐、字节打包全干完了。但缺点是增加物料成本和PCB面积,而且桥接芯片的型号你得仔细选,有些只支持CSI-2摄像头方向,有些只支持DSI屏方向,下单向桥和双向桥完全是两类东西。
LVDS直连这条线比较取巧,只接HS高速数据,想办法绕开LP握手。比如把传感器的LP状态信号通过额外的GPIO引出来喂给FPGA做状态判断,而数据线用AC耦合之后直接进LVDS接收器。我自己实测下来,在1Gbps以内、短PCB走线的条件下,这套方案能跑通,但时序裕量很薄,环境一变化就容易出花屏。所以它比较适合在实验室做算法验证,不太适合直接上产线。
3. 方案落地:LVDS直连方式实现DPHY接收(含代码)
如果你手头正好有一块7系列板子和一个MIPI CSI-2摄像头,不想去买桥接芯片,就想先看看HS数据长什么样,那么LVDS直连这条路是最快能出波形的方式。下面我来拆解一下具体怎么做。
3.1 硬件上要做什么处理
首先,传感器的MIPI_TX_DP/N、MIPI_TX_CLKP/N这四根线,不能直接怼到FPGA的普通I/O上。因为MIPI HS模式下共模电压太低,而7系列HP bank的LVDS接收器在没有外部偏置的情况下,输入范围可能够不到那个共模点。所以通常的做法是在靠近FPGA端加AC耦合电容,做隔直处理,再通过电阻网络建立一个大概0.2V到0.3V的直流偏置。
这个偏置电阻怎么选?量一下7系列HP bank的LVDS输入等效电阻,再按分压算出一个合适的共模点,经验值是用两个1K电阻从1.8V分压,得到大概0.9V偏置就有点偏高了;比较稳妥的是用两个470欧姆电阻从0.2V基准分压,或者干脆用一个外置有源偏置芯片。注意不同速率下偏置电阻值可能需要微调,这个没有仿真很难算准,我建议样板阶段留好0402封装的位置,方便现场换阻值。
LP状态信号也别浪费。传感器的GPIO1和GPIO2上面通常会输出MIPI的HS状态标志,你可以用两颗普通单端I/O把这几个脚接进来,在FPGA里当普通数字信号采样就行。通过这组信号就能判断摄像头正处于LP阶段还是HS阶段,进而控制内部的数据通路做复位和同步。
3.2 HDL设计:ISERDESE2高速采样
HS数据进入FPGA之后,需要一个高速解串器把串行bit流变成并行字节。Xilinx 7系列没有像Ultrascale那样子直接给你一个MIPI_RX硬核,通用的方案是用ISERDESE2原语,配合IDELAYE2做输入延时微调。
我用的是一个常规的8位DDR解串结构,核心代码如下:
// 顶层输入 // data_p, data_n : MIPI DPHY 数据差分对(AC耦合进入) // clk_p, clk_n : MIPI DPHY 时钟差分对(AC耦合进入) IBUFDS_DIFF_OUT #( .DIFF_TERM("TRUE"), .IBUF_LOW_PWR("TRUE") ) u_ibuf_data ( .I(data_p), .IB(data_n), .O(data_p_int), .OB(data_n_int) ); IDELAYE2 #( .IDELAY_TYPE("FIXED"), .DELAY_SRC("IDATAIN"), .IDELAY_VALUE(0), // 后面在调试环节通过扫描确定 .HIGH_PERFORMANCE_MODE("TRUE") ) u_idelay_data ( .IDATAIN(data_p_int), .DATAOUT(delayed_data), .C(clk_hs), .REGRST(1'b0), .LD(1'b0), .CE(1'b0), .CINVCTRL(1'b0), .CNTVALUEIN(5'd0), .CNTVALUEOUT(), .INC(1'b0), .LDCNTVALUEIN(1'b0) ); ISERDESE2 #( .DATA_RATE("DDR"), .DATA_WIDTH(8), // 8:1 解串 .INTERFACE_TYPE("NETWORKING_PIPELINED"), .NUM_CE(1), .DYN_CLKDIV_INV_EN("FALSE"), .DYN_CLK_INV_EN("FALSE"), .OFB_USED("FALSE"), .IOBDELAY("IFD"), // 从IDELAYE2取数据 .SERDES_MODE("MASTER") ) u_iserdese_data ( .O(oq_out), .Q8(q8), .Q7(q7), .Q6(q6), .Q5(q5), .Q4(q4), .Q3(q3), .Q2(q2), .Q1(q1), .SHIFTOUT1(), .SHIFTOUT2(), .BITSLIP(bitslip), // 字节对齐用 .CE1(1'b1), .CE2(1'b1), .CLK(clk_hs), // 高速DDR时钟 .CLKB(clk_hs_b), // clk_hs 的反相时钟 .CLKDIV(clk_div), // 并行字节时钟 .CLKDIVP(1'b0), .D(delayed_data), // 延时后的MIPI数据 .DDLY(1'b0), .RST(!reset_n_hs), .SHIFTIN1(1'b0), .SHIFTIN2(1'b0), .DYNCLKDIVSEL(1'b0), .DYNCLKSEL(1'b0) );这段代码的关键点有三个。一是IBUFDS_DIFF_OUT里的DIFF_TERM属性要打开,这样FPGA内部会接上100欧姆差分终端电阻,符合MIPI的终端匹配要求。二是IDELAYE2的延时值不能拍脑袋填,要在调试的时候用一个固定测试图案逐级扫描,找到最稳的采样窗口。三是BITSLIP信号,它负责把串行bit流的边界往左或往右“滑”,直到看到训练码字,这一步也叫字节对齐或者字对齐。
时钟通道的处理逻辑类似,同样走IBUFDS_DIFF_OUT,然后进BUFIO驱动ISERDESE2的CLK/CLKB,再进BUFR生成一个并行域时钟clk_div,这个clk_div就是后续所有并行逻辑的基准时钟。我建议把BUFIO到ISERDESE2之间的时钟走线尽量靠近,时钟偏斜大了,高速采样直接废。
3.3 字节对齐与核心训练状态机
解串出来的初始bit流没有任何对齐保证,不同lane的解串器采样起点可能差好几个bit。MIPI CSI-2的SoT(Start of Transmission)序列是固定的二进制图案,通常是0xB8(前导码)加后面的0x00,对齐逻辑就是靠找这个图案来做的。
实现思路是:把每条lane解出来的8bit数据持续比较,如果没对齐就置位bitslip拉高一个周期,然后重新采样,直到8bit数据里出现SoT的前导码。等所有lane都对齐之后,再开始往后解析包头的ECC字段,做包完整性校验。这里有一个容易踩的坑:MIPI的SoT图案出现在高字节还是低字节,跟数据线的连接顺序有关,如果你把传感器的DP/N接反了,对齐模块会一直在错误状态里跳不出来。新板子回来先检查这个。
对齐完成后,数据就能按字节正常送入后续的FIFO和图像处理链了。对于CSI-2来说,后续还要做包头解析、payload重组、CRC校验等一层一层的协议栈处理,但物理层接收部分到这里已经算通了。这时候你可以用ILA抓一下解串后的数据,如果能看到规律的0xB8开头帧头,说明LVDS直连方案已经跑通。
4. Xilinx 7系列配置指南实战手册
光有HDL代码还不够,Xilinx 7系列的引脚和时钟规划非常讲究,稍有不慎就算逻辑写对了也跑不起来。这一节我把Vivado里涉及的配置、约束和跨时钟域处理完整过一遍。
4.1 Vivado中的引脚约束与电平配置
用LVDS直连方式接MIPI时,引脚约束要特别注意bank电压。MIPI的信号共模电压很低,但7系列HP bank的VCCO通常接1.5V或1.8V才能让LVDS接收器正常工作。这一点要跟PCB的供电设计配合好,别把MIPI信号引到HR bank上,HR bank的最高I/O速率比HP bank低一截,高速数据上去容易翻车。
XDC约束这样写:
set_property PACKAGE_PIN AK19 [get_ports {data_p[0]}] set_property IOSTANDARD LVDS_25 [get_ports {data_p[0]}] set_property PACKAGE_PIN AK18 [get_ports {data_n[0]}] set_property IOSTANDARD LVDS_25 [get_ports {data_n[0]}] set_property DIFF_TERM TRUE [get_ports {data_p[0]}] set_property IBUF_LOW_PWR TRUE [get_ports {data_p[0]}] # 差分引脚成对约束 set_property PIN_GROUP data_p[0] [get_ports {data_n[0]}]IOSTANDARD这里我写的LVDS_25,具体要匹配你bank的VCCO。如果VCCO是1.8V,可以写LVDS_25这个名称不一定对,实际上7系列支持LVDS、LVDSEXT、LVDS_25、LVDS_18等,选错标准会编译报错或者实际电平不对。查手册确认你的bank电压等级,然后选对应的LVDS标准。差分终端电阻必须打开,因为MIPI链路要求接收端提供100Ω差分终端,这个靠外置电阻也行,但绝大多数情况用内部DIFF_TERM就够了,还能省PCB空间。
另外输入端别忘了加AC耦合电容,这个不是Vivado约束能管的,但必须在PCB上实现。没有AC耦合的后果是摄像头端的TX共模电压和FPGA端的RX共模电压直接打架,轻则波形劣化,重则烧IO。
4.2 高速时钟规划与跨时钟域处理
MIPI的时钟频率跟传感器输出的像素时钟往往不是一个频率。比如一个1080p30的摄像头,数据lane跑800Mbps,DDR时钟400MHz,但图像处理链路的像素时钟可能只有148.5MHz。两个域之间必须用异步FIFO做桥接,否则数据根本没法稳定传递。
我习惯的做法是:ISERDESE2出来的8bit数据,用一个xpm_fifo_async把数据从clk_div域(MIPI恢复时钟域)安全地转到图像处理域(比如100MHz或者150MHz的全局时钟域)。FIFO深度按最大帧大小加一些余量来定,通常1080p单帧2K字节以上,选8192深度足够。如果要做多lane拼接,要注意每条lane解出来的字节必须按lane顺序重新组合,这个拼接逻辑要放在异步FIFO之前做,因为lane之间的时序本来就同源,跨域之后再做字节顺序调整会非常痛苦。
xpm_fifo_async #( .FIFO_MEMORY_TYPE("auto"), .FIFO_WRITE_DEPTH(8192), .WRITE_DATA_WIDTH(32), .READ_DATA_WIDTH(32), .READ_MODE("std") ) u_fifo_mipi ( .wr_clk(clk_div), .wr_rst(~reset_n_hs), .wr_en(wr_en), .din(wr_data_bus), .rd_clk(pixel_clk), .rd_rst(~reset_n_pix), .rd_en(rd_en), .dout(rd_data_bus), .full(wr_full), .empty(rd_empty) );跨时钟域这里要说明一个细节:wr_data_bus要先把四条lane的并行字节拼接成32bit再写入FIFO,这样读侧恢复出像素数据的效率最高。写侧Wait状态由wr_full信号反馈回上游状态机即可,这样MIPI端永远不会丢数据。
4.3 调试手法:ILA和示波器怎么配合看MIPI波形
调试MIPI接口跟调试普通UART不是一个思路。MIPI HS信号的宽度很窄,普通逻辑分析仪根本抓不了。正确姿势是先用示波器确认物理层正常,再用ILA在FPGA内部确认逻辑层正常。
示波器一定要用差分探头,或者至少有1GHz以上带宽的探头用两通道做A-B差分。探头接到MIPI时钟差分对上,先看能不能采到稳定的差分时钟。MIPI时钟信号在高速状态下是持续的方波,屏上看起来应该是一串幅度稳定、频率精确的波形;看到幅度在100mV到300mV之间飘忽不定,多半是AC耦合电容贴错、偏置不对,或者PCB走线太长。确认时钟有了之后,再切到数据lane看HS bursts是否出现,出现大片的空隙和突发传输是正常的,因为MIPI在每帧之间会退回LP状态省电。
示波器确认物理层没问题,下一步才轮到Vivado里的ILA。ILA只能抓单端信号,所以你要抓的是data_p_int或者解串后的并行总线。建议先抓时钟域的clk_div和复位信号,再抓解串后的原始bit总线,最后抓字节对齐状态机。我调试时的习惯是把IDELAYE2的键值做成可在线修改的寄存器,这样用Vivado的VIO(Virtual IO)就能在调试界面里实时滑动延时值,观察采样的数据是否稳定。整个过程不用反复重新编译,效率能高很多。
还有个小技巧:ILA的触发条件里,直接设捕获值为匹配SoT图案0xB8。这样只要MIPI链路一进入HS突发,ILA就会立刻抓到一堆数据包,不用手动在那数波形数得眼睛疼。抓到之后再frame by frame看,数据错位情况一目了然。
5. 常见问题与排查技巧实录
每次帮人看MIPI调试问题,翻来覆去就那么几个坑。我整理一下最常遇到的场景和对应的排查思路,建议大家直接收藏当速查表用。
5.1 解串数据完全对不上,全部是花屏乱码
这种情况大概率不是逻辑问题,而是物理链路有问题。先按这个顺序查:示波器看MIPI时钟有没有稳定的HS时钟;差分探针量数据lane的HS burst幅度是否在正常范围;用ILA抓clk_div域里解串后的原始数据,看看是不是全0或全1。全0全1代表ISERDESE2压根没采到有效信号,问题在IBUFDS级别,可能引脚约束等不匹配、终端电阻没开、bank电压不对;采到数据但乱七八糟,才轮到怀疑IDELAYE2延时值和bitslip对齐的问题。
IDELAYE2延时值调的时候要看数据稳定窗口。用一个固定的测试图像,把延时值从0扫到31,每档抓几百个像素,算一下错误率,错误率最低的那段中间值就是最佳采样点。听起来很原始,但实测特别管用,30分钟内就能扫完,比瞎猜快得多。
5.2 外接桥接芯片初始化失败或输出无信号
用桥接芯片的时候,坑通常在初始化时序上。很多桥接芯片需要先给参考时钟,等时钟稳定后,再拉复位,然后通过I2C写寄存器配置;配置顺序错了就会锁死。我踩过的一个具体坑是:某颗桥接芯片要求参考时钟先于MIPI CSI信号出现,但我一直用FPGA的时钟错误地把桥接芯片的复位拉高,导致它的内部PLL一直锁不上。
对策是每次上电后延时至少10ms再拉复位,复位释放后再等待PLL锁定状态信号拉高,然后才能通过I2C写配置。标准I2C初始化代码框架可以直接照搬,但寄存器配置值要对着芯片手册逐条核对,尤其是那些带双向lane和时钟方向的寄存器,写反了必然跑不通。还有一点容易被忽略,桥接芯片正常工作需要参考时钟,比如24MHz晶振或者FPGA产生的一个稳定时钟喂给它,这个时钟要用LVCMOS电平驱动,不能用差分跨上去,否则芯片可能起振不了。
5.3 速查表:MIPI DPHY关键参数与排查清单
| 检查项 | 参考值/标准 | 排查提示 |
|---|---|---|
| HS差分摆幅 | 单端100~300mV,差分200mV左右 | 用示波器AC耦合测,靠近摄像头端测 |
| HS差分终端 | 接收端100Ω差分 | 确认FPGA内部DIFF_TERM已打开 |
| LP电平范围 | 0~1.2V单端CMOS | 用GVIO普通IO采样,别走差分通路 |
| AC耦合电容 | 典型100nF~220nF | 贴错值会导致HS幅度异常 |
| 高速时钟频率 | 取决于像素时钟和通道数 | 用频谱仪或示波器FFT看频谱 |
| ISERDESE2采样窗口 | 需要扫描IDELAYE2值 | 用测试图案扫描最佳采样点 |
| 字节对齐图案 | CSI-2 SoT:0xB8 | 用ILA触发0xB8 |
| 跨时钟域 | MIPI时钟域到像素时钟域 | 必须用异步FIFO,不能用寄存器传导 |
最后再多说一句,MIPI这个活儿,看起来是数字逻辑问题,实际上物理层的占比非常高。我见过太多人花半个月死磕Verilog,最后发现是PCB上少焊了一颗AC耦合电容。所以我的习惯永远是:先拿示波器确认物理层OK,再动ILA,最后才是改代码。这套流程走下来,再复杂的MIPI链路也能啃下来。