feat(saolei): 新增纯 C 扫雷(MMIO 显示 + 中断读鼠标),扩展 CPU 外设并清理仓库
## 背景
在原有五级流水 CPU 的基础上,用一颗 RISC-V 工具链把"纯 C 写的扫雷"编译成机器码
固化进片内 ROM 运行,并为此把 MMIO 外设从"只有一个 LED 寄存器"扩展成
显示(VGA 显存)+ 七段数码管 + 鼠标事件(含中断)的完整子系统。
原 LED MMIO 的语义保持不变,原有跑马灯/中断/指令回归不受影响。
## 硬件(pipelined-cpu/srcs)
- cpu21_mmio.v:扩展成 MMIO 中枢,新增
* 七段数码管寄存器(0x1004,8 位十六进制)
* 鼠标寄存器组(0x1100+):STATUS/DATA/ACK/CTRL/INPUT/TIME/FRAME/EVCNT/LEVEL/MAGIC
—— 含 16 项事件 FIFO 与 mouse_irq_o(读空自动回 0,不丢事件)
* 显示控制器(0x1140+):CTRL(光标可见/背景色)、CURSOR(列、行)
* 显存窗口(0x2000-0x27FF):写端口输出到板级顶层的双口 RAM,读保持组合读
* LEVEL(0x1120):软件中断寄存器,写 1 拉起 level_irq_o(用作"关卡入口")
* 地址译码、字节写合并、读数据 mux 全部按原风格实现
- 新增外设 RTL:cpu21_vga.v(640x480@60 按格子扫描,8x8 字模放大 2 倍)、
cpu21_seg7.v(8 位数码管动态扫描)、cpu21_pointer.v(5 向按键→鼠标事件,1 ms 去抖
+ 事件排队)、cpu21_ps2_mouse.v(可选真鼠标,PS/2 11 位帧接收 + 3 字节包解析)
- cpu21_riscv_redirect_int_bpb.v:新增外设总线端口(显存写/读回、鼠标事件输入、
中断输出、数码管数据、帧脉冲),例化扩展后的 cpu21_mmio
- cpu21_board_top.v:接入显存双口 RAM + VGA + 数码管 + 鼠标事件源,中断接线为
irq_i = {SW2, LEVEL|SW1, 鼠标|SW0};ROM 宏切换(定义 SAOLEI_ROM 加载扫雷程序);
ROM_WORDS 1024→8192
- cpu21_board_top.xdc:新增按键、SW[3]、8 位数码管、VGA、PS/2 的全部引脚约束
- tb_board_top.v:新端口显式接 0/1,避免悬空影响原跑马灯回归
- create_project.tcl:把 cpu21_saolei_rom.vh 按 Verilog Header 条件加入
## 软件(saolei/)
- sw/mmio.h:全部 MMIO 地址与格子编码(换地址只改这一个文件)
- sw/saolei.c:纯 C 扫雷 —— 背景"伪操作系统" main()(开始界面)+ 关卡 level_main()
(在 IRQ1 里跑完整局)+ 棋盘位图/布雷(首点安全)/BFS 展开/插旗/胜负判定/逐格绘制
- sw/isr.c:**中断读鼠标的代码**。三个固定地址入口:
* isr1_entry(.text.isr1=0x100)关卡入口:保存全部调用者保存寄存器 +
csrrsi 打开 MIE(让鼠标中断可嵌套)→ level_main
* isr2_entry(.text.isr2=0x200)鼠标读取:只存 ra → 纯 C 主体 isr_body
* isr3_entry(.text.isr3=0x300)备用:直接 uret
- sw/start.c:设 sp、清 .bss、进 main(只有这一处必要内联汇编)
- sw/linker.ld:复位 0x0、三个中断入口 0x100/0x200/0x300、栈顶 0x0FFC;
用 ASSERT 禁止 .data/.rodata(这颗核读不到 ROM 里的数据)
## 构建与工具链适配(saolei/tools)
- build.ps1:一键 C → .o → ELF → .hex + cpu21_saolei_rom.vh,任一步失败立刻停止
- check_isa.py:用"指令白名单"逐条检查反汇编,出现本核未实现的指令即报错并给修复提示
- elf2hex.py:裸二进制 → $readmemh hex + ROM 初始化头文件
- 三处针对本核的关键配置(都已写进脚本注释):
1) -march=rv32im_zicsr(GCC 15 起 rv32im 不含 Zicsr,csrrsi 会报错)
2) 不能禁链接器松弛:gcc 函数调用默认是 auipc+jalr,靠松弛压成 jal,
禁掉后 35 条 auipc 会留在代码里(本核没有 auipc);
用 -msmall-data-limit=0 防止松弛引入 gp 相对寻址
3) 中断入口地址必须写在段声明上(.text.isr1 0x100 : ...),
只写 ". = 0x100;" 会被 ld 忽略(实测 isr1 掉到了 0x14)
- 实测结果:text=3368B、data=0、bss=1436B,842 条指令全部通过 ISA 检查,
入口地址 _start=0x0 / isr1_entry=0x100 / isr2_entry=0x200 / isr3_entry=0x300
## 仓库清理
- 删除被工程取代的 saolei/hw、saolei/constrs(外设已并入 srcs/rtl)
- 删除全部 Vivado 生成物:.xpr、.runs、.sim、.cache、.hw、.ip_user_files、.Xil、
vivado.jou/log —— 需要工程时用 source create_project.tcl 一键重建(约 0.3 MB 源文件)
- .gitignore 增加 saolei/build/(构建产物可一键重建)
## 文档
- README:更新目录结构、仓库约定(不放生成物)、MMIO 地址表、文件与模块划分表、
上板说明,新增"扫雷程序(saolei/)"章节
- saolei/README.md:重写 —— 程序结构(背景/关卡/鼠标 + 三个中断入口)、
本核指令子集与对应写法、构建步骤与实测数据、MMIO 地址表、上板/仿真步骤、
操作说明、验证状态与已知可改进项
## 验证状态(如实说明)
- 已实测:C 工具链链路(编译/链接/ISA 检查/hex 生成/入口地址逐条核对)
- 未验证:所有 RTL 未经综合与仿真(本机无 Vivado),C 程序也未在仿真器/硬件上运行过;
建议先跑 tools/run_regress.ps1 确认原回归仍全 PASS,再用 SAOLEI_ROM 宏做行为仿真
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
`timescale 1ns / 1ps
|
||||
`default_nettype none
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// cpu21_vga —— 640x480@60 的"格子"显示控制器(显存端口接板级顶层的双口 RAM)。
|
||||
//
|
||||
// CPU 不直接操作像素:它只通过 MMIO 往显示缓冲区里写"每格 1 字节"(字形 + 颜色),
|
||||
// 本模块按像素扫描把格子展开成画面(8x8 字模放大 2 倍填满 16x16 的格子)。
|
||||
// 这样一格只要 1 次字节写,刷屏开销极小。
|
||||
//
|
||||
// 网格:40 列 x 30 行,每格 16x16 像素 = 640x480。
|
||||
//
|
||||
// 时钟:本模块工作在 CPU 时钟(clk)下,用 tick(每 2 个 clk 一个使能)产生
|
||||
// 25 MHz 的像素节拍。**不引入第二个时钟域**,因此显示缓冲区的读(本模块)
|
||||
// 与写(MMIO,同一时钟)之间没有跨时钟域问题。
|
||||
//
|
||||
// 光标:由 CURSOR 寄存器的 (x,y) 决定,在对应格子上画一个白色闪动的方框
|
||||
// (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:2]; // 字地址(1 字 = 4 格)
|
||||
|
||||
wire [1:0] lane = cidx[1:0];
|
||||
wire [31:0] cell_word = fb_word_i;
|
||||
wire [7:0] cell_byte = cell_word >> {lane, 3'b000};
|
||||
|
||||
wire [3:0] glyph = cell_byte[3:0];
|
||||
wire [2:0] gcolor = cell_byte[6:4];
|
||||
// 全局背景色(字模点为 0 时显示的颜色)
|
||||
wire [2:0] bgcolor = ctrl_i[7:4];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 8x8 字模(bit7 = 最左像素),放大 2 倍填满 16x16 的格子
|
||||
// ---------------------------------------------------------------------------
|
||||
wire [2:0] fx = hx[3:1];
|
||||
wire [2:0] fy = vy[3:1];
|
||||
|
||||
function [7:0] font_row;
|
||||
input [3:0] g;
|
||||
input [2:0] r;
|
||||
begin
|
||||
case (g)
|
||||
4'd0:
|
||||
font_row = 8'h00; // 空白
|
||||
4'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
|
||||
4'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
|
||||
4'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
|
||||
4'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
|
||||
4'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
|
||||
4'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
|
||||
4'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
|
||||
4'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
|
||||
4'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; // 未揭开的方块
|
||||
4'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; // 旗
|
||||
4'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; // 雷
|
||||
4'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; // 爆炸
|
||||
4'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; // 问号
|
||||
4'd14:
|
||||
font_row = (r == 3'd3) ? 8'hFF : (r == 3'd4) ? 8'hFF : 8'h00; // 横线
|
||||
4'd15:
|
||||
font_row = 8'hFF; // 实心
|
||||
default:
|
||||
font_row = 8'h00;
|
||||
endcase
|
||||
end
|
||||
endfunction
|
||||
|
||||
wire [7:0] frow = font_row(glyph, fy);
|
||||
wire glyph_px = frow[3'd7 - fx]; // 1 = 画字模点
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 光标框(闪动)
|
||||
// ---------------------------------------------------------------------------
|
||||
reg [24:0] blink_q;
|
||||
always @(posedge clk or posedge reset) begin
|
||||
if (reset)
|
||||
blink_q <= 25'd0;
|
||||
else if (tick)
|
||||
blink_q <= blink_q + 25'd1;
|
||||
end
|
||||
wire blink = blink_q[24];
|
||||
|
||||
wire cursor_here = (cursor_i[4:0] == ccol) && (cursor_i[12:8] == crow);
|
||||
wire on_border = (hx[3:0] == 4'd0) || (hx[3:0] == 4'd15) ||
|
||||
(vy[3:0] == 4'd0) || (vy[3:0] == 4'd15);
|
||||
wire cursor_box = cursor_here && blink && 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'd7 : px_color;
|
||||
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
|
||||
Tcl 一键重建
|
||||
Reference in New Issue
Block a user