Alpha 4e3525ffe2 fix(saolei): 修复被文本工具改坏的 RTL —— 这是综合失败的直接原因
## 现象(Vivado 2020.2 综合日志)

`synth_design -top cpu21_board_top -part xc7a100tcsg324-1` 报 13 个
`ERROR: [Synth 8-2715] syntax error`,指向:
- `cpu21_pointer.v:106`、`cpu21_board_top.v:308`、`cpu21_vga.v:242`
  —— `non-printable character with the hex value '0xe4'`(UTF-8 中文字的首字节)
- `cpu21_board_top.v:139` —— `syntax error near "="`
- `cpu21_ps2_mouse.v:45 / 47 / 145` —— `0xe3`、`syntax error near "ta_s1"`
- `cpu21_board_rom.vh:3...` —— 一连串 `syntax error near "="`(前一处错误的级联)

## 根因

**不是 RTL 设计问题,是工作区文件被改坏**(编辑器/格式化类的文本改动把 README 的
片段插进了 Verilog 文件里):

1. `cpu21_pointer.v`、`cpu21_vga.v`、`cpu21_ps2_mouse.v` 文件末尾被追加了一行
   来自 README 的 `Tcl 一键重建`(于是合成器在 `endmodule` 之后遇到中文字节);
2. `cpu21_board_top.v` 里被插入了同样的残行,**顶掉了 VGA 例化的 `.vga_hs` 那一行**;
3. `cpu21_board_top.v` 的 ROM 段被吞掉一整块:
   `reg [31:0] rom [...]` / `integer i;` / `initial begin` 全部消失,
   只剩下 `for (i = 0; ...) rom[i] = ...;` —— 这也正是 `cpu21_board_rom.vh`
   里 `syntax error near "="` 的由来(`initial` 块没了,`rom[i]=...` 成了非法语句);
4. `cpu21_ps2_mouse.v` 的 `reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;`
   声明被拼进了 `assign ps2_data_oe_o = 1'b0` 里(变成
   `assign ps2_data_oe_o = 1'b02_data_s1, ps2_data_s2;`),注释残片
   `。本模块只接收,两条线都不驱动。` 夹在中间。

## 修复

- 删除 3 处 README 残行,恢复 `cpu21_board_top.v` 的 `.vga_hs` 端口连接;
- 补齐 `cpu21_board_top.v` ROM 段的 `reg/integer/initial begin`;
- 恢复 `cpu21_ps2_mouse.v` 被拼掉的 `reg` 声明,并整理开漏赋值的注释;
- 全量扫描 `srcs/**` 确认:无残留 README 文本碎片、`module/endmodule` 与
  `` `ifdef/`endif `` 配对正常、四个受损文件的关键结构与例化(ROM/RAM/CPU/VGA/
  数码管/按键、16 个字形表、PS2 状态机、按键去抖)均完整。

## 状态

- 本次只改 RTL 文本,未改任何逻辑/端口/地址。
- 本机没有 Vivado / xsim / iverilog,**无法在本地复验语法**,需要重新跑一次综合确认。
2026-09-18 15:58:13 +08:00

cpu21-riscv-4.circ 的 Verilog 实现

处理器顶层文件:pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v

该设计对应原电路中的“重定向流水线+中断+分支预测”部分,顶层模块为 cpu21_riscv_redirect_int_bpb。为便于单独仿真与调试,各功能单元已拆分为 独立文件,顶层的 ALU、BPB 也已移出,具体见文末“文件与模块划分”。

目录结构

risc-v/
├─ README.md                             本文档
├─ pipelined-cpu/                        Vivado 工程目录(也是源文件所在地)
│  ├─ create_project.tcl                 一键重建工程的 Tcl 脚本
│  ├─ srcs/rtl/*.v                       处理器与外设 RTL(17 个文件,cpu21_riscv_redirect_int_bpb.v 为处理器顶层)
│  │                                     外设:cpu21_mmio.v(MMIO 中枢)、cpu21_vga.v(显示)、
│  │                                     cpu21_seg7.v(数码管)、cpu21_pointer.v(按键当鼠标)、
│  │                                     cpu21_ps2_mouse.v(可选 PS/2 鼠标)
│  ├─ srcs/board/*.v|*.vh                板级顶层(时钟分频 + 片内 ROM/RAM + 引脚)、片内 ROM 初始化
│  ├─ srcs/constrs/*.xdc                 引脚与分频时钟约束(+ Nexys4DDR 母版)
│  ├─ srcs/tb/*.v                        三个测试平台
│  ├─ srcs/rom/*.hex                     测试平台加载的 ROM 镜像
│  ├─ tools/                             rv_asm.py(迷你汇编器)、hex2vh.py(hex → ROM 初始化)、run_regress.ps1(一键回归)
│  └─ programs/                          汇编源程序、期望值、课程原始资产(RARS)
└─ saolei/                               纯 C 扫雷(MMIO 显示 + 中断读鼠标),见下文
   ├─ sw/                                C 源码 + 链接脚本(三个固定中断入口)
   └─ tools/                             build.ps1(C → ROM)、check_isa.py(指令白名单校验)

仓库里不放任何 Vivado 生成物(.xpr、.runs、.sim、.cache、.hw、 .ip_user_files、.Xil、vivado.jou/log 本地已清理干净):需要工程时用 source create_project.tcl 一键重建,这样仓库始终是最清爽的源文件状态。

约定:git 只跟踪 pipelined-cpu/ 下的源文件(srcs/、tools/、programs/、 create_project.tcl),Vivado 生成物(*.xpr、*.runs、*.sim、*.cache…)由 .gitignore 排除。srcs/rtl/ 只放可综合的处理器源码;板级顶层与约束在 srcs/board/、 srcs/constrs/;测试平台与 ROM 镜像在 srcs/tb/、srcs/rom/(镜像是按 MMIO 写 LED 修改过的版本,课程原始程序与 RARS 输出保留在 programs/)。

已转换的功能

  • 五级流水:IF、ID、EX、MEM、WB。
  • EX/MEM 与 MEM/WB 前递,以及 load-use 一拍停顿。
  • 条件分支 beq、bne、blt/bge(有符号)、bltu/bgeu(无符号)在 EX 段判定,错误预测时清空 IF/ID、ID/EX 并重定向 PC。真值表中 BLT 的 ALU_OP 为 SLT,与实现一致。
  • jal、jalr 在 EX 段重定向,返回地址写回 rd。
  • 8 项全相联 BPB:标签 PC[11:2],每项包含 valid、目标 PC、2 位饱和计数器和 3 位年龄字段。BPB 只由条件分支更新,避免 jal、jalr 占用不会被查询的表项;未命中时优先使用无效项,否则替换年龄最大的项。
  • 三路中断输入 irq_i[2:0]:两级同步、上升沿挂起、IRQ3 > IRQ2 > IRQ1 优先级,仅在 ustatus[0](MIE)允许时响应。
  • 支持嵌套中断:保存 EPC、ustatus 和当前优先级;URET 使用当前 uepc 返回,并从硬件栈恢复外层上下文。三个入口按照原电路的 INTpc 选择:IRQ1=0x30ac、IRQ2=0x31e4、IRQ3=0x3310。0x22 是 ecall 的服务号 34,不是中断向量。
  • CSRRSI、CSRRCI,以及 CSRRW:支持 CSR 地址 0x004(ustatus)和 0x041(uepc)。立即数为 IR[19:15] 的零扩展值,并按 RISC-V 规则把旧 CSR 值写回 rd;这覆盖任务书中的 csrrsi s6,0x41,0 和 csrrw zero,0x41,s6 用法。
  • ecall:停机和调试用。a7 == 34 是历史遗留的“显示”调用,现已保留为无操作;其余取值(例如程序结尾惯用的 a7 = 10)置 halted_o,直到 go_i 为高。LED 显示改由 MMIO 存储完成(见下文“MMIO 与外设”)。
  • sw 和 sb:输出 data_wstrb_o,支持按字节写入。
  • lui:加载高位立即数,rd = imm[31:12] << 12(按需求补充的扩展指令,标准 opcode 0x37)。
  • sltiu:I 型 funct3=011,立即数符号扩展后按无符号与 rs1 比较。
  • M 扩展乘法(funct7=0000001):mul(乘积低 32 位)、mulh(有符号×有符号高 32 位)、mulhsu(有符号×无符号高 32 位)、mulhu(无符号×无符号高 32 位)。
  • M 扩展除法:课程真值表里的 divu(funct3=101,无符号商)与 remu(funct3=111,无符号余数)。 以上六条乘法/除法指令都由 多周期单元 pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v 完成(见“时序和使用说明”第 7 条), 而不是组合逻辑。

指令编码

表中的 opcode 是控制器使用的五位字段 IR[6:2],不是包含最低两位的 7 位原始 opcode。 因此它与标准 RISC-V 指令的低 7 位 opcode 相差右移两位。

指令类别 opcode 说明
R 型 0x0c add/sub/and/or/xor/slt/sltu/sll/srl/sra
M 扩展(R 型) 0x0c funct7=0000001:mul/mulh/mulhsu/mulhu、divu(funct3=101)、remu(funct3=111)
I 型 ALU 0x04 addi/andi/ori/xori/slti/sltiu/slli/srli/srai
lui(扩展) 0x0d 加载高位立即数,rd = imm[31:12] << 12
lw 0x00 funct3=010
sw / sb 0x08 funct3=010 / 000
beq / bne / blt / bge / bltu / bgeu 0x18 funct3=000 / 001 / 100 / 101 / 110 / 111
jal 0x1b J 型立即数
jalr 0x19 funct3=000
系统类 0x1c ecall、URET、CSRRSI、CSRRCI

ecall 和 URET 都是 opcode=0x1c、funct3=0,由 IR[21] 区分: IR[21]=0 是 ecall,IR[21]=1 是 URET。这一点来自原控制器中的 IR21 分支逻辑。

MMIO 与外设

外设寄存器映射在数据存储器地址空间,由 pipelined-cpu/srcs/rtl/cpu21_mmio.v 在 MEM 级译码。 这些地址都在 4 KB 数据 RAM(0x0000–0x0FFC)之外,因此不会与 RAM 冲突。 每行的字节偏移都按字对齐译码(同一 32 位寄存器内任意字节偏移都命中)。

地址 外设 写 读
0x0000_1000 LED 数据寄存器 sw 写全字;sb 按 data_wstrb_o 只改对应字节通道,led_valid_o 拉高一拍 lw 返回 LED 寄存器当前值
0x0000_1004 七段数码管 sw/sb 写 8 位十六进制数(低 4 位 = 最右边那个数码管) lw 读回
0x0000_1100+ 鼠标寄存器组 STATUS / DATA / ACK / CTRL / INPUT / TIME / FRAME / EVCNT / LEVEL / MAGIC 同左(DATA 读无副作用)
0x0000_1140+ 显示控制器 CTRL(光标可见、背景色)/ CURSOR(列、行) 同左(另含 MAGIC 自检值)
0x0000_2000–0x27FF 显存 40 列 × 30 行、每格 1 字节(字形 + 颜色),写端口接到板级顶层的双口 RAM lw 组合读回

其中 LEVEL(0x1120)是一个软件中断寄存器:写 1 会拉起 irq_i[1],用来让程序 "跳到关卡入口";MAGIC(0xCAFE0001/0xCAFE0002)可以读来自检 MMIO 是否活着。 鼠标事件 FIFO(深度 16)配合 mouse_irq_o 提供"事件到达即中断",isr_body 读空 FIFO 后 中断线自动回 0,因此不会丢事件。

地址译码按字对齐:同一 32 位寄存器内的任意字节偏移(如 0x1001)都算命中, 具体修改哪几个字节由 data_wstrb_o 决定。data_addr_o 在命中 MMIO 时不会 访问外部 RAM;读数据由 cpu21_mmio 的输出在顶层与 data_rdata_i 做选择。

典型用法(LED 写 0x12345678,可用 pipelined-cpu/tools/rv_asm.py 汇编):

    lui   t0, 0x1            # t0 = LED_ADDR = 0x00001000
    lui   t1, 0x12345
    addi  t1, t1, 0x678      # t1 = 0x12345678
    sw    t1, 0(t0)          # MMIO 写 LED -> LED 输出 12345678
    lw    a0, 0(t0)          # MMIO 读回

pipelined-cpu/programs/mmio_test.asm(期望值见 mmio_test.expected)覆盖了 全字写、读回、字节写三种情况。加入新外设(拨码开关、UART 等)时只需在 pipelined-cpu/srcs/rtl/cpu21_mmio.v 里扩展地址译码,处理器流水线无需改动。

顶层端口约定

  • instr_i:当前 instr_addr_o 对应的 32 位指令。原 Logisim 中由 ROM 提供。
  • data_rdata_i:当前 data_addr_o 的读数据。原 Logisim 中由 MIPS RAM 提供。
  • data_addr_o、data_wdata_o、data_wstrb_o、data_we_o:数据存储器接口。地址按字节地址输出;若使用原 32 位 RAM,可用 data_addr_o[11:2] 作为字地址。
  • irq_i[0]、irq_i[1]、irq_i[2]:1、2、3 号中断源;输入应为脉冲或边沿信号,模块内部会同步并锁存挂起状态。
  • go_i:对应原电路的“继续运行”按键。
  • if_pc_o、id_pc_o、ex_pc_o、mem_pc_o、wb_pc_o:五级流水当前保存的 PC,气泡时输出 0,便于替代原电路的调试探针。
  • rdin_o、mdin_o、reg_write_o、mem_write_o:对应原电路的寄存器写回和存储器写调试输出。
  • cycle_count_o、stall_count_o、bubble_count_o、conditional_taken_count_o、unconditional_branch_count_o、prediction_success_count_o、prediction_failure_count_o:对应原电路和课程要求的周期、停顿/气泡、分支和动态预测统计量。

时序和使用说明

  1. reset 为高时异步清空流水线、寄存器文件、BPB、挂起中断和中断栈,PC 置为 RESET_PC;ustatus 复位为参数 USTATUS_INIT,默认 0x0000_0001(MIE 上电即使能)。这样做的原因是课程的中断测试程序只在中断处理程序的“保护现场”之后写 MIE,主流程不会开中断;若不默认使能,第一次中断只会挂在 irq_pending_o 上而永不响应。参数可覆盖,例如把 USTATUS_INIT 设为 32'h0 即可恢复“上电关中断”的行为。
  2. 指令存储器按组合读处理;数据存储器读数据在一个 MEM 周期内返回。若 RAM 为同步读,需要在外部增加一级数据寄存或相应调整 MEM/WB。
  3. 响应中断时允许当前 EX 指令完成,并保存下一条尚未执行指令的 PC;若 EX 是已正确预测的跳转分支,则保存实际目标地址。EX 为空时优先保存 IF/ID 中尚未执行的 PC。这样 URET 不会重复执行已经提交的存储或寄存器写操作。
  4. 默认 BPB 新表项采用弱不跳转/弱跳转状态:首次更新为不跳转时计数器为 01,首次更新为跳转时为 10;命中后按 2 位饱和规则增减。
  5. bpb_predict_hit_o、bpb_predict_taken_o、bpb_mispredict_o 和 irq_*_o 是观测端口,不参与外部控制,可以直接接逻辑分析仪或测试平台。
  6. cycle_count_o 在 halted_o 为高时暂停;load-use 每次计入一个气泡,重定向或中断清空 IF/ID 与当前 IF 时计入两个气泡。
  7. 乘除法是多周期指令:结果算完之前 ex_md_stall 有效,ID/EX 保持(指令继续占用 EX)、EX/MEM 插气泡、取指/译码冻结,同时屏蔽中断(避免结果未定型就写回)。一次乘/除共占用 34 拍(1 拍装载 + 32 拍迭代 + 1 拍定型),这些等待周期也计入 stall_count_o / bubble_count_o。乘除法迭代期间不会把错误值写进 rd,也不会因为中断被打断。

原 .circ 文件中的 ROM、MIPS RAM、按钮、LED 和调试显示器属于 Logisim 外围,不直接搬入 RTL;其功能分别由指令接口、数据接口、go_i、MMIO(led_data_o/led_valid_o)和调试端口替代。

文件与模块划分

文件 模块 职责
pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v cpu21_riscv_redirect_int_bpb 顶层:五级流水寄存器、子模块例化、PC/IF-ID/ID-EX 控制、对外输出
pipelined-cpu/srcs/rtl/cpu21_riscv_alu.v cpu21_riscv_alu 组合 ALU(加减/移位/逻辑/比较,不做乘除)
pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v cpu21_riscv_muldiv 多周期乘除法单元:32 拍移位迭代,mul/mulh/mulhsu/mulhu/divu/remu
pipelined-cpu/srcs/rtl/cpu21_bpb_8.v cpu21_bpb_8 8 项全相联分支目标缓冲(2 位饱和计数器 + 近似 LRU)
pipelined-cpu/srcs/rtl/cpu21_riscv_decoder.v cpu21_riscv_decoder ID 级组合译码器(操作码IR[6:2])
pipelined-cpu/srcs/rtl/cpu21_riscv_regfile.v cpu21_riscv_regfile 寄存器堆,x0 恒为 0,同周期 WB 写优先旁路
pipelined-cpu/srcs/rtl/cpu21_riscv_forward_unit.v cpu21_riscv_forward_unit EX 级前递网络(EX/MEM 优先于 MEM/WB)
pipelined-cpu/srcs/rtl/cpu21_riscv_store_unit.v cpu21_riscv_store_unit EX 级存储数据对齐:sw/sb 的写数据与字节写掩码
pipelined-cpu/srcs/rtl/cpu21_riscv_branch_unit.v cpu21_riscv_branch_unit EX 级控制流裁决:分支方向/目标、JAL/JALR 重定向
pipelined-cpu/srcs/rtl/cpu21_riscv_hazard_unit.v cpu21_riscv_hazard_unit load-use 冒险、停顿(stall)与冲刷(flush)判定
pipelined-cpu/srcs/rtl/cpu21_riscv_irq_ctrl.v cpu21_riscv_irq_ctrl 中断采样与优先级、ustatus/uepc CSR、嵌套返回栈
pipelined-cpu/srcs/rtl/cpu21_riscv_perf_counters.v cpu21_riscv_perf_counters 周期/停顿/气泡/分支/预测成功率等统计计数器
pipelined-cpu/srcs/rtl/cpu21_mmio.v cpu21_mmio MMIO 中枢:LED、七段数码管、鼠标寄存器组 + 事件 FIFO + 中断、显示控制器、显存窗口
pipelined-cpu/srcs/rtl/cpu21_vga.v cpu21_vga 640x480@60 显示控制器:按"格子"扫描,8x8 字模放大 2 倍填满 16x16 的格子
pipelined-cpu/srcs/rtl/cpu21_seg7.v cpu21_seg7 板载 8 位数码管动态扫描驱动(共阳极,位选/段选低有效)
pipelined-cpu/srcs/rtl/cpu21_pointer.v cpu21_pointer 把 5 向按键变成"鼠标"事件(去抖 + 事件排队,单位 1/8 格)
pipelined-cpu/srcs/rtl/cpu21_ps2_mouse.v cpu21_ps2_mouse 可选的真鼠标:PS/2 接收(11 位帧 + 奇校验 + 3 字节包解析),只实现接收
pipelined-cpu/srcs/board/cpu21_board_top.v cpu21_board_top 板级顶层:时钟分频、复位同步、片内 ROM/RAM、显存双口 RAM、VGA/数码管/鼠标/中断连线
pipelined-cpu/srcs/board/cpu21_saolei_rom.vh — 扫雷程序的 ROM 初始化(由 saolei/tools/build.ps1 生成)
pipelined-cpu/srcs/constrs/cpu21_board_top.xdc — Nexys4DDR 引脚约束 + 分频时钟(create_generated_clock)
pipelined-cpu/srcs/board/cpu21_board_rom.vh — 片内 ROM 内容(生成文件,rom[i] = 32'h…;)
pipelined-cpu/srcs/tb/tb_board_top.v tb_board_top 板级顶层的 RTL 仿真(观察 LED 跑马灯与写入指示)
pipelined-cpu/srcs/tb/tb_no_intr.v tb_no_intr 无中断测试平台(指令集/MMIO 测试,默认加载 benchmark 镜像)
pipelined-cpu/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v tb_cpu21_riscv_redirect_int_bpb 中断与分支预测测试平台(默认加载中断测试镜像)
pipelined-cpu/tools/rv_asm.py — 迷你汇编器:.asm → 机器码 hex(.lst 带反汇编清单)
pipelined-cpu/tools/hex2vh.py — hex →pipelined-cpu/srcs/board/cpu21_board_rom.vh(rom[i] = 32'h…; 初始化语句)

拆分只调整了模块边界与文件组织,端口、时序与行为完全等价:使用同一 ROM 程序 在 xsim 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支 跳转、预测成败)完全一致。

上板(Nexys4DDR + Vivado)

处理器顶层 cpu21_riscv_redirect_int_bpb 有 775 位调试端口(各级 PC/IR、计数器、 BPB/中断观测值)。如果直接把它设为实现顶层,这些端口会全部变成引脚,导致 [Place 30-415] IO Placement failed due to overutilization(占用 775 个 IO, 而 xc7a100tcsg324-1 只有 210 个)。因此上板时用 cpu21_board_top 作为顶层, 只引出板上真正需要的那些引脚(LED、按键、拨码、VGA、8 位数码管、可选的 PS/2), 其余调试端口不连接。定义宏 SAOLEI_ROM 时还会把扫雷程序固化进 ROM。

引脚分配(cpu21_board_top.xdc)

端口 引脚 板上元件 说明
clk100 E3 100 MHz 晶振 经CLK_DIV 分频后给 CPU
rst_n C12 按键 CPU_RESETN(低有效) 异步置位、与 CPU 时钟同步释放
go N17 按键 BTNC 对应原电路“继续运行”(ecall 停机后恢复执行)
irq_sw[2:0] J15/L16/M13 拨码开关 SW0–SW2 三路中断源,拨动即触发(内部同步并锁存)
led[15:0] H17…V11 LED0–LED15 MMIO(LED_ADDR)写入值的低 16 位
led_we R12 LED16_B MMIO 写入指示,每写一次拉长约 2.7 s

时钟:为什么还保留 2 分频

乘除法原本是 ALU 里的组合运算(a*b、a%b),实测关键路径约 94 ns(325 级逻辑、 286 个 CARRY4),在 100 MHz 下必然不收敛,所以之前只能把时钟 16 分频到 6.25 MHz。 现在乘除法改成了 pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v 里的 32 拍迭代多周期单元,关键路径上 不再有组合乘/除,也不需要再分频了;但把所有加法器/比较器都换成迭代后,在 100 MHz 下仍有约 1.1 ns 的违例——那是 EX 级“分支判决 / 中断保存 → BPB 更新”这条与乘除法 无关的组合路径(逻辑只占约 3 ns,剩下 7~8 ns 全是布线)。

因此在 cpu21_board_top 里保留一个参数化的分频器 CLK_DIV,默认取 2(50 MHz, 周期 20 ns),既比原来的 16 分频快 8 倍,又留出充足余量:

配置 CPU 时钟 实测 WNS(xc7a100tcsg324-1) 说明
CLK_DIV=1 100 MHz -1.1 ns ✗ 违例路径与乘除法无关(分支/中断/BPB)
CLK_DIV=2 50 MHz +5.1 ns ✓ 默认,余量约 25%
CLK_DIV=16 6.25 MHz +6.1 ns ✓ 改造前的设置,现在没必要了

分频后的时钟在综合/实现时由 Vivado 自动插入 BUFG 走全局时钟网络(RTL 里不例化 BUFG 原语,这样在不认识 Xilinx 原语的仿真器里也能直接仿真)。XDC 中必须用 create_generated_clock 声明这个分频时钟,否则时序分析会按 10 ns 去要求 CPU 内部路径而误报大量违例:

create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
  -divide_by 2 [get_pins { clk_div_reg/Q }]

改分频比时 RTL 的 CLK_DIV 与 XDC 的 -divide_by 要同时改。

片内 ROM / RAM

  • ROM:ROM_WORDS = 1024 字(4 KB),组合读,取指地址 instr_addr[11:2]; 内容由 `include "cpu21_board_rom.vh" 给出(与 cpu21_board_top.v 同目录, 文件里是 1024 条 rom[i] = 32'h…;,放在 initial 里,等价于 $readmemh,且不依赖文件路径)。
  • RAM:RAM_WORDS = 1024 字(4 KB),字节写使能 + 组合读,只响应 data_addr < 4 KB,因此 MMIO 地址(默认 0x1000)不会落进 RAM。
  • 两个 initial 块都用 NOP(32'h0000_0013)或 0 预填充,ROM 未使用区域是 NOP, 这样即使程序跑飞也不会执行到随机指令而乱写寄存器。

演示程序的生成与仿真

# 在工程目录 pipelined-cpu/ 下执行:
# 1) 汇编 + 生成 ROM 初始化文件
python tools/rv_asm.py programs/board_demo.asm _tmp/board_demo.hex
python tools/hex2vh.py _tmp/board_demo.hex srcs/board/cpu21_board_rom.vh 1024

# 2) 上板顶层的 RTL 仿真(默认程序延时约 20 ms,只需确认第一次写入 LED=0001)
xvlog srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim
xsim board_sim -tclbatch run_quit.tcl

board_demo.asm 是一段 16 步跑马灯(0001 → 0002 → … → 8000 → 0001), 每步之间用软件循环延时约 20 ms(50 MHz 下)。仓库里同时提供了两份 ROM 初始化 文件,由 BOARD_ROM_FAST 宏选择(综合流程不会定义该宏):

文件 用途 每步耗时
pipelined-cpu/srcs/board/cpu21_board_rom.vh 综合/上板(默认) 约 20 ms
pipelined-cpu/srcs/board/cpu21_board_rom_fast.vh 仅仿真:一次跑完整个跑马灯序列 2.6 µs
# 快速版仿真:编译时加上 -d BOARD_ROM_FAST(xvlog 与 xelab 都要带)
xvlog -d BOARD_ROM_FAST srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim_fast -d BOARD_ROM_FAST

两份 ROM 初始化文件由 programs/board_demo.asm(默认,≈ 20 ms/步)与 programs/board_demo_fast.asm(快速版,≈ 2.6 µs/步,只差延时循环的初值)生成:

python tools/rv_asm.py programs/board_demo_fast.asm _tmp/board_demo_fast.hex
python tools/hex2vh.py _tmp/board_demo_fast.hex srcs/board/cpu21_board_rom_fast.vh 1024

Vivado 工程设置与实现结果

Vivado 工程 pipelined-cpu/pipelined-cpu.xpr 被 .gitignore 排除,需要手动设置:

  1. sources_1 顶层设为 cpu21_board_top(纯 RTL 仿真时也可切回 cpu21_riscv_redirect_int_bpb);
  2. 加入 pipelined-cpu/srcs/board/cpu21_board_top.v、pipelined-cpu/srcs/constrs/cpu21_board_top.xdc, pipelined-cpu/srcs/board/cpu21_board_rom.vh 的文件类型设成 Verilog Header(不能当普通源码, 否则会被当成顶层候选);
  3. 三个 testbench 放在仿真 fileset(sim_1 顶层 tb_cpu21_riscv_redirect_int_bpb、 sim_2 顶层 tb_no_intr),不参与综合,因此不会再出现“testbench 被当成顶层” 导致的 [Place 30-494] The design is empty。

xc7a100tcsg324-1 上的实现结果(乘除法改为多周期单元、CLK_DIV=2 = 50 MHz):

项目 结果
综合 synth_design Complete!
实现 route_design Complete!、write_bitstream Complete!,WNS = +5.109 ns(无 Failed Timing)
资源 LUT 3420 (5.39%)、FF 2507、DSP 0(组合乘法器已消失)、IOB 23 (10.95%)
bit 文件 pipelined-cpu/pipelined-cpu.runs/impl_1/cpu21_board_top.bit

对比改造前(组合乘除法 + CLK_DIV=16):LUT 5400 → 3420、DSP 12 → 0、 CPU 时钟 6.25 MHz → 50 MHz(快 8 倍),并且乘除法不再位于关键路径上。

上板操作:用 Vivado 打开工程 → Open Hardware Manager → Program Device,选上述 .bit。复位后跑马灯立即从 0001 开始逐位左移(演示程序不执行 ecall,所以不用 按 go;go 只对执行了 ecall 停机、等待继续运行的程序有意义),LED16_B 每次写入时点亮。

扫雷程序(saolei/)

saolei/ 是一个纯 C 写的扫雷,用 RISC-V 工具链编译成机器码后固化进片内 ROM 运行, 反过来验证这套 MMIO/中断/显示/鼠标外设。完整说明见 saolei/README.md,要点:

  • 三段中断入口(地址由 saolei/sw/linker.ld 定死,与顶层 IRQ1/2/3_VECTOR 成对): IRQ1=0x100 关卡(游戏)程序、IRQ2=0x200 鼠标读取、IRQ3=0x300 备用;
  • 两层结构:背景 main() 画开始界面并一直运行 → 点"开始"写 LEVEL 寄存器拉起软件中断 → CPU 跳进 0x100 的关卡程序跑完整局 → 点左上角黄格 uret 回到背景被打断的那条指令;
  • 纯 C,只有三处必要的内联汇编:设 sp、三个中断入口的现场保护、uret;
  • 指令子集适配:没有 auipc/lb/lbu/有符号 div,所以程序只按字节写显存、 索引全用移位、且不含 .data/.rodata(链接脚本用 ASSERT 兜住);
  • 构建:saolei/tools/build.ps1(C → ELF → .hex + cpu21_saolei_rom.vh), 并用 check_isa.py 逐条反汇编检查有没有用到未实现的指令。实测 842 条指令全部合法;
  • 上板切换:Vivado 里定义宏 SAOLEI_ROM 就加载扫雷程序,不定义仍是下面的跑马灯。

回归测试

工程里带了一个 xsim 回归脚本,在 pipelined-cpu/ 目录下运行:

powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1

它会按顺序做四件事:

  1. 编译 srcs/rtl/ 下的处理器源码;
  2. 跑 4 个指令测试(muldiv_test、isa_test、lui_test、mmio_test),把测试平台 打印的 LED 序列与 programs/*.expected 逐值比对;
  3. 跑两个流水基线(tb_no_intr、tb_cpu21_riscv_redirect_int_bpb),打印 FINAL 统计行;
  4. 跑板级顶层(-d BOARD_ROM_FAST),检查跑马灯 LED 序列。

最后汇总 PASS/FAIL,并以失败数作为退出码。编译与仿真的中间文件都写在 %TEMP%\cpu21_regress 下,不会污染工程目录;需要 xvlog/xelab/xsim 在 PATH 上。

期望结果(2026-09,多周期乘除法 + CLK_DIV=2):

测试 期望
muldiv_test / isa_test / lui_test / mmio_test 13 / 24 / 4 / 5 个 LED 值全部匹配
tb_no_intr(benchmark,5560 周期) stalls=1528 bubbles=1830 cond_taken=639 uncond=71 pred_ok=687 pred_fail=80
tb_cpu21_riscv_redirect_int_bpb(中断,6003 周期) stalls=963 bubbles=1205 cond_taken=547 uncond=28 pred_ok=557 pred_fail=87
板级(快速 ROM) 跑马灯 0001 → … → 8000 循环,每步约 2.6 µs

工程分享(Tcl 一键重建)

pipelined-cpu/ 已整理成“自包含 + 可直接分享”的工程目录,设计文件都在里面, 别人拿到整个文件夹后不需要改任何路径:

pipelined-cpu/
├─ create_project.tcl              一键重建脚本(用 [info script] 定位自身目录)
├─ srcs/rtl/*.v                    处理器 RTL(13 个文件)
├─ srcs/board/*.v|*.vh             板级顶层、片内 ROM 初始化(含 __FAST 仿真版本)
├─ srcs/constrs/*.xdc              引脚约束 + 板级母版
├─ srcs/tb/*.v                     三个测试平台
├─ srcs/rom/*.hex                  测试平台加载的 ROM 镜像
├─ tools/、programs/               迷你汇编器与测试程序(重生成 ROM 用)
├─ cpu21_riscv_redirect_int_bpb说明.md
└─ pipelined-cpu.xpr(+ *.runs/…) 本机工程,分享时可删

用法(在 Vivado Tcl Shell 里,或命令行 vivado -mode batch -source create_project.tcl):

cd <pipelined-cpu 所在目录>
source create_project.tcl

脚本会就地创建 pipelined-cpu.xpr,并配好:

  • sources_1 顶层 cpu21_board_top(处理器 RTL + 板级顶层 + 片内 ROM 初始化, cpu21_board_rom.vh 标记为 Verilog Header);
  • constrs_1:cpu21_board_top.xdc(引脚 + CLK_DIV 分频时钟)与板级母版;
  • sim_1(顶层 tb_cpu21_riscv_redirect_int_bpb)与 sim_2(顶层 tb_no_intr); 测试平台会依次探测 ROM 镜像,其中 ../../../../../srcs/rom/ 对应 GUI 仿真的 工作目录(<proj>.sim/sim_1/behav/xsim),所以创建完就能直接 Run Simulation;
  • 两个仿真 fileset 的 xsim.simulate.runtime 设为 all,避免 GUI 仿真只跑 1000 ns 就停下(看起来像卡死)。

分享前建议删掉生成目录:*.xpr、*.runs、*.sim、*.cache、*.hw、*.gen、 *.ip_user_files、.Xil。实测只带 create_project.tcl + srcs/ + pipelined-cpu/tools/ + programs/ + 说明文档约 0.3 MB,在任意路径重建工程并综合成功。

源文件只有一份,就在 pipelined-cpu/srcs/(tools/、programs/ 同理), 直接在工程目录里改即可,没有需要同步的副本。

也可以用 Vivado 自动生成:

write_project_tcl -force -paths_relative_to <工程目录> <工程目录>/create_project.tcl

注意自动生成的版本会把源路径写成生成时的绝对路径、并且要求 origin_dir 指向 工程目录,所以要手动改两行才能给别人用:

set origin_dir [file dirname [file normalize [info script]]]
create_project ${_xil_proj_name_} $origin_dir -part xc7a100tcsg324-1
S
Description
26华科硬件综合训练
Readme
2.7 MiB
Languages
SystemVerilog 45.3%
Verilog 28.2%
Python 6.8%
Tcl 6.6%
Assembly 5.9%
Other 7.2%