拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于FPGA的串口图片传输至VGA显示设计与实现

简介基于FPGA的串口发送彩色图片数据至VGA显示工程是一套完整的数字系统设计学习资源适合电子、通信、计算机等专业学生及FPGA入门者。资源覆盖UART串口通信、图像数据解析、块RAM存储与VGA时序控制等关键环节包含VHDL/Verilog硬件描述源码、约束文件、仿真脚本及bit下载文件。包内共366个文件以.v/.vhd源码、.xdc约束、.rpt报告、.xpr工程文件和.log日志为主并附有编译/仿真批处理脚本与.mem图像数据总体约25.79MB结构清晰便于对照学习。已有1545人学习下载该工程不仅演示了如何将PC端发送的彩色图片数据经FPGA实时显示在VGA屏幕上还提供了完整的Vivado工程与布线结果方便读者复现和二次开发。通过学习可深入理解串行协议与显示接口的硬件实现是实践FPGA数据流处理的优质范例。 我最初做这个项目是因为跑完了流水灯、按键消抖、数码管显示这些基础实验之后突然不知道拿FPGA做什么像样的东西了。串口我会收VGA时序我能写但把两者连起来——用串口发一张彩色图片在显示器上稳定显示出来——却让我卡了整整两个周末。这篇文章就是基于FPGA的串口发送彩色图片数据至VGA显示的完整记录从数据通路设计、串口帧协议、双口RAM配置到上板调试的踩坑过程都会讲到。如果你手里有一块入门级FPGA开发板想从单模块练习过渡到完整小系统这个项目是性价比极高的一站。1. 项目的整体数据通路与关键设计决策1.1 数据从串口到VGA要经过哪几道关口先看清楚数据是怎么流起来的。PC端用Python脚本把一张图片缩放到指定分辨率转换成RGB565格式按行封装成协议帧通过USB转串口发出去。FPGA内部的UART接收模块把串行比特流还原成字节交给协议解析状态机解析出帧头、行号、像素数据写入一块双口RAM的一侧写端口。VGA控制器这边按照标准的640x48060时序产生行场同步信号同时在有效显示窗口内生成RAM读地址从双口RAM的另一侧读端口取出像素数据经RGB引脚送到显示器。这条链路里最核心的概念是写入RAM和读出RAM处于完全不同的时钟域。串口侧工作在系统时钟比如50MHz但字节到达节奏由波特率决定约8.68微秒一个字节VGA侧工作在像素时钟25MHz左右每个像素只有40纳秒。两侧速率差了几百倍不能用简单的寄存器数组必须靠块RAM或异步FIFO做缓冲。这也是这个项目最有价值的地方——它把通信协议、跨时钟域存储、视频时序三件事压缩在一个系统里做完一遍很多书上的抽象概念都落地了。1.2 为什么必须“整帧存储、循环显示”而不是边收边放我在动手前偷懒想过一个方案串口收一行VGA就显示一行收一帧显示一帧这样只用极小的缓存。算完带宽之后彻底放弃了这个念头。串口波特率按115200bps算每个字节带起始位和停止位共10个bit实际有效数据速率只有11520字节每秒。而VGA在640x48060下像素时钟约25MHz如果按RGB565即每像素16bit计算读带宽是400Mbps是串口写入速率的三千多倍。更本质的问题是串口写完一行要80毫秒左右而VGA扫过一行只要31.8微秒。等待串口的下一行数据时显示端早就把上一行刷了上千遍。所以“边收边放”在低速串口下根本不成立唯一合理的方案是先用双口RAM或FIFO把整帧图片完整存下来存完之后给VGA侧一个帧完成信号VGA再按自己的像素时钟反复读出显示。想播放下一张图就等下一帧数据完全写入后再切换。1.3 分辨率、色深与片内RAM容量的取舍整帧存储意味着RAM容量必须装下一整张图。按位宽公式计算分辨率宽度x高度x每像素色深。以我自己用的Cyclone IV入门板为例片内块RAM大约400Kbit左右这就直接决定了分辨率上限。分辨率色深格式一帧所需RAM是否适合入门级FPGA160x120RGB56516bit约307Kbit很合适160x120RGB88824bit约461Kbit偏紧可能放不下320x240RGB3328bit约614Kbit容量中等偏上可试320x240RGB56516bit约1.23Mbit通常需要SDRAM或大容量芯片我推荐以160x120 RGB565作为起点原因有三300Kbit左右的RAM在多数入门级芯片里能放下RGB565的16bit位宽正好占两个字节串口按字节发送好处理分辨率低时VGA显示窗口居中后仍有不错的视觉效果。等这块跑通了再考虑320x240 RGB332或者外挂SDRAM做更大图复杂度会集中到存储控制上。2. 串口接收链路帧协议与可靠传输2.1 串口是字节流没有帧边界很多人第一次调通UART接收后会直接用串口助手把图片字节一股脑发过去结果发现显示出来的画面完全随机。原因很简单串口协议只保证字节传输不保证帧边界。FPGA上电后可能从任何位置开始接收数据如果发的第一包数据里混入了半个历史帧整个RAM里的数据就全错位了。更麻烦的是传输过程中一旦丢一个字节后面所有像素地址全部偏移画面会呈现行错乱或整体左移一列的效果。所以串口传图片必须设计帧协议协议的核心目的是让接收端明确知道三件事数据从哪里开始、这包数据属于哪一行、数据有没有传错。我最初图省事只用了一个帧头结果调了一天错位问题后来老老实实加了行号和校验字段。2.2 工程上最稳的帧格式设计我采用的方案是“按行发送”而不是整帧一次性发。按行发送的好处是某一行校验失败只需要重发该行而且行号能直接作为双口RAM写地址的高位让RAM地址生成逻辑变得非常简单。帧结构定义如下字段长度字节说明帧头2固定为0xAA 0x55用于帧同步命令字10x01表示写图像数据行号10~119对应RAM地址高位数据长度2小端序表示数据域字节数数据域NRGB565像素数据一行160像素对应320字节校验和1前面所有字段按字节累加后取低8位FPGA侧解析状态机的思路是IDLE状态下等待0xAA收到后再等0x55一旦匹配成功进入接收命令和行号状态然后接收两字节长度按长度接收数据域最后接收校验和并验证。校验通过后将行号左移8位作为RAM写起始地址把数据域的320字节按每两字节拼成一个16bit像素依次写入RAM。校验失败则整包丢弃不更新RAM同时拉高一个错误标志方便调试。帧头只当作同步标记真正决定数据归属的是行号字段。2.3 Python上位机把图片拆成协议帧上位机用Python加Pillow库最方便。核心流程读图缩放到160x120逐像素转RGB565按行封装成上面定义的帧格式用pyserial发送。import time import serial from PIL import Image ser serial.Serial(COM10, 115200, timeout1) # 按实际串口号修改 img Image.open(demo.png).convert(RGB).resize((160, 120)) pixels list(img.getdata()) CMD_WRITE_LINE 0x01 for row in range(120): payload bytes([CMD_WRITE_LINE, row]) payload (320).to_bytes(2, little) line_data b for col in range(160): r, g, b pixels[row * 160 col] rgb565 ((r 3) 11) | ((g 2) 5) | (b 3) line_data rgb565.to_bytes(2, little) payload line_data checksum sum(payload) 0xFF frame b\xAA\x55 payload bytes([checksum]) ser.write(frame) time.sleep(0.002)这里有两个细节非常重要。第一是RGB565的小端字节序to_bytes(2, little)意味着低字节先发送FPGA侧必须先收低字节再收高字节拼成{high_byte, low_byte}。如果两边字节序不一致画面会呈现“像素错位加颜色错乱”的双重问题。第二是time.sleep(0.002)这个延时不是随便加的。很多USB转串口芯片的驱动缓冲区不大连续高速写入数据而FPGA接收端来不及取走时驱动层会发生静默丢包。实测下来115200波特率下加2到5毫秒的行间延时连续发120行基本不丢。3. VGA输出侧时序、窗口定位与颜色映射3.1 自定义时序 vs 沿用640x48060FPGA驱动VGA显示器的本质是产生符合显示器规范的行场同步信号并在有效显示区间输出RGB数据。很多初学者会想我只显示160x120是不是可以用更简化的时序我的建议是别这么做。市面上的显示器对输入时序有一定的容错范围但自定义参数容易踩到显示器的接受边界出现黑屏、闪屏甚至提示“超出显示范围”。最稳妥的方式是沿用标准的640x48060时序把160x120的图片放在有效区域的中间显示其余区域输出全黑。标准时序参数如下像素单位参数数值像素时钟25.175MHz近似25MHz行有效区640像素行前廊16像素行同步脉冲96像素行后廊48像素整行周期800像素帧有效区480行帧前廊10行帧同步脉冲2行帧后廊33行整帧周期525行行同步和场同步都用负极性。FPGA里用两组计数器分别产生行计数和场计数当行计数位于有效区、场计数位于有效区时输出数据使能信号de此时RGB引脚才有意义。3.2 如何把160x120的图片放到屏幕正中间让图片居中显示本质是在de有效期间判断当前扫描像素是否落入目标窗口。水平方向屏幕有效区是640像素图片宽度160水平起点是(640-160)/2240终点是399。垂直方向有效区是480行图片高度120垂直起点是(480-120)/2180终点是299。判断到当前像素坐标落在窗口内时需要产生一个RAM读地址。地址计算有两种思路一种是在窗口起点处用独立计数器清零并递增读取连续的RAM地址另一种是用减法得到窗口内相对坐标再用乘加计算addr y_win * 160 x_win。我用的后者因为Verilog里160是128加32可以拆成移位加法占用逻辑很有限。伪代码逻辑如下always (posedge clk_vga or negedge rst_n) begin if (!rst_n) begin x_win 0; y_win 0; end else if (hcnt 240 hcnt 400 vcnt 180 vcnt 300) begin x_win hcnt - 240; y_win vcnt - 180; end end assign ram_rd_addr y_win * 160 x_win;这里必须留意RAM读延迟。简单双端口RAM的读端口如果配置了输出寄存器读地址变化后数据会滞后一拍才出现在读数据总线上。解决办法是把窗口判断信号也打一拍让RGB输出与窗口使能对齐。我一开始没做这个对齐结果显示的图片整体向右和向下偏移了一两个像素虽然不影响整体判断但这个细节值得一次说清。3.3 RGB565到VGA引脚的高低bit对齐RGB565格式中红色占5bit绿色占6bit蓝色占5bit。开发板上的VGA接口一般通过电阻网络将FPGA引脚的数字信号转换成模拟电平常见的是每通道3bit、4bit或5bit。以最常见的12bit VGA接口每通道4bit为例映射规则是把高位对齐红色的5bit取高4位接红色模拟通道绿色6bit取高4位蓝色5bit取高4位低位直接丢弃。偏色问题大多出在这里。很多人想当然地直接把RGB565的16bit完整接到VGA接口上却发现图像颜色完全不对这就是因为板卡只实现了12bit的电阻网络多接的引脚悬空或电平不匹配。看原理图时确认每个颜色通道实际占用几位然后按高位对齐截断是最省事的处理方式。如果板子是标准的24bit RGB888接口则要把RGB565扩展成RGB888例如红色5bit{r[4:0], r[4:2]}补齐到8bit。4. 跨时钟域存储与帧同步控制4.1 为什么用双口RAM而不是异步FIFO串口侧写入慢VGA侧读取快这个速度差异让很多初学者第一反应是“用异步FIFO缓冲”。但从工程角度看这个场景用异步FIFO并不是最优解。FIFO的核心价值在于缓冲突发数据并隔离两侧读写速率它天然强调“先进先出”和动态空满状态。而这里的数据是图像帧VGA读出口需要的不是最早写入的数据而是按像素坐标随机访问的数据——某个像素可能在RAM的任意位置。双口RAM允许写端口和读端口使用独立时钟、独立地址线完全靠地址索引更适合“串口按行写入、VGA按扫描顺序读取”的模式。另一个关键点是串口写入一帧需要几秒VGA读取一帧只需16.7毫秒FIFO的空满信号在这个场景里几乎没有意义——读侧总是空的写侧需要等待自己慢慢填满。所以核心逻辑不是检测空满而是接收完整个一帧后通知VGA侧可以开始读取。双口RAM加一个帧完成标志逻辑上更直接。4.2 帧完成信号如何安全地交给VGA时钟域串口写完整帧后协议解析状态机会拉高一个frame_done脉冲。这个脉冲位于系统时钟域而VGA控制器工作在像素时钟域两个时钟虽然同源都由板载晶振产生但相位和频率不同直接使用存在亚稳态风险。标准做法是两级同步器把frame_done信号在像素时钟域打两拍得到同步后的帧有效标志。reg frame_done_meta, frame_done_sync; always (posedge clk_vga or negedge rst_n) begin if (!rst_n) begin frame_done_meta 1b0; frame_done_sync 1b0; end else begin frame_done_meta frame_done; frame_done_sync frame_done_meta; end end同步后的信号在VGA控制器中作为“允许读取新帧”的使能。需要注意frame_done是高电平脉冲两级同步后脉冲宽度会被拉长并延迟若干周期但只要VGA控制器把它当电平标志使用采到上升沿即可。更稳妥的做法是握手机制但在这个项目中两级同步器足够。4.3 IP核位宽、深度与读延迟配置以160x120 RGB565为例双口RAM配置如下位宽16bit深度19200写端口工作在系统时钟读端口工作像素时钟。Altera环境下用ROM/RAM向导生成“Simple Dual-Port RAM”即可Xilinx环境对应Block Memory Generator的“Simple Dual Port RAM”。配置时要注意两个点。第一是深度参数一定要按像素数而非字节数填写因为每个地址存的是一个完整的RGB565像素。第二是读端口是否带输出寄存器这决定了读延迟。有的IP核提供“Primitive Output Register”选项开启后读延迟加一拍关闭后数据组合输出、读地址变化的当拍就能拿到数据。片内块RAM的原始读本身就有一定延迟通常建议保留输出寄存器然后在VGA控制器里把窗口使能同步打拍对齐而不是为了省一拍去关闭寄存器否则时序收敛会很痛苦。5. 上板调试从一片漆黑到稳定显示的排错路径5.1 先别急着发图VGA彩条自检联调之前必须先验证VGA输出链路本身是健康的。我的做法是让VGA控制器先工作在“彩条模式”根据行计数器的高位或低三位直接输出红、绿、蓝、白、黑等纯色块完全不依赖RAM数据。如果显示器能稳定显示彩条说明像素时钟、行场同步时序、RGB引脚映射、复位逻辑都没问题可以排除VGA输出侧的错误。彩条显示异常时按优先级排查先查行场极性是否和板卡要求一致再查像素时钟频率是否在显示器接受范围最后查RGB引脚的物理连接和位宽映射。我用SignalTap抓过行计数和场计数波形最容易发现的问题是时钟频率不对——有些开发板的VGA像素时钟由锁相环输出如果锁相环配置错误输出频率偏高或者偏低显示器会直接黑屏提示无信号。5.2 串口环回与RAM写入验证VGA彩条通过后第二步验证串口接收和RAM写入。此时还不接图形数据而是用一个固定的RGB值填充整帧上位机发送全红色、全蓝色或渐变色的测试帧。FPGA把收到的数据写入RAMVGA显示读取。如果屏幕能稳定显示整屏红色说明串口帧协议解析、校验、行到RAM地址的映射、跨时钟域同步全部正确。如果显示的花屏带有明显的行错位排查顺序是先在协议解析状态机里确认行号是否正确递增再检查数据长度字段是否为320最后确认RAM写地址计算时行号是否占对了位数。我调试时遇到过“每三行重复一次”的情况查了半天发现是上位机发送的行号从1开始而FPGA侧的RAM地址从0开始计算导致第一行被覆盖且整体偏移。5.3 联调后我踩过的四个典型问题这个项目真正的坑集中在联调阶段我把遇到的四个问题整理成表每个都是真实排过雷的。现象根因排查思路画面完全花屏像压缩包乱码串口字节序反了或帧头同步丢失先检查RGB565高低字节拼接顺序再用固定色块测试排除字节序问题画面横向错位右侧颜色串到左侧RAM读延迟未与窗口对齐用SignalTap抓RAM读地址和数据确认数据是否晚于地址一拍颜色整体偏红或偏蓝RGB位数截断没有高位对齐对照板卡原理图确认每通道有效位宽高位截断而非低位截断图片显示正常但十几秒后局部变花串口写入新帧时VGA仍在读同一RAM让上位机在帧间增加间隔或采用双缓冲切换帧第四种现象最容易迷惑人我看波形看了很久才发现是帧覆盖冲突。帧协议本身没错但发完第一帧后立刻发第二帧双口RAM在VGA还没扫完上一帧时就被写入了新数据。对于入门项目最简单的处理是上位机发完一帧后延时1秒再发下一帧正式一点的做法是双缓冲写A缓冲时读B缓冲切换由帧完成信号控制。5.4 调试工具与建议这个项目只用LED灯和串口打印很难定位内部问题强烈建议学会用逻辑分析仪或FPGA自带的在线逻辑分析仪。Altera平台用SignalTapXilinx平台用ILA把串口解析状态机的状态、RAM写地址、RAM读地址、frame_done信号抓出来和理论波形对比。调试时也可以故意制造特征数据比如让图片第一行全红、第二行全绿这样一旦错位立刻能从屏幕颜色分布判断出偏移方向。6. 打通之后还能怎么玩串口加VGA这套链路跑通后等于在FPGA里建立了一条“外部数据输入→存储→视频时序输出”的通用骨架往后的扩展都可以挂在这条骨架上。我列几个自己规划的方向一是双缓冲加帧同步解决连续刷新时的撕裂问题二是把双口RAM换成SDRAM控制器分辨率可以直接突破到640x480甚至以上三是在读写之间插入图像处理模块比如灰度化、二值化、边缘检测变成一个小小的实时图像处理流水线四是把串口换成SD卡读取图片文件摆脱上位机接线做成独立的图片播放器。我自己打算先做双缓冲和灰度化把FPGA片内资源真正榨干一遍。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门