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,310 @@
|
||||
`timescale 1ns / 1ps
|
||||
`default_nettype none
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MMIO:把外设寄存器映射到数据存储器地址空间。
|
||||
//
|
||||
// 地址映射(字节地址,都是"字对齐"译码:同一 32 位寄存器内的任意字节偏移都命中,
|
||||
// 具体改哪几个字节由 wstrb_i 决定,与原来 LED 的语义一致):
|
||||
//
|
||||
// 0x0000_1000 LED 数据寄存器(**保持原有语义不变**,led_valid_o 拉高一拍)
|
||||
// 0x0000_1004 七段数码管:8 位十六进制数(每 4 位一个数码管,低 4 位 = 最右)
|
||||
// 0x0000_1100 + 4*k 鼠标寄存器组(见下)
|
||||
// 0x0000_1140 + 4*k 显示控制器寄存器(见下)
|
||||
// 0x0000_2000 - 0x27FF 显存窗口:40x30 格、每格 1 字节(只用到前 1200 字节)
|
||||
//
|
||||
// 鼠标寄存器(基址 0x1100):
|
||||
// +0x00 STATUS RO [0] FIFO 非空 [5:1] 项数 [6] 中断使能 [7] FIFO 满
|
||||
// +0x04 DATA RO [7:0] dx [15:8] dy [20:16] 按键(读无副作用)
|
||||
// +0x08 ACK WO 写任意值 -> 弹出 FIFO 一项
|
||||
// +0x0C CTRL RW [0] 中断使能(复位默认 1)
|
||||
// +0x10 INPUT RO [15:0] 拨码开关 [20:16] 按键(原样)
|
||||
// +0x14 TIME RO 自由运行计数器(随机种子)
|
||||
// +0x18 FRAME RO VGA 帧计数
|
||||
// +0x1C EVCNT RO 累计鼠标事件数
|
||||
// +0x20 LEVEL RW [0] 写 1 -> 拉起 level_irq_o(软件中断,用作"关卡入口");
|
||||
// 关卡程序返回前要写 0 清掉,否则下次没有上升沿
|
||||
// +0x24 MAGIC RO 0xCAFE0002(自检用:读得到说明 MMIO 活着)
|
||||
//
|
||||
// 显示控制器寄存器(基址 0x1140):
|
||||
// +0x00 CTRL RW [0] 光标可见 [6:4] 背景色(3 位调色板)
|
||||
// +0x04 CURSOR RW [5:0] 列(0..39) [13:8] 行(0..29)
|
||||
// +0x08 MAGIC RO 0xCAFE0001
|
||||
//
|
||||
// 显存本体不在本模块里:写操作通过 fb_* 端口直接送到板级顶层的双口 RAM(VGA 读
|
||||
// 同一块 RAM),读操作把 fb_rdata_i 组合回读数据,保持"组合读"的时序不变。
|
||||
//
|
||||
// 两个中断源:
|
||||
// mouse_irq_o = 中断使能 && 事件 FIFO 非空(接 irq_i[0])
|
||||
// level_irq_o = LEVEL 寄存器(软件中断,接 irq_i[1],用来"进入关卡")
|
||||
//
|
||||
// 以后要增加外设(UART、SD 卡等)时只需在这里扩展地址译码。
|
||||
// -----------------------------------------------------------------------------
|
||||
module cpu21_mmio #(
|
||||
parameter [31:0] LED_ADDR = 32'h0000_1000,
|
||||
parameter integer FB_WORDS = 300,
|
||||
parameter integer FIFO_DEPTH = 16
|
||||
) (
|
||||
input wire clk,
|
||||
input wire reset,
|
||||
// MEM 级的访存请求(来自 EX/MEM 流水寄存器)
|
||||
input wire mem_valid_i,
|
||||
input wire mem_write_i,
|
||||
input wire mem_read_i,
|
||||
input wire [31:0] addr_i,
|
||||
input wire [31:0] wdata_i,
|
||||
input wire [ 3:0] wstrb_i,
|
||||
// MMIO 读回
|
||||
output wire [31:0] rdata_o,
|
||||
output wire rdata_hit_o,
|
||||
// LED 外设(原样保留)
|
||||
output reg [31:0] led_data_o,
|
||||
output reg led_valid_o,
|
||||
// 七段数码管:8 位十六进制数
|
||||
output reg [31:0] seg_data_o,
|
||||
output reg seg_valid_o,
|
||||
// 显存写端口(板级顶层的双口 RAM)
|
||||
output wire fb_we_o,
|
||||
output wire [ 9:0] fb_waddr_o,
|
||||
output wire [31:0] fb_wdata_o,
|
||||
output wire [ 3:0] fb_wstrb_o,
|
||||
input wire [31:0] fb_rdata_i,
|
||||
// 显示控制器寄存器(给 VGA 用)
|
||||
output wire [31:0] disp_ctrl_o,
|
||||
output wire [31:0] disp_cursor_o,
|
||||
// 鼠标事件源(来自板级顶层:按键去抖 / PS/2 解析)
|
||||
input wire mouse_ev_valid_i,
|
||||
input wire [ 7:0] mouse_dx_i,
|
||||
input wire [ 7:0] mouse_dy_i,
|
||||
input wire [ 4:0] mouse_btn_i,
|
||||
input wire [15:0] sw_i,
|
||||
input wire frame_tick_i,
|
||||
// 中断
|
||||
output wire mouse_irq_o,
|
||||
output wire level_irq_o
|
||||
);
|
||||
// ---------------------------------------------------------------------------
|
||||
// 地址译码(都按"字"对齐:同一 32 位寄存器内的任意字节偏移都算命中)
|
||||
// ---------------------------------------------------------------------------
|
||||
localparam [31:0] SEG_ADDR = 32'h0000_1004;
|
||||
localparam [31:0] MOUSE_BASE = 32'h0000_1100;
|
||||
localparam [31:0] DISP_BASE = 32'h0000_1140;
|
||||
localparam integer FIFO_AW = 4;
|
||||
localparam [FIFO_AW:0] FIFO_FULL_V = FIFO_DEPTH[FIFO_AW:0];
|
||||
|
||||
wire sel_led = (addr_i[31:2] == LED_ADDR[31:2]);
|
||||
wire sel_seg = (addr_i[31:2] == SEG_ADDR[31:2]);
|
||||
wire sel_mouse = (addr_i[31:8] == MOUSE_BASE[31:8]) && (addr_i[7:6] == 2'b00);
|
||||
wire sel_disp = (addr_i[31:8] == DISP_BASE[31:8]) && (addr_i[7:6] == 2'b01);
|
||||
// 显存窗口 0x2000 - 0x27FF(addr[12]=1 且 addr[11]=0)
|
||||
wire sel_fb = (addr_i[31:13] == 19'd0) && addr_i[12] && !addr_i[11];
|
||||
|
||||
wire [3:0] midx = addr_i[5:2];
|
||||
wire [3:0] didx = addr_i[5:2];
|
||||
wire [9:0] fb_widx = addr_i[10:2];
|
||||
wire fb_ok = (fb_widx < FB_WORDS);
|
||||
|
||||
wire led_wr = mem_valid_i && mem_write_i && sel_led;
|
||||
wire led_rd = mem_valid_i && mem_read_i && sel_led;
|
||||
wire seg_wr = mem_valid_i && mem_write_i && sel_seg;
|
||||
wire seg_rd = mem_valid_i && mem_read_i && sel_seg;
|
||||
wire mo_wr = mem_valid_i && mem_write_i && sel_mouse;
|
||||
wire mo_rd = mem_valid_i && mem_read_i && sel_mouse;
|
||||
wire dp_wr = mem_valid_i && mem_write_i && sel_disp;
|
||||
wire dp_rd = mem_valid_i && mem_read_i && sel_disp;
|
||||
wire fb_wr = mem_valid_i && mem_write_i && sel_fb && fb_ok;
|
||||
wire fb_rd = mem_valid_i && mem_read_i && sel_fb && fb_ok;
|
||||
|
||||
assign fb_we_o = fb_wr;
|
||||
assign fb_waddr_o = fb_widx;
|
||||
assign fb_wdata_o = wdata_i;
|
||||
assign fb_wstrb_o = wstrb_i;
|
||||
|
||||
assign rdata_hit_o = led_rd | seg_rd | mo_rd | dp_rd | fb_rd;
|
||||
|
||||
// 字节写合并:sw 改 4 字节、sb 只改 1 字节(与 RAM 的字节写语义一致)
|
||||
function [31:0] merge_bytes;
|
||||
input [31:0] new_v;
|
||||
input [31:0] old_v;
|
||||
input [ 3:0] strb;
|
||||
begin
|
||||
merge_bytes[7:0] = strb[0] ? new_v[7:0] : old_v[7:0];
|
||||
merge_bytes[15:8] = strb[1] ? new_v[15:8] : old_v[15:8];
|
||||
merge_bytes[23:16] = strb[2] ? new_v[23:16] : old_v[23:16];
|
||||
merge_bytes[31:24] = strb[3] ? new_v[31:24] : old_v[31:24];
|
||||
end
|
||||
endfunction
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LED:保持原有语义(led_valid_o 只拉高一拍)
|
||||
// ---------------------------------------------------------------------------
|
||||
always @(posedge clk or posedge reset) begin
|
||||
if (reset) begin
|
||||
led_data_o <= 32'b0;
|
||||
led_valid_o <= 1'b0;
|
||||
end
|
||||
else begin
|
||||
led_valid_o <= 1'b0;
|
||||
if (led_wr) begin
|
||||
led_data_o <= merge_bytes(wdata_i, led_data_o, wstrb_i);
|
||||
led_valid_o <= 1'b1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 七段数码管:8 位十六进制数(低 4 位 = 最右边那个数码管)
|
||||
// ---------------------------------------------------------------------------
|
||||
always @(posedge clk or posedge reset) begin
|
||||
if (reset) begin
|
||||
seg_data_o <= 32'b0;
|
||||
seg_valid_o <= 1'b0;
|
||||
end
|
||||
else begin
|
||||
seg_valid_o <= 1'b0;
|
||||
if (seg_wr) begin
|
||||
seg_data_o <= merge_bytes(wdata_i, seg_data_o, wstrb_i);
|
||||
seg_valid_o <= 1'b1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 显示控制器寄存器(显存本体在板级顶层)
|
||||
// ---------------------------------------------------------------------------
|
||||
reg [31:0] disp_ctrl_q, disp_cursor_q;
|
||||
|
||||
assign disp_ctrl_o = disp_ctrl_q;
|
||||
assign disp_cursor_o = disp_cursor_q;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 鼠标:事件 FIFO + 寄存器 + 中断
|
||||
// ---------------------------------------------------------------------------
|
||||
reg [31:0] fifo [0:FIFO_DEPTH-1];
|
||||
reg [FIFO_AW-1:0] wr_ptr_q, rd_ptr_q;
|
||||
reg [FIFO_AW:0] cnt_q;
|
||||
reg [31:0] mctrl_q, time_q, frame_q, evcnt_q;
|
||||
reg level_q;
|
||||
|
||||
wire fifo_empty = (cnt_q == {FIFO_AW+1{1'b0}});
|
||||
wire fifo_full = (cnt_q == FIFO_FULL_V);
|
||||
wire [31:0] fifo_head = fifo[rd_ptr_q];
|
||||
wire irq_en = mctrl_q[0];
|
||||
|
||||
assign mouse_irq_o = irq_en && !fifo_empty;
|
||||
assign level_irq_o = level_q;
|
||||
|
||||
wire push = mouse_ev_valid_i && !fifo_full;
|
||||
wire pop = mo_wr && (midx == 4'd2) && !fifo_empty;
|
||||
|
||||
always @(posedge clk or posedge reset) begin
|
||||
if (reset) begin
|
||||
mctrl_q <= 32'h0000_0001; // 鼠标中断默认打开
|
||||
time_q <= 32'd0;
|
||||
frame_q <= 32'd0;
|
||||
evcnt_q <= 32'd0;
|
||||
level_q <= 1'b0;
|
||||
wr_ptr_q <= {FIFO_AW{1'b0}};
|
||||
rd_ptr_q <= {FIFO_AW{1'b0}};
|
||||
cnt_q <= {(FIFO_AW+1){1'b0}};
|
||||
disp_ctrl_q <= 32'h0000_0001; // 光标可见,背景色 0
|
||||
disp_cursor_q <= 32'h0000_0000;
|
||||
end
|
||||
else begin
|
||||
time_q <= time_q + 32'd1;
|
||||
if (frame_tick_i)
|
||||
frame_q <= frame_q + 32'd1;
|
||||
|
||||
if (push) begin
|
||||
fifo[wr_ptr_q] <= {11'b0, mouse_btn_i, mouse_dy_i, mouse_dx_i};
|
||||
wr_ptr_q <= wr_ptr_q + 1'b1;
|
||||
evcnt_q <= evcnt_q + 32'd1;
|
||||
end
|
||||
if (pop)
|
||||
rd_ptr_q <= rd_ptr_q + 1'b1;
|
||||
cnt_q <= cnt_q + (push ? 1'b1 : 1'b0) - (pop ? 1'b1 : 1'b0);
|
||||
|
||||
// 鼠标寄存器:CTRL(3) / LEVEL(8)
|
||||
if (mo_wr) begin
|
||||
case (midx)
|
||||
4'd3:
|
||||
mctrl_q <= merge_bytes(wdata_i, mctrl_q, wstrb_i);
|
||||
4'd8:
|
||||
level_q <= wdata_i[0];
|
||||
default:
|
||||
;
|
||||
endcase
|
||||
end
|
||||
|
||||
// 显示控制器:CTRL(0) / CURSOR(1)
|
||||
if (dp_wr) begin
|
||||
case (didx)
|
||||
4'd0:
|
||||
disp_ctrl_q <= merge_bytes(wdata_i, disp_ctrl_q, wstrb_i);
|
||||
4'd1:
|
||||
disp_cursor_q <= merge_bytes(wdata_i, disp_cursor_q, wstrb_i);
|
||||
default:
|
||||
;
|
||||
endcase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 读数据(组合,与"指令存储器组合读/数据存储器组合读"的时序一致)
|
||||
// ---------------------------------------------------------------------------
|
||||
reg [31:0] rdata_mux;
|
||||
|
||||
always @* begin
|
||||
rdata_mux = 32'b0;
|
||||
if (led_rd) begin
|
||||
rdata_mux = led_data_o;
|
||||
end
|
||||
else if (seg_rd) begin
|
||||
rdata_mux = seg_data_o;
|
||||
end
|
||||
else if (fb_rd) begin
|
||||
rdata_mux = fb_rdata_i;
|
||||
end
|
||||
else if (mo_rd) begin
|
||||
case (midx)
|
||||
4'd0:
|
||||
rdata_mux = {24'b0, fifo_full, irq_en, cnt_q[4:0], !fifo_empty};
|
||||
4'd1:
|
||||
rdata_mux = fifo_head;
|
||||
4'd3:
|
||||
rdata_mux = mctrl_q;
|
||||
4'd4:
|
||||
rdata_mux = {11'b0, mouse_btn_i, sw_i};
|
||||
4'd5:
|
||||
rdata_mux = time_q;
|
||||
4'd6:
|
||||
rdata_mux = frame_q;
|
||||
4'd7:
|
||||
rdata_mux = evcnt_q;
|
||||
4'd8:
|
||||
rdata_mux = {31'b0, level_q};
|
||||
4'd9:
|
||||
rdata_mux = 32'hCAFE_0002;
|
||||
default:
|
||||
rdata_mux = 32'b0;
|
||||
endcase
|
||||
end
|
||||
else if (dp_rd) begin
|
||||
case (didx)
|
||||
4'd0:
|
||||
rdata_mux = disp_ctrl_q;
|
||||
4'd1:
|
||||
rdata_mux = disp_cursor_q;
|
||||
4'd2:
|
||||
rdata_mux = 32'hCAFE_0001;
|
||||
default:
|
||||
rdata_mux = 32'b0;
|
||||
endcase
|
||||
end
|
||||
end
|
||||
|
||||
assign rdata_o = rdata_mux;
|
||||
endmodule
|
||||
|
||||
`default_nettype wire
|
||||
Reference in New Issue
Block a user