FPGA串口传图至VGA显示:从参数计算到屏幕点亮的完整实战
简介这是一份面向FPGA初学者与数字系统设计学习者的完整工程实例围绕串口接收彩色图片数据并驱动VGA显示这一典型场景展开帮助读者理解FPGA如何充当中央处理器协调UART通信、图像数据缓存与VGA时序生成。资源包共366个文件约25.79MB涵盖Verilog与VHDL源码、XDC约束、仿真脚本、综合与实现报告、比特流及工程配置文件等完整保留了从代码到上板的工程结构。项目涉及波特率与数据位设置、RGB色彩编码、分布式RAM或块RAM存储、行场同步信号生成等知识点串口接收、内存管理与VGA控制器各模块分工明确。已有1546人学习下载适合作为课程设计、实验课或自学练手的参考便于读者对照工程文件梳理模块划分、时序约束与仿真验证思路快速建立对实时数据流处理与外部设备交互的整体认识。1. 从一根串口线到一块 VGA 屏FPGA 图像链路到底难在哪很多人第一次听到「基于 FPGA 的串口发送彩色图片数据至 VGA 显示」直觉是串口慢、VGA 时序紧、图片数据大这三件事凑一起不是自找麻烦吗但真正做过的人知道这条链路恰恰是数字系统设计里性价比最高的练手路径——它逼着你同时处理异步串行通信、跨时钟域、片上存储和视频时序四件事任何一环没想清楚屏幕要么全黑要么花屏要么颜色像蒙了一层灰。这个方案适合谁一类是刚学完 Verilog 基础语法、想找一个「能看见结果」的项目把知识串起来的人另一类是做嵌入式或图像处理的工程师想把手里的算法从软件搬到硬件上跑需要一个最小可用的图像通路做验证平台。串口在这里扮演的是「低速数据入口」VGA 是「高速显示出口」中间靠 FPGA 内部的 RAM 做缓冲和速率匹配。整条链路跑通之后你换掉串口换成 SD 卡、换成以太网换掉 VGA 换成 HDMI骨架是不变的。我见过太多人卡在第一步PC 端发了一张 640×480 的彩色图FPGA 端收到的数据永远对不上。问题往往不在代码而在「你有没有想清楚一张图到底要传多少字节、串口要发多久、FPGA 要存多少」。这篇笔记就按这个思路从参数计算开始一路写到屏幕点亮中间该踩的坑一个不落。2. 先算清楚数据量和时序串口传一张图到底要多久2.1 图像格式与数据量的硬账在动手写任何一行 Verilog 之前先把账算明白。假设你要显示的是一张 640×480 的彩色图每个像素用 RGB565 表示也就是红 5 位、绿 6 位、蓝 5 位共 16 位两个字节。那么一帧图像的原始数据量是640 × 480 × 2 614400 字节也就是 600 KB。如果串口波特率设成 115200每个字节在 UART 协议下实际要传 10 位1 位起始 8 位数据 1 位停止那么每秒能传的字节数是 115200 / 10 11520 字节。传完一帧需要614400 / 11520 ≈ 53.3 秒。这个数字很关键。它意味着你不可能做实时视频只能做「静态图片刷新」。如果你把波特率提到 921600时间缩短到约 6.7 秒但很多 USB 转串口芯片在非标准高波特率下会丢数据这是后话。我一般建议新手先用 115200 把链路跑通确认数据完整再考虑提速。注意RGB565 的绿色多一位是因为人眼对绿色更敏感。如果你用 RGB332一个像素一个字节数据量减半但颜色会明显发暗、发灰做演示可以做产品不够看。2.2 为什么必须用片上 RAM 做缓冲串口是异步的来一个字节收一个字节速率慢且不规律VGA 扫描是同步的每行 640 个像素必须在 25.175 MHz640×48060Hz 的标准像素时钟下连续输出中间不能断。这两个速率差了几个数量级直接对接必然花屏。常见做法是在 FPGA 内部开一块双口 RAM串口接收端往里面写VGA 扫描端从里面读。写时钟是串口恢复出来的时钟或系统时钟下的使能信号读时钟是像素时钟。这就是典型的跨时钟域问题但因为是「一写一读、地址不冲突」的场景用简单的双口 RAM 加读写指针就可以不需要复杂的握手。这里有个容量选择是存整帧还是存一行存整帧需要 600 KB 的 RAM对大多数入门级 FPGA 来说太大了片上 Block RAM 根本不够。所以实际工程里通常存一行或者几行串口按行发送VGA 按行读取。但这样又带来一个新问题串口发一行的速度远慢于 VGA 扫一行的速度VGA 扫完一行后必须等串口把下一行填好。解决办法是开两块行缓冲乒乓操作——一块在写一块在读读完切换。2.3 行缓冲乒乓操作的参数设计假设一行 640 个像素每个像素 2 字节一行缓冲需要 1280 字节。开两块就是 2560 字节。这个容量对绝大多数 FPGA 都很轻松。乒乓操作的核心是状态机当写缓冲 0 写满一行时产生一个「行就绪」信号读端切换到缓冲 0 读同时写端切到缓冲 1 继续写。读端读完一行后等待下一个「行就绪」。这里必须注意如果串口发送速度太慢读端会出现「饥饿」VGA 扫描到一半没数据了屏幕会出现撕裂或闪烁。所以 PC 端发送时最好连续发送中间不要加人为延时。下面是一段行缓冲乒乓切换的核心逻辑用 Verilog 写// 行缓冲乒乓切换写满一行后翻转 bank_sel // wr_bank: 当前写入的 bankrd_bank: 当前读出的 bank reg wr_bank; reg rd_bank; reg line_ready; always (posedge clk_uart) begin if (wr_addr LINE_BYTES - 1) begin // 一行写满翻转写 bank并置位行就绪 wr_bank ~wr_bank; line_ready 1b1; end else begin line_ready 1b0; end end always (posedge clk_pixel) begin if (line_ready rd_done) begin // 读端完成一行后切换到刚写满的 bank rd_bank ~wr_bank; end end这段代码里LINE_BYTES是一行的字节数clk_uart是串口接收侧的时钟域clk_pixel是 VGA 像素时钟域。line_ready是跨时钟域信号实际工程里要打两拍同步这里为了简洁省略了。参数上wr_addr是写地址计数器每收一个字节加一到LINE_BYTES - 1归零。rd_done是读端读完一行的标志。提示跨时钟域信号一定要做同步处理否则line_ready在clk_pixel域里可能出现亚稳态表现为偶尔丢一行或重复一行。这是血泪经验别省这两级触发器。3. 串口接收模块从字节流到像素数据的拆解3.1 UART 接收状态机的标准写法串口接收是整条链路的入口也是最容易翻车的地方。标准 UART 接收流程是检测起始位下降沿然后在中点采样每一位数据最后检查停止位。这里的关键是采样时钟的生成——通常用系统时钟 50 MHz 分频出 16 倍波特率的采样时钟在起始位确认后每隔 16 个采样时钟取一次数据。下面是一个可综合的 UART 接收模块核心片段// 参数CLK_FREQ 系统时钟频率BAUD 波特率 // 采样计数器16 倍过采样 localparam SAMPLE_MAX CLK_FREQ / BAUD / 16; reg [3:0] sample_cnt; reg [3:0] bit_cnt; reg [7:0] rx_data; reg rx_done; always (posedge clk) begin if (!rx_sync) begin // 检测到起始位下降沿启动采样 sample_cnt 0; bit_cnt 0; rx_busy 1b1; end else if (rx_busy) begin if (sample_cnt SAMPLE_MAX - 1) begin sample_cnt 0; if (bit_cnt 0) begin // 起始位中点确认仍为低电平 if (!rx_sync) bit_cnt bit_cnt 1; else rx_busy 1b0; // 误触发放弃 end else if (bit_cnt 8) begin // 采样 8 位数据LSB 在前 rx_data[bit_cnt - 1] rx_sync; bit_cnt bit_cnt 1; end else begin // 停止位 rx_done 1b1; rx_busy 1b0; end end else begin sample_cnt sample_cnt 1; end end endSAMPLE_MAX决定了采样精度16 倍过采样是工业界默认值既能容忍一定波特率误差又不会让计数器太大。rx_sync是经过两级同步后的串口输入信号。bit_cnt从 0 到 90 是起始位1 到 8 是数据位9 是停止位。rx_done拉高一个时钟周期通知下游取走rx_data。3.2 字节流如何拼成 RGB565 像素串口收到的是一连串字节你需要按约定把它们拼成像素。常见协议是每两个字节组成一个像素高字节在前低字节在后。比如收到0xF8和0x00拼成0xF800这是纯红色。这里有个坑PC 端发送时如果用的是 Python 的serial.write()它默认按字节发送但如果你把图像数据当成字符串处理可能会遇到编码问题。正确做法是把图像读成二进制逐字节发送。下面是一段 PC 端发送的 Python 示例import serial import struct # 打开串口波特率 115200 ser serial.Serial(COM3, 115200, timeout1) # 假设 img 是 640x480 的 RGB565 数据每个像素两个字节 # 这里用纯红色填充作为测试 width, height 640, 480 red_pixel 0xF800 for y in range(height): for x in range(width): # 高字节在前 ser.write(struct.pack(H, red_pixel)) # 每行发完可以加一个小延时但不要太大 # time.sleep(0.001) ser.close()struct.pack(H, red_pixel)把 16 位整数按大端序打包成两个字节。H里的表示大端H表示无符号短整型。如果你在 FPGA 端按小端序拼接颜色就会错乱红色变蓝色。这个坑我踩过排查了半天以为是时序问题结果是字节序搞反了。3.3 接收端的流控与丢包排查串口没有硬件流控时如果 PC 端发得太快FPGA 端处理不过来就会丢字节。表现是图像整体偏移、颜色错位或者每隔几行出现一条杂色带。排查方法先发一张纯色图比如全红。如果屏幕显示均匀红色说明链路基本通如果出现杂色说明有丢字节。然后在 PC 端每发完一行加一个短延时比如 1 ms看是否改善。如果改善说明 FPGA 端写入速度跟不上需要优化写逻辑或降低发送速率。另一个常见问题是串口线质量差或太长导致误码。表现是随机位置的像素颜色错误。解决方法是换短一点的线或者在 FPGA 端加简单的校验比如每行末尾发一个校验字节FPGA 端比对不对就丢弃该行并请求重发。但请求重发需要反向通道入门阶段可以先不做先把线换好。4. VGA 时序生成640×48060Hz 的像素时钟与行场同步4.1 VGA 时序参数表与像素时钟计算VGA 显示的核心是严格按照时序产生行同步HSync和场同步VSync信号同时在有效像素区间输出 RGB 数据。640×48060Hz 是 VGA 的基准模式标准参数如下参数行像素场行有效像素640480前肩1610同步脉冲962后肩4833总计800525像素时钟 800 × 525 × 60 ≈ 25.2 MHz标准值取 25.175 MHz。如果你手头的晶振是 50 MHz可以用 PLL 分频得到 25 MHz虽然略低于标准但大多数显示器能容忍画面会稍微有点抖动不影响功能验证。4.2 行场计数器的同步逻辑行场计数器是 VGA 模块的心脏。行计数器从 0 数到 799数到 799 归零并让场计数器加一场计数器从 0 数到 524数到 524 归零。HSync 在行计数器处于 656 到 751 之间时拉低VSync 在场计数器处于 490 到 491 之间时拉低。有效像素区间是行计数器 0 到 639、场计数器 0 到 479。// 640x48060Hz 行场同步生成 localparam H_TOTAL 800; localparam V_TOTAL 525; localparam H_SYNC_START 656; localparam H_SYNC_END 752; localparam V_SYNC_START 490; localparam V_SYNC_END 492; reg [9:0] h_cnt; reg [9:0] v_cnt; always (posedge clk_pixel) begin if (h_cnt H_TOTAL - 1) begin h_cnt 0; if (v_cnt V_TOTAL - 1) v_cnt 0; else v_cnt v_cnt 1; end else begin h_cnt h_cnt 1; end end assign hsync ~(h_cnt H_SYNC_START h_cnt H_SYNC_END); assign vsync ~(v_cnt V_SYNC_START v_cnt V_SYNC_END); assign video_active (h_cnt 640) (v_cnt 480);h_cnt和v_cnt都是 10 位宽因为 800 和 525 都小于 1024。hsync和vsync是低电平有效这是 VGA 标准。video_active在有效像素区间为高用来选通 RGB 输出其他时候输出黑色0,0,0。4.3 从行缓冲读出像素并驱动 DACVGA 接口通常是模拟信号FPGA 的 IO 口输出的是数字信号所以需要电阻网络做简易 DAC。RGB565 转 VGA 的常见做法是红 5 位接 5 个电阻绿 6 位接 6 个电阻蓝 5 位接 5 个电阻每个电阻的阻值按二进制权重选取比如红 5 位用 1k、2k、4k、8k、16k 电阻并联到地输出端就是模拟电压。读行缓冲的逻辑很简单当video_active为高时用h_cnt作为读地址因为一行 640 个像素每个像素 2 字节所以地址是h_cnt * 2从当前rd_bank读出 16 位数据直接送到 RGB 输出。注意读地址要提前一个时钟周期准备好因为 RAM 读数据有一个时钟的延迟。注意如果你用的 FPGA 开发板上有专用的 VGA 接口和 DAC 芯片直接按板卡手册连接即可不需要自己搭电阻网络。但电阻网络方案成本极低适合自己搭电路验证。5. 避坑与排查屏幕不亮、花屏、颜色错乱的 5 个真实原因5.1 屏幕全黑连背光都没有现象上电后显示器提示「无信号」或直接黑屏。原因最常见的是 VSync 或 HSync 极性搞反了。有些显示器要求同步信号高电平有效有些要求低电平有效。另一个可能是像素时钟根本没输出PLL 没锁定。解决先用示波器或逻辑分析仪看 HSync 和 VSync 有没有波形频率对不对。如果波形正常试着把同步极性取反。如果 PLL 没锁定检查输入时钟和 PLL 参数配置。5.2 屏幕亮但花屏像雪花一样现象显示器有信号但画面全是杂色噪点。原因像素时钟频率偏差太大或者行场计数器没复位。也可能是行缓冲读地址错位读到了未写入的区域。解决确认像素时钟是否接近 25.175 MHz。如果是 50 MHz 直接当像素时钟用显示器肯定不认。检查复位逻辑确保上电后行场计数器从 0 开始。读地址错位的话用纯色图测试如果纯色图正常说明地址逻辑没问题问题在数据写入侧。5.3 图像整体偏移或滚动现象画面能显示但位置偏左偏右或者上下滚动。原因行场同步的前肩、后肩参数不对。不同显示器对时序的容忍度不同参数偏差几行几列就可能出问题。解决严格按标准参数表设置。如果标准参数不行微调前肩和后肩每次改几个像素直到画面稳定。滚动通常是场同步参数不对重点检查 V_SYNC_START 和 V_SYNC_END。5.4 颜色错乱红色变蓝色现象图像内容正确但颜色完全不对。原因RGB565 的字节序搞反了或者红蓝通道接反了。解决先发一张纯红图看屏幕显示什么颜色。如果显示蓝色说明红蓝通道接反交换接线即可。如果显示其他颜色检查 PC 端发送的字节序和 FPGA 端拼接的字节序是否一致。大端小端这个问题我至少见过三次不同的人踩。5.5 图像每隔几行出现一条杂色带现象大部分画面正常但规律性地出现横条纹。原因串口丢字节导致行缓冲写入的数据错位。通常是 PC 端发送太快FPGA 端来不及处理。解决在 PC 端每行发送后加 1 到 2 ms 延时看是否改善。如果改善说明是速率匹配问题。长期方案是优化 FPGA 端写逻辑或者提高串口波特率但降低发送频率。另一个可能是行缓冲乒乓切换时读端读到了正在写的 bank这需要检查line_ready的同步逻辑。6. 进阶技巧用双缓冲加校验把刷新率再提一档链路跑通之后下一步就是优化。我一般会做两件事一是把行缓冲升级成帧缓冲的双缓冲二是加一个简单的行校验。帧缓冲双缓冲的思路是开两块帧缓冲串口往缓冲 A 写整帧写完后切换VGA 从缓冲 B 读。这样 VGA 读的时候不会被打断画面完全无撕裂。代价是需要 600 KB × 2 的 RAM对中端 FPGA 来说可以接受。如果 RAM 不够可以只对下半屏做双缓冲上半屏用行缓冲折中方案。行校验的做法是PC 端每行末尾发一个校验字节内容是前面 1280 字节的异或和。FPGA 端收到一行后计算异或与校验字节比对不一致就丢弃该行用上一行的数据填充。这样即使偶尔丢字节画面也只是某一行重复不会出现杂色带。下面是一个简单的异或校验计算片段// 行校验对一行 1280 字节做异或 reg [7:0] checksum; reg [7:0] rx_byte; reg [10:0] byte_cnt; always (posedge clk_uart) begin if (rx_done) begin if (byte_cnt 1280) begin checksum checksum ^ rx_byte; byte_cnt byte_cnt 1; end else begin // 收到校验字节比对 if (rx_byte checksum) line_valid 1b1; else line_valid 1b0; byte_cnt 0; checksum 8h00; end end endchecksum初始为 0每收一个数据字节就异或一次收满 1280 字节后下一个字节就是校验字节。line_valid为高时行缓冲的数据才被 VGA 读取否则 VGA 继续用上一行的数据。这个逻辑很简单但效果立竿见影。还有一个技巧是动态调整波特率。如果 FPGA 端处理能力有富余可以在 PC 端发一个握手字节FPGA 回复确认后再发下一行。这样虽然增加了协议复杂度但能彻底杜绝丢包。不过对于静态图片显示这个优化意义不大因为丢一行肉眼几乎看不出来。最后说一个我自己的习惯每次改完代码先发一张 8 色竖条纹图而不是直接发照片。竖条纹能一眼看出行同步、像素对齐和颜色通道的问题比照片好排查得多。等条纹图完美显示再发照片基本一次过。这个习惯帮我省了无数个调试的下午。希望帮到你。本文还有配套的精品资源点击获取