Files
risc-v-cpu/pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v
T
Alpha f50639e90f 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 宏做行为仿真
2026-09-16 20:11:00 +08:00

170 lines
6.9 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// 多周期乘除法单元(时序逻辑,32 拍迭代)
//
// 原来乘除法直接写在 ALU 里(组合的 a*b / a%b):综合后关键路径约 94 ns
// (325 级逻辑、286 个 CARRY4),100 MHz 下无法收敛,只能把板上 100 MHz 分频
// 到 6.25 MHz 再跑。这里改成移位迭代:
// * 乘法:32 拍移位加法得到 64 位乘积;有符号变体先取操作数绝对值,
// 迭代结束后按符号对 64 位乘积取负。
// * 除法:32 拍恢复余数法得到商与余数;除零按 RISC-V 约定处理
// (商为全 1、余数为被除数)。DIVU/REMU 都是无符号运算,不需要符号修正。
//
// 顶层的 EX 级会停留若干拍(往 EX/MEM 插气泡)等待 done_o 有效,然后照常写回。
// 迭代期间顶层会屏蔽中断,避免在结果未定型时提前推进流水线。
//
// op_i 编码(由 cpu21_riscv_decoder 给出,MD_NONE 表示不是乘除法指令):
// MD_MUL mul :无符号乘积低 32 位(有符号乘法的低 32 位与之相同)
// MD_MULH mulh :有符号 x 有符号,取高 32 位
// MD_MULHSU mulhsu :有符号 x 无符号,取高 32 位
// MD_MULHU mulhu :无符号 x 无符号,取高 32 位
// MD_DIVU divu :无符号商
// MD_REMU remu :无符号余数
//
// result_o / result2_o 的含义与原来的 ALU 保持一致:乘法时 result_o 为低 32 位、
// result2_o 为高 32 位;除法时 result_o 为商、result2_o 为余数。译码器用 wb_sel
// 决定写回哪一个(mulhu、remu 用 wb_sel=3 取 result2),因此顶层只需把这一对
// 结果接到原来的 EX 结果总线上,其余流水逻辑不用改。
// -----------------------------------------------------------------------------
module cpu21_riscv_muldiv (
input wire clk,
input wire reset, // 异步复位,高有效(与顶层一致)
input wire start_i, // 指令在 EX 级且是乘除法(电平,模块内部只在空闲时响应)
input wire [2:0] op_i, // 乘除法操作类型
input wire [31:0] a_i, // EX 级 src1
input wire [31:0] b_i, // EX 级 src2(ALU 的 B 端)
output wire busy_o, // 迭代中(不含结果定型那一拍)
output wire done_o, // 结果有效(单拍,此时顶层推进流水线)
output wire [31:0] result_o, // 主结果
output wire [31:0] result2_o // 第二结果(乘法高 32 位 / 除法余数)
);
localparam [2:0] MD_NONE = 3'd0;
localparam [2:0] MD_MUL = 3'd1;
localparam [2:0] MD_MULH = 3'd2;
localparam [2:0] MD_MULHSU = 3'd3;
localparam [2:0] MD_MULHU = 3'd4;
localparam [2:0] MD_DIVU = 3'd5;
localparam [2:0] MD_REMU = 3'd6;
localparam [1:0] S_IDLE = 2'd0;
localparam [1:0] S_RUN = 2'd1;
localparam [1:0] S_DONE = 2'd2;
reg [ 1:0] state_q;
reg [ 5:0] cnt_q; // 0..32:0..31 为迭代,32 为结果定型
reg [ 2:0] op_q;
reg is_div_q;
// 乘法数据通路:64 位被乘数(每拍左移)、32 位乘数(每拍右移)、64 位累加器。
reg [63:0] mcand_q;
reg [31:0] mplier_q;
reg [63:0] prod_q;
reg sign_q;
// 除法数据通路:32 位除数、32 位商/被除数移位寄存器、33 位余数。
reg [31:0] dvsr_q;
reg [31:0] quo_q;
reg [32:0] rem_q;
reg [31:0] dvd_q; // 原始被除数(除零时原样返回)
reg div_zero_q;
// 结果寄存器。
reg [31:0] res_q;
reg [31:0] res2_q;
assign busy_o = (state_q == S_RUN);
assign done_o = (state_q == S_DONE);
assign result_o = res_q;
assign result2_o = res2_q;
// 乘法操作数准备:按操作类型决定是否取绝对值,并给出结果符号。
wire a_signed = (op_i == MD_MULH) || (op_i == MD_MULHSU);
wire b_signed = (op_i == MD_MULH);
wire [31:0] a_mag = (a_signed && a_i[31]) ? (~a_i + 32'd1) : a_i;
wire [31:0] b_mag = (b_signed && b_i[31]) ? (~b_i + 32'd1) : b_i;
wire sign_in = ((op_i == MD_MULH) && (a_i[31] ^ b_i[31])) || ((op_i == MD_MULHSU) && a_i[31]);
// 除法的一步(恢复余数法):把被除数最高位移进余数,够减则减并把商位置 1。
wire [32:0] rem_shift = {rem_q[31:0], quo_q[31]};
wire rem_ge = (rem_shift >= {1'b0, dvsr_q});
wire [32:0] rem_next = rem_ge ? (rem_shift - {1'b0, dvsr_q}) : rem_shift;
wire [31:0] quo_next = {quo_q[30:0], rem_ge};
// 迭代结束时的符号修正:prod_mag 为无符号乘积,sign_q 为 1 时取负。
wire [63:0] prod_fix = sign_q ? (~prod_q + 64'd1) : prod_q;
always @(posedge clk or posedge reset) begin
if (reset) begin
state_q <= S_IDLE;
cnt_q <= 6'd0;
op_q <= MD_NONE;
is_div_q <= 1'b0;
mcand_q <= 64'd0;
mplier_q <= 32'd0;
prod_q <= 64'd0;
sign_q <= 1'b0;
dvsr_q <= 32'd0;
quo_q <= 32'd0;
rem_q <= 33'd0;
dvd_q <= 32'd0;
div_zero_q <= 1'b0;
res_q <= 32'd0;
res2_q <= 32'd0;
end else begin
case (state_q)
// 空闲:指令进入 EX 且是乘除法时装载操作数,下一拍开始迭代。
S_IDLE: begin
if (start_i) begin
state_q <= S_RUN;
cnt_q <= 6'd0;
op_q <= op_i;
is_div_q <= (op_i == MD_DIVU) || (op_i == MD_REMU);
// 乘法初始化(先取绝对值,符号留到最后修正)。
mcand_q <= {32'b0, a_mag};
mplier_q <= b_mag;
prod_q <= 64'd0;
sign_q <= sign_in;
// 除法初始化(无符号)。
dvsr_q <= b_i;
quo_q <= a_i;
rem_q <= 33'd0;
dvd_q <= a_i;
div_zero_q <= (b_i == 32'd0);
end
end
// 迭代:cnt 0..31 各走一步,32 拍后把结果定型。
S_RUN: begin
if (cnt_q == 6'd32) begin
state_q <= S_DONE;
if (is_div_q) begin
res_q <= div_zero_q ? 32'hffff_ffff : quo_q;
res2_q <= div_zero_q ? dvd_q : rem_q[31:0];
end else begin
res_q <= (op_q == MD_MULHU || op_q == MD_MUL) ? prod_fix[31:0] : prod_fix[63:32];
res2_q <= prod_fix[63:32];
end
end else begin
cnt_q <= cnt_q + 6'd1;
if (is_div_q) begin
rem_q <= rem_next;
quo_q <= quo_next;
end else begin
prod_q <= mplier_q[0] ? (prod_q + mcand_q) : prod_q;
mcand_q <= {mcand_q[62:0], 1'b0};
mplier_q <= {1'b0, mplier_q[31:1]};
end
end
end
// 结果有效一拍,顶层据此推进流水线;随后回到空闲。
default: state_q <= S_IDLE;
endcase
end
end
endmodule
`default_nettype wire