设计并实现VGA文本模式控制器,在显示器上呈现80×25的字符界面。VGA信号时序是数字电路设计中最经典也最考验功底的课题——精确到像素级的时序控制,是硬件设计能力的试金石。
VGA(Video Graphics Array)是IBM在1987年随PS/2系列推出的显示标准。虽然物理接口已被HDMI/DP取代,但VGA的信号时序原理仍然是理解所有显示接口的基础。VGA的核心是三路模拟信号(R/G/B)加上两路同步信号(HSYNC/VSYNC)。
| 参数 | 水平(像素) | 垂直(行) | 时钟/时间 |
|---|---|---|---|
| 可视区域 | 640 | 480 | 25.42μs / 15.25ms |
| 前肩(Front Porch) | 16 | 10 | 0.64μs / 0.318ms |
| 同步脉冲(Sync) | 96 | 2 | 3.81μs / 0.0635ms |
| 后肩(Back Porch) | 48 | 33 | 1.91μs / 1.047ms |
| 总计 | 800 | 525 | 31.78μs / 16.68ms |
| 像素时钟 | 25.175 MHz (实际可用25MHz) | ||
| 刷新率 | 59.94 Hz ≈ 60Hz | ||
我们的VGA文本模式参考了IBM CGA/MDA的经典设计:
| 参数 | 规格 | 说明 |
|---|---|---|
| 分辨率 | 80×25字符 | 共2000个字符位置 |
| 字符大小 | 8×8像素 | 8像素宽×8像素高 |
| 像素分辨率 | 640×480 | 标准VGA |
| 颜色深度 | 4位/色 | 16级灰度或16色 |
| 属性字节 | 8位 | 高4位前景色+低4位背景色 |
| 字符缓冲 | 2000字节 | $7800-$7FFF(偶地址) |
| 属性缓冲 | 2000字节 | $7800-$7FFF(奇地址) |
| 字符ROM | 2048字节 | 256字符×8行/字符 |
| 光标 | 可编程位置 | 闪烁频率约0.5Hz |
// vga_text_mode.v - VGA文本模式控制器
// 80×25文本模式,8×8字符,16色前景/背景
module vga_text_mode (
input wire clk, // 25MHz像素时钟
input wire rst_n,
output reg [3:0] vga_r,
output reg [3:0] vga_g,
output reg [3:0] vga_b,
output reg vga_hs,
output reg vga_vs,
input wire [7:0] char_data,
output reg [11:0] char_addr,
input wire [7:0] attr_data,
output reg [11:0] attr_addr,
input wire [7:0] font_data,
output reg [10:0] font_addr,
input wire [11:0] cursor_pos,
input wire cursor_on
);
// VGA 640×480@60Hz 时序常数
localparam H_VISIBLE = 640, H_FRONT = 16,
H_SYNC = 96, H_BACK = 48,
H_TOTAL = 800;
localparam V_VISIBLE = 480, V_FRONT = 10,
V_SYNC = 2, V_BACK = 33,
V_TOTAL = 525;
reg [9:0] h_count, v_count;
// 时序计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
h_count <= 10'd0; v_count <= 10'd0;
vga_hs <= 1'b1; vga_vs <= 1'b1;
end else begin
if (h_count == H_TOTAL - 1)
h_count <= 10'd0;
else
h_count <= h_count + 10'd1;
if (h_count == H_TOTAL - 1) begin
if (v_count == V_TOTAL - 1)
v_count <= 10'd0;
else
v_count <= v_count + 10'd1;
end
// 同步信号(负极性)
vga_hs <= ~((h_count >= H_VISIBLE + H_FRONT) &&
(h_count < H_VISIBLE + H_FRONT + H_SYNC));
vga_vs <= ~((v_count >= V_VISIBLE + V_FRONT) &&
(v_count < V_VISIBLE + V_FRONT + V_SYNC));
end
end
// 文本定位
wire [6:0] col = h_count[9:3];
wire [4:0] row = v_count[8:4];
wire [2:0] char_row = v_count[3:1];
wire [2:0] pixel_col = h_count[2:0];
wire display_active = (h_count < H_VISIBLE) &&
(v_count < V_VISIBLE);
// 字符缓冲地址
wire [11:0] screen_addr = row * 80 + {{5{1'b0}}, col};
always @(posedge clk) begin
char_addr <= screen_addr;
attr_addr <= screen_addr;
end
// 字符ROM地址
reg [7:0] char_reg;
always @(posedge clk) begin
char_reg <= char_data;
font_addr <= {char_data, char_row};
end
// 属性锁存
reg [7:0] attr_reg;
always @(posedge clk) attr_reg <= attr_data;
wire [3:0] fg_color = attr_reg[7:4];
wire [3:0] bg_color = attr_reg[3:0];
// 光标闪烁
reg [19:0] blink_counter;
reg cursor_blink;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
blink_counter <= 20'd0;
cursor_blink <= 1'b1;
end else begin
blink_counter <= blink_counter + 20'd1;
if (blink_counter == 20'd0)
cursor_blink <= ~cursor_blink;
end
end
wire cursor_here = (screen_addr == cursor_pos) &&
cursor_on && cursor_blink;
// 像素输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
vga_r <= 4'd0; vga_g <= 4'd0; vga_b <= 4'd0;
end else if (display_active) begin
if (font_data[pixel_col] || cursor_here) begin
vga_r <= fg_color; vga_g <= fg_color; vga_b <= fg_color;
end else begin
vga_r <= bg_color; vga_g <= bg_color; vga_b <= bg_color;
end
end else begin
vga_r <= 4'd0; vga_g <= 4'd0; vga_b <= 4'd0;
end
end
endmodule
字符ROM存储每个ASCII字符的8×8点阵数据。每个字符占8字节,共256×8=2048字节:
// char_rom.v - 8×8字符点阵ROM
// 存储256个字符的8×8点阵
module char_rom (
input wire clk,
input wire [10:0] addr, // {char_code[7:0], row[2:0]}
output reg [7:0] data // 8像素行数据
);
reg [7:0] rom [0:2047];
initial begin
// 初始化全部为空
integer i;
for (i = 0; i < 2048; i = i + 1)
rom[i] = 8'd0;
// 字符 'A' (0x41) - 8行点阵
rom[8'h41*8+0] = 8'b00011000; // ##
rom[8'h41*8+1] = 8'b00100100; // # #
rom[8'h41*8+2] = 8'b01000010; // # #
rom[8'h41*8+3] = 8'b01111110; // ######
rom[8'h41*8+4] = 8'b01000010; // # #
rom[8'h41*8+5] = 8'b01000010; // # #
rom[8'h41*8+6] = 8'b01000010; // # #
rom[8'h41*8+7] = 8'b00000000; //
// 字符 '0' (0x30)
rom[8'h30*8+0] = 8'b00111100; // ####
rom[8'h30*8+1] = 8'b01100110; // ## ##
rom[8'h30*8+2] = 8'b01100110; // ## ##
rom[8'h30*8+3] = 8'b01101110; // ## ###
rom[8'h30*8+4] = 8'b01110110; // ### ##
rom[8'h30*8+5] = 8'b01100110; // ## ##
rom[8'h30*8+6] = 8'b00111100; // ####
rom[8'h30*8+7] = 8'b00000000; //
// 字符 'H' (0x48) - "HELLO"中的H
rom[8'h48*8+0] = 8'b01000010;
rom[8'h48*8+1] = 8'b01000010;
rom[8'h48*8+2] = 8'b01000010;
rom[8'h48*8+3] = 8'b01111110;
rom[8'h48*8+4] = 8'b01000010;
rom[8'h48*8+5] = 8'b01000010;
rom[8'h48*8+6] = 8'b01000010;
rom[8'h48*8+7] = 8'b00000000;
end
always @(posedge clk)
data <= rom[addr];
endmodule
VGA控制器需要持续读取显存来刷新屏幕,而CPU也需要随时写入字符。这需要双端口RAM来解决访问冲突:
// vga_dual_ram.v - VGA双端口显示缓冲
// CPU端口写+VGA端口读,避免总线冲突
module vga_dual_ram (
input wire clk,
// CPU写端口
input wire cpu_we,
input wire [10:0] cpu_addr,
input wire [7:0] cpu_wdata,
// VGA读端口
input wire [10:0] vga_addr,
output wire [7:0] vga_rdata
);
reg [7:0] mem [0:1999]; // 2000个字符位置
always @(posedge clk) begin
if (cpu_we)
mem[cpu_addr] <= cpu_wdata;
end
assign vga_rdata = mem[vga_addr];
endmodule
// tb_vga_text_mode.v - VGA文本模式测试
module tb_vga_text_mode;
reg clk, rst_n;
wire [3:0] vga_r, vga_g, vga_b;
wire vga_hs, vga_vs;
reg [7:0] char_data, attr_data, font_data;
wire [11:0] char_addr, attr_addr;
wire [10:0] font_addr;
reg [11:0] cursor_pos;
reg cursor_on;
vga_text_mode uut (.*);
initial clk = 0;
always #20 clk = ~clk; // 25MHz
// 模拟字符ROM响应
reg [7:0] font_mem [0:2047];
initial begin
integer i;
for (i = 0; i < 2048; i = i + 1)
font_mem[i] = 8'd0;
// 'A' 的点阵
font_mem[8'h41*8+0] = 8'b00011000;
font_mem[8'h41*8+1] = 8'b00100100;
font_mem[8'h41*8+2] = 8'b01000010;
font_mem[8'h41*8+3] = 8'b01111110;
font_mem[8'h41*8+4] = 8'b01000010;
font_mem[8'h41*8+5] = 8'b01000010;
font_mem[8'h41*8+6] = 8'b01000010;
end
always @(posedge clk)
font_data <= font_mem[font_addr];
// 模拟字符缓冲:所有位置都是'A'
always @(*)
char_data = 8'h41;
// 属性:白字蓝底
always @(*)
attr_data = 8'hF1; // fg=0xF(白), bg=0x1(蓝)
initial begin
rst_n = 0; cursor_pos = 12'd0; cursor_on = 0;
#100; rst_n = 1;
// 等待一帧完成验证同步时序
#21000000; // ~一帧时间
// 验证HSYNC/VSYNC波形
if (vga_hs !== 1'bx && vga_vs !== 1'bx)
$display("PASS: Sync signals active");
else
$display("FAIL: Sync signals invalid");
$display("VGA text mode test complete");
$finish;
end
endmodule
verilator --lint-only 检查,无错误无警告。
我们使用经典的CGA 16色调色板,这是8位电脑最标志性的配色方案:
| 代码 | 颜色 | RGB近似值 | 示例 |
|---|---|---|---|
| 0 | 黑色 | #000000 | ███ |
| 1 | 蓝色 | #0000AA | ███ |
| 2 | 绿色 | #00AA00 | ███ |
| 3 | 青色 | #00AAAA | ███ |
| 4 | 红色 | #AA0000 | ███ |
| 5 | 品红 | #AA00AA | ███ |
| 6 | 棕色 | #AA5500 | ███ |
| 7 | 浅灰 | #AAAAAA | ███ |
| 8 | 深灰 | #555555 | ███ |
| 9 | 亮蓝 | #5555FF | ███ |
| A | 亮绿 | #55FF55 | ███ |
| B | 亮青 | #55FFFF | ███ |
| C | 亮红 | #FF5555 | ███ |
| D | 亮品红 | #FF55FF | ███ |
| E | 黄色 | #FFFF55 | ███ |
| F | 白色 | #FFFFFF | ███ |
实现文本模式的上滚功能:当光标到达第25行底部时,将2-25行的字符和属性复制到1-24行,第25行清空,光标移到第24行开头。这是一个偏移寄存器操作——提示:可以用一个滚动偏移寄存器来避免实际的数据搬运。
当前实现中R/G/B各4位都取同一个颜色值,导致只能显示灰度。修改像素输出逻辑,引入真正的16色调色板ROM:颜色代码→12位{R[3:0],G[3:0],B[3:0]}映射,让前景色0x1真正显示为蓝色而不是深灰。
扩展VGA控制器支持两种模式:80×25文本模式和40×25大字符模式(16×16字符点阵)。添加模式寄存器,根据模式调整字符ROM地址生成逻辑和像素缩放。
实现完整的光标控制:光标起始行、结束行(控制光标高度和形状),光标闪烁速度可配置。参考VGA标准中CRT控制器(MC6845)的光标寄存器设计。
你实现了VGA文本模式显示控制器!这包括:
你的8位电脑现在有了"眼睛"——能显示文字了!