793 lines
63 KiB
Markdown
793 lines
63 KiB
Markdown
# `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
|
||
```
|