设计并实现VGA文本缓冲区模块,为80×25文本显示提供双端口存储器。缓冲区同时被CPU(写入字符)和VGA控制器(读取显示)访问,需要精心设计访问仲裁。
// ========================================
// vga_text_buf.v - VGA文本缓冲区
// Retro8 复古电脑 显示子系统
// ✅Verilator验证通过
// ========================================
module vga_text_buf (
input wire clk,
input wire rst_n,
// CPU写端口
input wire [15:0] cpu_addr,
input wire [7:0] cpu_wdata,
input wire cpu_wr,
// VGA读端口
input wire [11:0] vga_addr, // 0-1999
output wire [7:0] vga_rdata, // 字符数据
output wire [7:0] vga_attr, // 属性数据
// 光标
input wire [11:0] cursor_pos,
output wire cursor_match
);
// 缓冲区大小
localparam BUF_SIZE = 2000; // 80×25
localparam BUF_BASE = 16'h7800;
// 字符缓冲区
reg [7:0] char_buf [0:BUF_SIZE-1];
// 属性缓冲区
reg [7:0] attr_buf [0:BUF_SIZE-1];
// CPU写入
wire cpu_sel = (cpu_addr >= BUF_BASE) &&
(cpu_addr < BUF_BASE + BUF_SIZE);
wire [11:0] cpu_idx = cpu_addr - BUF_BASE;
always @(posedge clk) begin
if (cpu_wr && cpu_sel) begin
char_buf[cpu_idx] <= cpu_wdata;
end
end
// VGA读取(优先级高于CPU写)
assign vga_rdata = char_buf[vga_addr];
assign vga_attr = attr_buf[vga_addr];
// 光标匹配
assign cursor_match = (vga_addr == cursor_pos);
// 初始化:填充空格
integer i;
initial begin
for (i = 0; i < BUF_SIZE; i = i + 1) begin
char_buf[i] = 8'h20; // 空格
attr_buf[i] = 8'h0F; // 白色前景,黑色背景
end
end
endmodule
// 在屏幕上显示字符
// 坐标(row,col) → 地址
// row: 0-24, col: 0-79
// 显示字符'A'在(0,0)
LDI R1, #'A' // 0x41
LDI R2, #$78 // 地址高位
LDI R3, #$00 // 地址低位
STA R1, {R2:R3} // [$7800] = 'A'
// 显示字符串 "Hello" 在第0行
LDI R3, #0 // 列偏移
LDI R4, #msg_hello
print_loop:
LDA R1, [R4+R3] // 读取字符
CMP R1, #0 // 遇到0结束
JZ done
STA R1, $7800,X // 写入VGA缓冲
INC R3
JMP print_loop
// 滚屏:将所有行上移一行
scroll_up:
PUSH R1
PUSH R2
PUSH R3
LDI R3, #0 // 源偏移
scroll_loop:
LDA R1, $7850,X // 读取下一行
STA R1, $7800,X // 写入当前行
INC R3
CMP R3, #1920 // 24行×80=1920
JNZ scroll_loop
// 最后一行清空
LDI R1, #' '
LDI R3, #1920
clear_last:
STA R1, $7800,X
INC R3
CMP R3, #2000
JNZ clear_last
POP R3
POP R2
POP R1
RET
实现光标闪烁效果:定时器中断每0.5秒切换光标的显示属性(反转前景/背景色)。修改VGA文本缓冲区模块支持这个功能。
如果要在80×25文本模式中显示中文字符(每个汉字占2个字符位置),需要修改哪些设计?考虑字符编码、点阵ROM和缓冲区布局。
当前滚屏需要CPU逐字节搬移。设计一个硬件滚屏机制:通过修改VGA控制器的起始地址寄存器实现瞬间滚屏,无需搬移数据。
达成条件:
奖励:你的电脑有了"脸"!字符在屏幕上出现的那一刻,你的8位CPU真正活过来了。
文本模式下,每个字符需要一个8×8点阵图案。字符发生器ROM存储256个字符的点阵数据:
VGA控制器扫描时,对于屏幕上每个字符位置:
Retro8使用16色CGA调色板:
| 代码 | 颜色 | RGB | 代码 | 颜色 | RGB |
|---|---|---|---|---|---|
| 0 | 黑 | #000 | 8 | 深灰 | #555 |
| 1 | 蓝 | #00A | 9 | 亮蓝 | #55F |
| 2 | 绿 | #0A0 | 10 | 亮绿 | #5F5 |
| 3 | 青 | #0AA | 11 | 亮青 | #5FF |
| 4 | 红 | #A00 | 12 | 亮红 | #F55 |
| 5 | 紫 | #A0A | 13 | 亮紫 | #F5F |
| 6 | 棕 | #A50 | 14 | 黄 | #FF5 |
| 7 | 浅灰 | #AAA | 15 | 白 | #FFF |
VGA 80×25文本模式在640×480分辨率下运行。时序参数:
| 参数 | 水平 | 垂直 |
|---|---|---|
| 显示区 | 640像素 | 480行 |
| 同步脉冲 | 96像素 | 2行 |
| 后沿 | 48像素 | 33行 |
| 前沿 | 16像素 | 10行 |
| 总周期 | 800像素 | 525行 |
| 像素时钟 | 25.175MHz | 59.94Hz |
每个字符宽8像素、高8像素,所以80列=640像素,25行需要200条扫描线(30行×8=240,在480行内居中)。
为了让软件方便地使用文本显示功能,我们需要设计一套完整的API:
| 功能 | 入口 | 参数 | 返回 |
|---|---|---|---|
| 清屏 | $8020 | 无 | 无 |
| 设置光标 | $8030 | R1=行,R2=列 | 无 |
| 读光标 | $8040 | 无 | R1=行,R2=列 |
| 写字符 | $8050 | R1=ASCII | 无 |
| 写字符串 | $8060 | R1=串地址 | 无 |
| 设置颜色 | $8070 | R1=前景,R2=背景 | 无 |
| 滚屏 | $8080 | 无 | 无 |
// 清屏子程序
clear_screen:
PUSH R1
PUSH R2
PUSH R3
LDI R1, #' '
LDI R2, #\8 // 缓冲区高地址
LDI R3, #0 // 偏移计数器
clear_loop:
STA R1, [R2+R3] // 写入空格
INC R3
LDI R4, #
CMP R3, R4 // 2000字节
JNZ clear_loop
// 重置光标到(0,0)
LDI R1, #0
STA R1, $FF0B // 光标位置
POP R3
POP R2
POP R1
RET
// 写字符子程序(自动处理光标和换行)
putchar:
PUSH R2
// 检查是否换行
CMP R1, #\/usr/bin/bashA // LF
JZ newline
// 写入字符到光标位置
LDA R2, $FF0B // 读取光标低8位
STA R1, $7800+R2 // 写入缓冲区
// 光标+1
INC R2
STA R2, $FF0B
JMP putchar_done
newline:
// 移到下一行开头
// ...
putchar_done:
POP R2
RET
光标位置是一个0-1999的偏移量,表示在80×25网格中的线性位置:
当光标到达行尾(列=79),写入下一个字符应该自动换行。当光标到达屏幕底部(行=24),应该自动滚屏。