`timescale 1ns / 1ps `default_nettype none // ----------------------------------------------------------------------------- // cpu21_vga —— 640x480@60 的"格子"显示控制器(显存端口接板级顶层的双口 RAM)。 // // CPU 不直接操作像素:它只通过 MMIO 往显示缓冲区里写"每格 2 字节"(字形 + 颜色), // 本模块按像素扫描把格子展开成画面(8x8 字模放大 2 倍填满 16x16 的格子)。 // 这样一格只要 1 次字节写,刷屏开销极小。 // // 网格:40 列 x 30 行,每格 16x16 像素 = 640x480。 // // 时钟:本模块工作在 CPU 时钟(clk)下,用 tick(每 2 个 clk 一个使能)产生 // 25 MHz 的像素节拍。**不引入第二个时钟域**,因此显示缓冲区的读(本模块) // 与写(MMIO,同一时钟)之间没有跨时钟域问题。 // // 光标:由 CURSOR 寄存器的 (x,y) 决定,在对应格子上画一个黄色常亮的方框(2 像素宽) // (CTRL[0] 控制显示与否)。缓冲区字节的 bit7 在本模块中不使用(保留)。 // ----------------------------------------------------------------------------- module cpu21_vga #( parameter integer H_VISIBLE = 640, parameter integer H_FP = 16, parameter integer H_SYNC = 96, parameter integer H_BP = 48, parameter integer V_VISIBLE = 480, parameter integer V_FP = 10, parameter integer V_SYNC = 2, parameter integer V_BP = 33, parameter integer COLS = 40, parameter integer ROWS = 30 ) ( input wire clk, input wire reset, input wire tick, // 25 MHz 像素使能(每 2 个 clk 一拍) // 显示缓冲区读端口 output wire [ 9:0] fb_raddr_o, input wire [31:0] fb_word_i, // 控制 input wire [31:0] ctrl_i, input wire [31:0] cursor_i, // VGA 输出 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, output reg frame_tick_o ); localparam integer H_TOTAL = H_VISIBLE + H_FP + H_SYNC + H_BP; // 800 localparam integer V_TOTAL = V_VISIBLE + V_FP + V_SYNC + V_BP; // 525 // --------------------------------------------------------------------------- // 扫描计数 // --------------------------------------------------------------------------- reg [9:0] hx, vy; always @(posedge clk or posedge reset) begin if (reset) begin hx <= 10'd0; vy <= 10'd0; end else if (tick) begin if (hx == H_TOTAL - 1) begin hx <= 10'd0; vy <= (vy == V_TOTAL - 1) ? 10'd0 : (vy + 10'd1); end else begin hx <= hx + 10'd1; end end end wire de = (hx < H_VISIBLE) && (vy < V_VISIBLE); // 同步信号在 tick 上寄存输出 wire hs_n = ~((hx >= (H_VISIBLE + H_FP)) && (hx < (H_VISIBLE + H_FP + H_SYNC))); wire vs_n = ~((vy >= (V_VISIBLE + V_FP)) && (vy < (V_VISIBLE + V_FP + V_SYNC))); // --------------------------------------------------------------------------- // 格子索引与缓冲区读出 // --------------------------------------------------------------------------- wire [5:0] ccol = hx[9:4]; // 0..39 wire [4:0] crow = vy[8:4]; // 0..29 wire [10:0] cidx = (crow << 5) + (crow << 3) + ccol; // row*40 + col assign fb_raddr_o = cidx[10:1]; // 字地址(1 字 = 2 格) wire [ 1:0] lane = {cidx[0], 1'b0}; wire [ 1:0] color_lane = lane + 2'd1; wire [31:0] cell_word = fb_word_i; wire [ 7:0] glyph_byte = cell_word >> {lane, 3'b000}; wire [ 7:0] color_byte = cell_word >> {color_lane, 3'b000}; wire [ 5:0] glyph = glyph_byte[5:0]; wire [ 2:0] gcolor = color_byte[2:0]; // 全局背景色(字模点为 0 时显示的颜色) wire [ 2:0] bgcolor = ctrl_i[7:4]; // --------------------------------------------------------------------------- // 8x8 字模(bit7 = 最左像素),放大 2 倍填满 16x16 的格子;字形编号 0..63。 // --------------------------------------------------------------------------- wire [ 2:0] fx = hx[3:1]; wire [ 2:0] fy = vy[3:1]; function [7:0] font_row; input [5:0] g; input [2:0] r; begin case (g) 6'd0: font_row = 8'h00; // 空白 6'd1: font_row = (r == 3'd0) ? 8'h18 : (r == 3'd1) ? 8'h38 : (r == 3'd2) ? 8'h18 : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 : (r == 3'd6) ? 8'h7E : 8'h00; // 1 6'd2: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 : (r == 3'd3) ? 8'h0C : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h30 : (r == 3'd6) ? 8'h7E : 8'h00; // 2 6'd3: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 : (r == 3'd3) ? 8'h1C : (r == 3'd4) ? 8'h06 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // 3 6'd4: font_row = (r == 3'd0) ? 8'h0C : (r == 3'd1) ? 8'h1C : (r == 3'd2) ? 8'h3C : (r == 3'd3) ? 8'h6C : (r == 3'd4) ? 8'h7E : (r == 3'd5) ? 8'h0C : (r == 3'd6) ? 8'h0C : 8'h00; // 4 6'd5: font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h7C : (r == 3'd3) ? 8'h06 : (r == 3'd4) ? 8'h06 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // 5 6'd6: font_row = (r == 3'd0) ? 8'h1C : (r == 3'd1) ? 8'h30 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // 6 6'd7: font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h0C : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 : (r == 3'd6) ? 8'h18 : 8'h00; // 7 6'd8: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h3C : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // 8 6'd9: font_row = (r == 3'd0) ? 8'hFF : (r == 3'd1) ? 8'h81 : (r == 3'd2) ? 8'hBD : (r == 3'd3) ? 8'hA5 : (r == 3'd4) ? 8'hA5 : (r == 3'd5) ? 8'hBD : (r == 3'd6) ? 8'h81 : 8'hFF; // 未揭开的方块 6'd10: font_row = (r == 3'd0) ? 8'h40 : (r == 3'd1) ? 8'h70 : (r == 3'd2) ? 8'h78 : (r == 3'd3) ? 8'h70 : (r == 3'd4) ? 8'h40 : (r == 3'd5) ? 8'h40 : (r == 3'd6) ? 8'h40 : 8'h00; // 旗 6'd11: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h7E : (r == 3'd2) ? 8'hFF : (r == 3'd3) ? 8'hFF : (r == 3'd4) ? 8'hFF : (r == 3'd5) ? 8'hFF : (r == 3'd6) ? 8'h7E : 8'h3C; // 雷 6'd12: font_row = (r == 3'd0) ? 8'h81 : (r == 3'd1) ? 8'h42 : (r == 3'd2) ? 8'h24 : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h24 : (r == 3'd6) ? 8'h42 : 8'h81; // 爆炸 6'd13: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h06 : (r == 3'd3) ? 8'h0C : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h00 : (r == 3'd6) ? 8'h18 : 8'h00; // 问号 6'd14: font_row = (r == 3'd3) ? 8'hFF : (r == 3'd4) ? 8'hFF : 8'h00; // 横线 6'd15: font_row = 8'hFF; // 实心 6'd16: font_row = (r == 3'd0) ? 8'h18 : (r == 3'd1) ? 8'h3C : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h7E : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h66 : 8'h00; // A 6'd17: font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h7C : 8'h00; // B 6'd18: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h60 : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // C 6'd19: font_row = (r == 3'd0) ? 8'h78 : (r == 3'd1) ? 8'h6C : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h6C : (r == 3'd6) ? 8'h78 : 8'h00; // D 6'd20: font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 : (r == 3'd6) ? 8'h7E : 8'h00; // E 6'd21: font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 : (r == 3'd6) ? 8'h60 : 8'h00; // F 6'd22: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h6E : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3E : 8'h00; // G 6'd23: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h7E : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h66 : 8'h00; // H 6'd24: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h18 : (r == 3'd2) ? 8'h18 : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 : (r == 3'd6) ? 8'h3C : 8'h00; // I 6'd25: font_row = (r == 3'd0) ? 8'h1E : (r == 3'd1) ? 8'h0C : (r == 3'd2) ? 8'h0C : (r == 3'd3) ? 8'h0C : (r == 3'd4) ? 8'h0C : (r == 3'd5) ? 8'h6C : (r == 3'd6) ? 8'h38 : 8'h00; // J 6'd26: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h6C : (r == 3'd2) ? 8'h78 : (r == 3'd3) ? 8'h70 : (r == 3'd4) ? 8'h78 : (r == 3'd5) ? 8'h6C : (r == 3'd6) ? 8'h66 : 8'h00; // K 6'd27: font_row = (r == 3'd0) ? 8'h60 : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h60 : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 : (r == 3'd6) ? 8'h7E : 8'h00; // L 6'd28: font_row = (r == 3'd0) ? 8'h63 : (r == 3'd1) ? 8'h77 : (r == 3'd2) ? 8'h7F : (r == 3'd3) ? 8'h6B : (r == 3'd4) ? 8'h63 : (r == 3'd5) ? 8'h63 : (r == 3'd6) ? 8'h63 : 8'h00; // M 6'd29: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h76 : (r == 3'd2) ? 8'h7E : (r == 3'd3) ? 8'h6E : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h66 : 8'h00; // N 6'd30: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // O 6'd31: font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h60 : (r == 3'd5) ? 8'h60 : (r == 3'd6) ? 8'h60 : 8'h00; // P 6'd32: font_row = (r == 3'd0) ? 8'h3C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h6E : (r == 3'd5) ? 8'h3C : (r == 3'd6) ? 8'h0E : 8'h00; // Q 6'd33: font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h7C : (r == 3'd4) ? 8'h78 : (r == 3'd5) ? 8'h6C : (r == 3'd6) ? 8'h66 : 8'h00; // R 6'd34: font_row = (r == 3'd0) ? 8'h3E : (r == 3'd1) ? 8'h60 : (r == 3'd2) ? 8'h60 : (r == 3'd3) ? 8'h3C : (r == 3'd4) ? 8'h06 : (r == 3'd5) ? 8'h06 : (r == 3'd6) ? 8'h7C : 8'h00; // S 6'd35: font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h18 : (r == 3'd2) ? 8'h18 : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 : (r == 3'd6) ? 8'h18 : 8'h00; // T 6'd36: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h3C : 8'h00; // U 6'd37: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h66 : (r == 3'd3) ? 8'h66 : (r == 3'd4) ? 8'h66 : (r == 3'd5) ? 8'h3C : (r == 3'd6) ? 8'h18 : 8'h00; // V 6'd38: font_row = (r == 3'd0) ? 8'h63 : (r == 3'd1) ? 8'h63 : (r == 3'd2) ? 8'h63 : (r == 3'd3) ? 8'h6B : (r == 3'd4) ? 8'h7F : (r == 3'd5) ? 8'h77 : (r == 3'd6) ? 8'h63 : 8'h00; // W 6'd39: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h3C : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h3C : (r == 3'd5) ? 8'h66 : (r == 3'd6) ? 8'h66 : 8'h00; // X 6'd40: font_row = (r == 3'd0) ? 8'h66 : (r == 3'd1) ? 8'h66 : (r == 3'd2) ? 8'h3C : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h18 : (r == 3'd5) ? 8'h18 : (r == 3'd6) ? 8'h18 : 8'h00; // Y 6'd41: font_row = (r == 3'd0) ? 8'h7E : (r == 3'd1) ? 8'h06 : (r == 3'd2) ? 8'h0C : (r == 3'd3) ? 8'h18 : (r == 3'd4) ? 8'h30 : (r == 3'd5) ? 8'h60 : (r == 3'd6) ? 8'h7E : 8'h00; // Z default: font_row = 8'h00; endcase end endfunction wire [7:0] frow = font_row(glyph, fy); wire glyph_px = frow[3'd7-fx]; // 1 = 画字模点 // --------------------------------------------------------------------------- // 光标框:黄色、常亮、2 像素宽 // 原来是 1 像素浅灰 + 闪动(blink 周期约 1.3 s):在白色未揭开格上几乎看不清, // 而且截图有一半概率正好截到"灭"的那个相位。改成黄色常亮 2 像素后, // VGA 上和图里都能一眼看到指针。想改回闪动:把 blink_q 加回来, // 再在 cursor_box 里与上 blink 即可。 // --------------------------------------------------------------------------- // 列必须比 6 位(0..39):hard 难度的棋盘宽 30,光标能到列 35, // 以前这里只取 [4:0],35 被当成 3,光标会“跳回最左边”。 wire cursor_here = (cursor_i[5:0] == ccol) && (cursor_i[12:8] == crow); wire on_border = (hx[3:0] < 4'd2) || (hx[3:0] > 4'd13) || (vy[3:0] < 4'd2) || (vy[3:0] > 4'd13); wire cursor_box = cursor_here && ctrl_i[0] && on_border; // --------------------------------------------------------------------------- // 颜色(3 位调色板 -> 4:4:4 RGB) // --------------------------------------------------------------------------- function [11:0] pal; input [2:0] c; begin case (c) 3'd0: pal = {4'h0, 4'h0, 4'h0}; // 黑 3'd1: pal = {4'h0, 4'h0, 4'hA}; // 深蓝 3'd2: pal = {4'h0, 4'hA, 4'h0}; // 绿 3'd3: pal = {4'h0, 4'hA, 4'hA}; // 青 3'd4: pal = {4'hA, 4'h0, 4'h0}; // 红 3'd5: pal = {4'hA, 4'h0, 4'hA}; // 洋红 3'd6: pal = {4'hA, 4'hA, 4'h0}; // 黄 default: pal = {4'hA, 4'hA, 4'hA}; // 浅灰 endcase end endfunction wire [ 2:0] px_color = glyph_px ? gcolor : bgcolor; wire [ 2:0] px_final = cursor_box ? 3'd6 : px_color; // 6 = 黄,光标框 wire [11:0] px_rgb = de ? pal(px_final) : 12'h000; always @(posedge clk or posedge reset) begin if (reset) begin {vga_r, vga_g, vga_b} <= 12'h000; vga_hs <= 1'b1; vga_vs <= 1'b1; frame_tick_o <= 1'b0; end else if (tick) begin {vga_r, vga_g, vga_b} <= px_rgb; vga_hs <= hs_n; vga_vs <= vs_n; frame_tick_o <= (hx == H_TOTAL - 1) && (vy == V_TOTAL - 1); end end endmodule `default_nettype wire