- programs/risc-v-intrupt-sllsb.asm/.hex: 课程原始程序(344 字,hex 已去掉 Logisim
的 v2.0 raw 头),保留原文件以便追溯
- tools/adapt_intrupt_sllsb.py: 把它适配到本 CPU 上板(只改 ROM 内容,地址不变)
- 课程用 addi a7,zero,34 + ecall 显示,本 CPU 里那是空操作 -> 每条显示点原地换成
一次调用:主程序 jal ra,DISP_DLY;ISR 用 add a7,a0,zero + jal a0,DISP_DLY_ISR
(三个 ISR 都没保存 ra,所以 ISR 版把链接寄存器放 a0、显示值放 a7)
- DISP_DLY / DISP_DLY_ISR 各 8 条,追加在 uret 之后,中断入口 0x0AC/0x240/0x3E8 不变
- 返回地址存 sp+256(越过 ISR 最多 192 字节的栈区),嵌套中断也破坏不了
- 脚本自带跳转目标自检(曾发现 ISR 侧 jal 偏移少算一条指令的问题)
- srcs/board/cpu21_board_top.v: 新增 RISC_V_INTRUPT_SLLSB_ROM 分支(ROM 选择、
CPU 分频用 CLK_DIV_INTRUPT、中断向量 0x0AC/0x240/0x3E8)
- srcs/board/risc-v-intrupt-sllsb.vh: 生成的 ROM 初始化(1024 字,程序 360 字)
- create_project.tcl: 新增 use_intrupt_sllsb 开关(默认 1,已把 ccab 置 0)
实测:Vivado 2020.2 建工程 + 综合通过(sources_1 verilog_define =
RISC_V_INTRUPT_SLLSB_ROM,synth_design Complete! 100%)
cpu21-riscv-4.circ 的 Verilog 实现
处理器顶层文件:pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v
该设计对应原电路中的“重定向流水线+中断+分支预测”部分,顶层模块为
cpu21_riscv_redirect_int_bpb。为便于单独仿真与调试,各功能单元已拆分为
独立文件,顶层的 ALU、BPB 也已移出,具体见文末“文件与模块划分”。
目录结构
risc-v/
├─ README.md 本文档
├─ pipelined-cpu/ Vivado 工程目录(也是源文件所在地)
│ ├─ create_project.tcl 一键重建工程的 Tcl 脚本
│ ├─ srcs/rtl/*.v 处理器与外设 RTL(17 个文件,cpu21_riscv_redirect_int_bpb.v 为处理器顶层)
│ │ 外设:cpu21_mmio.v(MMIO 中枢)、cpu21_vga.v(显示)、
│ │ cpu21_seg7.v(数码管)、cpu21_pointer.v(按键当鼠标)、
│ │ cpu21_ps2_mouse.v(可选 PS/2 鼠标)
│ ├─ srcs/board/*.v|*.vh 板级顶层(时钟分频 + 片内 ROM/RAM + 引脚);四份 ROM 初始化:
│ │ 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 初始化)、
│ │ 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(指令白名单校验)
仓库里不放任何 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(按需求补充的扩展指令,标准 opcode0x37)。sltiu:I 型funct3=011,立即数符号扩展后按无符号与 rs1 比较。- M 扩展乘法(
funct7=0000001):mul(乘积低 32 位)、mulh(有符号×有符号高 32 位)、mulhsu(有符号×无符号高 32 位)、mulhu(无符号×无符号高 32 位)。 - M 扩展除法:课程真值表里的
divu(funct3=101,无符号商)与remu(funct3=111,无符号余数)。 以上六条乘法/除法指令都由 多周期单元pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v完成(见“时序和使用说明”第 7 条), 而不是组合逻辑。
指令编码
表中的 opcode 是控制器使用的五位字段 IR[6:2],不是包含最低两位的 7 位原始 opcode。
因此它与标准 RISC-V 指令的低 7 位 opcode 相差右移两位。
| 指令类别 | opcode | 说明 |
|---|---|---|
| R 型 | 0x0c |
add/sub/and/or/xor/slt/sltu/sll/srl/sra |
| M 扩展(R 型) | 0x0c |
funct7=0000001:mul/mulh/mulhsu/mulhu、divu(funct3=101)、remu(funct3=111) |
| I 型 ALU | 0x04 |
addi/andi/ori/xori/slti/sltiu/slli/srli/srai |
lui(扩展) |
0x0d |
加载高位立即数,rd = imm[31:12] << 12 |
lw |
0x00 |
funct3=010 |
sw / sb |
0x08 |
funct3=010 / 000 |
beq / bne / blt / bge / bltu / bgeu |
0x18 |
funct3=000 / 001 / 100 / 101 / 110 / 111 |
jal |
0x1b |
J 型立即数 |
jalr |
0x19 |
funct3=000 |
| 系统类 | 0x1c |
ecall、URET、CSRRSI、CSRRCI |
ecall 和 URET 都是 opcode=0x1c、funct3=0,由 IR[21] 区分:
IR[21]=0 是 ecall,IR[21]=1 是 URET。这一点来自原控制器中的 IR21 分支逻辑。
MMIO 与外设
外设寄存器映射在数据存储器地址空间,由 pipelined-cpu/srcs/rtl/cpu21_mmio.v 在 MEM 级译码。
这些地址都在 4 KB 数据 RAM(0x0000–0x0FFC)之外,因此不会与 RAM 冲突。
每行的字节偏移都按字对齐译码(同一 32 位寄存器内任意字节偏移都命中)。
| 地址 | 外设 | 写 | 读 |
|---|---|---|---|
0x0000_1000 |
LED 数据寄存器 | sw 写全字;sb 按 data_wstrb_o 只改对应字节通道,led_valid_o 拉高一拍 |
lw 返回 LED 寄存器当前值 |
0x0000_1004 |
七段数码管 | sw/sb 写 8 位十六进制数(低 4 位 = 最右边那个数码管) |
lw 读回 |
0x0000_1100+ |
鼠标寄存器组 | STATUS / DATA / ACK / CTRL / INPUT / TIME / FRAME / EVCNT / LEVEL / MAGIC | 同左(DATA 读无副作用) |
0x0000_1140+ |
显示控制器 | CTRL(光标可见、背景色)/ CURSOR(列、行) | 同左(另含 MAGIC 自检值) |
0x0000_2000–0x27FF |
显存 | 40 列 × 30 行、每格 1 字节(字形 + 颜色),写端口接到板级顶层的双口 RAM | lw 组合读回 |
其中 LEVEL(0x1120)是一个软件中断寄存器:写 1 会拉起 irq_i[1],用来让程序
"跳到关卡入口";MAGIC(0xCAFE0001/0xCAFE0002)可以读来自检 MMIO 是否活着。
鼠标事件 FIFO(深度 16)配合 mouse_irq_o 提供"事件到达即中断",isr_body 读空 FIFO 后
中断线自动回 0,因此不会丢事件。
地址译码按字对齐:同一 32 位寄存器内的任意字节偏移(如 0x1001)都算命中,
具体修改哪几个字节由 data_wstrb_o 决定。data_addr_o 在命中 MMIO 时不会
访问外部 RAM;读数据由 cpu21_mmio 的输出在顶层与 data_rdata_i 做选择。
典型用法(LED 写 0x12345678,可用 pipelined-cpu/tools/rv_asm.py 汇编):
lui t0, 0x1 # t0 = LED_ADDR = 0x00001000
lui t1, 0x12345
addi t1, t1, 0x678 # t1 = 0x12345678
sw t1, 0(t0) # MMIO 写 LED -> LED 输出 12345678
lw a0, 0(t0) # MMIO 读回
pipelined-cpu/programs/mmio_test.asm(期望值见 mmio_test.expected)覆盖了
全字写、读回、字节写三种情况。加入新外设(拨码开关、UART 等)时只需在
pipelined-cpu/srcs/rtl/cpu21_mmio.v 里扩展地址译码,处理器流水线无需改动。
顶层端口约定
instr_i:当前instr_addr_o对应的 32 位指令。原 Logisim 中由 ROM 提供。data_rdata_i:当前data_addr_o的读数据。原 Logisim 中由 MIPS RAM 提供。data_addr_o、data_wdata_o、data_wstrb_o、data_we_o:数据存储器接口。地址按字节地址输出;若使用原 32 位 RAM,可用data_addr_o[11:2]作为字地址。irq_i[0]、irq_i[1]、irq_i[2]:1、2、3 号中断源;输入应为脉冲或边沿信号,模块内部会同步并锁存挂起状态。go_i:对应原电路的“继续运行”按键。if_pc_o、id_pc_o、ex_pc_o、mem_pc_o、wb_pc_o:五级流水当前保存的 PC,气泡时输出 0,便于替代原电路的调试探针。rdin_o、mdin_o、reg_write_o、mem_write_o:对应原电路的寄存器写回和存储器写调试输出。cycle_count_o、stall_count_o、bubble_count_o、conditional_taken_count_o、unconditional_branch_count_o、prediction_success_count_o、prediction_failure_count_o:对应原电路和课程要求的周期、停顿/气泡、分支和动态预测统计量。
时序和使用说明
reset为高时异步清空流水线、寄存器文件、BPB、挂起中断和中断栈,PC 置为RESET_PC;ustatus复位为参数USTATUS_INIT,默认0x0000_0001(MIE 上电即使能)。这样做的原因是课程的中断测试程序只在中断处理程序的“保护现场”之后写 MIE,主流程不会开中断;若不默认使能,第一次中断只会挂在irq_pending_o上而永不响应。参数可覆盖,例如把USTATUS_INIT设为32'h0即可恢复“上电关中断”的行为。- 指令存储器按组合读处理;数据存储器读数据在一个 MEM 周期内返回。若 RAM 为同步读,需要在外部增加一级数据寄存或相应调整 MEM/WB。
- 响应中断时允许当前 EX 指令完成,并保存下一条尚未执行指令的 PC;若 EX 是已正确预测的跳转分支,则保存实际目标地址。EX 为空时优先保存 IF/ID 中尚未执行的 PC。这样
URET不会重复执行已经提交的存储或寄存器写操作。 - 默认 BPB 新表项采用弱不跳转/弱跳转状态:首次更新为不跳转时计数器为
01,首次更新为跳转时为10;命中后按 2 位饱和规则增减。 bpb_predict_hit_o、bpb_predict_taken_o、bpb_mispredict_o和irq_*_o是观测端口,不参与外部控制,可以直接接逻辑分析仪或测试平台。cycle_count_o在halted_o为高时暂停;load-use 每次计入一个气泡,重定向或中断清空 IF/ID 与当前 IF 时计入两个气泡。- 乘除法是多周期指令:结果算完之前
ex_md_stall有效,ID/EX 保持(指令继续占用 EX)、EX/MEM 插气泡、取指/译码冻结,同时屏蔽中断(避免结果未定型就写回)。一次乘/除共占用 34 拍(1 拍装载 + 32 拍迭代 + 1 拍定型),这些等待周期也计入stall_count_o/bubble_count_o。乘除法迭代期间不会把错误值写进 rd,也不会因为中断被打断。
原 .circ 文件中的 ROM、MIPS RAM、按钮、LED 和调试显示器属于 Logisim 外围,不直接搬入 RTL;其功能分别由指令接口、数据接口、go_i、MMIO(led_data_o/led_valid_o)和调试端口替代。
文件与模块划分
| 文件 | 模块 | 职责 |
|---|---|---|
pipelined-cpu/srcs/rtl/cpu21_riscv_redirect_int_bpb.v |
cpu21_riscv_redirect_int_bpb |
顶层:五级流水寄存器、子模块例化、PC/IF-ID/ID-EX 控制、对外输出 |
pipelined-cpu/srcs/rtl/cpu21_riscv_alu.v |
cpu21_riscv_alu |
组合 ALU(加减/移位/逻辑/比较,不做乘除) |
pipelined-cpu/srcs/rtl/cpu21_riscv_muldiv.v |
cpu21_riscv_muldiv |
多周期乘除法单元:32 拍移位迭代,mul/mulh/mulhsu/mulhu/divu/remu |
pipelined-cpu/srcs/rtl/cpu21_bpb_8.v |
cpu21_bpb_8 |
8 项全相联分支目标缓冲(2 位饱和计数器 + 近似 LRU) |
pipelined-cpu/srcs/rtl/cpu21_riscv_decoder.v |
cpu21_riscv_decoder |
ID 级组合译码器(操作码IR[6:2]) |
pipelined-cpu/srcs/rtl/cpu21_riscv_regfile.v |
cpu21_riscv_regfile |
寄存器堆,x0 恒为 0,同周期 WB 写优先旁路 |
pipelined-cpu/srcs/rtl/cpu21_riscv_forward_unit.v |
cpu21_riscv_forward_unit |
EX 级前递网络(EX/MEM 优先于 MEM/WB) |
pipelined-cpu/srcs/rtl/cpu21_riscv_store_unit.v |
cpu21_riscv_store_unit |
EX 级存储数据对齐:sw/sb 的写数据与字节写掩码 |
pipelined-cpu/srcs/rtl/cpu21_riscv_branch_unit.v |
cpu21_riscv_branch_unit |
EX 级控制流裁决:分支方向/目标、JAL/JALR 重定向 |
pipelined-cpu/srcs/rtl/cpu21_riscv_hazard_unit.v |
cpu21_riscv_hazard_unit |
load-use 冒险、停顿(stall)与冲刷(flush)判定 |
pipelined-cpu/srcs/rtl/cpu21_riscv_irq_ctrl.v |
cpu21_riscv_irq_ctrl |
中断采样与优先级、ustatus/uepc CSR、嵌套返回栈 |
pipelined-cpu/srcs/rtl/cpu21_riscv_perf_counters.v |
cpu21_riscv_perf_counters |
周期/停顿/气泡/分支/预测成功率等统计计数器 |
pipelined-cpu/srcs/rtl/cpu21_mmio.v |
cpu21_mmio |
MMIO 中枢:LED、七段数码管、鼠标寄存器组 + 事件 FIFO + 中断、显示控制器、显存窗口 |
pipelined-cpu/srcs/rtl/cpu21_vga.v |
cpu21_vga |
640x480@60 显示控制器:按"格子"扫描,8x8 字模放大 2 倍填满 16x16 的格子 |
pipelined-cpu/srcs/rtl/cpu21_seg7.v |
cpu21_seg7 |
板载 8 位数码管动态扫描驱动(共阳极,位选/段选低有效) |
pipelined-cpu/srcs/rtl/cpu21_pointer.v |
cpu21_pointer |
把 5 向按键变成"鼠标"事件(去抖 + 事件排队,单位 1/8 格) |
pipelined-cpu/srcs/rtl/cpu21_ps2_mouse.v |
cpu21_ps2_mouse |
可选的真鼠标:PS/2 接收(11 位帧 + 奇校验 + 3 字节包解析),只实现接收 |
pipelined-cpu/srcs/board/cpu21_board_top.v |
cpu21_board_top |
板级顶层:时钟分频、复位同步、片内 ROM/RAM、显存双口 RAM、VGA/数码管/鼠标/中断连线 |
pipelined-cpu/srcs/board/cpu21_saolei_rom.vh |
— | 扫雷程序的 ROM 初始化(由saolei/tools/build.ps1 生成) |
pipelined-cpu/srcs/constrs/cpu21_board_top.xdc |
— | Nexys4DDR 引脚约束 + 分频时钟(create_generated_clock) |
pipelined-cpu/srcs/board/cpu21_board_rom.vh |
— | 片内 ROM 内容(生成文件,rom[i] = 32'h…;) |
pipelined-cpu/srcs/tb/tb_board_top.v |
tb_board_top |
板级顶层的 RTL 仿真(观察 LED 跑马灯与写入指示) |
pipelined-cpu/srcs/tb/tb_no_intr.v |
tb_no_intr |
无中断测试平台(指令集/MMIO 测试,默认加载 benchmark 镜像) |
pipelined-cpu/srcs/tb/tb_cpu21_riscv_redirect_int_bpb.v |
tb_cpu21_riscv_redirect_int_bpb |
中断与分支预测测试平台(默认加载中断测试镜像) |
pipelined-cpu/tools/rv_asm.py |
— | 迷你汇编器:.asm → 机器码 hex(.lst 带反汇编清单) |
pipelined-cpu/tools/hex2vh.py |
— | hex →pipelined-cpu/srcs/board/cpu21_board_rom.vh(rom[i] = 32'h…; 初始化语句) |
拆分只调整了模块边界与文件组织,端口、时序与行为完全等价:使用同一 ROM 程序 在 xsim 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支 跳转、预测成败)完全一致。
各模块的实现方式
下面按“处理器流水线 → 外设 → 板级”的顺序说明每个模块的实现要点(对应上面
“文件与模块划分”表)。全部是可综合的纯 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、计数器、
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 | 三路中断源,拨动即触发(内部同步并锁存) |
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 |
时钟:分频比怎么选(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
内部路径而误报大量违例:
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 分支走。
怎么定义宏
-
Vivado GUI:
Project→Settings→Synthesis→Verilog Options→Verilog Macros里填RISC_V_INTRUPT_CCAB_ROM(只对sources_1有效), 确定后Reset Synthesis Run再综合。也可以直接在 Tcl Console 里写: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]。 -
一键重建:改
create_project.tcl里的use_intrupt_ccab/use_saolei两个开关(同一时间只开一个),重新source create_project.tcl。 -
命令行仿真:
xvlog与xelab都要带同一个-d,并用-i指向头文件目录: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 -
注意:这些宏只影响“综合用”的
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):
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
六处适配(脚本内每步都有断言,输入不对会直接报错):
- 三个 ISR 里的
addi a7,zero,10+ecall→ 两条nop(a7 != 34的ecall会让这颗 CPU 停机等go_i); - 11 个显示点
lui a7,1+sw a0,0(a7)→jal调用追加的显示例程; 顺带纠正早期手改留下的sb误码(只写低字节会显示错值); - 追加主程序用的
DISP_DLY(0x438):写 LED 寄存器(数码管显示它)+ 约 10 ms 延时;返回地址存在sp+256,位于 ISR 压栈区(最多 192 B)之上,所以中断 随时打断延时也不会破坏它; - 修正 ISR2/ISR3 出栈顺序写乱的 12 条
lw(否则 ISR2 的循环计数会变成 -3823,看起来像“二级中断返回之后卡住”); - ISR3 的 SB 测试改用 ISR3 自己保存的
s4/s5/s6(原来用未保存的t1/t3/s2, 抢占 ISR1 时会毁掉 ISR1 的 DIVU 循环计数,造成死循环); - 追加 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(否则一次中断就是几十毫秒仿真时间)。
演示程序的生成与仿真
# 在工程目录 pipelined-cpu/ 下执行:
# 1) 汇编 + 生成 ROM 初始化文件
python tools/rv_asm.py programs/board_demo.asm _tmp/board_demo.hex
python tools/hex2vh.py _tmp/board_demo.hex srcs/board/cpu21_board_rom.vh 1024
# 2) 上板顶层的 RTL 仿真(默认程序延时约 20 ms,只需确认第一次写入 LED=0001)
xvlog srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim
xsim board_sim -tclbatch run_quit.tcl
board_demo.asm 是一段 16 步跑马灯(0001 → 0002 → … → 8000 → 0001),
每步之间用软件循环延时约 20 ms(50 MHz 下)。仓库里同时提供了两份 ROM 初始化
文件,由 BOARD_ROM_FAST 宏选择(综合流程不会定义该宏):
| 文件 | 用途 | 每步耗时 |
|---|---|---|
pipelined-cpu/srcs/board/cpu21_board_rom.vh |
综合/上板(默认) | 约 20 ms |
pipelined-cpu/srcs/board/cpu21_board_rom_fast.vh |
仅仿真:一次跑完整个跑马灯序列 | 2.6 µs |
# 快速版仿真:编译时加上 -d BOARD_ROM_FAST(xvlog 与 xelab 都要带)
xvlog -d BOARD_ROM_FAST srcs/rtl/*.v srcs/board/cpu21_board_top.v srcs/tb/tb_board_top.v
xelab -top tb_board_top -snapshot board_sim_fast -d BOARD_ROM_FAST
两份 ROM 初始化文件由 programs/board_demo.asm(默认,≈ 20 ms/步)与
programs/board_demo_fast.asm(快速版,≈ 2.6 µs/步,只差延时循环的初值)生成:
python tools/rv_asm.py programs/board_demo_fast.asm _tmp/board_demo_fast.hex
python tools/hex2vh.py _tmp/board_demo_fast.hex srcs/board/cpu21_board_rom_fast.vh 1024
Vivado 工程设置与实现结果
Vivado 工程 pipelined-cpu/pipelined-cpu.xpr 被 .gitignore 排除,需要手动设置:
sources_1顶层设为cpu21_board_top(纯 RTL 仿真时也可切回cpu21_riscv_redirect_int_bpb);- 加入
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(不能当普通源码,否则会被当成顶层候选); - 三个 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就加载扫雷程序,不定义仍是下面的跑马灯。
回归测试
工程里带了一个 xsim 回归脚本,在 pipelined-cpu/ 目录下运行:
powershell -ExecutionPolicy Bypass -File tools\run_regress.ps1
它会按顺序做四件事:
- 编译
srcs/rtl/下的处理器源码; - 跑 4 个指令测试(
muldiv_test、isa_test、lui_test、mmio_test),把测试平台 打印的 LED 序列与programs/*.expected逐值比对; - 跑两个流水基线(
tb_no_intr、tb_cpu21_riscv_redirect_int_bpb),打印 FINAL 统计行; - 跑板级顶层(
-d BOARD_ROM_FAST),检查跑马灯 LED 序列。
最后汇总 PASS/FAIL,并以失败数作为退出码。编译与仿真的中间文件都写在
%TEMP%\cpu21_regress 下,不会污染工程目录;需要 xvlog/xelab/xsim 在 PATH 上。
期望结果(2026-09,多周期乘除法 + CLK_DIV=2):
| 测试 | 期望 |
|---|---|
| muldiv_test / isa_test / lui_test / mmio_test | 13 / 24 / 4 / 5 个 LED 值全部匹配 |
| tb_no_intr(benchmark,5560 周期) | stalls=1528 bubbles=1830 cond_taken=639 uncond=71 pred_ok=687 pred_fail=80 |
| tb_cpu21_riscv_redirect_int_bpb(中断,6003 周期) | stalls=963 bubbles=1205 cond_taken=547 uncond=28 pred_ok=557 pred_fail=87 |
| 板级(快速 ROM) | 跑马灯0001 → … → 8000 循环,每步约 2.6 µs |
工程分享(Tcl 一键重建)
pipelined-cpu/ 已整理成“自包含 + 可直接分享”的工程目录,设计文件都在里面,
别人拿到整个文件夹后不需要改任何路径:
pipelined-cpu/
├─ create_project.tcl 一键重建脚本(用 [info script] 定位自身目录)
├─ srcs/rtl/*.v 处理器与外设 RTL(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):
cd <pipelined-cpu 所在目录>
source create_project.tcl
脚本会就地创建 pipelined-cpu.xpr,并配好:
sources_1顶层cpu21_board_top(处理器 RTL + 板级顶层 + 片内 ROM 初始化,cpu21_board_rom.vh标记为Verilog Header);constrs_1:cpu21_board_top.xdc(引脚 +CLK_DIV分频时钟)与板级母版;sim_1(顶层tb_cpu21_riscv_redirect_int_bpb)与sim_2(顶层tb_no_intr); 测试平台会依次探测 ROM 镜像,其中../../../../../srcs/rom/对应 GUI 仿真的 工作目录(<proj>.sim/sim_1/behav/xsim),所以创建完就能直接Run Simulation;- 两个仿真 fileset 的
xsim.simulate.runtime设为all,避免 GUI 仿真只跑 1000 ns 就停下(看起来像卡死)。
分享前建议删掉生成目录:*.xpr、*.runs、*.sim、*.cache、*.hw、*.gen、
*.ip_user_files、.Xil。实测只带 create_project.tcl + srcs/ + pipelined-cpu/tools/ +
programs/ + 说明文档约 0.3 MB,在任意路径重建工程并综合成功。
源文件只有一份,就在
pipelined-cpu/srcs/(tools/、programs/同理), 直接在工程目录里改即可,没有需要同步的副本。
也可以用 Vivado 自动生成:
write_project_tcl -force -paths_relative_to <工程目录> <工程目录>/create_project.tcl
注意自动生成的版本会把源路径写成生成时的绝对路径、并且要求 origin_dir 指向
工程目录,所以要手动改两行才能给别人用:
set origin_dir [file dirname [file normalize [info script]]]
create_project ${_xil_proj_name_} $origin_dir -part xc7a100tcsg324-1