Author SHA1 Message Date
Anccend 266ec6a049 重构:按功能拆分流水线模块、修复 BLT 指令、注释中文化
1) 模块拆分(便于单独仿真调试)
   把原来单文件的三模块结构按功能拆成 10 个独立文件,顶层只保留五级流水
   寄存器、子模块例化、PC/IF-ID/ID-EX 控制优先级与对外输出:
     cpu21_riscv_alu.v            组合 ALU(result2 用于 MUL 高位/余数)
     cpu21_bpb_8.v                8 项全相联分支目标缓冲
     cpu21_riscv_decoder.v        ID 级组合译码器
     cpu21_riscv_regfile.v        寄存器堆(写优先旁路)
     cpu21_riscv_forward_unit.v   EX 级前递网络
     cpu21_riscv_store_unit.v     EX 级存储数据对齐(sw/sb)
     cpu21_riscv_branch_unit.v    EX 级控制流裁决
     cpu21_riscv_hazard_unit.v    load-use 冒险与停顿/冲刷判定
     cpu21_riscv_irq_ctrl.v       中断优先级、ustatus/uepc、嵌套返回栈
     cpu21_riscv_perf_counters.v  性能计数器
   已把上述文件加入 Vivado 工程 sources_1;端口、时序与行为经 xsim 对同一
   ROM 逐周期回归验证,统计量完全一致。

2) 修复 BLT 指令(真值表第 28 行:opcode IR[6:2]=0x18、funct3=100、ALU_OP=SLT)
   此前 funct3=100 未译码,blt 被当作空指令执行,于是
   `blt s1,zero,loop` 的循环只执行一次就顺序落到退出代码并停机
   (现象:只输出第一个值后就不再运行)。
     - cpu21_riscv_decoder.v:新增 blt_o,OP_BRANCH 接受 funct3=100,
       并按真值表给出 ALU_SLT(beq/bne 仍为 SUB,bltu 仍为 SLTU)
     - cpu21_riscv_branch_unit.v:新增 blt_i,用有符号比较
       $signed(src1) < $signed(src2) 裁决
     - cpu21_riscv_redirect_int_bpb.v:新增 idex_blt_q 流水寄存器,
       IF 级 f_is_branch 纳入 funct3=100,使 blt 也参与 BPB 预测
   验证:blt 小程序正确输出 -3/-2/-1 后停机(cond_taken=2);
   benchmark 第 [4] 段完整输出 fffffff1..ffffffff;
   中断测试程序的中断入口与停留周期与改动前一致。

3) 注释中文化
   - cpu21_riscv_redirect_int_bpb.v 及全部新增子模块使用中文注释;
   - testbench/tb_cpu21_riscv_redirect_int_bpb.v 与新增的 tb_no_intr.v
     注释全部译为中文。
   - testbench/tb_no_intr.v:与中断测试平台同框架但不注入 irq 脉冲的对照
     测试平台(修复其 $dumpvars 引用了不存在的模块名,并改用 tb_no_intr.vcd
     避免与另一个 TB 的波形文件互相覆盖)。

4) 其他
   - testbench/tb_cpu21_riscv_redirect_int_bpb.v 的默认 ROM_FILE 改为
     cpu21_riscv_redirect_int_bpb_rom.hex(中断测试程序);
   - cpu21_riscv_redirect_int_bpb说明.md 增补"文件与模块划分"章节与 BLT 说明;
   - 真值表.txt 移动到 testbench/programs/ 下。
2026-09-12 18:48:31 +08:00
Anccend abb2e161af 注释添加和翻译 2026-09-12 16:34:30 +08:00
Anccend 03ac91bf65 初版,Logisim转换Verilog 2026-09-12 13:18:24 +08:00
85 changed files with 1355 additions and 23029 deletions
-25
View File
@@ -1,26 +1 @@
# Vivado 工程生成物不入库;源文件都在 pipelined-cpu/ 下(srcs/、tools/、programs/)
pipelined-cpu/*
!pipelined-cpu/srcs/
!pipelined-cpu/tools/
!pipelined-cpu/programs/
!pipelined-cpu/create_project.tcl
!pipelined-cpu/create_project_saolei.tcl
# 本地临时目录(仿真脚本、日志、中间文件)
_tmp/
_vivado/
# 仿真/综合中间产物
*.vcd
*.pb
*.hex.lst
.Xil/
# 在仓库根目录批量跑 Vivado 时产生的日志/命令记录(pipelined-cpu/ 下已被上面的规则忽略)
vivado*.jou
vivado*.log
vivado*.backup.jou
vivado*.backup.log
# saolei 的构建产物(tools/build.ps1 一键重建,不入库)
saolei/build/
-792
View File
@@ -1,792 +0,0 @@
# `cpu21-riscv-4.circ` 的 Verilog 实现
处理器顶层文件:`pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v`
该设计对应原电路中的“重定向流水线+中断+分支预测”部分,顶层模块为
`cpu21_riscv_redirect_int_bpb`。为便于单独仿真与调试,各功能单元已拆分为
独立文件,顶层的 ALU、BPB 也已移出,具体见文末“文件与模块划分”。
## 目录结构
```text
risc-v/
├─ README.md 本文档
├─ pipelined-cpu/ Vivado 工程目录(也是源文件所在地)
│ ├─ create_project.tcl 一键重建工程的 Tcl 脚本
│ ├─ srcs/rtl/*.v 处理器与外设 RTL(18 个文件,cpu21_riscv_redirect_int_bpb.v 为处理器顶层)
│ │ 外设:cpu21_mmio.v(MMIO 中枢)、cpu21_vga.v(显示)、
│ │ cpu21_seg7.v(数码管)、cpu21_pointer.v(按键当鼠标)、
│ │ cpu21_ps2_mouse.v / cpu21_ps2_kbd.v(PS/2 鼠标、键盘接收)
│ ├─ srcs/board/*.v|*.vh 板级顶层(时钟分频 + 片内 ROM/RAM + 引脚);四份 ROM 初始化:
│ │ cpu21_board_rom.vh / _fast.vh(跑马灯)、risc-v-intrupt-ccab.vh
│ │ (课程中断综合测试程序)、cpu21_saolei_rom.vh(扫雷)
│ ├─ srcs/constrs/*.xdc 引脚与分频时钟约束(+ Nexys4DDR 母版)
│ ├─ srcs/tb/*.v 七个测试平台(含 PS/2 鼠标/键盘的单独自测 tb_ps2_mouse.v / tb_ps2_kbd.v)
│ ├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像
│ ├─ tools/ rv_asm.py(迷你汇编器)、hex2vh.py(hex → ROM 初始化)、
│ │ adapt_intrupt_ccab.py(课程中断程序 → 上板 ROM)、
│ │ run_regress.ps1(一键回归)、check_rtl_text.ps1(RTL 文本体检)、
│ │ rars_f0c874c.jar(课程提供的 RARS 汇编器)、play_saolei.tcl(命令行玩扫雷)
│ └─ programs/ 汇编源程序与期望值:跑马灯 demo、指令集/MMIO 测试、课程原始
│ 资产(risc-v-benchmark_ccab、真值表),以及中断综合测试程序
│ risc-v-intrupt-ccab.asm / .hex
└─ 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`–`0x2FFF` | 显存 | 40 列 × 30 行、每格 2 字节(低字节 = 字形[5:0]、高字节 = 颜色[2:0],共 2400 字节),写端口接到板级顶层的双口 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` 汇编):
```asm
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/rtl/cpu21_ps2_kbd.v` | `cpu21_ps2_kbd` | 可选的真键盘:扫描码集 2 解码(`F0` 断码 / `E0` 扩展前缀),把按键映射成与鼠标同格式的事件 |
| `pipelined-cpu/srcs/board/cpu21_board_top.v` | `cpu21_board_top` | 板级顶层:时钟分频、复位同步、片内 ROM/RAM、显存双口 RAM、VGA/数码管、PS/2 键鼠选择(SW15)、中断连线 |
| `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/srcs/tb/tb_saolei.v` | `tb_saolei` | 扫雷自动打完一局并 dump 出显存(配 `saolei/tools/fb2png.py` 渲染成 PNG) |
| `pipelined-cpu/srcs/tb/tb_saolei_play.v` | `tb_saolei_play` | 命令行“边玩边截图”的测试平台(配 `tools/play_saolei.tcl`) |
| `pipelined-cpu/srcs/tb/tb_ps2_mouse.v` | `tb_ps2_mouse` | PS/2 鼠标接收器自测:奇校验、3 字节包、按键位(几秒出结果) |
| `pipelined-cpu/srcs/tb/tb_ps2_kbd.v` | `tb_ps2_kbd` | PS/2 键盘解码自测:方向键 / Enter / 左 Shift / 松开码不产生事件 |
| `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 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支
跳转、预测成败)完全一致。
## 各模块的实现方式
下面按“处理器流水线 → 外设 → 板级”的顺序说明每个模块的实现要点(对应上面
“文件与模块划分”表)。全部是可综合的纯 RTL,没有例化 Xilinx 原语,
因此 `xvlog`/`xelab` 可以直接仿真。
### 流水线主体
- **`cpu21_riscv_redirect_int_bpb`(顶层)**:五级寄存器 IF/ID、ID/EX、EX/MEM、
MEM/WB 各自带 valid 位;下一个 PC 由一个多路选择器决定(复位 `RESET_PC`、
顺序 PC+4、分支/跳转重定向目标、`URET` 返回地址、中断入口)。停顿与冲刷分别
由 hazard unit 的 `pipeline_stall_o`、`flush_younger_o` 控制:停顿时冻结
PC/IF-ID 并给 ID/EX 插气泡;冲刷时只把年轻指令的 valid 清零,所以已经提交的
存储/寄存器写不会被执行两次。
- **`cpu21_riscv_alu`**:纯组合,4 位 `alu_op` 直接选择加减/逻辑/移位/比较;
减法用“B 取反加 1”实现,比较分有符号/无符号两条路径。乘除法不在 ALU 里
(历史上的组合 `a*b`/`a%b` 是关键路径,已拆到多周期单元)。
- **`cpu21_riscv_muldiv`**:32 拍移位迭代(乘法 shift-add、除法 restoring),
加装载与定型共占用 EX **34 拍**;期间 `ex_md_stall` 有效 → 保持 ID/EX、给
EX/MEM 插气泡、冻结取指、屏蔽中断(见“时序和使用说明”第 7 条)。接口沿用
ALU 约定:`result_o` 是低位/商、`result2_o` 是高位/余数;除零时商全 1、
余数取被除数。
- **`cpu21_riscv_decoder`**:ID 级组合译码,操作码取 `IR[6:2]`,`funct3`/
`funct7`/`IR[21]` 区分 R 型细分、M 扩展、CSR 与 `ecall`/`URET`;输出一组
控制位(`alu_op`、`alu_src`、立即数、`reg_write`、访存使能、`wb_sel`、
分支/跳转、CSR 操作、`md_op`)。
- **`cpu21_riscv_regfile`**:32×32 寄存器堆,`x0` 恒 0;两个组合读口 + 一个写口,
同周期 WB 的数据通过“写优先旁路”直送到读口,省掉一拍。
- **`cpu21_riscv_forward_unit`**:EX 级前递,把 EX/MEM 与 MEM/WB 的结果和 ID/EX
的 `rs1/rs2` 比较,EX/MEM 优先;EX/MEM 是 load 时不参与前递(load 结果要
到 MEM 才有效,改由 hazard unit 停顿一拍);store 的数据同样可以前递。
- **`cpu21_riscv_store_unit`**:EX 级数据对齐,`sw` 产生四个字节通道、`sb` 按
`addr[1:0]` 只打开一个通道,并把数据搬到对应字节位置,输出 `data_wstrb_o`。
- **`cpu21_riscv_branch_unit`**:EX 级裁决分支方向与目标:6 条条件分支各用有
符号/无符号比较,`jal`/`jalr` 总是重定向,并把 `PC+4` 写回 `rd`(链接地址
在 EX 级预先算好放进 EX/MEM,避免在写回/前递路径上再加一个加法器)。
- **`cpu21_riscv_hazard_unit`**:load-use 冒险(ID/EX 是 load 且和 ID 级要用的
寄存器相同)插一个气泡;`flush_younger` 的优先级高于停顿:中断、`URET`、
`ecall` 停机、分支/跳转重定向都会丢弃年轻指令,此时不再插气泡。
- **`cpu21_riscv_irq_ctrl`**:`irq_i` 先打两拍同步,再与“上一拍”比较取出
**上升沿**(挂起位锁存),优先级 IRQ3 > IRQ2 > IRQ1;响应条件是“有挂起 +
`ustatus[0]`(MIE) + 嵌套深度 < 4 + 未停机 + EX 级没有重定向/停机”。
`ustatus`(0x004) 与 `uepc`(0x041) 用组合逻辑实现 CSRRS/CSRRC/CSRRW 三种读写
语义(旧值回写 `rd`);嵌套栈保存 EPC / `ustatus` / 上一层优先级,`URET` 时
逐层恢复。响应时保存“下一条尚未执行的指令的 PC”,EX 是已正确预测的跳转
分支则保存实际目标,避免 `URET` 重复执行已提交的操作。
- **`cpu21_riscv_perf_counters`**:周期、停顿、气泡、条件分支、无条件跳转、
预测成功/失败六个计数器;`halted_o` 期间不计周期;load-use 记 1 个气泡,
重定向/中断清空记 2 个气泡。
### 外设
- **`cpu21_mmio`**:MEM 级译码;先用**字对齐**地址比较命中哪个寄存器(这样
`sb` 写到 `0x1001` 也能命中 LED 寄存器),再用 `data_wstrb_o` 决定改哪几个
字节;命中时给出 `rdata_hit_o`,顶层据此在 RAM 与 MMIO 之间选择读数据。
内部包含 LED/数码管寄存器、鼠标寄存器组(含 16 深事件 FIFO 与 `mouse_irq_o`)、
显示控制器、显存窗口,以及用于自检的 `MAGIC` 常量。
- **`cpu21_vga`**:工作在 CPU 时钟下,用 `tick`(每 2 拍一个使能)产生 25 MHz
像素节拍(**不引入第二个时钟域**);按 640×480@60 的行场计数产生同步信号,
并按“格子”寻址显存:一个显存字对应 4 个像素,8×8 字模放大 2 倍填满 16×16
的格子(40×30 格),帧末产生 `frame_tick_o`。
- **`cpu21_seg7`**:8 位动态扫描。计数到 `REFRESH_DIV` 换一位(默认 50 MHz 下
50000 → 每步 1 ms,每位数码管 125 Hz 刷新);十六进制→七段码用查表;共阳极
所以位选/段选都取反输出,`an_en_i` 可以把某几位熄灭。
- **`cpu21_pointer`**:把 5 向按键变成鼠标事件。用约 1 ms 的采样节拍对两级同步
后的按键采样,只在“按下”边沿产生事件;一次采样可能同时按下多个键,用待发
队列 + 优先级选择器逐个上报(每拍最多一个事件,不丢键)。事件单位是 1/8 格,
软件端只用整数加减就能换算成格(不需要除法)。
- **`cpu21_ps2_mouse`**:可选的真鼠标,只实现接收:`ps2_clk` 下降沿把 11 位帧
**按位的下标**存进 `frame_q`(`frame_q[i]` = 第 i 位,所以字节 = `frame_q[8:1]`、
校验 = `frame_q[9]`、停止 = `frame_q[10]`),停下来后判“停止位 = 1 + 奇校验”,
再按 3 字节包解析出 dx/dy/按键,产生 `ev_valid_o` 脉冲。这里两个细节曾经写错过,
导致真设备的数据**全被丢掉**(上板表现为 `rx_count` 恒定 0):
① 原来用整体右移 `{data, frame_q[10:1]}`,判 `frame_q[10]` 时停止位还没移进来,
判到的是校验位,字节也整体错了一位;② 原来按**偶**校验判,而 PS/2 是**奇**校验。
- **`cpu21_ps2_kbd`**:可选的真键盘,扫描码集 2:`0xE0` = 扩展键前缀、`0xF0` = 松开
前缀,只有**按下**才产生事件(松开码丢弃),所以按住方向键时键盘自己的重复码正好
变成“连续移动”。产出的事件格式与鼠标**完全一致**(↑/↓/←/→ = dy/dx ±8,
一格;Enter/空格 = 左键;左 Shift/F/C = 右键),因此 `saolei.c`/`isr.c` 一行都不用改。
### 板级
- **`cpu21_board_top`**:把这些接起来,只引出板上真正需要的引脚。内含参数化分频器
(`CLK_DIV` / `CLK_DIV_INTRUPT`)、复位同步器(异步置位 + 同步释放)、片内
ROM(组合读、`nop` 填充、`ifdef` 链选程序)与 RAM(字节写使能 + 组合读、
只响应 4 KB 以内地址,避免和 MMIO 别名)、显存双口 RAM(CPU 写口 + VGA 读口
同处一个时钟域,用像素使能而不是第二时钟)、数码管、VGA、PS/2 键鼠(同一根
PS/2 线上同时挂鼠标与键盘两个解码器,用 `hid_sel`(SW15) 选哪个生效,板载 5 向
按键始终有效)、中断连线(SW0/1/2 分别与鼠标事件/关卡软件中断相或)、
锁存式中断指示 LED(请求上升沿置位、对应级别的 ISR 执行 `uret` 后熄灭)和
`led_we` 写入指示;另有一个“PS/2 诊断”模式把收到的字节数/时钟活动/线电平
摆到 LED 高位(`DEBUG_PS2_RX_LED`,见下文“PS/2 键鼠”)。
## 上板(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;当前
`create_project.tcl` 默认定义的是 `RISC_V_INTRUPT_CCAB_ROM`(跑课程的
中断综合测试程序),宏的含义、优先级和改法见下文“内置程序与宏的用法”。
### 引脚分配(`cpu21_board_top.xdc`)
| 端口 | 引脚 | 板上元件 | 说明 |
| --------------- | ----------- | ------------------------- | ----------------------------------------------------------------------- |
| `clk100` | E3 | 100 MHz 晶振 | 经`CPU_DIV` 分频后给 CPU(默认 50 MHz) |
| `rst_n` | C12 | 按键 CPU_RESETN(低有效) | 异步置位、与 CPU 时钟同步释放 |
| `go` | N17 | 按键 BTNC | 对应原电路“继续运行”(`ecall` 停机后恢复执行) |
| `irq_sw[2:0]` | J15/L16/M13 | 拨码开关 SW0–SW2 | 三路中断源,拨动即触发(内部同步并锁存) |
| `hid_sel` | V10 | 拨码开关 SW15 | 选 PS/2 设备:0 = 键盘(默认),1 = 鼠标(见下文“PS/2 键鼠”) |
| `sw_flag` | R15 | 拨码开关 SW3 | 扫雷的插旗模式(软件读 INPUT 寄存器;自动鼠标右键同样能插旗) |
| `led[15:0]` | H17…V11 | LED0–LED15 | `LED[2:0]` = 三路中断指示(锁存,ISR 返回后熄灭);`LED[15:3]` 平时熄灭,`DEBUG_PS2_RX_LED=1` 时是 PS/2 诊断(见下文) |
| `led_we` | R12 | LED16_B | MMIO 写入指示,每写一次拉长约 0.34 s @50 MHz |
### PS/2 键鼠(板载 USB HID 口,也可改接 Pmod JA)
Nexys4DDR 右上角那个 **USB HOST** 口不是给 FPGA 直连的 USB:板上有一颗 USB↔PS/2
桥接芯片,它把自己当 USB 主机,把键盘/鼠标当 PS/2 设备,输出的是 PS/2 的两根线
(官方母版标明 `ps2_clk = F4`、`ps2_data = B2`)。所以 FPGA 侧只需要一个 PS/2
接收器,**不用写 USB 协议栈**。
| 接线方式 | 能不能用 | 说明 |
| ------------------------------------- | -------- | ---------------------------------------------------------------- |
| 键鼠用**有线模式**插 USB HOST | 最推荐 | Type-C/PS2-USB 转换都用它的有线模式 |
| 2.4G USB 接收器 | 可以试 | 部分复合设备桥接芯片不转发 |
| 蓝牙 | 不行 | 桥接芯片不支持蓝牙,键盘的蓝牙链路它根本看不见 |
| Pmod PS/2(JA)+ 真 PS/2 键鼠 | 保底 | 直接进 FPGA,只需把 XDC 改回 `ps2_clk = C17`、`ps2_data = D18` |
**键盘/鼠标不动时一个字节都不发**,所以插上不动时诊断 LED 不跳是正常的:要一边按
键/移鼠标一边看。
**选设备:SW15 拨下(0)= PS/2 键盘,拨上(1)= PS/2 鼠标**(默认键盘)。两个
解码器挂在同一根线上,但只有选中的那一路事件会送给 CPU —— 不能让两路同时生效:
键盘的扫描码会凑巧组成“合法的鼠标包”,会让光标乱跳。
诊断 LED(`DEBUG_PS2_RX_LED = 1`,默认开):
| LED | 含义 |
| ------------------ | ---------------------------------------------------------- |
| `LED[15:6]` | 当前选中设备**收到的字节数**低 10 位(按键时数字跳 = 通了) |
| `LED[5]` | `ps2_clk` 有边沿活动(时钟在动就快速翻转) |
| `LED[4]` / `LED[3]` | `ps2_data` / `ps2_clk` 当前电平(静态,判断线有没有被拉死) |
| `LED[2:0]` | 三路中断指示(和其它模式一样) |
排查口诀:**LED[5] 在跳但字节数不动** = 桥接芯片在发数据、我们的解码没认(帧格式/
校验不匹配,先怀疑奇偶校验);**LED[5] 也不跳** = 桥接芯片根本没转发这个设备
(换有线模式,或改走 Pmod JA)。
| 操作 | 键盘 | 鼠标 |
| ---------------- | --------------------------------- | ---------- |
| 移动光标 | `↑` `↓` `←` `→` | 移动 |
| 左键(开始/开格) | `Enter`、空格 | 左键 |
| 右键(插旗) | 左键 `Shift`、`F`、`C` | 右键 |
按住方向键 = 连续移动(用的是键盘自己的重复码)。**C 程序一行都不用改**:键盘产出的
事件与 `cpu21_pointer`、`cpu21_ps2_mouse` 格式完全相同(±8 = 一格),一样从 IRQ2
进 `saolei/sw/isr.c`。不想要 PS/2 就把顶层参数 `USE_PS2_MOUSE` 设为 0(只剩板载
5 向按键);PS/2 接收器的帧格式/校验有专门的几秒钟自测(见下文“回归测试”)。
两条**上板实测**出来的方向约定(都值得写进报告):
- **鼠标的 dy 要取负**:PS/2 的原生约定是「正 dx = 右、**正 dy = 鼠标上移**」,
而软件里「正 dx = 列 +1、**正 dy = 行 +1(向下)**」(板载按键 BTND 就是 +8),
所以 `cpu21_ps2_mouse.v` 在拼包时把 dy 取负、dx 不动。不定这个负号的表现就是
**左右正常、上下反向** —— 不是解码错。
- **键鼠静止时一个字节都不发**,所以“插上不动没反应”是正常的,必须一边按键/
移鼠标一边看 LED。
### 时钟:分频比怎么选(`CLK_DIV` / `CLK_DIV_INTRUPT`)
乘除法原本是 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
内部路径而误报大量违例:
```tcl
create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 2 [get_pins { clk_div_reg/Q }]
```
改分频比时:**变慢**(分频比变大)不用动 XDC;**变快**(分频比小于 2)就要把 XDC 的
`-divide_by` 一起改小,否则约束会比实际时钟松、可能掩盖违例。
### 中断测试程序为什么还要再慢一档
标着 `RISC_V_INTRUPT_CCAB_ROM` 的课程中断综合测试程序里,两次显示之间的延时常量
是按 50 MHz 写的:50 MHz 下每个数值只停留约 10 ms,动态扫描的数码管上根本读不
出来。所以顶层在跑这个程序时改用**另一个分频比参数 `CLK_DIV_INTRUPT`**:停留时间
与分频比成正比:取 8 → 12.5 MHz(约 40 ms/值)、取 32 → 3.125 MHz(约 160 ms/值)——
当前默认 32,在 `cpu21_board_top.v` 里改。这个分频比只在定义了
`RISC_V_INTRUPT_CCAB_ROM` 时生效,其它程序仍用 `CLK_DIV`。
### 分频与刷新率一览(想改速度看这里)
| 想改什么 | 改哪里 | 当前值 / 换算 |
| -------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
| CPU 主频(程序快慢) | 参数`CLK_DIV` | 2 → 50 MHz(`100 MHz / CLK_DIV`) |
| 中断程序的 CPU 主频 | 参数`CLK_DIV_INTRUPT`(只在该 ROM 宏下生效) | 32 → 3.125 MHz;每个数值约 10 ms × (`CLK_DIV_INTRUPT`/2) |
| 数码管扫描刷新率 | 顶层`SEG_REFRESH_DIV`(自动跟随 `CPU_DIV`) | `100000 / CPU_DIV` → 每步 1 ms、每位 125 Hz |
| LED16_B 写入指示点亮时长 | `led_we_cnt` 的装载值 `24'hFF_FFFF` | 约 0.34 s @50 MHz(`0xFFFFFF / f_cpu`) |
| 中断程序每个数值的停留时间 | `tools/adapt_intrupt_ccab.py` 的 `DELAY_NEG_HI` | `0xFFFC0` → 约 10 ms @50 MHz |
- **数码管**:`SEG_REFRESH_DIV = 100000 / CPU_DIV` 保证“每步 1 ms、每位数码管
125 Hz”与主频无关(不这样缩放的话,主频降 4 倍时全屏刷新只剩 31 Hz,能看到
闪烁)。想更亮可以把它改小(扫描更快),但每位刷新率要远高于 50 Hz。
`cpu21_seg7` 的分频计数是 20 位,最大约 100 万,所以 100 MHz(不分频)也放得下。
- **慢下来的副作用**:`cpu21_pointer` 的按键采样节拍(`SAMPLE_DIV = 50000`)也会
跟着变慢(3.125 MHz 下约 16 ms 采样一次,仍在可接受范围);`cpu21_vga` 的
25 MHz 像素节拍是从 CPU 时钟分频出来的,主频一降 VGA 行场时序就超出规范
(中断测试程序不用 VGA,无影响)。
- **XDC 不用跟着改(只要不是变快)**:`cpu_clk` 固定按最坏情况 `-divide_by 2`
(50 MHz)约束,实际时钟更慢属于“约束偏严”,不会掩盖违例;只有把分频比改到
比 2 更小(更快)时,才需要同步改小 XDC 的 `-divide_by`。
### 片内 ROM / RAM
- ROM:`ROM_WORDS = 8192` 字(32 KB),组合读,取指地址 `instr_addr[ROM_AW+1:2]`;
内容由 `` `include `` 一个 ROM 初始化文件给出(与 `cpu21_board_top.v` 同目录,
文件里是 `rom[i] = 32'h…;` 语句,放在 `initial` 里,等价于 `$readmemh`,
且不依赖仿真工作目录)。具体包含哪份由宏决定(见下文“内置程序与宏的用法”)。
超出镜像长度的地址填 `nop`(`32'h0000_0013`),所以跳飞时不会执行到随机指令。
- RAM:`RAM_WORDS = 1024` 字(4 KB),字节写使能 + 组合读,只响应
`data_addr < 4 KB`,因此 MMIO 地址(默认 `0x1000`)不会落进 RAM。
- 两个 `initial` 块都用 NOP(`32'h0000_0013`)或 0 预填充,ROM 未使用区域是 NOP,
这样即使程序跑飞也不会执行到随机指令而乱写寄存器。
### 内置程序与宏的用法
板级顶层用一条 `` `ifdef `` 链决定 ROM 里固化哪个程序(连带决定中断入口地址),
`create_project.tcl` 里的两个开关就是给这条链用的:
| 宏(`sources_1` 的 Verilog Define) | ROM 文件 | 中断入口`IRQ1/2/3` | 说明 |
| ------------------------------------- | -------------------------------------- | --------------------------------- | -------------------------------------------- |
| `SAOLEI_ROM`(优先级最高) | `srcs/board/cpu21_saolei_rom.vh` | `0x100` / `0x200` / `0x300` | 纯 C 扫雷(`saolei/tools/build.ps1` 生成) |
| `RISC_V_INTRUPT_CCAB_ROM` | `srcs/board/risc-v-intrupt-ccab.vh` | `0x0AC` / `0x1E4` / `0x308` | 课程中断综合测试程序(当前默认) |
| `BOARD_ROM_FAST` | `srcs/board/cpu21_board_rom_fast.vh` | `0x100` / `0x200` / `0x300` | 仿真专用:跑马灯延时改小(综合不用) |
| 都不定义 | `srcs/board/cpu21_board_rom.vh` | `0x100` / `0x200` / `0x300` | 上板跑马灯 demo(默认) |
同一时间只应定义一个(`ifdef` 链按上表顺序取第一个命中的)。**中断入口必须和
ROM 里 ISR 的起点一致**,所以换程序时宏与入口是一起换的:`cpu21_board_top.v`
里 `.IRQ1/2/3_VECTOR` 也跟着 `RISC_V_INTRUPT_CCAB_ROM` 分支走。
#### 怎么定义宏
1. **Vivado GUI**:`Project` → `Settings` → `Synthesis` → `Verilog Options` →
`Verilog Macros` 里填 `RISC_V_INTRUPT_CCAB_ROM`(只对 `sources_1` 有效),
确定后 `Reset Synthesis Run` 再综合。也可以直接在 Tcl Console 里写:
```tcl
set_property verilog_define {RISC_V_INTRUPT_CCAB_ROM} [get_filesets sources_1]
reset_run synth_1
launch_runs synth_1
wait_on_run synth_1
```
换回跑马灯就是 `set_property verilog_define {} [get_filesets sources_1]`。
2. **一键重建**:改 `create_project.tcl` 里的 `use_intrupt_ccab` / `use_saolei`
两个开关(同一时间只开一个),重新 `source create_project.tcl`。
3. **命令行仿真**:`xvlog` 与 `xelab` 都要带同一个 `-d`,并用 `-i` 指向头文件目录:
```powershell
xvlog -d RISC_V_INTRUPT_CCAB_ROM -i srcs/board `
srcs/rtl/*.v srcs/board/cpu21_board_top.v 你的tb.v
xelab -top 你的tb -snapshot s -d RISC_V_INTRUPT_CCAB_ROM
```
4. **注意**:这些宏只影响“综合用”的 `sources_1`。跑 `tb_no_intr` /
`tb_cpu21_riscv_redirect_int_bpb` 时,程序来自测试平台自己探测的
`srcs/rom/*.hex`,与宏无关。
#### 中断综合测试程序的适配(`tools/adapt_intrupt_ccab.py`)
课程给的 `programs/risc-v-intrupt-ccab.hex` 不能直接上板,脚本把它适配成
`srcs/board/risc-v-intrupt-ccab.vh`,且**不移动任何指令地址**(三个中断入口
仍是 `0x0AC` / `0x1E4` / `0x308`):
```powershell
python tools/adapt_intrupt_ccab.py programs/risc-v-intrupt-ccab.hex `
srcs/board/risc-v-intrupt-ccab.vh _tmp/fast.hex _tmp/plain.hex
```
六处适配(脚本内每步都有断言,输入不对会直接报错):
1. 三个 ISR 里的 `addi a7,zero,10` + `ecall` → 两条 `nop`(`a7 != 34` 的
`ecall` 会让这颗 CPU 停机等 `go_i`);
2. 11 个显示点 `lui a7,1` + `sw a0,0(a7)` → `jal` 调用追加的显示例程;
顺带纠正早期手改留下的 `sb` 误码(只写低字节会显示错值);
3. 追加主程序用的 `DISP_DLY`(0x438):写 LED 寄存器(数码管显示它)+ 约 10 ms
延时;返回地址存在 `sp+256`,位于 ISR 压栈区(最多 192 B)之上,所以中断
随时打断延时也不会破坏它;
4. 修正 ISR2/ISR3 出栈顺序写乱的 12 条 `lw`(否则 ISR2 的循环计数会变成
-3823,看起来像“二级中断返回之后卡住”);
5. ISR3 的 SB 测试改用 ISR3 自己保存的 `s4/s5/s6`(原来用未保存的 `t1/t3/s2`,
抢占 ISR1 时会毁掉 ISR1 的 DIVU 循环计数,造成死循环);
6. 追加 ISR 专用的 `DISP_DLY_ISR`(0x458):**链接寄存器改用 `a0`、显示值走
`a7`**(这两个寄存器三个 ISR 都保存/恢复),于是 ISR 完全不碰 `ra`。否则
中断若恰好落在主程序 `DISP_DLY` 末尾的 `jalr` 上,返回会跳进 ISR 代码、
最后执行到 ISR 结尾的 `csrrci` 把 MIE 清零(“跑一阵子之后拨开关不再响应”);
第 3 个参数生成“延时极小”的仿真版,便于在 xsim 里跑完整流程;第 4 个参数生成
只去掉停机 `ecall`、显示仍是 `lui`+`sw` 的“原样”版本,用于对比。
适配后的 ROM 在仿真里验证控制流很简单:用 `-d RISC_V_INTRUPT_CCAB_ROM` 编译
`srcs/rtl/*.v` + `srcs/board/cpu21_board_top.v` 加一个自建的 tb(或直接用
`srcs/tb/tb_board_top.v` 看 LED),观察 `irq_current_level_o`(0→1→2→3→…→0)、
`uepc_o`、`halted_o` 是否依次回到 0,以及 `LED[2:0]` 是否随嵌套逐层点亮/熄灭。
建议用第 3 个参数生成的快版 ROM(否则一次中断就是几十毫秒仿真时间)。
### 演示程序的生成与仿真
```powershell
# 在工程目录 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 |
```powershell
# 快速版仿真:编译时加上 -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/步,只差延时循环的初值)生成:
```powershell
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`,
四份 ROM 初始化文件(`cpu21_board_rom.vh`、`cpu21_board_rom_fast.vh`、
`risc-v-intrupt-ccab.vh`、`cpu21_saolei_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`。复位后先跑 `sources_1` 里宏选中的那个程序。以跑马灯为例:复位后立即从
`0001` 开始逐位左移(演示程序不执行 `ecall`,所以不用按 `go`;`go` 只对执行了
`ecall` 停机、等待继续运行的程序有意义),LED16_B 每次写入时点亮。
#### 跑中断综合测试程序时的现象
(`create_project.tcl` 默认就是它,无需额外设置)
- 复位后主程序先跑自己的指令测试,数码管上显示程序写进 `LED_ADDR` 的 32 位十六进制值
(低 4 位在最右边那个数码管);
- 拨动 **SW0 / SW1 / SW2**(J15/L16/M13)就对应 1/2/3 号中断:数码管随即切成该 ISR 的
显示内容(SB 读写测试、DIVU 测试、多层循环等),每个数值停留约 160 ms(3.125 MHz 下);
- `LED[2:0]` 是**锁存**的中断指示:从该路中断请求出现开始亮,一直到它的 ISR 执行完
`uret` 返回才熄灭。抢占(例如 ISR1 还没跑完就拨 SW2)不会把外层的灯灭掉,一级
一级返回时才依次熄灭,正好能看出嵌套层次;
- LED16_B 每次 MMIO 写入内一下;程序里若有 `ecall a7 != 34` 的停机点,会停在原地等
按 **BTNC**(`go`)继续;
- 想重新开始随时按 **CPU_RESETN**(`rst_n`);想让它更快/更慢就改 `CLK_DIV_INTRUPT`
(见上文“分频与刷新率一览”)。
## 扫雷程序(`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` 就加载扫雷程序,不定义仍是下面的跑马灯。
### 扫雷:怎么烧到板上
```powershell
# 1) 生成扫雷的 ROM 镜像(改了 saolei/sw/*.c 之后必须重跑这一步)
cd saolei
powershell -ExecutionPolicy Bypass -File tools\build.ps1 `
-Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
# 2) 一键建"扫雷专用"工程(先清掉旧的同名工程再重建;sources_1 和 sim_1 都固定定义 SAOLEI_ROM)
cd ..\pipelined-cpu
& 'C:\Xilinx\Vivado\2020.2\bin\vivado.bat' -mode batch -source create_project_saolei.tcl -log create_saolei.log
# 产出 pipelined-cpu-saolei.xpr(和通用工程 pipelined-cpu 互不影响)
```
然后在 Vivado GUI 里依次点:`Run Synthesis` → `Run Implementation` → `Generate Bitstream`
→ `Open Hardware Manager` → `Open Target / Auto Connect` → `Program Device`。
板上操作:
| 板子上 | 作用 |
| ------------------------- | ---------------------------------------------------------- |
| BTNU / BTND / BTNL / BTNR | 光标 上 / 下 / 左 / 右 |
| BTNC | 左键:开始界面确认 / 关卡里开格 |
| SW3 拨上 + BTNC | 插旗 / 取消旗 |
| SW15 = 1 | 改用 PS/2 鼠标(默认 SW15 = 0 = PS/2 键盘,见上文“PS/2 键鼠”) |
| 键盘 `↑↓←→` / `Enter`·空格 / 左`Shift`·`F`·`C` | 移动 / 左键 / 右键(插旗)—— 需 SW15 = 0 |
| 鼠标 移动 / 左键 / 右键 | 移动 / 左键 / 右键(插旗)—— 需 SW15 = 1 |
| SW0 / SW1 / SW2 | **别动**,它们会 OR 进外部中断线,一拨就可能触发中断 |
开始界面的光标默认停在 START 按钮上,按 BTNC 进关卡;关卡里把光标移到盘面**左上角那个黄格**
再按 BTNC 就退回开始界面(分出胜负后随便点一下也能退)。
### 扫雷:没有 VGA 显示器怎么玩(命令行仿真 + 每步截图)
显存是内存映射的(`0x2000` 起,40×30 格、每格 2 字节:低字节 = 字形[5:0]、
高字节 = 颜色[2:0],共 2400 字节 = 600 字),
所以仿真里可以把整屏 dump 出来,再按 `cpu21_vga.v` 的字库/调色板还原成 PNG —— 等价于给 VGA 截图。
`tools/play_saolei.tcl` 更进一步:用 `cmd.txt` 往仿真里"喂"按键,每按一步自动存一张 PNG。
```powershell
# ① 建仿真快照(只在第一次,或改了 RTL 之后需要重跑)
cd D:\Desktop\risc-v-cpu
New-Item -ItemType Directory -Force -Path _tmp\saolei_sim | Out-Null # 用哪个空目录都行
cd _tmp\saolei_sim
$p = 'D:\Desktop\risc-v-cpu\pipelined-cpu'
$rtl = (Get-ChildItem "$p\srcs\rtl\*.v").FullName
& 'C:\Xilinx\Vivado\2020.2\bin\xvlog.bat' -d SAOLEI_ROM -i "$p\srcs\board" -i "$p\srcs\rtl" $rtl "$p\srcs\board\cpu21_board_top.v" "$p\srcs\tb\tb_saolei_play.v"
& 'C:\Xilinx\Vivado\2020.2\bin\xelab.bat' -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
# ② 开始玩(之后是交互式的,命令直接敲在 xsim 提示符下)
& 'C:\Xilinx\Vivado\2020.2\bin\xsim.bat' saolei_play
```
到 `xsim%` 提示符后(**`source` 要带 `-encoding utf-8`**,否则 xsim 按 GBK 读脚本会乱码):
```text
source -encoding utf-8 D:/Desktop/risc-v-cpu/pipelined-cpu/tools/play_saolei.tcl
k c 左键点击(进关卡用这个)
k u/d/l/r 光标 上/下/左/右(也认 w/s/a/d)
k f 插旗模式点击 k p 只切换插旗开关
k z 复位 k uucc 连写多个动作(上、上、点、点)
shot 重画 + 存图 view 用看图器打开最新那张图
help / quit
```
- **每按一个动作都会自动存一张 PNG**:`shots/001_c.png`、`shots/002_u.png`… 可以直接贴进报告;
同时会覆盖 `shots/latest.png` —— 在 VS Code 里打开这一张,每操作一步它自动刷新,等于边玩边看屏幕;
- 仿真速度约 1 ms 仿真时间 = 0.5 s 墙钟,所以每一步等 1~2 秒;
- 只想"看一局"不想手动操作:用 `srcs/tb/tb_saolei.v`(`-top tb_saolei`)会自动打完一整局
(首击展开 → 全盘扫描 → 216/216 通关),并 dump 出 `fb_00_start.txt` … `fb_13_after_sweep.txt`,
再用 `py saolei\tools\fb2png.py <dump.txt> <out.png>` 渲染成 640×480 PNG;
- 光标格在画面里是**黄色 2 像素常亮方框**,VGA 上和截图里都能一眼看到指针在哪。
## 回归测试
工程里带了一个 xsim 回归脚本,在 `pipelined-cpu/` 目录下运行:
```powershell
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 |
PS/2 接收器不在上面这个脚本里(它跟板载芯片/真设备有关,不属于纯 RTL 回归),
但解码逻辑(帧对齐、奇校验、包解析、键位映射)值得单独盯,两个自测各几秒钟:
```powershell
cd pipelined-cpu
$p = 'D:\Desktop\risc-v-cpu\pipelined-cpu'
# 鼠标接收器
& 'C:\Xilinx\Vivado\2020.2\bin\xvlog.bat' "$p\srcs\rtl\cpu21_ps2_mouse.v" "$p\srcs\tb\tb_ps2_mouse.v"
& 'C:\Xilinx\Vivado\2020.2\bin\xelab.bat' -top tb_ps2_mouse -snapshot ps2m
& 'C:\Xilinx\Vivado\2020.2\bin\xsim.bat' ps2m -R # 期望 ALL PASS
# 键盘解码(把 mouse 换成 kbd)
& 'C:\Xilinx\Vivado\2020.2\bin\xvlog.bat' "$p\srcs\rtl\cpu21_ps2_kbd.v" "$p\srcs\tb\tb_ps2_kbd.v"
& 'C:\Xilinx\Vivado\2020.2\bin\xelab.bat' -top tb_ps2_kbd -snapshot ps2k
& 'C:\Xilinx\Vivado\2020.2\bin\xsim.bat' ps2k -R # 期望 ALL PASS
```
两个自测都会:发几个帧 → 检查事件内容是否正确;**故意发一个校验位错的帧** →
必须整帧丢掉(不计数、不产生事件);键盘那个还会检查“松开码(F0 xx)不产生事件”。
## 工程分享(Tcl 一键重建)
`pipelined-cpu/` 已整理成“自包含 + 可直接分享”的工程目录,设计文件都在里面,
别人拿到整个文件夹后**不需要改任何路径**:
```text
pipelined-cpu/
├─ create_project.tcl 一键重建脚本(用 [info script] 定位自身目录)
├─ srcs/rtl/*.v 处理器与外设 RTL(17 个文件)
├─ srcs/board/*.v|*.vh 板级顶层、四份 ROM 初始化(跑马灯/中断测试/扫雷/仿真快版)
├─ 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`):
```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 自动生成:
```tcl
write_project_tcl -force -paths_relative_to <工程目录> <工程目录>/create_project.tcl
```
注意自动生成的版本会把源路径写成**生成时的绝对路径**、并且要求 `origin_dir` 指向
工程目录,所以要手动改两行才能给别人用:
```tcl
set origin_dir [file dirname [file normalize [info script]]]
create_project ${_xil_proj_name_} $origin_dir -part xc7a100tcsg324-1
```
+60
View File
@@ -0,0 +1,60 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// ALU:纯组合运算单元。
//
// result 为主结果;result2 为第二结果,用于 MUL 的高 32 位与 DIVU 的余数。
// 除法做了除零保护:b==0 时商为 32'hffff_ffff、余数为被除数。
// -----------------------------------------------------------------------------
module cpu21_riscv_alu (
input wire [ 3:0] op,
input wire [31:0] a,
input wire [31:0] b,
output reg [31:0] result,
output reg [31:0] result2
);
localparam ALU_SLL = 4'd0;
localparam ALU_SRA = 4'd1;
localparam ALU_SRL = 4'd2;
localparam ALU_MUL = 4'd3;
localparam ALU_DIVU = 4'd4;
localparam ALU_ADD = 4'd5;
localparam ALU_SUB = 4'd6;
localparam ALU_AND = 4'd7;
localparam ALU_OR = 4'd8;
localparam ALU_XOR = 4'd9;
localparam ALU_NOR = 4'd10;
localparam ALU_SLT = 4'd11;
localparam ALU_SLTU = 4'd12;
reg [63:0] mult_result;
always @* begin
mult_result = a * b;
result2 = 32'b0;
case (op)
ALU_SLL: result = a << b[4:0];
ALU_SRA: result = $signed(a) >>> b[4:0];
ALU_SRL: result = a >> b[4:0];
ALU_MUL: begin
result = mult_result[31:0];
result2 = mult_result[63:32];
end
ALU_DIVU: begin
result = (b == 32'b0) ? 32'hffff_ffff : a / b;
result2 = (b == 32'b0) ? a : a % b;
end
ALU_ADD: result = a + b;
ALU_SUB: result = a - b;
ALU_AND: result = a & b;
ALU_OR: result = a | b;
ALU_XOR: result = a ^ b;
ALU_NOR: result = ~(a | b);
ALU_SLT: result = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0;
ALU_SLTU: result = (a < b) ? 32'd1 : 32'd0;
default: result = 32'b0;
endcase
end
endmodule
`default_nettype wire
@@ -18,9 +18,7 @@ module cpu21_riscv_branch_unit (
input wire beq_i,
input wire bne_i,
input wire blt_i,
input wire bge_i,
input wire bltu_i,
input wire bgeu_i,
input wire jal_i,
input wire jalr_i,
// IF 级携带过来的预测信息
@@ -33,20 +31,15 @@ module cpu21_riscv_branch_unit (
output wire redirect_valid_o,
output wire [31:0] redirect_pc_o
);
// 比较结果预先算好,避免表达式过长被自动换行拆散。
wire lt_signed = $signed(src1_i) < $signed(src2_i);
wire ge_signed = $signed(src1_i) >= $signed(src2_i);
wire lt_unsigned = src1_i < src2_i;
wire ge_unsigned = src1_i >= src2_i;
// 条件分支裁决:beq 相等、bne 不等、blt/bge 有符号比较、bltu/bgeu 无符号比较。
// 条件分支裁决:beq 相等、bne 不等、blt 有符号小于、bltu 无符号小于。
assign branch_taken_o = branch_i &&
((beq_i && (src1_i == src2_i)) ||
(bne_i && (src1_i != src2_i)) ||
(blt_i && lt_signed) ||
(bge_i && ge_signed) ||
(bltu_i && lt_unsigned) ||
(bgeu_i && ge_unsigned));
(blt_i && ($signed(
src1_i
) < $signed(
src2_i
))) || (bltu_i && (src1_i < src2_i)));
// 跳转目标:jalr 为 (rs1+imm) 且最低位清零;jal 为 PC+imm。
assign target_o = jalr_i ? ((src1_i + imm_i) & 32'hffff_fffe) : (pc_i + imm_i);
@@ -17,7 +17,6 @@ module cpu21_riscv_decoder (
output reg [31:0] imm_o,
output reg [ 3:0] alu_op_o,
output reg [ 2:0] wb_sel_o,
output reg [ 2:0] md_op_o,
output wire [11:0] csr_addr_o,
output reg uses_rs1_o,
output reg uses_rs2_o,
@@ -30,9 +29,7 @@ module cpu21_riscv_decoder (
output reg beq_o,
output reg bne_o,
output reg blt_o,
output reg bge_o,
output reg bltu_o,
output reg bgeu_o,
output reg jal_o,
output reg jalr_o,
output reg ecall_o,
@@ -50,8 +47,6 @@ module cpu21_riscv_decoder (
localparam OP_BRANCH = 5'h18;
localparam OP_JAL = 5'h1b;
localparam OP_SYS = 5'h1c;
// LUI 为扩展指令(标准 RISC-V opcode 0x37 -> IR[6:2]=0x0d)。
localparam OP_LUI = 5'h0d;
localparam ALU_SLL = 4'd0;
localparam ALU_SRA = 4'd1;
@@ -65,18 +60,6 @@ module cpu21_riscv_decoder (
localparam ALU_XOR = 4'd9;
localparam ALU_SLT = 4'd11;
localparam ALU_SLTU = 4'd12;
localparam ALU_MULH = 4'd13;
localparam ALU_MULHSU = 4'd14;
localparam ALU_PASSB = 4'd15; // LUI:直接输出 B 端(立即数)
// 乘除法单元(cpu21_riscv_muldiv)的操作编码,MD_NONE 表示非乘除法指令。
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;
wire [4:0] opcode = ir_i[6:2];
wire [2:0] funct3 = ir_i[14:12];
@@ -93,7 +76,6 @@ module cpu21_riscv_decoder (
imm_o = {{20{ir_i[31]}}, ir_i[31:20]};
alu_op_o = ALU_ADD;
wb_sel_o = 3'd0;
md_op_o = MD_NONE;
uses_rs1_o = 1'b0;
uses_rs2_o = 1'b0;
reg_write_o = 1'b0;
@@ -105,9 +87,7 @@ module cpu21_riscv_decoder (
beq_o = 1'b0;
bne_o = 1'b0;
blt_o = 1'b0;
bge_o = 1'b0;
bltu_o = 1'b0;
bgeu_o = 1'b0;
jal_o = 1'b0;
jalr_o = 1'b0;
ecall_o = 1'b0;
@@ -122,40 +102,13 @@ module cpu21_riscv_decoder (
uses_rs1_o = 1'b1;
uses_rs2_o = 1'b1;
reg_write_o = 1'b1;
// M 扩展(funct7=0000001):mul/mulh/mulhsu/mulhu,以及课程真值表里的
// divu(funct3=101,商)与 remu(funct3=111,余数)。它们都由多周期
// 乘除法单元完成,这里只给出操作类型 md_op_o;mulhu 与 remu 依旧用
// wb_sel=3 取 result2(高位乘积 / 余数)。
if (funct7 == 7'b0000001) begin
case (funct3)
3'b000: begin // mul:低 32 位
alu_op_o = ALU_MUL;
md_op_o = MD_MUL;
end
3'b001: begin // mulh:有符号高 32 位
alu_op_o = ALU_MULH;
md_op_o = MD_MULH;
end
3'b010: begin // mulhsu:有符号 x 无符号高 32 位
alu_op_o = ALU_MULHSU;
md_op_o = MD_MULHSU;
end
3'b011: begin // mulhu:无符号高 32 位
alu_op_o = ALU_MUL;
md_op_o = MD_MULHU;
wb_sel_o = 3'd3;
end
3'b101: begin // divu:无符号商
// 电路中额外的 REMU 控制对应标准 R 型的 funct7=1/funct3=111
// 形式;此时 ALU 的 result2 即为余数。
if ((funct7 == 7'b0000001) && (funct3 == 3'b111)) begin
alu_op_o = ALU_DIVU;
md_op_o = MD_DIVU;
end
3'b111: begin // remu:无符号余数
alu_op_o = ALU_DIVU;
md_op_o = MD_REMU;
wb_sel_o = 3'd3;
end
default: reg_write_o = 1'b0;
endcase
end else if ((funct7 == 7'b0000001) && (funct3 == 3'b000)) begin
alu_op_o = ALU_MUL;
end else begin
// 标准 R 型 funct3 译码;add/sub 与 sra/srl 由 funct7[5] 区分。
case (funct3)
@@ -172,7 +125,7 @@ module cpu21_riscv_decoder (
end
end
// I 型运算(addi/slli/slti/sltiu/xori/srai/srli/ori/andi)。
// I 型运算(addi/slli/slti/xori/srai/srli/ori/andi)。
OP_I: begin
uses_rs1_o = 1'b1;
alu_src_o = 1'b1;
@@ -181,7 +134,6 @@ module cpu21_riscv_decoder (
3'b000: alu_op_o = ALU_ADD; // addi:立即数加
3'b001: alu_op_o = ALU_SLL; // slli:立即数逻辑左移
3'b010: alu_op_o = ALU_SLT; // slti:有符号小于置 1
3'b011: alu_op_o = ALU_SLTU; // sltiu:无符号小于置 1(立即数符号扩展)
3'b100: alu_op_o = ALU_XOR; // xori:立即数异或
3'b101:
alu_op_o = (funct7[5] ? ALU_SRA : ALU_SRL); // srai/srli:立即数算术/逻辑右移
@@ -191,16 +143,6 @@ module cpu21_riscv_decoder (
endcase
end
// 加载高位立即数 lui:rd = imm[31:12] << 12。
// 不读寄存器;用 ALU_SRC 选立即数,ALU 直通 B 端输出。
OP_LUI: begin
reg_write_o = 1'b1;
alu_src_o = 1'b1;
alu_op_o = ALU_PASSB;
imm_o = {ir_i[31:12], 12'b0};
wb_sel_o = 3'd0;
end
// 加载指令,当前仅支持 lw(funct3=010)。
OP_LOAD: begin
if (funct3 == 3'b010) begin
@@ -226,22 +168,19 @@ module cpu21_riscv_decoder (
end
end
// 条件分支:beq(000)/bne(001)/blt(100)/bge(101)/bltu(110)/bgeu(111)。
// 条件分支:beq(000)/bne(001)/blt(100,有符号小于)/bltu(110,无符号小于)。
OP_BRANCH: begin
if ((funct3 == 3'b000) || (funct3 == 3'b001) || (funct3 == 3'b100) ||
(funct3 == 3'b101) || (funct3 == 3'b110) || (funct3 == 3'b111)) begin
if ((funct3 == 3'b000) || (funct3 == 3'b001) ||
(funct3 == 3'b100) || (funct3 == 3'b110)) begin
uses_rs1_o = 1'b1;
uses_rs2_o = 1'b1;
branch_o = 1'b1;
beq_o = (funct3 == 3'b000);
bne_o = (funct3 == 3'b001);
blt_o = (funct3 == 3'b100);
bge_o = (funct3 == 3'b101);
bltu_o = (funct3 == 3'b110);
bgeu_o = (funct3 == 3'b111);
// 有符号比较用 ALU_SLT、无符号比较用 ALU_SLTU,相等/不等用减法;
// 分支是否成立由 EX 级的分支裁决单元决定,这里只与真值表保持一致。
alu_op_o = (blt_o || bge_o) ? ALU_SLT : ((bltu_o || bgeu_o) ? ALU_SLTU : ALU_SUB);
// 真值表:beq/bne 用减法比较,blt 用 ALU_SLT,bltu 用 ALU_SLTU。
alu_op_o = blt_o ? ALU_SLT : (bltu_o ? ALU_SLTU : ALU_SUB);
imm_o = {{19{ir_i[31]}}, ir_i[31], ir_i[7], ir_i[30:25], ir_i[11:8], 1'b0};
end
end
@@ -37,10 +37,7 @@ module cpu21_riscv_hazard_unit (
assign flush_younger_o = take_irq_i || uret_redirect_i || halt_event_i || redirect_valid_i;
// 停顿条件里不再包含 flush_younger:冲刷时 valid 位会被清 0、PC/IF-ID 也由
// 冲刷分支(优先级高于停顿)处理,同时发生停顿只会多保持一拍 payload,
// 不影响正确性。这样可以避免把“冲刷”这条长路径引到 payload 的时钟使能上。
assign pipeline_stall_o = load_use_hazard_o && !halted_i;
assign pipeline_stall_o = load_use_hazard_o && !flush_younger_o && !halted_i;
endmodule
`default_nettype wire
@@ -12,7 +12,7 @@ module cpu21_riscv_perf_counters (
input wire clk,
input wire reset,
input wire inc_cycle_i, // 未停机时周期数 +1
input wire stall_i, // load-use 停顿或多周期乘除法停留
input wire stall_i, // load-use 停顿
input wire flush_i, // 冲刷年轻指令
input wire cond_taken_i, // EX 级条件分支实际跳转
input wire cond_valid_i, // EX 级条件分支有效
+733
View File
@@ -0,0 +1,733 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21-risc-v-4.circ -> 可综合 Verilog 实现(顶层)
//
// 处理器为五级流水线 IF/ID/EX/MEM/WB。本文件只保留:
// * 五级流水寄存器与各级之间的连线;
// * 各功能子模块的例化;
// * PC / IF-ID / ID-EX 的控制优先级(中断、URET、停机、重定向、停顿、
// 正常推进);
// * 对外输出与调试信号。
//
// 各功能单元已拆分为独立文件,便于单独仿真调试:
// cpu21_riscv_alu.v 组合 ALU(result2 用于 MUL 高位 / 余数)
// cpu21_bpb_8.v 八表项全相联分支目标缓冲
// cpu21_riscv_decoder.v ID 级组合译码器
// cpu21_riscv_regfile.v 寄存器堆(写优先旁路)
// cpu21_riscv_forward_unit.v EX 级前递网络
// cpu21_riscv_store_unit.v EX 级存储数据对齐
// cpu21_riscv_branch_unit.v EX 级控制流裁决
// cpu21_riscv_hazard_unit.v load-use 冒险与冲刷/停顿判定
// cpu21_riscv_irq_ctrl.v 中断控制、ustatus/uepc 与嵌套返回栈
// cpu21_riscv_perf_counters.v 性能计数器
//
// 主要特性:
// * EX 级前递(forwarding)与 load-use 冒险互锁;
// * EX 级控制流裁决与流水线重定向;
// * 八表项全相联分支目标缓冲(BPB);
// * 三路采样的中断源,带优先级与可嵌套的 URET 返回;
// * 沿用原 ecall 约定:a7==34 时写出 LED 值,否则流水线等待 go_i。
//
// 存储器接口约定:
// instr_addr_o 与 data_addr_o 均为字节地址。data_wstrb_o 是字节写掩码,
// 因此外部字宽 RAM 可用 data_addr_o[11:2] 作为字索引,再依据
// data_wstrb_o 完成字节合并。
// -----------------------------------------------------------------------------
module cpu21_riscv_redirect_int_bpb #(
parameter RESET_PC = 32'h0000_0000,
parameter IRQ1_VECTOR = 32'h0000_30ac,
parameter IRQ2_VECTOR = 32'h0000_31e4,
parameter IRQ3_VECTOR = 32'h0000_3310,
parameter IRQ_STACK_DEPTH = 4,
// ustatus 复位值。详见 cpu21_riscv_irq_ctrl 中的说明。
parameter [31:0] USTATUS_INIT = 32'h0000_0001
) (
input wire clk,
input wire reset,
input wire [31:0] instr_i,
input wire [31:0] data_rdata_i,
input wire [ 2:0] irq_i,
input wire go_i,
output wire [31:0] instr_addr_o,
output wire [31:0] data_addr_o,
output wire [31:0] data_wdata_o,
output wire [ 3:0] data_wstrb_o,
output wire data_we_o,
output wire [31:0] led_data_o,
output wire led_valid_o,
output wire [31:0] if_pc_o,
output wire [31:0] id_pc_o,
output wire [31:0] ex_pc_o,
output wire [31:0] mem_pc_o,
output wire [31:0] wb_pc_o,
output wire [31:0] if_ir_o,
output wire [31:0] id_ir_o,
output wire [31:0] ex_ir_o,
output wire [31:0] mem_ir_o,
output wire [31:0] wb_ir_o,
output wire [31:0] rdin_o,
output wire [31:0] mdin_o,
output wire reg_write_o,
output wire mem_write_o,
output wire halted_o,
output wire [ 2:0] irq_pending_o,
output wire [ 1:0] irq_current_level_o,
output wire [31:0] uepc_o,
output wire [31:0] ustatus_o,
output wire bpb_predict_hit_o,
output wire bpb_predict_taken_o,
output wire bpb_mispredict_o,
output wire [15:0] cycle_count_o,
output wire [15:0] stall_count_o,
output wire [15:0] bubble_count_o,
output wire [15:0] conditional_taken_count_o,
output wire [15:0] unconditional_branch_count_o,
output wire [15:0] prediction_success_count_o,
output wire [15:0] prediction_failure_count_o
);
// 顶层只用到少量常量,完整译码表见 cpu21_riscv_decoder.v。
localparam OP_BRANCH = 5'h18;
localparam ALU_ADD = 4'd5;
// -------------------------------------------------------------------------
// 流水线寄存器
// -------------------------------------------------------------------------
// 当前取指 PC。
reg [31:0] pc_q;
// IF/ID 流水寄存器(含本条指令所携带的分支预测信息)。
reg ifid_valid_q;
reg [31:0] ifid_pc_q;
reg [31:0] ifid_ir_q;
reg ifid_pred_taken_q;
reg [31:0] ifid_pred_target_q;
// ID/EX 流水寄存器:保存译码结果与操作数。
reg idex_valid_q;
reg [31:0] idex_pc_q;
reg [31:0] idex_ir_q;
reg [31:0] idex_rs1_value_q;
reg [31:0] idex_rs2_value_q;
reg [4:0] idex_rs1_idx_q;
reg [4:0] idex_rs2_idx_q;
reg [4:0] idex_rd_q;
reg [31:0] idex_imm_q;
reg [3:0] idex_aluop_q;
reg idex_alu_src_q;
reg idex_reg_write_q;
reg idex_mem_to_reg_q;
reg idex_mem_write_q;
reg idex_mem_byte_q;
reg [2:0] idex_wb_sel_q;
reg [11:0] idex_csr_addr_q;
reg idex_branch_q;
reg idex_beq_q;
reg idex_bne_q;
reg idex_blt_q;
reg idex_bltu_q;
reg idex_jal_q;
reg idex_jalr_q;
reg idex_ecall_q;
reg idex_uret_q;
reg idex_csr_set_q;
reg idex_csr_clear_q;
reg idex_csr_write_q;
reg idex_pred_taken_q;
reg [31:0] idex_pred_target_q;
// EX/MEM 流水寄存器:保存 ALU/存储结果与写回控制。
reg exmem_valid_q;
reg [31:0] exmem_pc_q;
reg [31:0] exmem_ir_q;
reg [31:0] exmem_alu_result_q;
reg [31:0] exmem_alu_result2_q;
reg [31:0] exmem_csr_old_q;
reg [31:0] exmem_write_data_q;
reg [4:0] exmem_rd_q;
reg exmem_reg_write_q;
reg exmem_mem_to_reg_q;
reg exmem_mem_write_q;
reg [3:0] exmem_wstrb_q;
reg [2:0] exmem_wb_sel_q;
// MEM/WB 流水寄存器:保存访存数据与最终写回信息。
reg memwb_valid_q;
reg [31:0] memwb_pc_q;
reg [31:0] memwb_ir_q;
reg [31:0] memwb_alu_result_q;
reg [31:0] memwb_alu_result2_q;
reg [31:0] memwb_csr_old_q;
reg [31:0] memwb_mem_data_q;
reg [4:0] memwb_rd_q;
reg memwb_reg_write_q;
reg memwb_mem_to_reg_q;
reg [2:0] memwb_wb_sel_q;
// LED 输出与停机标志。
reg [31:0] led_data_q;
reg led_valid_q;
reg halted_q;
// -------------------------------------------------------------------------
// IF 级:分支预测。JAL/JALR 在 EX 级裁决,条件分支则可以直接依据 BPB
// 给出的目标地址重定向取指 PC。
// -------------------------------------------------------------------------
wire [4:0] f_opcode = instr_i[6:2];
wire [2:0] f_funct3 = instr_i[14:12];
wire f_is_branch = (f_opcode == OP_BRANCH) &&
((f_funct3 == 3'b000) || (f_funct3 == 3'b001) ||
(f_funct3 == 3'b100) || (f_funct3 == 3'b110));
wire [31:0] bpb_predict_target;
wire bpb_predict_hit;
wire bpb_predict_taken;
wire ex_branch_taken;
wire [31:0] ex_target;
wire ex_controlflow;
wire ex_redirect_valid;
wire [31:0] ex_redirect_pc;
// BPB 更新端口:在 EX 级用条件分支的实际结果训练预测器。
wire bpb_update_enable = idex_valid_q && idex_branch_q;
wire bpb_update_taken = ex_branch_taken;
wire [31:0] bpb_update_target = ex_target;
// 分支目标缓冲实例。
cpu21_bpb_8 u_bpb (
.clk (clk),
.reset (reset),
.predict_pc (pc_q),
.predict_enable (f_is_branch),
.predict_hit (bpb_predict_hit),
.predict_taken (bpb_predict_taken),
.predict_target (bpb_predict_target),
.update_enable (bpb_update_enable),
.update_controlflow(1'b1),
.update_pc (idex_pc_q),
.update_target (bpb_update_target),
.update_taken (bpb_update_taken)
);
// 下一取指 PC:预测跳转命中时使用 BPB 目标地址,否则顺序加 4。
wire [31:0] fetch_next_pc =
(f_is_branch && bpb_predict_hit && bpb_predict_taken) ?
bpb_predict_target : (pc_q + 32'd4);
// -------------------------------------------------------------------------
// ID 级:译码与寄存器堆读取
// -------------------------------------------------------------------------
wire [4:0] d_src1_idx;
wire [4:0] d_src2_idx;
wire [4:0] d_rd;
wire [31:0] d_imm;
wire [3:0] d_aluop;
wire [2:0] d_wb_sel;
wire [11:0] d_csr_addr;
wire d_uses_rs1;
wire d_uses_rs2;
wire d_reg_write;
wire d_mem_to_reg;
wire d_mem_write;
wire d_mem_byte;
wire d_alu_src;
wire d_branch;
wire d_beq;
wire d_bne;
wire d_blt;
wire d_bltu;
wire d_jal;
wire d_jalr;
wire d_ecall;
wire d_uret;
wire d_csr_set;
wire d_csr_clear;
wire d_csr_write;
cpu21_riscv_decoder u_decoder (
.ir_i (ifid_ir_q),
.rs1_idx_o (d_src1_idx),
.rs2_idx_o (d_src2_idx),
.rd_o (d_rd),
.imm_o (d_imm),
.alu_op_o (d_aluop),
.wb_sel_o (d_wb_sel),
.csr_addr_o (d_csr_addr),
.uses_rs1_o (d_uses_rs1),
.uses_rs2_o (d_uses_rs2),
.reg_write_o (d_reg_write),
.mem_to_reg_o(d_mem_to_reg),
.mem_write_o (d_mem_write),
.mem_byte_o (d_mem_byte),
.alu_src_o (d_alu_src),
.branch_o (d_branch),
.beq_o (d_beq),
.bne_o (d_bne),
.blt_o (d_blt),
.bltu_o (d_bltu),
.jal_o (d_jal),
.jalr_o (d_jalr),
.ecall_o (d_ecall),
.uret_o (d_uret),
.csr_set_o (d_csr_set),
.csr_clear_o (d_csr_clear),
.csr_write_o (d_csr_write)
);
// 写回值选择:wb_sel 0=ALU 结果,1=访存数据,2=PC+4,3=MUL 高位/余数,
// 4=CSR 旧值。
wire [31:0] wb_value;
assign wb_value = (memwb_wb_sel_q == 3'd1) ? memwb_mem_data_q :
(memwb_wb_sel_q == 3'd2) ? (memwb_pc_q + 32'd4) :
(memwb_wb_sel_q == 3'd3) ? memwb_alu_result2_q :
(memwb_wb_sel_q == 3'd4) ? memwb_csr_old_q :
memwb_alu_result_q;
// WB 级写回使能(同时用于寄存器堆的写端口与写优先旁路)。
wire memwb_reg_write_en = memwb_valid_q && memwb_reg_write_q;
// ID 级读寄存器:x0 恒为 0,并对同周期 WB 的结果做写优先旁路。
wire [31:0] d_rs1_value;
wire [31:0] d_rs2_value;
cpu21_riscv_regfile u_regfile (
.clk (clk),
.reset (reset),
.rs1_idx_i (d_src1_idx),
.rs2_idx_i (d_src2_idx),
.rs1_data_o(d_rs1_value),
.rs2_data_o(d_rs2_value),
.we_i (memwb_reg_write_en),
.waddr_i (memwb_rd_q),
.wdata_i (wb_value)
);
// 中断/CSR 相关连线(中断控制器实例见下方"中断 / CSR"小节)。
wire take_irq;
wire [31:0] irq_vector;
wire [31:0] ex_return_pc;
wire [2:0] irq_pending;
wire [1:0] irq_current_level;
wire [31:0] uepc_value;
wire [31:0] ustatus_value;
// -------------------------------------------------------------------------
// EX 级:前递网络、ALU 运算、存储对齐与控制流裁决
// -------------------------------------------------------------------------
// EX/MEM 级要前递的值(按 wb_sel 选取)。
wire [31:0] exmem_forward_value =
(exmem_wb_sel_q == 3'd2) ? (exmem_pc_q + 32'd4) :
(exmem_wb_sel_q == 3'd3) ? exmem_alu_result2_q :
(exmem_wb_sel_q == 3'd4) ? exmem_csr_old_q :
exmem_alu_result_q;
// 前递优先级:EX/MEM 的结果最新,其次为 MEM/WB 写回值,最后才是
// 译码时读到的寄存器值。
wire [31:0] ex_src1;
wire [31:0] ex_src2;
cpu21_riscv_forward_unit u_forward_unit (
.rs1_value_i (idex_rs1_value_q),
.rs2_value_i (idex_rs2_value_q),
.rs1_idx_i (idex_rs1_idx_q),
.rs2_idx_i (idex_rs2_idx_q),
.exmem_valid_i (exmem_valid_q),
.exmem_reg_write_i (exmem_reg_write_q),
.exmem_mem_to_reg_i(exmem_mem_to_reg_q),
.exmem_rd_i (exmem_rd_q),
.exmem_value_i (exmem_forward_value),
.memwb_valid_i (memwb_valid_q),
.memwb_reg_write_i (memwb_reg_write_q),
.memwb_rd_i (memwb_rd_q),
.memwb_value_i (wb_value),
.src1_o (ex_src1),
.src2_o (ex_src2)
);
// ALU 的 B 端:立即数或 rs2。
wire [31:0] ex_alu_b = idex_alu_src_q ? idex_imm_q : ex_src2;
wire [31:0] ex_alu_result;
wire [31:0] ex_alu_result2;
// 读取 CSR 旧值(ustatus=0x004、uepc=0x041),供 CSR 指令回写 rd。
wire [31:0] ex_csr_old_value =
(idex_csr_addr_q == 12'h004) ? ustatus_value :
(idex_csr_addr_q == 12'h041) ? uepc_value : 32'b0;
// ALU 实例(纯组合逻辑)。
cpu21_riscv_alu u_ex_alu (
.op (idex_aluop_q),
.a (ex_src1),
.b (ex_alu_b),
.result (ex_alu_result),
.result2(ex_alu_result2)
);
// 控制流裁决:条件分支方向/目标、JAL/JALR 目标、是否需要重定向。
cpu21_riscv_branch_unit u_branch_unit (
.valid_i (idex_valid_q),
.pc_i (idex_pc_q),
.imm_i (idex_imm_q),
.src1_i (ex_src1),
.src2_i (ex_src2),
.branch_i (idex_branch_q),
.beq_i (idex_beq_q),
.bne_i (idex_bne_q),
.blt_i (idex_blt_q),
.bltu_i (idex_bltu_q),
.jal_i (idex_jal_q),
.jalr_i (idex_jalr_q),
.pred_taken_i (idex_pred_taken_q),
.pred_target_i (idex_pred_target_q),
.branch_taken_o (ex_branch_taken),
.controlflow_o (ex_controlflow),
.target_o (ex_target),
.redirect_valid_o(ex_redirect_valid),
.redirect_pc_o (ex_redirect_pc)
);
// 存储数据对齐:依据字节地址低两位生成写数据与字节写掩码。
wire [31:0] ex_store_data;
wire [ 3:0] ex_store_wstrb;
cpu21_riscv_store_unit u_store_unit (
.addr_i (ex_alu_result),
.data_i (ex_src2),
.mem_write_i (idex_mem_write_q),
.mem_byte_i (idex_mem_byte_q),
.store_data_o (ex_store_data),
.store_wstrb_o(ex_store_wstrb)
);
// ecall 约定:a7==34 时输出 LED;否则令处理器停机,等待 go_i 重新启动。
wire ex_led_event = idex_valid_q && idex_ecall_q && (ex_src1 == 32'd34);
wire ex_halt_event = idex_valid_q && idex_ecall_q && (ex_src1 != 32'd34);
// -------------------------------------------------------------------------
// 中断 / CSR / 嵌套返回栈
// -------------------------------------------------------------------------
wire ex_uret_redirect = idex_valid_q && idex_uret_q;
// EX 级正在重定向/URET/停机时不允许响应中断。
wire ex_block_irq = ex_redirect_valid || ex_uret_redirect || ex_halt_event;
// 中断发生时要保存的返回地址取"下一个架构 PC":若 EX 是一条预测正确
// 且已跳转的分支,则该 PC 就是它的目标地址;若 EX 为空,则 IF/ID 中
// 保存的就是尚未执行的最老指令。
wire [31:0] irq_save_pc = idex_valid_q ?
((idex_branch_q && ex_branch_taken) ? ex_target : (idex_pc_q + 32'd4)) :
(ifid_valid_q ? ifid_pc_q : pc_q);
cpu21_riscv_irq_ctrl #(
.IRQ1_VECTOR (IRQ1_VECTOR),
.IRQ2_VECTOR (IRQ2_VECTOR),
.IRQ3_VECTOR (IRQ3_VECTOR),
.IRQ_STACK_DEPTH(IRQ_STACK_DEPTH),
.USTATUS_INIT (USTATUS_INIT)
) u_irq_ctrl (
.clk (clk),
.reset (reset),
.irq_i (irq_i),
.halted_i (halted_q),
.ex_block_i (ex_block_irq),
.save_pc_i (irq_save_pc),
.uret_i (ex_uret_redirect),
.csr_valid_i(idex_valid_q),
.csr_set_i (idex_csr_set_q),
.csr_clear_i(idex_csr_clear_q),
.csr_write_i(idex_csr_write_q),
.csr_addr_i (idex_csr_addr_q),
.csr_imm_i (idex_imm_q),
.csr_wdata_i(ex_src1),
.take_irq_o (take_irq),
.vector_o (irq_vector),
.return_pc_o(ex_return_pc),
.pending_o (irq_pending),
.level_o (irq_current_level),
.uepc_o (uepc_value),
.ustatus_o (ustatus_value)
);
// -------------------------------------------------------------------------
// 冒险 / 停顿 / 冲刷判定
// -------------------------------------------------------------------------
wire load_use_hazard;
wire flush_younger;
wire pipeline_stall;
cpu21_riscv_hazard_unit u_hazard_unit (
.ifid_valid_i (ifid_valid_q),
.idex_valid_i (idex_valid_q),
.idex_mem_to_reg_i(idex_mem_to_reg_q),
.idex_rd_i (idex_rd_q),
.d_uses_rs1_i (d_uses_rs1),
.d_uses_rs2_i (d_uses_rs2),
.d_src1_idx_i (d_src1_idx),
.d_src2_idx_i (d_src2_idx),
.take_irq_i (take_irq),
.uret_redirect_i (ex_uret_redirect),
.halt_event_i (ex_halt_event),
.redirect_valid_i (ex_redirect_valid),
.halted_i (halted_q),
.load_use_hazard_o(load_use_hazard),
.flush_younger_o (flush_younger),
.pipeline_stall_o (pipeline_stall)
);
// -------------------------------------------------------------------------
// 对外输出与调试信号。
// -------------------------------------------------------------------------
// 存储器接口:指令地址来自取指 PC;数据地址/写数据/写掩码来自 MEM 级。
assign instr_addr_o = pc_q;
assign data_addr_o = exmem_alu_result_q;
assign data_wdata_o = exmem_write_data_q;
assign data_wstrb_o = (exmem_valid_q && exmem_mem_write_q) ? exmem_wstrb_q : 4'b0;
assign data_we_o = |data_wstrb_o;
// LED 输出与流水线各级 PC/指令回显(用于调试与波形观察)。
assign led_data_o = led_data_q;
assign led_valid_o = led_valid_q;
assign if_pc_o = pc_q;
assign id_pc_o = ifid_valid_q ? ifid_pc_q : 32'b0;
assign ex_pc_o = idex_valid_q ? idex_pc_q : 32'b0;
assign mem_pc_o = exmem_valid_q ? exmem_pc_q : 32'b0;
assign wb_pc_o = memwb_valid_q ? memwb_pc_q : 32'b0;
assign if_ir_o = instr_i;
assign id_ir_o = ifid_valid_q ? ifid_ir_q : 32'b0;
assign ex_ir_o = idex_valid_q ? idex_ir_q : 32'b0;
assign mem_ir_o = exmem_valid_q ? exmem_ir_q : 32'b0;
assign wb_ir_o = memwb_valid_q ? memwb_ir_q : 32'b0;
assign rdin_o = wb_value;
assign mdin_o = exmem_write_data_q;
assign reg_write_o = memwb_valid_q && memwb_reg_write_q;
assign mem_write_o = exmem_valid_q && exmem_mem_write_q;
assign halted_o = halted_q;
assign irq_pending_o = irq_pending;
assign irq_current_level_o = irq_current_level;
assign uepc_o = uepc_value;
assign ustatus_o = ustatus_value;
assign bpb_predict_hit_o = bpb_predict_hit;
assign bpb_predict_taken_o = bpb_predict_taken;
assign bpb_mispredict_o = ex_redirect_valid && idex_branch_q;
// -------------------------------------------------------------------------
// 状态更新。顺序遵循五级流水线:先捕获 MEM/WB、EX/MEM,再处理
// ID/EX 与 IF/ID/PC 的控制。
// -------------------------------------------------------------------------
// 复位时清空所有流水寄存器与 LED/停机标志(寄存器堆、中断状态与
// 性能计数器在各自的子模块内复位)。
always @(posedge clk or posedge reset) begin
if (reset) begin
pc_q <= RESET_PC;
ifid_valid_q <= 1'b0;
ifid_pc_q <= 32'b0;
ifid_ir_q <= 32'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
idex_valid_q <= 1'b0;
idex_pc_q <= 32'b0;
idex_ir_q <= 32'b0;
idex_rs1_value_q <= 32'b0;
idex_rs2_value_q <= 32'b0;
idex_rs1_idx_q <= 5'b0;
idex_rs2_idx_q <= 5'b0;
idex_rd_q <= 5'b0;
idex_imm_q <= 32'b0;
idex_aluop_q <= ALU_ADD;
idex_alu_src_q <= 1'b0;
idex_reg_write_q <= 1'b0;
idex_mem_to_reg_q <= 1'b0;
idex_mem_write_q <= 1'b0;
idex_mem_byte_q <= 1'b0;
idex_wb_sel_q <= 3'b0;
idex_csr_addr_q <= 12'b0;
idex_branch_q <= 1'b0;
idex_beq_q <= 1'b0;
idex_bne_q <= 1'b0;
idex_blt_q <= 1'b0;
idex_bltu_q <= 1'b0;
idex_jal_q <= 1'b0;
idex_jalr_q <= 1'b0;
idex_ecall_q <= 1'b0;
idex_uret_q <= 1'b0;
idex_csr_set_q <= 1'b0;
idex_csr_clear_q <= 1'b0;
idex_csr_write_q <= 1'b0;
idex_pred_taken_q <= 1'b0;
idex_pred_target_q <= 32'b0;
exmem_valid_q <= 1'b0;
exmem_pc_q <= 32'b0;
exmem_ir_q <= 32'b0;
exmem_alu_result_q <= 32'b0;
exmem_alu_result2_q <= 32'b0;
exmem_csr_old_q <= 32'b0;
exmem_write_data_q <= 32'b0;
exmem_rd_q <= 5'b0;
exmem_reg_write_q <= 1'b0;
exmem_mem_to_reg_q <= 1'b0;
exmem_mem_write_q <= 1'b0;
exmem_wstrb_q <= 4'b0;
exmem_wb_sel_q <= 3'b0;
memwb_valid_q <= 1'b0;
memwb_pc_q <= 32'b0;
memwb_ir_q <= 32'b0;
memwb_alu_result_q <= 32'b0;
memwb_alu_result2_q <= 32'b0;
memwb_csr_old_q <= 32'b0;
memwb_mem_data_q <= 32'b0;
memwb_rd_q <= 5'b0;
memwb_reg_write_q <= 1'b0;
memwb_mem_to_reg_q <= 1'b0;
memwb_wb_sel_q <= 3'b0;
led_data_q <= 32'b0;
led_valid_q <= 1'b0;
halted_q <= 1'b0;
end else begin
led_valid_q <= 1'b0;
if (ex_led_event) begin
led_data_q <= ex_src2;
led_valid_q <= 1'b1;
end
if (go_i) halted_q <= 1'b0;
if (ex_halt_event) halted_q <= 1'b1;
// MEM/WB 流水寄存器捕获。
memwb_valid_q <= exmem_valid_q;
memwb_pc_q <= exmem_pc_q;
memwb_ir_q <= exmem_ir_q;
memwb_alu_result_q <= exmem_alu_result_q;
memwb_alu_result2_q <= exmem_alu_result2_q;
memwb_csr_old_q <= exmem_csr_old_q;
memwb_mem_data_q <= data_rdata_i;
memwb_rd_q <= exmem_rd_q;
memwb_reg_write_q <= exmem_reg_write_q;
memwb_mem_to_reg_q <= exmem_mem_to_reg_q;
memwb_wb_sel_q <= exmem_wb_sel_q;
// EX/MEM 流水寄存器捕获。
exmem_valid_q <= idex_valid_q;
exmem_pc_q <= idex_pc_q;
exmem_ir_q <= idex_ir_q;
exmem_alu_result_q <= ex_alu_result;
exmem_alu_result2_q <= ex_alu_result2;
exmem_csr_old_q <= ex_csr_old_value;
exmem_write_data_q <= ex_store_data;
exmem_rd_q <= idex_rd_q;
exmem_reg_write_q <= idex_reg_write_q;
exmem_mem_to_reg_q <= idex_mem_to_reg_q;
exmem_mem_write_q <= idex_mem_write_q;
exmem_wstrb_q <= ex_store_wstrb;
exmem_wb_sel_q <= idex_wb_sel_q;
// 控制优先级:中断 > URET > ecall 停机 > 分支重定向 >
// load-use 停顿 > 正常流水推进。
if (flush_younger || pipeline_stall || halted_q) begin
idex_valid_q <= 1'b0;
idex_pred_taken_q <= 1'b0;
idex_pred_target_q <= 32'b0;
end else begin
idex_valid_q <= ifid_valid_q;
idex_pc_q <= ifid_pc_q;
idex_ir_q <= ifid_ir_q;
idex_rs1_value_q <= d_rs1_value;
idex_rs2_value_q <= d_rs2_value;
idex_rs1_idx_q <= d_src1_idx;
idex_rs2_idx_q <= d_src2_idx;
idex_rd_q <= d_rd;
idex_imm_q <= d_imm;
idex_aluop_q <= d_aluop;
idex_alu_src_q <= d_alu_src;
idex_reg_write_q <= d_reg_write;
idex_mem_to_reg_q <= d_mem_to_reg;
idex_mem_write_q <= d_mem_write;
idex_mem_byte_q <= d_mem_byte;
idex_wb_sel_q <= d_wb_sel;
idex_csr_addr_q <= d_csr_addr;
idex_branch_q <= d_branch;
idex_beq_q <= d_beq;
idex_bne_q <= d_bne;
idex_blt_q <= d_blt;
idex_bltu_q <= d_bltu;
idex_jal_q <= d_jal;
idex_jalr_q <= d_jalr;
idex_ecall_q <= d_ecall;
idex_uret_q <= d_uret;
idex_csr_set_q <= d_csr_set;
idex_csr_clear_q <= d_csr_clear;
idex_csr_write_q <= d_csr_write;
idex_pred_taken_q <= ifid_pred_taken_q;
idex_pred_target_q <= ifid_pred_target_q;
end
if (take_irq) begin
pc_q <= irq_vector;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (ex_uret_redirect) begin
pc_q <= ex_return_pc;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (ex_halt_event) begin
pc_q <= idex_pc_q + 32'd4;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (ex_redirect_valid) begin
pc_q <= ex_redirect_pc;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end else if (pipeline_stall) begin
pc_q <= pc_q;
ifid_valid_q <= ifid_valid_q;
ifid_pc_q <= ifid_pc_q;
ifid_ir_q <= ifid_ir_q;
ifid_pred_taken_q <= ifid_pred_taken_q;
ifid_pred_target_q <= ifid_pred_target_q;
end else if (halted_q) begin
pc_q <= pc_q;
ifid_valid_q <= 1'b0;
end else begin
pc_q <= fetch_next_pc;
ifid_valid_q <= 1'b1;
ifid_pc_q <= pc_q;
ifid_ir_q <= instr_i;
ifid_pred_taken_q <= f_is_branch && bpb_predict_hit && bpb_predict_taken;
ifid_pred_target_q <= bpb_predict_target;
end
end
end
// -------------------------------------------------------------------------
// 性能计数器(只影响统计量,不参与控制)。
// -------------------------------------------------------------------------
cpu21_riscv_perf_counters u_perf_counters (
.clk (clk),
.reset (reset),
.inc_cycle_i (!halted_q),
.stall_i (pipeline_stall),
.flush_i (flush_younger),
.cond_taken_i (idex_valid_q && idex_branch_q && ex_branch_taken),
.cond_valid_i (idex_valid_q && idex_branch_q),
.mispredict_i (ex_redirect_valid),
.uncond_i (idex_valid_q && (idex_jal_q || idex_jalr_q)),
.cycle_count_o (cycle_count_o),
.stall_count_o (stall_count_o),
.bubble_count_o (bubble_count_o),
.conditional_taken_count_o (conditional_taken_count_o),
.unconditional_branch_count_o(unconditional_branch_count_o),
.prediction_success_count_o (prediction_success_count_o),
.prediction_failure_count_o (prediction_failure_count_o)
);
endmodule
`default_nettype wire
+81
View File
@@ -0,0 +1,81 @@
# `cpu21-riscv-4.circ` 的 Verilog 实现
顶层文件:`cpu21_riscv_redirect_int_bpb.v`
该设计对应原电路中的“重定向流水线+中断+分支预测”部分,顶层模块为
`cpu21_riscv_redirect_int_bpb`。为便于单独仿真与调试,各功能单元已拆分为
独立文件,顶层的 ALU、BPB 也已移出,具体见文末“文件与模块划分”。
## 已转换的功能
- 五级流水:IF、ID、EX、MEM、WB。
- EX/MEM 与 MEM/WB 前递,以及 load-use 一拍停顿。
- 条件分支 `beq`、`bne`、`blt`(有符号小于)、`bltu`(无符号小于)在 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` 时把 `a0` 锁存到 `led_data_o`;否则置 `halted_o`,直到 `go_i` 为高。
- `sw` 和 `sb`:输出 `data_wstrb_o`,支持按字节写入。
## 指令编码
表中的 `opcode` 是控制器使用的五位字段 `IR[6:2]`,不是包含最低两位的 7 位原始 opcode。
因此它与标准 RISC-V 指令的低 7 位 opcode 相差右移两位。
| 指令类别 | opcode | 说明 |
| ------------------------------ | -----: | --------------------------------------- |
| R 型 | `0x0c` | add/sub/and/or/xor/slt/sltu/sll/srl/sra |
| I 型 ALU | `0x04` | addi/andi/ori/xori/slti/slli/srli/srai |
| `lw` | `0x00` | `funct3=010` |
| `sw` / `sb` | `0x08` | `funct3=010` / `000` |
| `beq` / `bne` / `blt` / `bltu` | `0x18` | `funct3=000` / `001` / `100` / `110` |
| `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` 分支逻辑。
## 顶层端口约定
- `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 时计入两个气泡。
原 `.circ` 文件中的 ROM、MIPS RAM、按钮、LED 和调试显示器属于 Logisim 外围,不直接搬入 RTL;其功能分别由指令接口、数据接口、`go_i`、`led_*_o` 和调试端口替代。
## 文件与模块划分
| 文件 | 模块 | 职责 |
| -------------------------------- | ------------------------------ | --------------------------------------------------------------- |
| `cpu21_riscv_redirect_int_bpb.v` | `cpu21_riscv_redirect_int_bpb` | 顶层:五级流水寄存器、子模块例化、PC/IF-ID/ID-EX 控制、对外输出 |
| `cpu21_riscv_alu.v` | `cpu21_riscv_alu` | 组合 ALU(`result2` 用于 MUL 高位 / DIVU 余数) |
| `cpu21_bpb_8.v` | `cpu21_bpb_8` | 8 项全相联分支目标缓冲(2 位饱和计数器 + 近似 LRU) |
| `cpu21_riscv_decoder.v` | `cpu21_riscv_decoder` | ID 级组合译码器(操作码 `IR[6:2]`) |
| `cpu21_riscv_regfile.v` | `cpu21_riscv_regfile` | 寄存器堆,`x0` 恒为 0,同周期 WB 写优先旁路 |
| `cpu21_riscv_forward_unit.v` | `cpu21_riscv_forward_unit` | EX 级前递网络(EX/MEM 优先于 MEM/WB) |
| `cpu21_riscv_store_unit.v` | `cpu21_riscv_store_unit` | EX 级存储数据对齐:`sw`/`sb` 的写数据与字节写掩码 |
| `cpu21_riscv_branch_unit.v` | `cpu21_riscv_branch_unit` | EX 级控制流裁决:分支方向/目标、JAL/JALR 重定向 |
| `cpu21_riscv_hazard_unit.v` | `cpu21_riscv_hazard_unit` | load-use 冒险、停顿(stall)与冲刷(flush)判定 |
| `cpu21_riscv_irq_ctrl.v` | `cpu21_riscv_irq_ctrl` | 中断采样与优先级、`ustatus`/`uepc` CSR、嵌套返回栈 |
| `cpu21_riscv_perf_counters.v` | `cpu21_riscv_perf_counters` | 周期/停顿/气泡/分支/预测成功率等统计计数器 |
拆分只调整了模块边界与文件组织,端口、时序与行为完全等价:使用同一 ROM 程序
在 xsim 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支
跳转、预测成败)完全一致。
-233
View File
@@ -1,233 +0,0 @@
# ============================================================================
# cpu21-riscv 流水线 CPU —— 一键重建 Vivado 工程
# FPGA:Nexys4DDR / xc7a100tcsg324-1(Vivado 2020.2 实测:建工程 + 综合均通过)
#
# 目录结构(本脚本所在目录就是工程目录):
# create_project.tcl 本脚本
# srcs/rtl/*.v 处理器 RTL(顶层 cpu21_riscv_redirect_int_bpb)
# 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
#
# 用法:
# 1) 在 Vivado Tcl Shell 里执行: source create_project.tcl
# 或命令行: vivado -mode batch -source create_project.tcl -log create.log
# 2) 脚本会**先自动清理旧工程**再新建:只删生成物(*.xpr / *.srcs / *.runs /
# *.sim / *.cache / *.hw / *.gen / *.ip_user_files / .Xil / xsim.dir /
# vivado*.jou|log|str / webtalk.*),srcs/、tools/、programs/ 一律不动。
# 不想自动清理(比如想在旧工程上追加设置)就把下面的 clean_first 改成 0。
# 3) 工程就地创建:pipelined-cpu.xpr(源文件仍在 srcs/ 下,不会被搬动)。
# 4) 若提示 .xpr 删不掉,说明有 Vivado(含 Hardware Manager)正开着那个工程,
# 关掉它再跑一遍即可。
#
# 脚本用 [info script] 定位自身目录,所以整个文件夹拷到任何路径都能直接用。
# 若想用 Vivado 自动生成的版本,见说明文档“工程分享”一节。
# ============================================================================
set origin_dir [file dirname [file normalize [info script]]]
set proj_name "pipelined-cpu"
set part_name "xc7a100tcsg324-1"
# ---------------------------------------------------------------- 自检
set required [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
"$origin_dir/srcs/board/cpu21_board_rom.vh" \
"$origin_dir/srcs/board/cpu21_board_rom_fast.vh" \
"$origin_dir/srcs/constrs/cpu21_board_top.xdc" \
"$origin_dir/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v" \
"$origin_dir/srcs/tb/tb_no_intr.v" \
"$origin_dir/srcs/tb/tb_board_top.v"]
set missing {}
foreach f $required { if {![file exists $f]} { lappend missing $f } }
if {[llength [glob -nocomplain "$origin_dir/srcs/rtl/*.v"]] == 0} {
lappend missing "$origin_dir/srcs/rtl/*.v"
}
if {[llength $missing] > 0} {
puts "ERROR: 缺少下列文件,请确认 srcs/ 目录完整:"
foreach f $missing { puts " $f" }
return 1
}
# ---------------------------------------------------------------- 清理旧工程
# 一键重建:先删掉旧工程和所有生成目录。工程完全由 srcs/ + 本脚本决定,
# 所以删掉不会丢任何源文件;不想自动清理就把 clean_first 改成 0。
set clean_first 1
if {$clean_first} {
# 必须删掉的工程目录:任何一个删不掉都不能继续(否则 Vivado 会拒绝建工程)
set proj_items [list \
"$origin_dir/$proj_name.xpr" \
"$origin_dir/$proj_name.srcs" \
"$origin_dir/$proj_name.runs" \
"$origin_dir/$proj_name.sim" \
"$origin_dir/$proj_name.cache" \
"$origin_dir/$proj_name.hw" \
"$origin_dir/$proj_name.gen" \
"$origin_dir/$proj_name.ip_user_files" \
"$origin_dir/$proj_name.lpr" \
"$origin_dir/.Xil"]
# 顺带清掉的杂项:删不掉也无所谓(本进程自己会占着 vivado.jou / .log / .str)
set junk_items [list \
"$origin_dir/xsim.dir" \
"$origin_dir/webtalk.jou" \
"$origin_dir/webtalk.log" \
"$origin_dir/usage_statistics_webtalk.html" \
"$origin_dir/usage_statistics_webtalk.xml"]
foreach pat {vivado*.jou vivado*.log vivado*.str} {
foreach f [glob -nocomplain "$origin_dir/$pat"] { lappend junk_items $f }
}
set blocked {}
set removed 0
foreach {group items} [list proj $proj_items junk $junk_items] {
foreach d $items {
if {![file exists $d]} { continue }
if {[catch {file delete -force $d} err]} {
puts " WARNING: 删不掉 [file tail $d]($err)"
if {$group eq "proj"} { lappend blocked [file tail $d] }
} else {
puts " 已删除:[file tail $d]"
incr removed
}
}
}
puts "旧工程清理完成(删除 $removed 项)"
if {[llength $blocked] > 0} {
puts "ERROR: 这些工程目录删不掉:$blocked"
puts " 它们正被另一个 Vivado(GUI / Hardware Manager)占用。"
puts " 请关闭 Vivado 后重跑本脚本。"
return 1
}
} else {
if {[file exists "$origin_dir/$proj_name.xpr"] || [file exists "$origin_dir/$proj_name.runs"]} {
puts "ERROR: 工程已存在且 clean_first = 0:$origin_dir/$proj_name.xpr"
puts " 请先删除它,或把 clean_first 改成 1 让脚本自动清理。"
return 1
}
}
# ---------------------------------------------------------------- 建工程
# 只用 -part 建工程,**不依赖"板卡文件"**:Vivado 默认不带 Nexys4DDR 的 board 文件,
# 所以 GUI 新建向导的 Boards 页里找不到它;board 只影响"选板/板级预设",
# 不影响综合与实现(引脚全在 srcs/constrs/*.xdc 里显式约束)。
# 若以后装了 Digilent 板卡文件(data/boards/board_files/nexys4-ddr),想按板卡建:
# create_project $proj_name $origin_dir -part $part_name \
# -board_part digilentinc.com:nexys4ddr:part0:1.1
# (那样工程会记上板卡,别人没装板卡文件就打不开了,所以默认不这么写。)
# -force:万一还有残留目录(例如被占用的 .runs)也能覆盖,不至于卡死。
create_project $proj_name $origin_dir -part $part_name -force
# 设计源文件:处理器 RTL + 板级顶层 + 片内 ROM 初始化
add_files -norecurse -fileset sources_1 [glob "$origin_dir/srcs/rtl/*.v"]
add_files -norecurse -fileset sources_1 [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
"$origin_dir/srcs/board/cpu21_board_rom.vh"]
set_property file_type {Verilog Header} \
[get_files "$origin_dir/srcs/board/cpu21_board_rom.vh"]
# 默认跑哪个内置程序(同一时间只应启用一个):
# use_intrupt_sllsb = 1 -> 课程新版「多级中断测试 + 附加 SLL/SB/BLTU」
# (programs/risc-v-intrupt-sllsb.asm / .hex)
# use_intrupt_ccab = 1 -> 上一版中断测试(programs/risc-v-intrupt-ccab.*)
# use_saolei = 1 -> 扫雷(saolei/tools/build.ps1 生成)
# 三个都为 0 -> 走马灯 demo
set use_intrupt_sllsb 0
set use_intrupt_ccab 1
set use_saolei 0
# 中断测试程序的 ROM 初始化。
set intrupt_ccab_rom "$origin_dir/srcs/board/risc-v-intrupt-ccab.vh"
if {[file exists $intrupt_ccab_rom]} {
add_files -norecurse -fileset sources_1 [list $intrupt_ccab_rom]
set_property file_type {Verilog Header} [get_files $intrupt_ccab_rom]
} else {
puts "NOTE: 还没有 $intrupt_ccab_rom"
}
# 课程新版中断测试(附加 SLL/SB/BLTU)的 ROM 初始化。
set intrupt_sllsb_rom "$origin_dir/srcs/board/risc-v-intrupt-sllsb.vh"
if {[file exists $intrupt_sllsb_rom]} {
add_files -norecurse -fileset sources_1 [list $intrupt_sllsb_rom]
set_property file_type {Verilog Header} [get_files $intrupt_sllsb_rom]
} else {
puts "NOTE: 还没有 $intrupt_sllsb_rom(用 tools/hex2vh.py 生成)"
}
# 扫雷程序的 ROM 初始化(由 saolei/tools/build.ps1 生成,可能还不存在)。
# 综合/仿真时定义宏 SAOLEI_ROM 就会用它替换上面的跑马灯 ROM。
set saolei_rom "$origin_dir/srcs/board/cpu21_saolei_rom.vh"
if {[file exists $saolei_rom]} {
add_files -norecurse -fileset sources_1 [list $saolei_rom]
set_property file_type {Verilog Header} [get_files $saolei_rom]
} else {
puts "NOTE: 还没有 $saolei_rom —— 需要先跑 saolei/tools/build.ps1 生成"
}
# 关键:sources_1 里定义对应宏,cpu21_board_top.v 的 `ifdef 链才会选中那个 ROM
# (宏只影响综合用的 sources_1;仿真 fileset 仍各自用 tb 自带的 ROM)
if {$use_intrupt_sllsb && [file exists $intrupt_sllsb_rom]} {
set_property verilog_define {RISC_V_INTRUPT_SLLSB_ROM} [get_filesets sources_1]
puts "NOTE: sources_1 已定义 RISC_V_INTRUPT_SLLSB_ROM —— 上板跑「中断测试 + SLL/SB/BLTU」"
} elseif {$use_intrupt_ccab && [file exists $intrupt_ccab_rom]} {
set_property verilog_define {RISC_V_INTRUPT_CCAB_ROM} [get_filesets sources_1]
puts "NOTE: sources_1 已定义 RISC_V_INTRUPT_CCAB_ROM —— 上板跑 risc-v-intrupt-ccab"
} elseif {$use_saolei && [file exists $saolei_rom]} {
set_property verilog_define {SAOLEI_ROM} [get_filesets sources_1]
puts "NOTE: sources_1 已定义 SAOLEI_ROM —— 上板跑扫雷;想跑走马灯把本脚本里 use_saolei 改成 0"
} else {
puts "NOTE: 未定义 SAOLEI_ROM —— 上板跑走马灯 demo"
}
# 约束(cpu21_board_top.xdc 是引脚与分频时钟约束)
add_files -norecurse -fileset constrs_1 [glob "$origin_dir/srcs/constrs/*.xdc"]
# 仿真文件:测试平台 + ROM 镜像 + 快速版 ROM 头文件
add_files -norecurse -fileset sim_1 [list \
"$origin_dir/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v" \
"$origin_dir/srcs/tb/tb_no_intr.v" \
"$origin_dir/srcs/tb/tb_board_top.v" \
"$origin_dir/srcs/board/cpu21_board_rom_fast.vh"]
add_files -norecurse -fileset sim_1 [glob "$origin_dir/srcs/rom/*.hex"]
set_property file_type {Verilog Header} \
[get_files "$origin_dir/srcs/board/cpu21_board_rom_fast.vh"]
# 第二个仿真 fileset:无中断测试平台(tb_no_intr)
create_fileset -simset sim_2
add_files -norecurse -fileset sim_2 [list \
"$origin_dir/srcs/tb/tb_no_intr.v" \
"$origin_dir/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v"]
# ---------------------------------------------------------------- 顶层与顺序
set_property top cpu21_board_top [get_filesets sources_1]
set_property top tb_cpu21_riscv_redirect_int_bpb [get_filesets sim_1]
set_property top tb_no_intr [get_filesets sim_2]
update_compile_order -fileset sources_1
update_compile_order -fileset sim_1
update_compile_order -fileset sim_2
# GUI 仿真默认只跑 1000 ns,这里改成跑完(否则看起来像卡住)
foreach fs {sim_1 sim_2} {
catch { set_property -name {xsim.simulate.runtime} -value {all} -objects [get_filesets $fs] }
}
# ---------------------------------------------------------------- 结果汇总
puts "工程已创建:$origin_dir/$proj_name.xpr"
puts " sources_1 顶层 = cpu21_board_top(上板:时钟分频 + 片内 ROM/RAM + 引脚)"
if {$use_intrupt_sllsb && [file exists $intrupt_sllsb_rom]} {
set selected_program {risc-v-intrupt-sllsb (RISC_V_INTRUPT_SLLSB_ROM) 中断+SLL/SB/BLTU}
} elseif {$use_intrupt_ccab && [file exists $intrupt_ccab_rom]} {
set selected_program {risc-v-intrupt-ccab (RISC_V_INTRUPT_CCAB_ROM)}
} elseif {$use_saolei && [file exists $saolei_rom]} {
set selected_program {扫雷 (SAOLEI_ROM)}
} else {
set selected_program {走马灯 demo}
}
puts " sources_1 程序 = $selected_program"
puts " sim_1 顶层 = tb_cpu21_riscv_redirect_int_bpb(中断/分支预测)"
puts " sim_2 顶层 = tb_no_intr(指令集测试,ROM 镜像在 srcs/rom/)"
puts " 下一步:Run Synthesis → Run Implementation → Generate Bitstream,或 Run Simulation"
puts " 提示:CPU 时钟 = 100 MHz / CLK_DIV(默认 2)= 50 MHz;改分频比时"
puts " cpu21_board_top.v 的 CLK_DIV 与 cpu21_board_top.xdc 的 -divide_by 要一起改。"
-170
View File
@@ -1,170 +0,0 @@
# ============================================================================
# create_project_saolei.tcl —— 一键重建【扫雷专用】Vivado 工程
# FPGA:Nexys4DDR / xc7a100tcsg324-1(Vivado 2020.2)
#
# 与 create_project.tcl 的区别(那个是"通用/可切换程序"的脚本):
# * 只装配扫雷需要的东西:rtl/*.v + board/cpu21_board_top.v + 扫雷 ROM 头文件
# * sources_1 与 sim_1 都固定定义宏 **SAOLEI_ROM**(不用再改任何开关)
# * 工程名是独立的 pipelined-cpu-saolei,跟通用工程 pipelined-cpu 互不影响,
# 两个工程可以并存;本脚本只清理自己的生成物。
# * sim_1 顶层直接就是 tb_saolei(GUI 里 Run Simulation 就能出画面 dump)
#
# 用法(在 Vivado Tcl Shell 里):
# source create_project_saolei.tcl
# 或命令行:
# vivado -mode batch -source create_project_saolei.tcl -log create_saolei.log
#
# 前置条件:扫雷的 ROM 镜像必须先由 C 程序生成
# cd saolei
# powershell -ExecutionPolicy Bypass -File tools\build.ps1 `
# -Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
# → 产出 saolei/build/saolei.hex 与 pipelined-cpu/srcs/board/cpu21_saolei_rom.vh
# (改了 saolei/sw/*.c 之后要重跑这条命令,再综合/再跑仿真)
#
# 不想自动清理旧工程:把下面的 clean_first 改成 0。
# 想建完就顺手综合:把 run_synth 改成 1(2020.2 上约 1~2 分钟)。
#
# 附:没有 VGA 显示器时怎么"看/玩"扫雷
# 看:本工程的 sim_1 跑 tb_saolei,会把显存 dump 成文本;再用
# py saolei\tools\fb2png.py <dump.txt> <out.png> 还原成 640x480 画面。
# 玩:见 pipelined-cpu/tools/play_saolei.tcl(命令行交互式仿真,一步一截图)。
# ============================================================================
set origin_dir [file dirname [file normalize [info script]]]
set proj_name "pipelined-cpu-saolei"
set part_name "xc7a100tcsg324-1"
# ---------------------------------------------------------------- 自检
set saolei_rom "$origin_dir/srcs/board/cpu21_saolei_rom.vh"
set required [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
"$origin_dir/srcs/constrs/cpu21_board_top.xdc" \
"$origin_dir/srcs/tb/tb_saolei.v"]
if {[llength [glob -nocomplain "$origin_dir/srcs/rtl/*.v"]] == 0} {
lappend required "$origin_dir/srcs/rtl/*.v"
}
set missing {}
foreach f $required { if {![file exists $f]} { lappend missing $f } }
if {[llength $missing] > 0} {
puts "ERROR: 缺少下列文件,请确认 srcs/ 目录完整:"
foreach f $missing { puts " $f" }
return 1
}
if {![file exists $saolei_rom]} {
puts "ERROR: 还没有扫雷 ROM 镜像:$saolei_rom"
puts " 先在 saolei/ 目录跑 tools\\build.ps1 生成它(见本脚本头部注释)。"
return 1
}
# ---------------------------------------------------------------- 清理旧工程
# 和 create_project.tcl 同一套策略:只删生成物,srcs/ 等源文件一律不动。
set clean_first 1
set run_synth 0
set synth_jobs 4
if {$clean_first} {
set proj_items [list \
"$origin_dir/$proj_name.xpr" \
"$origin_dir/$proj_name.srcs" \
"$origin_dir/$proj_name.runs" \
"$origin_dir/$proj_name.sim" \
"$origin_dir/$proj_name.cache" \
"$origin_dir/$proj_name.hw" \
"$origin_dir/$proj_name.gen" \
"$origin_dir/$proj_name.ip_user_files" \
"$origin_dir/$proj_name.lpr" \
"$origin_dir/.Xil"]
set junk_items [list \
"$origin_dir/xsim.dir" \
"$origin_dir/webtalk.jou" \
"$origin_dir/webtalk.log" \
"$origin_dir/usage_statistics_webtalk.html" \
"$origin_dir/usage_statistics_webtalk.xml"]
foreach pat {vivado*.jou vivado*.log vivado*.str} {
foreach f [glob -nocomplain "$origin_dir/$pat"] { lappend junk_items $f }
}
set blocked {}
set removed 0
foreach {group items} [list proj $proj_items junk $junk_items] {
foreach d $items {
if {![file exists $d]} { continue }
if {[catch {file delete -force $d} err]} {
puts " WARNING: 删不掉 [file tail $d]($err)"
if {$group eq "proj"} { lappend blocked [file tail $d] }
} else {
puts " 已删除:[file tail $d]"
incr removed
}
}
}
puts "旧工程清理完成(删除 $removed 项,只动 $proj_name.* 与自己产生的临时文件)"
if {[llength $blocked] > 0} {
puts "ERROR: 这些工程目录删不掉:$blocked"
puts " 它们正被另一个 Vivado(GUI / Hardware Manager)占用,关掉后重跑。"
return 1
}
} else {
if {[file exists "$origin_dir/$proj_name.xpr"] || [file exists "$origin_dir/$proj_name.runs"]} {
puts "ERROR: 工程已存在且 clean_first = 0:$origin_dir/$proj_name.xpr"
puts " 请先删掉它,或把 clean_first 改成 1。"
return 1
}
}
# ---------------------------------------------------------------- 建工程
# 只用 -part,不依赖 Digilent 板卡文件(引脚全在 srcs/constrs/*.xdc 里)。
create_project $proj_name $origin_dir -part $part_name -force
# 设计源文件:处理器 RTL + 板级顶层 + 扫雷 ROM
add_files -norecurse -fileset sources_1 [glob "$origin_dir/srcs/rtl/*.v"]
add_files -norecurse -fileset sources_1 [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
$saolei_rom]
set_property file_type {Verilog Header} [get_files $saolei_rom]
# 关键:两个 fileset 都定义 SAOLEI_ROM,cpu21_board_top.v 的 `ifdef 链才会选中扫雷 ROM。
# (sources_1 管综合/上板,sim_1 管 GUI 仿真;sim_1 不定义的话会跑成走马灯。)
set_property verilog_define {SAOLEI_ROM} [get_filesets sources_1]
set_property verilog_define {SAOLEI_ROM} [get_filesets sim_1]
# 约束(引脚 + 时钟分频)
add_files -norecurse -fileset constrs_1 [glob "$origin_dir/srcs/constrs/*.xdc"]
# 仿真:扫雷调试台。POINTER_SAMPLE_DIV 由 tb 自己 defparam 压小,上板仍是 50000。
add_files -norecurse -fileset sim_1 [list "$origin_dir/srcs/tb/tb_saolei.v"]
# ---------------------------------------------------------------- 顶层与顺序
set_property top cpu21_board_top [get_filesets sources_1]
set_property top tb_saolei [get_filesets sim_1]
update_compile_order -fileset sources_1
update_compile_order -fileset sim_1
# GUI 仿真默认只跑 1000 ns,这里改成跑完(否则看起来像卡住)
catch { set_property -name {xsim.simulate.runtime} -value {all} -objects [get_filesets sim_1] }
# ---------------------------------------------------------------- 可选:顺手综合
if {$run_synth} {
puts "开始综合 synth_1(run_synth = 1)..."
reset_run synth_1
launch_runs synth_1 -jobs $synth_jobs
wait_on_run synth_1
if {[get_property PROGRESS [get_runs synth_1]] ne "100%"} {
puts "ERROR: 综合未完成,状态 = [get_property STATUS [get_runs synth_1]]"
return 1
}
puts "综合完成:0 error 即可 Run Implementation → Generate Bitstream。"
}
# ---------------------------------------------------------------- 结果汇总
puts ""
puts "工程已创建:$origin_dir/$proj_name.xpr"
puts " sources_1 顶层 = cpu21_board_top,程序 = 扫雷(已定义 SAOLEI_ROM)"
puts " sources_1 ROM = [file tail $saolei_rom]"
puts " sim_1 顶层 = tb_saolei(跑仿真会打印显存 dump,用 fb2png.py 还原成 PNG)"
puts " 约束 = [file tail [lindex [glob "$origin_dir/srcs/constrs/*.xdc"] 0]]"
puts " 下一步:Run Synthesis → Run Implementation → Generate Bitstream"
puts " (本工程已固定扫雷,不用再改 create_project.tcl 里的三个开关)"
puts " 上板操作:BTNU/D/L/R 移动光标,BTNC = 左键,SW3 = 插旗模式"
puts " 注意:CPU 时钟 = 100 MHz / CLK_DIV(默认 2) = 50 MHz;改分频比时"
puts " cpu21_board_top.v 的 CLK_DIV 与 cpu21_board_top.xdc 的 -divide_by 要一起改。"
-22
View File
@@ -1,22 +0,0 @@
# 上板演示程序(Nexys4DDR):通过 MMIO 写 LED 数据寄存器,做 16 位跑马灯。
# LED_ADDR = 0x0000_1000(与 cpu21_board_top.v / cpu21_mmio.v 的默认参数一致)。
# 用 tools/hex2vh.py 把它转成 cpu21_board_rom.vh,供综合时初始化片内 ROM。
lui t0, 0x1 # t0 = LED_ADDR = 0x00001000
addi t1, zero, 1 # 跑马灯初值:bit0
main_loop:
sw t1, 0(t0) # MMIO 写 LED -> 板载 LED[15:0]
slli t1, t1, 1 # 左移一位
lui t4, 0x8 # t4 = 0x00008000
bltu t4, t1, restart # 移出 16 位后重新开始
jal zero, delay
restart:
addi t1, zero, 1
delay: # 软件延时,让人眼能看到跑马灯(50 MHz 下约 20 ms)
lui t2, 0x7a # 延时计数 0x0007A000 ≈ 5e5 次循环
addi t3, zero, 1
delay_loop:
sub t2, t2, t3
bne t2, zero, delay_loop
jal zero, main_loop
@@ -1,25 +0,0 @@
# 上板演示程序的**快速版**(仅仿真用):与 board_demo.asm 只差延时循环的初值——
# 从 `lui t2, 0x7a`(≈ 20 ms/步)改成 `addi t2, zero, 64`(≈ 2.6 µs/步 @50 MHz),
# 这样一次仿真(tb_board_top,300 µ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
# 综合时用 -d BOARD_ROM_FAST 才会选到它(见 srcs/board/cpu21_board_top.v)。
lui t0, 0x1 # t0 = LED_ADDR = 0x00001000
addi t1, zero, 1 # 跑马灯初值:bit0
main_loop:
sw t1, 0(t0) # MMIO 写 LED -> 板载 LED[15:0]
slli t1, t1, 1 # 左移一位
lui t4, 0x8 # t4 = 0x00008000
bltu t4, t1, restart # 移出 16 位后重新开始
jal zero, delay
restart:
addi t1, zero, 1
delay: # 短延时(仿真用,人眼看不见)
addi t2, zero, 64 # 延时计数 64 次循环
addi t3, zero, 1
delay_loop:
sub t2, t2, t3
bne t2, zero, delay_loop
jal zero, main_loop
-110
View File
@@ -1,110 +0,0 @@
# 指令扩展验证程序:每条被测指令的结果都通过 MMIO 写 LED 输出,最后用 halt 停机。
# 期望输出顺序见每行注释(共 24 次输出),同时保存在同目录 isa_test.expected(每行一项 LED 值)。
#
# 用法(在仓库根目录执行):
# python tools/rv_asm.py testbench/programs/isa_test.asm <仿真工作目录>/risc-v-benchmark_ccab.hex
# 再用 testbench/tb_no_intr.v 仿真;hex 命名为 risc-v-benchmark_ccab.hex 时会被测试
# 平台优先从仿真工作目录载入(参见 tb_no_intr.v 的 ROM 探测顺序)。
# print / halt 是 tools/rv_asm.py 提供的双字伪指令(print 会占用 t6):
# print = lui t6,0x1000>>12 + sw a0,0(t6)(写 MMIO 的 LED 寄存器);
# halt = addi a7,zero,10 + ecall(ecall 停机)。
# ---- sltiu:立即数符号扩展后做无符号比较 ----
addi t0, zero, 1
addi t1, zero, -1 # t1 = 0xffffffff
sltiu a0, t0, -1 # 1 <u 0xffffffff -> 00000001
print
sltiu a0, t1, 1 # 0xffffffff <u 1 -> 00000000
print
# ---- xor / sll / srl / sra(原有指令回归)----
addi t0, zero, 0xff
addi t1, zero, -1
xor a0, t0, t1 # 0xff ^ 0xffffffff -> ffffff00
print
addi t0, zero, 1
addi t1, zero, 31
sll a0, t0, t1 # 1 << 31 -> 80000000
print
addi t0, zero, -1
addi t1, zero, 28
srl a0, t0, t1 # 逻辑右移 -> 0000000f
print
sra a0, t0, t1 # 算术右移 -> ffffffff
print
# ---- mul:无符号乘积低 32 位 ----
addi t0, zero, 1234
addi t1, zero, 567
mul a0, t0, t1 # 1234*567 = 0x000aad1e
print
# ---- mulh / mulhu / mulhsu:64 位乘积的高 32 位 ----
addi t0, zero, -1 # 有符号 -1 / 无符号 0xffffffff
addi t1, zero, -1
mulh a0, t0, t1 # (-1)*(-1)=1 -> 00000000
print
mulhu a0, t0, t1 # 0xffffffff^2 -> fffffffe
print
addi t0, zero, -1
addi t1, zero, 2
mulhsu a0, t0, t1 # (-1)*2=-2 -> ffffffff
print
# ---- bltu:0 <u 0xffffffff 成立(按有符号比较则不成立)----
addi t0, zero, -1
bltu zero, t0, bltu_ok
addi a0, zero, 0x101 # 不应执行
print
bltu_ok:
addi a0, zero, 0x601 # 00000601
print
# ---- bge:有符号 >= ,(-1) >= 0 不成立,应走 fall-through ----
addi t0, zero, -1
bge t0, zero, bge_bad
addi a0, zero, 0x602 # 00000602
print
jal zero, bge_done
bge_bad:
addi a0, zero, 0x102 # 不应执行
print
bge_done:
# ---- bgeu:无符号 >= ,0xffffffff >=u 0 成立 ----
addi t0, zero, -1
bgeu t0, zero, bgeu_ok
addi a0, zero, 0x103 # 不应执行
print
bgeu_ok:
addi a0, zero, 0x603 # 00000603
print
# ---- bge 循环:从 -3 递增到 0(共 4 次输出)----
addi s1, zero, -3
bge_loop:
add a0, zero, s1
print # fffffffd / fffffffe / ffffffff / 00000000
addi s1, s1, 1
bge zero, s1, bge_loop
# ---- bgeu 循环:3 -> 1(共 3 次输出)----
addi t0, zero, 3
bgeu_loop:
add a0, zero, t0
print # 00000003 / 00000002 / 00000001
addi t0, t0, -1
addi t2, zero, 1
bgeu t0, t2, bgeu_loop
# ---- bltu 循环:4 -> 1(共 4 次输出)----
addi s1, zero, 4
bltu_loop:
add a0, zero, s1
print # 00000004 / 00000003 / 00000002 / 00000001
addi s1, s1, -1
bltu zero, s1, bltu_loop
halt
-24
View File
@@ -1,24 +0,0 @@
00000001
00000000
ffffff00
80000000
0000000f
ffffffff
000aad1e
00000000
fffffffe
ffffffff
00000601
00000602
00000603
fffffffd
fffffffe
ffffffff
00000000
00000003
00000002
00000001
00000004
00000003
00000002
00000001
-21
View File
@@ -1,21 +0,0 @@
# LUI 指令测试:依次把 lui 的结果写到 MMIO 的 LED 寄存器,最后停机。
# 期望输出:12345000 / fffff000 / 80000000 / 12345678,随后停机(halted=1)。
# 用法:python tools/rv_asm.py testbench/programs/lui_test.asm <仿真工作目录>/risc-v-benchmark_ccab.hex
lui t0, 0x12345 # t0 = 0x12345000
add a0, zero, t0
print # MMIO 写 LED -> 12345000
lui t0, 0xfffff # t0 = 0xfffff000
add a0, zero, t0
print # -> fffff000
lui t0, 0x80000 # t0 = 0x80000000
add a0, zero, t0
print # -> 80000000
lui t0, 0x12345 # 常见用法 lui+addi:0x12345678
addi t0, t0, 0x678
add a0, zero, t0
print # -> 12345678
halt # 停机
-4
View File
@@ -1,4 +0,0 @@
12345000
fffff000
80000000
12345678
-25
View File
@@ -1,25 +0,0 @@
# MMIO 验证程序:写 LED 寄存器(0x0000_1000)-> 读回 -> 字节写 -> 再读回。
# 期望 LED 输出顺序(共 5 次,见同目录 mmio_test.expected):
# ① 12345678 sw 0x12345678
# ② 12345678 lw 读回后 print(print = lui t6 + sw a0,0(t6),本身就是一次 MMIO 写)
# ③ ffffffff sw 0xffffffff
# ④ ffff7fff sb 0x7f 覆盖 led[15:8]
# ⑤ ffff7fff 读回后 print
# 用法:python tools/rv_asm.py testbench/programs/mmio_test.asm <仿真工作目录>/risc-v-benchmark_ccab.hex
lui t0, 0x1 # t0 = LED_ADDR = 0x00001000
lui t1, 0x12345
addi t1, t1, 0x678 # t1 = 0x12345678
sw t1, 0(t0) # ① MMIO 写 LED
lw a0, 0(t0) # MMIO 读回
print # ② 应为 12345678
addi t2, zero, -1 # t2 = 0xffffffff
sw t2, 0(t0) # ③ 应为 ffffffff
addi t3, zero, 0x7f
sb t3, 1(t0) # 字节写:led[15:8] = 0x7f
lw a0, 0(t0) # 读回
print # ④ 应为 ffff7fff
halt
@@ -1,5 +0,0 @@
12345678
12345678
ffffffff
ffff7fff
ffff7fff
-95
View File
@@ -1,95 +0,0 @@
# 乘除法(多周期单元)测试:每个结果都写给 MMIO 的 LED 寄存器,供测试平台比对。
# 期望输出顺序见同目录 muldiv_test.expected(共 13 个值)。
#
# ① mul 7 * 6 = 42 -> 0000002a
# ② mul -5 * 4 = -20 -> ffffffec
# ③ mulh 0x40000000 * 4 高 32 位 -> 00000001
# ④ mulhu 0xffffffff * 2 高 32 位 -> 00000001
# ⑤ mulhsu -1 * 0xffffffff 高 32 位 -> ffffffff
# ⑥ divu 100 / 7 = 14 -> 0000000e
# ⑦ remu 100 % 7 = 2 -> 00000002
# ⑧ divu 0xffffffff / 2 = 2147483647 -> 7fffffff
# ⑨ divu 12345 / 0 除零约定 -> ffffffff
# ⑩ remu 12345 % 0 除零约定 -> 00003039
# ⑪ remu 0xffffffff % 16 = 15 -> 0000000f
# ⑫ mul 0x12345678 * 16 低 32 位 -> 23456780
# ⑬ mulh 0x12345678 * 0x1000 高 32 位 -> 00000123
#
# 用法:python tools/rv_asm.py testbench/programs/muldiv_test.asm testbench/rom/muldiv_test.hex
# ① mul:有符号乘法的低 32 位
addi t1, zero, 7
addi t2, zero, 6
mul a0, t1, t2
print
# ② mul:负乘数(低 32 位)
addi t1, zero, -5
addi t2, zero, 4
mul a0, t1, t2
print
# ③ mulh:正数相乘,高 32 位不为 0
lui t1, 0x40000
addi t2, zero, 4
mulh a0, t1, t2
print
# ④ mulhu:无符号高位(0xffffffff * 2 = 0x1_fffffffe)
addi t1, zero, -1
addi t2, zero, 2
mulhu a0, t1, t2
print
# ⑤ mulhsu:有符号 x 无符号(-1 * 4294967295 = 0xffffffff_00000001)
addi t1, zero, -1
addi t2, zero, -1
mulhsu a0, t1, t2
print
# ⑥ divu:普通除法
addi t1, zero, 100
addi t2, zero, 7
divu a0, t1, t2
print
# ⑦ remu:普通取余
remu a0, t1, t2
print
# ⑧ divu:无符号大数
addi t1, zero, -1
addi t2, zero, 2
divu a0, t1, t2
print
# ⑨ divu:除零(商为全 1)
lui t1, 0x3
addi t1, t1, 0x39 # t1 = 12345
addi t2, zero, 0
divu a0, t1, t2
print
# ⑩ remu:除零(余数为被除数)
remu a0, t1, t2
print
# ⑪ remu:0xffffffff % 16
addi t1, zero, -1
addi t2, zero, 16
remu a0, t1, t2
print
# ⑫ mul:0x12345678 * 16 的低 32 位
lui t1, 0x12345
addi t1, t1, 0x678
addi t2, zero, 16
mul a0, t1, t2
print
# ⑬ mulh:0x12345678 * 0x1000 的高 32 位
lui t2, 0x1
mulh a0, t1, t2
print
halt
@@ -1,13 +0,0 @@
0000002a
ffffffec
00000001
00000001
ffffffff
0000000e
00000002
7fffffff
ffffffff
00003039
0000000f
23456780
00000123
@@ -1,498 +0,0 @@
# =============================================================================
# 注意:上板/仿真真正使用的镜像是 programs/risc-v-intrupt-ccab.hex(270 字),
# 本文件是课程原始清单。由 tools/adapt_intrupt_ccab.py 生成 ROM 时会做六处适配:
# 1) 三个 ISR 里的 "addi a7,zero,10 + ecall" 会让这颗 CPU 停机(a7 != 34 的
# ecall 是"停机等 go_i"的约定),中断服务程序跑到那里就卡住、后面的中断显示
# 循环永远不执行 -> ROM 里换成两条空操作;
# 2) 原来的 "addi a7,zero,34 + ecall" 显示改成写 MMIO 的 LED 寄存器(0x1000),
# 并且每个显示点都改为调用 DISP_DLY 加约 10 ms 延时 —— 否则 50 MHz 下刷新
# 太快,数码管的动态扫描根本分辨不出来,只会看到一片亮;
# 3) DISP_DLY 追加在程序末尾(0x438),因此所有指令地址、特别是三个中断入口
# 0x0AC / 0x1E4 / 0x308 都保持不变;
# 4) **hex 里 ISR2 / ISR3 的现场恢复(出栈)顺序被写乱了**(本清单是正确的,
# ISR1 的 hex 也是正确的)。ISR3 把 s1 从 s3 的槽位弹出,于是 s1 被写成
# s3 保存的 0xFFFFF111;ISR3 返回后,正在跑 BLT 测试的 ISR2 的循环计数 s1
# 就变成 -3823,循环要跑约 170 万周期才退出,表现就是"二级/三级中断返回
# 之后卡住";ISR2 的 s0/s2/a0/a7/s1/t3/t1 也全部错位。适配脚本按本清单的
# 正确 LIFO 顺序改回 12 条 lw 的目标寄存器(字数不变、地址不动)。
# 5) **ISR3 的 SB 测试用了它自己没有保存的寄存器**(t1 当地址、t3 当计数、
# s2 当增量)。ISR3 的现场只保存了 s1/a7/a0/s0/s3/s4/s5/s6/uepc,所以:
# 若 IRQ3 直接抢占正在跑 DIVU 测试的 ISR1(ISR1 用 t3 递减计数),ISR1 的
# 计数会被改掉;ISR1 恢复后 `addi t3,t3,-1` 得到负数,`bne t3,zero` 永远
# 成立,DIVU 循环再也回不来 —— 表现就是“拨了 SW0 再拨 SW2 之后卡住”;
# 主程序的跑马灯步长 t1 也会被改掉。适配脚本把这 14 处寄存器域改成 ISR3
# 自己保存的 s4/s5/s6(SB 测试结束后紧接着的 IntLoop3 会重新给它们赋值,
# 所以这样换是安全的),字数与地址不变。
# 6) **ISR 不能破坏被打断代码的 ra**:三个 ISR 都保存 s1/a7/a0/s0/s3/s4/s5/s6,
# 但都没有保存 ra,而它们要调用显示例程,"jal ra,DISP_DLY" 会把 ra 写成 ISR
# 内部的返回地址。如果中断恰好落在被打断代码依赖 ra 的位置(典型就是主程序
# 正停在 DISP_DLY 末尾的 jalr zero,0(ra) 那一条上),中断返回后就会跳进 ISR
# 代码里,一路执行到 ISR 结尾的 csrrci zero,0x4,1,把 MIE 永久清零:表现
# 就是“跑一阵子之后拨开关再也不响应,只能复位”。窗口只有 1~2 拍,平时很
# 难遇到(仿真里把延时改到极小才会碰上)。适配脚本给 ISR 另加一份显示例程
# DISP_DLY_ISR(0x458),链接寄存器改用 a0、显示值走 a7(这两个寄存器三个
# ISR 都保存/恢复),于是 ISR 完全不碰 ra;ISR 的显示点由 "jal ra,DISP_DLY +
# nop" 换成 "add a7,a0,zero + jal a0,DISP_DLY_ISR",仍是 2 字换 2 字,
# 字数与地址不变。
# =============================================================================
.text
addi sp, zero, 2000 #内存堆栈初始化
#############################################################
#走马灯测?,测试addi,andi,slli,srli,srai,or,ori,nor,ecall LED按走马灯方式来回显示数据
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #走马灯计数
zmd_loop:
addi s0, s0, 1 #计算下一个走马灯的数
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #走马灯左移
or s3, s3, s0
add a0,zero,s3 # display s3
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #计算下一个走马灯的数
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #走马灯右移
or s3, s3, s0
add a0,zero,s3 # display s3
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram1:
sw t0, 0(sp) #保护t0现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# DIVU测试
addi t0,zero,2 # /2
addi s1,zero, 0x11
slli s1,s1,8
addi s1,s1,0x11
slli s1,s1,16
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t3,zero,28 #ѭ������
divu_branch:
divu s1,s1,t0 #����ָ��
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t3,t3, -1
bne t3,zero,divu_branch #ѭ��
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,1 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop1
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t0, 0(sp) #恢复t0现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram2:
sw t1, 0(sp) #保护t1现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw s2, 0(sp) #保护s2现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# BLT测试
addi s1,zero,-15 #初始值
blt_branch:
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi s1,s1,1
blt s1,zero,blt_branch #当前指令
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,2 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop2
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp) #恢复s0现场
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s2, 0(sp) #恢复s2现场
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t1, 0(sp) #恢复t1现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram3:
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# SB测试
# 注:ROM 镜像里下面这段的 t1/t3/s2 已改成 ISR3 自己保存的 s4/s5/s6
# (t1 当地址 -> s4,t3 当计数 -> s5,s2 当增量 -> s6),避免抢占 ISR1
# 时破坏 ISR1 的 DIVU 循环计数,详见文件头部第 5 项。
addi t1,zero,0 #init_addr
addi t3,zero,32 #counter
#sb写入 01,02,03,04
addi s1,zero, 0x00 #
addi s2,zero, 0x01 #
sb_store:
sb s1,(t1)
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
add s1,s1,s2 #data +1
addi t1,t1,1 # addr ++
addi t3,t3,-1 #counter
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0 # addr
sb_branch:
lw s1,(t1) #读出数据
add a0,zero,s1
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
addi t1,t1,4
addi t3,t3, -1
bne t3,zero,sb_branch
nop # 原为 addi a7,zero,10 + ecall:这颗 CPU 会停机,改成空操作
################# 中断服务
addi s6,zero,3 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
jal ra, DISP_DLY # 显示 a0(写 LED 寄存器 0x1000)并延时约 10 ms
nop # 占位:保持每个显示点仍是 2 个字,指令地址不变
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop3
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
################ 中断返回
uret #同步开中断,uepc-->pc
# -----------------------------------------------------------------------------
# DISP_DLY —— 显示 + 延时(对应 ROM 末尾 0x438 起的 8 条指令;11 个显示点都用
# "jal ra, DISP_DLY" 调用它)
#
# sw ra, 256(sp) # 保存返回地址(故意存在 sp+256,见下面的说明)
# lui a7, 1 # a7 = 0x1000 = LED_ADDR
# sw a0, 0(a7) # 把 a0 写进 LED 寄存器(数码管显示它的 8 位十六进制)
# lui t2, 0xFFFC0 # 计数初值为负(-262144),向上数到 0 ≈ 10 ms @50 MHz
# 1:addi t2, t2, 1
# blt t2, zero, 1b
# lw ra, 256(sp)
# jalr zero, ra, 0
#
# 为什么返回地址存在 sp+256,而不是按常规压在 sp-8 的栈帧里:
# 本程序的 ISR 用的是"先把 sp 加上去、再往里存"的向上增长栈,一层最多 48 字节,
# 最多嵌套 4 层(IRQ_STACK_DEPTH=4)共 192 字节 —— 也就是说 [sp, sp+192) 是中断
# 随时可能写到的区域。而显示调用既可能被中断打断,也可能从 ISR 内部发出,所以:
# * 若压在 sp-8(常规做法),从 ISR 里调用时会正好盖掉 ISR 存好的 s6 / uepc;
# uepc 一坏,uret 就返回到 ISR 内部地址,最后执行到 ISR 尾部的 csrrci 把 MIE
# 永久清零,之后再也不响应中断(实测就是这个现象);
# * 放在 sp+256 越过了中断可能压栈的 192 字节区域,嵌套中断打断延时也不会破坏
# 它;每次调用各用自己的 sp+256,嵌套调用互不影响。
#
# 延时计数用"负数向上数到 0"而不是"递减到 0":万一 t2 被嵌套调用改掉,循环只会
# 在 t2 >= 0 时立即退出,不会像递减写法那样回绕成 40 亿次而死等。
# -----------------------------------------------------------------------------
DISP_DLY:
sw ra, 256(sp)
lui a7, 1
sw a0, 0(a7)
lui t2, 0xFFFC0
DISP_DLY_WAIT:
addi t2, t2, 1
blt t2, zero, DISP_DLY_WAIT
lw ra, 256(sp)
jalr zero, ra, 0
@@ -1,477 +0,0 @@
.text
addi sp, zero, 2000 #�ڴ��ջ��ʼ��
#############################################################
#�����Ʋ�?,����addi,andi,slli,srli,srai,or,ori,nor,ecall LED�������Ʒ�ʽ������ʾ����
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #�����Ƽ���
zmd_loop:
addi s0, s0, 1 #������һ�������Ƶ���
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #����������
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #������һ�������Ƶ���
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #����������
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
addi a7,zero,34 # system call for LED display
ecall # display
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# ʹ��?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# �����ֳ�
InteruptProgram1:sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepcѹջ����
addi sp, sp, 4
################# ���ж�
sw t0,0(sp)
addi sp,sp,4
sw t1,0(sp)
addi sp,sp,4
sw t2,0(sp)
addi sp,sp,4
sw t3,0(sp)
addi sp,sp,4
sw t4,0(sp)
addi sp,sp,4
sw t5,0(sp)
addi sp,sp,4
sw t6,0(sp)
addi sp,sp,4
sw s1,0(sp)
addi sp,sp,4
sw s2,0(sp)
addi sp,sp,4
csrrsi zero,0x4,1 #ustatus.MIE=1
################# �жϷ���
addi s6,zero,1 #�ж�?1,2,3 ��ͬ�жϺ���ʾֵ��һ��
addi s4,zero,3 #ѭ��������ʼֵ
addi s5,zero,1 #�������ۼ�ֵ
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #ѭ�������ݼ�
bne s4, zero, IntLoop1
################# ���ж�
csrrci zero,0x4,1 #ustatus.MIE=0
############### ����ָ�� ����(SLL/SB/BLTU ֮һ) ###############
addi t0,zero,1
addi t1,zero,3
addi s1,zero,0x8
slli s1,s1,8
addi s1,s1,0x76
add a0,zero,s1
addi a7,zero,34
ecall
addi t3,zero,8
sll_branch:
sll s1,s1,t0
sll s1,s1,t1
add a0,zero,s1
addi a7,zero,34
ecall
addi t3,t3,-1
bne t3,zero,sll_branch
addi sp,sp,-4
lw s2,0(sp)
addi sp,sp,-4
lw s1,0(sp)
addi sp,sp,-4
lw t6,0(sp)
addi sp,sp,-4
lw t5,0(sp)
addi sp,sp,-4
lw t4,0(sp)
addi sp,sp,-4
lw t3,0(sp)
addi sp,sp,-4
lw t2,0(sp)
addi sp,sp,-4
lw t1,0(sp)
addi sp,sp,-4
lw t0,0(sp)
################# �ָ��ֳ�
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
################ �жϷ���
uret #ͬ�����жϣ�uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# ʹ��?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# �����ֳ�
InteruptProgram2:sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepcѹջ����
addi sp, sp, 4
################# ���ж�
sw t0,0(sp)
addi sp,sp,4
sw t1,0(sp)
addi sp,sp,4
sw t2,0(sp)
addi sp,sp,4
sw t3,0(sp)
addi sp,sp,4
sw t4,0(sp)
addi sp,sp,4
sw t5,0(sp)
addi sp,sp,4
sw t6,0(sp)
addi sp,sp,4
sw s1,0(sp)
addi sp,sp,4
sw s2,0(sp)
addi sp,sp,4
csrrsi zero,0x4,1 #ustatus.MIE=1
################# �жϷ���
addi s6,zero,2 #�ж�?1,2,3 ��ͬ�жϺ���ʾֵ��һ��
addi s4,zero,3 #ѭ��������ʼֵ
addi s5,zero,1 #�������ۼ�ֵ
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #ѭ�������ݼ�
bne s4, zero, IntLoop2
################# ���ж�
csrrci zero,0x4,1 #ustatus.MIE=0
############### ����ָ�� ����(SLL/SB/BLTU ֮һ) ###############
addi t1,zero,0
addi t3,zero,32
addi s1,zero,0x00
addi s2,zero,0x01
sb_store:
sb s1,0(t1)
add a0,zero,s1
addi a7,zero,34
ecall
add s1,s1,s2
addi t1,t1,1
addi t3,t3,-1
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0
sb_branch:
lw s1,0(t1)
add a0,zero,s1
addi a7,zero,34
ecall
addi t1,t1,4
addi t3,t3,-1
bne t3,zero,sb_branch
addi sp,sp,-4
lw s2,0(sp)
addi sp,sp,-4
lw s1,0(sp)
addi sp,sp,-4
lw t6,0(sp)
addi sp,sp,-4
lw t5,0(sp)
addi sp,sp,-4
lw t4,0(sp)
addi sp,sp,-4
lw t3,0(sp)
addi sp,sp,-4
lw t2,0(sp)
addi sp,sp,-4
lw t1,0(sp)
addi sp,sp,-4
lw t0,0(sp)
################# �ָ��ֳ�
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
################ �жϷ���
uret #ͬ�����жϣ�uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# ʹ��?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# �����ֳ�
InteruptProgram3:sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepcѹջ����
addi sp, sp, 4
################# ���ж�
sw t0,0(sp)
addi sp,sp,4
sw t1,0(sp)
addi sp,sp,4
sw t2,0(sp)
addi sp,sp,4
sw t3,0(sp)
addi sp,sp,4
sw t4,0(sp)
addi sp,sp,4
sw t5,0(sp)
addi sp,sp,4
sw t6,0(sp)
addi sp,sp,4
sw s1,0(sp)
addi sp,sp,4
sw s2,0(sp)
addi sp,sp,4
csrrsi zero,0x4,1 #ustatus.MIE=1
################# �жϷ���
addi s6,zero,3 #�ж�?1,2,3 ��ͬ�жϺ���ʾֵ��һ��
addi s4,zero,3 #ѭ��������ʼֵ
addi s5,zero,1 #�������ۼ�ֵ
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #ѭ�������ݼ�
bne s4, zero, IntLoop3
################# ���ж�
csrrci zero,0x4,1 #ustatus.MIE=0
############### ����ָ�� ����(SLL/SB/BLTU ֮һ) ###############
addi s1,zero,-15
bltu_branch:
add a0,zero,s1
addi a7,zero,34
ecall
addi s1,s1,1
bltu zero,s1,bltu_branch
add a0,zero,s1
addi a7,zero,34
ecall
addi sp,sp,-4
lw s2,0(sp)
addi sp,sp,-4
lw s1,0(sp)
addi sp,sp,-4
lw t6,0(sp)
addi sp,sp,-4
lw t5,0(sp)
addi sp,sp,-4
lw t4,0(sp)
addi sp,sp,-4
lw t3,0(sp)
addi sp,sp,-4
lw t2,0(sp)
addi sp,sp,-4
lw t1,0(sp)
addi sp,sp,-4
lw t0,0(sp)
################# �ָ��ֳ�
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
################ �жϷ���
uret #ͬ�����жϣ�uepc-->pc
@@ -1,344 +0,0 @@
7d000113
00100413
01f41993
41f9d993
00000433
00c00913
00800b13
00140413
00f47413
00800293
00100313
00499993
0089e9b3
01300533
02200893
00000073
406282b3
fe0294e3
00140413
00f00f93
01f47433
01c41413
00800293
00100313
0049d993
0089e9b3
01300533
02200893
00000073
406282b3
fe0294e3
01c45413
406b0b33
000b0463
f95ff06f
000002b3
fff2c293
01029293
fff2e293
00500533
02200893
00000073
f5dff06f
01112023
00410113
00a12023
00410113
00812023
00410113
01312023
00410113
01412023
00410113
01512023
00410113
01612023
00410113
04106b73
01612023
00410113
00512023
00410113
00612023
00410113
00712023
00410113
01c12023
00410113
01d12023
00410113
01e12023
00410113
01f12023
00410113
00912023
00410113
01212023
00410113
0040e073
00100b13
00300a13
00100a93
01600433
00441413
014469b3
01300533
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
0040f073
00100293
00300313
00800493
00849493
07648493
00900533
02200893
00000073
00800e13
005494b3
006494b3
00900533
02200893
00000073
fffe0e13
fe0e14e3
ffc10113
00012903
ffc10113
00012483
ffc10113
00012f83
ffc10113
00012f03
ffc10113
00012e83
ffc10113
00012e03
ffc10113
00012383
ffc10113
00012303
ffc10113
00012283
ffc10113
00012b03
041b1073
ffc10113
00012b03
ffc10113
00012a83
ffc10113
00012a03
ffc10113
00012983
ffc10113
00012403
ffc10113
00012503
ffc10113
00012883
00200073
01112023
00410113
00a12023
00410113
00812023
00410113
01312023
00410113
01412023
00410113
01512023
00410113
01612023
00410113
04106b73
01612023
00410113
00512023
00410113
00612023
00410113
00712023
00410113
01c12023
00410113
01d12023
00410113
01e12023
00410113
01f12023
00410113
00912023
00410113
01212023
00410113
0040e073
00200b13
00300a13
00100a93
01600433
00441413
014469b3
01300533
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
0040f073
00000313
02000e13
00000493
00100913
00930023
00900533
02200893
00000073
012484b3
00130313
fffe0e13
fe0e12e3
00800e13
00000313
00032483
00900533
02200893
00000073
00430313
fffe0e13
fe0e14e3
ffc10113
00012903
ffc10113
00012483
ffc10113
00012f83
ffc10113
00012f03
ffc10113
00012e83
ffc10113
00012e03
ffc10113
00012383
ffc10113
00012303
ffc10113
00012283
ffc10113
00012b03
041b1073
ffc10113
00012b03
ffc10113
00012a83
ffc10113
00012a03
ffc10113
00012983
ffc10113
00012403
ffc10113
00012503
ffc10113
00012883
00200073
01112023
00410113
00a12023
00410113
00812023
00410113
01312023
00410113
01412023
00410113
01512023
00410113
01612023
00410113
04106b73
01612023
00410113
00512023
00410113
00612023
00410113
00712023
00410113
01c12023
00410113
01d12023
00410113
01e12023
00410113
01f12023
00410113
00912023
00410113
01212023
00410113
0040e073
00300b13
00300a13
00100a93
01600433
00441413
014469b3
01300533
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
0040f073
ff100493
00900533
02200893
00000073
00148493
fe9068e3
00900533
02200893
00000073
ffc10113
00012903
ffc10113
00012483
ffc10113
00012f83
ffc10113
00012f03
ffc10113
00012e83
ffc10113
00012e03
ffc10113
00012383
ffc10113
00012303
ffc10113
00012283
ffc10113
00012b03
041b1073
ffc10113
00012b03
ffc10113
00012a83
ffc10113
00012a03
ffc10113
00012983
ffc10113
00012403
ffc10113
00012503
ffc10113
00012883
00200073
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-549
View File
@@ -1,549 +0,0 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// 上板顶层(Nexys4DDR,xc7a100tcsg324-1)
//
// 处理器顶层 cpu21_riscv_redirect_int_bpb 暴露了 775 位调试端口(各级 PC/IR、
// 计数器、BPB/中断观测等),直接作为实现顶层会因 I/O 超限而无法布局布线
// ([Place 30-415] IO Placement failed due to overutilization)。本模块只把
// 开发板真正需要的外设引出来,其余调试端口不连接,因此不占用引脚:
//
// clk100 (E3) -> CPU 时钟(100 MHz,经 CPU_DIV 分频后给 CPU;默认 50 MHz,
// 跑中断综合测试程序时用更慢的 CLK_DIV_INTRUPT,
// 让数码管上每个数值停留更久,便于观察)
// rst_n (C12) -> 复位按键 CPU_RESETN,低有效,经复位同步器
// go (N17) -> 按键 BTNC:ecall 停机后按下继续运行
// irq_sw[2:0] -> 拨码开关 SW[2:0]:3 路中断源
// hid_sel (V10) -> 拨码开关 SW[15]:选 PS/2 设备(0 = 键盘,1 = 鼠标)
// sw_flag (R15) -> 拨码开关 SW[3]:插旗模式(软件读 INPUT 寄存器)
// led[15:0] -> 板载 LED[15:0]:LED[2:0] = 三路中断指示,从该路中断
// 请求出现开始点亮,一直到它的 ISR 执行 uret 返回后才熄灭;
// LED[15:3] 熄灭
// led_we (R12) -> LED16_B:有 MMIO 写入时亮,并拉长约 0.34 s 便于观察
// an/seg/dp -> 板载 8 位七段数码管:显示程序写入 LED_ADDR 的 32 位
// 十六进制数据(每 4 位对应一个数码管)
//
// 片内存储器:
// ROM:ROM_WORDS 字,组合读,内容由 `include "cpu21_board_rom.vh"` 给出
// (该文件由 tools/hex2vh.py 从 testbench/programs/*.hex 生成);
// RAM:RAM_WORDS 字,字节写使能 + 组合读;只响应地址 < 2^(RAM_AW+2),
// 因此 MMIO 地址不会落进 RAM(避免与 LED_ADDR 别名)。
//
// 注意:ROM_WORDS / RAM_WORDS 需为 2 的幂(默认 1024,对应地址位 [11:2])。
// -----------------------------------------------------------------------------
module cpu21_board_top #(
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter ROM_WORDS = 8192,
parameter RAM_WORDS = 1024,
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 板载 100 MHz 的分频比(1 = 直用 100 MHz,2 = 50 MHz,…)。
// 乘除法改成多周期单元后,原来 ~94 ns 的组合乘法/除法已经不在关键路径上,
// 实测(xc7a100tcsg324-1):
// CLK_DIV=1(100 MHz):WNS = -1.1 ns,违例路径是 EX 级的
// 分支判决/中断保存 → BPB 更新,与乘除法无关(逻辑只占 ~3 ns,
// 7~8 ns 都是布线);
// CLK_DIV=2( 50 MHz):WNS ≈ +8 ns,余量充足。
// 因此默认取 2,既比原来的 16 分频快 8 倍,又能保证时序收敛。
parameter integer CLK_DIV = 2,
// 中断综合测试程序(RISC_V_INTRUPT_CCAB_ROM)专用的 CPU 分频比。
// 该程序每次显示之间的延时常量是按 50 MHz 调的:50 MHz 下每个数值只停留
// 约 10 ms,数码管上根本读不出来。停留时间与分频比成正比,例如:
// 8 → 12.5 MHz(每个数值约 40 ms)、16 → 6.25 MHz(约 80 ms)、
// 32 → 3.125 MHz(约 160 ms)、64 → 1.5625 MHz(约 320 ms)。
// 取大于 2 的值只是把实际时钟变慢,XDC 的 cpu_clk 仍按最坏情况 50 MHz 约束,
// 所以不会掩盖时序违例(约束偏严)。
parameter integer CLK_DIV_INTRUPT = 32,
// PS/2 输入(板载 USB HID 口,也可改接 Pmod JA):0 = 关闭(只剩板上 5 向按键);
// 1 = 开启。开启后同时例化鼠标与键盘两个解码器(它们共用同一根 PS/2 线),
// 用 SW[15](端口 hid_sel)选哪个生效:0 = 键盘(默认),1 = 鼠标。
// 不能两个同时生效:键盘的扫描码会凑巧组成合法的鼠标包,让光标乱跳。
parameter integer USE_PS2_MOUSE = 1,
// 调试:1 = 用 LED 高位显示 PS/2 诊断信息:
// LED[15:6] = 当前选中设备收到的字节数低 10 位(按一下键/动一下鼠标就跳 = 收发都通)
// LED[5] = ps2_clk 有边沿活动(时钟在动就快速翻转)
// LED[4] = ps2_data 当前电平
// LED[3] = ps2_clk 当前电平
// LED[2:0] 始终是三路中断指示。
parameter integer DEBUG_PS2_RX_LED = 1,
// 5 向按键去抖的采样周期(CPU 周期数):上板默认 50000(1 ms @50 MHz)。
// 仿真里可以传一个小值,让"按一下"在几十微秒内就被识别。
parameter integer POINTER_SAMPLE_DIV = 50000
) (
input wire clk100, // E3
input wire rst_n, // C12:CPU_RESETN,低有效
input wire go, // N17:BTNC(同时用作鼠标左键)
input wire [ 2:0] irq_sw, // SW[2:0]:原来的 3 路中断源(保持兼容)
input wire btnu, // M18:光标上
input wire btnd, // P18:光标下
input wire btnl, // P17:光标左
input wire btnr, // M17:光标右
input wire sw_flag, // R15:SW[3],插旗模式(软件读 INPUT 寄存器)
input wire hid_sel, // V10:SW[15],选 PS/2 设备(0 = 键盘,1 = 鼠标)
output wire [15:0] led, // LED[15:0]
output wire led_we, // LED16_B:MMIO 写入指示
// 七段数码管(共阳极:位选/段选都是低有效)
output wire [ 7:0] an,
output wire [ 6:0] seg,
output wire dp,
// VGA
output wire [ 3:0] vga_r,
output wire [ 3:0] vga_g,
output wire [ 3:0] vga_b,
output wire vga_hs,
output wire vga_vs,
// PS/2(只有 USE_PS2_MOUSE=1 时才用)
input wire ps2_clk,
input wire ps2_data
);
localparam ROM_AW = $clog2(ROM_WORDS);
localparam RAM_AW = $clog2(RAM_WORDS);
// CPU 时钟实际用的分频比:中断综合测试程序用更慢的 CLK_DIV_INTRUPT,
// 其余程序用 CLK_DIV。两者必须都是偶数(保证 50% 占空比)。
`ifdef RISC_V_INTRUPT_CCAB_ROM
localparam integer CPU_DIV = CLK_DIV_INTRUPT;
`elsif RISC_V_INTRUPT_SLLSB_ROM
localparam integer CPU_DIV = CLK_DIV_INTRUPT;
`else
localparam integer CPU_DIV = CLK_DIV;
`endif
// 数码管扫描分频:目标是“每位数码管约 125 Hz 刷新”(即每步 1 ms),与 CPU
// 时钟频率无关。每步时间 = REFRESH_DIV / f_cpu,而 f_cpu = 100 MHz / CPU_DIV,
// 所以 REFRESH_DIV = 1 ms × f_cpu = 100000 / CPU_DIV:
// CPU_DIV = 2( 50 MHz)-> 50000(每步 1 ms,每位数码管 125 Hz)
// CPU_DIV = 8(12.5 MHz)-> 12500(同上)
// 例:CPU_DIV = 32(3.125 MHz)-> 3125(每步仍是 1 ms)。
// 不这样缩放的话,CPU 降频时扫描会按同样比例变慢,肉眼能看到闪烁。
localparam integer SEG_REFRESH_DIV = 100000 / CPU_DIV;
wire arst = ~rst_n; // 异步复位(高有效)
// ---------------------------------------------------------------------------
// 时钟:100 MHz / CPU_DIV 给 CPU。默认 50 MHz(CLK_DIV=2);跑中断综合测试
// 程序时用更慢的 CLK_DIV_INTRUPT(见该参数说明),让数码管上的数值停留更久。
//
// 乘除法已经是 cpu21_riscv_muldiv 里的多周期单元,关键路径上不再有组合的
// 32x32 乘法/除法;剩下的违例路径是 EX 级“分支判决/中断保存 → BPB 更新”,
// 与乘除法无关,实测 100 MHz 下 WNS = -1.1 ns,50 MHz 下余量约 +8 ns。
// 分频后的时钟由 Vivado 自动插 BUFG;XDC 里用 create_generated_clock 声明。
// XDC 按最坏情况固定成 -divide_by 2(50 MHz),所以这里把实际时钟变慢属于
// “约束偏严”,不会掩盖时序违例。
//
// 中断综合测试程序会用更慢的 CPU_DIV(见 CLK_DIV_INTRUPT):程序里的延时常量
// 按 50 MHz 调好,50 MHz 下每个数值只停留约 10 ms,读数根本看不清;
// 停留时间与分频比成正比,具体见 CLK_DIV_INTRUPT 的说明。
// 注意:VGA 的 25 MHz 像素节拍也是从这个 CPU 时钟分频出来的,CPU 降频时
// VGA 行场时序会跟着变慢(超出 VGA 规范)。中断测试程序不用 VGA,无影响。
// ---------------------------------------------------------------------------
reg [15:0] clk_cnt;
reg clk_div;
always @(posedge clk100 or posedge arst) begin
if (arst) begin
clk_cnt <= 16'd0;
clk_div <= 1'b0;
end else if (clk_cnt == (CPU_DIV / 2 - 1)) begin
clk_cnt <= 16'd0;
clk_div <= ~clk_div;
end else begin
clk_cnt <= clk_cnt + 16'd1;
end
end
wire clk_cpu = clk_div;
// ---------------------------------------------------------------------------
// 复位同步器:按键异步置位、释放与 CPU 时钟同步
// ---------------------------------------------------------------------------
reg [2:0] rst_sync_q;
always @(posedge clk_cpu or posedge arst) begin
if (arst) rst_sync_q <= 3'b111;
else rst_sync_q <= {rst_sync_q[1:0], 1'b0};
end
wire reset = rst_sync_q[2];
// CPU 接口连线(先声明:后面的 ROM/RAM 逻辑与例化都要用到)
wire [31:0] instr_addr;
wire [31:0] data_addr;
wire [31:0] data_wdata;
wire [ 3:0] data_wstrb;
wire data_we;
wire [15:0] ps2_rx_count; // PS/2 鼠标收到的字节数(用于 LED 调试显示)
wire [15:0] kbd_rx_count; // PS/2 键盘收到的字节数(先声明:LED 调试段要用)
wire [31:0] led_data; // LED_ADDR 写入值,同时作为七段数码管显示数据
wire led_valid;
// 扫雷扩展外设的连线(先声明,例化在下面)
wire [31:0] disp_ctrl, disp_cursor; // 显示控制器(CPU 内部 MMIO 驱动)
wire [31:0] seg_data; // 保留的独立数码管 MMIO 数据
wire seg_valid;
wire fb_we; // 显存写端口
wire [ 9:0] fb_waddr;
wire [31:0] fb_wdata;
wire [ 3:0] fb_wstrb;
wire [ 9:0] fb_raddr; // 显存读端口(VGA)
wire mouse_irq, level_irq; // 鼠标事件中断 / 软件中断(关卡入口)
wire [1:0] irq_level; // 当前正在服务的中断级别(0 = 不在中断里,1/2/3 = 在 ISR 里)
wire frame_tick;
wire [15:0] sw_vec = {12'b0, sw_flag, irq_sw};
// 中断连线:SW0 与鼠标共用一个入口,SW1 与"关卡入口"共用一个入口。
// 中断线对应关系(必须与 saolei/sw/linker.ld、mmio.h 的约定一致):
// irq_vec[0] = IRQ1 -> 入口 0x100 = 关卡(游戏),由 LEVEL 软件中断拉起
// irq_vec[1] = IRQ2 -> 入口 0x200 = 鼠标读取,由事件 FIFO 非空拉起
// irq_vec[2] = IRQ3 -> 入口 0x300 = 备用
// (原来 mouse 接在 [0]、level 接在 [1],正好对调:第一个鼠标事件会跳进关卡
// 入口把棋盘画出来,而鼠标 ISR 永不执行,FIFO 不弹空、中断线恒高 —— 于是
// 只能收到一次事件。拨码开关 SW[2:0] 仍按位 OR 进来,兼容原中断测试程序。)
wire [2:0] irq_vec = {irq_sw[2], (mouse_irq | irq_sw[1]), (level_irq | irq_sw[0])};
// ---------------------------------------------------------------------------
// 指令 ROM:组合读(未用到的地址填 NOP)
// ---------------------------------------------------------------------------
reg [31:0] rom[0:ROM_WORDS-1];
integer i;
initial begin
for (i = 0; i < ROM_WORDS; i = i + 1) rom[i] = 32'h0000_0013; // NOP 填充
`ifdef SAOLEI_ROM
// 扫雷程序(由 saolei/tools/build.ps1 生成的 cpu21_saolei_rom.vh)。
// 综合/仿真时定义宏 SAOLEI_ROM 即可切换到这个程序;不定义就是下面的跑马灯。
`include "cpu21_saolei_rom.vh"
`elsif RISC_V_INTRUPT_CCAB_ROM
// 中断综合测试程序:三个中断入口由镜像中的实际标签位置决定。
`include "risc-v-intrupt-ccab.vh"
`elsif RISC_V_INTRUPT_SLLSB_ROM
// 课程新版中断测试(附加 SLL/SB/BLTU),ISR 入口 0x0AC / 0x240 / 0x3E8。
`include "risc-v-intrupt-sllsb.vh"
`elsif BOARD_ROM_FAST
// 仿真专用:把演示程序的延时改小的版本(综合流程不会用到)
`include "cpu21_board_rom_fast.vh"
`else
`include "cpu21_board_rom.vh"
`endif
end
// ---------------------------------------------------------------------------
// 数据 RAM:字节写使能 + 组合读
// ---------------------------------------------------------------------------
reg [31:0] ram[0:RAM_WORDS-1];
integer j;
initial begin
for (j = 0; j < RAM_WORDS; j = j + 1) ram[j] = 32'b0;
end
wire [ROM_AW+1:2] rom_index = instr_addr[ROM_AW+1:2];
wire [RAM_AW+1:2] ram_index = data_addr[RAM_AW+1:2];
wire ram_sel = (data_addr[31:RAM_AW+2] == 0);
wire [31:0] instr_word = rom[rom_index];
wire [31:0] data_rdata = ram_sel ? ram[ram_index] : 32'b0;
always @(posedge clk_cpu) begin
if (ram_sel && data_we) begin
if (data_wstrb[0]) ram[ram_index][7:0] <= data_wdata[7:0];
if (data_wstrb[1]) ram[ram_index][15:8] <= data_wdata[15:8];
if (data_wstrb[2]) ram[ram_index][23:16] <= data_wdata[23:16];
if (data_wstrb[3]) ram[ram_index][31:24] <= data_wdata[31:24];
end
end
// ---------------------------------------------------------------------------
// 处理器:只连接上板需要的端口,调试端口留空(不产生 I/O)
// ---------------------------------------------------------------------------
cpu21_riscv_redirect_int_bpb #(
.LED_ADDR (LED_ADDR),
.USTATUS_INIT(USTATUS_INIT),
// 中断入口必须与所选 ROM 镜像中的 ISR 起点一致。
`ifdef RISC_V_INTRUPT_CCAB_ROM
// risc-v-intrupt-ccab.hex:字 43/121/194
.IRQ1_VECTOR (32'h0000_00AC),
.IRQ2_VECTOR (32'h0000_01E4),
.IRQ3_VECTOR (32'h0000_0308)
`elsif RISC_V_INTRUPT_SLLSB_ROM
// risc-v-intrupt-sllsb.hex:字 43/144/250(加了 SLL/SB/BLTU 后 ISR2/3 后移)
.IRQ1_VECTOR (32'h0000_00AC),
.IRQ2_VECTOR (32'h0000_0240),
.IRQ3_VECTOR (32'h0000_03E8)
`else
// 扫雷程序:linker.ld 中固定的 0x100/0x200/0x300
.IRQ1_VECTOR (32'h0000_0100),
.IRQ2_VECTOR (32'h0000_0200),
.IRQ3_VECTOR (32'h0000_0300)
`endif
) u_cpu (
.clk (clk_cpu),
.reset (reset),
.instr_i (instr_word),
.data_rdata_i(data_rdata),
.irq_i (irq_vec),
.go_i (go),
.instr_addr_o(instr_addr),
.data_addr_o (data_addr),
.data_wdata_o(data_wdata),
.data_wstrb_o(data_wstrb),
.data_we_o (data_we),
.led_data_o (led_data),
.led_valid_o(led_valid),
// 扫雷扩展外设
.seg_data_o (seg_data),
.seg_valid_o (seg_valid),
.fb_we_o (fb_we),
.fb_waddr_o (fb_waddr),
.fb_wdata_o (fb_wdata),
.fb_wstrb_o (fb_wstrb),
.fb_rdata_i (fb_rdata),
.disp_ctrl_o (disp_ctrl),
.disp_cursor_o (disp_cursor),
.mouse_ev_valid_i (ev_valid),
.mouse_dx_i (ev_dx),
.mouse_dy_i (ev_dy),
.mouse_btn_i (ev_btn),
.sw_i (sw_vec),
.frame_tick_i (frame_tick),
.mouse_irq_o (mouse_irq),
.level_irq_o (level_irq),
// 中断服务级别,供板上的中断指示 LED 使用(见下面 LED 那一段)
.irq_current_level_o(irq_level)
);
// ---------------------------------------------------------------------------
// 板载 LED:三路中断指示;十六进制数据改由七段数码管显示。
//
// 第 k 路中断从"请求出现"开始点亮,一直亮到它的 ISR 执行完 uret(中断返回)
// 之后才熄灭:
// 置位:irq_vec[k] 的上升沿。中断控制器内部也是按上升沿响应中断的
// (cpu21_riscv_irq_ctrl 里的 irq_event),两者行为一致;开关一直
// 接通不会重复点亮,只有新的上升沿才会重新置位。
// 清零:服务级别 irq_level 从 k+1 **降下来**,说明第 k+1 级的中断已经返回。
// 注意必须判断“下降”:级别从 1 升到 2 是深层中断抢占,此时第 1 级
// 的中断还在服务中,LED 不能熄。中断可以嵌套,返回时按级别逐层降
// 下来,因此每一路都在自己的 ISR 结束时熄灭。
// ---------------------------------------------------------------------------
reg [2:0] irq_req_q; // 中断请求打一拍,用于取上升沿
reg [1:0] irq_level_q; // 服务级别打一拍,用于检测"返回"
reg [2:0] irq_led_q; // 三路中断指示
wire isr_ret0 = (irq_level_q == 2'd1) && (irq_level < irq_level_q);
wire isr_ret1 = (irq_level_q == 2'd2) && (irq_level < irq_level_q);
wire isr_ret2 = (irq_level_q == 2'd3) && (irq_level < irq_level_q);
always @(posedge clk_cpu or posedge arst) begin
if (arst) begin
irq_req_q <= 3'b000;
irq_level_q <= 2'd0;
irq_led_q <= 3'b000;
end else begin
irq_req_q <= irq_vec;
irq_level_q <= irq_level;
// 请求上升沿 -> 点亮;对应级别的 ISR 返回 -> 熄灭
irq_led_q[0] <= (irq_vec[0] & ~irq_req_q[0]) | (irq_led_q[0] & ~isr_ret0);
irq_led_q[1] <= (irq_vec[1] & ~irq_req_q[1]) | (irq_led_q[1] & ~isr_ret1);
irq_led_q[2] <= (irq_vec[2] & ~irq_req_q[2]) | (irq_led_q[2] & ~isr_ret2);
end
end
// ---- PS/2 输入诊断:把两根线的电平与活动情况摆到 LED 上,
// 便于判断"板载 USB HID 芯片到底有没有在发 PS/2 数据" ----
reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;
reg [7:0] ps2_edge_cnt;
always @(posedge clk_cpu or posedge reset) begin
if (reset) begin
ps2_clk_s1 <= 1'b1;
ps2_clk_s2 <= 1'b1;
ps2_data_s1 <= 1'b1;
ps2_data_s2 <= 1'b1;
ps2_edge_cnt <= 8'd0;
end else begin
ps2_clk_s1 <= ps2_clk;
ps2_clk_s2 <= ps2_clk_s1;
ps2_data_s1 <= ps2_data;
ps2_data_s2 <= ps2_data_s1;
if (ps2_clk_s1 != ps2_clk_s2)
ps2_edge_cnt <= ps2_edge_cnt + 8'd1;
end
end
// 当前选中的 PS/2 设备收到的字节数(SW[15] 选谁就显示谁)
wire [15:0] hid_rx_sel = hid_sel ? ps2_rx_count : kbd_rx_count;
// LED[15:3] 平时熄灭;DEBUG_PS2_RX_LED=1 时显示 PS/2 诊断信息(含义见参数注释)
assign led = (DEBUG_PS2_RX_LED != 0)
? {hid_rx_sel[9:0], ps2_edge_cnt[0], ps2_data_s2, ps2_clk_s2, irq_led_q}
: {{13{1'b0}}, irq_led_q};
reg [23:0] led_we_cnt;
always @(posedge clk_cpu or posedge arst) begin
if (arst) led_we_cnt <= 24'd0;
else if (led_valid) led_we_cnt <= 24'hFF_FFFF; // 约 0.34 s @50 MHz
// (12.5 MHz 下约 1.36 s)
else if (led_we_cnt != 24'd0) led_we_cnt <= led_we_cnt - 24'd1;
end
assign led_we = (led_we_cnt != 24'd0);
// ===========================================================================
// 扫雷外设:显存 + VGA + 七段数码管 + 鼠标事件源 + 中断连线
//
// 显存是一块 2400 字节的双口 RAM(40x30 格,每格 2 字节):CPU 通过 MMIO 的
// fb_* 写端口写入,VGA 用另一个读端口组合读出。两者同处 clk_cpu 域(VGA 的
// 25 MHz 像素节拍由使能信号产生,不是独立时钟),因此没有跨时钟域问题。
// ===========================================================================
localparam integer FB_WORDS = 600; // 600 字 = 2400 字节,40x30 格每格 2 字节
// ---- 显存 ----
reg [31:0] fb[0:FB_WORDS-1];
integer k;
initial begin
for (k = 0; k < FB_WORDS; k = k + 1) fb[k] = 32'b0;
end
always @(posedge clk_cpu) begin
if (fb_we) begin
if (fb_wstrb[0]) fb[fb_waddr][7:0] <= fb_wdata[7:0];
if (fb_wstrb[1]) fb[fb_waddr][15:8] <= fb_wdata[15:8];
if (fb_wstrb[2]) fb[fb_waddr][23:16] <= fb_wdata[23:16];
if (fb_wstrb[3]) fb[fb_waddr][31:24] <= fb_wdata[31:24];
end
end
wire [31:0] fb_rdata = fb[fb_raddr];
// ---- VGA 像素节拍:clk_cpu 二分频 => 25 MHz ----
reg px_q;
always @(posedge clk_cpu or posedge arst) begin
if (arst) px_q <= 1'b0;
else px_q <= ~px_q;
end
wire px_tick = ~px_q;
cpu21_vga u_vga (
.clk (clk_cpu),
.reset (reset),
.tick (px_tick),
.fb_raddr_o (fb_raddr),
.fb_word_i (fb_rdata),
.ctrl_i (disp_ctrl),
.cursor_i (disp_cursor),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.frame_tick_o(frame_tick)
);
// ---- 七段数码管(显示程序写入 LED_ADDR 的 8 位十六进制数)----
// 扫描分频按 CPU 时钟频率缩放,保持每位数码管约 125 Hz 刷新(不闪烁)。
cpu21_seg7 #(
.REFRESH_DIV(SEG_REFRESH_DIV)
) u_seg7 (
.clk (clk_cpu),
.reset (reset),
.seg_data_i(led_data),
.an_en_i (8'hFF),
.an_o (an),
.seg_o (seg),
.dp_o (dp)
);
// ---- 鼠标事件源:板载按键 与 PS/2 鼠标**同时**有效(两路事件在顶层合并) ----
wire ev_valid;
wire [7:0] ev_dx, ev_dy;
wire [4:0] ev_btn;
wire [4:0] btn_vec = {go, btnu, btnd, btnl, btnr}; // {BTNC, BTNU, BTND, BTNL, BTNR}
// 1) 板载按键:永远保留(BTNC 当左键、上下左右当移动、SW3 当插旗开关)
wire ptr_valid;
wire [7:0] ptr_dx, ptr_dy;
wire [4:0] ptr_btn;
cpu21_pointer #(
.SAMPLE_DIV(POINTER_SAMPLE_DIV)
) u_ptr (
.clk (clk_cpu),
.reset (reset),
.btn_i (btn_vec),
.ev_valid_o(ptr_valid),
.ev_dx_o (ptr_dx),
.ev_dy_o (ptr_dy),
.ev_btn_o (ptr_btn)
);
// 2) PS/2 鼠标:板载 USB HID 口经板上芯片转成 PS/2 信号(ps2_clk/ps2_data)
wire ps2_valid;
wire [7:0] ps2_dx, ps2_dy;
wire [4:0] ps2_btn;
// PS/2 键盘:与鼠标共用同一根 PS/2 线(板上只有一个 USB HID 口)。
// 线上只有一个设备在说话,"是哪一个"用 SW[15] 告诉硬件(hid_sel)。
wire kbd_valid;
wire [7:0] kbd_dx, kbd_dy;
wire [4:0] kbd_btn;
generate
if (USE_PS2_MOUSE != 0) begin : g_ps2
cpu21_ps2_mouse u_ps2 (
.clk (clk_cpu),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_clk_oe_o (),
.ps2_data_i (ps2_data),
.ps2_data_oe_o(),
.ev_valid_o (ps2_valid),
.ev_dx_o (ps2_dx),
.ev_dy_o (ps2_dy),
.ev_btn_o (ps2_btn),
.rx_count_o (ps2_rx_count)
);
cpu21_ps2_kbd u_kbd (
.clk (clk_cpu),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_data_i (ps2_data),
.ev_valid_o (kbd_valid),
.ev_dx_o (kbd_dx),
.ev_dy_o (kbd_dy),
.ev_btn_o (kbd_btn),
.rx_count_o (kbd_rx_count)
);
end else begin : g_no_ps2
assign ps2_valid = 1'b0;
assign ps2_dx = 8'd0;
assign ps2_dy = 8'd0;
assign ps2_btn = 5'd0;
assign ps2_rx_count = 16'd0;
assign kbd_valid = 1'b0;
assign kbd_dx = 8'd0;
assign kbd_dy = 8'd0;
assign kbd_btn = 5'd0;
assign kbd_rx_count = 16'd0;
end
endgenerate
// 3) 只放行 SW[15] 选中的那一路 PS/2 设备(另一个解码器虽然也在听,但事件被挡住)
wire m_valid = ps2_valid & hid_sel; // SW[15] = 1 -> PS/2 鼠标
wire k_valid = kbd_valid & ~hid_sel; // SW[15] = 0 -> PS/2 键盘
// 4) 合并:三个源相互独立,同一拍只会有一方产生事件;优先 PS/2
assign ev_valid = m_valid | k_valid | ptr_valid;
assign ev_dx = m_valid ? ps2_dx : k_valid ? kbd_dx : ptr_dx;
assign ev_dy = m_valid ? ps2_dy : k_valid ? kbd_dy : ptr_dy;
assign ev_btn = m_valid ? ps2_btn : k_valid ? kbd_btn : ptr_btn;
endmodule
`default_nettype wire
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,265 +0,0 @@
## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project
## Clock signal
#set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { CLK100MHZ }]; #IO_L12P_T1_MRCC_35 Sch=clk100mhz
#create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {CLK100MHZ}];
##Switches
#set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { SW[0] }]; #IO_L24N_T3_RS0_15 Sch=sw[0]
#set_property -dict { PACKAGE_PIN L16 IOSTANDARD LVCMOS33 } [get_ports { SW[1] }]; #IO_L3N_T0_DQS_EMCCLK_14 Sch=sw[1]
#set_property -dict { PACKAGE_PIN M13 IOSTANDARD LVCMOS33 } [get_ports { SW[2] }]; #IO_L6N_T0_D08_VREF_14 Sch=sw[2]
#set_property -dict { PACKAGE_PIN R15 IOSTANDARD LVCMOS33 } [get_ports { SW[3] }]; #IO_L13N_T2_MRCC_14 Sch=sw[3]
#set_property -dict { PACKAGE_PIN R17 IOSTANDARD LVCMOS33 } [get_ports { SW[4] }]; #IO_L12N_T1_MRCC_14 Sch=sw[4]
#set_property -dict { PACKAGE_PIN T18 IOSTANDARD LVCMOS33 } [get_ports { SW[5] }]; #IO_L7N_T1_D10_14 Sch=sw[5]
#set_property -dict { PACKAGE_PIN U18 IOSTANDARD LVCMOS33 } [get_ports { SW[6] }]; #IO_L17N_T2_A13_D29_14 Sch=sw[6]
#set_property -dict { PACKAGE_PIN R13 IOSTANDARD LVCMOS33 } [get_ports { SW[7] }]; #IO_L5N_T0_D07_14 Sch=sw[7]
#set_property -dict { PACKAGE_PIN T8 IOSTANDARD LVCMOS18 } [get_ports { SW[8] }]; #IO_L24N_T3_34 Sch=sw[8]
#set_property -dict { PACKAGE_PIN U8 IOSTANDARD LVCMOS18 } [get_ports { SW[9] }]; #IO_25_34 Sch=sw[9]
#set_property -dict { PACKAGE_PIN R16 IOSTANDARD LVCMOS33 } [get_ports { SW[10] }]; #IO_L15P_T2_DQS_RDWR_B_14 Sch=sw[10]
#set_property -dict { PACKAGE_PIN T13 IOSTANDARD LVCMOS33 } [get_ports { SW[11] }]; #IO_L23P_T3_A03_D19_14 Sch=sw[11]
#set_property -dict { PACKAGE_PIN H6 IOSTANDARD LVCMOS33 } [get_ports { SW[12] }]; #IO_L24P_T3_35 Sch=sw[12]
#set_property -dict { PACKAGE_PIN U12 IOSTANDARD LVCMOS33 } [get_ports { SW[13] }]; #IO_L20P_T3_A08_D24_14 Sch=sw[13]
#set_property -dict { PACKAGE_PIN U11 IOSTANDARD LVCMOS33 } [get_ports { SW[14] }]; #IO_L19N_T3_A09_D25_VREF_14 Sch=sw[14]
#set_property -dict { PACKAGE_PIN V10 IOSTANDARD LVCMOS33 } [get_ports { SW[15] }]; #IO_L21P_T3_DQS_14 Sch=sw[15]
## LEDs
#set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { LED[0] }]; #IO_L18P_T2_A24_15 Sch=led[0]
#set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports { LED[1] }]; #IO_L24P_T3_RS1_15 Sch=led[1]
#set_property -dict { PACKAGE_PIN J13 IOSTANDARD LVCMOS33 } [get_ports { LED[2] }]; #IO_L17N_T2_A25_15 Sch=led[2]
#set_property -dict { PACKAGE_PIN N14 IOSTANDARD LVCMOS33 } [get_ports { LED[3] }]; #IO_L8P_T1_D11_14 Sch=led[3]
#set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports { LED[4] }]; #IO_L7P_T1_D09_14 Sch=led[4]
#set_property -dict { PACKAGE_PIN V17 IOSTANDARD LVCMOS33 } [get_ports { LED[5] }]; #IO_L18N_T2_A11_D27_14 Sch=led[5]
#set_property -dict { PACKAGE_PIN U17 IOSTANDARD LVCMOS33 } [get_ports { LED[6] }]; #IO_L17P_T2_A14_D30_14 Sch=led[6]
#set_property -dict { PACKAGE_PIN U16 IOSTANDARD LVCMOS33 } [get_ports { LED[7] }]; #IO_L18P_T2_A12_D28_14 Sch=led[7]
#set_property -dict { PACKAGE_PIN V16 IOSTANDARD LVCMOS33 } [get_ports { LED[8] }]; #IO_L16N_T2_A15_D31_14 Sch=led[8]
#set_property -dict { PACKAGE_PIN T15 IOSTANDARD LVCMOS33 } [get_ports { LED[9] }]; #IO_L14N_T2_SRCC_14 Sch=led[9]
#set_property -dict { PACKAGE_PIN U14 IOSTANDARD LVCMOS33 } [get_ports { LED[10] }]; #IO_L22P_T3_A05_D21_14 Sch=led[10]
#set_property -dict { PACKAGE_PIN T16 IOSTANDARD LVCMOS33 } [get_ports { LED[11] }]; #IO_L15N_T2_DQS_DOUT_CSO_B_14 Sch=led[11]
#set_property -dict { PACKAGE_PIN V15 IOSTANDARD LVCMOS33 } [get_ports { LED[12] }]; #IO_L16P_T2_CSI_B_14 Sch=led[12]
#set_property -dict { PACKAGE_PIN V14 IOSTANDARD LVCMOS33 } [get_ports { LED[13] }]; #IO_L22N_T3_A04_D20_14 Sch=led[13]
#set_property -dict { PACKAGE_PIN V12 IOSTANDARD LVCMOS33 } [get_ports { LED[14] }]; #IO_L20N_T3_A07_D23_14 Sch=led[14]
#set_property -dict { PACKAGE_PIN V11 IOSTANDARD LVCMOS33 } [get_ports { LED[15] }]; #IO_L21N_T3_DQS_A06_D22_14 Sch=led[15]
#set_property -dict { PACKAGE_PIN R12 IOSTANDARD LVCMOS33 } [get_ports { LED16_B }]; #IO_L5P_T0_D06_14 Sch=led16_b
#set_property -dict { PACKAGE_PIN M16 IOSTANDARD LVCMOS33 } [get_ports { LED16_G }]; #IO_L10P_T1_D14_14 Sch=led16_g
#set_property -dict { PACKAGE_PIN N15 IOSTANDARD LVCMOS33 } [get_ports { LED16_R }]; #IO_L11P_T1_SRCC_14 Sch=led16_r
#set_property -dict { PACKAGE_PIN G14 IOSTANDARD LVCMOS33 } [get_ports { LED17_B }]; #IO_L15N_T2_DQS_ADV_B_15 Sch=led17_b
#set_property -dict { PACKAGE_PIN R11 IOSTANDARD LVCMOS33 } [get_ports { LED17_G }]; #IO_0_14 Sch=led17_g
#set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports { LED17_R }]; #IO_L11N_T1_SRCC_14 Sch=led17_r
##7 segment display
#set_property -dict { PACKAGE_PIN T10 IOSTANDARD LVCMOS33 } [get_ports { CA }]; #IO_L24N_T3_A00_D16_14 Sch=ca
#set_property -dict { PACKAGE_PIN R10 IOSTANDARD LVCMOS33 } [get_ports { CB }]; #IO_25_14 Sch=cb
#set_property -dict { PACKAGE_PIN K16 IOSTANDARD LVCMOS33 } [get_ports { CC }]; #IO_25_15 Sch=cc
#set_property -dict { PACKAGE_PIN K13 IOSTANDARD LVCMOS33 } [get_ports { CD }]; #IO_L17P_T2_A26_15 Sch=cd
#set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { CE }]; #IO_L13P_T2_MRCC_14 Sch=ce
#set_property -dict { PACKAGE_PIN T11 IOSTANDARD LVCMOS33 } [get_ports { CF }]; #IO_L19P_T3_A10_D26_14 Sch=cf
#set_property -dict { PACKAGE_PIN L18 IOSTANDARD LVCMOS33 } [get_ports { CG }]; #IO_L4P_T0_D04_14 Sch=cg
#set_property -dict { PACKAGE_PIN H15 IOSTANDARD LVCMOS33 } [get_ports { DP }]; #IO_L19N_T3_A21_VREF_15 Sch=dp
#set_property -dict { PACKAGE_PIN J17 IOSTANDARD LVCMOS33 } [get_ports { AN[0] }]; #IO_L23P_T3_FOE_B_15 Sch=an[0]
#set_property -dict { PACKAGE_PIN J18 IOSTANDARD LVCMOS33 } [get_ports { AN[1] }]; #IO_L23N_T3_FWE_B_15 Sch=an[1]
#set_property -dict { PACKAGE_PIN T9 IOSTANDARD LVCMOS33 } [get_ports { AN[2] }]; #IO_L24P_T3_A01_D17_14 Sch=an[2]
#set_property -dict { PACKAGE_PIN J14 IOSTANDARD LVCMOS33 } [get_ports { AN[3] }]; #IO_L19P_T3_A22_15 Sch=an[3]
#set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports { AN[4] }]; #IO_L8N_T1_D12_14 Sch=an[4]
#set_property -dict { PACKAGE_PIN T14 IOSTANDARD LVCMOS33 } [get_ports { AN[5] }]; #IO_L14P_T2_SRCC_14 Sch=an[5]
#set_property -dict { PACKAGE_PIN K2 IOSTANDARD LVCMOS33 } [get_ports { AN[6] }]; #IO_L23P_T3_35 Sch=an[6]
#set_property -dict { PACKAGE_PIN U13 IOSTANDARD LVCMOS33 } [get_ports { AN[7] }]; #IO_L23N_T3_A02_D18_14 Sch=an[7]
##Buttons
#set_property -dict { PACKAGE_PIN C12 IOSTANDARD LVCMOS33 } [get_ports { CPU_RESETN }]; #IO_L3P_T0_DQS_AD1P_15 Sch=cpu_resetn
#set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { BTNC }]; #IO_L9P_T1_DQS_14 Sch=btnc
#set_property -dict { PACKAGE_PIN M18 IOSTANDARD LVCMOS33 } [get_ports { BTNU }]; #IO_L4N_T0_D05_14 Sch=btnu
#set_property -dict { PACKAGE_PIN P17 IOSTANDARD LVCMOS33 } [get_ports { BTNL }]; #IO_L12P_T1_MRCC_14 Sch=btnl
#set_property -dict { PACKAGE_PIN M17 IOSTANDARD LVCMOS33 } [get_ports { BTNR }]; #IO_L10N_T1_D15_14 Sch=btnr
#set_property -dict { PACKAGE_PIN P18 IOSTANDARD LVCMOS33 } [get_ports { BTND }]; #IO_L9N_T1_DQS_D13_14 Sch=btnd
##Pmod Headers
##Pmod Header JA
#set_property -dict { PACKAGE_PIN C17 IOSTANDARD LVCMOS33 } [get_ports { JA[1] }]; #IO_L20N_T3_A19_15 Sch=ja[1]
#set_property -dict { PACKAGE_PIN D18 IOSTANDARD LVCMOS33 } [get_ports { JA[2] }]; #IO_L21N_T3_DQS_A18_15 Sch=ja[2]
#set_property -dict { PACKAGE_PIN E18 IOSTANDARD LVCMOS33 } [get_ports { JA[3] }]; #IO_L21P_T3_DQS_15 Sch=ja[3]
#set_property -dict { PACKAGE_PIN G17 IOSTANDARD LVCMOS33 } [get_ports { JA[4] }]; #IO_L18N_T2_A23_15 Sch=ja[4]
#set_property -dict { PACKAGE_PIN D17 IOSTANDARD LVCMOS33 } [get_ports { JA[7] }]; #IO_L16N_T2_A27_15 Sch=ja[7]
#set_property -dict { PACKAGE_PIN E17 IOSTANDARD LVCMOS33 } [get_ports { JA[8] }]; #IO_L16P_T2_A28_15 Sch=ja[8]
#set_property -dict { PACKAGE_PIN F18 IOSTANDARD LVCMOS33 } [get_ports { JA[9] }]; #IO_L22N_T3_A16_15 Sch=ja[9]
#set_property -dict { PACKAGE_PIN G18 IOSTANDARD LVCMOS33 } [get_ports { JA[10] }]; #IO_L22P_T3_A17_15 Sch=ja[10]
##Pmod Header JB
#set_property -dict { PACKAGE_PIN D14 IOSTANDARD LVCMOS33 } [get_ports { JB[1] }]; #IO_L1P_T0_AD0P_15 Sch=jb[1]
#set_property -dict { PACKAGE_PIN F16 IOSTANDARD LVCMOS33 } [get_ports { JB[2] }]; #IO_L14N_T2_SRCC_15 Sch=jb[2]
#set_property -dict { PACKAGE_PIN G16 IOSTANDARD LVCMOS33 } [get_ports { JB[3] }]; #IO_L13N_T2_MRCC_15 Sch=jb[3]
#set_property -dict { PACKAGE_PIN H14 IOSTANDARD LVCMOS33 } [get_ports { JB[4] }]; #IO_L15P_T2_DQS_15 Sch=jb[4]
#set_property -dict { PACKAGE_PIN E16 IOSTANDARD LVCMOS33 } [get_ports { JB[7] }]; #IO_L11N_T1_SRCC_15 Sch=jb[7]
#set_property -dict { PACKAGE_PIN F13 IOSTANDARD LVCMOS33 } [get_ports { JB[8] }]; #IO_L5P_T0_AD9P_15 Sch=jb[8]
#set_property -dict { PACKAGE_PIN G13 IOSTANDARD LVCMOS33 } [get_ports { JB[9] }]; #IO_0_15 Sch=jb[9]
#set_property -dict { PACKAGE_PIN H16 IOSTANDARD LVCMOS33 } [get_ports { JB[10] }]; #IO_L13P_T2_MRCC_15 Sch=jb[10]
##Pmod Header JC
#set_property -dict { PACKAGE_PIN K1 IOSTANDARD LVCMOS33 } [get_ports { JC[1] }]; #IO_L23N_T3_35 Sch=jc[1]
#set_property -dict { PACKAGE_PIN F6 IOSTANDARD LVCMOS33 } [get_ports { JC[2] }]; #IO_L19N_T3_VREF_35 Sch=jc[2]
#set_property -dict { PACKAGE_PIN J2 IOSTANDARD LVCMOS33 } [get_ports { JC[3] }]; #IO_L22N_T3_35 Sch=jc[3]
#set_property -dict { PACKAGE_PIN G6 IOSTANDARD LVCMOS33 } [get_ports { JC[4] }]; #IO_L19P_T3_35 Sch=jc[4]
#set_property -dict { PACKAGE_PIN E7 IOSTANDARD LVCMOS33 } [get_ports { JC[7] }]; #IO_L6P_T0_35 Sch=jc[7]
#set_property -dict { PACKAGE_PIN J3 IOSTANDARD LVCMOS33 } [get_ports { JC[8] }]; #IO_L22P_T3_35 Sch=jc[8]
#set_property -dict { PACKAGE_PIN J4 IOSTANDARD LVCMOS33 } [get_ports { JC[9] }]; #IO_L21P_T3_DQS_35 Sch=jc[9]
#set_property -dict { PACKAGE_PIN E6 IOSTANDARD LVCMOS33 } [get_ports { JC[10] }]; #IO_L5P_T0_AD13P_35 Sch=jc[10]
##Pmod Header JD
#set_property -dict { PACKAGE_PIN H4 IOSTANDARD LVCMOS33 } [get_ports { JD[1] }]; #IO_L21N_T3_DQS_35 Sch=jd[1]
#set_property -dict { PACKAGE_PIN H1 IOSTANDARD LVCMOS33 } [get_ports { JD[2] }]; #IO_L17P_T2_35 Sch=jd[2]
#set_property -dict { PACKAGE_PIN G1 IOSTANDARD LVCMOS33 } [get_ports { JD[3] }]; #IO_L17N_T2_35 Sch=jd[3]
#set_property -dict { PACKAGE_PIN G3 IOSTANDARD LVCMOS33 } [get_ports { JD[4] }]; #IO_L20N_T3_35 Sch=jd[4]
#set_property -dict { PACKAGE_PIN H2 IOSTANDARD LVCMOS33 } [get_ports { JD[7] }]; #IO_L15P_T2_DQS_35 Sch=jd[7]
#set_property -dict { PACKAGE_PIN G4 IOSTANDARD LVCMOS33 } [get_ports { JD[8] }]; #IO_L20P_T3_35 Sch=jd[8]
#set_property -dict { PACKAGE_PIN G2 IOSTANDARD LVCMOS33 } [get_ports { JD[9] }]; #IO_L15N_T2_DQS_35 Sch=jd[9]
#set_property -dict { PACKAGE_PIN F3 IOSTANDARD LVCMOS33 } [get_ports { JD[10] }]; #IO_L13N_T2_MRCC_35 Sch=jd[10]
##Pmod Header JXADC
#set_property -dict { PACKAGE_PIN A14 IOSTANDARD LVDS } [get_ports { XA_N[1] }]; #IO_L9N_T1_DQS_AD3N_15 Sch=xa_n[1]
#set_property -dict { PACKAGE_PIN A13 IOSTANDARD LVDS } [get_ports { XA_P[1] }]; #IO_L9P_T1_DQS_AD3P_15 Sch=xa_p[1]
#set_property -dict { PACKAGE_PIN A16 IOSTANDARD LVDS } [get_ports { XA_N[2] }]; #IO_L8N_T1_AD10N_15 Sch=xa_n[2]
#set_property -dict { PACKAGE_PIN A15 IOSTANDARD LVDS } [get_ports { XA_P[2] }]; #IO_L8P_T1_AD10P_15 Sch=xa_p[2]
#set_property -dict { PACKAGE_PIN B17 IOSTANDARD LVDS } [get_ports { XA_N[3] }]; #IO_L7N_T1_AD2N_15 Sch=xa_n[3]
#set_property -dict { PACKAGE_PIN B16 IOSTANDARD LVDS } [get_ports { XA_P[3] }]; #IO_L7P_T1_AD2P_15 Sch=xa_p[3]
#set_property -dict { PACKAGE_PIN A18 IOSTANDARD LVDS } [get_ports { XA_N[4] }]; #IO_L10N_T1_AD11N_15 Sch=xa_n[4]
#set_property -dict { PACKAGE_PIN B18 IOSTANDARD LVDS } [get_ports { XA_P[4] }]; #IO_L10P_T1_AD11P_15 Sch=xa_p[4]
##VGA Connector
#set_property -dict { PACKAGE_PIN A3 IOSTANDARD LVCMOS33 } [get_ports { VGA_R[0] }]; #IO_L8N_T1_AD14N_35 Sch=vga_r[0]
#set_property -dict { PACKAGE_PIN B4 IOSTANDARD LVCMOS33 } [get_ports { VGA_R[1] }]; #IO_L7N_T1_AD6N_35 Sch=vga_r[1]
#set_property -dict { PACKAGE_PIN C5 IOSTANDARD LVCMOS33 } [get_ports { VGA_R[2] }]; #IO_L1N_T0_AD4N_35 Sch=vga_r[2]
#set_property -dict { PACKAGE_PIN A4 IOSTANDARD LVCMOS33 } [get_ports { VGA_R[3] }]; #IO_L8P_T1_AD14P_35 Sch=vga_r[3]
#set_property -dict { PACKAGE_PIN C6 IOSTANDARD LVCMOS33 } [get_ports { VGA_G[0] }]; #IO_L1P_T0_AD4P_35 Sch=vga_g[0]
#set_property -dict { PACKAGE_PIN A5 IOSTANDARD LVCMOS33 } [get_ports { VGA_G[1] }]; #IO_L3N_T0_DQS_AD5N_35 Sch=vga_g[1]
#set_property -dict { PACKAGE_PIN B6 IOSTANDARD LVCMOS33 } [get_ports { VGA_G[2] }]; #IO_L2N_T0_AD12N_35 Sch=vga_g[2]
#set_property -dict { PACKAGE_PIN A6 IOSTANDARD LVCMOS33 } [get_ports { VGA_G[3] }]; #IO_L3P_T0_DQS_AD5P_35 Sch=vga_g[3]
#set_property -dict { PACKAGE_PIN B7 IOSTANDARD LVCMOS33 } [get_ports { VGA_B[0] }]; #IO_L2P_T0_AD12P_35 Sch=vga_b[0]
#set_property -dict { PACKAGE_PIN C7 IOSTANDARD LVCMOS33 } [get_ports { VGA_B[1] }]; #IO_L4N_T0_35 Sch=vga_b[1]
#set_property -dict { PACKAGE_PIN D7 IOSTANDARD LVCMOS33 } [get_ports { VGA_B[2] }]; #IO_L6N_T0_VREF_35 Sch=vga_b[2]
#set_property -dict { PACKAGE_PIN D8 IOSTANDARD LVCMOS33 } [get_ports { VGA_B[3] }]; #IO_L4P_T0_35 Sch=vga_b[3]
#set_property -dict { PACKAGE_PIN B11 IOSTANDARD LVCMOS33 } [get_ports { VGA_HS }]; #IO_L4P_T0_15 Sch=vga_hs
#set_property -dict { PACKAGE_PIN B12 IOSTANDARD LVCMOS33 } [get_ports { VGA_VS }]; #IO_L3N_T0_DQS_AD1N_15 Sch=vga_vs
##Micro SD Connector
#set_property -dict { PACKAGE_PIN E2 IOSTANDARD LVCMOS33 } [get_ports { SD_RESET }]; #IO_L14P_T2_SRCC_35 Sch=sd_reset
#set_property -dict { PACKAGE_PIN A1 IOSTANDARD LVCMOS33 } [get_ports { SD_CD }]; #IO_L9N_T1_DQS_AD7N_35 Sch=sd_cd
#set_property -dict { PACKAGE_PIN B1 IOSTANDARD LVCMOS33 } [get_ports { SD_SCK }]; #IO_L9P_T1_DQS_AD7P_35 Sch=sd_sck
#set_property -dict { PACKAGE_PIN C1 IOSTANDARD LVCMOS33 } [get_ports { SD_CMD }]; #IO_L16N_T2_35 Sch=sd_cmd
#set_property -dict { PACKAGE_PIN C2 IOSTANDARD LVCMOS33 } [get_ports { SD_DAT[0] }]; #IO_L16P_T2_35 Sch=sd_dat[0]
#set_property -dict { PACKAGE_PIN E1 IOSTANDARD LVCMOS33 } [get_ports { SD_DAT[1] }]; #IO_L18N_T2_35 Sch=sd_dat[1]
#set_property -dict { PACKAGE_PIN F1 IOSTANDARD LVCMOS33 } [get_ports { SD_DAT[2] }]; #IO_L18P_T2_35 Sch=sd_dat[2]
#set_property -dict { PACKAGE_PIN D2 IOSTANDARD LVCMOS33 } [get_ports { SD_DAT[3] }]; #IO_L14N_T2_SRCC_35 Sch=sd_dat[3]
##Accelerometer
#set_property -dict { PACKAGE_PIN E15 IOSTANDARD LVCMOS33 } [get_ports { ACL_MISO }]; #IO_L11P_T1_SRCC_15 Sch=acl_miso
#set_property -dict { PACKAGE_PIN F14 IOSTANDARD LVCMOS33 } [get_ports { ACL_MOSI }]; #IO_L5N_T0_AD9N_15 Sch=acl_mosi
#set_property -dict { PACKAGE_PIN F15 IOSTANDARD LVCMOS33 } [get_ports { ACL_SCLK }]; #IO_L14P_T2_SRCC_15 Sch=acl_sclk
#set_property -dict { PACKAGE_PIN D15 IOSTANDARD LVCMOS33 } [get_ports { ACL_CSN }]; #IO_L12P_T1_MRCC_15 Sch=acl_csn
#set_property -dict { PACKAGE_PIN B13 IOSTANDARD LVCMOS33 } [get_ports { ACL_INT[1] }]; #IO_L2P_T0_AD8P_15 Sch=acl_int[1]
#set_property -dict { PACKAGE_PIN C16 IOSTANDARD LVCMOS33 } [get_ports { ACL_INT[2] }]; #IO_L20P_T3_A20_15 Sch=acl_int[2]
##Temperature Sensor
#set_property -dict { PACKAGE_PIN C14 IOSTANDARD LVCMOS33 } [get_ports { TMP_SCL }]; #IO_L1N_T0_AD0N_15 Sch=tmp_scl
#set_property -dict { PACKAGE_PIN C15 IOSTANDARD LVCMOS33 } [get_ports { TMP_SDA }]; #IO_L12N_T1_MRCC_15 Sch=tmp_sda
#set_property -dict { PACKAGE_PIN D13 IOSTANDARD LVCMOS33 } [get_ports { TMP_INT }]; #IO_L6N_T0_VREF_15 Sch=tmp_int
#set_property -dict { PACKAGE_PIN B14 IOSTANDARD LVCMOS33 } [get_ports { TMP_CT }]; #IO_L2N_T0_AD8N_15 Sch=tmp_ct
##Omnidirectional Microphone
#set_property -dict { PACKAGE_PIN J5 IOSTANDARD LVCMOS33 } [get_ports { M_CLK }]; #IO_25_35 Sch=m_clk
#set_property -dict { PACKAGE_PIN H5 IOSTANDARD LVCMOS33 } [get_ports { M_DATA }]; #IO_L24N_T3_35 Sch=m_data
#set_property -dict { PACKAGE_PIN F5 IOSTANDARD LVCMOS33 } [get_ports { M_LRSEL }]; #IO_0_35 Sch=m_lrsel
##PWM Audio Amplifier
#set_property -dict { PACKAGE_PIN A11 IOSTANDARD LVCMOS33 } [get_ports { AUD_PWM }]; #IO_L4N_T0_15 Sch=aud_pwm
#set_property -dict { PACKAGE_PIN D12 IOSTANDARD LVCMOS33 } [get_ports { AUD_SD }]; #IO_L6P_T0_15 Sch=aud_sd
##USB-RS232 Interface
#set_property -dict { PACKAGE_PIN C4 IOSTANDARD LVCMOS33 } [get_ports { UART_TXD_IN }]; #IO_L7P_T1_AD6P_35 Sch=uart_txd_in
#set_property -dict { PACKAGE_PIN D4 IOSTANDARD LVCMOS33 } [get_ports { UART_RXD_OUT }]; #IO_L11N_T1_SRCC_35 Sch=uart_rxd_out
#set_property -dict { PACKAGE_PIN D3 IOSTANDARD LVCMOS33 } [get_ports { UART_CTS }]; #IO_L12N_T1_MRCC_35 Sch=uart_cts
#set_property -dict { PACKAGE_PIN E5 IOSTANDARD LVCMOS33 } [get_ports { UART_RTS }]; #IO_L5N_T0_AD13N_35 Sch=uart_rts
##USB HID (PS/2)
#set_property -dict { PACKAGE_PIN F4 IOSTANDARD LVCMOS33 } [get_ports { PS2_CLK }]; #IO_L13P_T2_MRCC_35 Sch=ps2_clk
#set_property -dict { PACKAGE_PIN B2 IOSTANDARD LVCMOS33 } [get_ports { PS2_DATA }]; #IO_L10N_T1_AD15N_35 Sch=ps2_data
##SMSC Ethernet PHY
#set_property -dict { PACKAGE_PIN C9 IOSTANDARD LVCMOS33 } [get_ports { ETH_MDC }]; #IO_L11P_T1_SRCC_16 Sch=eth_mdc
#set_property -dict { PACKAGE_PIN A9 IOSTANDARD LVCMOS33 } [get_ports { ETH_MDIO }]; #IO_L14N_T2_SRCC_16 Sch=eth_mdio
#set_property -dict { PACKAGE_PIN B3 IOSTANDARD LVCMOS33 } [get_ports { ETH_RSTN }]; #IO_L10P_T1_AD15P_35 Sch=eth_rstn
#set_property -dict { PACKAGE_PIN D9 IOSTANDARD LVCMOS33 } [get_ports { ETH_CRSDV }]; #IO_L6N_T0_VREF_16 Sch=eth_crsdv
#set_property -dict { PACKAGE_PIN C10 IOSTANDARD LVCMOS33 } [get_ports { ETH_RXERR }]; #IO_L13N_T2_MRCC_16 Sch=eth_rxerr
#set_property -dict { PACKAGE_PIN C11 IOSTANDARD LVCMOS33 } [get_ports { ETH_RXD[0] }]; #IO_L13P_T2_MRCC_16 Sch=eth_rxd[0]
#set_property -dict { PACKAGE_PIN D10 IOSTANDARD LVCMOS33 } [get_ports { ETH_RXD[1] }]; #IO_L19N_T3_VREF_16 Sch=eth_rxd[1]
#set_property -dict { PACKAGE_PIN B9 IOSTANDARD LVCMOS33 } [get_ports { ETH_TXEN }]; #IO_L11N_T1_SRCC_16 Sch=eth_txen
#set_property -dict { PACKAGE_PIN A10 IOSTANDARD LVCMOS33 } [get_ports { ETH_TXD[0] }]; #IO_L14P_T2_SRCC_16 Sch=eth_txd[0]
#set_property -dict { PACKAGE_PIN A8 IOSTANDARD LVCMOS33 } [get_ports { ETH_TXD[1] }]; #IO_L12N_T1_MRCC_16 Sch=eth_txd[1]
#set_property -dict { PACKAGE_PIN D5 IOSTANDARD LVCMOS33 } [get_ports { ETH_REFCLK }]; #IO_L11P_T1_SRCC_35 Sch=eth_refclk
#set_property -dict { PACKAGE_PIN B8 IOSTANDARD LVCMOS33 } [get_ports { ETH_INTN }]; #IO_L12P_T1_MRCC_16 Sch=eth_intn
##Quad SPI Flash
#set_property -dict { PACKAGE_PIN K17 IOSTANDARD LVCMOS33 } [get_ports { QSPI_DQ[0] }]; #IO_L1P_T0_D00_MOSI_14 Sch=qspi_dq[0]
#set_property -dict { PACKAGE_PIN K18 IOSTANDARD LVCMOS33 } [get_ports { QSPI_DQ[1] }]; #IO_L1N_T0_D01_DIN_14 Sch=qspi_dq[1]
#set_property -dict { PACKAGE_PIN L14 IOSTANDARD LVCMOS33 } [get_ports { QSPI_DQ[2] }]; #IO_L2P_T0_D02_14 Sch=qspi_dq[2]
#set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports { QSPI_DQ[3] }]; #IO_L2N_T0_D03_14 Sch=qspi_dq[3]
#set_property -dict { PACKAGE_PIN L13 IOSTANDARD LVCMOS33 } [get_ports { QSPI_CSN }]; #IO_L6P_T0_FCS_B_14 Sch=qspi_csn
@@ -1,108 +0,0 @@
## cpu21_board_top.xdc —— Nexys4DDR (xc7a100tcsg324-1) 上板引脚
## 引脚编号取自 ConstraintsData/Nexys4DDR_Master.xdc(母版默认全部注释)。
## 只约束 cpu21_board_top 实际使用的端口。
## 100 MHz 板载时钟(分频器输入)
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { clk100 }]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports { clk100 }]
## CPU 时钟:板上 100 MHz 经 CLK_DIV(=2) 分频到 50 MHz
## (与 cpu21_board_top.v 的 CLK_DIV 保持一致;改分频比时这里也要改。
## 乘除法已是多周期单元,不再需要 16 分频;实测 100 MHz 下剩余违例路径是
## EX 级分支判决/中断保存 → BPB 更新,50 MHz 则余量充足)
create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 2 [get_pins { clk_div_reg/Q }]
## 这里固定按最坏情况 50 MHz(-divide_by 2)约束。跑中断综合测试程序时 RTL 里的
## CPU_DIV 会变成 8(12.5 MHz,见 cpu21_board_top.v 的 CLK_DIV_INTRUPT),实际
## 时钟比这里的约束更慢,因此仍然满足——约束偏严,不会掩盖时序违例。
## (真要改分频比时,两处都要改;把这里的 -divide_by 调大反而会放松约束。)
## 复位按键(CPU_RESETN,低有效)与"继续运行"按键(BTNC)
set_property -dict { PACKAGE_PIN C12 IOSTANDARD LVCMOS33 } [get_ports { rst_n }]
set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { go }]
## 拨码开关 SW[2:0] 作为三路中断源
set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { irq_sw[0] }]
set_property -dict { PACKAGE_PIN L16 IOSTANDARD LVCMOS33 } [get_ports { irq_sw[1] }]
set_property -dict { PACKAGE_PIN M13 IOSTANDARD LVCMOS33 } [get_ports { irq_sw[2] }]
## 16 位 LED:LED[2:0] = 三路中断指示(请求出现即亮,ISR 执行 uret 返回后灭),
## LED[15:3] 熄灭
set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { led[0] }]
set_property -dict { PACKAGE_PIN K15 IOSTANDARD LVCMOS33 } [get_ports { led[1] }]
set_property -dict { PACKAGE_PIN J13 IOSTANDARD LVCMOS33 } [get_ports { led[2] }]
set_property -dict { PACKAGE_PIN N14 IOSTANDARD LVCMOS33 } [get_ports { led[3] }]
set_property -dict { PACKAGE_PIN R18 IOSTANDARD LVCMOS33 } [get_ports { led[4] }]
set_property -dict { PACKAGE_PIN V17 IOSTANDARD LVCMOS33 } [get_ports { led[5] }]
set_property -dict { PACKAGE_PIN U17 IOSTANDARD LVCMOS33 } [get_ports { led[6] }]
set_property -dict { PACKAGE_PIN U16 IOSTANDARD LVCMOS33 } [get_ports { led[7] }]
set_property -dict { PACKAGE_PIN V16 IOSTANDARD LVCMOS33 } [get_ports { led[8] }]
set_property -dict { PACKAGE_PIN T15 IOSTANDARD LVCMOS33 } [get_ports { led[9] }]
set_property -dict { PACKAGE_PIN U14 IOSTANDARD LVCMOS33 } [get_ports { led[10] }]
set_property -dict { PACKAGE_PIN T16 IOSTANDARD LVCMOS33 } [get_ports { led[11] }]
set_property -dict { PACKAGE_PIN V15 IOSTANDARD LVCMOS33 } [get_ports { led[12] }]
set_property -dict { PACKAGE_PIN V14 IOSTANDARD LVCMOS33 } [get_ports { led[13] }]
set_property -dict { PACKAGE_PIN V12 IOSTANDARD LVCMOS33 } [get_ports { led[14] }]
set_property -dict { PACKAGE_PIN V11 IOSTANDARD LVCMOS33 } [get_ports { led[15] }]
## LED16_B:MMIO 写入指示(约 0.34 s @50 MHz;中断程序的 12.5 MHz 下约 1.36 s)
set_property -dict { PACKAGE_PIN R12 IOSTANDARD LVCMOS33 } [get_ports { led_we }]
## 上板调试时可把 MMIO 写入的 LED 寄存器也引到 LED16_R/G,
## 但默认只用一个引脚,避免与 led_we 争用。
## ======================= 扫雷扩展外设 =======================
## 5 向按键:BTNC 已作 go(同时也当鼠标左键),另外 4 个当光标方向
set_property -dict { PACKAGE_PIN M18 IOSTANDARD LVCMOS33 } [get_ports { btnu }]
set_property -dict { PACKAGE_PIN P18 IOSTANDARD LVCMOS33 } [get_ports { btnd }]
set_property -dict { PACKAGE_PIN P17 IOSTANDARD LVCMOS33 } [get_ports { btnl }]
set_property -dict { PACKAGE_PIN M17 IOSTANDARD LVCMOS33 } [get_ports { btnr }]
## SW[3]:插旗模式(1 = 点击时插旗/取消旗)
set_property -dict { PACKAGE_PIN R15 IOSTANDARD LVCMOS33 } [get_ports { sw_flag }]
## SW[15]:选 PS/2 设备(0 = 键盘,1 = 鼠标);工程里插的是哪个就拨到哪边
set_property -dict { PACKAGE_PIN V10 IOSTANDARD LVCMOS33 } [get_ports { hid_sel }]
## 七段数码管(共阳极:位选/段选都是低有效)
set_property -dict { PACKAGE_PIN T10 IOSTANDARD LVCMOS33 } [get_ports { seg[0] }]
set_property -dict { PACKAGE_PIN R10 IOSTANDARD LVCMOS33 } [get_ports { seg[1] }]
set_property -dict { PACKAGE_PIN K16 IOSTANDARD LVCMOS33 } [get_ports { seg[2] }]
set_property -dict { PACKAGE_PIN K13 IOSTANDARD LVCMOS33 } [get_ports { seg[3] }]
set_property -dict { PACKAGE_PIN P15 IOSTANDARD LVCMOS33 } [get_ports { seg[4] }]
set_property -dict { PACKAGE_PIN T11 IOSTANDARD LVCMOS33 } [get_ports { seg[5] }]
set_property -dict { PACKAGE_PIN L18 IOSTANDARD LVCMOS33 } [get_ports { seg[6] }]
set_property -dict { PACKAGE_PIN H15 IOSTANDARD LVCMOS33 } [get_ports { dp }]
set_property -dict { PACKAGE_PIN J17 IOSTANDARD LVCMOS33 } [get_ports { an[0] }]
set_property -dict { PACKAGE_PIN J18 IOSTANDARD LVCMOS33 } [get_ports { an[1] }]
set_property -dict { PACKAGE_PIN T9 IOSTANDARD LVCMOS33 } [get_ports { an[2] }]
set_property -dict { PACKAGE_PIN J14 IOSTANDARD LVCMOS33 } [get_ports { an[3] }]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports { an[4] }]
set_property -dict { PACKAGE_PIN T14 IOSTANDARD LVCMOS33 } [get_ports { an[5] }]
set_property -dict { PACKAGE_PIN K2 IOSTANDARD LVCMOS33 } [get_ports { an[6] }]
set_property -dict { PACKAGE_PIN U13 IOSTANDARD LVCMOS33 } [get_ports { an[7] }]
## VGA(640x480@60,25 MHz 像素节拍由 CPU 时钟使能产生)
set_property -dict { PACKAGE_PIN A3 IOSTANDARD LVCMOS33 } [get_ports { vga_r[0] }]
set_property -dict { PACKAGE_PIN B4 IOSTANDARD LVCMOS33 } [get_ports { vga_r[1] }]
set_property -dict { PACKAGE_PIN C5 IOSTANDARD LVCMOS33 } [get_ports { vga_r[2] }]
set_property -dict { PACKAGE_PIN A4 IOSTANDARD LVCMOS33 } [get_ports { vga_r[3] }]
set_property -dict { PACKAGE_PIN C6 IOSTANDARD LVCMOS33 } [get_ports { vga_g[0] }]
set_property -dict { PACKAGE_PIN A5 IOSTANDARD LVCMOS33 } [get_ports { vga_g[1] }]
set_property -dict { PACKAGE_PIN B6 IOSTANDARD LVCMOS33 } [get_ports { vga_g[2] }]
set_property -dict { PACKAGE_PIN A6 IOSTANDARD LVCMOS33 } [get_ports { vga_g[3] }]
set_property -dict { PACKAGE_PIN B7 IOSTANDARD LVCMOS33 } [get_ports { vga_b[0] }]
set_property -dict { PACKAGE_PIN C7 IOSTANDARD LVCMOS33 } [get_ports { vga_b[1] }]
set_property -dict { PACKAGE_PIN D7 IOSTANDARD LVCMOS33 } [get_ports { vga_b[2] }]
set_property -dict { PACKAGE_PIN D8 IOSTANDARD LVCMOS33 } [get_ports { vga_b[3] }]
set_property -dict { PACKAGE_PIN B11 IOSTANDARD LVCMOS33 } [get_ports { vga_hs }]
set_property -dict { PACKAGE_PIN B12 IOSTANDARD LVCMOS33 } [get_ports { vga_vs }]
## PS/2 鼠标:接**板载 USB HID 口**(板上芯片把 USB 键鼠转成 PS/2)
## Nexys4DDR_Master.xdc 里写明:ps2_clk = F4、ps2_data = B2
## 顶层参数 USE_PS2_MOUSE=1 启用;板载按键仍然可用(两路事件在顶层合并)
## 若改用 Pmod JA 上的 Pmod PS2,把下面两行的引脚换成 C17 / D18 即可
set_property -dict { PACKAGE_PIN F4 IOSTANDARD LVCMOS33 PULLUP true } [get_ports { ps2_clk }]
set_property -dict { PACKAGE_PIN B2 IOSTANDARD LVCMOS33 PULLUP true } [get_ports { ps2_data }]
-13
View File
@@ -1,13 +0,0 @@
000012B7
00100313
0062A023
00131313
00008EB7
006EE463
0080006F
00100313
0000F3B7
00100E13
41C383B3
FE039EE3
FD9FF06F
-90
View File
@@ -1,90 +0,0 @@
00100293
FFF00313
FFF2B513
00001FB7
00AFA023
00133513
00001FB7
00AFA023
0FF00293
FFF00313
0062C533
00001FB7
00AFA023
00100293
01F00313
00629533
00001FB7
00AFA023
FFF00293
01C00313
0062D533
00001FB7
00AFA023
4062D533
00001FB7
00AFA023
4D200293
23700313
02628533
00001FB7
00AFA023
FFF00293
FFF00313
02629533
00001FB7
00AFA023
0262B533
00001FB7
00AFA023
FFF00293
00200313
0262A533
00001FB7
00AFA023
FFF00293
00506863
10100513
00001FB7
00AFA023
60100513
00001FB7
00AFA023
FFF00293
0002DA63
60200513
00001FB7
00AFA023
0100006F
10200513
00001FB7
00AFA023
FFF00293
0002F863
10300513
00001FB7
00AFA023
60300513
00001FB7
00AFA023
FFD00493
00900533
00001FB7
00AFA023
00148493
FE9058E3
00300293
00500533
00001FB7
00AFA023
FFF28293
00100393
FE72F6E3
00400493
00900533
00001FB7
00AFA023
FFF48493
FE9068E3
00A00893
00000073
-19
View File
@@ -1,19 +0,0 @@
123452B7
00500533
00001FB7
00AFA023
FFFFF2B7
00500533
00001FB7
00AFA023
800002B7
00500533
00001FB7
00AFA023
123452B7
67828293
00500533
00001FB7
00AFA023
00A00893
00000073
-16
View File
@@ -1,16 +0,0 @@
000012B7
12345337
67830313
0062A023
0002A503
00001FB7
00AFA023
FFF00393
0072A023
07F00E13
01C280A3
0002A503
00001FB7
00AFA023
00A00893
00000073
-64
View File
@@ -1,64 +0,0 @@
00700313
00600393
02730533
00001FB7
00AFA023
FFB00313
00400393
02730533
00001FB7
00AFA023
40000337
00400393
02731533
00001FB7
00AFA023
FFF00313
00200393
02733533
00001FB7
00AFA023
FFF00313
FFF00393
02732533
00001FB7
00AFA023
06400313
00700393
02735533
00001FB7
00AFA023
02737533
00001FB7
00AFA023
FFF00313
00200393
02735533
00001FB7
00AFA023
00003337
03930313
00000393
02735533
00001FB7
00AFA023
02737533
00001FB7
00AFA023
FFF00313
01000393
02737533
00001FB7
00AFA023
12345337
67830313
01000393
02730533
00001FB7
00AFA023
000013B7
02731533
00001FB7
00AFA023
00A00893
00000073
-281
View File
@@ -1,281 +0,0 @@
`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 - 0x2FFF 显存窗口:40x30 格、每格 2 字节(共 2400 字节)
//
// 鼠标寄存器(基址 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[1] = IRQ2 = 入口 0x200)
// level_irq_o = LEVEL 寄存器(软件中断,顶层接到 irq_i[0] = IRQ1 = 入口 0x100 = 关卡)
//
// 以后要增加外设(UART、SD 卡等)时只需在这里扩展地址译码。
// -----------------------------------------------------------------------------
module cpu21_mmio #(
parameter [31:0] LED_ADDR = 32'h0000_1000,
parameter integer FB_WORDS = 600, // 600 字 = 2400 字节(40x30 格,每格 2 字节)
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 - 0x2FFF:共 600 个字(每格两个字节)。
wire sel_fb = (addr_i[31:12] == 20'h00002);
wire [3:0] midx = addr_i[5:2];
wire [3:0] didx = addr_i[5:2];
wire [9:0] fb_widx = addr_i[11: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
-109
View File
@@ -1,109 +0,0 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_pointer —— 把板上的 5 向按键变成"鼠标"事件源(板级顶层用它给 MMIO 送事件)。
//
// 事件单位:1/8 格。按键一次 = 8 个单位 = 光标移动一格,这样软件端用
// 纯整数加减(不需要任何除法)就能把累积量换算成格,配合 PS/2 鼠标时
// 手感也一致(PS/2 的原始计数直接按 1/8 格处理)。
//
// BTNU -> dy = -8 BTND -> dy = +8
// BTNL -> dx = -8 BTNR -> dx = +8
// BTNC -> 左键按下(btn[0] = 1,dx = dy = 0)
//
// 去抖方式:用 ~1 ms 的采样节拍(50 MHz 下计数 50000)采样两级同步后的按键,
// 与上一次的稳定值比较,只在"按下"的边沿产生事件。一次采样可能同时检测到
// 多个按下,此时用 press_pend_q 排队,由优先级选择器逐个上报(每个时钟周期
// 最多一个事件),因此不会丢按键。
// -----------------------------------------------------------------------------
module cpu21_pointer #(
// 采样节拍 = clk / (SAMPLE_DIV),默认 50 MHz / 50000 = 1 kHz
parameter integer SAMPLE_DIV = 50000
) (
input wire clk,
input wire reset,
input wire [4:0] btn_i, // {BTNC, BTNU, BTND, BTNL, BTNR}
output wire ev_valid_o, // 单周期脉冲
output wire [7:0] ev_dx_o, // 有符号,1/8 格
output wire [7:0] ev_dy_o,
output wire [4:0] ev_btn_o
);
// ---------------------------------------------------------------------------
// 1 ms 采样节拍
// ---------------------------------------------------------------------------
reg [15:0] div_q;
wire sample = (div_q == (SAMPLE_DIV - 1));
always @(posedge clk or posedge reset) begin
if (reset)
div_q <= 16'd0;
else if (sample)
div_q <= 16'd0;
else
div_q <= div_q + 16'd1;
end
// ---------------------------------------------------------------------------
// 两级同步 + 采样保持(采样率远低于按键抖动频率,等效去抖)
// ---------------------------------------------------------------------------
reg [4:0] btn_sync1_q, btn_sync2_q, btn_stable_q;
always @(posedge clk or posedge reset) begin
if (reset) begin
btn_sync1_q <= 5'b0;
btn_sync2_q <= 5'b0;
btn_stable_q <= 5'b0;
end
else begin
btn_sync1_q <= btn_i;
btn_sync2_q <= btn_sync1_q;
if (sample)
btn_stable_q <= btn_sync2_q;
end
end
// 只在按下的边沿产生新事件
wire [4:0] press_new = sample ? (btn_sync2_q & ~btn_stable_q) : 5'b0;
// ---------------------------------------------------------------------------
// 待上报队列 + 优先级选择(每周期最多上报一个)
// ---------------------------------------------------------------------------
reg [4:0] press_pend_q;
wire [4:0] pend_next = press_pend_q | press_new;
reg [4:0] sel;
always @* begin
sel = 5'b0;
// 注意:sel 的每一位必须和下面的输出一致,也必须和 pend_next 一一对应!
// sel[4]=点击 sel[3]=上(-dy) sel[2]=下(+dy) sel[1]=左(-dx) sel[0]=右(+dx)
// 而且 press_pend_q <= pend_next & ~sel 要求 sel 的位数与 pend_next 同域,
// 否则被选中的那一位清不掉、ev_valid_o 会永久为 1(中断线一直挂着)。
if (pend_next[4])
sel = 5'b10000; // BTNC:点击
else if (pend_next[3])
sel = 5'b01000; // BTNU:上
else if (pend_next[2])
sel = 5'b00100; // BTND:下
else if (pend_next[1])
sel = 5'b00010; // BTNL:左
else if (pend_next[0])
sel = 5'b00001; // BTNR:右
end
always @(posedge clk or posedge reset) begin
if (reset)
press_pend_q <= 5'b0;
else
press_pend_q <= pend_next & ~sel;
end
assign ev_valid_o = |pend_next;
assign ev_dx_o = sel[1] ? 8'd248 : (sel[0] ? 8'd8 : 8'd0); // 248 = -8 的补码
assign ev_dy_o = sel[3] ? 8'd248 : (sel[2] ? 8'd8 : 8'd0);
assign ev_btn_o = {4'b0, sel[4]};
endmodule
`default_nettype wire
-151
View File
@@ -1,151 +0,0 @@
`timescale 1ns / 1ps
// =============================================================================
// cpu21_ps2_kbd —— PS/2 键盘接收(扫描码集 2),把按键映射成"鼠标事件"
//
// 为什么映射成鼠标事件:saolei.c / isr.c 早就是按鼠标事件 {btn, dy, dx} 写的,
// 键盘只要产出**同样格式**的事件,软件一行都不用改。
//
// ↑ ↓ ← → 光标移动(dx/dy = ±8,恰好是软件里的"一格")
// W A S D 同上(别名,与 tools/play_saolei.tcl 一致)
// Enter / 空格 左键(开格)
// 左 Shift / F / C 右键(软件里右键 = 插旗)
//
// 方向约定跟软件对齐:**dy = +8 是向下**(与 cpu21_pointer 的 BTND、cpu21_ps2_mouse 一致),
// 所以 ↑ 发 dy = 248(= -8 的补码)。
//
// PS/2 键盘发扫描码(集 2):
// 0xF0 前缀 -> 下一个码是"松开"(break);0xE0 前缀 -> 下一个码是扩展键
// 方向键是扩展键:↑ = E0 75、↓ = E0 72、← = E0 6B、→ = E0 74
// 所以只对"按下"(make)产生事件。按住不动时键盘自己会发重复码,
// 于是按住方向键正好变成"连续移动"。
//
// 与 cpu21_ps2_mouse 共用同一根 PS/2 线:两个接收器都挂在线上,只有插着的那个
// 设备会产生有效数据,所以顶层把两者的事件一起 OR 起来即可,不用二选一。
// =============================================================================
module cpu21_ps2_kbd (
input wire clk,
input wire reset,
input wire ps2_clk_i,
input wire ps2_data_i,
output reg ev_valid_o, // 单周期脉冲
output reg [7:0] ev_dx_o, // 有符号,1/8 格(±8 = 一格)
output reg [7:0] ev_dy_o,
output reg [4:0] ev_btn_o, // bit0 = 左键, bit1 = 右键
output reg [15:0] rx_count_o // 收到的字节数(调试观察用)
);
// ---- 两级同步 ----
reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;
always @(posedge clk or posedge reset) begin
if (reset) begin
ps2_clk_s1 <= 1'b1;
ps2_clk_s2 <= 1'b1;
ps2_data_s1 <= 1'b1;
ps2_data_s2 <= 1'b1;
end
else begin
ps2_clk_s1 <= ps2_clk_i;
ps2_clk_s2 <= ps2_clk_s1;
ps2_data_s1 <= ps2_data_i;
ps2_data_s2 <= ps2_data_s1;
end
end
wire clk_fall = ~ps2_clk_s1 & ps2_clk_s2;
reg [3:0] bit_cnt_q;
reg [10:0] frame_q;
reg brk_q; // 刚收到 0xF0:下一个码是松开
reg ext_q; // 刚收到 0xE0:下一个码是扩展键
// 11 位里 data + parity 共 9 位,PS/2 是**奇校验**:1 的个数应为奇数
function automatic parity_ok(input [8:0] dp); // dp = {parity, d7..d0}
parity_ok = ^dp;
endfunction
// 处理一个扫描码(按下才产生事件)
task handle_code;
input [7:0] sc;
reg pressed;
begin
rx_count_o <= rx_count_o + 16'd1;
pressed = (brk_q == 1'b0); // 0xF0 之后的码是"松开"
brk_q <= 1'b0;
if (sc == 8'hE0)
ext_q <= 1'b1; // 扩展前缀
else if (sc == 8'hF0)
brk_q <= 1'b1; // 断码前缀
else if (pressed) begin
if (ext_q) begin
case (sc)
8'h75: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd248; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ↑
8'h72: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd8; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ↓
8'h6B: begin ev_dx_o <= 8'd248; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // ←
8'h74: begin ev_dx_o <= 8'd8; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // →
default: ;
endcase
end
else begin
case (sc)
8'h5A: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00001; ev_valid_o <= 1'b1; end // Enter
8'h29: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00001; ev_valid_o <= 1'b1; end // 空格
8'h12: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // 左 Shift
8'h2B: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // F
8'h2E: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd0; ev_btn_o <= 5'b00010; ev_valid_o <= 1'b1; end // C
// W/A/S/D 也当方向键(和 tools/play_saolei.tcl 里的别名保持一致)
8'h1D: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd248; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // W = ↑
8'h1B: begin ev_dx_o <= 8'd0; ev_dy_o <= 8'd8; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // S = ↓
8'h1C: begin ev_dx_o <= 8'd248; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // A = ←
8'h23: begin ev_dx_o <= 8'd8; ev_dy_o <= 8'd0; ev_btn_o <= 5'd0; ev_valid_o <= 1'b1; end // D = →
default: ;
endcase
end
end
// 一次前缀组合处理完,清掉扩展标志(松开码 E0 F0 xx 也走这里)
if ((sc != 8'hE0) && (sc != 8'hF0))
ext_q <= 1'b0;
end
endtask
always @(posedge clk or posedge reset) begin
if (reset) begin
bit_cnt_q <= 4'd0;
frame_q <= 11'd0;
brk_q <= 1'b0;
ext_q <= 1'b0;
ev_valid_o <= 1'b0;
ev_dx_o <= 8'd0;
ev_dy_o <= 8'd0;
ev_btn_o <= 5'd0;
rx_count_o <= 16'd0;
end
else begin
ev_valid_o <= 1'b0;
if (clk_fall) begin
// frame_q[i] = 这一帧的第 i 位:0 = 起始,1..8 = 数据 d0..d7(LSB 先),
// 9 = 校验,10 = 停止。按”位的下标“存,不要再整体右移:
// 右移的话停止位还没进来就先去判 frame_q[10],判到的是校验位。
if (bit_cnt_q == 4'd0) begin
if (ps2_data_s2 == 1'b0) // 起始位有效,开始收
bit_cnt_q <= 4'd1;
end
else begin
frame_q[bit_cnt_q] <= ps2_data_s2;
if (bit_cnt_q == 4'd10) begin
bit_cnt_q <= 4'd0;
// 停止位就是刚采到的这一位(frame_q[10] 本拍还没更新到),
// 校验位查 data + parity 的奇校验
if ((ps2_data_s2 == 1'b1) && parity_ok(frame_q[9:1]))
handle_code(frame_q[8:1]);
end
else
bit_cnt_q <= bit_cnt_q + 4'd1;
end
end
end
end
endmodule
-158
View File
@@ -1,158 +0,0 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_ps2_mouse —— 可选的真鼠标输入:PS/2 鼠标(Digilent Pmod PS2 插在 JA 上)。
//
// JA[1] = PS/2 DATA,JA[2] = PS/2 CLK(两条线都需要上拉)。当前 XDC 把 ps2_clk /
// ps2_data 接到板载 USB HID 口的 PS/2 侧(F4 / B2),也可以改回 JA(C17 / D18)。
//
// 注意:接收/解析逻辑(包括帧对齐与奇校验)已由 srcs/tb/tb_ps2_mouse.v 仿真验证;
// 板上那一侧能不能收到数据,取决于板载 USB→PS/2 桥接芯片是否肯转发你插的设备。
// 如果你不用真鼠标,请让顶层用 cpu21_pointer(5 向按键)作为事件源即可,
// C 程序完全不用改。板级顶层 cpu21_board_top 用参数 USE_PS2_MOUSE 选择这两者。
//
// 实现内容:
// 1) 接收:在 ps2_clk 的下降沿采样 ps2_data,收满 11 位(起始 0 + 8 数据
// LSB 先 + 奇校验 + 停止 1)后校验并拼成 3 字节一包;
// frame_q[i] = 这一帧的第 i 位(0 起始、1..8 = d0..d7、9 校验、10 停止),
// 所以字节 = frame_q[8:1](frame_q[1] 就是 d0),校验 = frame_q[9]。
// 注意 PS/2 是**奇校验**:data + parity 9 位里 1 的个数为奇数。
// 2) 解析:byte0 = 标志(bit0 左键、bit1 右键、bit3 固定 1),byte1 = dx,
// byte2 = dy,bit7 为符号位;dx 正 = 右,**dy 正 = 上**(PS/2 的原生方向),
// 而软件里正 dy = 下,所以上报前把 dy 取负,让鼠标和板载按键/键盘方向一致;
// 3) 上报:当 dx/dy 非 0 或按键状态变化时产生一个事件(单位与
// cpu21_pointer 相同,即原始计数 = 1/8 格);
// 4) 本模块只做"接收",不实现主机→鼠标的命令发送(0xF4 使能上报)。多数
// PS/2 鼠标上电后就会主动上报,这类鼠标可以直接用;如果你的鼠标上电毫无
// 反应,说明它需要主机先发 0xF4 —— 那部分需要自行补上,或者直接用默认的
// 5 向按键(cpu21_pointer),C 程序完全不用改。
// -----------------------------------------------------------------------------
module cpu21_ps2_mouse (
input wire clk,
input wire reset,
// PS/2 双向线(开漏)
input wire ps2_clk_i,
output wire ps2_clk_oe_o,
input wire ps2_data_i,
output wire ps2_data_oe_o,
// 事件上报
output reg ev_valid_o,
output reg [7:0] ev_dx_o,
output reg [7:0] ev_dy_o,
output reg [4:0] ev_btn_o,
// 调试:收到的字节数(可选观察)
output reg [15:0] rx_count_o
);
// 开漏:oe = 1 时把线拉低,否则高阻(外部上拉)。本模块只接收,两条线都不驱动。
assign ps2_clk_oe_o = 1'b0;
assign ps2_data_oe_o = 1'b0;
// 两级同步
reg ps2_clk_s1, ps2_clk_s2, ps2_data_s1, ps2_data_s2;
always @(posedge clk or posedge reset) begin
if (reset) begin
ps2_clk_s1 <= 1'b1;
ps2_clk_s2 <= 1'b1;
ps2_data_s1 <= 1'b1;
ps2_data_s2 <= 1'b1;
end
else begin
ps2_clk_s1 <= ps2_clk_i;
ps2_clk_s2 <= ps2_clk_s1;
ps2_data_s1 <= ps2_data_i;
ps2_data_s2 <= ps2_data_s1;
end
end
wire clk_fall = ~ps2_clk_s1 & ps2_clk_s2; // 同步后的下降沿
wire clk_rise = ps2_clk_s1 & ~ps2_clk_s2;
// ---------------------------------------------------------------------------
// 接收移位:11 位一帧,bit0 = 起始位,bit1..8 = 数据,bit9 = 校验,bit10 = 停止
// ---------------------------------------------------------------------------
reg [3:0] bit_cnt_q;
reg [10:0] frame_q;
reg frame_ok_q;
reg [23:0] pkt_q; // 3 字节包
reg [1:0] pkt_cnt_q;
reg [7:0] byte_cnt_q; // 一包内的字节序号(0..2)
reg [7:0] dx_q, dy_q;
reg [4:0] btn_q; // {4'b0, middle, right, left} -> 只取低 3 位使用
// 校验:PS/2 是**奇校验**,data 8 位 + parity 位共 9 位里 1 的个数应为奇数
function automatic parity_ok(input [8:0] dp); // dp = {parity, d7..d0}
parity_ok = ^dp;
endfunction
always @(posedge clk or posedge reset) begin
if (reset) begin
bit_cnt_q <= 4'd0;
frame_q <= 11'd0;
frame_ok_q <= 1'b0;
byte_cnt_q <= 8'd0;
pkt_q <= 24'd0;
rx_count_o <= 16'd0;
ev_valid_o <= 1'b0;
end
else begin
ev_valid_o <= 1'b0;
if (clk_fall) begin
// frame_q[i] = 这一帧的第 i 位:0 = 起始,1..8 = 数据 d0..d7(LSB 先),
// 9 = 校验,10 = 停止。按”位的下标“存,不要再整体右移:
// 右移的话停止位还没进来就先去判 frame_q[10],判到的是校验位。
if (bit_cnt_q == 4'd0) begin
if (ps2_data_s2 == 1'b0) // 起始位有效,开始收
bit_cnt_q <= 4'd1;
end
else begin
frame_q[bit_cnt_q] <= ps2_data_s2;
if (bit_cnt_q == 4'd10) begin
bit_cnt_q <= 4'd0;
// 停止位就是刚采到的这一位(frame_q[10] 本拍还没更新到),
// 校验查 data + parity 的奇校验
if ((ps2_data_s2 == 1'b1) && parity_ok(frame_q[9:1])) begin
rx_count_o <= rx_count_o + 16'd1;
case (byte_cnt_q)
8'd0: begin
// 标志字节:bit3 必须为 1(否则是鼠标发来的其他响应,丢弃)
// 数据字节 = frame_q[8:1],它的 bit3 就是 frame_q[4]
// (不能写 frame_q[8:1][3]:Verilog-2001 不允许 part-select 再取位)
if (frame_q[4] == 1'b1) begin
pkt_q[7:0] <= frame_q[8:1];
byte_cnt_q <= 8'd1;
end
end
8'd1: begin
pkt_q[15:8] <= frame_q[8:1]; // dx
byte_cnt_q <= 8'd2;
end
8'd2: begin
byte_cnt_q <= 8'd0;
ev_dx_o <= pkt_q[15:8]; // byte1 = dx(正 = 右,与软件一致)
// byte2 = dy:**PS/2 的正 dy 是鼠标上移**,而软件里正 dy = 光标下移
// (与 cpu21_pointer 的 BTND = +8 一致),所以这里要取负。
ev_dy_o <= 8'd0 - frame_q[8:1];
// 与 cpu21_mmio / mmio.h 一致:bit0=左键, bit1=右键, bit2=中键
ev_btn_o <= {2'b0, pkt_q[2], pkt_q[1], pkt_q[0]};
ev_valid_o <= 1'b1;
end
default:
byte_cnt_q <= 8'd0;
endcase
end
end
else begin
bit_cnt_q <= bit_cnt_q + 4'd1;
end
end
end
end
end
endmodule
`default_nettype wire
-65
View File
@@ -1,65 +0,0 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// ALU:组合运算单元(只做加减/移位/逻辑/比较等单拍运算)。
//
// 乘法与除法已移到 cpu21_riscv_muldiv.v(时序多周期单元),因为组合的 a*b / a%b
// 关键路径过长(综合后约 94 ns,100 MHz 下无法收敛)。因此本模块不再实现
// ALU_MUL / ALU_DIVU:这两个操作码只作为“这是乘除法指令”的标识,对应结果由
// 顶层从 muldiv 的输出选取(result2 的 wb_sel=3 约定保持不变)。
//
// result 为主结果;result2 为第二结果,由译码器用 wb_sel=3 选取。
// -----------------------------------------------------------------------------
module cpu21_riscv_alu (
input wire [ 3:0] op,
input wire [31:0] a,
input wire [31:0] b,
output reg [31:0] result,
output reg [31:0] result2
);
localparam ALU_SLL = 4'd0;
localparam ALU_SRA = 4'd1;
localparam ALU_SRL = 4'd2;
localparam ALU_MUL = 4'd3;
localparam ALU_DIVU = 4'd4;
localparam ALU_ADD = 4'd5;
localparam ALU_SUB = 4'd6;
localparam ALU_AND = 4'd7;
localparam ALU_OR = 4'd8;
localparam ALU_XOR = 4'd9;
localparam ALU_NOR = 4'd10;
localparam ALU_SLT = 4'd11;
localparam ALU_SLTU = 4'd12;
localparam ALU_MULH = 4'd13;
localparam ALU_MULHSU = 4'd14;
localparam ALU_PASSB = 4'd15; // 直接输出 B 端(供 LUI 使用)
always @* begin
result = 32'b0;
result2 = 32'b0;
case (op)
ALU_SLL: result = a << b[4:0];
ALU_SRA: result = $signed(a) >>> b[4:0];
ALU_SRL: result = a >> b[4:0];
// 乘法(ALU_MUL)与除法(ALU_DIVU)在本模块内不产生结果:
// 它们由 cpu21_riscv_muldiv 多周期单元完成,顶层用 ex_muldiv 选择其输出。
ALU_MUL: ;
ALU_DIVU: ;
ALU_ADD: result = a + b;
ALU_SUB: result = a - b;
ALU_AND: result = a & b;
ALU_OR: result = a | b;
ALU_XOR: result = a ^ b;
ALU_NOR: result = ~(a | b);
ALU_SLT: result = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0;
ALU_SLTU: result = (a < b) ? 32'd1 : 32'd0;
ALU_MULH: ;
ALU_MULHSU: ;
ALU_PASSB: result = b;
default: result = 32'b0;
endcase
end
endmodule
`default_nettype wire
-169
View File
@@ -1,169 +0,0 @@
`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
@@ -1,900 +0,0 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21-risc-v-4.circ -> 可综合 Verilog 实现(顶层)
//
// 处理器为五级流水线 IF/ID/EX/MEM/WB。本文件只保留:
// * 五级流水寄存器与各级之间的连线;
// * 各功能子模块的例化;
// * PC / IF-ID / ID-EX 的控制优先级(中断、URET、停机、重定向、停顿、
// 正常推进);
// * 对外输出与调试信号。
//
// 各功能单元已拆分为独立文件,便于单独仿真调试:
// cpu21_riscv_alu.v 组合 ALU(result2 用于 MUL 高位 / 余数)
// cpu21_bpb_8.v 八表项全相联分支目标缓冲
// cpu21_riscv_decoder.v ID 级组合译码器
// cpu21_riscv_regfile.v 寄存器堆(写优先旁路)
// cpu21_riscv_forward_unit.v EX 级前递网络
// cpu21_riscv_store_unit.v EX 级存储数据对齐
// cpu21_riscv_branch_unit.v EX 级控制流裁决
// cpu21_riscv_hazard_unit.v load-use 冒险与冲刷/停顿判定
// cpu21_riscv_irq_ctrl.v 中断控制、ustatus/uepc 与嵌套返回栈
// cpu21_riscv_perf_counters.v 性能计数器
// cpu21_mmio.v MMIO:LED 数据寄存器(可写、可读回)
//
// 主要特性:
// * EX 级前递(forwarding)与 load-use 冒险互锁;
// * EX 级控制流裁决与流水线重定向;
// * 八表项全相联分支目标缓冲(BPB);
// * 三路采样的中断源,带优先级与可嵌套的 URET 返回;
// * LED 显示通过 MMIO 完成:向 LED_ADDR(默认 0x0000_1000)存储即写 LED,
// 也可以从该地址读回 LED 当前值;
// * ecall 仍用于停机:a7==34 的历史“显示”调用保留为无操作,其余(如 a7=10)
// 令流水线停机并等待 go_i。
//
// 存储器接口约定:
// instr_addr_o 与 data_addr_o 均为字节地址。data_wstrb_o 是字节写掩码,
// 因此外部字宽 RAM 可用 data_addr_o[11:2] 作为字索引,再依据
// data_wstrb_o 完成字节合并。
// -----------------------------------------------------------------------------
module cpu21_riscv_redirect_int_bpb #(
parameter RESET_PC = 32'h0000_0000,
parameter IRQ1_VECTOR = 32'h0000_30ac,
parameter IRQ2_VECTOR = 32'h0000_31e4,
parameter IRQ3_VECTOR = 32'h0000_3310,
parameter IRQ_STACK_DEPTH = 4,
// MMIO:LED 数据寄存器地址(默认 0x0000_1000,位于 4 KB 数据 RAM 之外)。
parameter [31:0] LED_ADDR = 32'h0000_1000,
// ustatus 复位值。详见 cpu21_riscv_irq_ctrl 中的说明。
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 显存字数:600 字 = 2400 字节 = 40x30 格、每格 2 字节(字形 + 颜色)。
// 必须传给 cpu21_mmio:它的 fb_ok 是 (fb_widx < FB_WORDS),不传就会
// 把显存写卡在前 300 字(前 15 行),屏幕下半部分永远刷不出来。
parameter integer FB_WORDS = 600
) (
input wire clk,
input wire reset,
input wire [31:0] instr_i,
input wire [31:0] data_rdata_i,
input wire [ 2:0] irq_i,
input wire go_i,
output wire [31:0] instr_addr_o,
output wire [31:0] data_addr_o,
output wire [31:0] data_wdata_o,
output wire [ 3:0] data_wstrb_o,
output wire data_we_o,
output wire [31:0] led_data_o,
output wire led_valid_o,
// ---- 扫雷扩展外设(由 cpu21_mmio 驱动;板级顶层接 VGA/数码管/鼠标)----
output wire [31:0] seg_data_o, // 七段数码管:8 位十六进制数
output wire seg_valid_o,
output wire fb_we_o, // 显存写端口(板级顶层的双口 RAM)
output wire [ 9:0] fb_waddr_o, // 显存字地址(0..599)
output wire [31:0] fb_wdata_o,
output wire [ 3:0] fb_wstrb_o,
input wire [31:0] fb_rdata_i, // 显存读回(组合)
output wire [31:0] disp_ctrl_o, // 显示控制器寄存器(给 VGA)
output wire [31:0] disp_cursor_o,
input wire mouse_ev_valid_i, // 鼠标事件源(板级顶层:按键去抖 / PS/2)
input wire [ 7:0] mouse_dx_i, // 单位 1/8 格,有符号
input wire [ 7:0] mouse_dy_i,
input wire [ 4:0] mouse_btn_i, // {BTNC,BTNU,BTND,BTNL,BTNR}
input wire [15:0] sw_i, // 板载拨码(软件读 INPUT 寄存器)
input wire frame_tick_i, // VGA 每帧一个脉冲
output wire mouse_irq_o, // 鼠标事件中断(板级顶层接 irq_i[0])
output wire level_irq_o, // 软件中断:关卡入口(接 irq_i[1])
output wire [31:0] if_pc_o,
output wire [31:0] id_pc_o,
output wire [31:0] ex_pc_o,
output wire [31:0] mem_pc_o,
output wire [31:0] wb_pc_o,
output wire [31:0] if_ir_o,
output wire [31:0] id_ir_o,
output wire [31:0] ex_ir_o,
output wire [31:0] mem_ir_o,
output wire [31:0] wb_ir_o,
output wire [31:0] rdin_o,
output wire [31:0] mdin_o,
output wire reg_write_o,
output wire mem_write_o,
output wire halted_o,
output wire [ 2:0] irq_pending_o,
output wire [ 1:0] irq_current_level_o,
output wire [31:0] uepc_o,
output wire [31:0] ustatus_o,
output wire bpb_predict_hit_o,
output wire bpb_predict_taken_o,
output wire bpb_mispredict_o,
output wire [15:0] cycle_count_o,
output wire [15:0] stall_count_o,
output wire [15:0] bubble_count_o,
output wire [15:0] conditional_taken_count_o,
output wire [15:0] unconditional_branch_count_o,
output wire [15:0] prediction_success_count_o,
output wire [15:0] prediction_failure_count_o
);
// 顶层只用到少量常量,完整译码表见 cpu21_riscv_decoder.v。
localparam OP_BRANCH = 5'h18;
localparam ALU_ADD = 4'd5;
// -------------------------------------------------------------------------
// 流水线寄存器
// -------------------------------------------------------------------------
// 当前取指 PC。
reg [31:0] pc_q;
// IF/ID 流水寄存器(含本条指令所携带的分支预测信息)。
reg ifid_valid_q;
reg [31:0] ifid_pc_q;
reg [31:0] ifid_ir_q;
reg ifid_pred_taken_q;
reg [31:0] ifid_pred_target_q;
// ID/EX 流水寄存器:保存译码结果与操作数。
reg idex_valid_q;
reg [31:0] idex_pc_q;
reg [31:0] idex_ir_q;
reg [31:0] idex_rs1_value_q;
reg [31:0] idex_rs2_value_q;
reg [4:0] idex_rs1_idx_q;
reg [4:0] idex_rs2_idx_q;
reg [4:0] idex_rd_q;
reg [31:0] idex_imm_q;
reg [3:0] idex_aluop_q;
reg idex_alu_src_q;
reg idex_reg_write_q;
reg idex_mem_to_reg_q;
reg idex_mem_write_q;
reg idex_mem_byte_q;
reg [2:0] idex_wb_sel_q;
reg [2:0] idex_md_op_q; // 乘除法操作类型(0=非乘除法)
reg [11:0] idex_csr_addr_q;
reg idex_branch_q;
reg idex_beq_q;
reg idex_bne_q;
reg idex_blt_q;
reg idex_bge_q;
reg idex_bltu_q;
reg idex_bgeu_q;
reg idex_jal_q;
reg idex_jalr_q;
reg idex_ecall_q;
reg idex_uret_q;
reg idex_csr_set_q;
reg idex_csr_clear_q;
reg idex_csr_write_q;
reg idex_pred_taken_q;
reg [31:0] idex_pred_target_q;
// EX/MEM 流水寄存器:保存 ALU/存储结果与写回控制。
reg exmem_valid_q;
reg [31:0] exmem_pc_q;
reg [31:0] exmem_ir_q;
reg [31:0] exmem_alu_result_q;
reg [31:0] exmem_alu_result2_q;
reg [31:0] exmem_csr_old_q;
reg [31:0] exmem_write_data_q;
reg [4:0] exmem_rd_q;
reg exmem_reg_write_q;
reg exmem_mem_to_reg_q;
reg exmem_mem_write_q;
reg [3:0] exmem_wstrb_q;
reg [2:0] exmem_wb_sel_q;
// MEM/WB 流水寄存器:保存访存数据与最终写回信息。
reg memwb_valid_q;
reg [31:0] memwb_pc_q;
reg [31:0] memwb_ir_q;
reg [31:0] memwb_alu_result_q;
reg [31:0] memwb_alu_result2_q;
reg [31:0] memwb_csr_old_q;
reg [31:0] memwb_mem_data_q;
reg [4:0] memwb_rd_q;
reg memwb_reg_write_q;
reg memwb_mem_to_reg_q;
reg [2:0] memwb_wb_sel_q;
// 停机标志(LED 寄存器已移入 cpu21_mmio.v)。
reg halted_q;
// -------------------------------------------------------------------------
// IF 级:分支预测。JAL/JALR 在 EX 级裁决,条件分支则可以直接依据 BPB
// 给出的目标地址重定向取指 PC。
// -------------------------------------------------------------------------
wire [4:0] f_opcode = instr_i[6:2];
wire [2:0] f_funct3 = instr_i[14:12];
wire f_is_branch = (f_opcode == OP_BRANCH) &&
((f_funct3 == 3'b000) || (f_funct3 == 3'b001) ||
(f_funct3 == 3'b100) || (f_funct3 == 3'b101) ||
(f_funct3 == 3'b110) || (f_funct3 == 3'b111));
wire [31:0] bpb_predict_target;
wire bpb_predict_hit;
wire bpb_predict_taken;
wire ex_branch_taken;
wire [31:0] ex_target;
wire ex_controlflow;
wire ex_redirect_valid;
wire [31:0] ex_redirect_pc;
// BPB 更新端口:在 EX 级用条件分支的实际结果训练预测器。
wire bpb_update_enable = idex_valid_q && idex_branch_q;
wire bpb_update_taken = ex_branch_taken;
wire [31:0] bpb_update_target = ex_target;
// 分支目标缓冲实例。
cpu21_bpb_8 u_bpb (
.clk (clk),
.reset (reset),
.predict_pc (pc_q),
.predict_enable (f_is_branch),
.predict_hit (bpb_predict_hit),
.predict_taken (bpb_predict_taken),
.predict_target (bpb_predict_target),
.update_enable (bpb_update_enable),
.update_controlflow(1'b1),
.update_pc (idex_pc_q),
.update_target (bpb_update_target),
.update_taken (bpb_update_taken)
);
// 下一取指 PC:预测跳转命中时使用 BPB 目标地址,否则顺序加 4。
wire [31:0] fetch_next_pc =
(f_is_branch && bpb_predict_hit && bpb_predict_taken) ?
bpb_predict_target : (pc_q + 32'd4);
// -------------------------------------------------------------------------
// ID 级:译码与寄存器堆读取
// -------------------------------------------------------------------------
wire [4:0] d_src1_idx;
wire [4:0] d_src2_idx;
wire [4:0] d_rd;
wire [31:0] d_imm;
wire [3:0] d_aluop;
wire [2:0] d_wb_sel;
wire [2:0] d_md_op;
wire [11:0] d_csr_addr;
wire d_uses_rs1;
wire d_uses_rs2;
wire d_reg_write;
wire d_mem_to_reg;
wire d_mem_write;
wire d_mem_byte;
wire d_alu_src;
wire d_branch;
wire d_beq;
wire d_bne;
wire d_blt;
wire d_bge;
wire d_bltu;
wire d_bgeu;
wire d_jal;
wire d_jalr;
wire d_ecall;
wire d_uret;
wire d_csr_set;
wire d_csr_clear;
wire d_csr_write;
cpu21_riscv_decoder u_decoder (
.ir_i (ifid_ir_q),
.rs1_idx_o (d_src1_idx),
.rs2_idx_o (d_src2_idx),
.rd_o (d_rd),
.imm_o (d_imm),
.alu_op_o (d_aluop),
.wb_sel_o (d_wb_sel),
.md_op_o (d_md_op),
.csr_addr_o (d_csr_addr),
.uses_rs1_o (d_uses_rs1),
.uses_rs2_o (d_uses_rs2),
.reg_write_o (d_reg_write),
.mem_to_reg_o(d_mem_to_reg),
.mem_write_o (d_mem_write),
.mem_byte_o (d_mem_byte),
.alu_src_o (d_alu_src),
.branch_o (d_branch),
.beq_o (d_beq),
.bne_o (d_bne),
.blt_o (d_blt),
.bge_o (d_bge),
.bltu_o (d_bltu),
.bgeu_o (d_bgeu),
.jal_o (d_jal),
.jalr_o (d_jalr),
.ecall_o (d_ecall),
.uret_o (d_uret),
.csr_set_o (d_csr_set),
.csr_clear_o (d_csr_clear),
.csr_write_o (d_csr_write)
);
// 写回值选择:wb_sel 0=ALU 结果,1=访存数据,2=PC+4(已存在 alu_result 里),
// 3=MUL 高位/余数,4=CSR 旧值。
wire [31:0] wb_value;
assign wb_value = (memwb_wb_sel_q == 3'd1) ? memwb_mem_data_q :
(memwb_wb_sel_q == 3'd3) ? memwb_alu_result2_q :
(memwb_wb_sel_q == 3'd4) ? memwb_csr_old_q :
memwb_alu_result_q;
// WB 级写回使能(同时用于寄存器堆的写端口与写优先旁路)。
wire memwb_reg_write_en = memwb_valid_q && memwb_reg_write_q;
// ID 级读寄存器:x0 恒为 0,并对同周期 WB 的结果做写优先旁路。
wire [31:0] d_rs1_value;
wire [31:0] d_rs2_value;
cpu21_riscv_regfile u_regfile (
.clk (clk),
.reset (reset),
.rs1_idx_i (d_src1_idx),
.rs2_idx_i (d_src2_idx),
.rs1_data_o(d_rs1_value),
.rs2_data_o(d_rs2_value),
.we_i (memwb_reg_write_en),
.waddr_i (memwb_rd_q),
.wdata_i (wb_value)
);
// 中断/CSR 相关连线(中断控制器实例见下方"中断 / CSR"小节)。
wire take_irq;
wire [31:0] irq_vector;
wire [31:0] ex_return_pc;
wire [2:0] irq_pending;
wire [1:0] irq_current_level;
wire [31:0] uepc_value;
wire [31:0] ustatus_value;
// -------------------------------------------------------------------------
// EX 级:前递网络、ALU 运算、存储对齐与控制流裁决
// -------------------------------------------------------------------------
// EX/MEM 级要前递的值(按 wb_sel 选取)。JAL/JALR 的链接地址(PC+4)已在
// EX 级预先算好并存入 exmem_alu_result_q,所以这里 wb_sel=2 直接取该寄存器,
// 前递路径上不再有 PC+4 加法器(100 MHz 下这是最长路径之一)。
wire [31:0] exmem_forward_value =
(exmem_wb_sel_q == 3'd3) ? exmem_alu_result2_q :
(exmem_wb_sel_q == 3'd4) ? exmem_csr_old_q :
exmem_alu_result_q;
// 前递优先级:EX/MEM 的结果最新,其次为 MEM/WB 写回值,最后才是
// 译码时读到的寄存器值。
wire [31:0] ex_src1;
wire [31:0] ex_src2;
cpu21_riscv_forward_unit u_forward_unit (
.rs1_value_i (idex_rs1_value_q),
.rs2_value_i (idex_rs2_value_q),
.rs1_idx_i (idex_rs1_idx_q),
.rs2_idx_i (idex_rs2_idx_q),
.exmem_valid_i (exmem_valid_q),
.exmem_reg_write_i (exmem_reg_write_q),
.exmem_mem_to_reg_i(exmem_mem_to_reg_q),
.exmem_rd_i (exmem_rd_q),
.exmem_value_i (exmem_forward_value),
.memwb_valid_i (memwb_valid_q),
.memwb_reg_write_i (memwb_reg_write_q),
.memwb_rd_i (memwb_rd_q),
.memwb_value_i (wb_value),
.src1_o (ex_src1),
.src2_o (ex_src2)
);
// ALU 的 B 端:立即数或 rs2。
wire [31:0] ex_alu_b = idex_alu_src_q ? idex_imm_q : ex_src2;
wire [31:0] ex_alu_result;
wire [31:0] ex_alu_result2;
// 读取 CSR 旧值(ustatus=0x004、uepc=0x041),供 CSR 指令回写 rd。
wire [31:0] ex_csr_old_value =
(idex_csr_addr_q == 12'h004) ? ustatus_value :
(idex_csr_addr_q == 12'h041) ? uepc_value : 32'b0;
// ALU 实例(只做单拍运算;乘除法见下面的多周期单元)。
cpu21_riscv_alu u_ex_alu (
.op (idex_aluop_q),
.a (ex_src1),
.b (ex_alu_b),
.result (ex_alu_result),
.result2(ex_alu_result2)
);
// -------------------------------------------------------------------------
// 多周期乘除法单元:EX 级停留若干拍等待结果,不再把 32x32 乘法/除法
// 放在组合路径上(原来这部分的组合延时约 94 ns,100 MHz 下无法收敛)。
// 指令在 EX 级期间 ex_md_stall 有效:ID/EX 保持不变、EX/MEM 插气泡、
// 取指/译码冻结;迭代结束(md_done)后照常推进。
// -------------------------------------------------------------------------
wire md_busy;
wire md_done;
wire [31:0] md_result;
wire [31:0] md_result2;
wire ex_is_muldiv = idex_valid_q && (idex_md_op_q != 3'd0);
wire ex_md_stall = ex_is_muldiv && !md_done;
cpu21_riscv_muldiv u_muldiv (
.clk (clk),
.reset (reset),
.start_i (ex_is_muldiv),
.op_i (idex_md_op_q),
.a_i (ex_src1),
.b_i (ex_alu_b),
.busy_o (md_busy),
.done_o (md_done),
.result_o (md_result),
.result2_o(md_result2)
);
// EX 级最终结果:乘除法取多周期单元的输出;JAL/JALR(wb_sel=2)的链接地址
// PC+4 在这里算好随流水寄存器传下去,避免“写回值 → 前递 → 停机判定 →
// 流水寄存器时钟使能”这条组合路径上出现 32 位加法器。
wire [31:0] ex_result =
ex_is_muldiv ? md_result :
(idex_wb_sel_q == 3'd2) ? (idex_pc_q + 32'd4) :
ex_alu_result;
wire [31:0] ex_result2 = ex_is_muldiv ? md_result2 : ex_alu_result2;
// 控制流裁决:条件分支方向/目标、JAL/JALR 目标、是否需要重定向。
cpu21_riscv_branch_unit u_branch_unit (
.valid_i (idex_valid_q),
.pc_i (idex_pc_q),
.imm_i (idex_imm_q),
.src1_i (ex_src1),
.src2_i (ex_src2),
.branch_i (idex_branch_q),
.beq_i (idex_beq_q),
.bne_i (idex_bne_q),
.blt_i (idex_blt_q),
.bge_i (idex_bge_q),
.bltu_i (idex_bltu_q),
.bgeu_i (idex_bgeu_q),
.jal_i (idex_jal_q),
.jalr_i (idex_jalr_q),
.pred_taken_i (idex_pred_taken_q),
.pred_target_i (idex_pred_target_q),
.branch_taken_o (ex_branch_taken),
.controlflow_o (ex_controlflow),
.target_o (ex_target),
.redirect_valid_o(ex_redirect_valid),
.redirect_pc_o (ex_redirect_pc)
);
// 存储数据对齐:依据字节地址低两位生成写数据与字节写掩码。
wire [31:0] ex_store_data;
wire [ 3:0] ex_store_wstrb;
cpu21_riscv_store_unit u_store_unit (
.addr_i (ex_alu_result),
.data_i (ex_src2),
.mem_write_i (idex_mem_write_q),
.mem_byte_i (idex_mem_byte_q),
.store_data_o (ex_store_data),
.store_wstrb_o(ex_store_wstrb)
);
// -------------------------------------------------------------------------
// MMIO:LED 等外设映射到数据地址空间,在 MEM 级做地址译码。
// -------------------------------------------------------------------------
wire [31:0] mmio_rdata;
wire mmio_rdata_hit;
cpu21_mmio #(
.LED_ADDR(LED_ADDR),
.FB_WORDS(FB_WORDS)
) u_mmio (
.clk (clk),
.reset (reset),
.mem_valid_i(exmem_valid_q),
.mem_write_i(exmem_mem_write_q),
.mem_read_i (exmem_mem_to_reg_q),
.addr_i (exmem_alu_result_q),
.wdata_i (exmem_write_data_q),
.wstrb_i (exmem_wstrb_q),
.rdata_o (mmio_rdata),
.rdata_hit_o(mmio_rdata_hit),
// LED(原样)
.led_data_o (led_data_o),
.led_valid_o(led_valid_o),
// 七段数码管
.seg_data_o (seg_data_o),
.seg_valid_o(seg_valid_o),
// 显存(本体在板级顶层)
.fb_we_o (fb_we_o),
.fb_waddr_o (fb_waddr_o),
.fb_wdata_o (fb_wdata_o),
.fb_wstrb_o (fb_wstrb_o),
.fb_rdata_i (fb_rdata_i),
// 显示控制器
.disp_ctrl_o (disp_ctrl_o),
.disp_cursor_o(disp_cursor_o),
// 鼠标事件源与中断
.mouse_ev_valid_i(mouse_ev_valid_i),
.mouse_dx_i (mouse_dx_i),
.mouse_dy_i (mouse_dy_i),
.mouse_btn_i (mouse_btn_i),
.sw_i (sw_i),
.frame_tick_i (frame_tick_i),
.mouse_irq_o (mouse_irq_o),
.level_irq_o (level_irq_o)
);
// 访存读数据:命中 MMIO 时用外设数据,否则用外部存储器数据。
wire [31:0] mem_rdata = mmio_rdata_hit ? mmio_rdata : data_rdata_i;
// ecall 约定:LED 显示已改由 MMIO 存储完成(见上方的 cpu21_mmio 例化)。
// a7==34 的历史“显示”调用保留为无操作;其余(例如 a7=10)令处理器停机,
// 等待 go_i 重新启动。
wire ex_halt_event = idex_valid_q && idex_ecall_q && (ex_src1 != 32'd34);
// -------------------------------------------------------------------------
// 中断 / CSR / 嵌套返回栈
// -------------------------------------------------------------------------
wire ex_uret_redirect = idex_valid_q && idex_uret_q;
// EX 级正在 URET,或乘除法还没算完时,不响应中断:前者与中断同时发生会丢掉
// URET 的返回地址,后者会在结果未定型时推进流水线,向 rd 写回错误值。
// 这里不再包含 ex_redirect_valid / ex_halt_event:它们本身就会让
// flush_younger 有效(流水线照常被冲刷),而中断优先于重定向时返回地址由
// irq_save_pc 给出(已正确跳转的分支取目标地址)。这样就把
// “分支判决 → 中断控制器 → flush → 流水寄存器控制”这条 100 MHz 下的
// 关键路径切断了(原来要绕经中断控制器再回到 flush)。
wire ex_block_irq = ex_uret_redirect || ex_md_stall;
// 中断发生时要保存的返回地址取"下一个架构 PC":若 EX 是一条预测正确
// 且已跳转的分支,则该 PC 就是它的目标地址;若 EX 为空,则 IF/ID 中
// 保存的就是尚未执行的最老指令。
wire [31:0] irq_save_pc = idex_valid_q ?
((idex_branch_q && ex_branch_taken) ? ex_target : (idex_pc_q + 32'd4)) :
(ifid_valid_q ? ifid_pc_q : pc_q);
cpu21_riscv_irq_ctrl #(
.IRQ1_VECTOR (IRQ1_VECTOR),
.IRQ2_VECTOR (IRQ2_VECTOR),
.IRQ3_VECTOR (IRQ3_VECTOR),
.IRQ_STACK_DEPTH(IRQ_STACK_DEPTH),
.USTATUS_INIT (USTATUS_INIT)
) u_irq_ctrl (
.clk (clk),
.reset (reset),
.irq_i (irq_i),
.halted_i (halted_q),
.ex_block_i (ex_block_irq),
.save_pc_i (irq_save_pc),
.uret_i (ex_uret_redirect),
.csr_valid_i(idex_valid_q),
.csr_set_i (idex_csr_set_q),
.csr_clear_i(idex_csr_clear_q),
.csr_write_i(idex_csr_write_q),
.csr_addr_i (idex_csr_addr_q),
.csr_imm_i (idex_imm_q),
.csr_wdata_i(ex_src1),
.take_irq_o (take_irq),
.vector_o (irq_vector),
.return_pc_o(ex_return_pc),
.pending_o (irq_pending),
.level_o (irq_current_level),
.uepc_o (uepc_value),
.ustatus_o (ustatus_value)
);
// -------------------------------------------------------------------------
// 冒险 / 停顿 / 冲刷判定
// -------------------------------------------------------------------------
wire load_use_hazard;
wire flush_younger;
wire pipeline_stall;
cpu21_riscv_hazard_unit u_hazard_unit (
.ifid_valid_i (ifid_valid_q),
.idex_valid_i (idex_valid_q),
.idex_mem_to_reg_i(idex_mem_to_reg_q),
.idex_rd_i (idex_rd_q),
.d_uses_rs1_i (d_uses_rs1),
.d_uses_rs2_i (d_uses_rs2),
.d_src1_idx_i (d_src1_idx),
.d_src2_idx_i (d_src2_idx),
.take_irq_i (take_irq),
.uret_redirect_i (ex_uret_redirect),
.halt_event_i (ex_halt_event),
.redirect_valid_i (ex_redirect_valid),
.halted_i (halted_q),
.load_use_hazard_o(load_use_hazard),
.flush_younger_o (flush_younger),
.pipeline_stall_o (pipeline_stall)
);
// -------------------------------------------------------------------------
// 对外输出与调试信号。
// -------------------------------------------------------------------------
// 存储器接口:指令地址来自取指 PC;数据地址/写数据/写掩码来自 MEM 级。
assign instr_addr_o = pc_q;
assign data_addr_o = exmem_alu_result_q;
assign data_wdata_o = exmem_write_data_q;
assign data_wstrb_o = (exmem_valid_q && exmem_mem_write_q) ? exmem_wstrb_q : 4'b0;
assign data_we_o = |data_wstrb_o;
// LED 输出由 cpu21_mmio 驱动,这里只保留流水线各级 PC/指令回显。
assign if_pc_o = pc_q;
assign id_pc_o = ifid_valid_q ? ifid_pc_q : 32'b0;
assign ex_pc_o = idex_valid_q ? idex_pc_q : 32'b0;
assign mem_pc_o = exmem_valid_q ? exmem_pc_q : 32'b0;
assign wb_pc_o = memwb_valid_q ? memwb_pc_q : 32'b0;
assign if_ir_o = instr_i;
assign id_ir_o = ifid_valid_q ? ifid_ir_q : 32'b0;
assign ex_ir_o = idex_valid_q ? idex_ir_q : 32'b0;
assign mem_ir_o = exmem_valid_q ? exmem_ir_q : 32'b0;
assign wb_ir_o = memwb_valid_q ? memwb_ir_q : 32'b0;
assign rdin_o = wb_value;
assign mdin_o = exmem_write_data_q;
assign reg_write_o = memwb_valid_q && memwb_reg_write_q;
assign mem_write_o = exmem_valid_q && exmem_mem_write_q;
assign halted_o = halted_q;
assign irq_pending_o = irq_pending;
assign irq_current_level_o = irq_current_level;
assign uepc_o = uepc_value;
assign ustatus_o = ustatus_value;
assign bpb_predict_hit_o = bpb_predict_hit;
assign bpb_predict_taken_o = bpb_predict_taken;
assign bpb_mispredict_o = ex_redirect_valid && idex_branch_q;
// -------------------------------------------------------------------------
// 状态更新。顺序遵循五级流水线:先捕获 MEM/WB、EX/MEM,再处理
// ID/EX 与 IF/ID/PC 的控制。
// -------------------------------------------------------------------------
// 复位时清空所有流水寄存器与 LED/停机标志(寄存器堆、中断状态与
// 性能计数器在各自的子模块内复位)。
always @(posedge clk or posedge reset) begin
if (reset) begin
pc_q <= RESET_PC;
ifid_valid_q <= 1'b0;
ifid_pc_q <= 32'b0;
ifid_ir_q <= 32'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
idex_valid_q <= 1'b0;
idex_pc_q <= 32'b0;
idex_ir_q <= 32'b0;
idex_rs1_value_q <= 32'b0;
idex_rs2_value_q <= 32'b0;
idex_rs1_idx_q <= 5'b0;
idex_rs2_idx_q <= 5'b0;
idex_rd_q <= 5'b0;
idex_imm_q <= 32'b0;
idex_aluop_q <= ALU_ADD;
idex_alu_src_q <= 1'b0;
idex_reg_write_q <= 1'b0;
idex_mem_to_reg_q <= 1'b0;
idex_mem_write_q <= 1'b0;
idex_mem_byte_q <= 1'b0;
idex_wb_sel_q <= 3'b0;
idex_md_op_q <= 3'b0;
idex_csr_addr_q <= 12'b0;
idex_branch_q <= 1'b0;
idex_beq_q <= 1'b0;
idex_bne_q <= 1'b0;
idex_blt_q <= 1'b0;
idex_bge_q <= 1'b0;
idex_bltu_q <= 1'b0;
idex_bgeu_q <= 1'b0;
idex_jal_q <= 1'b0;
idex_jalr_q <= 1'b0;
idex_ecall_q <= 1'b0;
idex_uret_q <= 1'b0;
idex_csr_set_q <= 1'b0;
idex_csr_clear_q <= 1'b0;
idex_csr_write_q <= 1'b0;
idex_pred_taken_q <= 1'b0;
idex_pred_target_q <= 32'b0;
exmem_valid_q <= 1'b0;
exmem_pc_q <= 32'b0;
exmem_ir_q <= 32'b0;
exmem_alu_result_q <= 32'b0;
exmem_alu_result2_q <= 32'b0;
exmem_csr_old_q <= 32'b0;
exmem_write_data_q <= 32'b0;
exmem_rd_q <= 5'b0;
exmem_reg_write_q <= 1'b0;
exmem_mem_to_reg_q <= 1'b0;
exmem_mem_write_q <= 1'b0;
exmem_wstrb_q <= 4'b0;
exmem_wb_sel_q <= 3'b0;
memwb_valid_q <= 1'b0;
memwb_pc_q <= 32'b0;
memwb_ir_q <= 32'b0;
memwb_alu_result_q <= 32'b0;
memwb_alu_result2_q <= 32'b0;
memwb_csr_old_q <= 32'b0;
memwb_mem_data_q <= 32'b0;
memwb_rd_q <= 5'b0;
memwb_reg_write_q <= 1'b0;
memwb_mem_to_reg_q <= 1'b0;
memwb_wb_sel_q <= 3'b0;
halted_q <= 1'b0;
end
else begin
if (go_i)
halted_q <= 1'b0;
if (ex_halt_event)
halted_q <= 1'b1;
// MEM/WB 流水寄存器捕获。
memwb_valid_q <= exmem_valid_q;
memwb_pc_q <= exmem_pc_q;
memwb_ir_q <= exmem_ir_q;
memwb_alu_result_q <= exmem_alu_result_q;
memwb_alu_result2_q <= exmem_alu_result2_q;
memwb_csr_old_q <= exmem_csr_old_q;
memwb_mem_data_q <= mem_rdata;
memwb_rd_q <= exmem_rd_q;
memwb_reg_write_q <= exmem_reg_write_q;
memwb_mem_to_reg_q <= exmem_mem_to_reg_q;
memwb_wb_sel_q <= exmem_wb_sel_q;
// EX/MEM 流水寄存器捕获。乘除法迭代期间(ex_md_stall)往 EX/MEM 插气泡,
// 让乘除法指令继续占用 EX,结果在 md_done 那一拍才写入 EX/MEM。
if (ex_md_stall) begin
exmem_valid_q <= 1'b0;
exmem_pc_q <= 32'b0;
exmem_ir_q <= 32'b0;
exmem_alu_result_q <= 32'b0;
exmem_alu_result2_q <= 32'b0;
exmem_csr_old_q <= 32'b0;
exmem_write_data_q <= 32'b0;
exmem_rd_q <= 5'b0;
exmem_reg_write_q <= 1'b0;
exmem_mem_to_reg_q <= 1'b0;
exmem_mem_write_q <= 1'b0;
exmem_wstrb_q <= 4'b0;
exmem_wb_sel_q <= 3'b0;
end
else begin
exmem_valid_q <= idex_valid_q;
exmem_pc_q <= idex_pc_q;
exmem_ir_q <= idex_ir_q;
exmem_alu_result_q <= ex_result;
exmem_alu_result2_q <= ex_result2;
exmem_csr_old_q <= ex_csr_old_value;
exmem_write_data_q <= ex_store_data;
exmem_rd_q <= idex_rd_q;
exmem_reg_write_q <= idex_reg_write_q;
exmem_mem_to_reg_q <= idex_mem_to_reg_q;
exmem_mem_write_q <= idex_mem_write_q;
exmem_wstrb_q <= ex_store_wstrb;
exmem_wb_sel_q <= idex_wb_sel_q;
end
// ---------------------------------------------------------------------
// ID/EX 装载。
//
// payload(PC/IR/控制位/预测位)只在 load-use 停顿、乘除法迭代、停机时
// 保持(时钟使能与“冲刷”无关),其余情况一律装载;只有 valid 位受冲刷
// 控制,冲刷时清 0。这样“EX 级组合判决(分支/中断)→ 流水寄存器时钟
// 使能”这条 100 MHz 下最长的路径就只剩下一个触发器(valid),payload
// 即使装进错误路径的数据也无害——所有使用者都以 idex_valid_q 为前提。
// ---------------------------------------------------------------------
if (!(pipeline_stall || halted_q || ex_md_stall)) begin
idex_pc_q <= ifid_pc_q;
idex_ir_q <= ifid_ir_q;
idex_rs1_value_q <= d_rs1_value;
idex_rs2_value_q <= d_rs2_value;
idex_rs1_idx_q <= d_src1_idx;
idex_rs2_idx_q <= d_src2_idx;
idex_rd_q <= d_rd;
idex_imm_q <= d_imm;
idex_aluop_q <= d_aluop;
idex_alu_src_q <= d_alu_src;
idex_reg_write_q <= d_reg_write;
idex_mem_to_reg_q <= d_mem_to_reg;
idex_mem_write_q <= d_mem_write;
idex_mem_byte_q <= d_mem_byte;
idex_wb_sel_q <= d_wb_sel;
idex_md_op_q <= d_md_op;
idex_csr_addr_q <= d_csr_addr;
idex_branch_q <= d_branch;
idex_beq_q <= d_beq;
idex_bne_q <= d_bne;
idex_blt_q <= d_blt;
idex_bge_q <= d_bge;
idex_bltu_q <= d_bltu;
idex_bgeu_q <= d_bgeu;
idex_jal_q <= d_jal;
idex_jalr_q <= d_jalr;
idex_ecall_q <= d_ecall;
idex_uret_q <= d_uret;
idex_csr_set_q <= d_csr_set;
idex_csr_clear_q <= d_csr_clear;
idex_csr_write_q <= d_csr_write;
idex_pred_taken_q <= ifid_pred_taken_q;
idex_pred_target_q <= ifid_pred_target_q;
end
if (halted_q || ex_md_stall) begin
// 停机 / 乘除法迭代:valid 保持不变(指令继续占用 EX)。
end
else if (pipeline_stall || flush_younger) begin
// load-use 停顿:load 已经进 EX/MEM,这里给 EX 装一个气泡(payload 保持
// 原值,valid=0 时无害);冲刷同理。
idex_valid_q <= 1'b0;
end
else begin
idex_valid_q <= ifid_valid_q;
end
if (take_irq) begin
pc_q <= irq_vector;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end
else if (ex_uret_redirect) begin
pc_q <= ex_return_pc;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end
else if (ex_halt_event) begin
pc_q <= idex_pc_q + 32'd4;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end
else if (ex_redirect_valid) begin
pc_q <= ex_redirect_pc;
ifid_valid_q <= 1'b0;
ifid_pred_taken_q <= 1'b0;
ifid_pred_target_q <= 32'b0;
end
else if (pipeline_stall || ex_md_stall) begin
pc_q <= pc_q;
ifid_valid_q <= ifid_valid_q;
ifid_pc_q <= ifid_pc_q;
ifid_ir_q <= ifid_ir_q;
ifid_pred_taken_q <= ifid_pred_taken_q;
ifid_pred_target_q <= ifid_pred_target_q;
end
else if (halted_q) begin
pc_q <= pc_q;
ifid_valid_q <= 1'b0;
end
else begin
pc_q <= fetch_next_pc;
ifid_valid_q <= 1'b1;
ifid_pc_q <= pc_q;
ifid_ir_q <= instr_i;
ifid_pred_taken_q <= f_is_branch && bpb_predict_hit && bpb_predict_taken;
ifid_pred_target_q <= bpb_predict_target;
end
end
end
// -------------------------------------------------------------------------
// 性能计数器(只影响统计量,不参与控制)。
// -------------------------------------------------------------------------
cpu21_riscv_perf_counters u_perf_counters (
.clk (clk),
.reset (reset),
.inc_cycle_i (!halted_q),
.stall_i (pipeline_stall || ex_md_stall),
.flush_i (flush_younger),
.cond_taken_i (idex_valid_q && idex_branch_q && ex_branch_taken),
.cond_valid_i (idex_valid_q && idex_branch_q),
.mispredict_i (ex_redirect_valid),
.uncond_i (idex_valid_q && (idex_jal_q || idex_jalr_q)),
.cycle_count_o (cycle_count_o),
.stall_count_o (stall_count_o),
.bubble_count_o (bubble_count_o),
.conditional_taken_count_o (conditional_taken_count_o),
.unconditional_branch_count_o(unconditional_branch_count_o),
.prediction_success_count_o (prediction_success_count_o),
.prediction_failure_count_o (prediction_failure_count_o)
);
endmodule
`default_nettype wire
-107
View File
@@ -1,107 +0,0 @@
`timescale 1ns / 1ps
`default_nettype none
// -----------------------------------------------------------------------------
// cpu21_seg7 —— Nexys4DDR 板载 8 位数码管的动态扫描驱动。
//
// 输入:seg_data_i 每 4 位一个十六进制数字(低 4 位 = 最右边那个数码管),
// an_en_i 每一位对应一个数码管的使能(1 = 显示,0 = 熄灭)。
// 输出:an_o[7:0] 位选(低有效)、seg_o[6:0] 段选 a..g(低有效)、dp_o 小数点。
//
// 扫描频率:REFRESH_DIV 分频后约 1 kHz 切换一位(8 位 => 每位数码管 125 Hz
// 刷新)。REFRESH_DIV 由顶层按 CPU 时钟频率给出(默认 50000 @50 MHz),
// 这样 CPU 降频时扫描刷新率不会跟着变慢。
// 板载数码管是共阳极:位选拉低选中,段选拉低点亮。
// -----------------------------------------------------------------------------
module cpu21_seg7 #(
parameter integer REFRESH_DIV = 50000 // 50 MHz / 50000 = 1 kHz 切换一位
) (
input wire clk,
input wire reset,
input wire [31:0] seg_data_i, // 8 个十六进制数字
input wire [ 7:0] an_en_i, // 每位是否显示
output reg [ 7:0] an_o, // 位选,低有效
output reg [ 6:0] seg_o, // 段选 a..g,低有效
output reg dp_o // 小数点,低有效
);
// ---------------------------------------------------------------------------
// 刷新计数与当前位
// ---------------------------------------------------------------------------
// 计数位宽要能装下 REFRESH_DIV-1:顶层按 CPU 频率等比给出该值
//(50 MHz 时 50000,12.5 MHz 时 12500;若不分频直接跑 100 MHz 则 100000),
// 因此这里用 20 位(最大约 100 万)留足余量。
reg [19:0] div_q;
reg [ 2:0] digit_q;
wire tick = (div_q == (REFRESH_DIV - 1));
always @(posedge clk or posedge reset) begin
if (reset) begin
div_q <= 20'd0;
digit_q <= 3'd0;
end else if (tick) begin
div_q <= 20'd0;
digit_q <= digit_q + 3'd1;
end else begin
div_q <= div_q + 16'd1;
end
end
// 当前要显示的十六进制数字
reg [3:0] nib;
always @* begin
case (digit_q)
3'd0: nib = seg_data_i[3:0];
3'd1: nib = seg_data_i[7:4];
3'd2: nib = seg_data_i[11:8];
3'd3: nib = seg_data_i[15:12];
3'd4: nib = seg_data_i[19:16];
3'd5: nib = seg_data_i[23:20];
3'd6: nib = seg_data_i[27:24];
default: nib = seg_data_i[31:28];
endcase
end
// ---------------------------------------------------------------------------
// 十六进制 -> 七段码(bit0=a ... bit6=g,1 表示该段点亮)
// ---------------------------------------------------------------------------
reg [6:0] seg_on;
always @* begin
case (nib)
4'h0: seg_on = 7'b0111111;
4'h1: seg_on = 7'b0000110;
4'h2: seg_on = 7'b1011011;
4'h3: seg_on = 7'b1001111;
4'h4: seg_on = 7'b1100110;
4'h5: seg_on = 7'b1101101;
4'h6: seg_on = 7'b1111101;
4'h7: seg_on = 7'b0000111;
4'h8: seg_on = 7'b1111111;
4'h9: seg_on = 7'b1101111;
4'hA: seg_on = 7'b1110111;
4'hB: seg_on = 7'b1111100;
4'hC: seg_on = 7'b0111001;
4'hD: seg_on = 7'b1011110;
4'hE: seg_on = 7'b1111001;
default: seg_on = 7'b1110001; // F
endcase
end
// ---------------------------------------------------------------------------
// 位选与段选输出(共阳极 => 低有效)
// ---------------------------------------------------------------------------
always @(posedge clk or posedge reset) begin
if (reset) begin
an_o <= 8'hFF;
seg_o <= 7'h7F;
dp_o <= 1'b1;
end else if (tick) begin
an_o <= ~(8'b0000_0001 << digit_q) | ~an_en_i;
seg_o <= ~seg_on;
dp_o <= 1'b1; // 不用小数点
end
end
endmodule
`default_nettype wire
-328
View File
@@ -1,328 +0,0 @@
`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
-74
View File
@@ -1,74 +0,0 @@
`timescale 1ns / 1ps
// -----------------------------------------------------------------------------
// cpu21_board_top 的仿真测试(不上板,纯 RTL 仿真)
//
// 检查内容:
// 1) 复位释放后 CPU 能从片内 ROM 取指(ROM 由 cpu21_board_rom.vh 初始化);
// 2) 程序通过 MMIO 写 LED,板载 LED 端口上能看到 16 位跑马灯序列。
//
// 默认的 board_demo 程序带约 20 ms 软件延时,仿真只需确认第一次写入;
// 若要完整验证跑马灯序列,可生成一份短延时的 ROM 并用 -d BOARD_ROM_FAST 编译:
// xvlog -d BOARD_ROM_FAST board/cpu21_board_top.v <rtl/*.v> testbench/tb_board_top.v
// -----------------------------------------------------------------------------
module tb_board_top;
reg clk100 = 1'b0;
reg rst_n = 1'b0;
reg go = 1'b0;
reg [2:0] irq_sw = 3'b000;
wire [15:0] led;
wire led_we;
always #5 clk100 = ~clk100; // 100 MHz
cpu21_board_top #(
.LED_ADDR(32'h0000_1000)
) dut (
.clk100(clk100),
.rst_n (rst_n),
.go (go),
.irq_sw(irq_sw),
.led (led),
.led_we(led_we),
// 扫雷扩展端口:本测试平台不复位测试这些功能,输入显式接 0/1,
// 避免悬空(z)传到中断控制器里影响跑马灯的比对结果。
.btnu (1'b0),
.btnd (1'b0),
.btnl (1'b0),
.btnr (1'b0),
.sw_flag (1'b0),
.hid_sel (1'b0),
.ps2_clk (1'b1),
.ps2_data(1'b1),
.an (),
.seg (),
.dp (),
.vga_r (),
.vga_g (),
.vga_b (),
.vga_hs (),
.vga_vs ()
);
integer changes = 0;
reg [15:0] led_prev = 16'hxxxx;
always @(posedge clk100) begin
if (led !== led_prev) begin
changes = changes + 1;
led_prev = led;
$display("t=%t LED=%04h led_we=%b", $time, led, led_we);
end
end
initial begin
$timeformat(-9, 3, " ns", 12); // %t 按 ns 显示(默认按精度 ps 显示)
$display("NOTE: 复位后观察 LED(默认程序首次写入应为 0001)");
repeat (10) @(posedge clk100);
rst_n = 1'b1;
repeat (30000) @(posedge clk100); // 300 us(CPU 时钟为 clk100/2 = 50 MHz)
$display("FINAL: LED 变化次数=%0d 当前 LED=%04h", changes, led);
$finish;
end
endmodule
-169
View File
@@ -1,169 +0,0 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_ps2_kbd —— 单独验证 PS/2 键盘解码(扫描码集 2)
//
// 发 → 键(E0 74)应产生 dx=+8;发 ↑(E0 75)应产生 dy=-8;
// 发 Enter(5A)应产生 btn=1(开格);发 左Shift(12)应产生 btn=2(插旗);
// 发 W(1D)应产生 dy=-8(W/A/S/D 别名);
// 松开码(F0 xx)不应产生事件;校验位错的帧应被整帧丢掉。
//
// PS/2 键盘发扫描码(集 2):0xF0 前缀 = 下一个码是"松开",0xE0 前缀 = 扩展键,
// 方向键是扩展键(↑ = E0 75、↓ = E0 72、← = E0 6B、→ = E0 74),
// 所以解码器只对"按下"产生事件。键盘自动重复 = 按住方向键时连续移动。
//
// 跑法:
// xvlog srcs/rtl/cpu21_ps2_kbd.v srcs/tb/tb_ps2_kbd.v
// xelab -top tb_ps2_kbd -snapshot kbdsim
// xsim kbdsim -R
// =============================================================================
module tb_ps2_kbd;
reg clk = 1'b0;
reg reset = 1'b1;
reg ps2_clk = 1'b1;
reg ps2_data = 1'b1;
wire ev_valid;
wire [7:0] ev_dx, ev_dy;
wire [4:0] ev_btn;
wire [15:0] rx_count;
always #10 clk = ~clk; // 50 MHz
cpu21_ps2_kbd dut (
.clk (clk),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_data_i (ps2_data),
.ev_valid_o (ev_valid),
.ev_dx_o (ev_dx),
.ev_dy_o (ev_dy),
.ev_btn_o (ev_btn),
.rx_count_o (rx_count)
);
// 发一个 PS/2 位:先摆数据,稳定后拉低时钟(DUT 在下降沿采样)
task send_bit;
input b;
begin
ps2_data = b;
#(10_000);
ps2_clk = 1'b0;
#(20_000);
ps2_clk = 1'b1;
#(20_000);
end
endtask
// 发一个字节:起始 0 + 8 位数据(LSB first) + 奇校验 + 停止 1
task send_byte;
input [7:0] d;
integer i;
reg p;
begin
p = ~^d; // 奇校验
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(p);
send_bit(1'b1);
end
endtask
// 校验位错的字节(应被整帧丢掉)
task send_byte_bad_parity;
input [7:0] d;
integer i;
begin
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(^d);
send_bit(1'b1);
end
endtask
integer n_ev = 0;
integer ok = 1;
integer exp_dx, exp_dy, exp_btn;
always @(posedge clk) begin
if (ev_valid) begin
n_ev = n_ev + 1;
$display(" [kbd] event #%0d: dx=%0d dy=%0d btn=%b", n_ev, $signed(ev_dx), $signed(ev_dy), ev_btn);
if ((exp_dx != 999) && ($signed(ev_dx) != exp_dx)) begin
$display(" [kbd] FAIL: 期望 dx=%0d", exp_dx);
ok = 0;
end
if ((exp_dy != 999) && ($signed(ev_dy) != exp_dy)) begin
$display(" [kbd] FAIL: 期望 dy=%0d", exp_dy);
ok = 0;
end
if ((exp_btn != 999) && (ev_btn != exp_btn)) begin
$display(" [kbd] FAIL: 期望 btn=%b", exp_btn);
ok = 0;
end
end
end
initial begin
$display("[kbd] === PS/2 keyboard decode test ===");
#2000;
reset = 1'b0;
#2000;
// 校验错的字节(0x74)不能被当成"→"键
$display("[kbd] 校验位错的字节应该被丢掉");
exp_dx = 999; exp_dy = 999; exp_btn = 999;
send_byte_bad_parity(8'h74);
#(100_000);
if (n_ev != 0) begin
$display(" [kbd] FAIL: 校验错的帧不该产生事件");
ok = 0;
end
$display("[kbd] 只发松开码 F0 75(不该有事件)");
send_byte(8'hF0);
send_byte(8'h75);
#(100_000);
if (n_ev != 0) begin
$display(" [kbd] FAIL: 松开码不该产生事件");
ok = 0;
end
$display("[kbd] → 键(E0 74)期望 dx=+8");
exp_dx = 8; exp_dy = 0; exp_btn = 0;
send_byte(8'hE0);
send_byte(8'h74);
#(100_000);
$display("[kbd] ↑ 键(E0 75)期望 dy=-8");
exp_dx = 0; exp_dy = -8; exp_btn = 0;
send_byte(8'hE0);
send_byte(8'h75);
#(100_000);
$display("[kbd] Enter(5A)期望 btn=00001");
exp_dx = 0; exp_dy = 0; exp_btn = 5'b00001;
send_byte(8'h5A);
#(100_000);
$display("[kbd] 左 Shift(12)期望 btn=00010(软件里 = 插旗)");
exp_dx = 0; exp_dy = 0; exp_btn = 5'b00010;
send_byte(8'h12);
#(100_000);
$display("[kbd] W 键(1D)期望 dy=-8(W/A/S/D 是方向键别名)");
exp_dx = 0; exp_dy = -8; exp_btn = 0;
send_byte(8'h1D);
#(100_000);
$display("[kbd] rx_count=%0d events=%0d", rx_count, n_ev);
if ((ok == 0) || (n_ev != 5))
$display("[kbd] === FAIL ===");
else
$display("[kbd] === ALL PASS ===");
$finish;
end
endmodule
-153
View File
@@ -1,153 +0,0 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_ps2_mouse —— 单独验证 PS/2 鼠标接收逻辑(不跑 CPU,几秒出结果)
//
// 模拟 PS/2 鼠标发 3 字节包:
// byte0 = 标志字节(bit3 必须为 1 才是有效的鼠标数据;bit0/1/2 = 左/右/中键)
// byte1 = dx(有符号) byte2 = dy(有符号)
// 检查 cpu21_ps2_mouse 是否给出正确的 ev_valid / ev_dx / ev_dy / ev_btn。
//
// PS/2 时序:DUT 在 ps2_clk 的**下降沿**采样 ps2_data,所以这里先把数据摆好,
// 再拉低时钟。一帧 11 位 = 起始 0 + 8 位数据(LSB 先) + **奇校验** + 停止 1。
// 奇校验 = 校验位让 data + parity 共 9 位里 1 的个数为奇数(真设备就是这么发的)。
//
// 方向约定:PS/2 的 **dx 正 = 右、dy 正 = 上**(真鼠标往上推,byte2 是正数);
// 而软件(saolei.c / cpu21_pointer)里 **dy 正 = 向下(行号增加)**,
// 所以 cpu21_ps2_mouse 上报前把 dy 取负 —— 本测试就是盯这个约定:
// 发“向上”的 dy,事件里必须得到负的 dy。
//
// 跑法(Vivado 的 xvlog/xelab/xsim 或其它仿真器都行):
// xvlog srcs/rtl/cpu21_ps2_mouse.v srcs/tb/tb_ps2_mouse.v
// xelab -top tb_ps2_mouse -snapshot ps2sim
// xsim ps2sim -R
// =============================================================================
module tb_ps2_mouse;
reg clk = 1'b0;
reg reset = 1'b1;
reg ps2_clk = 1'b1;
reg ps2_data = 1'b1;
wire ev_valid;
wire [7:0] ev_dx;
wire [7:0] ev_dy;
wire [4:0] ev_btn;
wire [15:0] rx_count;
always #10 clk = ~clk; // 50 MHz,和板上 CPU 主频一致
cpu21_ps2_mouse dut (
.clk (clk),
.reset (reset),
.ps2_clk_i (ps2_clk),
.ps2_clk_oe_o (),
.ps2_data_i (ps2_data),
.ps2_data_oe_o(),
.ev_valid_o (ev_valid),
.ev_dx_o (ev_dx),
.ev_dy_o (ev_dy),
.ev_btn_o (ev_btn),
.rx_count_o (rx_count)
);
// 发一个 PS/2 位:先摆数据,稳定后再拉低时钟(DUT 在下降沿采样)
task send_bit;
input b;
begin
ps2_data = b;
#(10_000);
ps2_clk = 1'b0;
#(20_000);
ps2_clk = 1'b1;
#(20_000);
end
endtask
// 发一个字节:起始 0 + 8 位数据(LSB first) + 奇校验 + 停止 1
task send_byte;
input [7:0] d;
integer i;
reg p;
begin
p = ~^d; // 奇校验:data + parity 里 1 的个数为奇数
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(p);
send_bit(1'b1);
end
endtask
// 故意发一个校验位错的字节(用于确认校验真的在起作用)
task send_byte_bad_parity;
input [7:0] d;
integer i;
begin
send_bit(1'b0);
for (i = 0; i < 8; i = i + 1)
send_bit(d[i]);
send_bit(^d); // 错误的校验位
send_bit(1'b1);
end
endtask
integer n_ev = 0;
integer ok = 1;
always @(posedge clk) begin
if (ev_valid) begin
n_ev = n_ev + 1;
$display(" [ps2] event #%0d: dx=%0d dy=%0d btn=%b", n_ev, $signed(ev_dx), $signed(ev_dy), ev_btn);
if (n_ev == 1) begin
if (($signed(ev_dx) != 8) || ($signed(ev_dy) != -8) || (ev_btn != 5'b00000)) begin
$display(" [ps2] FAIL: 第 1 包应为 dx=+8 dy=-8 btn=00000(鼠标向上 -> 事件 dy 为负)");
ok = 0;
end
end
if (n_ev == 2) begin
if (($signed(ev_dx) != -3) || ($signed(ev_dy) != 5) || (ev_btn[0] != 1'b1)) begin
$display(" [ps2] FAIL: 第 2 包应为 dx=-3 dy=+5 btn=xxxx1(鼠标向下 -> 事件 dy 为正)");
ok = 0;
end
end
end
end
integer rx_after_bad;
initial begin
$display("[ps2] === PS/2 mouse receiver test ===");
#2000;
reset = 1'b0;
#2000;
$display("[ps2] 校验位错的字节应该被丢掉");
send_byte_bad_parity(8'h08);
#(100_000);
if (rx_count != 16'd0) begin
$display(" [ps2] FAIL: 校验错的帧不该被计数(rx_count=%0d)", rx_count);
ok = 0;
end
rx_after_bad = rx_count;
$display("[ps2] packet 1: flag=0x08(no button) dx=+8 dy=+8(鼠标向右上移)");
send_byte(8'h08);
send_byte(8'h08);
send_byte(8'h08);
#(100_000);
$display("[ps2] packet 2: flag=0x09(left) dx=-3 dy=-5(向左下移)");
send_byte(8'h09);
send_byte(8'hFD);
send_byte(8'hFB);
#(100_000);
$display("[ps2] rx_count=%0d events=%0d", rx_count, n_ev);
if ((rx_count != rx_after_bad + 16'd6) || (n_ev != 2) || (ok == 0))
$display("[ps2] === FAIL ===");
else
$display("[ps2] === ALL PASS ===");
$finish;
end
endmodule
-464
View File
@@ -1,464 +0,0 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_saolei —— 没有 VGA 显示器时的"扫雷调试台"
//
// 做法:跑真实的板级顶层 cpu21_board_top(宏 SAOLEI_ROM = 扫雷程序),用 5 向按键
// 模拟鼠标(板上 BTNC = 左键、BTNU/D/L/R = 上下左右,R15=SW3 = 插旗模式),
// 定期把显存 1200 字节 dump 成文本;再由 saolei/tools/fb2png.py 按 cpu21_vga.v
// 里的 8x8 字库与 3 位调色板还原成 PNG —— 等价于给 VGA 画面"截图"。
//
// 编译(注意两个 -d 都要带 SAOLEI_ROM):
// xvlog -d SAOLEI_ROM -i srcs/board -i srcs/rtl srcs/rtl/*.v \
// srcs/board/cpu21_board_top.v srcs/tb/tb_saolei.v
// xelab -d SAOLEI_ROM -top tb_saolei -snapshot saolei_sim
// xsim saolei_sim -tclbatch run_all.tcl
//
// 说明:为了在仿真里按键快一点,用 defparam 把 cpu21_pointer 的去抖采样周期
// 从 50000 个 CPU 周期(1 ms @50 MHz)压到 50(1 us),这样每次"按键"只要几微秒
// 就能产生一个事件;上板时那个参数仍然是 50000,行为不变。
// =============================================================================
module tb_saolei;
// ---- 板级顶层端口 ----
reg clk100 = 1'b0;
reg rst_n = 1'b0;
reg go = 1'b0;
reg [ 2:0] irq_sw = 3'b000;
reg btnu = 1'b0;
reg btnd = 1'b0;
reg btnl = 1'b0;
reg btnr = 1'b0;
reg sw_flag = 1'b0;
wire [15:0] led;
wire led_we;
wire [ 7:0] an;
wire [ 6:0] seg;
wire dp;
wire [ 3:0] vga_r, vga_g, vga_b;
wire vga_hs, vga_vs;
// 100 MHz 参考时钟
always #5 clk100 = ~clk100;
cpu21_board_top #(
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
) dut (
.clk100 (clk100),
.rst_n (rst_n),
.go (go),
.irq_sw (irq_sw),
.btnu (btnu),
.btnd (btnd),
.btnl (btnl),
.btnr (btnr),
.sw_flag (sw_flag),
.led (led),
.led_we (led_we),
.an (an),
.seg (seg),
.dp (dp),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.ps2_clk (1'b1),
.ps2_data(1'b1)
);
// (按键去抖已通过上面的 POINTER_SAMPLE_DIV(50) 参数加速,不用 defparam)
// ---------------------------------------------------------------- 按键
// which: 0=go(左键) 1=btnu 2=btnd 3=btnl 4=btnr
task press;
input integer which;
input integer us; // 按住时长(微秒)
begin
case (which)
0:
go = 1'b1;
1:
btnu = 1'b1;
2:
btnd = 1'b1;
3:
btnl = 1'b1;
4:
btnr = 1'b1;
endcase
#(us * 1000);
case (which)
0:
go = 1'b0;
1:
btnu = 1'b0;
2:
btnd = 1'b0;
3:
btnl = 1'b0;
4:
btnr = 1'b0;
endcase
#(200 * 1000); // 松开后留 200 us 让 CPU 消费事件
end
endtask
task click; // 左键点一下
begin
press(0, 50);
end
endtask
// ---------------------------------------------------------------- 显存 dump
integer fh, w, b;
integer cursor_row, cursor_col;
// ---- 统计:MMIO 显存写脉冲数 + 当前非零格数 ----
integer fb_we_cnt = 0;
always @(posedge dut.clk_cpu)
if (dut.fb_we)
fb_we_cnt = fb_we_cnt + 1;
// ---- 鼠标事件链路三个关键节点 ----
// ptr_ev : cpu21_pointer 产生事件(ev_valid_o 高的周期数)
// irq_hi : 鼠标中断线 irq_vec[0] 为高的周期数
// irq_rise: 该线的上升沿次数(CPU 按上升沿响应中断)
integer ptr_ev = 0, irq_hi = 0, irq_rise = 0;
reg irq_prev_q;
initial
irq_prev_q = 1'b0;
always @(posedge dut.clk_cpu) begin
if (dut.g_btn.u_ptr.ev_valid_o)
ptr_ev = ptr_ev + 1;
if (dut.irq_vec[0])
irq_hi = irq_hi + 1;
irq_prev_q <= dut.irq_vec[0];
if (dut.irq_vec[0] && !irq_prev_q)
irq_rise = irq_rise + 1;
end
// 每毫秒打一行,观察事件链路
integer tick_ms;
initial
for (tick_ms = 1; tick_ms <= 20; tick_ms = tick_ms + 1) begin
#1000000;
$display("[mouse] %0t ns: ptr_ev=%0d irq_hi=%0d irq_rise=%0d", $time, ptr_ev, irq_hi, irq_rise);
end
integer nz_cnt = 0; // 当前非零格数(dump 时统计)
// ---- PC 热点直方图:确定 CPU 此刻卡在哪个循环 ----
integer pc_hist[0:255];
integer pi;
initial
for (pi = 0; pi < 256; pi = pi + 1)
pc_hist[pi] = 0;
always @(posedge dut.clk_cpu)
pc_hist[dut.u_cpu.pc_q[9:2]] = pc_hist[dut.u_cpu.pc_q[9:2]] + 1;
// ---- 探针:控制流日志 ----
// 1) 记录所有非顺序跳转(1 ms 之后、最多 300 条),用来复盘"跑飞"的路径;
// 2) 单独标出跳进 ISR 段之间空洞(0x194-0x1FF / 0x218-0x2FF)的时刻。
reg [31:0] pc_prev;
integer jmp_log;
initial begin
pc_prev = 32'hFFFF_FFFF;
jmp_log = 0;
end
always @(posedge dut.clk_cpu) begin
pc_prev <= dut.u_cpu.pc_q;
if (dut.u_cpu.pc_q != (pc_prev + 32'd4)) begin
if ($time > 1000000 && jmp_log < 300) begin
$display("[jmp] %0t ns: 0x%03h -> 0x%03h", $time, pc_prev, dut.u_cpu.pc_q);
jmp_log = jmp_log + 1;
end
if ((dut.u_cpu.pc_q >= 32'h0000_0194 && dut.u_cpu.pc_q < 32'h0000_0200) ||
(dut.u_cpu.pc_q >= 32'h0000_0218 && dut.u_cpu.pc_q < 32'h0000_0300))
$display("[gap] %0t ns: PC 0x%03h -> 0x%03h (跳进空洞)", $time, pc_prev, dut.u_cpu.pc_q);
end
end
task dump_pc;
integer i1, i2, best, besti;
begin
$display(" [pc] 热点(pc, 命中数):");
for (i1 = 0; i1 < 3; i1 = i1 + 1) begin
best = -1;
besti = -1;
for (i2 = 0; i2 < 256; i2 = i2 + 1)
if (pc_hist[i2] > best) begin
best = pc_hist[i2];
besti = i2;
end
if (besti >= 0 && best > 0) begin
$display(" pc=0x%03h count=%0d", besti << 2, best);
pc_hist[besti] = 0;
end
end
end
endtask
// ---- RAM 窗口(核对变量地址映射)----
task dump_ramwin;
integer i3;
begin
$display(" [ram] 0x00..0x3F: %08h %08h %08h %08h %08h %08h %08h %08h",
dut.ram[0], dut.ram[1], dut.ram[2], dut.ram[3],
dut.ram[4], dut.ram[5], dut.ram[6], dut.ram[7]);
$display(" [ram] 0x100..0x11F: %08h %08h %08h %08h %08h %08h %08h %08h",
dut.ram[32'h0100 >> 2], dut.ram[32'h0104 >> 2],
dut.ram[32'h0108 >> 2], dut.ram[32'h010C >> 2],
dut.ram[32'h0110 >> 2], dut.ram[32'h0114 >> 2],
dut.ram[32'h0118 >> 2], dut.ram[32'h011C >> 2]);
end
endtask
// 最新版(显存每格 2 字节、难度三档不同尺寸)变量地址:
// g_exit=0x04 g_in_level=0x08 acc_y=0x78C acc_x=0x790 cur_c=0x794
// cur_r=0x798 g_difficulty=0x7A0 g_mines=0x7A4 g_first_done=0x7A8
// g_state=0x7AC flags_cnt=0x7B0 opened_cnt=0x7B4
// flag_bm=0x7F4 open_bm=0x830 mine_bm=0x86C
// g_ncells=0x8B4 g_evq_lost=0x8C0 g_evq_tail=0x8C4 g_evq_head=0x8C8
task dump_vars;
begin
$display(" [vars] exit=%0d in_level=%0d cur=(r%0d,c%0d) acc=(%0d,%0d) diff=%0d mines=%0d first=%0d state=%0d flags=%0d opened=%0d lost=%0d evq(h=%0d,t=%0d)",
dut.ram[32'h0004 >> 2], dut.ram[32'h0008 >> 2],
dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2],
dut.ram[32'h0790 >> 2], dut.ram[32'h078C >> 2],
dut.ram[32'h07A0 >> 2], dut.ram[32'h07A4 >> 2],
dut.ram[32'h07A8 >> 2], dut.ram[32'h07AC >> 2],
dut.ram[32'h07B0 >> 2], dut.ram[32'h07B4 >> 2],
dut.ram[32'h08C0 >> 2],
dut.ram[32'h08C8 >> 2], dut.ram[32'h08C4 >> 2]);
end
endtask
task dump_fb;
input [1023:0] fname;
integer k2, b2;
begin
nz_cnt = 0;
for (k2 = 0; k2 < 600; k2 = k2 + 1)
for (b2 = 0; b2 < 4; b2 = b2 + 1)
if (dut.fb[k2][b2*8 +: 8] != 8'h00)
nz_cnt = nz_cnt + 1;
cursor_row = dut.u_vga.cursor_i[12:8];
cursor_col = dut.u_vga.cursor_i[4:0];
fh = $fopen(fname, "w");
$fwrite(fh, "# tb_saolei framebuffer dump\n");
$fwrite(fh, "TIME %0t\n", $time);
$fwrite(fh, "CURSOR %0d %0d\n", cursor_row, cursor_col);
$fwrite(fh, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]);
$fwrite(fh, "STATS fb_we=%0d nonzero=%0d\n", fb_we_cnt, nz_cnt);
$fwrite(fh, "FB 2400\n");
for (w = 0; w < 600; w = w + 1)
for (b = 0; b < 4; b = b + 1)
$fwrite(fh, "%02X\n", dut.fb[w][b*8 +: 8]);
$fclose(fh);
$display("[tb_saolei] %0t ns dump -> %0s (cursor=%0d,%0d fb_we=%0d nonzero=%0d)",
$time, fname, cursor_row, cursor_col, fb_we_cnt, nz_cnt);
dump_vars;
dump_pc;
dump_ramwin;
end
endtask
// ---------------------------------------------------------------- 完整一局用的工具
// 从 RAM 的位图里取一位(mine_bm @0x86C / open_bm @0x830 / flag_bm @0x7F4)
function integer bitof;
input integer base;
input integer idx;
begin
bitof = (dut.ram[(base >> 2) + (idx >> 5)] >> (idx & 31)) & 1;
end
endfunction
// 已揭开格数:直接读程序里的 opened_cnt(0x7B4)
integer open_n;
task count_open;
begin
open_n = dut.ram[32'h07B4 >> 2];
end
endtask
// 快速按键(按住/松开时长都可控):去抖周期已由 POINTER_SAMPLE_DIV=50 缩短
task press_t;
input integer which;
input integer hold_us;
input integer gap_us;
begin
case (which)
0:
go = 1'b1;
1:
btnu = 1'b1;
2:
btnd = 1'b1;
3:
btnl = 1'b1;
default:
btnr = 1'b1;
endcase
#(hold_us * 1000);
case (which)
0:
go = 1'b0;
1:
btnu = 1'b0;
2:
btnd = 1'b0;
3:
btnl = 1'b0;
default:
btnr = 1'b0;
endcase
#(gap_us * 1000);
end
endtask
task click_t;
begin
press_t(0, 30, 150);
end
endtask
// 把光标移到盘面格 (tr, tc):每次按键后等软件把事件消化掉再回读位置,
// 丢了/多了事件都能自动纠正。盘面格 (0,0) 对应网格 (BOARD_R0, BOARD_C0) = (7, 12)。
task move_to_cell;
input integer tr;
input integer tc;
integer cr, cc2;
begin
cr = dut.ram[32'h0418 >> 2];
cc2 = dut.ram[32'h0414 >> 2];
while (cr > (7 + tr)) begin
press_t(1, 20, 120);
cr = dut.ram[32'h0418 >> 2];
end
while (cr < (7 + tr)) begin
press_t(2, 20, 120);
cr = dut.ram[32'h0418 >> 2];
end
while (cc2 > (12 + tc)) begin
press_t(3, 20, 120);
cc2 = dut.ram[32'h0414 >> 2];
end
while (cc2 < (12 + tc)) begin
press_t(4, 20, 120);
cc2 = dut.ram[32'h0414 >> 2];
end
end
endtask
// 落位 + 校验:没到位就重试(最多 6 次),彻底避免“以为到了、实际差一格”去点错格
task goto_cell;
input integer tr;
input integer tc;
integer tries;
begin
tries = 0;
while (((dut.ram[32'h0418 >> 2] != (7 + tr)) || (dut.ram[32'h0414 >> 2] != (12 + tc))) && (tries < 6)) begin
move_to_cell(tr, tc);
#(300 * 1000);
tries = tries + 1;
end
if (tries >= 6)
$display(" [game] !!! 定位 (%0d,%0d) 失败,实际 cur=(%0d,%0d)", tr, tc,
dut.ram[32'h0418 >> 2], dut.ram[32'h0414 >> 2]);
end
endtask
// 扫完全盘:安全格揭开、雷格插旗;雷区位置直接从 RAM 里的 mine_bm 读
task sweep_board;
integer rr, cc, idx;
begin : sweep_blk
for (rr = 0; rr < 16; rr = rr + 1) begin
for (cc = 0; cc < 16; cc = cc + 1) begin
idx = rr * 16 + cc;
goto_cell(rr, cc);
if (bitof(32'h0000_086C, idx) != 0) begin
sw_flag = 1'b1;
#(30 * 1000);
click_t;
sw_flag = 1'b0;
#(30 * 1000);
end
else
click_t;
// 中途退出关卡就立即报警并收手
if (dut.ram[32'h0008 >> 2] == 0) begin
$display(" [game] *** 在盘面 (%0d,%0d) 处退出了关卡!cur=(%0d,%0d) state=%0d",
rr, cc, dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2],
dut.ram[32'h07AC >> 2]);
disable sweep_blk;
end
// 赢了/输了也收手
if (dut.ram[32'h07AC >> 2] != 0)
disable sweep_blk;
end
count_open;
$display(" [game] 第 %0d 行扫完:已揭开 %0d / %0d, state=%0d, 旗=%0d, cur=(%0d,%0d)",
rr, open_n,
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07AC >> 2], dut.ram[32'h07B0 >> 2],
dut.ram[32'h0798 >> 2], dut.ram[32'h0794 >> 2]);
if (rr == 3)
dump_fb("fb_10_sweep_a.txt");
if (rr == 7)
dump_fb("fb_11_sweep_b.txt");
if (rr == 11)
dump_fb("fb_12_sweep_c.txt");
end
end
endtask
// ---------------------------------------------------------------- 主流程
initial begin
$display("[tb_saolei] === start (SAOLEI_ROM):完整一局 ===");
#2000;
rst_n = 1'b1;
// 1) 开始界面
#(2 * 1000000);
dump_fb("fb_00_start.txt");
// 2) 点 START(光标初始就停在按钮上)-> 写 LEVEL -> IRQ1 进关卡
click_t;
#(2 * 1000000);
dump_fb("fb_01_level.txt");
$display(" [game] 进关卡:in_level=%0d state=%0d", dut.ram[32'h0008 >> 2], dut.ram[32'h07AC >> 2]);
// 3) 关卡内点盘面中心一格:放雷 + 首次展开(第一下必安全)
click;
#(3 * 1000000);
dump_fb("fb_02_firstclick.txt");
count_open;
$display(" [game] 首次点击后:first=%0d 已揭开 %0d / %0d state=%0d diff=%0d",
dut.ram[32'h07A8 >> 2], open_n,
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07AC >> 2], dut.ram[32'h07A0 >> 2]);
// 4) 蛇形扫完全盘:揭开全部 216 个安全格 + 给 40 颗雷插旗 -> 目标通关
sweep_board;
#(3 * 1000000);
dump_fb("fb_13_after_sweep.txt");
count_open;
$display(" [game] 扫完后:已揭开 %0d / %0d, 旗=%0d, state=%0d, 丢事件=%0d",
open_n, (dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07B0 >> 2], dut.ram[32'h07AC >> 2], dut.ram[32'h08C0 >> 2]);
// 5) 通关后再点一下 = 返回背景(回到开始界面)
click_t;
#(3 * 1000000);
dump_fb("fb_14_back_to_start.txt");
$display(" [game] === 结束:state=%0d(1=WIN 2=LOSE)已揭开 %0d/%0d 旗=%0d 丢事件=%0d ===",
dut.ram[32'h07AC >> 2], open_n,
(dut.ram[32'h08B4 >> 2] - dut.ram[32'h07A4 >> 2]),
dut.ram[32'h07B0 >> 2], dut.ram[32'h08C0 >> 2]);
$finish;
end
endmodule
-201
View File
@@ -1,201 +0,0 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_saolei_play —— 没有 VGA 显示器时"在命令行里玩扫雷"用的测试平台
//
// 与 tb_saolei 的区别:没有写死的游戏脚本,改成受外部文件驱动:
//
// cmd.txt 外部(xsim 的 Tcl,见 pipelined-cpu/tools/play_saolei.tcl)往里写
// 按键字符;本 TB 每 100 us 仿真时间轮询一次,读到**一个**字符就执行
// 对应的按键脉冲,然后把文件清空。(多个动作用多次发送实现,
// play_saolei.tcl 的 k 命令会自动把 "uuc" 拆成三次发。)
// u/w 光标上移 d/s 光标下移
// l/a 光标左移 r 光标右移
// c 左键点击(BTNC)
// f 插旗模式点击(先按 SW3 再点击,点上/取消旗帜)
// p 只切换 SW3(插旗模式开关,不点击)
// g 按板上的 go 键
// z 复位(拉低 rst_n 再放开,等于重新上电)
//
// now.txt 每 1 ms 仿真时间把当前显存 dump 一次(格式同 tb_saolei):
// py saolei/tools/fb2png.py --term now.txt -> 终端里的彩色棋盘
// py saolei/tools/fb2png.py now.txt a.png -> 640x480 截图
//
// 编译(两个 -d 都要带 SAOLEI_ROM):
// xvlog -d SAOLEI_ROM -i srcs/board -i srcs/rtl srcs/rtl/*.v \
// srcs/board/cpu21_board_top.v srcs/tb/tb_saolei_play.v
// xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
// xsim saolei_play -tclbatch tools/play_saolei.tcl
//
// 说明:和 tb_saolei 一样用 POINTER_SAMPLE_DIV=50 压缩按键去抖(上板仍是 50000)。
// =============================================================================
module tb_saolei_play;
// ---- 板级顶层端口(全部由本 TB / cmd.txt 驱动,不用 add_force)----
reg clk100 = 1'b0;
reg rst_n = 1'b0;
reg go = 1'b0;
reg [ 2:0] irq_sw = 3'b000;
reg btnu = 1'b0;
reg btnd = 1'b0;
reg btnl = 1'b0;
reg btnr = 1'b0;
reg sw_flag = 1'b0;
wire [15:0] led;
wire led_we;
wire [ 7:0] an;
wire [ 6:0] seg;
wire dp;
wire [ 3:0] vga_r, vga_g, vga_b;
wire vga_hs, vga_vs;
always #5 clk100 = ~clk100;
cpu21_board_top #(
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
) dut (
.clk100 (clk100),
.rst_n (rst_n),
.go (go),
.irq_sw (irq_sw),
.btnu (btnu),
.btnd (btnd),
.btnl (btnl),
.btnr (btnr),
.sw_flag (sw_flag),
.led (led),
.led_we (led_we),
.an (an),
.seg (seg),
.dp (dp),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.ps2_clk (1'b1),
.ps2_data(1'b1)
);
// ---- 一个按键脉冲:按住 hold_us 再松开,然后留 200 us 让 CPU 消化事件 ----
task pulse;
input integer which;
input integer hold_us;
begin
case (which)
1:
btnu = 1'b1;
2:
btnd = 1'b1;
3:
btnl = 1'b1;
4:
btnr = 1'b1;
default:
go = 1'b1;
endcase
#(hold_us * 1000);
case (which)
1:
btnu = 1'b0;
2:
btnd = 1'b0;
3:
btnl = 1'b0;
4:
btnr = 1'b0;
default:
go = 1'b0;
endcase
#(200 * 1000);
end
endtask
// ---- 把 cmd.txt 里的一个字符变成按键动作 ----
// 注意:一次只取一个字符($fgetc),不用 $fgets/字符串,
// 这样不会踩 "reg [8*N:1] 里第一个字符落在哪个字节" 的坑。
task do_key;
input [7:0] ch;
begin
case (ch)
8'h75, 8'h77:
pulse(1, 20); // u / w 上
8'h64, 8'h73:
pulse(2, 20); // d / s 下
8'h6C, 8'h61:
pulse(3, 20); // l / a 左
8'h72:
pulse(4, 20); // r 右
8'h63:
pulse(0, 30); // c 左键点击
8'h67:
pulse(0, 30); // g go 键(同一次点击)
8'h66: begin // f 插旗模式点击
sw_flag = 1'b1;
#(30 * 1000);
pulse(0, 30);
#(30 * 1000);
sw_flag = 1'b0;
#(30 * 1000);
end
8'h70: begin // p 只切换插旗开关
sw_flag = ~sw_flag;
#(50 * 1000);
end
8'h7A: begin // z 复位
rst_n = 1'b0;
#(2 * 1000);
rst_n = 1'b1;
#(500 * 1000);
end
default:
; // 其它字符忽略
endcase
end
endtask
integer cfp;
integer chc;
// ---- 命令轮询:每 100 us 看一次 cmd.txt(只读第一个字符)----
initial begin
#2000;
rst_n = 1'b1; // 上电复位释放
forever begin
#(100 * 1000);
cfp = $fopen("cmd.txt", "r");
if (cfp != 0) begin
chc = $fgetc(cfp);
$fclose(cfp);
cfp = $fopen("cmd.txt", "w"); // 处理掉就清空,避免重复执行
if (cfp != 0)
$fclose(cfp);
if ((chc > 0) && (chc != 8'h0A) && (chc != 8'h0D) && (chc != 8'h20))
do_key(chc[7:0]);
end
end
end
// ---- 画面 dump:每 1 ms 覆盖写一次 now.txt ----
integer df, w2, b2;
initial begin
forever begin
#(1 * 1000 * 1000);
df = $fopen("now.txt", "w");
if (df != 0) begin
$fwrite(df, "TIME %0t\n", $time);
$fwrite(df, "CURSOR %0d %0d\n",
dut.u_vga.cursor_i[12:8], dut.u_vga.cursor_i[4:0]);
$fwrite(df, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]);
$fwrite(df, "FB 2400\n");
for (w2 = 0; w2 < 600; w2 = w2 + 1)
for (b2 = 0; b2 < 4; b2 = b2 + 1)
$fwrite(df, "%02X\n", dut.fb[w2][b2*8 +: 8]);
$fclose(df);
end
end
end
endmodule
-414
View File
@@ -1,414 +0,0 @@
#!/usr/bin/env python3
"""把中断测试程序适配到这块 CPU 上板运行(只改 ROM 内容,不改 CPU 语义)。
背景
----
programs/risc-v-intrupt-ccab.hex 是课程的中断/异常测试程序(270 字)。
直接放进板上 ROM 会有两个问题:
1) ISR 里的 ecall 会停机
三个中断服务程序里各有一条 "addi a7,zero,10" + "ecall"。这颗 CPU 的约定是
a7==34 的 ecall 是历史"显示"调用(无操作),其余 a7 值一律让流水线停机并等待
go_i。于是 ISR 跑到这里就卡住:后面的中断服务显示循环永远不执行,主程序的
跑马灯也停在原地。
-> 把 "addi a7,zero,10" + "ecall" 换成两条 NOP。
2) 显示刷新太快,数码管上看不出跑马灯
原来的显示是 "lui a7,1" + "sw a0,0(a7)"(写 MMIO 的 LED 寄存器)。主循环里
两次显示只隔几拍(50 MHz 下约 0.1 us),而数码管是按位动态扫描的,根本来不及
分辨,只会看到一片亮。
-> 把每条显示换成 "jal ra,DISP_DLY" + "nop",由追加在 ROM 末尾的 DISP_DLY
完成"写 LED 寄存器 + 约 10 ms 延时"。
(注意:早先把旧的 "addi a7,zero,34 + ecall" 换成 MMIO 存储时,rs1 域算错,
把 sw 写成了 sb —— 只更新 LED 寄存器的低字节,高位一直是旧值,显示自然是
错的。DISP_DLY 内部统一用正确的 sw 写整个字,这个脚本会顺带纠正。)
3) **修正 ISR2 / ISR3 的现场恢复顺序**
原始 hex 是手工改过的,两个 ISR 的出栈顺序被写乱(ISR1 是对的):
* ISR3:把 s1 从 s3 的槽位弹出,于是 s1 被写成 s3 保存的值 0xFFFFF111;
ISR3 返回后,正在跑 BLT 测试的 ISR2 的循环计数 s1 就变成了 -3823,
循环要跑 3800 多次(约 170 万周期)才退出 —— 看起来就是“二级中断返回
之后卡住”;
* ISR2:s0/s2/a0/a7/s1/t3/t1 全部错位,返回后调用者的这些寄存器被破坏。
-> 按 asm 清单里正确的 LIFO 顺序,只改 12 条 `lw` 的目标寄存器(字数不变、
地址不动),ISR3 改 5 条、ISR2 改 7 条。
5) **修正 ISR3 的 SB 测试用到的未保存寄存器**
ISR3 的现场只保存了 s1/a7/a0/s0/s3/s4/s5/s6/uepc,但它的 SB 测试却用
t1(地址)、t3(计数)、s2(增量)—— 这三个都没保存。后果:
* 若 IRQ3 抢占正在跑 DIVU 测试的 ISR1(ISR1 用 t3 做递减计数),ISR1 的
计数会被 ISR3 改成 0;ISR1 恢复后 `addi t3,t3,-1` 得到 -1,`bne t3,zero`
永远成立 —— ISR1 的 DIVU 循环再也出不来,表现为“三级中断之后卡住”;
* 主程序的跑马灯步长 t1 也会被改掉。
-> 改用 ISR3 自己保存的 s4/s5/s6:SB 测试结束后紧接着的 IntLoop3
(0x3B4 起)会重新给它们赋值,所以这样换是安全的;同样只改寄存器域,
字数与地址不变。
6) **ISR 不能破坏被打断代码的 ra**
三个 ISR 的现场里都保存了 s1/a7/a0/s0/s3/s4/s5/s6(外加各自的测试寄存器),
但**都没有保存 ra**,而它们要调用显示例程,`jal ra,DISP_DLY` 会把 ra 写成
ISR 内部的返回地址。后果:如果中断恰好落在被打断代码依赖 ra 的位置
—— 典型就是主程序正停在 DISP_DLY 末尾的 `jalr zero,0(ra)`(第 0x454 字)
—— 中断返回后 `jalr` 会跳到 ra 里的地址,也就是 ISR 内部的某条指令上,
于是一路执行到 ISR 结尾的 `csrrci zero,0x4,1`,把 MIE 永久清零:
“跑一阵子之后拨开关再也不响应,只能按复位”。窗口只有 1~2 拍,所以平时
很难遇到(仿真里把延时改到极小才会碰上)。
-> 给 ISR 另加一份显示例程 DISP_DLY_ISR(0x458),链接寄存器改用 **a0**,
显示值改走 **a7**:这两个寄存器三个 ISR 都保存/恢复,所以
* ISR 完全不碰 ra,被打断的代码停在 DISP_DLY 哪里都安全;
* ISR 自己被内层中断打断时,内层 ISR 返回也会把 a0/a7 恢复成被打断时
的值,所以 DISP_DLY_ISR 自己的 `jalr zero,0(a0)` 同样安全。
主程序的 3 个显示点仍用原来的 DISP_DLY(链接在 ra,行为不变)。
每个 ISR 显示点:`jal ra,DISP_DLY` + `nop` 换成
`add a7,a0,zero` + `jal a0,DISP_DLY_ISR` —— 仍是 2 字换 2 字。
以上替换都是 **2 字换 2 字**,两个例程又追加在最后一条指令(uret)之后,所以
**所有指令地址都保持不变**,三个中断入口仍然是 0x0AC / 0x1E4 / 0x308。
DISP_DLY(8 字,追加在程序末尾)
--------------------------------
sw ra, 256(sp) # 保存返回地址
lui a7, 1 # a7 = 0x1000 = LED_ADDR
sw a0, 0(a7) # 把 a0 写进 LED 寄存器(数码管显示它的 8 位十六进制)
lui t2, DELAY # 计数初值为负,向上数到 0(约 10 ms @50 MHz)
1: addi t2, t2, 1
blt t2, zero, 1b
lw ra, 256(sp)
jalr zero, ra, 0
为什么返回地址存在 sp+256 而不是程序栈里:
本程序的 ISR 是“先把 sp 加上去,再往里存”的向上增长栈,一层最多占 48 字节,
最多嵌套 4 层(IRQ_STACK_DEPTH=4)共 192 字节,所以 [sp, sp+192) 是中断随时
可能写到的区域。把返回地址放在 sp+256(越过了这个区域):
* 从 ISR 里调用时,不会覆盖 ISR 存好的 s6 / uepc(早期用 sp-8 就是这么坏掉的:
uepc 被写坏后 uret 会返回到 ISR 内部地址,最后执行到 csrrci 把 MIE 永久清零);
* 中断(包括多层嵌套)随时打断延时也不会破坏它,因为嵌套 ISR 只会在
[sp, sp+192) 里压栈;
* 每次调用各用自己 sp+256 的位置,所以嵌套调用也互不影响。
延时计数不用“递减到 0”而用“负数向上数到 0”:万一被嵌套调用改掉了 t2,循环也
只会在 t2 >= 0 时立即退出,不会像递减写法那样绕成 40 亿次而死等。
用法
----
python tools/adapt_intrupt_ccab.py <输入.hex> <输出.vh> [输出_fast.hex]
<输入.hex> programs/risc-v-intrupt-ccab.hex(未适配的原始镜像)
<输出.vh> srcs/board/risc-v-intrupt-ccab.vh(上板综合用)
[输出_fast.hex] 可选:延时改成约 0.6 us 的仿真专用版本。上板一次中断服务要跑
几十毫秒(几十万个时钟),仿真跑完整流程会很慢,因此仿真用这个
快版来验证控制流(入口 / 中断返回 / 不停机),延时量本身不影响
控制流。
"""
import sys
NOP = 0x00000013
LED_ADDR_HI = 1 # lui a7, 1 -> a7 = 0x1000(LED_ADDR)
# 延时计数:计数初值为负,向上数到 0。0xFFFC0000 -> 0x40000 = 262144 次
# (每轮约 2 个时钟)= 52.4 万个时钟 ≈ 10 ms @50 MHz。
DELAY_NEG_HI = 0xFFFC0
DELAY_FAST_NEG = -16 # 仿真用:addi t2, zero, -16 ≈ 0.6 us
# DISP_DLY 保存返回地址的偏移:必须大于 ISR 最大嵌套栈用量(4 层 x 48B = 192B)
LINK_SLOT = 256
# 寄存器号
ZERO, RA, SP, GP, T2, A0, A7 = 0, 1, 2, 3, 7, 10, 17
def enc_i(imm, rs1, f3, rd, op=0x13):
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op
def enc_s(imm, rs2, rs1, f3):
imm &= 0xFFF
return (((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| ((imm & 0x1F) << 7) | 0x23)
def enc_b(imm, rs2, rs1, f3):
imm &= 0x1FFF
return ((((imm >> 12) & 1) << 31) | (((imm >> 5) & 0x3F) << 25)
| (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (((imm >> 1) & 0xF) << 8) | (((imm >> 11) & 1) << 7) | 0x63)
def enc_u(imm, rd):
return ((imm & 0xFFFFF) << 12) | (rd << 7) | 0x37
def enc_j(imm, rd):
imm &= 0x1FFFFF
return ((((imm >> 20) & 1) << 31) | (((imm >> 1) & 0x3FF) << 21)
| (((imm >> 11) & 1) << 20) | (((imm >> 12) & 0xFF) << 12)
| (rd << 7) | 0x6F)
HALT_LOAD = enc_i(10, ZERO, 0, A7) # addi a7, zero, 10
ECALL = 0x00000073
DISP_LUI = enc_u(LED_ADDR_HI, A7) # lui a7, 1
DISP_SW = enc_s(0, A0, A7, 2) # sw a0, 0(a7) <- 正确:字写
DISP_SB_BAD = enc_s(0, A0, A7, 0) # sb a0, 0(a7) <- 误写成字节写,一并修正
# 说明:这两种都当成"显示点"。DISP_DLY 内部一律用正确的 sw 写整个字,因此即使
# 输入镜像里是 sb(只写低字节,显示的高位会残留旧值),适配后也会恢复正常。
def disp_dly(delay_word):
"""DISP_DLY 的 8 条指令(delay_word 决定延时长短)。
返回地址存在 sp+LINK_SLOT,位于 ISR 可能压栈的区域之上,所以中断(含嵌套)
打断延时也不会破坏它 —— 见文件头的说明。
"""
return [
enc_s(LINK_SLOT, RA, SP, 2), # sw ra, 256(sp)
DISP_LUI, # lui a7, 1
DISP_SW, # sw a0, 0(a7)
delay_word, # lui t2, 0xFFFC0 / addi t2, zero, -16
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, RA, 0x03), # lw ra, 256(sp)
enc_i(0, RA, 0, ZERO, 0x67), # jalr zero, ra, 0
]
def disp_dly_isr(delay_word):
"""ISR 专用的显示/延时例程(8 条)—— 链接寄存器用 a0,不用 ra。
三个 ISR 的现场都保存了 a7 与 a0(s1/a7/a0/s0/s3/s4/s5/s6),所以把链接放
a0、把显示值放 a7 不会额外破坏任何东西;而 ra 完全不被触碰,被打断的代码
即使停在主 DISP_DLY 的 `jalr zero,0(ra)` 上也不会被破坏。
"""
return [
enc_s(LINK_SLOT, A0, SP, 2), # sw a0, 256(sp) 保存链接寄存器
enc_u(LED_ADDR_HI, A0), # lui a0, 1 a0 = 0x1000
enc_s(0, A7, A0, 2), # sw a7, 0(a0) 显示 a7
delay_word, # lui t2, 0xFFFC0 / addi t2,zero,-16
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, A0, 0x03), # lw a0, 256(sp)
enc_i(0, A0, 0, ZERO, 0x67), # jalr zero, a0, 0
]
# 三个中断入口的原指令(核对地址没有跑偏)
ISR_ENTRY = {43: 0x00512023, 121: 0x00612023, 194: 0x00912023}
# 三个 ISR 的代码从 ISR1 入口开始一直排到程序末尾(主程序在它之前)
ISR1_WORD = min(ISR_ENTRY)
# 需要修正目标寄存器的 `lw X,0(sp)`:addr -> (当前错误的 rd, 正确的 rd)
# ISR3(入口 0x308)恢复顺序应为:uepc,s6,s5,s4,s3,s0,a0,a7,s1
# ISR2(入口 0x1E4)恢复顺序应为:uepc,s6,s5,s4,s3,s0,a0,a7,s2,s1,t3,t1
FIX_POPS = {
0x410: (9, 19), 0x418: (19, 8), 0x420: (8, 10),
0x428: (10, 17), 0x430: (17, 9), # ISR3
0x2D0: (18, 8), 0x2D8: (9, 10), 0x2E0: (28, 17),
0x2E8: (6, 18), 0x2F0: (8, 9), 0x2F8: (10, 28),
0x300: (17, 6), # ISR2
}
# ISR3 的 SB 测试用了未保存的 t1/t3/s2 -> 改成 ISR3 自己保存的 s4/s5/s6
# addr -> (要改的域, 原寄存器号, 新寄存器号)
T1, T3, S2, S4, S5, S6 = 6, 28, 18, 20, 21, 22
FIX_ISR3_SB = {
0x358: ('rd', T1, S4), 0x35C: ('rd', T3, S5),
0x364: ('rd', S2, S6), 0x368: ('rs1', T1, S4),
0x378: ('rs2', S2, S6), 0x37C: ('rd_rs1', T1, S4),
0x380: ('rd_rs1', T3, S5), 0x384: ('rs1', T3, S5),
0x388: ('rd', T3, S5), 0x38C: ('rd', T1, S4),
0x390: ('rs1', T1, S4), 0x3A0: ('rd_rs1', T1, S4),
0x3A4: ('rd_rs1', T3, S5), 0x3A8: ('rs1', T3, S5),
}
def read_hex(path):
words = []
with open(path, encoding="ascii") as f:
for line in f:
line = line.split("//")[0].strip()
if line:
words.append(int(line, 16))
return words
def patch(words):
"""返回 (适配后的字表, 适配后的快版字表, 统计信息)。"""
halt_sites = [i for i in range(len(words) - 1)
if words[i] == HALT_LOAD and words[i + 1] == ECALL]
if len(halt_sites) != 3:
raise SystemExit("预期 3 处 'addi a7,zero,10 + ecall'(ISR 停机点),"
"实际找到 %d 处;输入文件不是预期的原始镜像?" % len(halt_sites))
disp_sites = [i for i in range(len(words) - 1)
if words[i] == DISP_LUI
and words[i + 1] in (DISP_SW, DISP_SB_BAD)]
if len(disp_sites) != 11:
raise SystemExit("预期 11 处 'lui a7,1 + sw/sb a0,0(a7)'(显示点),"
"实际找到 %d 处;输入文件不是预期的原始镜像?" % len(disp_sites))
bad_store = [i for i in disp_sites if words[i + 1] == DISP_SB_BAD]
for i in halt_sites:
words[i] = NOP
words[i + 1] = NOP
# plain 变体:只去掉停机 ecall,显示保持原来的 "lui + sw" 两字写法
# (必须在把显示点改成 jal 之前取副本,否则会生成“跳到没有函数体的地址”的镜像)
plain = list(words)
# DISP_DLY 紧跟在原程序的最后一条指令之后(原程序长度不变)
disp_pc = 4 * len(words)
isr_pc = disp_pc + 32 # ISR 专用的 DISP_DLY_ISR 再占 8 字
isr_sites = []
for i in disp_sites:
if i >= ISR1_WORD:
# 三个 ISR 里的显示点:改用 a0 做链接寄存器、显示值走 a7
# 注意 jal 落在显示点的后一条(i+1)上,偏移要相对它自己算
isr_sites.append(i)
words[i] = enc_i(0, A0, 0, A7) # add a7, a0, zero
words[i + 1] = enc_j(isr_pc - 4 * (i + 1), A0) # jal a0, DISP_DLY_ISR
else:
# 主程序里的显示点:照旧用 ra 链接
words[i] = enc_j(disp_pc - 4 * i, RA)
words[i + 1] = NOP
if not isr_sites:
raise SystemExit("没有找到 ISR 里的显示点;中断入口地址对不上?")
# 自检:把刚写进去的 jal 偏移反解出来,确认真的跳到两个例程的入口
def jal_target(w, pc):
imm = (((w >> 31) & 1) << 20) | (((w >> 21) & 0x3FF) << 1) \
| (((w >> 20) & 1) << 11) | (((w >> 12) & 0xFF) << 12)
if imm & 0x100000:
imm -= 0x200000
return pc + imm
for i in disp_sites:
if i >= ISR1_WORD:
want, at = isr_pc, 4 * (i + 1) # ISR:jal 在显示点后一条
else:
want, at = disp_pc, 4 * i # 主程序:jal 就在显示点上
got = jal_target(words[at // 4], at)
if got != want:
raise SystemExit("显示点 0x%03X 的跳转目标算错了:0x%03X != 0x%03X"
% (4 * i, got, want))
for idx, want in ISR_ENTRY.items():
if words[idx] != want:
raise SystemExit("中断入口 0x%03X 处的指令被改动了(%08X != %08X)"
% (4 * idx, words[idx], want))
# --- 4) 修正 ISR2 / ISR3 的现场恢复顺序(只改 lw 的 rd 域) ---
fixed_pops = 0
for pc, (bad_rd, rd) in FIX_POPS.items():
w = words[pc // 4]
op, f3 = w & 0x7F, (w >> 12) & 7
rs1, imm = (w >> 15) & 0x1F, (w >> 20) & 0xFFF
old_rd = (w >> 7) & 0x1F
if not (op == 0x03 and f3 == 2 and rs1 == SP and imm == 0):
raise SystemExit("0x%03X 不是预期的 `lw X,0(sp)`(%08X)" % (pc, w))
if old_rd == rd:
continue # 已经是对的(重复运行时跳过)
if old_rd != bad_rd:
raise SystemExit("0x%03X 的目标寄存器不是预期的 %d(实际 %d)"
% (pc, bad_rd, old_rd))
words[pc // 4] = (w & ~(0x1F << 7)) | (rd << 7)
fixed_pops += 1
# --- 5) 把 ISR3 SB 测试里的 t1/t3/s2 换成 s4/s5/s6 ---
def set_field(w, name, val):
if name == 'rd':
return (w & ~(0x1F << 7)) | (val << 7)
if name == 'rs1':
return (w & ~(0x1F << 15)) | (val << 15)
if name == 'rs2':
return (w & ~(0x1F << 20)) | (val << 20)
if name == 'rd_rs1':
return ((w & ~(0x1F << 7)) | (val << 7)) & ~(0x1F << 15) | (val << 15)
raise SystemExit('未知域 ' + name)
fixed_sb = 0
for pc, (field, old, new) in FIX_ISR3_SB.items():
w = words[pc // 4]
idx = {'rd': (w >> 7) & 0x1F, 'rs1': (w >> 15) & 0x1F,
'rs2': (w >> 20) & 0x1F}
if field == 'rd_rs1':
cur = idx['rd']
if idx['rs1'] != cur:
raise SystemExit('0x%03X 的 rd/rs1 不一致' % pc)
else:
cur = idx[field]
if cur == new:
continue # 已经改过了
if cur != old:
raise SystemExit('0x%03X 的 %s 不是预期的 %d(实际 %d)'
% (pc, field, old, cur))
words[pc // 4] = set_field(w, field, new)
fixed_sb += 1
slow = disp_dly(enc_u(DELAY_NEG_HI, T2)) + disp_dly_isr(enc_u(DELAY_NEG_HI, T2))
fast = disp_dly(enc_i(DELAY_FAST_NEG, ZERO, 0, T2)) \
+ disp_dly_isr(enc_i(DELAY_FAST_NEG, ZERO, 0, T2))
return (words + slow, words + fast, plain,
(halt_sites, disp_sites, disp_pc,
[i for i in disp_sites if i < ISR1_WORD], isr_sites,
bad_store, fixed_pops, fixed_sb))
def write_hex(path, words):
with open(path, "w", encoding="ascii", newline="\n") as f:
for w in words:
f.write("%08X\n" % w)
def write_vh(path, words, src_name, total, note):
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write(" // 由 tools/adapt_intrupt_ccab.py 生成 —— 不要手改\n")
f.write(" // source: %s(%d 字,其余地址填 NOP)\n"
% (src_name, len(words)))
f.write(" // %s\n" % note)
for addr in range(total):
w = words[addr] if addr < len(words) else NOP
f.write(" rom[%d] = 32'h%08X;\n" % (addr, w))
def main():
if len(sys.argv) < 3:
raise SystemExit(__doc__.strip().split("用法")[-1])
src, dst_vh = sys.argv[1], sys.argv[2]
dst_fast = sys.argv[3] if len(sys.argv) > 3 else None
dst_plain = sys.argv[4] if len(sys.argv) > 4 else None
words = read_hex(src)
slow, fast, plain, (halt_sites, disp_sites, disp_pc, main_sites, isr_sites,
bad_store, fixed_pops, fixed_sb) = patch(words)
total = 1024
write_vh(dst_vh, slow, src, total,
"已适配:去掉 ISR 停机 ecall、显示改为 MMIO+DISP_DLY(约 10 ms)")
if dst_fast:
write_hex(dst_fast, fast)
if dst_plain:
write_hex(dst_plain, plain)
print("适配完成:%s(原程序 %d 字 + DISP_DLY %d 字 = %d 字)"
% (dst_vh, len(words), len(slow) - len(words), len(slow)))
print(" 去掉的停机 ecall 位置:%s"
% ", ".join("0x%03X" % (4 * i) for i in halt_sites))
print(" 换成显示调用的点:主程序 %s|ISR %s"
% (", ".join("0x%03X" % (4 * i) for i in main_sites),
", ".join("0x%03X" % (4 * i) for i in isr_sites)))
print(" DISP_DLY 本体:0x%03X - 0x%03X(主程序用,链接在 ra)"
% (disp_pc, disp_pc + 28))
print(" DISP_DLY_ISR 本体:0x%03X - 0x%03X(ISR 用,链接在 a0,不碰 ra)"
% (disp_pc + 32, disp_pc + 60))
print(" 修正的现场恢复顺序:%d 条" % fixed_pops)
print(" ISR3 SB 测试改用的已保存寄存器:%d 条" % fixed_sb)
if bad_store:
print(" 顺带修正的字节写显示:%s"
% ", ".join("0x%03X" % (4 * i) for i in bad_store))
print(" 中断入口(不变):0x0AC / 0x1E4 / 0x308")
if dst_fast:
print(" 仿真快版:%s(延时约 0.6 us)" % dst_fast)
main()
-222
View File
@@ -1,222 +0,0 @@
#!/usr/bin/env python3
"""把课程新版中断测试(EPC 内存堆栈保护 + 附加 SLL/SB/BLTU)适配到本 CPU 上板运行。
输入:programs/risc-v-intrupt-sllsb.hex
课程的原始镜像(344 字,已去掉 Logisim 的 "v2.0 raw" 头)
输出:<输出.vh> 供 cpu21_board_top.v 里 `include 的 ROM 初始化(RISC_V_INTRUPT_SLLSB_ROM)
[输出_adapted.hex] 可选:适配后的镜像本身(方便人工对比/仿真)
只改 ROM 内容,不改 CPU 语义,**所有指令地址保持不变**,因此顶层的
IRQ1/2/3_VECTOR = 0x0AC / 0x240 / 0x3E8 不用动。
改了什么
--------
课程程序用 "addi a7,zero,34" + "ecall" 显示(`ecall` 的服务号 34 是原 Logisim
电路的历史"显示"调用;本 CPU 把它保留为空操作,LED/数码管不会有任何反应)。
这里把每条显示点**原地换成一次调用**(仍然是 2 字换 2 字):
* 主程序(字 0..42): "jal ra,DISP_DLY" + "nop"
* 三个 ISR(字 >= 43): "add a7,a0,zero" + "jal a0,DISP_DLY_ISR"
两个显示例程(各 8 条)追加在程序末尾(最后一条 uret 之后),所以入口地址不变。
为什么 ISR 版不用 ra
--------------------
三个 ISR 的现场都保存/恢复 a7 与 a0,但**都没有保存 ra**(它们原来的显示是
ecall,不需要 ra)。若 ISR 里用 `jal ra,...`,ra 会被写成 ISR 内部地址;恰好
中断落在主程序 DISP_DLY 末尾的 `jalr zero,0(ra)` 上时,返回后会跳到 ISR 内部,
一路执行到 ISR 结尾的 `csrrci zero,0x4,1`,把 MIE 永久清零(表现就是"跑一阵子
之后拨开关再也不响应")。所以 ISR 版把链接寄存器放 a0、显示值放 a7。
为什么返回地址存在 sp+256
------------------------
本程序的 ISR 是"先加 sp 再存"的向上增长栈,一层最多 48 字节,最多嵌套 4 层共
192 字节,所以 [sp, sp+192) 是中断随时会写到的区域。返回地址放在 sp+256(越过
这个区域)后,中断(含嵌套)打断显示延时也破坏不了它。
用法
----
py tools/adapt_intrupt_sllsb.py programs/risc-v-intrupt-sllsb.hex \
srcs/board/risc-v-intrupt-sllsb.vh [programs/risc-v-intrupt-sllsb-adapted.hex]
"""
import sys
NOP = 0x00000013
ECALL = 0x00000073
ZERO, RA, SP, T2, A0, A7 = 0, 1, 2, 7, 10, 17
LED_ADDR_HI = 1 # lui a7, 1 -> a7 = 0x1000(LED_ADDR / 数码管)
# 延时计数初值为负、向上数到 0(约 2 拍一轮):
# 0xFFFC0000 -> 0x40000 = 262144 轮 ≈ 52 万拍 ≈ 10 ms @50 MHz
# @3.125 MHz(CLK_DIV_INTRUPT=32)时约 160 ms,肉眼看得很清楚
DELAY_NEG_HI = 0xFFFC0
LINK_SLOT = 256 # 链接寄存器保存位置,必须 > ISR 最大栈用量(4x48=192)
ISR1_WORD = 43 # 字 43 起是三个 ISR(主程序在它之前)
ROM_WORDS = 1024 # 与 cpu21_board_top 的 hex2vh 约定一致(ROM 深度 8192 也够)
# ---------------------------------------------------------------- 编码
def enc_i(imm, rs1, f3, rd, op=0x13):
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op
def enc_s(imm, rs2, rs1, f3):
imm &= 0xFFF
return (((imm >> 5) << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| ((imm & 0x1F) << 7) | 0x23)
def enc_b(imm, rs2, rs1, f3):
imm &= 0x1FFF
return ((((imm >> 12) & 1) << 31) | (((imm >> 5) & 0x3F) << 25)
| (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (((imm >> 1) & 0xF) << 8) | (((imm >> 11) & 1) << 7) | 0x63)
def enc_u(imm, rd):
return ((imm & 0xFFFFF) << 12) | (rd << 7) | 0x37
def enc_j(imm, rd):
imm &= 0x1FFFFF
return ((((imm >> 20) & 1) << 31) | (((imm >> 1) & 0x3FF) << 21)
| (((imm >> 11) & 1) << 20) | (((imm >> 12) & 0xFF) << 12)
| (rd << 7) | 0x6F)
def enc_r(f7, rs2, rs1, f3, rd):
return ((f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12)
| (rd << 7) | 0x33)
DISP_LUI = enc_u(LED_ADDR_HI, A7) # lui a7, 1
DISP_SW = enc_s(0, A0, A7, 2) # sw a0, 0(a7)
SHOW_SITE = enc_i(34, ZERO, 0, A7) # addi a7, zero, 34 <- 课程程序的"显示"点
SW_A7_0SP = enc_s(0, A7, SP, 2) # sw a7, 0(sp) <- ISR 入口第一条
def disp_dly():
"""主程序用:显示 a0,延时约 10 ms,链接在 ra(8 条)。"""
return [
enc_s(LINK_SLOT, RA, SP, 2), # sw ra, 256(sp)
DISP_LUI, # lui a7, 1
DISP_SW, # sw a0, 0(a7)
enc_u(DELAY_NEG_HI, T2), # lui t2, 0xFFFC0
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, RA, 0x03), # lw ra, 256(sp)
enc_i(0, RA, 0, ZERO, 0x67), # jalr zero, ra, 0
]
def disp_dly_isr():
"""ISR 用:显示 a7,链接在 a0,完全不碰 ra(8 条)。"""
return [
enc_s(LINK_SLOT, A0, SP, 2), # sw a0, 256(sp)
enc_u(LED_ADDR_HI, A0), # lui a0, 1
enc_s(0, A7, A0, 2), # sw a7, 0(a0)
enc_u(DELAY_NEG_HI, T2), # lui t2, 0xFFFC0
enc_i(1, T2, 0, T2), # addi t2, t2, 1
enc_b(-4, ZERO, T2, 4), # blt t2, zero, -4
enc_i(LINK_SLOT, SP, 2, A0, 0x03), # lw a0, 256(sp)
enc_i(0, A0, 0, ZERO, 0x67), # jalr zero, a0, 0
]
def read_hex(path):
words = []
with open(path, encoding="ascii") as f:
for line in f:
line = line.split("//")[0].strip()
if not line or line.lower().startswith("v2.0"):
continue # 容忍 Logisim 的 "v2.0 raw" 头
words.append(int(line, 16))
return words
def main():
if len(sys.argv) < 3:
raise SystemExit(__doc__)
src, dst_vh = sys.argv[1], sys.argv[2]
dst_hex = sys.argv[3] if len(sys.argv) > 3 else None
w = read_hex(src)
print("源程序 %d 字" % len(w))
# ---- 自检:三个 ISR 入口必须还是 sw a7,0(sp) ----
entries = [i for i, x in enumerate(w) if x == SW_A7_0SP]
print("ISR 入口候选(sw a7,0(sp))字号 = %s" % entries)
if entries != [43, 144, 250]:
print("警告:ISR 入口与预期 [43, 144, 250] 不一致,请核对顶层 IRQ_VECTOR!")
# ---- 例程地址(追加在程序末尾,入口地址因此不变)----
n = len(w)
dly_addr = n * 4
dly_isr_addr = (n + 8) * 4
# ---- 逐条替换显示点 ----
patched_main = patched_isr = 0
i = 0
while i < n - 1:
if w[i] == SHOW_SITE and w[i + 1] == ECALL:
if i < ISR1_WORD:
w[i] = enc_j(dly_addr - i * 4, RA) # jal ra, DISP_DLY
w[i + 1] = NOP
patched_main += 1
else:
w[i] = enc_r(0, 0, A0, 0, A7) # add a7, a0, zero
# 注意:jal 落在 w[i+1],所以偏移必须按 (i+1) 算,否则会跳过例程首条
w[i + 1] = enc_j(dly_isr_addr - (i + 1) * 4, A0) # jal a0, DISP_DLY_ISR
patched_isr += 1
i += 2
else:
i += 1
print("显示点替换:主程序 %d 处、ISR %d 处" % (patched_main, patched_isr))
# ---- 自检:每个显示调用的跳转目标必须正好落在例程首字 ----
def jal_target(word, addr):
imm = (((word >> 31) & 1) << 20) | (((word >> 21) & 0x3FF) << 1) \
| (((word >> 20) & 1) << 11) | (((word >> 12) & 0xFF) << 12)
if imm & (1 << 20):
imm -= (1 << 21)
return addr + imm
bad = []
for i in range(n - 1):
if w[i] == enc_r(0, 0, A0, 0, A7) and (w[i + 1] & 0x7F) == 0x6F:
if jal_target(w[i + 1], (i + 1) * 4) != dly_isr_addr:
bad.append(i)
if (w[i] & 0x7F) == 0x6F and ((w[i] >> 7) & 0x1F) == RA and w[i + 1] == NOP:
if jal_target(w[i], i * 4) != dly_addr:
bad.append(i)
if bad:
raise SystemExit("自检失败:字 %s 的跳转目标不对" % bad)
print("自检通过:所有显示调用都精确跳到例程首字(DISP_DLY 0x%X / ISR 0x%X)"
% (dly_addr, dly_isr_addr))
w += disp_dly() + disp_dly_isr()
print("追加 DISP_DLY @字 %d (0x%X)、DISP_DLY_ISR @字 %d (0x%X)"
% (n, dly_addr, n + 8, dly_isr_addr))
# ---- 写 .vh(与 tools/hex2vh.py 的格式一致)----
if len(w) > ROM_WORDS:
raise SystemExit("程序 %d 字,超过 %d 字上限" % (len(w), ROM_WORDS))
with open(dst_vh, "w", encoding="utf-8") as f:
f.write(" // generated by tools/adapt_intrupt_sllsb.py - do not edit by hand\n")
f.write(" // source: %s (%d words, remaining entries filled with NOP)\n"
% (src, len(w)))
for a in range(ROM_WORDS):
f.write(" rom[%d] = 32'h%08X;\n" % (a, w[a] if a < len(w) else NOP))
print("已生成 %s(%d 个字)" % (dst_vh, ROM_WORDS))
if dst_hex:
with open(dst_hex, "w", encoding="ascii") as f:
for x in w:
f.write("%08X\n" % x)
print("已生成 %s(适配后的镜像)" % dst_hex)
main()
-101
View File
@@ -1,101 +0,0 @@
# =============================================================================
# check_rtl_text.ps1 —— detect stray non-Verilog text in the RTL sources.
#
# WHY: this project's .v files were twice corrupted by a text tool that inserted a
# line of README prose into them ("Tcl ..." / a Chinese sentence), which made
# Vivado fail with
# ERROR: [Synth 8-2715] syntax error near "..." 'non-printable character 0xe4'
# and produced a cascade of bogus errors in the included ROM .vh. Running this
# script takes a second and catches it before spending a synthesis run.
#
# WHAT IT CHECKS
# 1) a line inside .v/.vh/.xdc that has non-ASCII characters *outside* a comment
# (i.e. real code position) -> corruption;
# 2) a line that looks like pasted prose/markdown (leading "|", "**", or a bare
# "#" that is NOT a Verilog delay/parameter override: #10, #(expr), #delay are
# legal Verilog, so only "#" followed by something else (space, CJK...) is flagged).
#
# USAGE (from the repository root or from pipelined-cpu/):
# powershell -ExecutionPolicy Bypass -File pipelined-cpu\tools\check_rtl_text.ps1
# Exit code 0 = clean, 1 = found something.
# =============================================================================
param(
# -Fix: 自动删除"整行都是粘贴进来的正文"的坏行(保守:只删不含 ; ( ) = 等代码标点的整行)。
# 被部分覆盖的行(例如某个端口行被改坏)只报告,需手工或 git 修。
[switch]$Fix
)
$ErrorActionPreference = 'Stop'
$srcs = Join-Path (Split-Path -Parent $PSScriptRoot) 'srcs'
if (-not (Test-Path $srcs)) { Write-Output "ERROR: cannot find $srcs"; exit 2 }
$bad = 0
$files = Get-ChildItem -Recurse -Path $srcs -Include *.v, *.vh, *.xdc -File
foreach ($f in $files) {
$rel = $f.FullName.Substring($srcs.Length).TrimStart('\', '/')
# 用 .NET 按 UTF-8 读原始字节:PS 5.1 的 Get-Content 会按 GBK 解码,中文会乱。
# 按行切开时保留行尾(\r\n / \n),写回时逐字节保持原样,不归一化换行/编码/BOM。
$bytes = [System.IO.File]::ReadAllBytes($f.FullName)
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
$lines = @([regex]::Split($text, "(?<=\n)"))
$drop = @()
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].TrimEnd("`r", "`n")
$lineNo = $i + 1
# ---- 只去掉 // (或 .xdc 的 #) 之后的部分,其余按代码位置检查 ----
# 注意:这里不处理 /* */ 块注释(srcs 里没有块注释,之前因此漏报过坏行)
$code = $line
if ($f.Extension -eq '.xdc') {
$h = $code.IndexOf('#'); if ($h -ge 0) { $code = $code.Substring(0, $h) }
}
else {
$c = $code.IndexOf('//'); if ($c -ge 0) { $code = $code.Substring(0, $c) }
}
# 字符串字面量里的中文是合法的(如 $display("中文")),先去掉再判断
$code = $code -replace '"[^"]*"', '""'
$trim = $line.Trim()
# ---- check 1: non-ASCII in code position ----
if ($code -match '[^\x00-\x7F]') {
# 整行只有正文(不含 ; ( ) = ` ' " 之类代码标点)⇒ 可以安全整行删掉
$onlyProse = ($trim -notmatch '[;()=`''"]')
$tag = if ($onlyProse) { ' [整行正文,可用 -Fix 删掉]' } else { ' [部分覆盖,需手工/git 修]' }
Write-Output ("!! $rel`:$lineNo non-ASCII text in code position${tag}:")
Write-Output (" " + $trim)
if ($Fix -and $onlyProse) { $drop += $i }
$bad++
continue
}
# ---- check 2: pasted prose / markdown leftovers ----
# 注意:.v/.vh 里 "#" 是合法的延时语法(#10 / #(expr) / #delay),
# 只有 "#" 后面跟着空格/中文等非代码字符才当成注释残留。
if ($trim -match '^\||^\*\*|^#[^0-9(A-Za-z_]' -and $f.Extension -ne '.xdc') {
Write-Output ("!! $rel`:$lineNo looks like pasted prose:")
Write-Output (" " + $trim)
$bad++
}
}
if ($Fix -and $drop.Count -gt 0) {
$kept = for ($i = 0; $i -lt $lines.Count; $i++) { if ($drop -notcontains $i) { $lines[$i] } }
[System.IO.File]::WriteAllText($f.FullName, ($kept -join ''), (New-Object System.Text.UTF8Encoding($false)))
Write-Output (" -Fix: 已从 $rel 删除 " + $drop.Count + " 行(编码/换行保持不变),请重跑自检确认")
}
}
Write-Output ""
Write-Output ("scanned " + $files.Count + " files under srcs/")
if ($bad -eq 0) {
Write-Output "RTL text check: OK (no stray non-Verilog text found)."
exit 0
}
Write-Output ("RTL text check: FAILED, " + $bad + " suspicious line(s) - fix them before synthesis.")
Write-Output "Hint1: 这类行通常是编辑器/工具粘贴进来的正文;整行正文可以用 -Fix 删掉:"
Write-Output " powershell -ExecutionPolicy Bypass -File tools\check_rtl_text.ps1 -Fix"
Write-Output "Hint2: 若报的是 [部分覆盖],对照 git diff 手工修(被编辑器保存覆盖时,先关掉那个标签页再改)。"
exit 1
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""把 $readmemh 格式的 hex 文件转换成 Verilog include 文件,供综合时初始化 ROM。
用法: python tools/hex2vh.py <input.hex> <output.vh> [ROM_WORDS]
输出内容形如(每行一条赋值,未用到的地址填 NOP 0x00000013):
rom[0] = 32'h00001037;
rom[1] = 32'h00100093;
配套 RTL 用法(见 srcs/board/cpu21_board_top.v,两者同目录,所以 include 能直接解析):
initial begin
for (i = 0; i < ROM_WORDS; i = i + 1) rom[i] = 32'h0000_0013; // NOP 填充
`include "cpu21_board_rom.vh"
end
这样 ROM 内容直接跟着 RTL 走,不依赖综合时的当前工作目录,
避免了 $readmemh 相对路径在 Vivado 综合流程里找不到文件(且只报 warning)的坑。
"""
import sys
NOP = 0x00000013
def main():
src, dst = sys.argv[1], sys.argv[2]
words_total = int(sys.argv[3]) if len(sys.argv) > 3 else 1024
words = []
with open(src, encoding="ascii") as f:
for line in f:
line = line.split("//")[0].strip()
if not line:
continue
words.append(int(line, 16))
if len(words) > words_total:
raise SystemExit(
"程序 %d 个字,超过 ROM 容量 %d 个字" % (len(words), words_total)
)
with open(dst, "w", encoding="utf-8") as f:
f.write(" // generated by tools/hex2vh.py - do not edit by hand\n")
f.write(
" // source: %s (%d words, remaining entries filled with NOP)\n"
% (src, len(words))
)
for addr in range(words_total):
w = words[addr] if addr < len(words) else NOP
f.write(" rom[%d] = 32'h%08X;\n" % (addr, w))
print("已生成 %s(%d 个字,源程序 %d 个字)" % (dst, words_total, len(words)))
main()
-145
View File
@@ -1,145 +0,0 @@
# =============================================================================
# play_saolei.tcl —— 没有 VGA 显示器时,在 xsim 命令行里"玩"扫雷
#
# 用法(建议用 source -encoding utf-8 加载本文件,否则 xsim 默认按 GBK 读,中文注释会乱):
# cd <某个空目录,例如 _tmp/saolei_sim>
# xvlog -d SAOLEI_ROM -i <pipelined-cpu/srcs/board> -i <pipelined-cpu/srcs/rtl> \
# <pipelined-cpu/srcs/rtl/*.v> <pipelined-cpu/srcs/board/cpu21_board_top.v> \
# <pipelined-cpu/srcs/tb/tb_saolei_play.v>
# xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
# xsim saolei_play -tclbatch <本脚本绝对路径>
#
# 然后在 xsim 提示符下敲命令(一个字母就是一步,会立刻重画棋盘):
# k c 左键点击(= 板上 BTNC)
# k u / k d / k l / k r 光标 上/下/左/右(也认 w s a d)
# k f 插旗模式点击(点一下插旗,再点取消;= 按住 SW3 再点)
# k p 只切换插旗模式开关(不点击)
# k z 复位(等于重新上电,回到开始界面)
# k uucc 可以连写:上、上、点、点
# shot 不按键,只重画一遍当前画面 + 存一张 PNG
# view 用系统看图器打开最新那张 PNG
# help 键位说明
#
# 截图:k 每执行**一个**按键动作就自动存一张 PNG 到 shots/NNN_<按键>.png
# (可以直接放进报告),同时覆盖 shots/latest.png。
# 在 VS Code 里打开 shots/latest.png,每次操作后它都会自动刷新 —— 这就是
# "每一步都能看到 PNG" 的用法。
#
# 原理:k 把字符写进 cmd.txt;tb_saolei_play 每 100 us 仿真时间轮询一次并执行按键,
# 同时每 1 ms 把显存 dump 到 now.txt;k 再调 fb2png.py --term 把画面画到终端。
# 仿真速度大约是 1 ms 仿真时间 = 0.5 s 墙钟,所以每步等 1~2 秒,比真实慢很多,
# 但足够用来验证"这个程序真的能玩"。
# =============================================================================
set script_dir [file dirname [file normalize [info script]]]
set fb2png [file normalize [file join $script_dir .. .. saolei tools fb2png.py]]
# 找系统 Python:xsim 的 PATH 里带着 Vivado 自带的 python3.8,直接 exec py 会撞上它
# (SRE module mismatch),所以用绝对路径优先。
proc find_python {} {
foreach ver {314 313 312 311 310} {
set c [file join $::env(LOCALAPPDATA) Programs Python "Python$ver" python.exe]
if {[file exists $c]} { return $c }
}
return "py"
}
set python [find_python]
# xsim 的进程环境里带着 Vivado 的 PYTHONHOME,会让**系统 python** 也去 Vivado 的
# lib 里找模块(SRE module mismatch)。清掉它,子进程才不会继承。
catch { unset ::env(PYTHONHOME) }
catch { unset ::env(PYTHONPATH) }
puts " (python: $python)"
# 按键字符 -> 人类可读的动作(只用于回显)
array set KEYNAME {
c "左键点击" u "上" w "上" d "下" s "下"
l "左" a "左" r "右"
f "插旗点击" p "切插旗开关" g "go 键" z "复位"
}
# 每次只把一个字符写进 cmd.txt(TB 每 100 us 读走一个,读完就清空)
proc send_char {c} {
set fh [open "cmd.txt" "w"]
puts -nonewline $fh $c
close $fh
}
# 先确保 cmd.txt 存在,否则 TB 每 100 us 都会刷一条 could not be opened 警告
if {![file exists "cmd.txt"]} {
set fh [open "cmd.txt" "w"]
close $fh
}
# 把 now.txt 渲染到终端(ANSI 彩色棋盘)
proc render {} {
global fb2png python
if {![file exists "now.txt"]} {
puts " (now.txt not ready yet - run a bit first)"
return
}
if {[catch { exec $python $fb2png --term now.txt >@stdout 2>@stderr } err]} {
puts " render failed: $err"
puts " try manually: py \"$fb2png\" --term now.txt"
}
}
proc help {} {
puts " ---------------- SAOLEI controls (type at the xsim prompt) ----------------"
puts " k c left click (board BTNC)"
puts " k u/d/l/r move cursor up/down/left/right (w/s/a/d work too)"
puts " k f click in flag mode k p toggle flag switch only"
puts " k z reset k uucc several actions at once"
puts " shot redraw+screenshot view open latest PNG"
puts " help this text quit exit"
puts " every key saved one PNG: shots/NNN_<key>.png and shots/latest.png"
puts " ---------------------------------------------------------------------------"
}
# ---- every step saves one PNG: shots/001_c.png ... plus shots/latest.png ----
set shot_dir "shots"
file mkdir $shot_dir
set shot_n 0
proc save_shot {tag} {
global fb2png shot_dir python
if {![file exists "now.txt"]} { return }
incr ::shot_n
set name [format "%s/%03d_%s.png" $shot_dir $::shot_n $tag]
if {[catch { exec $python $fb2png --quiet now.txt $name } err]} {
puts " (screenshot failed: $err)"
return
}
catch { file copy -force $name "$shot_dir/latest.png" }
puts " png -> $name"
}
proc view {} {
global shot_dir
set f [file normalize "$shot_dir/latest.png"]
if {![file exists $f]} { puts " (no screenshot yet)"; return }
catch { exec cmd /c start "" $f }
}
# one step: send the key -> advance simulation -> save PNG -> redraw
proc k {keys} {
set s [string map {" " "c"} [string tolower $keys]]
foreach ch [split $s ""] {
if {[string length $ch] == 0} { continue }
puts " >> $ch"
send_char $ch
run 1500 us
save_shot $ch
}
render
}
proc shot {} {
save_shot "shot"
render
}
# ---- opening: run a little so the start screen is drawn, then show it ----
help
puts "booting... (init + draw start screen)"
run 3 ms
render
-109
View File
@@ -1,109 +0,0 @@
# ============================================================================
# cpu21-riscv 回归脚本(Vivado xsim)
#
# 在工程目录 pipelined-cpu/ 下运行:
# powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
#
# 做四件事:
# 1) 逐个跑指令测试:muldiv_test / isa_test / lui_test / mmio_test,
# 把测试平台打印的 LED 序列与 programs/<名字>.expected 逐值比对;
# 2) 跑两个流水基线(默认 ROM):tb_no_intr、tb_cpu21_riscv_redirect_int_bpb,
# 只打印 FINAL 统计行,便于和已知基线核对;
# 3) 跑板级顶层(-d BOARD_ROM_FAST),检查跑马灯 LED 序列;
# 4) 最后汇总 PASS/FAIL 并按失败数设置退出码。
#
# 编译与仿真都放在 $env:TEMP\cpu21_regress 下,不会往工程目录写中间文件。
# 需要 xvlog / xelab / xsim 在 PATH 上(Vivado 安装目录的 bin)。
# ============================================================================
$ErrorActionPreference = 'Stop'
$proj = Split-Path -Parent $PSScriptRoot # tools/ 的上一级就是工程目录
$projF = ($proj -replace '\\', '/') # Verilog 字符串里要用正斜杠
$sim = Join-Path $env:TEMP 'cpu21_regress'
$enc = New-Object System.Text.UTF8Encoding($false)
foreach ($tool in 'xvlog', 'xelab', 'xsim') {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
Write-Output "ERROR: 找不到 $tool,请把 Vivado 的 bin 目录加到 PATH"
exit 1
}
}
New-Item -ItemType Directory -Force $sim | Out-Null
Set-Content -Path "$sim\run.tcl" -Value @('run all', 'quit') -Encoding ascii
$RTL = @(Get-ChildItem "$proj\srcs\rtl\*.v" | ForEach-Object FullName)
$fail = 0
# TB 里的 ROM_FILE 参数默认是文件名,这里替换成绝对路径(PowerShell 传参有坑,
# 直接改副本最稳)
function Sim-Top {
param($Top, $TbFile, $DefaultRom, $RealRom, $Snap)
$txt = [System.IO.File]::ReadAllText($TbFile, $enc)
$new = $txt.Replace('"' + $DefaultRom + '"', '"' + $projF + '/srcs/rom/' + $RealRom + '"')
[System.IO.File]::WriteAllText("$sim\$Snap.v", $new, $enc)
Remove-Item -Recurse -Force "$sim\xsim.dir" -ErrorAction SilentlyContinue
& xvlog @RTL "$sim\$Snap.v" 2>&1 | Out-File -Encoding ascii "$sim\xvlog_$Snap.txt"
& xelab -top $Top -snapshot $Snap 2>&1 | Out-File -Encoding ascii "$sim\xelab_$Snap.txt"
& xsim $Snap -tclbatch run.tcl 2>&1 | Out-File -Encoding ascii "$sim\run_$Snap.txt"
$err = Select-String -Path "$sim\xvlog_$Snap.txt", "$sim\xelab_$Snap.txt" -Pattern 'ERROR' |
Select-Object -First 1 -ExpandProperty Line
if ($err) { Write-Output "!! $Snap : $err"; $script:fail++ }
}
Push-Location $sim
try {
Write-Output '===== 1) 指令测试(tb_no_intr,与 programs/*.expected 比对)====='
foreach ($t in 'muldiv_test', 'isa_test', 'lui_test', 'mmio_test') {
Sim-Top -Top tb_no_intr -TbFile "$proj\srcs\tb\tb_no_intr.v" `
-DefaultRom 'risc-v-benchmark_ccab.hex' -RealRom "$t.hex" -Snap "sim_$t"
$log = "$sim\run_sim_$t.txt"
$obs = @(Select-String -Path $log -Pattern 'LEDv=1 LED=([0-9a-fA-F]{8})' -AllMatches |
ForEach-Object { $_.Matches[0].Groups[1].Value.ToLower() })
$exp = @(Get-Content "$proj\programs\$t.expected" |
ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ -ne '' })
if (($obs -join ',') -eq ($exp -join ',')) {
Write-Output (" {0,-13} PASS ({1} 个 LED 值)" -f $t, $obs.Count)
} else {
Write-Output (" {0,-13} FAIL 期望 {1} / 观测 {2}" -f $t, $exp.Count, $obs.Count)
Write-Output (" 期望: " + ($exp -join ' '))
Write-Output (" 观测: " + ($obs -join ' '))
$fail++
}
Write-Output (' ' + (Select-String -Path $log -Pattern 'FINAL:' |
Select-Object -First 1 -ExpandProperty Line))
}
Write-Output '===== 2) 流水基线(默认 ROM)====='
Sim-Top -Top tb_no_intr -TbFile "$proj\srcs\tb\tb_no_intr.v" `
-DefaultRom 'risc-v-benchmark_ccab.hex' -RealRom 'risc-v-benchmark_ccab.hex' -Snap 'base_no_intr'
Write-Output (' tb_no_intr : ' + (Select-String -Path "$sim\run_base_no_intr.txt" -Pattern 'FINAL:' |
Select-Object -First 1 -ExpandProperty Line))
Sim-Top -Top tb_cpu21_riscv_redirect_int_bpb -TbFile "$proj\srcs\tb\tb_cpu21_riscv_redirect_int_bpb.v" `
-DefaultRom 'cpu21_riscv_redirect_int_bpb_rom.hex' -RealRom 'cpu21_riscv_redirect_int_bpb_rom.hex' `
-Snap 'base_intr'
Write-Output (' tb_intr : ' + (Select-String -Path "$sim\run_base_intr.txt" -Pattern 'FINAL:' |
Select-Object -First 1 -ExpandProperty Line))
Write-Output '===== 3) 板级顶层(-d BOARD_ROM_FAST,跑马灯)====='
Remove-Item -Recurse -Force "$sim\xsim.dir" -ErrorAction SilentlyContinue
& xvlog -d BOARD_ROM_FAST @RTL "$proj\srcs\board\cpu21_board_top.v" "$proj\srcs\tb\tb_board_top.v" 2>&1 |
Out-File -Encoding ascii "$sim\xvlog_board.txt"
& xelab -top tb_board_top -snapshot board -d BOARD_ROM_FAST 2>&1 | Out-File -Encoding ascii "$sim\xelab_board.txt"
& xsim board -tclbatch run.tcl 2>&1 | Out-File -Encoding ascii "$sim\run_board.txt"
$seq = @(Select-String -Path "$sim\run_board.txt" -Pattern 'LED=([0-9a-f]{4})' -AllMatches |
ForEach-Object { $_.Matches[0].Groups[1].Value })
Write-Output (' 前 18 个 LED: ' + (($seq[0..17]) -join ' '))
Write-Output (" 变化次数: " + $seq.Count)
if (($seq[0..17] -join ' ') -ne '0000 0001 0002 0004 0008 0010 0020 0040 0080 0100 0200 0400 0800 1000 2000 4000 8000 0001') {
Write-Output ' FAIL:跑马灯序列与预期不符'; $fail++
} else {
Write-Output ' PASS'
}
} finally {
Pop-Location
}
Write-Output ''
if ($fail -eq 0) { Write-Output '全部通过 (所有回归 PASS)' } else { Write-Output "有 $fail 项失败" }
exit $fail
-232
View File
@@ -1,232 +0,0 @@
#!/usr/bin/env python3
"""极简 RISC-V 汇编器:仅支持本项目测试所需的指令子集。
用法: python tools/rv_asm.py <input.asm> <output.hex>
同时生成 <output.hex>.lst 反汇编/编码清单,便于核对。
支持:add/sub/and/or/xor/sll/srl/sra/slt/sltu、
mul/mulh/mulhsu/mulhu、
addi/slti/sltiu/xori/ori/andi、slli/srli/srai、
lw/sw/sb、beq/bne/blt/bge/bltu/bgeu、lui、jal/jalr、ecall、标签、# 注释
伪指令:print(= lui t6,LED_ADDR>>12 + sw a0,0(t6),向 MMIO 的 LED 寄存器写入 a0)、
halt(= addi a7,zero,10 + ecall),各占两个字。
示例:python tools/rv_asm.py programs/isa_test.asm srcs/rom/isa_test.hex
"""
import re
import sys
REGS = {
"zero": 0,
"ra": 1,
"sp": 2,
"gp": 3,
"tp": 4,
"t0": 5,
"t1": 6,
"t2": 7,
"s0": 8,
"s1": 9,
"a0": 10,
"a1": 11,
"a2": 12,
"a3": 13,
"a4": 14,
"a5": 15,
"a6": 16,
"a7": 17,
"s2": 18,
"s3": 19,
"s4": 20,
"s5": 21,
"s6": 22,
"s7": 23,
"s8": 24,
"s9": 25,
"s10": 26,
"s11": 27,
"t3": 28,
"t4": 29,
"t5": 30,
"t6": 31,
}
def reg(t):
t = t.strip()
if t in REGS:
return REGS[t]
m = re.fullmatch(r"x(\d+)", t)
if m:
return int(m.group(1))
raise SystemExit("未知寄存器: " + t)
def num(t):
return int(t.strip(), 0)
def enc_r(f7, rs2, rs1, f3, rd):
return (f7 << 25) | (rs2 << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | 0x33
def enc_i(imm, rs1, f3, rd, op=0x13):
return ((imm & 0xFFF) << 20) | (rs1 << 15) | (f3 << 12) | (rd << 7) | op
def enc_s(imm, rs2, rs1, f3):
imm &= 0xFFF
return (
((imm >> 5) << 25)
| (rs2 << 20)
| (rs1 << 15)
| (f3 << 12)
| ((imm & 0x1F) << 7)
| 0x23
)
def enc_b(imm, rs2, rs1, f3):
imm &= 0x1FFF
return (
(((imm >> 12) & 1) << 31)
| (((imm >> 5) & 0x3F) << 25)
| (rs2 << 20)
| (rs1 << 15)
| (f3 << 12)
| (((imm >> 1) & 0xF) << 8)
| (((imm >> 11) & 1) << 7)
| 0x63
)
def enc_u(imm, rd):
return ((imm & 0xFFFFF) << 12) | (rd << 7) | 0x37
def enc_j(imm, rd):
imm &= 0x1FFFFF
return (
(((imm >> 20) & 1) << 31)
| (((imm >> 1) & 0x3FF) << 21)
| (((imm >> 11) & 1) << 20)
| (((imm >> 12) & 0xFF) << 12)
| (rd << 7)
| 0x6F
)
RTYPE = {
"add": (0x00, 0),
"sub": (0x20, 0),
"sll": (0x00, 1),
"slt": (0x00, 2),
"sltu": (0x00, 3),
"xor": (0x00, 4),
"srl": (0x00, 5),
"sra": (0x20, 5),
"or": (0x00, 6),
"and": (0x00, 7),
"mul": (0x01, 0),
"mulh": (0x01, 1),
"mulhsu": (0x01, 2),
"mulhu": (0x01, 3),
"divu": (0x01, 5),
"remu": (0x01, 7),
}
ITYPE = {"addi": 0, "slti": 2, "sltiu": 3, "xori": 4, "ori": 6, "andi": 7}
SHIFT = {"slli": 1, "srli": 5, "srai": 5}
BTYPE = {"beq": 0, "bne": 1, "blt": 4, "bge": 5, "bltu": 6, "bgeu": 7}
# MMIO:LED 数据寄存器地址,需与 RTL 顶层 parameter LED_ADDR 保持一致
LED_ADDR = 0x1000
# 双字伪指令:print 向 MMIO 的 LED 写寄存器写入 a0;halt 停机
PSEUDO_SIZE = {"print": 2, "halt": 2}
def assemble(lines):
labels, items, addr = {}, [], 0
for raw in lines:
line = raw.split("#")[0].strip()
if not line:
continue
while True:
m = re.match(r"^([A-Za-z_.$][\w.$]*)\s*:\s*(.*)$", line)
if not m:
break
labels[m.group(1)] = addr
line = m.group(2).strip()
if not line:
break
if not line:
continue
items.append((addr, line))
addr += 4 * PSEUDO_SIZE.get(line.split()[0].lower(), 1)
out = []
for addr, line in items:
parts = re.split(r"[\s,]+", line)
mn, a = parts[0].lower(), parts[1:]
if mn in RTYPE:
f7, f3 = RTYPE[mn]
w = enc_r(f7, reg(a[2]), reg(a[1]), f3, reg(a[0]))
elif mn in ITYPE:
im = num(a[2])
if not (-2048 <= im <= 2047):
raise SystemExit("I 型立即数超出 12 位有符号范围: " + line)
w = enc_i(im, reg(a[1]), ITYPE[mn], reg(a[0]))
elif mn in SHIFT:
w = enc_i(num(a[2]) & 0x1F, reg(a[1]), SHIFT[mn], reg(a[0]))
elif mn == "lui":
w = enc_u(num(a[1]), reg(a[0]))
elif mn in ("lw", "sw", "sb"):
m = re.fullmatch(r"(-?\w+)\((\w+)\)", a[1])
if not m:
raise SystemExit("内存操作数格式应为 imm(rs1): " + line)
im = num(m.group(1))
if not (-2048 <= im <= 2047):
raise SystemExit("访存偏移超出 12 位有符号范围: " + line)
if mn == "lw":
w = enc_i(im, reg(m.group(2)), 2, reg(a[0]), 0x03)
elif mn == "sw":
w = enc_s(im, reg(a[0]), reg(m.group(2)), 2)
else: # sb
w = enc_s(im, reg(a[0]), reg(m.group(2)), 0)
elif mn in BTYPE:
tgt = labels[a[2]] if a[2] in labels else num(a[2])
w = enc_b(tgt - addr, reg(a[1]), reg(a[0]), BTYPE[mn])
elif mn == "jal":
tgt = labels[a[1]] if a[1] in labels else num(a[1])
w = enc_j(tgt - addr, reg(a[0]))
elif mn == "jalr":
w = enc_i(num(a[2]), reg(a[1]), 0, reg(a[0]), 0x67)
elif mn == "ecall":
w = 0x00000073
elif mn == "print":
# 写 MMIO 的 LED 寄存器:lui t6, LED_ADDR>>12 ; sw a0, 0(t6)
w = [
enc_u(LED_ADDR >> 12, reg("t6")),
enc_s(0, reg("a0"), reg("t6"), 2),
]
elif mn == "halt":
w = [enc_i(10, 0, 0, 17), 0x00000073]
else:
raise SystemExit("未知助记符: " + mn)
out.append((addr, line, w if isinstance(w, list) else [w]))
return out
def main():
src, dst = sys.argv[1], sys.argv[2]
with open(src, encoding="utf-8-sig") as f: # 兼容带/不带 BOM
prog = assemble(f.readlines())
with open(dst, "w", encoding="ascii") as f:
for _, _, ws in prog:
for w in ws:
f.write("%08X\n" % w)
with open(dst + ".lst", "w", encoding="utf-8") as f:
for addr, line, ws in prog:
f.write("%04X: %s %s\n" % (addr, " ".join("%08X" % w for w in ws), line))
print("已生成 %s(%d 个字)" % (dst, sum(len(ws) for _, _, ws in prog)))
main()
-206
View File
@@ -1,206 +0,0 @@
# 扫雷(RISC-V / cpu21)— 纯 C + MMIO 显示 + 中断读鼠标
给 `pipelined-cpu` 里那颗 CPU(`cpu21_riscv_redirect_int_bpb`)写的扫雷。
**游戏逻辑和三个中断入口都由纯 C 写成**,用 RISC-V 工具链编译成机器码后固化进片内 ROM 运行。
```
saolei/
├─ sw/
│ ├─ mmio.h 全部 MMIO 地址与格子编码(换外设地址只改这一个文件)
│ ├─ saolei.c 纯 C 扫雷:背景界面(main) + 关卡(level_main) + 棋盘与绘制
│ ├─ isr.c **中断读鼠标的代码**:三个固定地址入口 + 纯 C 服务主体 isr_body
│ ├─ start.c C 启动(设 sp、清 .bss、进 main)——只有这一处必需的内联汇编
│ └─ linker.ld ROM/RAM 布局:复位 0x0、中断入口 0x100/0x200/0x300、栈顶 0x0FFC
└─ tools/
├─ build.ps1 一键构建:C → .o → ELF → .hex + cpu21_saolei_rom.vh
├─ check_isa.py 逐条反汇编检查:有没有用到这颗核没实现的指令
└─ elf2hex.py 裸二进制 → $readmemh hex + ROM 初始化头文件
```
配套硬件在 Vivado 工程里(不在本目录):`srcs/rtl/cpu21_mmio.v`(外设中枢)、
`cpu21_vga.v`(显示)、`cpu21_seg7.v`(数码管)、`cpu21_pointer.v`(按键当鼠标)、
`cpu21_ps2_mouse.v` / `cpu21_ps2_kbd.v`(可选真鼠标/真键盘)、
`srcs/board/cpu21_board_top.v`(上板顶层)。
---
## 1. 程序结构:背景"伪操作系统" + 关卡 + 鼠标,正好占三个中断
CPU 复位后从 `0x0` 开始跑 `main()`(**背景程序**,从头到尾一直运行),它画开始界面
并等鼠标。整套交互是这样串起来的:
```mermaid
sequenceDiagram
participant BG as 背景 main()(0x0 起,一直跑)
participant LV as 关卡 level_main()(IRQ1 = 0x100)
participant MS as 鼠标 ISR(IRQ2 = 0x200)
BG->>BG: 画开始界面(标题 + 绿色"开始"按钮 + 光标框)
BG->>BG: 点中"开始"按钮 → 写 LEVEL 寄存器 = 1
Note over BG: 硬件拉起 irq_i[1],CPU 跳到 0x100
BG->>LV: isr1_entry:保存现场 + 打开 MIE → level_main()
LV->>LV: 初始化关卡(布雷、画盘面)
MS-->>LV: 鼠标事件中断(IRQ2 优先级更高,嵌套进来)
LV->>LV: 轮询事件队列:移动光标 / 揭开 / 插旗
LV->>BG: 点左上角黄格 = 返回 → 清 LEVEL + uret
Note over BG: 回到被打断的那条指令,继续等下一次"开始"
```
| 入口 | 地址 | 作用 | 对应 RTL 参数 |
| ---- | ------- | ----------------------------------- | ------------- |
| 复位 | `0x000` | `_start`:设栈、进 `main` | `RESET_PC` |
| IRQ1 | `0x100` | **关卡(游戏)程序** `level_main()` | `IRQ1_VECTOR` |
| IRQ2 | `0x200` | **鼠标读取** `isr_body()` | `IRQ2_VECTOR` |
| IRQ3 | `0x300` | 备用(只 `uret`) | `IRQ3_VECTOR` |
**地址是链接脚本定死的**(`linker.ld` 里 `.text.isr1 0x100 : {...}`),所以先定地址、
再设 RTL 参数,两边成对;不存在"编完才知道地址"的问题。优先级 IRQ3 > IRQ2 > IRQ1,
所以"关卡跑在 IRQ1、鼠标跑在 IRQ2"天然允许鼠标嵌套打断关卡。
中断硬件行为(见 `cpu21_riscv_irq_ctrl.v`):`irq_i` 上升沿置挂起位;响应时硬件自动清
挂起位、清 `ustatus[0]`(MIE)、把返回地址压进内部 EPC 栈;返回用 **`uret`**;**硬件不保存
任何通用寄存器**。因此:
- `isr1_entry`(游戏体量大会调函数)保存**全部调用者保存寄存器**,并用
`csrrsi x0,0x004,1` 打开 MIE 让鼠标中断能嵌套;
- `isr2_entry` 只存 `ra`,C 主体 `isr_body` 用 `-ffixed-x1,x5..x7,x10..x17,x28..x31`
编译,逼 GCC 只用 `s0-s11`(callee-saved,函数自己会存/恢复)和 `sp` —— 这样纯 C
主体不会破坏被打断的代码;
- 事件流:鼠标/按键 → RTL 事件 FIFO(深 16)→ 中断 → `isr_body` 搬进软件环形队列 →
主循环 `handle_event()` 处理。FIFO 读空后中断线自动回 0,下一个事件重新产生上升沿,
所以不丢事件;主循环只能轮询(这颗核没有 `wfi`)。
## 2. 针对这颗 CPU 的硬约束(改 C 代码时别踩)
`cpu21_riscv_decoder.v` 实现的是 RV32I 的一个子集:
| 类别 | 有 | **没有** |
| ------ | -------------------------------------------- | ---------------------------------- |
| R 型 | add sub sll slt sltu xor srl sra or and | — |
| M 扩展 | mul mulh mulhsu mulhu **divu remu** | **div / rem(有符号)** |
| I 型 | addi slti sltiu xori ori andi slli srli srai | — |
| 访存 | **lw / sw / sb** | **lb / lh / lbu / lhu / sh** |
| 高位 | lui | **auipc** |
| 控制流 | 6 条分支 + jal + jalr | — |
| 系统 | ecall uret csrrw csrrsi csrrci | mret / sret / fence / ebreak / wfi |
对应做法:不按字节*读*内存(**写**显存用 `sb` 可以)、不做有符号除法、索引运算全用
2 的幂 + 移位/掩码、**不能有 `.data`/`.rodata`**(数据空间读不到 ROM,所以没有带初值的
全局变量、没有字符串/常量表);字模放在 RTL 里,不在软件里。
## 3. 构建(已实测通过)
工具链:xPack `riscv-none-elf-gcc`(本机装在
`C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin`,已加入用户 PATH)。
```powershell
cd <仓库>\saolei
powershell -ExecutionPolicy Bypass -File tools\build.ps1
# 若没配 PATH:-Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
```
| 步骤 | 说明 |
| ------------ | -------------------------------------------------------------------------------------------- |
| 编译 | `start.c`/`saolei.c` 正常编;`isr.c` 额外加 `-ffixed-x...` |
| 链接 | `-T sw/linker.ld`;脚本里的 `ASSERT` 会拦住 `.data`/`.rodata` |
| **反汇编** | `build/saolei.lst` ← 这就是"编译出来的汇编",可以直接读 |
| **ISA 检查** | `tools/check_isa.py` 逐条比对指令白名单,出现 `auipc`/`lb`/`lbu`/`div`… 直接报错并给修复提示 |
| 转格式 | `build/saolei.hex`(`$readmemh`)+ `pipelined-cpu/srcs/board/cpu21_saolei_rom.vh` |
当前实测结果:`text=3368 B, data=0, bss=1436 B`,**842 条指令全部通过 ISA 检查**,
入口地址 `_start=0x0`、`isr1_entry=0x100`、`isr2_entry=0x200`、`isr3_entry=0x300`。
编译参数里有三处是这颗核/工具链的坑,已经写在脚本注释里:
1. `-march=rv32im_zicsr` —— GCC 15 起 `rv32im` 不含 Zicsr,`csrrsi` 会报
`unrecognized opcode ... extension 'zicsr' required`;
2. **不要禁链接器松弛**(别加 `-mno-relax`/`-Wl,--no-relax`)—— gcc 的函数调用默认是
`auipc ra,%pcrel_hi` + `jalr`,要靠链接器松弛压成一条 `jal`;禁掉后 35 条 `auipc`
会留在代码里(这颗核没有)。用 `-msmall-data-limit=0` 保证松弛不会引入 gp 相对寻址;
3. 中断入口地址必须写在**段声明**上(`.text.isr1 0x100 : {...} > ROM`)——
只写 `. = 0x100;` 会被 `ld` 忽略(实测 isr1 掉到了 0x14)。
## 4. MMIO 地址表(与 `cpu21_mmio.v` 的译码一致)
| 地址 | 外设 |
| -------------------- | ------------------------------------------------------------------------------- |
| `0x0000_1000` | LED 数据寄存器(低 16 位点亮板载 LED) |
| `0x0000_1004` | 七段数码管:8 位十六进制数(低 4 位 = 最右) |
| `0x0000_1100+` | 鼠标:STATUS / DATA / ACK / CTRL / INPUT / TIME / FRAME / EVCNT / LEVEL / MAGIC |
| `0x0000_1140+` | 显示控制器:CTRL(光标可见、背景色)/ CURSOR(列、行)/ MAGIC |
| `0x0000_2000-0x2FFF` | 显存:40 列 × 30 行、每格 2 字节(共 2400 字节) |
`LEVEL` 是**软件中断**寄存器:写 1 拉起 `irq_i[1]`(关卡入口),关卡返回前写 0 清掉,
否则下次点"开始"没有新的上升沿。`MAGIC`(`0xCAFE0001/2`)可以读来自检 MMIO 是否活着。
格子编码为两个字节:第一个字节低 6 位是字形编号,第二个字节低 3 位是颜色;字形 0 空白、
1..8 数字、9 未揭开、10 旗、11 雷、12 爆炸、13 问号、14 横线、15 实心、16..41 为 A-Z;
颜色是 3 位调色板。
## 5. 怎么跑起来
**仿真**(用你现成的测试平台):
```powershell
copy build\saolei.hex <仓库>\pipelined-cpu\srcs\rom\saolei.hex
# 把 srcs/tb/tb_no_intr.v 顶部的 ROM_FILE 改成 "saolei.hex"
```
**上板**:在 Vivado 里给综合/仿真**定义宏 `SAOLEI_ROM`**(Settings → Verilog Macros),
`cpu21_board_top.v` 就会加载 `cpu21_saolei_rom.vh`;不定义仍是原来的跑马灯程序。
接 VGA 显示器,按 `CPU_RESETN` 复位 → 出现开始界面(标题 + 绿色按钮 + 闪动光标框)。
**操作**(板载 5 向按键一直有效):
| 操作 | 板载按键 | PS/2 设备(SW15 选) |
| ------------------------------------ | ------------------------------------- | ------------------------------------- |
| 移动光标 | `BTNU` / `BTND` / `BTNL` / `BTNR` | 键盘 `↑↓←→`(按住 = 连续)/ 鼠标移动 |
| 左键(揭开;开始界面 = 选难度/开始) | `BTNC` | 键盘 `Enter`/空格 / 鼠标左键 |
| 右键(插旗 / 取消旗) | `SW3`(或 `SW0`)拨上 + `BTNC` | 键盘左 `Shift`/`F`/`C` / 鼠标右键 |
| 返回上一层(关卡内) | 光标移到盘面**左上角黄格**再按左键 | 同上 |
**PS/2 键鼠**(板载 USB HOST 口,也可把 Pmod PS/2 插在 JA):顶层参数
`USE_PS2_MOUSE` 默认为 1,鼠标解码器和键盘解码器都挂在同一根 PS/2 线上,**用 SW15
选哪个生效**(0 = 键盘,1 = 鼠标)。键盘解的是扫描码集 2(`E0` 扩展前缀、`F0` 松开
前缀,只对按下产生事件),映射成与鼠标**完全一样**的事件格式,所以 C 代码一行不用改。
接收器只做**接收**(不实现主机发 `0xF4` 使能上报):多数键鼠上电即上报。上板时用
LED 诊断看“当前设备收到的字节数”(见根目录 README 的“PS/2 键鼠”一节)。
USB 鼠标在这个核上不现实(协议栈放不下)。
**踩过的坑(值得写进报告)**:`cpu21_ps2_mouse.v` 早期把 11 位帧**整体右移**、又在
停止位还没移进来时就判 `frame_q[10]`,并且把 PS/2 的**奇**校验当成**偶**校验判,结果
真设备发来的帧**全部被丢掉** —— 上板表现就是“插上鼠标毫无反应、收到的字节数恒为 0”,
很容易误判成硬件/跳线问题。现在帧按**位的下标**存(`frame_q[i]` = 第 i 位)、停止位
单独判(它就在当前拍的 `ps2_data` 上)、校验按奇校验判,并且
`srcs/tb/tb_ps2_mouse.v` / `tb_ps2_kbd.v` 两个自测里都带“故意发错校验 → 必须丢帧”
的用例。
## 6. 验证状态(说实话)
| 部分 | 状态 |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| C 工具链链路 | 已实测通过:`text=4608 B, data=0, bss=2508 B`,**1152 条指令全部通过 ISA 检查** |
| 游戏逻辑(布雷、`count_adj`、三档难度、`draw_tile`) | PC 侧独立测试通过(3 难度 × 40 局重算比对 + 端到端显示测试) |
| 仿真显示(`tb_saolei` / `tb_saolei_play` + `tools/fb2png.py`) | 通过:整屏 dump 能还原成 PNG,hard 盘面(30×16)整屏都能画出来 |
| `cpu21_mmio.v` / `cpu21_vga.v` / `cpu21_seg7.v` / `cpu21_pointer.v` / 板级顶层 / XDC | `xvlog` 编译通过;已综合、已上板 |
| 上板实测 | VGA 画面、开始界面、三档难度、开格/插旗、板载按键均已实测可用 |
| PS/2 键鼠 | 接收器逻辑有自测(`tb_ps2_mouse` / `tb_ps2_kbd`);板载 USB HOST 口待复测 |
建议验证顺序:① 跑 `pipelined-cpu/tools/run_regress.ps1` 确认跑马灯/中断/指令测试仍全
PASS;② 定义 `SAOLEI_ROM` 跑行为仿真(`tb_saolei` 会自动打完一局并 dump 显存);
③ 上板看 VGA;④ 接键鼠时先用 `srcs/tb/tb_ps2_*` 确认解码无误,再看 LED 诊断确认板载
桥接芯片到底有没有转发你插的那个设备。
## 7. 已知可改进项
- **难度**:已按微软扫雷做成三档 —— `EASY` 9×9/10 雷、`NORMAL` 16×16/40 雷、
`HARD` 30×16/99 雷。盘面尺寸是**运行期变量**(`g_bw/g_bh/g_mines/g_ncells`),
开始界面上点那一行就即时切换、重新画盘面;所有索引走 `IDX_R/IDX_C` 宏。
再想更大就要动 `bfs_q` 的打包方式(数据 RAM 只有 4 KB)。
- **输入**:板载 5 向按键、PS/2 鼠标、PS/2 键盘三种事件源在顶层合并成同一种格式
(±8 = 一格),键鼠用 SW15 切换(见上文“操作”)。
- **文字显示**:显存每格 2 字节(字形[5:0] + 颜色[2:0]),RTL 内置 A-Z 和数字/游戏
符号字模,开始界面显示 `MINES`、`EASY`、`NORMAL`、`HARD`、`START`。注意**字符颜色
不能取黑色**(黑字画在黑背景上等于看不见)。
- 回归覆盖:`run_regress.ps1` 里还没有扫雷的自动测试项(PS/2 接收器有单独的
`tb_ps2_mouse` / `tb_ps2_kbd`);也还没有“自动玩一局 + 显存 dump + PNG 比对”的
一键回归。
-129
View File
@@ -1,129 +0,0 @@
/* -----------------------------------------------------------------------------
* isr.c —— 中断服务程序:从 MMIO 读鼠标事件(**这就是"中断读鼠标操作的指令"**)
*
* 这颗核的中断硬件行为(见 srcs/rtl/cpu21_riscv_irq_ctrl.v):
* * irq_i 上升沿 -> 挂起位置 1;响应中断时硬件自动清挂起位、清 ustatus[0](MIE)、
* 把返回地址压进内部 EPC 栈;
* * 从 uepc 返回用 `uret`(编码 0x00200073,不在标准 C 里,只能内联汇编);
* * **硬件不保存任何通用寄存器**,所以软件必须自己保证不破坏被打断的代码。
*
* 因此这里分了三个固定地址的入口 + 一个纯 C 主体:
* 1) isr1_entry(.text.isr1 = 0x100):**关卡(游戏)入口**,调 level_main()。
* 游戏是一整段会调用别的函数的 C 程序,所以入口把**所有调用者保存寄存器**
* (ra、t0-t6、a0-a7)都存起来,并用 csrrsi 打开 MIE 让鼠标中断能嵌套;
* 2) isr2_entry(.text.isr2 = 0x200):**鼠标读取**,只存 ra 后调下面的 isr_body;
* 3) isr3_entry(.text.isr3 = 0x300):备用,直接 uret;
* 4) isr_body:**纯 C**,本文件用 -ffixed-x1,x5..x7,x10..x17,x28..x31 编译,
* 逼 GCC 只用 s0-s11(callee-saved,函数自己会存/恢复)和 sp,于是被打断的
* 代码的调用者保存寄存器绝对不会被破坏 —— 这样主体就是安全的纯 C 叶子函数。
*
* 主体要做的事:把硬件事件 FIFO(深度 16)里的项全部搬到软件环形队列,
* 主循环再从队列里取。FIFO 读空后中断线自动回 0,下一个事件会重新产生上升沿,
* 所以不会丢事件。
* ---------------------------------------------------------------------------*/
#include "mmio.h"
/* ---- 与 saolei.c 共用的软件事件队列(定义在这里,声明在 mmio.h) ---- */
volatile unsigned g_evq[EVQ_SIZE];
volatile unsigned g_evq_head; /* 主循环消费 */
volatile unsigned g_evq_tail; /* 中断生产 */
volatile unsigned g_evq_lost; /* 队列满丢弃计数(调试) */
/* 中断主体:纯 C 叶子函数(不调用任何函数),只使用 s 寄存器和 sp。
* 编译本文件时必须加 -ffixed-x1 -ffixed-x5..x7 -ffixed-x10..x17 -ffixed-x28..x31
* (见 tools/build.ps1),否则它会用到调用者保存寄存器从而破坏被打断的代码。 */
__attribute__((used, noinline)) void isr_body(void);
void isr_body(void)
{
unsigned ev, next;
/* 硬件 FIFO 非空就一直搬(STATUS[0] = 非空) */
while ((REG_R(REG_STATUS) & 1u) != 0u)
{
ev = REG_R(REG_MDATA); /* lw:{btn[20:16], dy[15:8], dx[7:0]} */
next = (g_evq_tail + 1u) & EVQ_MASK;
if (next != g_evq_head)
{
g_evq[g_evq_tail] = ev;
g_evq_tail = next;
}
else
{
g_evq_lost = g_evq_lost + 1u; /* 软件队列满:丢弃并计数 */
}
REG_W(REG_MACK) = 1u; /* sw:弹出硬件 FIFO 一项 */
}
}
/* ---------------------------------------------------------------------------
* 三个中断入口,分别在链接脚本固定住的 0x100 / 0x200 / 0x300。
*
* IRQ1 = 0x100:**关卡(游戏)入口**。游戏是一整段会调用别的函数的 C 程序,
* 所以入口必须把**所有调用者保存寄存器**(ra、t0-t6、a0-a7)都存起来,
* 否则返回时会把被打断的背景程序弄坏。进入中断时硬件已把 MIE 清 0,这里再
* 重新打开,鼠标中断(IRQ2 优先级更高)才能嵌套进来;uret 时硬件会从内部
* 栈里恢复原来的 MIE。
* IRQ2 = 0x200:**鼠标读取**。C 主体用 -ffixed 编译(只用 s 寄存器和 sp),
* 所以入口只要保存 ra。
* IRQ3 = 0x300:备用(保留给拨码开关等外部源)。
* ------------------------------------------------------------------------- */
extern void level_main(void);
__attribute__((naked, section(".text.isr1"), used, noinline)) void isr1_entry(void)
{
__asm__ volatile(
"addi sp, sp, -64\n\t"
"sw ra, 0(sp)\n\t"
"sw t0, 4(sp)\n\t"
"sw t1, 8(sp)\n\t"
"sw t2, 12(sp)\n\t"
"sw t3, 16(sp)\n\t"
"sw t4, 20(sp)\n\t"
"sw t5, 24(sp)\n\t"
"sw t6, 28(sp)\n\t"
"sw a0, 32(sp)\n\t"
"sw a1, 36(sp)\n\t"
"sw a2, 40(sp)\n\t"
"sw a3, 44(sp)\n\t"
"sw a4, 48(sp)\n\t"
"sw a5, 52(sp)\n\t"
"sw a6, 56(sp)\n\t"
"sw a7, 60(sp)\n\t"
"csrrsi x0, 0x004, 1\n\t" /* ustatus[0]=MIE:允许鼠标中断嵌套 */
"jal ra, level_main\n\t"
"lw a7, 60(sp)\n\t"
"lw a6, 56(sp)\n\t"
"lw a5, 52(sp)\n\t"
"lw a4, 48(sp)\n\t"
"lw a3, 44(sp)\n\t"
"lw a2, 40(sp)\n\t"
"lw a1, 36(sp)\n\t"
"lw a0, 32(sp)\n\t"
"lw t6, 28(sp)\n\t"
"lw t5, 24(sp)\n\t"
"lw t4, 20(sp)\n\t"
"lw t3, 16(sp)\n\t"
"lw t2, 12(sp)\n\t"
"lw t1, 8(sp)\n\t"
"lw t0, 4(sp)\n\t"
"lw ra, 0(sp)\n\t"
"addi sp, sp, 64\n\t"
"uret\n\t");
}
__attribute__((naked, section(".text.isr2"), used, noinline)) void isr2_entry(void)
{
__asm__ volatile(
"addi sp, sp, -8\n\t" /* 保存被打断代码的 ra(jal 会覆盖它) */
"sw ra, 0(sp)\n\t"
"jal ra, isr_body\n\t" /* 调纯 C 主体(它自己保存 s 寄存器) */
"lw ra, 0(sp)\n\t"
"addi sp, sp, 8\n\t"
"uret\n\t"); /* 从 uepc 返回:这颗核的中断返回指令 */
}
__attribute__((naked, section(".text.isr3"), used, noinline)) void isr3_entry(void)
{
__asm__ volatile("uret\n\t");
}
-99
View File
@@ -1,99 +0,0 @@
/* -----------------------------------------------------------------------------
* linker.ld —— 扫雷程序的内存布局
*
* 这颗 CPU 是"哈佛"结构:指令从 ROM 取(instr_addr_o),数据访问 RAM
* (data_addr_o),两个地址空间都从 0 开始,所以这里用两个 MEMORY 区域,
* 都在 0 处开头。
*
* ROM 0x0000_0000 32 KB(= cpu21_board_top 的 ROM_WORDS = 8192 字)
* +0x0000 .text.init 复位入口 _start
* +0x0100 .text.isr1 中断入口 isr1_entry(关卡/游戏,**必须等于 IRQ1_VECTOR**)
* +0x0200 .text.isr2 中断入口 isr2_entry(鼠标读取,必须等于 IRQ2_VECTOR)
* +0x0300 .text.isr3 中断入口 isr3_entry(备用,必须等于 IRQ3_VECTOR)
* +0x0400 .text 其余代码(紧跟 isr3 之后)
*
* RAM 0x0000_0000 4 KB(= saolei_board_top 的 RAM_WORDS = 1024 字)
* +0x0000 .bss 全局变量(全部零初始化,不需要 ROM 里的初值)
* +0x0FFC __stack_top 栈顶(向下增长)
*
* 两个断言很关键(都是这颗 CPU 的硬限制):
* 1) 不能有 .data —— 数据空间读不到 ROM,没有地方放"带初值的变量",
* 所有全局变量必须零初始化;
* 2) 不能有 .rodata —— 同理,程序里不能出现字符串/常量表
* (字模在 RTL 里,不在软件里)。
* ---------------------------------------------------------------------------*/
ENTRY(_start)
MEMORY
{
ROM (rx) : ORIGIN = 0x00000000, LENGTH = 32K
RAM (rwx) : ORIGIN = 0x00000000, LENGTH = 4K
}
SECTIONS
{
/* ------------------------- 数据段(RAM) ------------------------- */
. = 0x00000000;
.bss (NOLOAD) :
{
__bss_start = .;
*(.sbss)
*(.sbss.*)
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
__bss_end = .;
} > RAM
/* 有初值的变量:不允许(见文件头的说明) */
.data : { *(.data) *(.data.*) *(.sdata) *(.sdata.*) } > RAM
/* 只读数据:不允许(没有 auipc / 数据空间读不到 ROM) */
.rodata : { *(.rodata) *(.rodata.*) } > RAM
ASSERT(SIZEOF(.data) == 0, "错误:程序里出现了带初值的全局变量(.data),这颗核没有地方存放它")
ASSERT(SIZEOF(.rodata) == 0, "错误:程序里出现了字符串或常量表(.rodata),这颗核读不到 ROM 里的数据")
/* ------------------------- 代码段(ROM) -------------------------
* 三个中断入口的地址**必须写在段声明里**(.text.isr1 0x100 : ...):
* 只写 ". = 0x100;" 这种位置计数器赋值,在段带 "> ROM" 时会被 ld 忽略,
* 它会按区域顺序把段紧挨着排下去(实测 isr1 掉到了 0x14)。
* 这三个地址要和顶层 cpu21_board_top 的 IRQ1/2/3_VECTOR 参数一致。 */
.text.init 0x00000000 :
{
KEEP(*(.text.init))
} > ROM
.text.isr1 0x00000100 : /* IRQ1:关卡(游戏)入口 */
{
KEEP(*(.text.isr1))
} > ROM
.text.isr2 0x00000200 : /* IRQ2:鼠标读取 */
{
KEEP(*(.text.isr2))
} > ROM
.text.isr3 0x00000300 : /* IRQ3:备用 */
{
KEEP(*(.text.isr3))
} > ROM
.text :
{
*(.text)
*(.text.*)
. = ALIGN(4);
} > ROM
/* ------------------------- 符号 ------------------------- */
__stack_top = ORIGIN(RAM) + LENGTH(RAM) - 4; /* 0x0FFC */
__isr_vector = 0x00000100;
/* .bss 不能长到撞到栈 */
ASSERT(__bss_end < (__stack_top - 0x200),
"错误:.bss 太大,会覆盖栈;请缩小棋盘或加大 RAM_WORDS")
}
-136
View File
@@ -1,136 +0,0 @@
/* -----------------------------------------------------------------------------
* mmio.h —— 扫雷程序的全部 MMIO 地址与编码定义(**只改这一个文件就能换地址**)
*
* 约定:本文件里的所有地址都是"数据空间"的字节地址,和 CPU 的 data_addr_o 一致。
* 写用 *(volatile unsigned *)addr(sw 全字写)或 *(volatile unsigned char *)addr
* (sb 字节写,显存必须用这个)。
*
* -----------------------------------------------------------------------------
* 地址表(与 pipelined-cpu/srcs/rtl/cpu21_mmio.v 里的译码完全一致):
*
* 0x0000_0000 - 0x0000_0FFF 数据 RAM(4 KB,栈 + 全局变量)
* 0x0000_1000 LED 数据寄存器(低 16 位点亮 LED)
* 0x0000_1004 七段数码管:8 位十六进制(低 4 位 = 最右)
* 0x0000_1100 + 0x00 STATUS [0] 事件FIFO非空 [5:1] 项数 [6] 中断使能 [7] 满
* 0x0000_1100 + 0x04 DATA [7:0] dx [15:8] dy [20:16] 按键(读无副作用)
* 0x0000_1100 + 0x08 ACK 写任意值 -> 弹出一次事件
* 0x0000_1100 + 0x0C CTRL [0] 鼠标中断使能
* 0x0000_1100 + 0x10 INPUT [15:0] 拨码开关 [20:16] 按键
* 0x0000_1100 + 0x14 TIME 自由计数器(随机种子)
* 0x0000_1100 + 0x18 FRAME VGA 帧计数(闪烁用)
* 0x0000_1100 + 0x1C EVENTCNT 累计事件数
* 0x0000_1100 + 0x20 LEVEL 写 1 -> 触发"关卡入口"软件中断(IRQ2)
* 0x0000_1100 + 0x24 MAGIC 0xCAFE0002(自检:读得到说明 MMIO 活着)
* 0x0000_1140 + 0x00 DISP_CTRL [0] 光标可见 [6:4] 背景色
* 0x0000_1140 + 0x04 DISP_CURSOR [5:0] 列 [13:8] 行
* 0x0000_1140 + 0x08 DISP_MAGIC 0xCAFE0001
* 0x0000_2000 - 0x0000_2FFF 显存:40 列 x 30 行,每格 2 字节,共 2400 字节
*
* 鼠标事件单位:1/8 格(按键一次 = 8 = 移动一格;PS/2 鼠标用原始计数)。
* 左键点击 -> 事件的 btn 位 0 置 1;右键(PS/2)-> 位 1。
*
* ⚠ 如果你的外设地址和上表不同:只改下面这几个宏,.c 文件不用动。
* ⚠ 三个中断入口必须与 linker.ld 里 .text.isr1/2/3 的地址一致:
* IRQ1(0x100) = 关卡(游戏)入口,IRQ2(0x200) = 鼠标读取,IRQ3(0x300) = 备用。
* 顶层 cpu21_board_top 的 IRQ1/2/3_VECTOR 参数也要跟着改(默认就是这个布局)。
* ---------------------------------------------------------------------------*/
#ifndef SAOLEI_MMIO_H
#define SAOLEI_MMIO_H
/* ---- LED / 七段数码管 ---- */
#define LED_ADDR 0x00001000u
#define REG_SEG 0x00001004u
/* ---- 显存 ---- */
#define FB_BASE 0x00002000u /* 显存起始(字节地址) */
#define FB_PITCH 40u /* 每行 40 格 */
#define FB_ROWS 30u /* 30 行 */
#define GRID_COLS 40u
#define GRID_ROWS 30u
/* ---- 鼠标寄存器组 ---- */
#define REG_STATUS 0x00001100u
#define REG_MDATA 0x00001104u
#define REG_MACK 0x00001108u
#define REG_MCTRL 0x0000110Cu
#define REG_INPUT 0x00001110u
#define REG_TIME 0x00001114u
#define REG_FRAME 0x00001118u
#define REG_EVCNT 0x0000111Cu
#define REG_LEVEL 0x00001120u
#define REG_MMAGIC 0x00001124u
/* ---- 显示控制器寄存器 ---- */
#define REG_CTRL 0x00001140u
#define REG_CURSOR 0x00001144u
#define REG_DMAGIC 0x00001148u
/* ---- 访问宏 ---- */
#define REG_R(a) (*(volatile unsigned *)((unsigned)(a)))
#define REG_W(a) (*(volatile unsigned *)((unsigned)(a)))
/* 显存按字节写(只能 sb,不能 lb —— 这颗核没有 lb/lbu)。每个格子占两个字节。 */
static inline void fb_write_cell(unsigned idx, unsigned glyph, unsigned color)
{
unsigned addr = FB_BASE + (idx << 1);
*(volatile unsigned char *)addr = (unsigned char)(glyph & 0x3Fu);
*(volatile unsigned char *)(addr + 1u) = (unsigned char)(color & 0x07u);
}
/* ---- 格子编码:两个字节 = 字形[5:0] + 颜色[2:0] ---- */
#define G_BLANK 0u /* 空 */
#define G_N1 1u /* 数字 1..8 = 1..8 */
#define G_COVER 9u /* 未揭开 */
#define G_FLAG 10u /* 旗 */
#define G_MINE 11u /* 雷 */
#define G_BOOM 12u /* 爆炸 */
#define G_QUEST 13u /* 问号 */
#define G_BAR 14u /* 横线 */
#define G_SOLID 15u /* 实心 */
/* 大写字母字形编号 16..41,供菜单文字使用。 */
#define G_A 16u
#define G_B 17u
#define G_C 18u
#define G_D 19u
#define G_E 20u
#define G_F 21u
#define G_G 22u
#define G_H 23u
#define G_I 24u
#define G_J 25u
#define G_K 26u
#define G_L 27u
#define G_M 28u
#define G_N 29u
#define G_O 30u
#define G_P 31u
#define G_Q 32u
#define G_R 33u
#define G_S 34u
#define G_T 35u
#define G_U 36u
#define G_V 37u
#define G_W 38u
#define G_X 39u
#define G_Y 40u
#define G_Z 41u
#define C_BLACK 0u
#define C_BLUE 1u
#define C_GREEN 2u
#define C_CYAN 3u
#define C_RED 4u
#define C_MAGENTA 5u
#define C_YELLOW 6u
#define C_GRAY 7u
/* ---- 软件事件环形队列:中断服务程序(isr.c)生产,主循环(saolei.c)消费 ---- */
#define EVQ_SIZE 64u
#define EVQ_MASK (EVQ_SIZE - 1u)
extern volatile unsigned g_evq[EVQ_SIZE];
extern volatile unsigned g_evq_head; /* 主循环消费 */
extern volatile unsigned g_evq_tail; /* 中断生产 */
extern volatile unsigned g_evq_lost; /* 队列满丢弃计数(调试用) */
#endif /* SAOLEI_MMIO_H */
-742
View File
@@ -1,742 +0,0 @@
/* -----------------------------------------------------------------------------
* saolei.c —— 纯 C 扫雷(RV32IM 子集:只用这颗核实现了的指令)
*
* 显示:把每个格子写成显存里的 2 个字节(MMIO 0x2000 起)
* 输入:鼠标事件(中断驱动)——中断里搬到软件队列,主循环在下面处理
*
* 针对这颗 CPU 的硬约束(很重要,改代码时别踩):
* 1) 存储只有 lw / sw / sb,**没有 lb/lh/lbu/lhu** ——
* 所以只按字节"写"显存,从不按字节"读";所有状态都用 32 位字保存。
* 2) **没有 auipc** —— 编译时必须 -fno-pic -mcmodel=medlow,并且不能出现
* 只读数据(.rodata),所以本文件没有任何字符串/常量表(字模在 RTL 里)。
* 3) 乘除只有 mul/mulh/mulhsu/mulhu 和**无符号**的 divu/remu ——
* 所以本文件所有索引运算都用"2 的幂 + 移位/掩码",一次除法都不用。
* 4) 没有 fence/ebreak 等;中断返回是 uret(在 isr.c 里)。
*
* 棋盘:16 x 16,雷数可选 20/40/60(16 是 2 的幂 => 索引全用移位)
* 网格:40 列 x 30 行,棋盘居中放在 (行 7..22, 列 12..27)
* ---------------------------------------------------------------------------*/
#include "mmio.h"
/* 三档难度完全照微软扫雷:初级 9x9/10 雷、中级 16x16/40 雷、高级 30x16/99 雷。
* 棋盘尺寸是运行期变量(三档各不相同),所以位图与队列都按最大尺寸分配。 */
#define BW_MAX 30u /* 最大棋盘宽(高级) */
#define BH_MAX 16u /* 最大棋盘高 */
#define NCELLS_MAX (BW_MAX * BH_MAX) /* 480 */
#define BM_WORDS_MAX ((NCELLS_MAX + 31u) / 32u) /* 15 */
/* 当前难度下的棋盘参数(由 set_difficulty() 设定) */
static unsigned g_bw, g_bh; /* 棋盘宽、高(格) */
static unsigned g_ncells; /* g_bw * g_bh */
static unsigned g_bm_words; /* 位图字数 */
static unsigned g_board_r0; /* 棋盘左上角在网格里的行(居中) */
static unsigned g_board_c0; /* 棋盘左上角在网格里的列(居中) */
/* 格索引 <-> (行, 列)。这颗核有无符号除法 divu/remu,直接用;虽然多周期,
* 但一局游戏这点开销无所谓。 */
#define IDX_R(i) ((i) / g_bw)
#define IDX_C(i) ((i) % g_bw)
/* 游戏状态 */
#define ST_PLAY 0u
#define ST_WIN 1u
#define ST_LOSE 2u
/* ---- 棋盘状态:3 个位图,每个 8 个字(256 位) ---- */
static unsigned mine_bm[BM_WORDS_MAX];
static unsigned open_bm[BM_WORDS_MAX];
static unsigned flag_bm[BM_WORDS_MAX];
static unsigned seen_bm[BM_WORDS_MAX]; /* BFS:已经入过队的格子,避免重复入队把队列撑满 */
/* 其它全局状态(全部进 .bss,靠 start.c 清零) */
static unsigned opened_cnt;
static unsigned flags_cnt;
static unsigned g_state;
static unsigned g_first_done;
static unsigned g_mines;
static unsigned g_difficulty;
static unsigned rnd_state;
static unsigned cur_r, cur_c; /* 光标所在的网格格 */
static int acc_x, acc_y; /* 指针累积量(1/8 格,有符号) */
static unsigned bfs_q[NCELLS_MAX]; /* 揭开用的队列(按最大棋盘分配) */
/* ============================ 小工具 ============================ */
/* 位图取位/置位/清位:i = r*16 + c,全部用移位和掩码 */
static unsigned bget(const unsigned *bm, unsigned i)
{
return (bm[i >> 5] >> (i & 31u)) & 1u;
}
static void bset(unsigned *bm, unsigned i)
{
bm[i >> 5] |= (1u << (i & 31u));
}
static void bclr(unsigned *bm, unsigned i)
{
bm[i >> 5] &= ~(1u << (i & 31u));
}
static void draw_cell(unsigned gr, unsigned gc, unsigned glyph, unsigned color)
{
fb_write_cell(gr * GRID_COLS + gc, glyph, color);
}
/* 把棋盘格 (r,c) 画出来 */
static void draw_tile(unsigned r, unsigned c, unsigned glyph, unsigned color)
{
draw_cell(g_board_r0 + r, g_board_c0 + c, glyph, color);
}
static unsigned num_color(unsigned n)
{
if (n == 1u)
return C_BLUE;
if (n == 2u)
return C_GREEN;
if (n == 3u)
return C_RED;
if (n == 4u)
return C_MAGENTA;
if (n == 5u)
return C_YELLOW;
if (n == 6u)
return C_CYAN;
return C_GRAY;
}
static unsigned in_board(void)
{
if (cur_r < g_board_r0)
return 0u;
if (cur_r >= (g_board_r0 + g_bh))
return 0u;
if (cur_c < g_board_c0)
return 0u;
if (cur_c >= (g_board_c0 + g_bw))
return 0u;
return 1u;
}
static unsigned cur_index(void)
{
return ((cur_r - g_board_r0) * g_bw) + (cur_c - g_board_c0);
}
/* LCG 随机数:只用 mul/add/移位 */
static unsigned rnd_next(void)
{
rnd_state = rnd_state * 1103515245u + 12345u;
return rnd_state >> 16;
}
/* 设定难度:棋盘尺寸 + 雷数 + 居中位置,三档完全照微软扫雷。 */
static void set_difficulty(unsigned difficulty)
{
g_difficulty = difficulty;
if (difficulty == 0u) /* 初级:9x9,10 雷 */
{
g_bw = 9u;
g_bh = 9u;
g_mines = 10u;
}
else if (difficulty == 2u) /* 高级:30x16,99 雷 */
{
g_bw = 30u;
g_bh = 16u;
g_mines = 99u;
}
else /* 中级:16x16,40 雷 */
{
g_bw = 16u;
g_bh = 16u;
g_mines = 40u;
}
g_ncells = g_bw * g_bh;
g_bm_words = (g_ncells + 31u) / 32u;
g_board_c0 = (GRID_COLS - g_bw) / 2u; /* 棋盘居中 */
g_board_r0 = (GRID_ROWS - g_bh) / 2u;
}
/* ============================ 画面 ============================ */
static void clear_screen(void)
{
unsigned i;
for (i = 0u; i < (GRID_COLS * GRID_ROWS); i = i + 1u)
{
fb_write_cell(i, G_BLANK, C_BLACK);
}
}
static void draw_frame(void)
{
unsigned gc, gr;
for (gc = (g_board_c0 - 1u); gc <= (g_board_c0 + g_bw); gc = gc + 1u)
{
draw_cell(g_board_r0 - 1u, gc, G_SOLID, C_CYAN);
draw_cell(g_board_r0 + g_bh, gc, G_SOLID, C_CYAN);
}
for (gr = (g_board_r0 - 1u); gr <= (g_board_r0 + g_bh); gr = gr + 1u)
{
draw_cell(gr, g_board_c0 - 1u, G_SOLID, C_CYAN);
draw_cell(gr, g_board_c0 + g_bw, G_SOLID, C_CYAN);
}
}
static void draw_all_covered(void)
{
unsigned r, c;
for (r = 0u; r < g_bh; r = r + 1u)
{
for (c = 0u; c < g_bw; c = c + 1u)
{
draw_tile(r, c, G_COVER, C_GRAY);
}
}
}
static void update_cursor_reg(void)
{
/* 列用 6 位(0..39):hard 棋盘宽 30,光标列能到 35,掩 31 会把它变成 3 */
REG_W(REG_CURSOR) = (cur_c & 63u) | ((cur_r & 31u) << 8);
}
static void set_bg(unsigned color)
{
REG_W(REG_CTRL) = (color << 4) | 1u; /* bit0 = 光标显示 */
}
/* ============================ 游戏逻辑 ============================ */
static unsigned count_adj(unsigned i)
{
unsigned r = IDX_R(i), c = IDX_C(i), n = 0u, rr, cc;
unsigned r0 = (r == 0u) ? 0u : (r - 1u);
unsigned r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u);
unsigned c0 = (c == 0u) ? 0u : (c - 1u);
unsigned c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u);
for (rr = r0; rr <= r1; rr = rr + 1u)
{
for (cc = c0; cc <= c1; cc = cc + 1u)
{
if ((rr == r) && (cc == c))
continue;
n = n + bget(mine_bm, (rr * g_bw) + cc);
}
}
return n;
}
static void lose(unsigned hit)
{
unsigned k;
for (k = 0u; k < g_ncells; k = k + 1u)
{
if (bget(mine_bm, k))
{
if (k == hit)
{
draw_tile(IDX_R(k), IDX_C(k), G_BOOM, C_RED);
}
else if (!bget(flag_bm, k))
{
draw_tile(IDX_R(k), IDX_C(k), G_MINE, C_RED);
}
}
}
REG_W(LED_ADDR) = 0xDEADu;
g_state = ST_LOSE;
}
static void win(void)
{
unsigned k;
for (k = 0u; k < g_ncells; k = k + 1u)
{
if (bget(mine_bm, k))
{
draw_tile(IDX_R(k), IDX_C(k), G_FLAG, C_YELLOW);
}
}
REG_W(LED_ADDR) = 0xF00Du;
g_state = ST_WIN;
}
static void reveal_at(unsigned i)
{
unsigned qh = 0u, qt = 0u, cur, n, r, c, rr, cc, ni;
unsigned r0, r1, c0, c1, k;
if (bget(open_bm, i) || bget(flag_bm, i))
return;
if (bget(mine_bm, i))
{
lose(i);
return;
}
for (k = 0u; k < g_bm_words; k = k + 1u)
seen_bm[k] = 0u;
bfs_q[qt] = i;
qt = qt + 1u;
bset(seen_bm, i);
while (qh < qt)
{
cur = bfs_q[qh];
qh = qh + 1u;
if (bget(open_bm, cur))
continue;
bset(open_bm, cur);
opened_cnt = opened_cnt + 1u;
n = count_adj(cur);
draw_tile(IDX_R(cur), IDX_C(cur), n, num_color(n));
if (n != 0u)
continue;
/* 空白格:把周围 8 格入队(已入过队的跳过,队列不会溢出) */
r = IDX_R(cur);
c = IDX_C(cur);
r0 = (r == 0u) ? 0u : (r - 1u);
r1 = (r + 1u >= g_bh) ? (g_bh - 1u) : (r + 1u);
c0 = (c == 0u) ? 0u : (c - 1u);
c1 = (c + 1u >= g_bw) ? (g_bw - 1u) : (c + 1u);
for (rr = r0; rr <= r1; rr = rr + 1u)
{
for (cc = c0; cc <= c1; cc = cc + 1u)
{
if ((rr == r) && (cc == c))
continue;
ni = (rr * g_bw) + cc;
if (bget(seen_bm, ni) || bget(open_bm, ni) || bget(flag_bm, ni))
continue;
if (qt < NCELLS_MAX)
{
bset(seen_bm, ni);
bfs_q[qt] = ni;
qt = qt + 1u;
}
}
}
}
if (opened_cnt == (g_ncells - g_mines))
win();
}
static void place_mines(unsigned safe)
{
unsigned placed = 0u, i, r, c;
unsigned sr = IDX_R(safe);
unsigned sc = IDX_C(safe);
while (placed < g_mines)
{
i = rnd_next() % g_ncells; /* 尺寸不再是 2 的幂,用 remu 取模 */
r = IDX_R(i);
c = IDX_C(i);
/* 首点及其 8 邻居不放雷,保证第一次点击一定能展开 */
if ((r + 1u >= sr) && (r <= (sr + 1u)) &&
(c + 1u >= sc) && (c <= (sc + 1u)))
continue;
if (bget(mine_bm, i))
continue;
bset(mine_bm, i);
placed = placed + 1u;
}
}
static void click_here(void)
{
unsigned i;
if (!in_board())
return;
i = cur_index();
if (bget(flag_bm, i))
return;
if (!g_first_done)
{
place_mines(i);
g_first_done = 1u;
}
reveal_at(i);
}
static void toggle_flag(void)
{
unsigned i, r, c;
if (!in_board())
return;
i = cur_index();
if (bget(open_bm, i))
return;
r = cur_r - g_board_r0;
c = cur_c - g_board_c0;
if (bget(flag_bm, i))
{
bclr(flag_bm, i);
flags_cnt = flags_cnt - 1u;
draw_tile(r, c, G_COVER, C_GRAY);
}
else
{
bset(flag_bm, i);
flags_cnt = flags_cnt + 1u;
draw_tile(r, c, G_FLAG, C_YELLOW);
}
REG_W(LED_ADDR) = g_mines - flags_cnt;
}
/* ============================ 鼠标事件 ============================ */
/* 关卡内:光标允许落在盘面外那一圈边框上(左上角那格是"返回") */
static void move_cursor(int dr, int dc)
{
int nr = (int)cur_r + dr;
int nc = (int)cur_c + dc;
if (nr < (int)(g_board_r0 - 1u))
nr = (int)(g_board_r0 - 1u);
if (nr > (int)(g_board_r0 + g_bh))
nr = (int)(g_board_r0 + g_bh);
if (nc < (int)(g_board_c0 - 1u))
nc = (int)(g_board_c0 - 1u);
if (nc > (int)(g_board_c0 + g_bw))
nc = (int)(g_board_c0 + g_bw);
cur_r = (unsigned)nr;
cur_c = (unsigned)nc;
update_cursor_reg();
}
/* ======================= 运行环境状态(背景 / 关卡) =======================
* 程序分成两层,正好对应"伪操作系统 + 关卡"的设想:
* 背景程序 main() 从头跑到尾:画开始界面、等鼠标;
* 关卡程序 level_main() 跑在 IRQ1(0x100) 的中断服务程序里:点"开始"进入,
* 点"返回"就 uret 回到背景被打断的那条指令。
* ========================================================================= */
static unsigned g_in_level; /* 0 = 背景界面,1 = 关卡内 */
static unsigned g_exit; /* 关卡内请求返回 */
/* 背景界面上"开始"按钮的位置(行 20,列 17..22) */
#define START_R 20u
#define START_C0 17u
#define START_C1 22u
#define DIFF_EASY_R 14u
#define DIFF_NORMAL_R 16u
#define DIFF_HARD_R 18u
static void draw_start_screen(void);
/* 背景用:光标可以在整个画面上移动 */
static void move_cursor_screen(int dr, int dc)
{
int nr = (int)cur_r + dr;
int nc = (int)cur_c + dc;
if (nr < 0)
nr = 0;
if (nr > (int)(GRID_ROWS - 1u))
nr = (int)(GRID_ROWS - 1u);
if (nc < 0)
nc = 0;
if (nc > (int)(GRID_COLS - 1u))
nc = (int)(GRID_COLS - 1u);
cur_r = (unsigned)nr;
cur_c = (unsigned)nc;
update_cursor_reg();
}
/* 处理一个鼠标事件:ev = {btn[20:16], dy[15:8], dx[7:0]} */
static void handle_event(unsigned ev)
{
unsigned ux = ev & 0xFFu;
unsigned uy = (ev >> 8) & 0xFFu;
unsigned btn = (ev >> 16) & 31u;
int dx = (ux & 0x80u) ? ((int)ux - 256) : (int)ux;
int dy = (uy & 0x80u) ? ((int)uy - 256) : (int)uy;
unsigned want_flag = (REG_R(REG_INPUT) & 9u) != 0u; /* SW0 或 SW3 = 插旗模式 */
if ((btn & 1u) != 0u)
{ /* 左键 */
if (g_in_level == 0u)
{
if (cur_r == DIFF_EASY_R)
{
set_difficulty(0u);
draw_start_screen();
return;
}
if (cur_r == DIFF_NORMAL_R)
{
set_difficulty(1u);
draw_start_screen();
return;
}
if (cur_r == DIFF_HARD_R)
{
set_difficulty(2u);
draw_start_screen();
return;
}
/* 背景:点到"开始"按钮 -> 拉起软件中断,跳到 IRQ1 里的关卡程序 */
if ((cur_r == START_R) && (cur_c >= START_C0) && (cur_c <= START_C1))
{
g_in_level = 1u; /* 关卡程序返回时自己清 0 */
REG_W(REG_LEVEL) = 1u;
}
}
else if ((cur_r == (g_board_r0 - 1u)) && (cur_c == (g_board_c0 - 1u)))
{
g_exit = 1u; /* 关卡:点左上角黄格 = 返回上一层 */
}
else if (want_flag != 0u)
{
toggle_flag();
}
else
{
click_here();
}
return;
}
if ((btn & 2u) != 0u)
{ /* 右键(PS/2 鼠标有;按键没有) */
if (g_in_level != 0u)
toggle_flag();
return;
}
/* 移动:单位是 1/8 格,累计到 8 就动一格(纯加减比较,没有除法) */
acc_x += dx;
acc_y += dy;
while (acc_x >= 8)
{
if (g_in_level != 0u)
move_cursor(0, 1);
else
move_cursor_screen(0, 1);
acc_x -= 8;
}
while (acc_x <= -8)
{
if (g_in_level != 0u)
move_cursor(0, -1);
else
move_cursor_screen(0, -1);
acc_x += 8;
}
while (acc_y >= 8)
{
if (g_in_level != 0u)
move_cursor(1, 0);
else
move_cursor_screen(1, 0);
acc_y -= 8;
}
while (acc_y <= -8)
{
if (g_in_level != 0u)
move_cursor(-1, 0);
else
move_cursor_screen(-1, 0);
acc_y += 8;
}
}
/* ============================ 背景程序(伪操作系统)============================
* 从头跑到尾:画开始界面 -> 等鼠标 -> 点到"开始"按钮就拉起软件中断。
* ========================================================================== */
static void draw_start_screen(void)
{
unsigned i, selected_color;
clear_screen();
/* 标题和难度菜单使用 RTL 字模中的大写字母。 */
draw_cell(6u, 18u, G_M, C_RED);
draw_cell(6u, 19u, G_I, C_RED);
draw_cell(6u, 20u, G_N, C_RED);
draw_cell(6u, 21u, G_E, C_RED);
draw_cell(6u, 22u, G_S, C_RED);
selected_color = C_YELLOW;
if (g_difficulty != 0u)
selected_color = C_GRAY;
draw_cell(DIFF_EASY_R, 18u, G_E, selected_color);
draw_cell(DIFF_EASY_R, 19u, G_A, selected_color);
draw_cell(DIFF_EASY_R, 20u, G_S, selected_color);
draw_cell(DIFF_EASY_R, 21u, G_Y, selected_color);
selected_color = C_YELLOW;
if (g_difficulty != 1u)
selected_color = C_GRAY;
draw_cell(DIFF_NORMAL_R, 16u, G_N, selected_color);
draw_cell(DIFF_NORMAL_R, 17u, G_O, selected_color);
draw_cell(DIFF_NORMAL_R, 18u, G_R, selected_color);
draw_cell(DIFF_NORMAL_R, 19u, G_M, selected_color);
draw_cell(DIFF_NORMAL_R, 20u, G_A, selected_color);
draw_cell(DIFF_NORMAL_R, 21u, G_L, selected_color);
selected_color = C_YELLOW;
if (g_difficulty != 2u)
selected_color = C_GRAY;
draw_cell(DIFF_HARD_R, 18u, G_H, selected_color);
draw_cell(DIFF_HARD_R, 19u, G_A, selected_color);
draw_cell(DIFF_HARD_R, 20u, G_R, selected_color);
draw_cell(DIFF_HARD_R, 21u, G_D, selected_color);
/* "START" 按钮:绿色实心条,上下各一条黄线框住它。 */
for (i = START_C0; i <= START_C1; i = i + 1u)
{
draw_cell(START_R - 1u, i, G_BAR, C_YELLOW);
draw_cell(START_R, i, G_SOLID, C_GREEN);
draw_cell(START_R + 1u, i, G_BAR, C_YELLOW);
}
/* 按钮上的字母不能用 C_BLACK:VGA 里像素色 = 字模点 ? 字符色 : 背景色,
* 而背景是黑的,所以“黑字”的字形点和空白处全黑 —— 整格完全看不见。
* 这里用黄色:黑底黄字,和上下的黄线框也更呼应。 */
draw_cell(START_R, 17u, G_S, C_YELLOW);
draw_cell(START_R, 18u, G_T, C_YELLOW);
draw_cell(START_R, 19u, G_A, C_YELLOW);
draw_cell(START_R, 20u, G_R, C_YELLOW);
draw_cell(START_R, 21u, G_T, C_YELLOW);
/* 下方提示条 */
for (i = 12u; i <= 27u; i = i + 1u)
{
draw_cell(24u, i, G_BAR, C_CYAN);
}
set_bg(C_BLACK);
cur_r = START_R;
cur_c = (START_C0 + START_C1) / 2u;
update_cursor_reg();
REG_W(LED_ADDR) = 0x0001u; /* LED = 背景状态码 */
REG_W(REG_SEG) = 0u;
}
/* ============================ 关卡(游戏)程序 ============================
* **运行在 IRQ1 的中断服务程序里**(入口 isr1_entry,固定地址 0x100):
* 背景 main() 点"开始" -> 写 LEVEL 寄存器 -> 硬件拉起 irq_i[1]
* -> CPU 跳到 0x100 -> isr1_entry 保存现场、打开 MIE、调用本函数
* -> 这里跑完整一局(鼠标中断 0x200 优先级更高,会嵌套进来把事件放进队列,
* 所以本循环只要轮询队列)
* -> 点左上角黄格 = 返回:清 LEVEL、return -> isr1_entry 恢复现场并 uret
* -> 回到背景程序被打断的那条指令,继续等下一次"开始"。
* ========================================================================== */
/* 关卡里“返回上一层”的黄格 = 棋盘左上角外面一格(随难度尺寸移动) */
void level_main(void)
{
unsigned r;
/* ---- 关卡初始化 ---- */
for (r = 0u; r < g_bm_words; r = r + 1u)
{
mine_bm[r] = 0u;
open_bm[r] = 0u;
flag_bm[r] = 0u;
seen_bm[r] = 0u;
}
opened_cnt = 0u;
flags_cnt = 0u;
g_first_done = 0u;
g_state = ST_PLAY;
g_exit = 0u;
acc_x = 0;
acc_y = 0;
cur_r = g_board_r0 + (g_bh / 2u); /* 光标从棋盘中心开始 */
cur_c = g_board_c0 + (g_bw / 2u);
clear_screen();
draw_frame();
draw_all_covered();
draw_cell(g_board_r0 - 1u, g_board_c0 - 1u, G_BAR, C_YELLOW); /* 左上角黄格 = 返回 */
set_bg(C_BLACK);
update_cursor_reg();
REG_W(LED_ADDR) = g_mines;
REG_W(REG_SEG) = g_mines; /* 数码管显示剩余雷数 */
/* ---- 关卡主循环 ---- */
while (g_exit == 0u)
{
if (g_evq_head != g_evq_tail)
{
unsigned ev = g_evq[g_evq_head];
g_evq_head = (g_evq_head + 1u) & EVQ_MASK;
if (g_state == ST_PLAY)
{
handle_event(ev);
}
else if ((ev >> 16) & 1u)
{
g_exit = 1u; /* 分出胜负后任意点击 = 返回 */
}
}
else if (g_state != ST_PLAY)
{
/* 分出胜负后让背景闪动(用 VGA 帧计数做时间基准,不需要定时器) */
static unsigned phase;
if ((REG_R(REG_FRAME) & 31u) == 0u)
{
phase = phase ^ 1u;
if (phase != 0u)
{
set_bg((g_state == ST_WIN) ? C_GREEN : C_RED);
}
else
{
set_bg(C_BLACK);
}
}
}
}
REG_W(REG_LEVEL) = 0u; /* 清软件中断:否则下次点"开始"没有新的上升沿 */
g_in_level = 0u; /* 回到背景状态 */
draw_start_screen(); /* 恢复开始界面 */
}
/* ============================ 背景主程序 ============================ */
extern void crt_zero_bss(void);
int main(void)
{
crt_zero_bss(); /* 清 .bss(上电 RAM 本来就是 0,保险起见) */
rnd_state = REG_R(REG_TIME) | 1u; /* 用自由计数器当随机种子 */
set_difficulty(1u); /* 默认中级:16x16,40 颗雷 */
REG_W(REG_MCTRL) = 1u; /* 打开鼠标中断(IRQ2,入口 0x200) */
REG_W(REG_LEVEL) = 0u; /* 清关卡软中断 */
g_in_level = 0u;
draw_start_screen();
/* 背景循环:只服务鼠标事件;点"开始"后会被 IRQ1 里的关卡程序"接管",
* 关卡返回时又回到这个循环里继续。 */
for (;;)
{
if (g_evq_head != g_evq_tail)
{
unsigned ev = g_evq[g_evq_head];
g_evq_head = (g_evq_head + 1u) & EVQ_MASK;
handle_event(ev);
}
}
}
-35
View File
@@ -1,35 +0,0 @@
/* -----------------------------------------------------------------------------
* start.c —— C 运行时启动:设置栈指针、把控制权交给 main。
*
* 这是全工程唯一非写不可的内联汇编(C 语言无法表达"设置 sp")。
* 复位时 PC = 0,所以 _start 必须落在 .text.init(见 linker.ld)。
*
* 注意用的是 lui/addi 绝对寻址 + jalr,**不是** auipc —— 这颗核没有 auipc。
* ---------------------------------------------------------------------------*/
extern unsigned __stack_top[];
int main(void);
__attribute__((naked, section(".text.init"), used, noreturn)) void _start(void)
{
__asm__ volatile(
"lui sp, %hi(__stack_top)\n\t"
"addi sp, sp, %lo(__stack_top)\n\t"
"lui t0, %hi(main)\n\t"
"addi t0, t0, %lo(main)\n\t"
"jalr x0, t0, 0\n\t");
}
/* 清 .bss(纯 C)。上电时 FPGA 里的 RAM 本来就是 0,这一步只是保险。 */
extern unsigned __bss_start[];
extern unsigned __bss_end[];
void crt_zero_bss(void)
{
unsigned *p = __bss_start;
while (p < __bss_end)
{
*p = 0u;
p = p + 1u;
}
}
-179
View File
@@ -1,179 +0,0 @@
# =============================================================================
# build.ps1 —— 扫雷程序一键构建:C -> 汇编/目标文件 -> ELF -> .hex + .vh
#
# 用法(在 saolei\ 目录下):
# powershell -ExecutionPolicy Bypass -File tools\build.ps1
# powershell -ExecutionPolicy Bypass -File tools\build.ps1 -Prefix riscv64-unknown-elf-
#
# 产物(都在 saolei\build\ 下):
# saolei.lst objdump -d 反汇编清单(这就是"编译出来的汇编",可以直接看)
# saolei.elf 可执行文件
# saolei.bin 裸二进制
# saolei.hex $readmemh 格式:拷到 pipelined-cpu\srcs\rom\saolei.hex 就能
# 用现成的测试平台(tb_no_intr 等)仿真
# saolei.map 链接 map(看段地址和大小)
# 并且会生成 pipelined-cpu\srcs\board\cpu21_saolei_rom.vh —— 供 cpu21_board_top.v 用
# `include 固化进 ROM(在 Vivado 里定义宏 SAOLEI_ROM 即可切换到扫雷程序)
#
# 构建流程里有三道针对这颗 CPU 的关卡:
# 1) -fno-pic -mcmodel=medlow -mno-relax:避免 auipc / 链接器松弛
# 2) 链接脚本里的 ASSERT:禁止 .data / .rodata(这颗核读不到 ROM 里的数据)
# 3) tools\check_isa.py:逐条检查反汇编,凡是用到未实现指令(auipc/lb/lbu/div...)
# 直接报错并给出修复提示
# =============================================================================
param(
[string]$Prefix = "",
[int]$RomWords = 8192,
[switch]$SkipIsaCheck
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$sw = Join-Path $root 'sw'
$build = Join-Path $root 'build'
New-Item -ItemType Directory -Force $build | Out-Null
# ---------------------------------------------------------------- 工具链探测
if (-not $Prefix) {
foreach ($cand in 'riscv-none-elf-', 'riscv64-unknown-elf-', 'riscv32-unknown-elf-',
'riscv-none-embed-', 'riscv64-linux-gnu-') {
if (Get-Command ($cand + 'gcc') -ErrorAction SilentlyContinue) { $Prefix = $cand; break }
}
}
if (-not $Prefix) {
Write-Output "ERROR: 找不到 RISC-V 交叉编译器(riscv*-gcc)。"
Write-Output ""
Write-Output "装一个即可(推荐 xPack 的 riscv-none-elf-gcc,解压就用,不用管理员权限):"
Write-Output " 1) 打开 https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases"
Write-Output " 2) 下载 win32-x64 的 .zip(例如 xpack-riscv-none-elf-gcc-14.2.0-3-win32-x64.zip)"
Write-Output " 3) 解压到例如 C:\tools\riscv-none-elf-gcc"
Write-Output " 4) 把 C:\tools\riscv-none-elf-gcc\bin 加进 PATH(或者直接运行本脚本时):"
Write-Output " powershell -ExecutionPolicy Bypass -File tools\build.ps1 -Prefix 'C:\tools\riscv-none-elf-gcc\bin\riscv-none-elf-'"
exit 1
}
$GCC = $Prefix + 'gcc'
$OBJDUMP = $Prefix + 'objdump'
$OBJCOPY = $Prefix + 'objcopy'
$SIZE = $Prefix + 'size'
$Python = 'py'
if (-not (Get-Command $Python -ErrorAction SilentlyContinue)) {
$Python = 'python'
}
Write-Output "工具链前缀: $Prefix"
# ---------------------------------------------------------------- 编译参数
# -march=rv32im_zicsr : RV32I + M 扩展 + Zicsr(CSR 指令:csrrw/csrrsi/csrrci)。
# GCC 15 起不再默认给 rv32im 开 zicsr,中断入口里的
# csrrsi 必须显式打开这个扩展才认。
# -mcmodel=medlow -fno-pic : 用 lui/addi 绝对寻址,不产生 auipc
# **必须放开链接器松弛**(不要 -mno-relax):gcc 的函数调用默认是
# auipc ra, %pcrel_hi(f) + jalr ra, ra, %pcrel_lo(f)
# 要靠链接器松弛把它变成一条 jal,否则 auipc 会留在最终代码里(这颗核没有)。
# 松弛只会在两种情况下改变指令:把 pcrel 调用变成 jal,或在使用 gp 相对寻址时
# 变成 gp 相对访问 —— 后者已被 -msmall-data-limit=0 从根上禁止。
# -msmall-data-limit=0 : 不用 gp 相对寻址
# -fno-builtin -nostdlib -ffreestanding : 不要 libc(没有环境,也没有 printf)
# -fno-jump-tables : 不生成跳转表(跳转表会落在 .rodata,这颗核读不到)
$ccCommon = @(
'-march=rv32im_zicsr', '-mabi=ilp32',
'-mcmodel=medlow',
'-fno-pic', '-fno-PIC', '-fno-pie',
'-msmall-data-limit=0', '-fno-common',
'-fno-builtin', '-fno-stack-protector', '-fno-jump-tables',
'-ffreestanding', '-nostdlib',
'-ffunction-sections', '-fdata-sections',
'-Os', '-Wall', '-Wextra'
)
# 中断主体(isr.c)必须只使用 callee-saved 寄存器,否则会破坏被打断的代码。
# 下面把所有"调用者保存"的通用寄存器都固定住,GCC 就只能用 s0-s11 和 sp。
$fixedRegs = @()
foreach ($r in 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17, 28, 29, 30, 31) {
$fixedRegs += ('-ffixed-x' + $r)
}
# ---------------------------------------------------------------- 编译
$objs = @()
# 跑外部命令并在非 0 退出码时立刻停下(native 命令失败不会抛异常,必须查 $LASTEXITCODE)
function Invoke-Tool {
param([string]$Exe, [string[]]$ArgList, [string]$What)
& $Exe @ArgList
if ($LASTEXITCODE -ne 0) {
Write-Output ("ERROR: 失败 -> " + $What)
exit 1
}
}
Write-Output "编译 start.c ..."
Invoke-Tool -Exe $GCC -ArgList ($ccCommon + @('-c', (Join-Path $sw 'start.c'), '-o', (Join-Path $build 'start.o'))) -What 'start.c'
$objs += (Join-Path $build 'start.o')
Write-Output "编译 saolei.c ..."
Invoke-Tool -Exe $GCC -ArgList ($ccCommon + @('-c', (Join-Path $sw 'saolei.c'), '-o', (Join-Path $build 'saolei.o'))) -What 'saolei.c'
$objs += (Join-Path $build 'saolei.o')
Write-Output "编译 isr.c(额外加 -ffixed 约束)..."
Invoke-Tool -Exe $GCC -ArgList ($ccCommon + $fixedRegs + @('-c', (Join-Path $sw 'isr.c'), '-o', (Join-Path $build 'isr.o'))) -What 'isr.c'
$objs += (Join-Path $build 'isr.o')
# ---------------------------------------------------------------- 链接
$elf = Join-Path $build 'saolei.elf'
$map = Join-Path $build 'saolei.map'
$ld = Join-Path $sw 'linker.ld'
Write-Output "链接 ..."
# 注意:PowerShell 会把未加引号的 "-Wl,--no-relax" 当成参数对解析(逗号引起),
# 所以这里统一用参数数组,所有选项都带引号。
$linkArgs = @(
'-march=rv32im_zicsr', '-mabi=ilp32', '-nostdlib', '-nostartfiles',
"-Wl,-Map=$map",
'-T', $ld, '-o', $elf
) + $objs
Invoke-Tool -Exe $GCC -ArgList $linkArgs -What '链接(-T sw/linker.ld)'
# ---------------------------------------------------------------- 反汇编 + ISA 检查
$lst = Join-Path $build 'saolei.lst'
& $OBJDUMP -d -M no-aliases $elf | Out-File -Encoding utf8 $lst
Write-Output "反汇编清单: $lst"
$isaOk = $true
if (-not $SkipIsaCheck) {
Write-Output ""
& $Python (Join-Path $PSScriptRoot 'check_isa.py') $lst
if ($LASTEXITCODE -ne 0) { $isaOk = $false }
}
# ---------------------------------------------------------------- 转 hex / vh
$bin = Join-Path $build 'saolei.bin'
$hex = Join-Path $build 'saolei.hex'
$vh = Join-Path $root '..\pipelined-cpu\srcs\board\cpu21_saolei_rom.vh'
& $OBJCOPY -O binary $elf $bin
& $Python (Join-Path $PSScriptRoot 'elf2hex.py') $bin $hex $RomWords $vh
# ---------------------------------------------------------------- 汇总
Write-Output ""
Write-Output "==== 段大小 ===="
& $SIZE $elf
$isrLine = Select-String -Path $lst -Pattern '^[0-9a-f]+ <(isr1_entry|isr2_entry|isr3_entry|_start)>:' |
Select-Object -ExpandProperty Line
Write-Output "==== 入口地址 ===="
foreach ($l in $isrLine) { Write-Output (" " + $l) }
Write-Output " 三个中断入口必须和顶层 cpu21_board_top 的参数一致:"
Write-Output " linker.ld 的 .text.isr1/2/3 = IRQ1/2/3_VECTOR = 0x100/0x200/0x300"
Write-Output ""
if ($isaOk) {
Write-Output "构建完成:$hex 与 $vh 已生成。"
Write-Output " 仿真:把 saolei.hex 拷到 pipelined-cpu\srcs\rom\saolei.hex,用 tb_no_intr 加载"
Write-Output " 上板:cpu21_saolei_rom.vh 已生成到 srcs/board/,Vivado 里定义宏 SAOLEI_ROM 后综合"
} else {
Write-Output "构建完成,但 ISA 检查没通过(见上面的清单),这个 hex 放到 CPU 上会跑飞。"
exit 1
}
-113
View File
@@ -1,113 +0,0 @@
#!/usr/bin/env python3
"""check_isa.py —— 校验反汇编清单里没有这颗 CPU 未实现的指令。
用法: python tools/check_isa.py <objdump输出.txt>
为什么需要它:本 CPU(srcs/rtl/cpu21_riscv_decoder.v)只实现了 RV32I 的一个
子集,最容易踩的三个坑是:
* 没有 auipc -> 编译器一旦生成它,整段代码就废了(多为 PIC/rodata 引起)
* 没有 lb/lh/lbu/lhu -> 只要碰 char/short 类型的读就会出现
* 没有有符号 div/rem(只有 divu/remu) -> 有符号除法/取余会被静默算错
所以这里用"白名单"方式检查:出现名单外的助记符就报错并给出修复提示。
退出码 = 违规条数(>255 时截断),0 表示通过。
"""
import sys
# 这颗 CPU 真正实现了的指令(见 cpu21_riscv_decoder.v)
ALLOWED = {
# R 型
"add", "sub", "sll", "slt", "sltu", "xor", "srl", "sra", "or", "and",
# M 扩展(只有这几个)
"mul", "mulh", "mulhsu", "mulhu", "divu", "remu",
# I 型
"addi", "slti", "sltiu", "xori", "ori", "andi", "slli", "srli", "srai",
# 访存:只有 lw / sw / sb(没有 lb/lh/lbu/lhu/sh)
"lw", "sw", "sb",
# 控制流
"beq", "bne", "blt", "bge", "bltu", "bgeu", "jal", "jalr",
# 高位立即数
"lui",
# 系统
"ecall", "uret", "csrrw", "csrrsi", "csrrci",
}
# objdump 有时仍打印伪指令;这些展开后都是上面支持的指令
ALIAS_OK = {
"nop", "mv", "li", "j", "jr", "ret", "neg", "not",
"beqz", "bnez", "blez", "bgez", "bltz", "bgtz",
"seqz", "snez", "sltz", "sgtz",
}
# 违规时给出针对性提示
HINTS = {
"auipc": "出现了 auipc:说明编译带了 PIC/PIE 或访问了只读数据。"
"请用 -fno-pic -mcmodel=medlow,并去掉字符串/常量表(.rodata)",
"lb": "没有 lb:不要用 char/short 类型读内存,改用 32 位字",
"lh": "没有 lh:同上",
"lbu": "没有 lbu:不要用 unsigned char/short 类型读内存(写显存可以用 sb,但别读回来)",
"lhu": "没有 lhu:同上",
"sh": "没有 sh:只用 sw / sb",
"div": "没有有符号 div:改用 unsigned 运算(unsigned 除法会编成 divu)",
"rem": "没有有符号 rem:改用 unsigned 运算(unsigned 取余会编成 remu)",
"divw": "不支持 64 位运算",
"remw": "不支持 64 位运算",
"call": "call 会展开成 auipc+jalr:请直接用普通函数调用(jal)",
"tail": "tail 会展开成 auipc:请用普通跳转",
"fence": "没有 fence/fence.i",
"ebreak": "没有 ebreak",
"mret": "这颗核的中断返回是 uret,不是 mret",
"sret": "这颗核的中断返回是 uret",
"wfi": "没有 wfi:主循环只能用轮询",
"unimp": "出现了 unimp(非法指令/内联汇编占位)",
}
def main():
if len(sys.argv) < 2:
print("用法: python tools/check_isa.py <objdump输出.txt>")
return 2
path = sys.argv[1]
total = 0
violations = []
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
if "\t" not in line:
continue
parts = line.split("\t")
if len(parts) < 3 or not parts[0].strip().endswith(":"):
continue
mnemonic = parts[2].strip().split()[0]
if not mnemonic:
continue
total += 1
if (mnemonic not in ALLOWED) and (mnemonic not in ALIAS_OK):
violations.append((parts[0].strip(), mnemonic, line.rstrip()))
print("检查了 %d 条指令。" % total)
if not violations:
print("ISA 检查通过:全部指令都在本 CPU 的实现范围内。")
return 0
print("")
print("!! ISA 检查失败:有 %d 条指令这颗 CPU 没有实现:" % len(violations))
seen = set()
for addr, mnemonic, raw in violations[:40]:
print(" %s %s" % (addr, raw.strip()))
seen.add(mnemonic)
if len(violations) > 40:
print(" ...(只列出前 40 条)")
print("")
print("修复提示:")
for m in sorted(seen):
print(" * %s: %s" % (m, HINTS.get(m, "不在本 CPU 的指令白名单里")))
return min(len(violations), 255)
if __name__ == "__main__":
sys.exit(main())
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env python3
"""elf2hex.py —— 把 objcopy 出来的裸二进制转成 ROM 需要的两份文件。
用法: python tools/elf2hex.py <input.bin> <output.hex> [ROM_WORDS] [output.vh]
产出:
1) <output.hex> 每行一个 32 位字($readmemh 格式,供 srcs/rom/ 下的
测试平台加载,例如拷成 srcs/rom/saolei.hex)
2) <output.vh>(可选) rom[i] = 32'h...; 语句,供
pipelined-cpu/srcs/board/cpu21_board_top.v 里 `include
固化进片内 ROM(未用到的地址填 NOP 0x00000013)
(未用到的地址填 NOP 0x00000013)
字序:输入的 .bin 是按字节小端排列的机器码,这里按小端拼成 32 位字。
"""
import sys
NOP = 0x00000013
def main():
if len(sys.argv) < 3:
print("用法: python tools/elf2hex.py <input.bin> <output.hex> [ROM_WORDS] [output.vh]")
return 2
src = sys.argv[1]
dst = sys.argv[2]
words_total = int(sys.argv[3]) if len(sys.argv) > 3 else 8192
vh_path = sys.argv[4] if len(sys.argv) > 4 else None
with open(src, "rb") as f:
data = f.read()
if len(data) % 4 != 0:
print("警告:二进制长度 %d 不是 4 的倍数,尾部会被补 0" % len(data))
data += b"\x00" * (4 - len(data) % 4)
words = []
for i in range(0, len(data), 4):
words.append(int.from_bytes(data[i:i + 4], "little"))
# 段间空洞填 NOP:objcopy 出来的 .bin 在 .text.isr1 / .text.isr2 / .text.isr3
# 这些"绝对地址段"之间会留下 0x00000000,而本 CPU 把 0x00000000 译成自跳指令
# (beq x0,x0,0 形式),PC 一旦落进去就永远出不来(仿真里已复现:PC 热点全在
# 0x194-0x1FF / 0x218-0x2FF 这些空洞地址上)。这里把"最后一条非零字之前"的
# 0 字统一换成 NOP,让 PC 即使误入也能顺势走过。
last_nz = -1
for i, w in enumerate(words):
if w != 0:
last_nz = i
filled = 0
for i in range(last_nz):
if words[i] == 0:
words[i] = NOP
filled += 1
if len(words) > words_total:
print("错误:程序 %d 个字,超过 ROM 容量 %d 个字(改大 ROM_WORDS 或精简程序)"
% (len(words), words_total))
return 1
with open(dst, "w", encoding="ascii") as f:
for w in words:
f.write("%08X\n" % w)
if vh_path:
with open(vh_path, "w", encoding="utf-8") as f:
f.write(" // 由 tools/elf2hex.py 生成,请勿手工编辑\n")
f.write(" // 源文件:%s(%d 个字,其余填 NOP)\n" % (src, len(words)))
for addr in range(words_total):
w = words[addr] if addr < len(words) else NOP
f.write(" rom[%d] = 32'h%08X;\n" % (addr, w))
print("已生成 %s(%d 个字)%s" % (dst, len(words),
("、%s(%d 个字,NOP 填充)" % (vh_path, words_total)) if vh_path else ""))
return 0
if __name__ == "__main__":
sys.exit(main())
-243
View File
@@ -1,243 +0,0 @@
#!/usr/bin/env python3
"""fb2png.py —— 把 tb_saolei 导出的显存 dump 还原成 PNG(等价于 VGA 截图)。
用法:
py saolei/tools/fb2png.py <dump.txt> [输出.png]
py saolei/tools/fb2png.py --term <dump.txt> # 只在终端打印彩色棋盘(交互玩扫雷用)
py saolei/tools/fb2png.py --quiet <dump.txt> [输出.png] # 只写 PNG,不打印任何东西
dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
CURSOR <row> <col>
CTRL 0x<byte> # [0] 光标可见 [6:4] 背景色
FB 2400 # 随后 2400 行十六进制字节:每格 2 字节(字形、颜色)
渲染依据(与 RTL 严格对应):
* 字库:直接从 cpu21_vga.v 的 font_row() 解析(8x8、bit7 最左,放大 2 倍 = 16x16 一格)
—— RTL 改了字库(比如新增 A-Z)这里会自动跟随,不用手工抄表
* 调色板 cpu21_vga.v 的 pal():3 位 -> 4:4:4 RGB(0xA 分量 = 170)
* 显存每格 2 字节:byte0 = 字形[5:0],byte1 = 颜色[2:0]
* 光标 cpu21_vga.v 的 cursor_box:格子四边一圈 2 像素宽的黄色框(常亮)
* 画面 40x30 格 -> 640x480
另外还会在终端打印一份 ASCII 图,方便在没有图片查看器的地方快速看结果。
"""
import os
import re
import struct
import sys
import zlib
COLS, ROWS = 40, 30
CELL_PX = 16 # 8x8 字模放大 2 倍
FB_BYTES = COLS * ROWS * 2 # 每格 2 字节(字形 + 颜色)= 2400
RTL_VGA = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "pipelined-cpu", "srcs", "rtl",
"cpu21_vga.v"))
def load_font_from_rtl(path=None):
"""直接从 cpu21_vga.v 的 font_row() 里解析 8x8 字模,返回 {字形号: [8 行]}。
RTL 里每个字形形如
6'd16: font_row = (r == 3'd0) ? 8'h7C : (r == 3'd1) ? 8'h66 : ... ;
或全部相同行时形如
6'd15: font_row = 8'hFF;
两种情况都能解析。
"""
path = path or RTL_VGA
try:
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
except OSError as e:
raise SystemExit("读不到 RTL 字库 %s:%s" % (path, e))
m = re.search(r"function\s*\[7:0\]\s*font_row.*?endfunction", text, re.S)
if not m:
raise SystemExit("%s 里找不到 font_row()" % path)
font = {}
parts = re.split(r"6'd(\d+)\s*:", m.group(0))
for i in range(1, len(parts) - 1, 2): # 奇数下标是字形号,后一格是它的分支体
idx, seg = int(parts[i]), parts[i + 1]
rows = [0] * 8
pairs = re.findall(r"\(r == 3'd(\d+)\)\s*\?\s*8'h([0-9A-Fa-f]{2})", seg)
if pairs:
for k, v in pairs:
rows[int(k)] = int(v, 16)
else: # 形如 "font_row = 8'hFF;":8 行一样
one = re.search(r"font_row\s*=\s*8'h([0-9A-Fa-f]{2})", seg)
if one:
rows = [int(one.group(1), 16)] * 8
font[idx] = rows
if not font:
raise SystemExit("%s 的 font_row() 没解析出任何字形" % path)
return font
# --- 调色板(3 位 -> RGB),抄自 cpu21_vga.v 的 pal(),4 位分量按 *17 扩到 8 位 ---
PAL = [
(0x0, 0x0, 0x0), # 0 黑
(0x0, 0x0, 0xA), # 1 深蓝
(0x0, 0xA, 0x0), # 2 绿
(0x0, 0xA, 0xA), # 3 青
(0xA, 0x0, 0x0), # 4 红
(0xA, 0x0, 0xA), # 5 洋红
(0xA, 0xA, 0x0), # 6 黄
(0xA, 0xA, 0xA), # 7 浅灰
]
GLYPH_CH = {0: ' ', 9: '#', 10: 'P', 11: '*', 12: 'X', 13: '?', 14: '-', 15: '='}
COLOR_CH = {0: '.', 1: 'b', 2: 'g', 3: 'c', 4: 'r', 5: 'm', 6: 'y', 7: 'w'}
def write_png(path, width, height, rows):
"""rows: 每行一个 bytes(长度 width*3),最小 PNG 编码。"""
raw = b"".join(b"\x00" + r for r in rows)
def chunk(tag, data):
return (struct.pack(">I", len(data)) + tag + data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(raw, 9))
png += chunk(b"IEND", b"")
with open(path, "wb") as f:
f.write(png)
def parse_dump(path):
cur = (None, None)
ctrl = 0
fb = []
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
line = line.strip()
if line.startswith("CURSOR"):
p = line.split()
cur = (int(p[1]), int(p[2]))
elif line.startswith("CTRL"):
ctrl = int(line.split()[1], 16)
elif line.startswith("FB"):
fb = []
elif len(line) == 2 and fb is not None:
try:
fb.append(int(line, 16))
except ValueError:
pass
if len(fb) != FB_BYTES:
raise SystemExit("显存字节数不对:%d(应为 %d;新版每格 2 字节)"
% (len(fb), FB_BYTES))
return cur, ctrl, fb
def cell_of(fb, idx):
"""第 idx 格 -> (字形, 颜色):byte0 = 字形[5:0],byte1 = 颜色[2:0]。"""
return fb[2 * idx] & 0x3F, fb[2 * idx + 1] & 0x07
def cell_char(g):
"""字形号 -> 一个可见字符(1..8 数字,16..41 = A-Z,其余见 GLYPH_CH)。"""
if 16 <= g <= 41:
return chr(ord('A') + g - 16)
if 1 <= g <= 8:
return str(g)
return GLYPH_CH.get(g, '?')
def dump_note(path):
"""把 dump 开头的 TIME / CURSOR / STATS 拼成一行,当终端画面的标题。"""
note = []
try:
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
if line.startswith(("TIME", "CURSOR", "STATS")):
note.append(line.strip())
elif line.startswith("FB"):
break
except OSError:
pass
return " ".join(note)
def draw_term(fb, cur, cursor_box, note=""):
"""在终端用 ANSI 24 位颜色画 40x30 棋盘(每格一个字符),光标格反色。"""
try: # Windows 控制台默认不是 UTF-8
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
out = ["\x1b[H\x1b[2J"] # 回左上角 + 清屏(连续刷新不闪屏残留)
if note:
out.append(" " + note + "\n")
out.append(" " + "".join(str(x % 10) for x in range(COLS)) + "\n")
for y in range(ROWS):
cells = []
for x in range(COLS):
g, c = cell_of(fb, y * COLS + x)
r, gg, b = PAL[c]
ch = cell_char(g)
if cursor_box and (x, y) == cur:
cells.append("\x1b[7m\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17,
ch if ch != ' ' else '_'))
else:
cells.append("\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17, ch))
out.append("%2d " % y + "".join(cells) + "\x1b[0m\n")
out.append("\x1b[0m")
out.append("图例: #=未揭开 P=旗 *=雷 X=爆炸 A-Z=文字 1..8=数字 空格=已揭开的空地;反色格 = 光标\n")
sys.stdout.write("".join(out))
sys.stdout.flush()
def main():
args = list(sys.argv[1:])
term_only = "--term" in args
quiet = "--quiet" in args
if term_only:
args.remove("--term")
if quiet:
args.remove("--quiet")
if quiet: # 只出图不说话(给脚本批量截图用)
sys.stdout = open(os.devnull, "w")
if not args:
raise SystemExit(__doc__)
src = args[0]
dst = args[1] if len(args) > 1 else src.rsplit(".", 1)[0] + ".png"
cur, ctrl, fb = parse_dump(src)
font = load_font_from_rtl()
bg = (ctrl >> 4) & 0x7
cursor_box = bool(ctrl & 1) and cur[0] is not None
if term_only:
draw_term(fb, cur, cursor_box, dump_note(src))
return
print("光标 = (%s, %s) 光标可见 = %s 背景色 = %d"
% (cur[0], cur[1], bool(ctrl & 1), bg))
# ---- 渲染 ----
rows = []
for y in range(ROWS * CELL_PX):
row = bytearray()
cy, fy = y // CELL_PX, (y % CELL_PX) // 2
for x in range(COLS * CELL_PX):
cx, fx = x // CELL_PX, (x % CELL_PX) // 2
g, c = cell_of(fb, cy * COLS + cx)
px_on = (font.get(g, [0] * 8)[fy] >> (7 - fx)) & 1
col = c if px_on else bg
if cursor_box and (cx, cy) == cur:
# 光标格:四边 2 像素黄色框,与 cpu21_vga.v 的 cursor_box 一致
ox, oy = x % CELL_PX, y % CELL_PX
if ox < 2 or ox > CELL_PX - 3 or oy < 2 or oy > CELL_PX - 3:
col = 6
r, gg, b = PAL[col]
row += bytes((r * 17, gg * 17, b * 17))
rows.append(bytes(row))
write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows)
print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX))
# ---- 终端 ASCII 图(彩色版;PNG 已经写好了,这里只是方便快速看一眼)----
draw_term(fb, cur, cursor_box, dump_note(src))
main()
@@ -0,0 +1,421 @@
.text
addi sp, zero, 2000 #内存堆栈初始化
#############################################################
#走马灯测?,测试addi,andi,slli,srli,srai,or,ori,nor,ecall LED按走马灯方式来回显示数据
#############################################################
.text
START:
addi s0,zero,1
slli s3, s0, 31 # s3=0x80000000
srai s3, s3, 31 # s3=0xFFFFFFFF
add s0,zero,zero # s0=0
addi s2,zero,12
addi s6,zero,8 #走马灯计数
zmd_loop:
addi s0, s0, 1 #计算下一个走马灯的数
andi s0, s0, 15
#######################################
addi t0,zero,8
addi t1,zero,1
left:
slli s3, s3, 4 #走马灯左移
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,left
#######################################
addi s0, s0, 1 #计算下一个走马灯的数
addi t6,zero,15
and s0, s0, t6
slli s0, s0, 28
addi t0,zero,8
addi t1,zero,1
zmd_right:
srli s3, s3, 4 #走马灯右移
or s3, s3, s0
add a0,zero,s3 # display s3
addi a7,zero,34 # system call for LED display
ecall # display
sub t0,t0,t1
bne t0,zero,zmd_right
srli s0, s0, 28
#######################################
sub s6,s6,t1
beq s6,zero, exit
j zmd_loop
exit:
add t0,zero,zero
xori t0,t0,-1 #test nor ori
slli t0,t0,16
ori t0,t0,-1
add a0,zero,t0 # display t0
addi a7,zero,34 # system call for LED display
ecall # display
j START # loop forever
#InteruptProgram1:
#############################################################################################
# exceptoin 1
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram1:
sw t0, 0(sp) #保护t0现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# DIVU测试
addi t0,zero,2 # /2
addi s1,zero, 0x11
slli s1,s1,8
addi s1,s1,0x11
slli s1,s1,16
add a0,zero,s1
addi a7,zero,34
ecall
addi t3,zero,28 #ѭ������
divu_branch:
divu s1,s1,t0 #����ָ��
add a0,zero,s1
addi a7,zero,34
ecall #�����ǰֵ
addi t3,t3, -1
bne t3,zero,divu_branch #ѭ��
addi a7,zero,10
################# 中断服务
addi s6,zero,1 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop1:
add s0,zero,s6
IntLeftShift1:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift1
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop1
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t0, 0(sp) #恢复t0现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram2:
#############################################################################################
# exceptoin 2
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram2:
sw t1, 0(sp) #保护t1现场
addi sp, sp, 4
sw t3, 0(sp) #保护t3现场
addi sp, sp, 4
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw s2, 0(sp) #保护s2现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# BLT测试
addi s1,zero,-15 #初始值
blt_branch:
add a0,zero,s1
addi a7,zero,34
ecall #输出当前值
addi s1,s1,1
blt s1,zero,blt_branch #当前指令
addi a7,zero,10
################# 中断服务
addi s6,zero,2 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop2:
add s0,zero,s6
IntLeftShift2:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift2
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop2
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp) #恢复s0现场
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s2, 0(sp) #恢复s2现场
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
addi sp, sp, -4
lw t3, 0(sp) #恢复t3现场
addi sp, sp, -4
lw t1, 0(sp) #恢复t1现场
################ 中断返回
uret #同步开中断,uepc-->pc
#InteruptProgram3:
#############################################################################################
# exceptoin 3
# 使用?s6? s5?s4?s3?s0?a0?a7
#############################################################################################
################# 保护现场
InteruptProgram3:
sw s1, 0(sp) #保护s1现场
addi sp, sp, 4
sw a7, 0(sp)
addi sp, sp, 4
sw a0, 0(sp)
addi sp, sp, 4
sw s0, 0(sp)
addi sp, sp, 4
sw s3, 0(sp)
addi sp, sp, 4
sw s4, 0(sp)
addi sp, sp, 4
sw s5, 0(sp)
addi sp, sp, 4
sw s6, 0(sp)
addi sp, sp, 4
csrrsi s6,0x41,0 #s6=uepc
sw s6, 0(sp) #uepc压栈保护
addi sp, sp, 4
################# 开中断
csrrsi zero,0x4,1 #ustatus.MIE=1
################# SB测试
addi t1,zero,0 #init_addr
addi t3,zero,32 #counter
#sb写入 01,02,03,04
addi s1,zero, 0x00 #
addi s2,zero, 0x01 #
sb_store:
sb s1,(t1)
add a0,zero,s1
addi a7,zero,34 # system call for print
ecall # print
add s1,s1,s2 #data +1
addi t1,t1,1 # addr ++
addi t3,t3,-1 #counter
bne t3,zero,sb_store
addi t3,zero,8
addi t1,zero,0 # addr
sb_branch:
lw s1,(t1) #读出数据
add a0,zero,s1
addi a7,zero,34 # system call for print
ecall # print
addi t1,t1,4
addi t3,t3, -1
bne t3,zero,sb_branch
addi a7,zero,10 # system call for exit
################# 中断服务
addi s6,zero,3 #中断?1,2,3 不同中断号显示值不一样
addi s4,zero,3 #循环次数初始值
addi s5,zero,1 #计数器累加值
IntLoop3:
add s0,zero,s6
IntLeftShift3:
slli s0, s0, 4
or s3,s0,s4
add a0,zero,s3 #display s0
addi a7,zero,34 # display hex
ecall
bne s0, zero, IntLeftShift3
sub s4,s4,s5 #循环次数递减
bne s4, zero, IntLoop3
################# 关中断
csrrci zero,0x4,1 #ustatus.MIE=0
################# 恢复现场
addi sp, sp, -4
lw s6, 0(sp)
csrrw zero,0x41,s6 #uepc=s6
addi sp, sp, -4
lw s6, 0(sp)
addi sp, sp, -4
lw s5, 0(sp)
addi sp, sp, -4
lw s4, 0(sp)
addi sp, sp, -4
lw s3, 0(sp)
addi sp, sp, -4
lw s0, 0(sp)
addi sp, sp, -4
lw a0, 0(sp)
addi sp, sp, -4
lw a7, 0(sp)
addi sp, sp, -4
lw s1, 0(sp) #恢复s1现场
################ 中断返回
uret #同步开中断,uepc-->pc
@@ -12,8 +12,8 @@
00499993
0089e9b3
01300533
000018B7
00A8A023
02200893
00000073
406282b3
fe0294e3
00140413
@@ -25,8 +25,8 @@ fe0294e3
0049d993
0089e9b3
01300533
000018B7
00A8A023
02200893
00000073
406282b3
fe0294e3
01c45413
@@ -38,8 +38,8 @@ fff2c293
01029293
fff2e293
00500533
000018B7
00A8A023
02200893
00000073
f5dff06f
00512023
00410113
@@ -71,13 +71,13 @@ f5dff06f
01148493
01049493
00900533
000018B7
00A8A023
02200893
00000073
01c00e13
0254d4b3
00900533
000018B7
00A8A023
02200893
00000073
fffe0e13
fe0e16e3
00a00893
@@ -89,8 +89,8 @@ fe0e16e3
00441413
014469b3
01300533
000018B7
00A8A023
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
@@ -147,8 +147,8 @@ ffc10113
0040e073
ff100493
00900533
000018B7
00A8A023
02200893
00000073
00148493
fe04c8e3
00a00893
@@ -160,8 +160,8 @@ fe04c8e3
00441413
014469b3
01300533
000018B7
00A8A023
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
@@ -218,8 +218,8 @@ ffc10113
00100913
00930023
00900533
000018B7
00A8A023
02200893
00000073
012484b3
00130313
fffe0e13
@@ -228,8 +228,8 @@ fe0e12e3
00000313
00032483
00900533
000018B7
00A8A023
02200893
00000073
00430313
fffe0e13
fe0e14e3
@@ -242,8 +242,8 @@ fe0e14e3
00441413
014469b3
01300533
000018B7
00A8A023
02200893
00000073
fe0416e3
415a0a33
fe0a10e3
@@ -1,6 +1,6 @@
// 与 RTL 文件(rtl/cpu21_*.v)一起编译,仿真相关文件都在 testbench/ 下。
// 与 RTL 文件(cpu21_riscv_*.v、cpu21_bpb_8.v)一起编译。
// 示例:
// iverilog -g2012 -o cpu21_sim rtl/*.v testbench/tb_cpu21_riscv_redirect_int_bpb.v
// iverilog -g2012 -o cpu21_sim cpu21_*.v tb_cpu21_riscv_redirect_int_bpb.v
// vvp cpu21_sim
// 仿真会写出 cpu21_riscv_redirect_int_bpb.vcd,可供 GTKWave 查看。
//
@@ -8,13 +8,9 @@
// 只会打印警告并保持内存原样,所以路径写错会让 ROM 看起来全是 NOP
// (0x00000013)。仿真器的工作目录并不固定(Vivado xsim 在
// <proj>.sim/sim_1/behav/xsim 下运行),因此本测试平台会依次探测若干候选
// 路径(testbench/rom/、../testbench/rom/、…,相对仓库根目录),并报告实际
// 命中的那个。显式给出的路径始终优先:
// 路径,并报告实际命中的那个。显式给出的路径始终优先:
// vvp cpu21_sim +ROM_FILE=<path> (iverilog/vvp)
// xelab ... -generic_top "ROM_FILE=<path>" (Vivado xsim)
// 注意:PowerShell 会把命令行参数里的 "=" 拆开(xelab/xsim 报
// "Expected a switch but found …"),此时直接改本文件的 ROM_FILE 参数即可;
// ROM 镜像都在 testbench/rom/ 下,路径请用正斜杠(Verilog 字符串里 "\" 是转义符)。
module tb_cpu21_riscv_redirect_int_bpb #(
parameter ROM_FILE = "cpu21_riscv_redirect_int_bpb_rom.hex"
// risc-v-benchmark_ccab.hex
@@ -156,8 +152,7 @@ module tb_cpu21_riscv_redirect_int_bpb #(
if (data_addr_o[11:2] < RAM_WORDS) data_rdata_i = ram[data_addr_o[11:2]];
end
// 带字节使能的同步写模型。MMIO 区域(LED_ADDR=0x1000 起)不在本模型的
// 译码范围(data_addr_o[11:2] < 1024)内,因此对 LED 的存储不会写进 RAM。
// 带字节使能的同步写模型。
always @(posedge clk) begin
if (!reset && data_we_o && (data_addr_o[11:2] < RAM_WORDS)) begin
if (data_wstrb_o[0]) ram[data_addr_o[11:2]][7:0] <= data_wdata_o[7:0];
@@ -231,15 +226,10 @@ module tb_cpu21_riscv_redirect_int_bpb #(
rom_loaded = 1'b0;
if ($value$plusargs("ROM_FILE=%s", rom_file_path)) load_rom_image(rom_file_path, rom_loaded);
if (!rom_loaded) load_rom_image(ROM_FILE, rom_loaded);
if (!rom_loaded) load_rom_image({"testbench/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../testbench/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../testbench/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../../../testbench/rom/", ROM_FILE}, rom_loaded);
// 分享出去的 Vivado 工程里的镜像目录(工程目录/srcs/rom/),GUI 仿真的工作
// 目录是 <proj>.sim/sim_1/behav/xsim,所以需要向上 5 层。
if (!rom_loaded) load_rom_image({"srcs/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../srcs/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../../../srcs/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../../../testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded)
$display(
"WARNING: no ROM image found for \"%0s\" - the ROM stays filled with NOPs (0x00000013).",
@@ -1,9 +1,9 @@
// 仿真顶层:tb_no_intr —— 与 tb_cpu21_riscv_redirect_int_bpb.v 相同的测试框架,
// 但不注入任何中断脉冲,因此主程序可以不受打扰地连续运行。
//
// 与 RTL 文件(rtl/cpu21_*.v)一起编译,仿真相关文件都在 testbench/ 下。
// 与 RTL 文件(cpu21_riscv_*.v、cpu21_bpb_8.v)一起编译。
// 示例:
// iverilog -g2012 -o cpu21_sim rtl/*.v testbench/tb_no_intr.v
// iverilog -g2012 -o cpu21_sim cpu21_*.v tb_no_intr.v
// vvp cpu21_sim
// 仿真会写出 tb_no_intr.vcd,可供 GTKWave 查看。
//
@@ -11,13 +11,9 @@
// 只会打印警告并保持内存原样,所以路径写错会让 ROM 看起来全是 NOP
// (0x00000013)。仿真器的工作目录并不固定(Vivado xsim 在
// <proj>.sim/sim_1/behav/xsim 下运行),因此本测试平台会依次探测若干候选
// 路径(testbench/rom/、../testbench/rom/、…,相对仓库根目录),并报告实际
// 命中的那个。显式给出的路径始终优先:
// 路径,并报告实际命中的那个。显式给出的路径始终优先:
// vvp cpu21_sim +ROM_FILE=<path> (iverilog/vvp)
// xelab ... -generic_top "ROM_FILE=<path>" (Vivado xsim)
// 注意:PowerShell 会把命令行参数里的 "=" 拆开(xelab/xsim 报
// "Expected a switch but found …"),此时直接改本文件的 ROM_FILE 参数即可;
// ROM 镜像都在 testbench/rom/ 下,路径请用正斜杠(Verilog 字符串里 "\" 是转义符)。
module tb_no_intr #(
parameter ROM_FILE = "risc-v-benchmark_ccab.hex"
// risc-v-benchmark_ccab.hex
@@ -159,8 +155,7 @@ module tb_no_intr #(
if (data_addr_o[11:2] < RAM_WORDS) data_rdata_i = ram[data_addr_o[11:2]];
end
// 带字节使能的同步写模型。MMIO 区域(LED_ADDR=0x1000 起)不在本模型的
// 译码范围(data_addr_o[11:2] < 1024)内,因此对 LED 的存储不会写进 RAM。
// 带字节使能的同步写模型。
always @(posedge clk) begin
if (!reset && data_we_o && (data_addr_o[11:2] < RAM_WORDS)) begin
if (data_wstrb_o[0]) ram[data_addr_o[11:2]][7:0] <= data_wdata_o[7:0];
@@ -234,15 +229,10 @@ module tb_no_intr #(
rom_loaded = 1'b0;
if ($value$plusargs("ROM_FILE=%s", rom_file_path)) load_rom_image(rom_file_path, rom_loaded);
if (!rom_loaded) load_rom_image(ROM_FILE, rom_loaded);
if (!rom_loaded) load_rom_image({"testbench/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../testbench/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../testbench/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../../../testbench/rom/", ROM_FILE}, rom_loaded);
// 分享出去的 Vivado 工程里的镜像目录(工程目录/srcs/rom/),GUI 仿真的工作
// 目录是 <proj>.sim/sim_1/behav/xsim,所以需要向上 5 层。
if (!rom_loaded) load_rom_image({"srcs/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../srcs/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../../../srcs/rom/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded) load_rom_image({"../../../../../testbench/", ROM_FILE}, rom_loaded);
if (!rom_loaded)
$display(
"WARNING: no ROM image found for \"%0s\" - the ROM stays filled with NOPs (0x00000013).",