一 VGA基础
在写代码之前,我们必须先搞清楚VGA到底是什么,以及它是如何让屏幕亮起来的。
1.1 什么是VGA?
VGA最早是IBM在1987年推出的一种视频传输标准。虽然它传输的是模拟信号,但其扫描时序被后来的DVI、HDMI等数字接口完美继承。在FPGA开发中,我们通过控制IO口输出高低电平来模拟这些时序,从而实现图像显示。
1.2 VGA物理接口与引脚
最常见的VGA接口是15针的D-Sub接口。虽然引脚很多,但真正用于传输图像和控制的核心引脚只有5根:
- R、G、B(红绿蓝):3根模拟信号线。注意,FPGA输出的是数字信号(0或1),要连接到VGA的模拟引脚,通常需要搭建一个电阻分压网络(DAC)。比如用3个电阻将FPGA的3个IO口组合成8种电压等级,实现8位色(RGB 3-3-2)。
- HSYNC(行同步):告诉显示器“一行开始了”。
- VSYNC(场同步):告诉显示器“一帧画面开始了”。
- GND(地):信号参考地。
1.3 VGA显示原理:光栅扫描
VGA显示器(无论是早期的CRT还是现在的LCD)都采用光栅扫描的方式显示图像。你可以想象一支“电子笔”在屏幕上画画:
- 从左到右画一行(行扫描)。
- 画到最右边后,电子笔快速回到左边,准备画下一行(行消隐期)。
- 从上到下画完所有行(场扫描)。
- 画到右下角后,电子笔快速回到左上角,准备画下一帧(场消隐期)。在这个过程中,电子笔在“回扫”的时候是关闭的(不发光),这段时间被称为消隐期(Blanking)。消隐期就是为了给电子笔留出足够的物理返回时间,同时显示器也需要依靠这段时间来同步信号。
1.4 核心参数:分辨率与像素时钟(Pclk)
我们常说的“640×480@60Hz”是什么意思?
640×480:表示显示区域的有效区域有640列,480行
60hz:表示一秒扫描的帧数。
那么,驱动VGA需要一个多快的时钟(像素时钟 Pclk)呢?由于消隐期的存在,扫描一行的时间绝对不是640个时钟,扫描一帧的时间也绝对不是480行。根据行业标准(V640x480@60Hz 的完整时序参数为:
一行总耗时(H_totaltime)= 800 个像素时钟
一帧总耗时(V_totaltime)= 525 行
计算像素时钟:Pclk = 800 * 525 * 60 ≈ 25,200,000 Hz (25.2 MHz)。这也是为什么在FPGA中,我们需要用PLL锁相环将板载的50MHz时钟分频/倍频到 25MHz(或精准的25.175MHz)作为VGA的驱动时钟。
二 VGA时序图基础
请看下面的时序图,它清晰地展示了VGA显示一帧图像时的信号变化规律(以行时序为例,场时序原理完全相同,只是单位从像素变成了行)。
2.
2.1 行时序
行时序描述的是电子枪在屏幕上扫过“一行”像素的过程。它包含以下几个关键阶段:
- Hor Sync(行同步):告诉显示器一行开始或结束。通常是一个低电平脉冲。
- H Back Porch(行后沿/后肩):同步脉冲结束到有效图像开始的时间,是电子枪回扫后稳定下来的缓冲期。
- H Left Border(行左边沿) & H Right Border(行右边沿):图像周围的黑色边框。
- Hor "Active" Video(行有效视频):实际用于显示的像素区域。图中同时标注了“Addressable Video(可寻址视频区)”,包含在Active Video中。
- H Front Porch(行前沿/前肩):有效图像结束到下一个同步脉冲开始的缓冲期。
2.2 场时序
场时序描述的是显示器从上到下扫描完一帧画面的过程。原理与行时序一致,但单位是“行”(Line):
- Ver Sync(场同步):告诉显示器一帧图像结束,准备开始下一帧。
- V Back Porch(场后沿):场同步后的缓冲期。
- V Top Border(场上边沿) & V Bottom Border(场下边沿):图像的上下黑边。
- Ver "Active" Video / Addressable Video(场有效视频):实际的显示图像区域。
- V Front Porch(场前沿):有效图像结束到下一场同步开始的缓冲期。
一句话总结:一帧画面的扫描,本质上是先进行行扫描(横向),每扫完一行,计数器加1,再进行下一行的扫描;扫完480行后,进入场消隐期,然后从头开始。
三 实战代码(基本时序生成)
结合上面的时序图与基础计算,我们来实现分辨率是 640×480@60Hz的实战项目。
3.1参数定义映射
行时序参数 (H_)
- H_totaltime = 800:扫描一行总耗时 800 个像素时钟。
- H_time = 96:行同步脉冲(H Sync)宽度。
- H_back = 40:行后沿(H Back Porch)。
- H_left = 8:行左边沿(H Left Border)。
- H_addrtime = 640:行有效像素(Horizontal Addressable Video)。
- H_right = 8:行右边沿(H Right Border)。
- H_front = 8:行前沿(H Front Porch)。
场时序参数 (V_)
- V_totaltime = 525:扫描一帧总耗时 525 行。
- V_time = 2:场同步脉冲(V Sync)宽度。
- V_back = 25:场后沿(V Back Porch)。
- V_top = 8:场上边沿(V Top Border)。
- V_addrtime = 480:场有效像素(Vertical Addressable Video)。
- V_bottom = 8:场下边沿(V Bottom Border)。
- V_front = 2:场前沿(V Front Porch)。
3.2 计数器逻辑 (cnt_h, cnt_v)
行计数器 cnt_h:在像素时钟 Pclk 驱动下循环计数 0~799。
场计数器 cnt_v:每当行计数器绕回0时(即扫完一行),cnt_v 加1,循环计数 0~524。这是一个典型的嵌套计数结构。
reg [9:0] cnt_h, cnt_v;
always @(posedge Pclk or negedge Prst_n) begin
if(!Prst_n)
cnt_h<=0;
else if(cnt_h==H_totaltime-1)
cnt_h<=0;
else
cnt_h<=cnt_h+1;
end
always @(posedge Pclk or negedge Prst_n) begin
if(!Prst_n)
cnt_v<=0;
else if(cnt_h==H_totaltime-1)begin // 行计数器满,场计数器加1
if(cnt_v==V_totaltime-1)
cnt_v<=0;
else
cnt_v<=cnt_v+1;
end
end
3.3数据有效信号 (data_valid)
assign data_valid = (cnt_h>=(H_time+H_back+H_left))&&(cnt_h<(H_time+H_back+H_left+H_addrtime))
&&(cnt_v>=(V_top+V_back+V_time))&&(cnt_v<(V_top+V_back+V_time+V_addrtime));
这是代码中最核心的部分。 data_valid 决定了何时向屏幕输出真实的图像数据。
水平有效起点:96+40+8 = 144,终点:144+640 = 784。
垂直有效起点:2+25+8 = 35,终点:35+480 = 515。
只有 cnt_h 和 cnt_v 同时处于这两个区间时,data_valid 才为高电平,此时输出的RGB数据才会被显示器识别为有效像素。
3.4. 同步信号生成
assign Hsync = (cnt_h>=H_time)?0:1;
assign Vsync = (cnt_v>=V_time)?0:1;
当计数器在 0 ~ H_time-1(0~95)时,Hsync 输出低电平(同步脉冲);同理,V_time 决定了 Vsync 的低电平脉宽。这与时序图中的负极性同步脉冲相对应。
3.5全部代码
module VGA (
input wire Pclk ,
input wire Prst_n ,
output wire Hsync ,//行同步
output wire Vsync ,//场同步
output wire data_valid //数据有效
);
//行信号基本时序
parameter H_totaltime = 800 ;
parameter H_right = 8 ;//右边沿
parameter H_front = 8 ;//前沿
parameter H_left = 8 ;//左边沿
parameter H_back = 40 ;//后沿
parameter H_time = 96 ;//行同步时间
parameter H_addrtime = 640 ;
//场信号基本时序
parameter V_totaltime = 525 ;
parameter V_top = 8 ;//上边沿
parameter V_front = 2 ;//前沿(前肩时间)
parameter V_bottom = 8 ;//下边沿
parameter V_back = 25 ;//后沿/削隐时间
parameter V_time = 2 ;//场同步时间
parameter V_addrtime = 480 ;
reg [9:0] cnt_h, cnt_v ;
always @(posedge Pclk or negedge Prst_n) begin
if(!Prst_n)
cnt_h<=0;
else if(cnt_h==H_totaltime-1)
cnt_h<=0;
else
cnt_h<=cnt_h+1;
end
always @(posedge Pclk or negedge Prst_n) begin
if(!Prst_n)
cnt_v<=0;
else if(cnt_h==H_totaltime-1)begin
if(cnt_v==V_totaltime-1)
cnt_v<=0;
else
cnt_v<=cnt_v+1;
end
end
assign data_valid = (cnt_h>=(H_time+H_back+H_left))&&(cnt_h<(H_time+H_back+H_left+H_addrtime))
&&(cnt_v>=(V_top+V_back+V_time))&&(cnt_v<(V_top+V_back+V_time+V_addrtime));
assign Hsync = (cnt_h>=H_time)?0:1 ;
assign Vsync = (cnt_v>=V_time)?0:1 ;
endmodule

