☰
FPGA串口传图至VGA显示:从参数计算到屏幕点亮的完整实战
2026/10/15 12:08:52 网站建设 项目流程

简介:这是一份面向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 MHz(640×480@60Hz 的标准像素时钟)下连续输出,中间不能断。这两个速率差了几个数量级,直接对接必然花屏。

常见做法是在 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: 当前写入的 bank,rd_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 <= 1'b1; end else begin line_ready <= 1'b0; 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 <= 1'b1; 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 <= 1'b0; // 误触发,放弃 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 <= 1'b1; rx_busy <= 1'b0; end end else begin sample_cnt <= sample_cnt + 1; end end end

SAMPLE_MAX决定了采样精度,16 倍过采样是工业界默认值,既能容忍一定波特率误差,又不会让计数器太大。rx_sync是经过两级同步后的串口输入信号。bit_cnt从 0 到 9,0 是起始位,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, timeout=1) # 假设 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×480@60Hz 的像素时钟与行场同步

4.1 VGA 时序参数表与像素时钟计算

VGA 显示的核心是严格按照时序产生行同步(HSync)和场同步(VSync)信号,同时在有效像素区间输出 RGB 数据。640×480@60Hz 是 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。

// 640x480@60Hz 行场同步生成 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 从行缓冲读出像素并驱动 DAC

VGA 接口通常是模拟信号,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 <= 1'b1; else line_valid <= 1'b0; byte_cnt <= 0; checksum <= 8'h00; end end end

checksum初始为 0,每收一个数据字节就异或一次,收满 1280 字节后,下一个字节就是校验字节。line_valid为高时,行缓冲的数据才被 VGA 读取,否则 VGA 继续用上一行的数据。这个逻辑很简单,但效果立竿见影。

还有一个技巧是动态调整波特率。如果 FPGA 端处理能力有富余,可以在 PC 端发一个握手字节,FPGA 回复确认后再发下一行。这样虽然增加了协议复杂度,但能彻底杜绝丢包。不过对于静态图片显示,这个优化意义不大,因为丢一行肉眼几乎看不出来。

最后说一个我自己的习惯:每次改完代码,先发一张 8 色竖条纹图,而不是直接发照片。竖条纹能一眼看出行同步、像素对齐和颜色通道的问题,比照片好排查得多。等条纹图完美显示,再发照片,基本一次过。这个习惯帮我省了无数个调试的下午。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询