上板支持:Nexys4DDR 顶层、片内 ROM/RAM 与 MMIO 跑马灯演示

- 新增 cpu21_board_top.v:上板顶层。处理器顶层暴露了 775 位调试端口,直接作为
  实现顶层会因 IO 超限无法布局布线([Place 30-415] IO Placement failed due to
  overutilization),本模块只引出板上真正需要的 23 个引脚
  (clk100 / rst_n / go / irq_sw[2:0] / led[15:0] / led_we)。
  * 100 MHz 经 CLK_DIV=16 分频到 6.25 MHz:ALU 的除法/取余是组合电路,
    100 MHz 下 WNS=-84.126 ns(325 级逻辑)无法收敛,分频后 WNS=+6.083 ns。
  * 分频时钟由 Vivado 自动插入 BUFG;RTL 中不例化原语,以便跨工具仿真。
  * 复位同步器、片内 ROM(1024x32,组合读,`include cpu21_board_rom.vh`)、
    片内 RAM(1024x32,字节写使能,只响应地址 <4KB,避免与 MMIO 地址别名)。
- 新增 cpu21_board_top.xdc:Nexys4DDR 引脚约束 + create_generated_clock。
- 新增 tools/hex2vh.py:把 hex 机器码转成 rom[i] = 32'h...; 初始化包含文件。
- 新增 testbench/programs/board_demo.asm 与 testbench/tb_board_top.v:
  16 步跑马灯演示程序(通过 MMIO 写 LED)。仿真验证:默认 ROM 下首次写入
  LED=0001,快速 ROM 下完整序列 0001..8000 循环。
- 新增 cpu21_board_rom.vh / cpu21_board_rom_fast.vh,由 BOARD_ROM_FAST 宏选择。
- tools/rv_asm.py:读源文件时兼容带 BOM 的 UTF-8。
- 测试资源:补齐 lui_test.expected;修正 mmio_test.expected(print 由
  lui+sw 实现,本身就是一次 LED 写,共 5 次)。三个回归全部通过:
  isa_test 24/24、lui_test 4/4、mmio_test 5/5。
- .gitignore:忽略 _tmp/、_vivado/、*.vcd、*.pb、*.hex.lst。
- 说明文档:模块划分表补上上板相关模块,新增“上板(Nexys4DDR + Vivado)”
  章节(引脚分配、分频原因、ROM/RAM 组织、ROM 生成流程、Vivado 工程设置、
  实现结果)。

实现结果:synth_design / route_design / write_bitstream 全部完成,
LUT 5400 (8.52%)、LUTRAM 512、FF 2507、DSP 12、IOB 23 (10.95%),
bit 文件为 pipelined-cpu/pipelined-cpu.runs/impl_1/cpu21_board_top.bit
(Vivado 工程目录被 .gitignore 排除,不入库)。
This commit is contained in:
2026-09-13 17:37:44 +08:00
parent 192e28795a
commit 1ae6fe6ebe
17 changed files with 2676 additions and 8 deletions
+9
View File
@@ -1 +1,10 @@
pipelined-cpu/*
# 本地临时目录(仿真脚本、日志、中间文件)
_tmp/
_vivado/
# 仿真/综合中间产物
*.vcd
*.pb
*.hex.lst
+1026
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+175
View File
@@ -0,0 +1,175 @@
`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)
// rst_n (C12) -> 复位按键 CPU_RESETN,低有效,经复位同步器
// go (N17) -> 按键 BTNC:ecall 停机后按下继续运行
// irq_sw[2:0] -> 拨码开关 SW[2:0]:3 路中断源
// led[15:0] -> 板载 LED[15:0]:显示 MMIO(LED_ADDR)写入值的低 16 位
// led_we (R12) -> LED16_B:有 MMIO 写入时亮,并拉长约 0.17 s 便于观察
//
// 片内存储器:
// 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 = 1024,
parameter RAM_WORDS = 1024,
parameter [31:0] USTATUS_INIT = 32'h0000_0001,
// 板载 100 MHz 分频给 CPU。ALU 里的除法/取余是组合电路(实测约 94 ns),
// 100 MHz 下无法收敛;默认 16 分频(6.25 MHz,周期 160 ns)留出足够余量。
parameter integer CLK_DIV = 16
) (
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]
output wire [15:0] led, // LED[15:0]
output wire led_we // LED16_B:MMIO 写入指示
);
localparam ROM_AW = $clog2(ROM_WORDS);
localparam RAM_AW = $clog2(RAM_WORDS);
wire arst = ~rst_n; // 异步复位(高有效)
// ---------------------------------------------------------------------------
// 时钟分频:100 MHz / CLK_DIV,经 BUFG 走全局时钟网络
// (XDC 中用 create_generated_clock 声明该分频时钟,否则时序分析会按
// 100 MHz 的 10 ns 要求去约束 CPU 内部路径而报大量违例)
// ---------------------------------------------------------------------------
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 == (CLK_DIV / 2 - 1)) begin
clk_cnt <= 16'd0;
clk_div <= ~clk_div;
end else begin
clk_cnt <= clk_cnt + 16'd1;
end
end
// 不直接例化 BUFG 原语,保持 RTL 可跨工具仿真;
// 综合/实现时 Vivado 会为这条时钟网络自动插入 BUFG(全局时钟缓冲)。
wire clk_cpu = clk_div;
// ---------------------------------------------------------------------------
// 复位同步器(在 CPU 时钟域):按键异步置位、释放与 clk_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 [31:0] led_data;
wire led_valid;
// ---------------------------------------------------------------------------
// 指令 ROM:组合读
// ---------------------------------------------------------------------------
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 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)
) u_cpu (
.clk (clk_cpu),
.reset (reset),
.instr_i (instr_word),
.data_rdata_i(data_rdata),
.irq_i (irq_sw),
.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)
);
// ---------------------------------------------------------------------------
// 板载 LED:显示 MMIO 写入值的低 16 位;写入指示拉长便于观察
// ---------------------------------------------------------------------------
assign led = led_data[15:0];
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; // 约 2.7 s @6.25 MHz(分频后的 CPU 时钟)
else if (led_we_cnt != 24'd0) led_we_cnt <= led_we_cnt - 24'd1;
end
assign led_we = (led_we_cnt != 24'd0);
endmodule
`default_nettype wire
+45
View File
@@ -0,0 +1,45 @@
## 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(=16) 分频
## (与 cpu21_board_top.v 的 CLK_DIV 保持一致;改分频比时这里也要改)
create_generated_clock -name cpu_clk -source [get_ports { clk100 }] \
-divide_by 16 [get_pins { clk_div_reg/Q }]
## 复位按键(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:显示 MMIO 写入值的低 16 位
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 写入指示(拉长约 2.7 s,CLK_DIV=16 下的 CPU 时钟)
set_property -dict { PACKAGE_PIN R12 IOSTANDARD LVCMOS33 } [get_ports { led_we }]
## 上板调试时可把 MMIO 写入的 LED 寄存器也引到 LED16_R/G,
## 但默认只用一个引脚,避免与 led_we 争用。
+115 -1
View File
@@ -50,7 +50,7 @@
该地址位于 4 KB 数据 RAM(`0x0000`–`0x0FFC`)之外,因此不会与 RAM 冲突。
| 地址 | 外设 | 写 | 读 |
| ------------ | -------------- | ------------------------------------------------------------------ | ------------------------ |
| ------------- | -------------- | ---------------------------------------------------------------------------- | -------------------------- |
| `0x0000_1000` | LED 数据寄存器 | `sw` 写全字;`sb` 按 `data_wstrb_o` 只改对应字节通道,`led_valid_o` 拉高一拍 | `lw` 返回 LED 寄存器当前值 |
地址译码按**字**对齐:同一 32 位寄存器内的任意字节偏移(如 `0x1001`)都算命中,
@@ -109,7 +109,121 @@
| `cpu21_riscv_irq_ctrl.v` | `cpu21_riscv_irq_ctrl` | 中断采样与优先级、`ustatus`/`uepc` CSR、嵌套返回栈 |
| `cpu21_riscv_perf_counters.v` | `cpu21_riscv_perf_counters` | 周期/停顿/气泡/分支/预测成功率等统计计数器 |
| `cpu21_mmio.v` | `cpu21_mmio` | MMIO 外设:LED 数据寄存器(可写、可读回,支持字节写) |
| `cpu21_board_top.v` | `cpu21_board_top` | 上板顶层:时钟分频、复位同步、片内 ROM/RAM、引脚外设连接 |
| `testbench/tb_board_top.v` | `tb_board_top` | 上板顶层的 RTL 仿真(观察 LED 跑马灯与写入指示) |
| `tools/rv_asm.py` | — | 迷你汇编器:`.asm` → 机器码 hex(`.lst` 带反汇编清单) |
| `tools/hex2vh.py` | — | hex → `cpu21_board_rom.vh`(`rom[i] = 32'h…;` 初始化语句) |
拆分只调整了模块边界与文件组织,端口、时序与行为完全等价:使用同一 ROM 程序
在 xsim 下对拆分前后逐周期对比,波形输出与最终统计(周期、停顿、气泡、分支
跳转、预测成败)完全一致。
## 上板(Nexys4DDR + Vivado)
处理器顶层 `cpu21_riscv_redirect_int_bpb` 有 775 位调试端口(各级 PC/IR、计数器、
BPB/中断观测值)。如果直接把它设为实现顶层,这些端口会全部变成引脚,导致
`[Place 30-415] IO Placement failed due to overutilization`(占用 775 个 IO,
而 `xc7a100tcsg324-1` 只有 210 个)。因此上板时用 `cpu21_board_top` 作为顶层,
只引出板上真正需要的 23 个引脚,其余调试端口不连接。
### 引脚分配(`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 |
### 时钟:为什么要把 100 MHz 分频
ALU 里的除法/取余(`a % b`)和 MUL 高位是纯组合电路。实测综合后在
100 MHz 下 `WNS = -84.126 ns`(关键路径 `ex_mem_pc → ex_mem_alu_result2`,
325 级逻辑、286 个 CARRY4),不可能收敛。因此 `cpu21_board_top` 用
`CLK_DIV = 16` 把 100 MHz 分频到 6.25 MHz(周期 160 ns)给 CPU,
`clk100` 只用于分频器本身:
```verilog
cpu21_board_top #(.CLK_DIV(16)) u_board ( ... );
```
分频后的时钟在综合/实现时由 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 16 [get_pins { clk_div_reg/Q }]
```
改分频比时 RTL 的 `CLK_DIV` 与 XDC 的 `-divide_by` 要同时改。
### 片内 ROM / RAM
- ROM:`ROM_WORDS = 1024` 字(4 KB),组合读,取指地址 `instr_addr[11:2]`;
内容由 `` `include "cpu21_board_rom.vh" `` 给出(文件里是 1024 条
`rom[i] = 32'h…;`,放在 `initial` 里,等价于 `$readmemh`,且不依赖文件路径)。
- RAM:`RAM_WORDS = 1024` 字(4 KB),字节写使能 + 组合读,只响应
`data_addr < 4 KB`,因此 MMIO 地址(默认 `0x1000`)不会落进 RAM。
- 两个 `initial` 块都用 NOP(`32'h0000_0013`)或 0 预填充,ROM 未使用区域是 NOP,
这样即使程序跑飞也不会执行到随机指令而乱写寄存器。
### 演示程序的生成与仿真
```powershell
# 1) 汇编 + 生成 ROM 初始化文件(在仓库根目录执行)
python tools/rv_asm.py testbench/programs/board_demo.asm _tmp/board_demo.hex
python tools/hex2vh.py _tmp/board_demo.hex cpu21_board_rom.vh 1024
# 2) 上板顶层的 RTL 仿真(默认程序延时约 20 ms,只需确认第一次写入 LED=0001)
xvlog <RTL…> testbench/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(6.25 MHz 下)。仓库里同时提供了两份 ROM 初始化
文件,由 `BOARD_ROM_FAST` 宏选择(综合流程不会定义该宏):
| 文件 | 用途 | 每步耗时 |
| ------------------------ | ------------------------------------- | -------- |
| `cpu21_board_rom.vh` | 综合/上板(默认) | 约 20 ms |
| `cpu21_board_rom_fast.vh` | 仅仿真:一次跑完整个跑马灯序列 | 4.8 µs |
```powershell
# 快速版仿真:生成/重新生成 CPU 时加上 -d BOARD_ROM_FAST
xvlog -d BOARD_ROM_FAST <RTL…> testbench/tb_board_top.v
```
两份文件都由同样的两步生成,只有延时循环的初值不同(把 `board_demo.asm` 里的
`lui t2, 0xf` 换成 `addi t2, zero, 8` 再照上面两步走一遍,输出到
`cpu21_board_rom_fast.vh`)。
### Vivado 工程设置与实现结果
Vivado 工程 `pipelined-cpu/pipelined-cpu.xpr` 被 `.gitignore` 排除,需要手动设置:
1. `sources_1` 顶层设为 `cpu21_board_top`(纯 RTL 仿真时也可切回
`cpu21_riscv_redirect_int_bpb`);
2. 加入 `cpu21_board_top.v`、`cpu21_board_top.xdc`,`cpu21_board_rom.vh` 的文件类型设成
`Verilog Header`(不能当普通源码,否则会被当成顶层候选);
3. 三个 testbench(`tb_cpu21_riscv_redirect_int_bpb.v`、`tb_no_intr.v`、`tb_board_top.v`)的
`USED_IN_SYNTHESIS` / `USED_IN_IMPLEMENTATION` 设为 `0`,否则综合可能把
testbench 当顶层,报 `[Place 30-494] The design is empty`。
`xc7a100tcsg324-1` 上的实现结果(`CLK_DIV=16`):
| 项目 | 结果 |
| -------------- | ------------------------------------------------------------- |
| 综合 | `synth_design Complete!` |
| 实现 | `route_design Complete!`,`WNS = +6.083 ns`(无 Failed Timing) |
| 资源 | LUT 5400 (8.52%)、LUTRAM 512、FF 2507 (1.98%)、DSP 12 (5%)、IOB 23 (10.95%) |
| bit 文件 | `pipelined-cpu/pipelined-cpu.runs/impl_1/cpu21_board_top.bit` |
上板操作:用 Vivado 打开工程 → `Open Hardware Manager` → `Program Device`,选上述
`.bit`。复位后跑马灯立即从 `0001` 开始逐位左移(演示程序不执行 `ecall`,所以不用
按 `go`;`go` 只对执行了 `ecall` 停机、等待继续运行的程序有意义),LED16_B 每次写入时点亮。
+13
View File
@@ -0,0 +1,13 @@
000012B7
00100313
0062A023
00131313
00008EB7
006EE463
0080006F
00100313
0000F3B7
00100E13
41C383B3
FE039EE3
FD9FF06F
+90
View File
@@ -0,0 +1,90 @@
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
@@ -0,0 +1,19 @@
123452B7
00500533
00001FB7
00AFA023
FFFFF2B7
00500533
00001FB7
00AFA023
800002B7
00500533
00001FB7
00AFA023
123452B7
67828293
00500533
00001FB7
00AFA023
00A00893
00000073
+16
View File
@@ -0,0 +1,16 @@
000012B7
12345337
67830313
0062A023
0002A503
00001FB7
00AFA023
FFF00393
0072A023
07F00E13
01C280A3
0002A503
00001FB7
00AFA023
00A00893
00000073
+22
View File
@@ -0,0 +1,22 @@
# 上板演示程序(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: # 软件延时,让人眼能看到跑马灯(6.25 MHz 下约 20 ms)
lui t2, 0xf # 延时计数 0x0000F000
addi t3, zero, 1
delay_loop:
sub t2, t2, t3
bne t2, zero, delay_loop
jal zero, main_loop
+4
View File
@@ -0,0 +1,4 @@
12345000
fffff000
80000000
12345678
+4 -3
View File
@@ -1,9 +1,10 @@
# MMIO 验证程序:写 LED 寄存器(0x0000_1000)-> 读回 -> 字节写 -> 再读回。
# 期望 LED 输出顺序(共 4 次,见同目录 mmio_test.expected):
# 期望 LED 输出顺序(共 5 次,见同目录 mmio_test.expected):
# ① 12345678 sw 0x12345678
# ② 12345678 lw 读回后再 print
# ② 12345678 lw 读回后 print(print = lui t6 + sw a0,0(t6),本身就是一次 MMIO 写)
# ③ ffffffff sw 0xffffffff
# ④ ffff7fff sb 0x7f 覆盖 led[15:8] 后读回
# ④ 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
+1
View File
@@ -2,3 +2,4 @@
12345678
ffffffff
ffff7fff
ffff7fff
+56
View File
@@ -0,0 +1,56 @@
`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 <RTL...> cpu21_board_top.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)
);
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 时钟经 16 分频,需要更长窗口)
$display("FINAL: LED 变化次数=%0d 当前 LED=%04h", changes, led);
$finish;
end
endmodule
+51
View File
@@ -0,0 +1,51 @@
#!/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 用法(见 cpu21_board_top.v):
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()
+1 -1
View File
@@ -215,7 +215,7 @@ def assemble(lines):
def main():
src, dst = sys.argv[1], sys.argv[2]
with open(src, encoding="utf-8") as f:
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: