
1. 从一根HDMI线说起为什么FPGA和RP2040都盯上了这块硬骨头很多人第一次冒出“自己驱动HDMI”的念头都是被一个很具体的场景逼出来的手头有一块FPGA开发板或者一块树莓派RP2040想让它往显示器上输出点东西结果发现板子上要么只有VGA要么只有SPI小屏想接个正经的HDMI显示器立刻就卡住了。市面上的HDMI驱动方案要么是专用芯片比如TFP410、ADV7511这类要么是SoC自带的显示控制器一旦你想用纯逻辑或者纯MCU去“手搓”一个HDMI信号就会发现这东西远比想象中麻烦。HDMI接口信号定义看起来不复杂TMDS差分对加上DDC、CEC、HPD这几根线但真正难的是TMDS的编码方式和时序。HDMI协议里每个像素周期要传输10bit的TMDS编码数据其中8bit是像素数据2bit是控制信号而且控制信号和视频数据之间还有一段“控制周期”和“数据岛周期”的过渡。更麻烦的是HDMI的时钟是随路时钟TMDS Clock Channel的频率是像素时钟的1倍而三个数据通道每个周期要传10bit所以实际数据率是像素时钟的10倍。这意味着如果你要输出1080p60像素时钟是148.5MHzTMDS数据率就是1.485Gbps这对FPGA的IO和逻辑都是不小的考验。FPGA做HDMI的优势在于并行处理和时序精确控制。你可以用FPGA内部的OSERDES或者LVDS收发器来生成高速串行信号用状态机精确控制控制周期和数据岛的插入时机。Intel FPGA的XAPP523就是一个经典的HDMI发送参考设计里面详细讲了如何用SelectIO资源实现TMDS编码和并串转换。而树莓派RP2040做HDMI则完全是另一条路——RP2040没有专用的高速串行接口它的PIO可编程IO虽然灵活但最高时钟也就133MHz左右要直接生成1.485Gbps的TMDS信号几乎不可能。所以RP2040驱动HDMI通常需要外挂一个TMDS编码芯片或者用PIO做低速的HDMI信号比如640x48060Hz像素时钟25.175MHzTMDS数据率251.75Mbps通过巧妙的PIO编程和DMA配合勉强能跑起来。这两个方向代表了两种完全不同的技术路线FPGA走的是“硬核逻辑”路线追求高分辨率和高刷新率RP2040走的是“低成本MCU”路线追求的是用几块钱的芯片就能点亮显示器。无论你选哪条路核心要解决的问题都是一样的TMDS编码、时序生成、时钟管理和DDC通信。接下来我会把这两条路都拆开讲清楚包括具体的资源推荐、代码思路和踩坑经验。2. FPGA驱动HDMI从XAPP523到你自己能跑的TMDS发送器2.1 为什么XAPP523是绕不开的起点Intel FPGA的XAPP523现在归AMD了但文档还在是很多人做HDMI发送的入门参考。它讲的是如何用Spartan-3系列的SelectIO资源实现TMDS发送虽然芯片型号老了但原理完全通用。核心思路是用FPGA内部的并串转换器OSERDES把10bit的TMDS编码数据串行化然后用LVDS差分对输出。TMDS编码本身不复杂就是8bit转10bit的查表操作但关键在于控制周期和数据岛的插入。TMDS编码的规则是这样的每个像素周期三个数据通道分别传输R、G、B的8bit数据经过TMDS编码后变成10bit。编码的目的是保证直流平衡和减少电磁干扰。具体来说TMDS编码会根据当前数据的“1”的个数和上一次传输的“1”的个数之差决定是否对数据取反并插入一个额外的bit来指示是否取反。这个过程可以用一个简单的状态机实现但要注意的是控制周期CTL0-CTL3和数据岛周期Guard Band、Data Island的编码规则和视频数据周期完全不同。XAPP523里给出的参考设计包含了一个完整的TMDS编码器和一个并串转换模块。并串转换模块用到了FPGA的OSERDES原语把10bit并行数据用5倍或10倍的时钟串行化。这里有个关键点OSERDES的时钟需要和像素时钟保持严格的相位关系否则输出眼图会很差。通常的做法是用一个PLL生成像素时钟的5倍频或10倍频时钟然后用这个高速时钟去驱动OSERDES。对于1080p60像素时钟148.5MHz5倍频就是742.5MHz这个频率对大多数FPGA的OSERDES来说是可以接受的。2.2 自己写TMDS发送器时最容易忽略的三个细节第一个细节是控制周期的插入时机。HDMI协议规定在视频数据周期之前必须先发送一段控制周期用来传输HSYNC、VSYNC和CTL信号。控制周期的长度通常是2个像素时钟周期但具体取决于你的时序参数。如果你忘了插入控制周期显示器可能能识别到信号但图像会偏移或者抖动。我在第一次做的时候就是直接连续发送视频数据结果显示器显示的画面左边总是缺一块后来加了控制周期才正常。第二个细节是数据岛的Guard Band。如果你要传输音频或者辅助数据需要在数据岛前后插入Guard BandGuard Band的编码是固定的CTL01, CTL10, CTL20, CTL30用来给接收端做时钟恢复的过渡。即使你不传音频有些显示器也会检查Guard Band的存在如果没有可能会拒绝显示。所以稳妥的做法是即使不传音频也在每帧的消隐区插入一个空的数据岛和Guard Band。第三个细节是DDC通道的I2C通信。HDMI的DDC通道本质上是一个I2C总线用来读取显示器的EDID信息。很多初学者以为只要把TMDS信号发出去就行了结果显示器黑屏原因就是没有正确读取EDID或者没有响应HPD信号。HPD是显示器发给源端的信号表示“我已经准备好了你可以发信号了”。如果你不检测HPD直接在显示器还没准备好的时候就发TMDS显示器可能根本不理你。正确的流程是先检测HPD为高然后通过DDC读取EDID解析出显示器的支持分辨率和时序参数再根据这些参数配置TMDS发送器。2.3 资源推荐从开源项目到开发板选型如果你不想从零开始写TMDS发送器有几个开源项目可以直接参考。第一个是hdmi-rs这是一个用Rust写的HDMI发送库虽然主要是针对嵌入式Linux的但里面的TMDS编码逻辑和时序生成代码很有参考价值。第二个是fpga4fun上的HDMI发送实验作者用Verilog写了一个简单的TMDS发送器支持640x48060Hz代码很简洁适合入门。第三个是Xilinx的XAPP460讲的是如何用Spartan-6的LVDS资源做HDMI发送比XAPP523更新一些。开发板选型方面如果你只是想入门一块黑金FPGA开发板比如AX301或者AX4010就足够了它们通常带HDMI输出接口板载的FPGA资源也够跑1080p60。如果你想要更高级的功能比如HDMI输入或者4K输出那就需要选带GTP/GTX收发器的FPGA比如Xilinx的Artix-7或者Kintex-7系列。注意HDMI 2.0的4K60Hz需要600MHz的像素时钟TMDS数据率高达6Gbps普通的LVDS IO跑不了必须用高速收发器。3. 树莓派RP2040驱动HDMI用PIO和DMA硬生生挤出TMDS信号3.1 RP2040的PIO到底能不能跑HDMIRP2040的PIO是一个很神奇的东西它本质上是一个可编程的状态机可以精确控制GPIO的时序。每个PIO有4个状态机每个状态机可以独立执行一段小程序时钟频率可以到系统时钟默认133MHz。理论上如果你用PIO来生成TMDS信号每个bit的持续时间是1/251.75MHz≈3.97ns而PIO的时钟周期是1/133MHz≈7.5ns也就是说一个PIO时钟周期只能输出一个bit但TMDS需要每个像素周期输出10bit所以PIO的时钟频率必须至少是像素时钟的10倍。对于640x48060Hz像素时钟25.175MHz10倍就是251.75MHz这超过了RP2040的PIO最高时钟133MHz。所以直接用PIO生成TMDS信号是不可能的必须用一些技巧。常见的技巧有两种第一种是用PIO生成低速的TMDS信号比如把分辨率降到320x24060Hz像素时钟12.5875MHz10倍就是125.875MHz刚好在PIO的时钟范围内。第二种是用PIO做并串转换的辅助外挂一个TMDS编码芯片比如TFP410PIO只负责生成并行数据和时钟TMDS编码和串行化交给专用芯片。第一种方案成本最低但分辨率也最低第二种方案成本稍高但可以支持1080p甚至更高。3.2 用PIO实现TMDS编码的完整思路如果你选择第一种方案用PIO直接生成TMDS信号那么核心思路是这样的PIO状态机在每个像素周期内依次输出10个bit的TMDS编码数据。由于PIO的时钟频率是像素时钟的10倍所以每个PIO时钟周期输出一个bit。TMDS编码本身可以在CPU或者DMA里预先算好存到一个缓冲区里然后PIO用DMA从缓冲区里读取数据并输出。具体实现上你需要三个PIO状态机分别对应R、G、B三个数据通道。每个状态机从DMA读取10bit的数据然后通过移位寄存器逐位输出到GPIO。时钟通道可以用一个额外的PIO状态机或者直接用PWM生成。这里的关键是DMA的传输速率要跟得上否则会出现数据断流。RP2040的DMA可以配置为每次传输10bit或者16bit取低10位传输触发源是PIO的TX FIFO空信号。只要DMA的优先级够高通常不会断流。TMDS编码的查表可以在CPU里预先算好存到一个数组里。对于640x48060Hz一帧有800x525个像素周期包括消隐区每个像素周期需要3个10bit的TMDS编码总共需要800x525x3x10bit≈12.6Mbit的缓冲区。RP2040有264KB的SRAM存一帧的TMDS数据肯定不够所以需要实时计算或者用双缓冲。实时计算的话CPU要在每个像素周期内完成TMDS编码这对RP2040的133MHz主频来说有点吃力但如果你用查表法每个像素周期只需要几次内存访问还是能跑起来的。3.3 实测中的坑时钟抖动、DDC通信和电源噪声我在用RP2040驱动HDMI的时候踩过几个很典型的坑。第一个是时钟抖动。RP2040的PIO时钟是由系统时钟分频得到的分频系数是整数所以像素时钟的精度有限。比如你要生成25.175MHz的像素时钟系统时钟133MHz分频系数是133/25.175≈5.28只能取5或者6实际生成的时钟是26.6MHz或者22.17MHz和标准值偏差很大。显示器对时钟偏差的容忍度有限偏差超过0.5%就可能黑屏。解决办法是用一个外部的晶振或者PLL芯片来生成精确的像素时钟或者用RP2040的PLL来生成一个接近的频率然后通过调整消隐区的长度来补偿。第二个是DDC通信。RP2040的I2C接口可以用来读取EDID但HDMI的DDC通道是5V电平而RP2040的GPIO是3.3V直接连接可能会损坏RP2040。你需要一个电平转换电路或者用两个电阻分压。另外DDC的时钟频率是100kHzRP2040的I2C可以轻松支持但要注意HPD信号的检测。HPD是显示器发出的5V信号你需要用电阻分压到3.3V再接到RP2040的GPIO。第三个是电源噪声。HDMI的TMDS信号是高速差分信号对电源噪声很敏感。如果你用RP2040的GPIO直接输出TMDS信号GPIO的驱动能力有限信号幅度可能不够而且电源噪声会直接耦合到信号上。实测下来最好加一个缓冲器或者用专用的TMDS驱动芯片。另外RP2040的3.3V电源要加足够的去耦电容否则高速切换时电源会塌陷。4. HDMI接口电路设计那些 datasheet 不会告诉你的细节4.1 TMDS差分对的阻抗匹配和ESD保护HDMI的TMDS差分对要求100Ω的差分阻抗这个在PCB设计时就要严格控制。如果你用的是FPGA的LVDS输出FPGA的IO通常可以配置为100Ω差分阻抗但前提是PCB走线也要匹配。走线长度要尽量短差分对之间的长度偏差要控制在5mil以内否则会导致眼图闭合。另外TMDS差分对需要串联一个0.1uF的耦合电容这个电容要放在靠近连接器的地方容值不能太大否则会影响信号边沿。ESD保护也是必须的。HDMI连接器是暴露在外的很容易受到静电冲击。你需要在TMDS差分对和DDC、CEC、HPD线上都加ESD保护二极管。注意ESD二极管的结电容要小否则会影响高速信号。通常选用0.5pF以下的ESD二极管比如ST的USBLC6-2或者ON Semi的ESD9L系列。4.2 DDC、CEC和HPD的电路设计要点DDC通道是I2C总线需要上拉电阻。HDMI规范要求DDC的SCL和SDA线上拉电阻是1.5kΩ到2kΩ上拉到5V。但你的FPGA或者MCU可能是3.3V的所以你需要一个电平转换电路。最简单的做法是用两个电阻分压但分压会影响I2C的上升沿所以更好的做法是用专用的I2C电平转换芯片比如PCA9306或者TXS0102。CEC通道是消费电子控制通道用来传输遥控器信号。如果你不需要CEC功能可以直接不接。但如果要接CEC是单线双向总线需要开漏输出加上拉电阻。CEC的电压是3.3V可以直接接到FPGA或者MCU的GPIO。HPD是热插拔检测信号显示器通过这个信号告诉源端“我已经连接好了”。HPD是5V信号你需要用电阻分压到3.3V再接到FPGA或者MCU。注意HPD信号可能会有抖动所以最好加一个RC滤波或者用施密特触发器整形。4.3 电源设计和连接器选型HDMI连接器有几种类型Type A标准、Type CMini、Type DMicro。如果你做的是嵌入式项目通常用Type A或者Type C。Type A的引脚间距是0.5mm焊接难度较大建议用带PCB封装的连接器。Type C的引脚间距更小但体积也更小适合便携设备。电源方面HDMI源端需要提供5V的电源给显示器的EDID芯片。这个5V电源要能提供至少50mA的电流并且要有过流保护。你可以用一个小型的LDO或者DC-DC转换器从你的主电源生成5V。注意这个5V电源的噪声要低否则会影响EDID通信。5. 从入门到进阶不同分辨率下的时序参数和资源消耗5.1 常见分辨率的像素时钟和TMDS数据率分辨率刷新率像素时钟TMDS数据率对FPGA IO的要求640x48060Hz25.175MHz251.75Mbps普通LVDS即可800x60060Hz40MHz400Mbps普通LVDS即可1024x76860Hz65MHz650Mbps需要高速LVDS1280x72060Hz74.25MHz742.5Mbps需要高速LVDS1920x108060Hz148.5MHz1.485Gbps需要OSERDES或收发器1920x1080120Hz297MHz2.97Gbps需要高速收发器3840x216060Hz594MHz5.94Gbps需要GTP/GTX收发器从表格可以看出1080p60是一个分水岭。在1080p60以下普通的LVDS IO就能搞定1080p60及以上就需要OSERDES或者高速收发器了。RP2040的PIO最多只能跑到125MHz左右所以它只能支持到800x60060Hz再高就不行了。5.2 FPGA资源消耗估算以Xilinx Artix-7为例一个1080p60的TMDS发送器大概消耗以下资源LUT约500-800个FF约300-500个OSERDES12个3个通道x4个bitPLL1个BRAM0-2个用于存储TMDS查找表这个资源消耗对于Artix-7来说是很小的XC7A35T就能轻松跑下。如果你要做HDMI输入资源消耗会大一些因为需要TMDS解码和时钟恢复大概需要1000-1500个LUT。5.3 RP2040的资源消耗和性能瓶颈RP2040做HDMI发送时主要的资源消耗在CPU和DMA上。以640x48060Hz为例每帧有800x525420000个像素周期每个像素周期需要3次TMDS编码查表和3次DMA传输。如果CPU主频是133MHz每个像素周期有133/25.175≈5.28个时钟周期也就是说CPU只有5个时钟周期来完成3次查表和3次DMA配置这几乎是不可能的。所以实际实现中通常用DMA来搬运预先算好的TMDS数据CPU只负责在消隐区更新缓冲区。但即使这样RP2040的SRAM也只有264KB存不下一整帧的TMDS数据。所以你需要用双缓冲或者行缓冲。行缓冲的话每行800个像素周期需要800x3x10bit24000bit3KB的缓冲区双缓冲就是6KB这个RP2040可以轻松放下。CPU在每行的消隐区计算下一行的TMDS数据然后DMA在行有效区搬运当前行的数据。这样CPU的负载就小很多了。6. 调试HDMI信号时示波器和逻辑分析仪该怎么用6.1 用示波器看TMDS眼图调试HDMI信号示波器是必不可少的。你需要一个带宽至少是TMDS数据率2倍以上的示波器。比如1080p60的TMDS数据率是1.485Gbps你需要一个3GHz以上带宽的示波器。对于640x48060HzTMDS数据率是251.75Mbps500MHz带宽的示波器就够了。看眼图的时候你需要一个差分探头直接测量TMDS差分对。眼图的张开度反映了信号质量如果眼图闭合说明信号完整性有问题。常见的原因包括阻抗不匹配、走线太长、电源噪声、驱动能力不足。你可以通过调整串联电阻、增加去耦电容、缩短走线来改善。6.2 用逻辑分析仪抓DDC和HPD时序DDC和HPD是低速信号用逻辑分析仪抓时序很方便。你需要一个支持I2C协议解码的逻辑分析仪比如Saleae或者DSLogic。抓DDC的时候注意看SCL和SDA的时序确保没有冲突。抓HPD的时候注意看HPD的上升沿和TMDS信号发出的时间关系确保在HPD为高之后才发TMDS。6.3 常见故障排查表故障现象可能原因排查方法显示器黑屏无信号HPD未检测到用万用表测HPD电压显示器显示“无信号”TMDS未发出用示波器测TMDS差分对图像偏移控制周期插入错误检查控制周期的时序图像抖动时钟抖动太大用示波器测像素时钟的抖动颜色错误TMDS编码错误检查TMDS编码器的输出显示器识别不到EDIDDDC通信失败用逻辑分析仪抓I2C波形7. 我个人的一些实操体会做HDMI驱动这件事最深的体会就是“细节决定成败”。TMDS编码本身不难难的是时序的精确控制和信号完整性的保证。我在第一次做的时候以为只要把TMDS编码发出去就行了结果显示器一直黑屏后来发现是HPD信号没有检测显示器根本没准备好。第二次做的时候图像出来了但是左边缺了一块后来发现是控制周期没有插入。第三次做的时候图像正常了但是颜色不对后来发现是TMDS编码的查表搞错了。所以我的建议是如果你刚开始做先从640x48060Hz开始这个分辨率的时序参数简单TMDS数据率低对硬件的要求也低。等你把这个跑通了再逐步提高分辨率。另外一定要用示波器和逻辑分析仪来调试光靠看代码是看不出问题的。最后多参考开源项目和官方文档XAPP523和XAPP460都是很好的起点fpga4fun上的实验也很有参考价值。RP2040的话可以看看GitHub上的pico-hdmi项目虽然它只支持320x240但代码很清晰适合入门。