以下内容学习自开源骚客教程: SDRAM那些事儿第二季。
一、VGA时序标准
VGA 显示器扫描方式为从屏幕左上角一点开始,从左向右逐点扫描。当扫描完一行后,电子束就会回到屏幕的左边下一行起始位置开始下一行的扫描,而在此期间,CRT 对电子束进行消隐,每行结束时,用行同步信号进行同步;当扫描完所有的行,形成一帧,则用场同步信号进行场同步,并使扫描点回到屏幕左上方,同时进行场消隐,开始下一帧。
以行同步时序为例说明:“Active” Video 是一行视频的有效像素,大部分分辨率时钟中 Top/Left Border 和 Bottom / Right Border 都是 0。“Blanking” 是一行的同步时间,“Blanking” 时间加上 “Active” Video 时间就是一行的时间。“Blanking” 又分为 “Front Porch”、“Sync”、“Back Porch” 三段。
完成一行扫描的时间称为水平扫描时间,其倒数称为行频率;完成一帧扫描的时间称为垂直扫描时间,其倒数称为场频率,即刷新一屏的频率,常见的有 60 Hz,75 Hz 等。标准的 VGA 显示场频为 60 Hz。
时钟频率的确定:以 1024x768@59.94Hz(60Hz)为例,每场对应 806 个行周期,其中 768 为显示行。每显示行包括 1344 点时钟,其中 1024 点为有效显示区。由此可知:需要点时钟频率: 806 * 1344 * 60 约 65 MHz。
这里附上 1024x768@60Hz 的时序参数(需求其余规格可自行在网上查找):
二、VGA模块设计
2.1 与教程不同的小差别
这里开发板的 VGA 相关电路和教程里的板子不太一样,因为 ADV7123 的缘故多了个 VGA_BLACK 的信号(结果在网上找了找数据手册发现真名是 /BLANK ?),作用类似开关;R、G、B 三种通道数均被物理限制到了 888 规格。
PS:目前 VGA 模块仅实现彩条显示测试。
那么在源码上自然也是要做一定的改动才行。此外,由于 1024x768 的 VGA 显示规格要求像素输入时钟为 65MHz,而开发板自带的晶振只有 50MHz 和 27MHz 两种,因此还需要使用到 PLL 锁相环对系统时钟进行分频/倍频才行。添加方法类似 FIFO。
2.2 使用 PLL IP核
打开 Quartus II 软件,选择菜单栏 Tools
⟶
\longrightarrow
⟶ MegaWizard Plug-In Manager:
检索要生成的 IP 核,并指定存放位置,之后会进入到 IP 核属性的配置页面:
之后配置好要输出的时钟频率即可(areset 信号和 locked 信号可酌情使用):
最后还需要在 SDC 文件中添加 PLL 时钟信息,添加方法类似系统时钟,选择菜单栏 Constraints
⟶
\longrightarrow
⟶ Derive PLL Clocks… 后更新时序网表即可看到新的 PLL 时钟,写入 SDC 即可完成时序约束的更新:
2.3 模块源码
module vga_driver(
// system signals
input sclk , // 像素时钟 65MHz
input s_rst_n , // 复位信号,低电平有效
// VGA
output wire vga_hsync , // 行同步信号
output wire vga_vsync , // 场同步信号
output wire vga_blank ,
output reg [15:0] vga_rgb // RGB565
);
/**************************************************************************/
/***************** Define Parameter and Internal Signals ******************/
/**************************************************************************/
// 不需要定义 H_FRONT_PORCH 和 V_FRONT_PORCH 是因为直接使用 H_TOTAL_TIME 与 V_TOTAL_TIME 作为计数周期
localparam H_TOTAL_TIME = 1344;
localparam H_ADD_TIME = 1024;
localparam H_SYNC_TIME = 136;
localparam H_BACK_PORCH = 160;
localparam V_TOTAL_TIME = 806;
localparam V_ADD_TIME = 768;
localparam V_SYNC_TIME = 6;
localparam V_BACK_PORCH = 29;
reg [10:0] cnt_h; // 从行同步信号拉高开始作为计数周期的起点
reg [9:0] cnt_v; // 场同步计数同理
/**************************************************************************/
/******************************* Main Code ********************************/
/**************************************************************************/
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
cnt_h <= 'd0;
else if(cnt_h >= H_TOTAL_TIME)
cnt_h <= 'd0;
else
cnt_h <= cnt_h + 1'b1;
end
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
cnt_v <= 'd0;
else if(cnt_v >= V_TOTAL_TIME && cnt_h >= H_TOTAL_TIME) // 最后一行也扫描完了才算完整的一场
cnt_v <= 'd0;
else if(cnt_h >= H_TOTAL_TIME)
cnt_v <= cnt_v + 1'b1;
end
// 彩条测试
always @(posedge sclk or negedge s_rst_n)
begin
if(s_rst_n == 1'b0)
vga_rgb <= 'd0;
else if(cnt_v >= (V_SYNC_TIME + V_BACK_PORCH) && cnt_v < (V_SYNC_TIME + V_BACK_PORCH + V_ADD_TIME)) // 场有效区域
begin
if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH - 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 256)) // 第一个彩条
vga_rgb <= 16'h0fff;
else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH - 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 512))
vga_rgb <= 16'hf0ff;
else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH- 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 768))
vga_rgb <= 16'hff0f;
else if(cnt_h >= (H_SYNC_TIME + H_BACK_PORCH- 1) && cnt_h < (H_SYNC_TIME + H_BACK_PORCH - 1 + 1024))
vga_rgb <= 16'hfff0;
else
vga_rgb <= 16'h0000;
end
else
vga_rgb <= 16'h0000;
end
assign vga_hsync = (cnt_h < H_SYNC_TIME) ? 1'b1 : 1'b0;
assign vga_vsync = (cnt_v < V_SYNC_TIME) ? 1'b1 : 1'b0;
assign vga_blank = ~(vga_hsync & vga_vsync);
endmodule
VGA 驱动比较简单,很好搞定,所以板级调试就不放图出来了(并不是因为测试的时候忘了拍照测试完了又不想专门为了拍照再测试一次)。