FPGA驱动MIPI DSI屏:手写RTL替代官方IP的完整实战
发布时间:2026/9/28 16:52:18来源:尧图网络
去年接了一个显示相关的项目平台是Xilinx Artix-7需要在FPGA上驱动一块720x1280的MIPI DSI接口LCD屏做实时图像显示。板卡上没有现成的MIPI输出只有一个FMC扩展口所以从硬件到FPGA逻辑都得自己折腾。动手之前我也很自然地想走捷径Xilinx官方不是有MIPI DSI TX Subsystem IP吗虽然看着配置项复杂但至少是现成的。结果这一试前前后后折腾了两周最后还是决定放弃官方方案从DPHY物理层到DSI协议层全部手写RTL。本文记录的就是这条完整的“踩坑”路线包括为什么官方IP不适合这个场景、MIPI DSI的时序和带宽怎么算、RTL怎么分层、约束怎么写以及上板后那些让人怀疑人生的故障现象怎么排查。这篇文章不是理论科普是工程实操记录。准备接手类似项目、或者正在被MIPI屏折磨的人应该能从中省下不少时间。1. 项目背景与方案选型为什么我放弃了官方IP1.1 项目需求与屏选型先交代具体项目背景。屏的型号这里就不写具体编号了但规格很有代表性720x1280竖屏24-bit RGB4 lane MIPI DSI接口支持60Hz刷新。主控端是Artix-7 XC7A35TVivado版本2018.3。图像源来自FPGA内部处理好的视频流通过AXI4-Stream输入到显示模块。这块屏需要的初始化序列不多但有几个关键寄存器要配比如像素格式、地址模式、TE使能等。这些都不是难题真正的难题在MIPI接口本身。选择Artix-7其实是个很现实的决定成本敏感又不想要Zynq的PS侧。XC7A35T逻辑资源有限LUT大概2万个出头所以后面对IP的资源占用也要严格控制。这为后面放弃官方IP埋下了伏笔。1.2 官方IP的“三座大山”我一开始的打算是直接用Xilinx的MIPI DSI TX Subsystem。查找IP时倒是一搜就有看起来也很正规视频输入走AXI4-Stream初始化序列和寄存器配置走AXI4-Lite物理层由配套的MIPI D-PHY IP完成。但真做起来我遇到三座大山每一座都足以让人崩溃。第一座是时钟域泛滥。这个IP要求的时钟实在是太多了S_AXI_ACLK走AXI寄存器DPI_PIXEL_CLK走像素BYTE_CLK走串行字节LP_CTRL_CLK走低功耗控制还要求多个时钟同步关系。每个时钟都得用MMCM/PLL或者BUFG管好少一个、偏一个IP就会在仿真或上板时报莫名其妙的状态。Vivado里接入这么多时钟布线资源和约束复杂度都上去了。第二座是初始化序列的下发方式。官方IP里屏的初始化命令要通过AXI4-Lite寄存器一条一条写进去寄存器偏移地址都在IP手册里。倒不是手册不给而是不同Vivado版本的寄存器映射还略有不同网上搜到的例程和手册v版本经常对不上。最麻烦的是屏厂提供的初始化序列往往很长动辄几百个字节IP的寄存器空间有限还得改寄存器窗口、分批写。调试过程中你根本不知道屏是在哪一行命令之后才出问题的。第三座是物理层IP的授权和配置。7系列没有硬核MIPI D-PHY只能用软核D-PHY IP。这个D-PHY IP本身还需要单独选型部分版本上板有授权限制不支持就只能在仿真里看波形没法实际点亮。对做硬件的人来说看到这种要求基本就想放弃了。1.3 什么时候才建议用官方IP这里不是一棍子打死官方IP。如果你的项目用了带硬核MIPI PHY的UltraScale器件、正好分辨率也跟IP例程一致、而且团队有Xilinx技术支持或者已经熟悉这套IP的寄存器体系那用官方IP是合理的毕竟省了自己维护协议栈。但在低成本7系列、非标准分辨率、初始化序列复杂的小项目里官方IP的灵活性和可观测性都很差。出问题时IP内部信号看不到协议层和物理层协作黑盒根本没法定位是物理时序问题还是协议包问题。手写RTL虽然工作量不小但至少每个状态、每个信号都在你掌控之中出了故障能自己分析。2. MIPI DSI协议与FPGA侧的时钟规划2.1 D-PHY物理层、LP与HS状态机MIPI DSI分三层物理层D-PHY、协议层DSI、应用层视频流/命令。FPGA侧写RTL时最花时间的就是D-PHY。D-PHY的工作模式分LPLow Power和HSHigh Speed。LP模式下差分线Dp和Dn分别用普通数字电平表示逻辑组成LP-00、LP-01、LP-10、LP-11四种状态。屏的初始化命令、寄存器读写、休眠唤醒都在LP模式下通过状态翻转进行。HS模式下Dp/Dn则是一对高速差分信号用于传输像素数据和快速命令差分摆幅很低频率很高。从LP进入HS并不是直接切要经过一段严格的状态序列。简单说就是从LP-11空闲态出发让Dn先拉低再让Dp拉低形成HS-0的建立过程然后发送一串同步码SoT接收端才认为高速数据开始。同样高速传输结束后也要经历HS-Trail和LP-11恢复。这些状态停留时间有最小/最大值要求比如T_LPX、T_HS_PREPARE、T_HS_ZERO、T_HS_TRAIL、T_HS_EXIT等。对于FPGA来说这意味着至少需要一个状态机来管理LP/HS切换并且要精确计时。我用的基准时钟是byte_clk也就是串行速率的1/8。每个状态停留多少个周期都要根据实际时钟频率算好。后面在3.2节我会给出代码骨架。2.2 数据带宽计算与时钟设计手写MIPI驱动第一步不是写代码而是算时钟。这个算不明白后面全是坑。以720x128060Hz为例。屏厂给的行场消隐参考值一般是水平HBP80HFP48HSYNC16所以H_TOTAL720801648864垂直VBP12VFP8VSYNC4所以V_TOTAL128012481304帧率约60Hz像素时钟计算 pclk H_TOTAL x V_TOTAL x 60Hz 864 x 1304 x 60 ≈ 67.6MHz如果直接让DSI以24-bit RGB传输所需原始带宽就是 带宽 pclk x 24bit 67.6MHz x 24bit ≈ 1.622Gbps四个lane平均 每lane速率 1.622Gbps / 4 ≈ 405.6Mbps这还只是纯视频数据没算包头、包尾、Lane空闲和保护间隔。实际取整的时候我留了20%以上裕量最终把lane rate定在500Mbps。这样byte_clk就是 byte_clk 500Mbps / 8 62.5MHz参数数值说明分辨率720x1280x24bitRGB888帧率60Hz含消隐约67.6MHz pclk所需总带宽1.622Gbps不含协议开销Lane数4每lane约405.6Mbps实际lane rate500Mbps留约23%裕量byte_clk62.5MHzlane rate的1/8这里要提醒一个工程细节pclk是67.6MHzbyte_clk是62.5MHz两者并不整除所以像素数据不能简单用同一个时钟搬运必须在中间加异步FIFO。写时钟用pclk读时钟用byte_clkFIFO深度按两行数据量起步防止消隐期间突发的数据堆积导致溢出。2.3 DSI包格式与初始化序列DSI协议层传两种包短包和长包。短包用于寄存器配置和简单命令固定4字节结构是Data Type1字节、WordCount2字节通常为0、ECC1字节。比如Sleep Out0x11、Display On0x29、Set Pixel Format0x3A这些DCS命令都用短包发送。长包用于批量写入。结构是4字节包头Data Type Word Count ECC接着是数据payload最后是2字节CRC。初始化序列里那些一长串厂商寄存器配置比如0xB0开头的定制命令基本都用长包发送。表里列出几个我实际用到的Data TypeData Type含义常见用途0x05Short Write No Parameter简单的零参数DCS命令0x15Short Write One Parameter带1字节参数的DCS命令0x39Generic Long Write通用长数据包0x29DCS Long WriteDCS规范长命令0x37Set Maximum Return Packet Size设置读回包长度CRC计算用的是CRC-16/XMODEM多项式0x1021初值0xFFFF对“包头payload”整体计算。这个在FPGA里用LFSR逐位算就行很简单。ECC是汉明码很多屏其实不校验但规范上短包必须带我后来写了个查表逻辑出来资源不多顺手就做了。初始化序列的发送顺序非常关键。屏规格书会给你一长串命令比如先把像素格式设为RGB888再设置地址模式旋转方向开启TE使能Sleep Out延时几十毫秒最后Display On。顺序错了、延时短了屏都是白屏。这些命令在FPGA里建议放到ROM或者RAM里由状态机逐个发送具体架构在3.4节说。2.4 Xilinx 7系列FPGA架构映射7系列FPGA没有真正的D-PHY硬核但可以利用OSERDESE2和OBUFTDS/OBUFDS实现HS高速输出和LP三态控制。OSERDESE2能做到8:1串行化也就是把8位并行数据转成1位高速串行输出。配合DDR模式可以让内部逻辑时钟工作在byte_clk而串行数据以8x速率从IOB输出。对于500Mbps lane ratebyte_clk是62.5MHzOSERDESE2工作在62.5MHz输出DDR刚好8:1。OBUFTDS是差分离散三态输出缓冲T信号控制是否输出。HS模式下T拉低数据直接输出LP模式下T拉高输出高阻同时用额外的GPIO来控制Dp/Dn的LP电平。这样设计最关键的一点HS和LP不能打架状态机必须保证先让OBUFTDS切到高阻再切LP驱动不然线上会出现毛刺。3. 从零手写RTL完整实现路径3.1 顶层模块划分正式开写前我把整条链路拆成几个模块每个模块职责单一方便仿真和ILA调试dphy_tx_ctrlD-PHY物理层状态机负责LP/HS切换、LP电平输出、HS数据输出控制dsi_packet_gen协议层包生成支持短包和长包自动加上ECC和CRCdsi_video_ctrl视频时序控制器从异步FIFO读像素按行场时序切成DSI burstdsi_init_engine初始化序列引擎从RAM/ROM读命令字节按命令间隔延时async_fifo_video跨时钟域FIFOpclk写、byte_clk读模块之间的连接关系是init_engine或video_ctrl产生需要发送的包packet_gen把包内容拼成DSI格式dphy_tx_ctrl最终把串行数据和LP状态送到引脚。视频流和初始化命令共享同一对差分线所以两者必须互斥不能同时占用。我加了一个busy仲裁信号谁请求谁发送写起来简单且可靠。3.2 DPHY发送控制器LP/HS状态机关键代码DPHY状态机是整个设计最核心的部分。我的状态编码是这样的localparam S_LP_STOP 4d0; // LP-11 空闲 localparam S_LP_ENTRY 4d1; // 进入LP握手 localparam S_HS_PREP 4d2; // HS准备 localparam S_HS_ZERO 4d3; // HS建立 localparam S_HS_SYNC 4d4; // 发送SoT同步码 localparam S_HS_DATA 4d5; // 高速数据 localparam S_HS_TRAIL 4d6; // HS结束 localparam S_LP_EXIT 4d7; // 回到LPHS模式下OSERDESE2持续输出数据来源由packet_gen提供。LP模式下Dp和Dn作为普通GPIO输出状态机的LP_ENTRY和LP_EXIT部分负责发送SoT/EoT序列。为了精确计时我引入T_HS_PREP等常量单位是byte_clk周期。比如500Mbps时byte_clk周期16nsDPHY要求的T_HS_PREPARE约40ns所以至少计数3拍。OSERDESE2的例化我摘一段OSERDESE2 #( .DATA_RATE_OQ(DDR), .DATA_RATE_TQ(BUF), .DATA_WIDTH(4), .SERDES_MODE(MASTER), .TRISTATE_WIDTH(1) ) u_oserdes_dp ( .D1 (ser_data[0]), .D2 (ser_data[1]), .D3 (ser_data[2]), .D4 (ser_data[3]), .D5 (1b0), .D6 (1b0), .D7 (1b0), .D8 (1b0), .TCE (1b1), .OCE (1b1), .CLK (ser_clk), // 500MHz .CLKDIV (byte_clk), // 62.5MHz .RST (rst), .T (tx_oe ? 1b0 : 1b1), .OQ (dphy_dp_int), .OFB (), .TQ () );注意这里T端口我直接接tx_oe当tx_oe为低时数据正常输出为高时输出高阻。OBUFTDS再把这个差分信号接到物理引脚OBUFTDS #(.IOSTANDARD(LVDS_25)) u_obuftds_dp ( .I (dphy_dp_int), .T (tx_oe ? 1b0 : 1b1), .O (dphy_dp_p), .OB(dphy_dp_n) );用LVDS_25直接驱动D-PHY其实是靠着短距离和FMC连接器上接触良好才能工作的。严格来说D-PHY的HS共模电平约200mV和LVDS的1.25V共模差很多长时间运行或者长走线会有可靠性问题。如果是量产产品建议用专门的MIPI DSI桥接芯片或者电阻网络做电平适配不要拿LVDS硬怼。这个我在后面踩坑里还会提。3.3 视频时序生成与数据FIFO设计视频发送的思路是按照面板的行场时序在消隐期间尽量让线空下来进入LP模式在有效显示区间把所有像素数据通过DSI长包打出去。这种方式就是DSI BURST MODE。我从AXI4-Stream或者内部图像模块接收像素先存入异步FIFO。FIFO读端速率是byte_clk写端是pclk。像素是24bitDSI一次长包最好按字节对齐所以FIFO位宽我设计成64bit每次读出8个像素再拆成24字节一个长包发出去。视频包的数据包类型我仍然用0x39通用长包发送因为很多屏对0x29和0x39都支持0x39在初始化序列里也会用到逻辑复用更简单。视频长包与命令长包的区别只在于Word Count和payload内容。行时序上我让整个行有效区都处于HS模式连续发送一行720个像素24bit RGB共2160字节。由于带宽有余量每一行发完很快就结束剩下的HFP、HBP时间里DPHY回到LP-11状态这也是省功耗的地方。异步FIFO必须有 almost_empty 和 almost_full 信号。视频模块只在 almost_full 为低时才写入否则图像源前一级停一拍。否则帧率不匹配时FIFO会溢出屏幕直接撕裂。3.4 初始化序列执行引擎初始化序列引擎说白了就是一个状态机加一块命令存储。我的实现方式是在Xilinx Block Design里加一个MicroBlaze软核通过AXI-Lite接口往FPGA侧的小RAM里写初始化命令字节写完后写一个启动寄存器init_engine就开始按命令表发送。每条命令的存储格式是一张三元组表命令类型、数据长度、延时时间。命令类型用来区分短包还是长包数据长度告诉引擎要发多少字节延时时间指的是发完这条命令后要等待多少ms。比如Sleep Out通常要等120ms这期间引擎就停在等待状态发一个计数器脉冲过去。如果不用软核也可以用纯RTL状态机把命令表写死在ROM里。临时项目这样最省事但后期换屏就要改RTL重新综合。我这边因为有MicroBlaze所以把命令表放RAM调试时直接改软件不用重新编译硬件。这里给出一小段命令存储格式的Verilog定义typedef struct packed { logic [1:0] pkt_type; // 2b00: short, 2b01: long logic [15:0] len; // payload byte count logic [31:0] delay_us; // post-command delay in us } cmd_entry_t;命令表按字节顺序存放在RAM里引擎逐个解析。命令发送完成后状态机拉高 init_done 信号随后视频模块才开始送图像数据。这个先后顺序很关键。很多屏如果Display On命令还没发就来了视频流会直接忽略掉结果就是一直黑屏。3.5 约束文件XDC的关键点MIPI DSI是高速差分接口XDC约束不能只写个引脚就完事。我实际用的关键约束大概是set_property PACKAGE_PIN AG12 [get_ports {dphy_dp_p[0]}] set_property PACKAGE_PIN AG11 [get_ports {dphy_dp_n[0]}] set_property IOSTANDARD LVDS_25 [get_ports {dphy_dp_p[0]}] set_property IOSTANDARD LVDS_25 [get_ports {dphy_dp_n[0]}] set_property DIFF_TERM TRUE [get_ports {dphy_dp_p[0]}] set_property PACKAGE_PIN AH12 [get_ports {dphy_clk_p}] set_property PACKAGE_PIN AH11 [get_ports {dphy_clk_n}] set_property IOSTANDARD LVDS_25 [get_ports {dphy_clk_p}] set_property IOSTANDARD LVDS_25 [get_ports {dphy_clk_n}] set_property DIFF_TERM TRUE [get_ports {dphy_clk_p}]时钟约束方面ser_clk是500MHz如果从PLL输出需要创建生成时钟create_generated_clock -name ser_clk -source [get_pins mmcm_inst/CLKOUT0] \ -divide_by 1 [get_pins oserdes_inst/CLK]实际做下来最容易被忽略的是差分极性。PCB上P/N一旦接反画面会出现颜色分量错位、花屏等问题。排查时先看原理图确认P和N没有反接如果反了就把PACKAGE_PIN对调一下或者换IOSTANDARD里的P/N极性。4. 上板调试实录与踩坑记录4.1 白屏问题的排查第一次上板屏背光亮但画面全白。这种问题最多见也最需要系统性排查。第一步检查初始化序列有没有真正执行完。我在init_engine里加了一个计数器整段命令表发完以后拉高init_done同时把计数值通过GPIO送到板上的LED。实测init_done始终为低说明初始化卡在中间的延时等待。用ILA抓了state寄存器发现引擎一直停在Sleep Out后的120ms等待状态。MicroBlaze侧的延时函数写的是毫秒但计时基准配错了导致实际等待时间远超预期后续命令一直发不出去。改掉定时器配置后init_done能拉高了但屏幕依然是白屏。第二步检查Display On命令。这里有个比较容易忽略的细节很多屏的初始化流程是先Sleep Out等待然后送显示配置最后Display On但Display On之前屏幕其实已经能接受视频数据只是还没开启显示。如果视频模块里没有等init_done就开始送数据像素会被屏内部丢弃。我调整了视频启动条件让init_done为高以后才开始发视频长包。第三步如果还是白屏就要怀疑像素格式。我用0x3A命令配置了RGB888但视频模块发送的像素顺序是BGR888直接被屏显示成蓝色调或花白。后来统一了RGB/BGR顺序并用ILA抓了第一个长包的字节序列逐个字节对才能正常显示。4.2 花屏和半屏的定位白屏之后最常见的是花屏。花屏分好几种每种背后原因不同。现象是颜色顺序错乱、出现彩色噪点基本都是RGB/BGR顺序或者字节序问题。MIPI DSI发送像素时如果按0xRR 0xGG 0xBB顺序发送面板不一定按这个顺序解释要按面板规格书来。排查时我用ILA抓一串像素数据和屏厂给的参考波形比对。现象是整个画面偏移、左侧有异常色带这是porch参数不对面板的HBP和HFP和视频模块配置不一致。比如视频模块按HBP80先生成消隐但屏端实际等到了120个像素时钟才开始采样图像就会整体偏移。这时候不要凭空猜直接把初始化代码里的porch参数和视频模块的porch参数对着屏厂规格书一个个查。现象是上半屏正常、下半屏错位这个我在另一个项目里遇到原因是TE信号和帧同步没有对齐。面板用TE信号作为内部帧同步基准视频源必须保证一帧数据从正确时刻开始发。如果FIFO在读空之后重新启动帧头位置漂了下半屏就会错。解决方法是把TE信号引入FPGA用TE上升沿清空FIFO并重新启动帧计数。4.3 LP通信正常但HS无输出这个是我在调试中最头疼的问题之一。初始化序列能发说明LP通信正常但初始化完成后屏幕没有反应ILA显示HS数据确实在发送但用示波器在引脚上却看不到差分信号。先怀疑OSERDESE2配置反复对着手册查了几遍没问题。后来用示波器单端探头去量Dp对地发现信号大概有300mV左右的噪声不像正常的0-200mV差分跳变。最后定位到是OBUFTDS的T端口时序问题tx_oe拉低的同时OSERDESE2的输出数据还没有稳定导致HS建立阶段出现毛刺D-PHY接收端根本识别不了SoT。解决办法是在状态机里加了一拍缓冲先让tx_oe拉低、等待至少2个byte_clk周期之后再送SoT同步码。这对很多FPGA设计都是个容易踩的坑HS信号必须在差分线上稳定建立以后才能开始传数据否则接收端会误判。4.4 常见问题速查表现象可能原因排查思路白屏背光亮初始化命令未发完 / Sleep Out后延时不够 / Display On未执行抓init_done确认命令表和延时ILA监测状态寄存器花屏颜色错乱RGB/BGR顺序错 / 字节序错用ILA对比第一个长包字节序列与屏厂规格整体偏移HBP/HFP与屏端不符核对porch参数与初始化代码上半屏正常下半屏错位TE信号未同步帧头漂移将TE引入FPGA用TE上升沿复位FIFO读写指针HS发送了但屏无反应OBUFTDS的T端口时序不对 / SoT毛刺示波器看Dp/Dn波形tx_oe拉低后至少延迟2拍再发SoT画面闪、帧率不稳FIFO几乎溢出/读空加大FIFO深度检查almost_full与视频源反压逻辑大量高频噪点排线过长、电平不匹配缩短FPC/FMC走线考虑增加MIPI转接芯片5. 与SDK/软核的配合5.1 通过AXI GPIO联动控制这块屏的调试离不开MicroBlaze但不是必须。我选择MicroBlaze主要是为了灵活修改初始化命令表和PWM调背光。工程上如果初始化序列固定不变纯Verilog状态机完全够用资源更少、启动更快。Block Design里我挂了三个外设AXI GPIO控制背光PWM、断电使能、TE输入AXI BRAM Controller挂一块8KB RAM存初始化命令表AXI Timer用于命令延时和PWM周期MicroBlaze上电后先把命令表写入BRAM然后置位start寄存器。init_engine检测到start后开始读BRAM并执行初始化序列执行完拉高init_done中断给MicroBlaze。这样软件的灵活性体现在换一块不同型号的屏只需要改命令表不需要动RTL。5.2 寄存器设计与MCU交互如果你所在项目有外部MCU或者Zynq PS那控制寄存器就更有必要了。我定义了一组简单的寄存器地址偏移地址寄存器描述0x00CTRLbit0: init_start, bit1: video_enable0x04STATUSbit0: init_done, bit1: fifo_full0x08CMD_ADDR命令表写地址0x0CCMD_DATA命令表写数据0x10TE_CNTTE脉冲计数用于帧率监测这套寄存器接口非常通用Zynq PS、MicroBlaze、甚至外部SPI主机都能访问。如果你不想在PL侧做CPU可以直接把这三个寄存器映射到AXI-Lite从机让外部MCU通过SPI-to-AXI桥来访问。灵活度很高。5.3 调试工具与观测方法MIPI问题用ILA看内部逻辑波形是基本功但一定要看对信号。我通常会把下面这些信号引到ILAinit_engine的statedphy_tx_ctrl的lp_state和hs_statevideo_ctrl的line_cnt和frame_cntfifo的rd_en、wr_en、almost_full、almost_emptytx_oe和dphy_dp_int数据量会很大ILA深度至少选16384采样时钟用byte_clk。用触发条件把tx_oe下降沿触发看第一包HS数据。如果第一包就错了后面的分析都是白费。示波器方面如果带宽不够直接测试Dp/Dn差分信号会很痛苦。我建议用单端探头分别量Dp和Dn看在HS传输时能否看到稳定翻转。如果只有微弱的电平抖动大概率是输出高阻或者电平不匹配。差分探头当然最好但很多项目里不一定有。6. 写在最后这套方案的后续扩展如果你问我现在再做一个MIPI DSI项目我还会选择手写RTL吗我的答案是会但前提是对MIPI协议已经有清晰的理解。手写方案的最大优势是可控性。任何一个IP黑盒不能告诉你的事在RTL里都是自己代码。遇到问题ILA一点就能看到状态机停在哪而不是拿着IP手册一页一页翻寄存器。尤其是在非标分辨率、定制初始化序列、特殊时序要求的项目中这套方案几乎是无敌的。往后扩展的方向也很多比如增加双lane转四lane的分拆逻辑、支持多屏拼接、把初始化命令表放到SD卡里动态加载、用CRC校验回读面板状态等。底层DPHY和DSI包生成模块只要写好了换屏基本只改参数和命令表不用动核心RTL。最后分享一个小技巧如果你第一次做MIPI DSI不要一上来就上屏。先用一块支持loopback的FMC转接板或者逻辑分析仪把D-PHY的LP状态翻转和HS数据抓下来确认没问题再接到屏上。省下的不只是调试时间还有你对着白屏发呆时逐渐崩溃的信心。
网站建设高端定制企业官网