feat(board): 中断综合测试程序上板 + 可变分频/刷新率;README 大幅扩写

板级顶层(srcs/board/cpu21_board_top.v)
- ROM 增加 RISC_V_INTRUPT_CCAB_ROM 分支,中断入口 0x0AC/0x1E4/0x308;
  与 SAOLEI_ROM / BOARD_ROM_FAST / 默认跑马灯构成 ifdef 链
- 新增参数 CLK_DIV_INTRUPT:跑中断测试程序时用更慢的 CPU 时钟(默认 32
  = 3.125 MHz),让数码管上每个数值停留约 160 ms 便于观察
- 数码管扫描分频随 CPU 分频缩放(SEG_REFRESH_DIV = 100000/CPU_DIV),
  保证每位数码管仍是 125 Hz(不闪);数码管改显示 MMIO 的 led_data;
  LED[2:0] 改为锁存式中断指示(请求上升沿点亮、对应 ISR 执行 uret 后熄灭)
- 复位同步器/led_we 指示等注释同步更新;srcs/rtl/cpu21_seg7.v 分频计数
  加宽到 20 位以容纳随主频变化的 REFRESH_DIV

工具与程序
- tools/adapt_intrupt_ccab.py:把课程中断测试镜像适配成上板 ROM。六处适配
  (ISR 停机 ecall、显示改 MMIO+DISP_DLY、ISR2/ISR3 出栈顺序、ISR3 SB 测试
  用未保存寄存器、ISR 破坏 ra 的 DISP_DLY_ISR),全部 2 字换 2 字,
  不移动任何指令地址;带断言与 jal 目标自检
- programs/risc-v-intrupt-ccab.asm/.hex:课程中断综合测试程序(含六处适配说明)
- create_project.tcl:新增 use_intrupt_ccab / use_saolei 开关,自动把对应
  ROM 头文件按 Verilog Header 加入 sources_1 并定义宏
- tools/rars_f0c874c.jar:课程 RARS 汇编器移到 tools/

文档(README.md)
- 新增"各模块的实现方式":按流水线主体 / 外设 / 板级逐模块讲实现要点
- 新增"内置程序与宏的用法":ROM 宏表(含各自的中断入口)、GUI/Tcl/命令行
  三种定义方式、中断程序的适配流程与仿真验证方法
- 新增"分频与刷新率一览":CLK_DIV / CLK_DIV_INTRUPT / SEG_REFRESH_DIV /
  led_we 时长 / DISP_DLY 延时常量分别在哪里改、怎么换算
- 修正引脚表(LED[2:0] 中断指示、led_we 0.34 s、CPU_DIV)与 ROM/RAM 说明
  (ROM 8192 字 = 32 KB),补充中断程序上板现象与 ROM 头文件清单

验证:不带宏 / 带 RISC_V_INTRUPT_CCAB_ROM 两种配置 xvlog 编译均无错误;
三路嵌套与 IRQ3 抢占 ISR1 的仿真通过(level 逐级升降、uepc 正确恢复、
halted=0);tools/check_rtl_text.ps1 体检 OK。
This commit is contained in:
2026-09-19 15:44:47 +08:00
parent 4f82331dbe
commit f7844b7b1c
11 changed files with 2702 additions and 890 deletions
+252 -26
View File
@@ -17,12 +17,19 @@ risc-v/
│ │ 外设:cpu21_mmio.v(MMIO 中枢)、cpu21_vga.v(显示)、
│ │ cpu21_seg7.v(数码管)、cpu21_pointer.v(按键当鼠标)、
│ │ cpu21_ps2_mouse.v(可选 PS/2 鼠标)
│ ├─ srcs/board/*.v|*.vh 板级顶层(时钟分频 + 片内 ROM/RAM + 引脚)、片内 ROM 初始化
│ ├─ srcs/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 三个测试平台
│ ├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像
│ ├─ tools/ rv_asm.py(迷你汇编器)、hex2vh.py(hex → ROM 初始化)、run_regress.ps1(一键回归)
│ └─ programs/ 汇编源程序、期望值、课程原始资产(RARS)
│ ├─ 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 汇编器)
│ └─ 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(指令白名单校验)
@@ -160,7 +167,7 @@ risc-v/
| `pipelined-cpu/srcs/rtl/cpu21_pointer.v` | `cpu21_pointer` | 把 5 向按键变成"鼠标"事件(去抖 + 事件排队,单位 1/8 格) |
| `pipelined-cpu/srcs/rtl/cpu21_ps2_mouse.v` | `cpu21_ps2_mouse` | 可选的真鼠标:PS/2 接收(11 位帧 + 奇校验 + 3 字节包解析),只实现接收 |
| `pipelined-cpu/srcs/board/cpu21_board_top.v` | `cpu21_board_top` | 板级顶层:时钟分频、复位同步、片内 ROM/RAM、显存双口 RAM、VGA/数码管/鼠标/中断连线 |
| `pipelined-cpu/srcs/board/cpu21_saolei_rom.vh` | — | 扫雷程序的 ROM 初始化(由 `saolei/tools/build.ps1` 生成) |
| `pipelined-cpu/srcs/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 跑马灯与写入指示) |
@@ -173,6 +180,89 @@ risc-v/
在 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 位帧
移位进来(起始位 + 8 数据 + 奇校验 + 停止位),校验后按 3 字节包解析出
dx/dy/按键,产生 `ev_valid_o` 脉冲。
### 板级
- **`cpu21_board_top`**:把这些接起来并裁到 23 个引脚。内含参数化分频器
(`CLK_DIV` / `CLK_DIV_INTRUPT`)、复位同步器(异步置位 + 同步释放)、片内
ROM(组合读、`nop` 填充、`ifdef` 链选程序)与 RAM(字节写使能 + 组合读、
只响应 4 KB 以内地址,避免和 MMIO 别名)、显存双口 RAM(CPU 写口 + VGA 读口
同处一个时钟域,用像素使能而不是第二时钟)、数码管、VGA、鼠标源选择
(`USE_PS2_MOUSE`)、中断连线(SW0/1/2 分别与鼠标事件/关卡软件中断相或)、
锁存式中断指示 LED(请求上升沿置位、对应级别的 ISR 执行 `uret` 后熄灭)和
`led_we` 写入指示。
## 上板(Nexys4DDR + Vivado)
处理器顶层 `cpu21_riscv_redirect_int_bpb` 有 775 位调试端口(各级 PC/IR、计数器、
@@ -180,20 +270,22 @@ 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。
其余调试端口不连接。定义宏 `SAOLEI_ROM` 时还会把扫雷程序固化进 ROM;当前
`create_project.tcl` 默认定义的是 `RISC_V_INTRUPT_CCAB_ROM`(跑课程的
中断综合测试程序),宏的含义、优先级和改法见下文“内置程序与宏的用法”。
### 引脚分配(`cpu21_board_top.xdc`)
| 端口 | 引脚 | 板上元件 | 说明 |
| ------------- | ----------- | ------------------------- | ---------------------------------------------- |
| `clk100` | E3 | 100 MHz 晶振 | 经`CLK_DIV` 分频后给 CPU |
| `rst_n` | C12 | 按键 CPU_RESETN(低有效) | 异步置位、与 CPU 时钟同步释放 |
| `go` | N17 | 按键 BTNC | 对应原电路“继续运行”(`ecall` 停机后恢复执行) |
| `irq_sw[2:0]` | J15/L16/M13 | 拨码开关 SW0–SW2 | 三路中断源,拨动即触发(内部同步并锁存) |
| `led[15:0]` | H17…V11 | LED0–LED15 | MMIO(`LED_ADDR`)写入值的低 16 位 |
| `led_we` | R12 | LED16_B | MMIO 写入指示,每写一次拉长约 2.7 s |
| 端口 | 引脚 | 板上元件 | 说明 |
| ------------- | ----------- | ------------------------- | ------------------------------------------------------------------- |
| `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 | 三路中断源,拨动即触发(内部同步并锁存) |
| `led[15:0]` | H17…V11 | LED0–LED15 | `LED[2:0]` = 三路中断指示(锁存,ISR 返回后熄灭),`LED[15:3]` 熄灭 |
| `led_we` | R12 | LED16_B | MMIO 写入指示,每写一次拉长约 0.34 s @50 MHz |
### 时钟:为什么还保留 2 分频
### 时钟:分频比怎么选(`CLK_DIV` / `CLK_DIV_INTRUPT`)
乘除法原本是 ALU 里的组合运算(`a*b`、`a%b`),实测关键路径约 94 ns(325 级逻辑、
286 个 CARRY4),在 100 MHz 下必然不收敛,所以之前只能把时钟 16 分频到 6.25 MHz。
@@ -221,19 +313,135 @@ create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 2 [get_pins { clk_div_reg/Q }]
```
改分频比时 RTL 的 `CLK_DIV` 与 XDC 的 `-divide_by` 要同时改。
改分频比时:**变慢**(分频比变大)不用动 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 = 1024` 字(4 KB),组合读,取指地址 `instr_addr[11:2]`;
内容由 `` `include "cpu21_board_rom.vh" `` 给出(与 `cpu21_board_top.v` 同目录,
文件里是 1024 条
`rom[i] = 32'h…;`,放在 `initial` 里,等价于 `$readmemh`,且不依赖文件路径)。
- 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
@@ -278,8 +486,9 @@ Vivado 工程 `pipelined-cpu/pipelined-cpu.xpr` 被 `.gitignore` 排除,需要
1. `sources_1` 顶层设为 `cpu21_board_top`(纯 RTL 仿真时也可切回
`cpu21_riscv_redirect_int_bpb`);
2. 加入 `pipelined-cpu/srcs/board/cpu21_board_top.v`、`pipelined-cpu/srcs/constrs/cpu21_board_top.xdc`,
`pipelined-cpu/srcs/board/cpu21_board_rom.vh` 的文件类型设成 `Verilog Header`(不能当普通源码,
否则会被当成顶层候选);
四份 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`。
@@ -297,8 +506,25 @@ Vivado 工程 `pipelined-cpu/pipelined-cpu.xpr` 被 `.gitignore` 排除,需要
CPU 时钟 6.25 MHz → 50 MHz(快 8 倍),并且乘除法不再位于关键路径上。
上板操作:用 Vivado 打开工程 → `Open Hardware Manager` → `Program Device`,选上述
`.bit`。复位后跑马灯立即从 `0001` 开始逐位左移(演示程序不执行 `ecall`,所以不用
按 `go`;`go` 只对执行了 `ecall` 停机、等待继续运行的程序有意义),LED16_B 每次写入时点亮。
`.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/`)
@@ -342,7 +568,7 @@ powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
| 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 |
| 板级(快速 ROM) | 跑马灯`0001 → … → 8000` 循环,每步约 2.6 µs |
## 工程分享(Tcl 一键重建)
@@ -352,8 +578,8 @@ powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
```text
pipelined-cpu/
├─ create_project.tcl 一键重建脚本(用 [info script] 定位自身目录)
├─ srcs/rtl/*.v 处理器 RTL(13 个文件)
├─ srcs/board/*.v|*.vh 板级顶层、片内 ROM 初始化(含 __FAST 仿真版本)
├─ srcs/rtl/*.v 处理器与外设 RTL(17 个文件)
├─ srcs/board/*.v|*.vh 板级顶层、四份 ROM 初始化(跑马灯/中断测试/扫雷/仿真快版)
├─ srcs/constrs/*.xdc 引脚约束 + 板级母版
├─ srcs/tb/*.v 三个测试平台
├─ srcs/rom/*.hex 测试平台加载的 ROM 镜像