首页 / 资讯中心 / 文章详情

FPGA串口传输图像至VGA显示:完整数据链路设计与避坑指南

FPGA串口传输图像至VGA显示:完整数据链路设计与避坑指南 ★ FEATURED ARTICLE
简介面向FPGA学习者和数字系统设计爱好者的完整工程资源。项目以UART串口接收PC端图片数据、经FPGA内部缓存与处理后输出至VGA显示器为核心完整覆盖串行通信协议参数设置、RGB三通道图像编码、显存组织方式以及VGA行同步与场同步时序生成等关键知识点可作为课程设计或电子竞赛项目的直接参考。压缩包共366个文件、约25.79MB包含17个Verilog源文件、6个VHDL文件、10个XDC约束文件另有Tcl/Bat自动化脚本、仿真波形与综合日志、Vivado工程配置及可直接烧录验证的bit比特流从源码到上板文件一应俱全目录结构清晰。目前已有1546人学习浏览。借助该资源可对照完整设计流程理解串口收发、图像缓存与VGA驱动模块间的协同机制学习跨模块时序约束、上板调试与仿真比对思路适合正在开展FPGA图像显示项目、需要边读源码边实践的中高级学习者。1. 基于FPGA的串口发送彩色图片数据至VGA显示一个能跑通的完整图像链路做嵌入式图像项目最怕的不是算法难而是数据链路断在半路。串口发数据、FPGA收数据、FIFO缓存、VGA时序输出这条链路里任何一个环节卡住屏幕上就是雪花或者黑屏。很多开发者一开始用IO翻转模拟时序刷个320x240都卡成PPT换FPGA配合串口和FIFO之后640x48060Hz的彩色图片能稳定显示而且整个工程代码量不到一千行。这套方案的适用人群是学过Verilog但没完整做过图像采集显示链路的开发者以及想用现有开发板快速验证图像处理算法的工程师。它能帮你把PC端图片数据通过串口灌进FPGA经过缓存和时序生成最终在VGA显示器上还原出完整画面整个过程不依赖价格昂贵的HDMI方案一块入门级FPGA开发板就能搞定。2. 架构设计与硬件选型为什么串口FIFO是目前最稳的落地组合2.1 系统数据流与模块划分整个系统的数据流向非常明确PC上位机先把BMP格式的彩色图片解析成RGB数据通过UART串口按字节发出FPGA内部由串口接收模块UART_RX把串行数据还原成8位并行数据写入异步FIFOVGA时序生成模块VGA_Controller按固定分辨率从FIFO读出像素数据同步产生行场同步信号和使能信号送往板载VGA接口。显示器端看到的就是一幅完整的彩色图片。这里为什么选串口而不是USB或者以太网原因很直白串口协议简单三根线TXD、RXD、GND就能通信调试时用串口助手就能发数据不需要在PC端写复杂的驱动。而且大多数开发板都板载USB转串口芯片插上USB线就能用省去硬件接线这一步。串口带宽虽然低但对于静态图片传输足够640x480分辨率一帧数据量约300KB115200波特率下大约需要25秒这个时间在开发调试场景下完全能接受。FPGA内部模块划分需要遵循单一职责原则每个模块只干一件事uart_rx模块解析串口波形输出8位数据字节和接收有效标志vga_controller模块产生行同步/场同步信号计算当前像素坐标从FIFO读数据async_fifo模块跨时钟域缓冲避免串口波特率时钟和VGA像素时钟不同步导致的数据错位pll模块用板载晶振产生VGA所需的像素时钟必要时分频给其他模块2.2 时钟方案单时钟域还是PLL分频很多初学者犯的第一个错误是直接用板载50MHz时钟去驱动VGA时序。VGA时序对像素时钟非常敏感640x48060Hz的标准像素时钟是25.175MHz。用50MHz时钟计数模拟25MHz时序虽然能显示但会出现行同步信号占空比不对的隐患显示器可能拒绝锁定信号。我一般会直接在FPGA内部例化PLL把50MHz晶振时钟分频成25MHz作为VGA像素时钟同时把原始50MHz时钟用于串口接收模块。这里有一个细节串口接收模块的时钟频率必须是波特率的整数倍一般用16倍频采样即115200bps对应1.8432MHz采样时钟而50MHz时钟需要分频成1.8432MHz用计数器做分频时要注意误差控制在2%以内否则串口字节容易错位。异步FIFO在这里起到了关键的桥接作用。串口写入数据用的是系统时钟域50MHzVGA读取数据用的是像素时钟域25MHz两者没有相位关系如果不用跨时钟域FIFO直接用一个双端口RAM不加同步处理会出现读空或者读错数据的情况。选择FIFO深度时需要考虑VGA一帧扫描时间内串口能传入多少数据。640x480分辨率下VGA扫描一帧需要约16.6ms不含消隐期而串口在115200波特率下大约只能传192字节所以FIFO深度不需要很大一般256字节就够但如果你打算边接收边显示流水线式建议直接配1024字节的FIFO留足余量。2.3 VGA接口硬件连接与电阻网络VGA接口的硬件连接比很多人想象中简单但也是最容易翻车的地方。常见开发板上VGA接口的RGB信号线并不是直连FPGA引脚而是通过电阻网络做DAC转换。原因很简单VGA接口的模拟输入电压范围是0V到0.7V对应RGB各通道FPGA的GPIO只能输出0V或3.3V的数字电平不能直接驱动。常见做法是每个颜色通道用3个或者4个电阻组成简单的电阻分压网络FPGA输出3位或4位数字信号经过电阻网络后合成模拟电压。比如4位电阻网络对应16级灰度三通道组合就是4096色虽然不如8位RGB的1670万色丰富但肉眼看起来已经很接近真实彩色效果了。公司在做原型验证时如果开发板上的电阻网络已经焊好直接查原理图确认各颜色通道对应的FPGA引脚号就行不用自己搭电路。这里要记住一个关键点VGA接口的HSYNC和VSYNC信号是TTL电平直接输出的不需要转换但极性非常讲究标准VGA时序要求这两个信号是负极性同步脉冲为低电平有效如果你的代码写成高电平有效显示器大概率黑屏或者提示无信号。3. VGA时序详解从行场同步到像素地址映射的完整参数3.1 640x48060Hz标准时序参数表VGA时序本质上是严格的计数器只要把行同步、行后沿、有效像素、行前沿、场同步这几个阶段的总像素数和总行数配置正确显示器就能正确锁定时钟信号。以下是640x48060Hz的标准参数每个数值都来自VESA标准不能随意改动时序阶段行方向像素时钟周期列方向行数同步脉冲962后沿4833有效区域640480前沿1610总周期800525像素时钟为25.175MHz一行的总周期是800个像素时钟即大约31.8微秒一帧的总周期是525行其中有效显示区域是640x480。熟练掌握这个参数表后续无论换什么分辨率比如800x600、1024x768都只需要改这四组数。3.2 Verilog实现行场计数与同步信号生成把时序参数翻译成Verilog并不难核心是两个计数器。行计数器h_count在0到799之间循环场计数器v_count在0到524之间循环。行计数器每循环一次场计数器加1。这样通过比较计数值就可以生成同步信号和使能信号。// VGA时序生成核心代码 module vga_controller ( input clk_25m, // 25MHz像素时钟 input rst_n, // 低电平复位 output reg hsync, // 行同步信号 output reg vsync, // 场同步信号 output reg video_en, // 数据有效使能 output [9:0] pixel_x, // 像素横坐标0~639 output [9:0] pixel_y, // 像素纵坐标0~479 input [15:0] pixel_data, // 从FIFO读出的RGB565数据 output reg [15:0] rgb_out // 输出到电阻网络的RGB数据 ); parameter H_TOTAL 800; // 行总周期 parameter H_SYNC 96; // 行同步脉冲宽度 parameter H_BACK 48; // 行后沿 parameter H_ACTIVE 640; // 有效像素数 parameter H_FRONT 16; // 行前沿 parameter V_TOTAL 525; // 场总周期 parameter V_SYNC 2; // 场同步脉冲宽度 parameter V_BACK 33; // 场后沿 parameter V_ACTIVE 480; // 有效行数 parameter V_FRONT 10; // 场前沿 reg [9:0] h_count; reg [9:0] v_count; // 行计数器循环周期为800 always (posedge clk_25m or negedge rst_n) begin if (!rst_n) h_count 0; else if (h_count H_TOTAL - 1) h_count 0; else h_count h_count 1; end // 场计数器循环周期为525 always (posedge clk_25m or negedge rst_n) begin if (!rst_n) v_count 0; else if (h_count H_TOTAL - 1) begin if (v_count V_TOTAL - 1) v_count 0; else v_count v_count 1; end end // 生成行同步信号前96个像素周期为低电平 always (posedge clk_25m or negedge rst_n) begin if (!rst_n) hsync 1; else if (h_count H_SYNC) hsync 0; else hsync 1; end // 生成场同步信号前2行为低电平 always (posedge clk_25m or negedge rst_n) begin if (!rst_n) vsync 1; else if (v_count V_SYNC) vsync 0; else vsync 1; end // 数据有效区域排除同步、后沿、前沿时段 assign video_en (h_count H_SYNC H_BACK) (h_count H_SYNC H_BACK H_ACTIVE) (v_count V_SYNC V_BACK) (v_count V_SYNC V_BACK V_ACTIVE); // 有效区域内像素坐标 assign pixel_x video_en ? h_count - H_SYNC - H_BACK : 10d0; assign pixel_y video_en ? v_count - V_SYNC - V_BACK : 10d0; // 有效区域输出FIFO数据非有效区域输出黑色 always (posedge clk_25m or negedge rst_n) begin if (!rst_n) rgb_out 16d0; else if (video_en) rgb_out pixel_data; // 从FIFO读取像素并输出 else rgb_out 16d0; // 消隐期间输出黑色 end endmodule这段代码里的关键点在坐标映射上。VGA的有效区域其实并不是从(0,0)开始的因为它前面还有96个像素的行同步和48个像素的行后沿所以有效区域是从第144个像素时钟9648开始的。pixel_x的计算方式是把当前h_count减去偏移量这样FIFO读地址才和屏幕坐标保持一一对应关系。参数H_TOTAL这些用了parameter定义这意味着你后续想改分辨率时不需要去改动计数器的比较逻辑只需要替换这组参数的值。比如换成1024x768时行总周期变成1344有效行变成768像素时钟变成65MHz把这几个数改掉就行。3.3 像素数据格式选择RGB565是串口传输的最优解VGA显示彩色图像最常用的是RGB888每像素3字节但在串口带宽有限的前提下RGB565是更理性的选择。RGB565把红色用5位、绿色用6位、蓝色用5位共16位2字节表示一个像素相比RGB888每像素节省了1字节一帧640x480图就能省大约300KB的数据量。你可能会问RGB565的绿色为什么是6位而不是5位原因很简单人眼对绿色最敏感人眼的视锥细胞对绿光的响应峰值最高所以标准里把绿色通道多分配了一位。开发板的电阻网络如果每个通道只接了4位直接使用RGB565的高位比特也可以红色取高4位绿色取高4位蓝色取高4位这样颜色精度从RGB565降为RGB444但显示效果依然能接受。从上位机角度解析BMP图片时可以直接提取每个像素的RGB565值BMP里存储的是BGR888然后按行序发送原始字节。FPGA收到两个字节后拼接成16位像素数据写入FIFOVGA侧直接读取就行不需要做任何格式转换。4. 串口接收与FIFO缓存从字节流到像素流的完整落地方案4.1 UART接收模块的状态机设计UART接收的协议很简单空闲时数据线为高电平起始位低电平到来后按波特率采样8个数据位最后是一个停止位高电平。FPGA内部的做法是用波特率时钟的16倍频进行采样每16个采样点取中间那一个这样能有效避开数据线边沿的毛刺干扰。// UART接收状态机核心代码 module uart_rx ( input clk_50m, input rst_n, input uart_rxd, output reg [7:0] rx_data, output reg rx_done ); parameter BAUD_DIV 1085; // 50MHz / 115200bps ≈ 434 用于时钟分频实际16倍采样需要除以16 // 实际工程中用以下两个parameter parameter CLK_FREQ 50_000_000; parameter BAUD_RATE 115_200; // 16倍过采样分频系数计算CLK_FREQ / (BAUD_RATE * 16) localparam BAUD_16 CLK_FREQ / (BAUD_RATE * 16); // ≈ 27 reg [4:0] sample_cnt; // 16倍采样计数器 reg [3:0] bit_cnt; // 当前接收位计数 reg rx_state; // 0空闲 1接收中 reg [7:0] shift_reg; // 移位寄存器 // 状态机主逻辑 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin rx_state 0; sample_cnt 0; bit_cnt 0; rx_done 0; end else begin case (rx_state) 0: begin // 空闲状态 rx_done 0; if (uart_rxd 0) begin // 检测到起始位 rx_state 1; sample_cnt 0; end end 1: begin // 接收状态 if (sample_cnt BAUD_16 - 1) begin sample_cnt 0; if (bit_cnt 7) begin // 取每个bit的中间采样点 if (bit_cnt 0) ; // 跳过起始位 else shift_reg[bit_cnt-1] uart_rxd; bit_cnt bit_cnt 1; end else begin // 停止位结束后完成一字节接收 rx_data shift_reg; rx_done 1; rx_state 0; bit_cnt 0; end end else sample_cnt sample_cnt 1; end endcase end end endmodule这里的核心逻辑是在空闲状态检测到下降沿uart_rxd 0时启动接收然后在每个bit的中间采样点采集数据。其中BAUD_16这个参数的值决定了采样精度公式是CLK_FREQ除以BAUD_RATE乘以1650MHz时钟下取整为27即每27个时钟周期采样一次。如果波特率改成9600这个分频系数就是要重新计算否则会出现采样点完全错位的情况。一个容易被忽略的细节是起始位的检测问题。在空闲状态下如果数据线上的毛刺恰好表现为一个短低脉冲会导致错误启动一次接收。所以工程上通常会先做一次电平消抖——把uart_rxd信号打两拍再用打拍后的信号做边沿检测。这个处理虽然不是必须的但在长线缆或者电磁环境差的场景下能显著降低误码率。4.2 FIFO深度计算与读写时钟同步从串口接收模块到VGA显示模块之间的FIFO设计是整个系统的承重墙。由于读写时钟来源不同必须使用异步FIFO。数据写入侧时钟是50MHz系统时钟数据读取侧时钟是25MHz像素时钟。虽然写入速率远低于读取速率但FIFO的深度仍然要根据一帧数据量和突发情况来确定。一个常见思路VGA每秒刷新60帧但你的串口传输一帧图片需要25秒这意味着VGA会重复扫描同一帧FIFO内容很多次。只要FIFO不读空画面就能持续显示。FIFO深度的下限是VGA一帧时间16.6ms内从串口收到的字节数115200波特率下约192字节所以配256字节的FIFO在理论上是够的。但实践里我建议直接上1024字节的FIFO。原因有三个一是FIFO读写指针的地址位宽按2的幂次设计比较简单二是串口接收的字节是间隙式到达的两个字节之间有大约87微秒的间隔FIFO需要一定的缓冲来吸收这种间隙抖动三是如果你后续把波特率提高到9216001024字节的FIFO依然能覆盖突发情况。// 异步FIFO例化使用IP核方式 fifo_generator_1024 u_fifo ( .wr_clk(clk_50m), // 写时钟系统时钟 .wr_en(rx_done), // 写使能串口每接收一个字节拉高 .din({rx_data[1:0], 8d0, rx_data[7:2]}), // 字节到RGB565拼接按需求调整 .rd_clk(clk_25m), // 读时钟VGA像素时钟 .rd_en(video_en_core), // 读使能在有效显示区域持续读 .dout(fifo_dout), .full(fifo_full), .empty(fifo_empty), .rd_data_count(vga_valid_count) );这里需要特别说明的是RGB565拼接方式。uart_rx模块每次输出的是一个8位字节而VGA需要一次读取16位像素数据所以上位机发送时必须按低字节在前、高字节在后的顺序传。也就是说对于一个RGB565像素比如0xF81C上位机先发送0x1C再发送0xF8。FPGA端收到两个字节后需要拼接成一个16位值写入FIFO而这个拼接过程有两种方案一种是串口接收模块内部增加一个字节计数器奇偶字节做拼接另一种是直接用两个8位FIFO分时写入读出来之后再合并。实际验证下来第一种方案更简单但代码执行时序上多了一个restricted条件rx_done拉高时你需要在下一个时钟上升沿把数据拼好这要求对时序收敛有充足余量。第二种方案看起来多用了硬件资源但逻辑更直白不容易踩坑。4.3 上位机发送协议设计PC端发送图片数据不是简单地把BMP字节流直接甩给串口而是需要设计一个简单的数据帧协议。我习惯的协议格式是帧头2字节如0xAA 0x55 图片宽度2字节 图片高度2字节 像素数据 帧尾2字节如0x0D 0x0A。FPGA收到帧头后开始缓存有效数据收到帧尾后通知显示模块刷新。这个协议的好处在于:FPGA不需要知道图片的固定分辨率它只需要从帧头帧尾的字段里读出当前图片的尺寸然后调节VGA时序或者数据的读地址映射即可。如果你固定只显示640x480的图片可以省略宽度高度字段把上位机发送逻辑写得更简单——直接发帧头和像素数据。还有一个细节串口发送数据时默认是LSB先行UART协议本身规定的就是低位先发这会让RGB565的低字节低位先上线路。对于静态图片这完全没问题因为最终拼接到FPGA侧是按字节对齐的并不关心线路上的位序。但如果你遇到的不是标准UART而是SPI之类的同步串口就需要额外处理位序问题VGA显示图像时会呈现明显的按位错位花屏现象颜色完全错乱。5. 避坑手册从图像花屏到显示器无信号的五个实战排查记录5.1 现象显示器无信号且指示灯不亮这个是最常见的入门问题现象是接上VGA线后显示器一直待机没有画面也没有任何提示。排查过程先查代码里的行场同步信号极性。标准VGA时序要求HSYNC和VSYNC都是负极性即空闲为高电平、同步脉冲为低电平。很多初学者按直觉写成高电平脉冲显示器检测不到同步信号就会拒绝显示。解决方法是把always块里的hsync和vsync初始值改成1然后计数区间内输出0。还要检查PLL是否真的输出25MHz时钟用仿真工具抓PLL锁定信号locked是否为高没有锁定时VGA模块拿到的时钟是悬空的。如果是开发板的VGA接口经过IIC电平转换芯片还要确认这个芯片的使能引脚是否按要求拉高或拉低。某公司的开发板就出过这种情况VGA芯片的使能脚默认配置成输出高电平但代码里没处理这个信号导致电平时序异常用户折腾了很久才发现是硬件上拉电阻缺失。5.2 现象画面显示出来了但整体偏色红色通道变绿色这个问题的典型特征是画面能显示但颜色明显不对比如红色方块变成了绿色方块。原因基本都是RGB引脚映射到电阻网络时搞反了属于硬件连接层面的问题。开发板原理图上FPGA的PIN_C19对应的是VGA_R_3红色通道高位如果你把代码里RGB565的高位红色绑定到了PIN_D19绿色通道那结果就是红绿互换。解决方法是翻阅开发板原理图确认每个通道的引脚编号然后重新编辑管脚约束文件xdc或者qsf把颜色通道对齐。这个过程不是一次就能成功的。公司以前有个项目为了省事直接把生成了一个引脚约束模板结果dout[15:12]映射到VGA_Rdout[11:5]映射到VGA_Gdout[4:0]映射到VGA_B最后发现VGA_G在板子上其实只接了4位电阻网络导致绿色通道偏低。你需要做一次全引脚扫描测试让FPGA依次输出纯红、纯绿、纯蓝画面对照实际显示结果来校准引脚约束。5.3 现象图像能显示但有明显的横条纹像隔行扫描一样这个现象经常发生在上位机通过串口发送图片的过程中。因为VGA模块是以25MHz的频率持续刷新显示而串口接收数据的速度是每秒不到12KB刷新频率远大于数据到达频率画面会出现边接收边显示的撕裂感上半部分是最新数据下半部分还是上一帧的老数据。有两种解决思路一是彻底等数据传完再开始显示即FPGA内部做一个帧同步标志——当帧尾字节到达时把FIFO读指针清零并刷新起始地址确保每一帧都是从同一基地址开始读取的二是用双缓冲方式接收侧写入一个FIFO显示侧读另一个FIFO等当前显示帧扫完后再切换读指针到新数据。双缓冲在最坏情况下会浪费一帧的存储空间但能彻底避免撕裂问题。FPGA资源里BRAM够用的话直接配置两个1024字节的FIFO即可。5.4 现象传输完一整帧图后画面只显示了上半部分下半部分是灰色这个问题指向FIFO的读空现象。串口一帧数据是连续写入FIFO的但FIFO读侧只要有video_en就会持续读数据。如果写入速率跟不上读取速率FIFO会变空读出数据变成0或者高阻态整帧画面变成上半有图下半黑。严格来说FIFO读空本身不会导致显示停止但读出的无效数据会被当作有效像素显示出来。解决方法是让FIFO在数据量达到一定阈值后再开始允许读取这个一般叫FIFO预加载且在数据读取过程中检查empty信号一旦FIFO为空就把当前行的输出强制为黑色。这样至少能避免花屏。真正的根治手段还是提高串口波特率或者压缩传输数据量RGB565已经是压缩过的另外一种思路是降低分辨率到320x240一帧数据量缩小到四分之一传输时间从25秒减少到6秒左右。5.5 现象图像上下颠倒但左右正常这个问题的原因很直接BMP图片格式本身存储的是从下到上的行序即第一行像素在文件尾部。如果你直接按BMP文件字节序发送FPGA按顺序写入FIFO并按顺序读取那么屏幕上显示的就是上下颠倒的图像因为VGA扫描是从左上角开始逐行往下的。解决方法是上位机在解析BMP时做一次垂直翻转从文件末尾开始往前读行或者按行逆序发送。这种转换在Python或者C#上位机里就是一个小循环但经常被人忽略。还有一个相关的坑是BMP的像素存储是从左到右、从上到下实际是从上到下反转写入如果你的图片原始数据来自别的采集方式比如摄像头捕获的RAW数据行序可能就是正序的直接发送会变成倒像需要根据图像源格式灵活处理。6. 进阶验证与调优用测试彩条验证链路、突破串口传输瓶颈的两种手段一个工程从写完到真正显示出来中间最好设计一套自检流程。我现在做这套系统时会分三级验证第一级是VGA直接输出逻辑分析仪验证时序信号把HSYNC和VSYNC接到逻辑分析仪上对照标准时序参数检查脉冲宽度是否符合要求第二级是FPGA内部生成一幅测试彩条图不依赖串口数据直接由VGA模块读取ROM里的固定图像数据验证时序和像素颜色是否正确第三级才接上串口发真实的BMP图像数据。这种分级验证能帮你快速定位问题出在链路哪一段。如果彩条显示正常说明VGA模块和外部硬件没问题问题大概率在串口接收或者FIFO拼接如果彩条都不出来那问题主要在VGA时序或者引脚约束上不用去看串口代码。关于串口带宽的瓶颈两个实用技巧值得一提。一是把波特率调到921600这是USB转串口芯片常常支持的最高速率FT232和CH340都能支持此时一帧640x480的RGB565数据大约3秒传完调试效率直接提升一个数量级。调整时只需要修改UART模块的BAUD_16分频系数但要注意9600和115200用同一个系数会出错快速换波特率时最容易栽在这里。二是用RLE压缩像素数据但注意RLE在重复颜色少的自然图像上压缩效果很差适合UI截图那种大块纯色区域建议加上压缩标志位让FPGA能区分压缩和未压缩数据。这套工程做完整之后最大感受是所有看起来玄学的花屏问题归根结底都是时序和跨时钟域的问题。从那以后我每次做图像链路都会强制走一遍三级自检流程ROM彩条测显示、串口小图测链路、全分辨率测极限这个习惯帮我排掉过至少三次引脚错绑的坑。希望帮到你这套流程值得在你自己的FPGA板子上完整跑一遍。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站