在这里插入图片描述

摘要:本文详细记录了在 Windows 11 环境下,使用 Icarus Verilog(iverilog)对平头哥(T-Head)开源玄铁 E906 RISC-V 处理器核进行 RTL 仿真的完整流程。涵盖环境搭建、项目结构分析、架构难点解析(含字节序问题)、无工具链快速验证、完整 C 程序编译仿真,以及 PowerShell 5.1 的若干陷阱与解决方案。


一、概述

玄铁 E906 是平头哥半导体开源的一款 32 位 RISC-V 嵌入式处理器核,支持 RV32IMA[F]C[P] 指令集。官方提供了完整的 RTL 代码和 smart_run 仿真环境,但主要面向 Linux + VCS/irun 等商业 EDA 工具。

本文的目标是在 纯 Windows 环境 下,使用 完全开源免费 的工具链完成从 RTL 编译到 C 程序仿真验证的完整流程。

最终成果

项目 结果
最小指令测试(手工编码) ✅ PASS(5625 ns / ~562 周期)
hello_world C 程序 ✅ PASS(138315 ns / ~13831 周期)
UART 文字输出 ✅ “Hello Friend! Welcome to T-HEAD World!”
波形文件 ✅ test.vcd(可 GTKWave 查看)

二、环境搭建

2.1 工具安装一览

工具 用途 安装方式
Icarus Verilog (iverilog + vvp) Verilog 编译与仿真 预装于 C:\iverilog\(也可 winget install Icarus.Verilog
GTKWave VCD 波形查看 预装于 C:\iverilog\gtkwave\
RISC-V GNU 工具链 编译 C/汇编测试程序 从平头哥 OCC 下载 Windows mingw 版本
GNU Make 构建自动化 winget install ezwinports.make
VS Code + Verible(可选) RTL 代码浏览与追踪 winget install chipsalliance.verible

2.2 RISC-V 工具链安装

平头哥 OCC 社区 下载 mingw 版本的工具链(Xuantie-900-gcc-elf-newlib-mingw-V3.2.0-20250627.tar.gz),解压到目标目录:

# 解压工具链
tar -xzf Xuantie-900-gcc-elf-newlib-mingw-V3.2.0-20250627.tar.gz -C D:\OD\install_self

注意:选择 mingw 版本而非 x86_64 版本,后者是 Linux 二进制文件。mingw 版本可在 Windows 原生运行。

2.3 环境变量设置

$env:TOOL_EXTENSION = "D:\OD\install_self\Xuantie-900-gcc-elf-newlib-mingw-V3.2.0\bin"
$env:CODE_BASE_PATH = "d:/OE/own_doc/vscode_pro/rtl_project/e906/E906_RTL_FACTORY"
$env:PATH = "$env:TOOL_EXTENSION;C:/iverilog/bin;$env:PATH"

关键说明CODE_BASE_PATH 必须指向 E906_RTL_FACTORY 目录(即 gen_rtl 的父目录),且路径分隔符用 / 而非 \,否则 Makefile 中的 ${CODE_BASE_PATH} 展开可能出问题。


三、项目结构分析

3.1 顶层目录

e906/
├── E906_RTL_FACTORY/        # RTL 源代码(可综合的处理器核)
│   └── gen_rtl/
│       ├── cpu/rtl/          # 顶层:openE906.v, pa_core.v, pa_core_top.v
│       ├── ifu/rtl/          # 指令取指单元
│       ├── idu/rtl/          # 指令译码单元
│       ├── iu/rtl/           # 整数运算单元
│       ├── lsu/rtl/          # 加载存储单元
│       ├── falu/rtl/         # 浮点运算单元
│       ├── biu/rtl/          # 总线接口单元
│       ├── clic/rtl/         # 中断控制器
│       ├── filelists/        # RTL 文件列表
│       └── ...
├── smart_run/                # 仿真环境
│   ├── Makefile              # 主仿真脚本
│   ├── logical/              # SoC 平台 + Testbench
│   │   ├── tb/tb.v           # 顶层 testbench
│   │   ├── system/soc.v      # SoC 顶层
│   │   └── mem/              # 内存控制器
│   ├── work/                 # 工作目录(编译产物、.pat 文件)
│   └── tests/                # 测试用例
│       ├── lib/              # 启动代码 (crt0.s)、链接脚本、C 库
│       └── cases/            # 测试程序源码
└── doc/                      # 文档

3.2 E906 的三总线架构

E906 处理器核有三种总线接口,理解它们的关系至关重要:

                    ┌─────────────────────────┐
                    │      openE906 (CPU)     │
                    │                         │
                    │  ┌───────────────────┐  │
  ──── BIU ────────►│  │   Bus Management  │  │──── IAHBL (指令) ──► iahb_mem_ctrl (0x0)
                    │  │   Unit (BMU)      │  │──── DAHBL (数据) ──► dahb_mem_ctrl (0x20000000+)
                    │  └───────────────────┘  │
                    └─────────┬───────────────┘
                              │ BIU (外部总线)
                              ▼
                    ┌──────────────────┐
                    │   AHB Bus Arbiter│
                    ├──────────────────┤
                    │ S1: System Mem   │  0x60000000 - 0x600FFFFF
                    │ S2: APB Periph   │  0x40000000 - 0x4FFFFFFF
                    │ S3: Error Gen    │  其他地址(默认从)
                    └──────────────────┘
  • IAHBL:指令 AHB-Lite 总线,连接内部指令存储器(iahb_mem_ctrl)。BMU 将地址 0x00000000 - 0x001FFFFF 映射到此总线。
  • DAHBL:数据 AHB-Lite 总线,连接内部数据存储器(dahb_mem_ctrl)。BMU 将地址 0x20000000 - 0x2FFFFFFF 映射到此总线。
  • BIU:外部 AHB 总线,连接 SoC 级外设。PASS 检测地址 0x6000FFF8 在此总线上。

BMU 配置参数(在 cpu_sub_system_ahb.v 中):

.pad_bmu_iahbl_base  (12'h000),   // IAHB 基地址
.pad_bmu_iahbl_mask  (12'he00),   // IAHB 地址掩码
.pad_bmu_dahbl_base  (12'h200),   // DAHB 基地址
.pad_bmu_dahbl_mask  (12'hfff),   // DAHB 地址掩码

3.3 PASS/FAIL 机制

Testbench 通过监视 BIU 总线上的写操作来判断仿真结果:

条件 结果
0x6000FFF8 写入 0x00000FFF0xFFFF0000 PASS
0x6000FFF8 写入 0x00000EEE0xEEEE0000 FAIL
连续 5000 周期无指令退休 FAIL(timeout)
仿真时间超过 700 ms FAIL(max time)

四、核心难点:testbench 的字节序问题

4.1 问题描述

这是仿真过程中最隐蔽也最关键的问题。Testbench 通过 $readmemh 读取 case.pat 文件,然后直接写入内存阵列

// tb.v
$readmemh("./case.pat", mem_inst_temp);

for(i=0;i<65536;i=i+1) begin
    `RTL_IAHBL_MEM.ram0.mem[i][7:0] = mem_inst_temp[i][31:24];  // MSB → ram0
    `RTL_IAHBL_MEM.ram1.mem[i][7:0] = mem_inst_temp[i][23:16];
    `RTL_IAHBL_MEM.ram2.mem[i][7:0] = mem_inst_temp[i][15: 8];
    `RTL_IAHBL_MEM.ram3.mem[i][7:0] = mem_inst_temp[i][ 7: 0];  // LSB → ram3
end

而指令内存控制器(iahb_mem_ctrl)读取时的重组方式是:

assign lite_mem_dout[31:0] = {ram3_dout[7:0], ram2_dout[7:0], ram1_dout[7:0], ram0_dout[7:0]};

这意味着:写入时 ram0 拿到的是 MSB,但读取时 ram0 被解释为 LSB——整个指令字被字节翻转了。

4.2 解决方案

case.pat 中的 32 位十六进制值必须做字节交换。设原始 RISC-V 指令字为 0x60010237lui x4, 0x6001):

原始指令字节顺序(内存中,小端):   37  02  01  60
$readmemh 加载到 mem_inst_temp[0]:  0x60010237
                                  → bits[31:24] = 0x60 → ram0
                                  → bits[23:16] = 0x01 → ram1
                                  → bits[15:8]  = 0x02 → ram2
                                  → bits[7:0]   = 0x37 → ram3
内存读出:{ram3, ram2, ram1, ram0} = {0x37, 0x02, 0x01, 0x60}
                                  = 0x37020160  ← 错误的指令!

修正:在 case.pat 中保存字节翻转后的值 0x37020160

$readmemh 加载:0x37020160 → {0x37, 0x02, 0x01, 0x60}
ram0=0x37, ram1=0x02, ram2=0x01, ram3=0x60
内存读出:{0x60, 0x01, 0x02, 0x37} = 0x60010237 ← 正确的指令!✓

这个字节交换逻辑已封装在后续的 srec2pat.ps1 脚本中,用户无需手动处理。


五、快速起步:无工具链仿真

在安装 RISC-V 工具链之前,可以先用手工编码的机器码验证仿真环境是否正常。

5.1 手工构造最小程序

编写一个最简单的 RV32I 程序:向 0x6000FFF8 写入 0xFFFF0000(PASS 条件)。

指令 机器码 说明
lui x4, 0x6001 60010237 x4 = 0x60010000
addi x4, x4, -8 FF820213 x4 = 0x6000FFF8
lui x3, 0xFFFF0 FFFF01B7 x3 = 0xFFFF0000
sw x3, 0(x4) 00322023 写 0x6000FFF8 → PASS
beq x0, x0, 0 00000063 无限循环

5.2 创建 case.pat(字节交换版)

# work/case.pat — 每行一个 32-bit 十六进制数(已做字节交换)
37020160
130282FF
B701FFFF
23203200
63000000
# work/data.pat
00000000

5.3 编译与仿真

$env:CODE_BASE_PATH = "d:/OE/own_doc/vscode_pro/rtl_project/e906/E906_RTL_FACTORY"
$env:PATH = "C:/iverilog/bin;$env:PATH"

cd smart_run/work

# 编译 RTL
iverilog -o xuantie_core.vvp -Diverilog=1 -g2012 -DIVERILOG_SIM -DNO_DUMP `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/E906_asic_rtl.fl" `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/tdt_dmi_top_rtl.fl" `
  -c ../logical/filelists/smart.fl -c ../logical/filelists/tb.fl

# 运行仿真
vvp xuantie_core.vvp

预期输出:

******START TO LOAD PROGRAM******
**********************************************
*    simulation finished successfully        *
**********************************************

5.4 启用波形

去掉 -DNO_DUMP 重新编译,即可生成 test.vcd 波形文件:

gtkwave test.vcd

六、完整流程:C 程序编译到仿真

6.1 Srec2vmem 替代方案

smart_run 工具链使用 Srec2vmem 将 S-record 格式的 .hex 文件转换为 Verilog $readmemh 兼容的 .pat 文件。但 Srec2vmem 是一个 Linux x86_64 ELF 二进制文件,无法在 Windows 上运行。

为此,我们编写了纯 PowerShell 实现:srec2pat.ps1。使用方法:

.\srec2pat.ps1 -HexFile hello_world_main.hex -PatFile case.pat

6.2 完整编译流程

$env:TOOL_EXTENSION = "D:\OD\install_self\Xuantie-900-gcc-elf-newlib-mingw-V3.2.0\bin"
$env:CODE_BASE_PATH = "d:/OE/own_doc/vscode_pro/rtl_project/e906/E906_RTL_FACTORY"
$env:PATH = "$env:TOOL_EXTENSION;C:/iverilog/bin;$env:PATH"

cd smart_run/work

# === 1. 编译 C/汇编源码 ===
$CFLAGS = @("-mtune=e906", "-march=rv32imafc_zifencei", "-mabi=ilp32f", "-c", "-O2")

riscv64-unknown-elf-gcc @CFLAGS -o crt0.o crt0.s
riscv64-unknown-elf-gcc @CFLAGS -o hello_world_main.o hello_world_main.c
riscv64-unknown-elf-gcc @CFLAGS -o uart.o uart.c
riscv64-unknown-elf-gcc @CFLAGS -o intc.o intc.c
riscv64-unknown-elf-gcc @CFLAGS -o fputc.o fputc.c
riscv64-unknown-elf-gcc @CFLAGS -o printf.o printf.c
riscv64-unknown-elf-gcc @CFLAGS -o vasprintf.o vasprintf.c
riscv64-unknown-elf-gcc @CFLAGS -o __thead_printf.o __thead_printf.c

# === 2. 链接 ===
$LFLAGS = @("-T", "linker.lcf", "-nostartfiles", "-mtune=e906", 
            "-march=rv32imafc_zifencei", "-mabi=ilp32f")
$OBJS = @("crt0.o", "hello_world_main.o", "uart.o", "intc.o", "fputc.o", 
          "printf.o", "vasprintf.o", "__thead_printf.o")

riscv64-unknown-elf-gcc @LFLAGS @OBJS -o hello_world_main.elf -lc -lgcc

# === 3. 生成 S-record ===
riscv64-unknown-elf-objcopy -O srec hello_world_main.elf hello_world_main.hex
riscv64-unknown-elf-objcopy -O srec -j .text -j .rodata -j .srodata.* `
  hello_world_main.elf hello_world_main_inst.hex
riscv64-unknown-elf-objcopy -O srec -j .data -j .sdata.* -j .bss -j .sbss.* `
  hello_world_main.elf hello_world_main_data.hex

# === 4. 转换 .hex → .pat ===
..\tests\bin\srec2pat.ps1 -HexFile hello_world_main.hex -PatFile case.pat
..\tests\bin\srec2pat.ps1 -HexFile hello_world_main_data.hex -PatFile data.pat

# === 5. 编译 RTL ===
iverilog -o xuantie_core.vvp -Diverilog=1 -g2012 -DIVERILOG_SIM -DNO_DUMP `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/E906_asic_rtl.fl" `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/tdt_dmi_top_rtl.fl" `
  -c ../logical/filelists/smart.fl -c ../logical/filelists/tb.fl

# === 6. 仿真 ===
vvp xuantie_core.vvp

6.3 预期结果

******START TO LOAD PROGRAM******

Hello Friend!
Welcome to T-HEAD World!
a is 1!
b is 2!
c is 0!
!!! PASS !!!after ASM c is changed to 3!

**********************************************
*    simulation finished successfully        *
**********************************************

七、问题排查与经验总结

🪲 问题 1:PowerShell 5.1 的 [byte] 左移陷阱

症状:生成的 .pat 文件全为 00000000

原因:PowerShell 5.1 中,对 [byte] 类型进行 -shl N 运算时,如果 N >= 8,结果被截断为 0——因为中间结果超出了 byte 范围(255)。

# ❌ 错误
$b = [byte]0x17
$b -shl 24   # → 0x00000000  (被截断!)

# ✅ 正确
[int]$b -shl 24   # → 0x17000000

解决方案:所有位运算前先转 [int]

🪲 问题 2:PowerShell 哈希表键类型严格匹配

症状:哈希表有 24164 条数据,但 $data[0] 返回 $null

原因$addr = [Convert]::ToUInt32(...) 返回 UInt32 类型的 0,而 $data[0] 中的 0Int32。PowerShell 哈希表做严格的类型匹配查找。

# ❌ 错误
$addr = [Convert]::ToUInt32("0000", 16)  # UInt32
$data[$addr] = 0x17
$data[0]        # → $null  (Int32 0 ≠ UInt32 0)

# ✅ 正确
$addr = [int][Convert]::ToUInt32(...)    # 转 Int32
$data[$addr] = 0x17
$data[0]        # → 0x17

🪲 问题 3:新版本 GCC 的 fence.i 指令

症状:编译 crt0.s 时报错 unrecognized opcode 'fence.i', extension 'zifencei' required

原因:较新的 RISC-V GCC(14.x+)将 fence.i 从基础 ISA 分离到了 zifencei 扩展。

解决方案:在 march 中添加 _zifencei

# ❌ 旧 march(会报错)
-march=rv32imafc

# ✅ 新 march
-march=rv32imafc_zifencei

🪲 问题 4:Iverilog 的 -c 标志

症状iverilog: unknown option -c(早期版本)。

说明smart_run 的 Makefile 使用 -c 标志读取命令文件,这是较新版本 Icarus Verilog 才支持的特性。本文使用的 iverilog 12.0 支持此标志,如果遇到问题可改用 -f

🪲 问题 5:make 未安装

症状make buildcase CASE=hello_world 提示命令未找到。

解决方案

winget install ezwinports.make

或在没有 make 的情况下手动执行编译步骤(详见第六节)。

🪲 问题 6:路径分隔符

症状:Makefile 传递路径时报错。

原因:Makefile 中使用 ${CODE_BASE_PATH}/gen_rtl/...,如果 CODE_BASE_PATH 包含 Windows 反斜杠 \,在某些上下文中会被解释为转义字符。

解决方案:设置环境变量时使用正斜杠 /

$env:CODE_BASE_PATH = "d:/OE/own_doc/vscode_pro/rtl_project/e906/E906_RTL_FACTORY"

🪲 问题 7:-mtune=e906 不被识别

症状cc1.exe: error: unknown cpu 'e906' for '-mtune'

说明-mtune=e906 是平头哥 GCC 中 TIER 特有的优化参数。如果使用上游 RISC-V GCC(非 Xuantie 版本),这个选项不会被识别。使用平头哥官方工具链则无此问题。


八、快速参考命令

8.1 一键仿真(需要 make)

$env:TOOL_EXTENSION = "D:\OD\install_self\Xuantie-900-gcc-elf-newlib-mingw-V3.2.0\bin"
$env:CODE_BASE_PATH = "d:/OE/own_doc/vscode_pro/rtl_project/e906/E906_RTL_FACTORY"
$env:PATH = "$env:TOOL_EXTENSION;C:/iverilog/bin;$env:PATH"

cd smart_run
make runcase CASE=hello_world

8.2 查看可用测试用例

cd smart_run
make showcase

可用用例:hello_world, dhry, coremark, memset, memcp, ISA_RV32IMAC, ISA_RV32F, csr_rw_extend, debug, debug_2wire_jtag

8.3 查看波形

gtkwave work/test.vcd

8.4 重新编译 RTL(不带波形加速)

# 带 -DNO_DUMP 仿真更快,适合功能验证
iverilog -o xuantie_core.vvp -Diverilog=1 -g2012 -DIVERILOG_SIM -DNO_DUMP `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/E906_asic_rtl.fl" `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/tdt_dmi_top_rtl.fl" `
  -c ../logical/filelists/smart.fl -c ../logical/filelists/tb.fl

# 不带 -DNO_DUMP 生成波形
iverilog -o xuantie_core.vvp -Diverilog=1 -g2012 -DIVERILOG_SIM `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/E906_asic_rtl.fl" `
  -f "${env:CODE_BASE_PATH}/gen_rtl/filelists/tdt_dmi_top_rtl.fl" `
  -c ../logical/filelists/smart.fl -c ../logical/filelists/tb.fl

九、总结

本文验证了在 纯 Windows 环境 下,使用 完全开源免费工具 成功对玄铁 E906 RISC-V 处理器进行 RTL 仿真的可行性。关键结论:

  1. 工具链可行:Icarus Verilog + Xuantie GCC mingw 版本 + GNU Make 构成了完整的 Windows 仿真工具链。

  2. 字节序是核心问题:E906 testbench 的 RAM 直接加载方式存在字节翻转,所有 .pat 文件必须做字节交换。srec2pat.ps1 脚本封装了这个逻辑。

  3. PowerShell 有隐藏陷阱[byte] 类型的左移运算和哈希表键类型严格匹配是两个容易忽视的问题。

  4. 无需工具链也能验证:通过手工编码最小程序的机器码,可以在几分钟内验证仿真环境是否正常,便于快速排错。


十、后处理步骤与原创内容说明

本节说明在原始 E906 开源项目基础上,本文做了哪些后处理(post-processing)改造,哪些内容属于原创,便于读者区分"项目自带的"和"我们自己加的"。

10.1 原始项目自带的内容

以下内容来自平头哥官方发布的 openE906,未经修改可直接使用:

类别 内容 说明
RTL 源码 E906_RTL_FACTORY/gen_rtl/ 完整的处理器 RTL 设计
仿真环境 smart_run/Makefile 支持 iverilog/vcs/irun 的编译仿真脚本
SoC 平台 smart_run/logical/ SoC 集成、AHB 总线、外设模型、testbench
启动代码 tests/lib/crt0.s RISC-V 复位初始化代码
链接脚本 tests/lib/linker.lcf 内存布局定义
测试用例 tests/cases/ hello_world、dhrystone、coremark 等
Srec2vmem tests/bin/Srec2vmem Linux x86_64 ELF 二进制(S-record → .pat 转换器)
工具链安装指南 setup/example_setup.csh Linux 环境变量设置参考

10.2 本文新增的原创内容

原创内容 文件/章节 说明
srec2pat.ps1 tests/bin/srec2pat.ps1 核心原创。Windows PowerShell 实现的 S-record → .pat 转换器,替代 Linux-only 的 Srec2vmem,自动处理 E906 字节序
手工编码最小程序 第五节 5 条 RV32I 指令的机器码,绕过工具链直接验证仿真环境
Windows 编译流程 第六节 全套 PowerShell 命令,替代 make + Linux 工具链
编译参数修正 第六节 -march=rv32imafc_zifencei(新 GCC 需要)、-fpermissive
问题排查清单 第七节 7 个 Windows 特有问题的原因分析与解决方案

10.3 哪些场景需要后处理

根据你的使用场景,需要的后处理内容不同:

场景 A:快速尝鲜(已有 Linux 环境)
  └─ 不需要任何后处理。直接用 make runcase CASE=hello_world

场景 B:Windows + 仅验证仿真环境
  ├─ srec2pat.ps1 ✓ (必须,替代 Srec2vmem)
  ├─ 手工编码最小程序 (可选,快速排错用)
  └─ 字节序修正 ✓ (必须,已封装在 srec2pat.ps1 中)

场景 C:Windows + 完整开发
  ├─ srec2pat.ps1 ✓
  ├─ 手动编译流程 ✓
  ├─ `_zifencei` march 修正 ✓
  └─ 哈希表/byte 移位陷阱注意 ✓

10.4 原始项目的设计意图

理解原始项目的 Linux 优先设计,有助于排查 Windows 上的问题:

  1. Srec2vmem 是 Linux 二进制:这是最直接的 Windows 障碍。原始团队可能默认在 Linux EDA 环境下工作。

  2. Makefile 使用 csh/tcshsetup/example_setup.csh 用 csh 语法设置环境变量,Windows 需要手动翻译为 PowerShell 或 cmd。

  3. 字节序设计是刻意的:testbench 的字节翻转并非 bug,而是与 Srec2vmem 的 .pat 输出格式配套的设计。.hex.pat 的转换过程中已经包含了字节交换。我们编写的 srec2pat.ps1 保持了相同的转换约定。

  4. GCC 选项的演进:原始使用的 GCC 版本较老(8.x),fence.i 仍在基础 march 中。新工具链(14.x)遵循更新版的 RISC-V ISA 规范,需要显式启用 _zifencei 扩展。


附录 A:srec2pat.ps1 完整源码(原创)

本脚本是本文的核心原创产出,用于替代原始项目中 Linux-only 的 Srec2vmem 二进制文件。它读取 objcopy -O srec 生成的 Motorola S-record 格式文件,输出 Verilog $readmemh 兼容的 .pat 文件。

A.1 设计思路

原始 Srec2vmem 的功能链:

.elf ─[objcopy -O srec]─→ .hex (S-record)[Srec2vmem]─→ .pat ($readmemh)

srec2pat.ps1 替代 Srec2vmem,核心逻辑:

  1. 解析 S-record:支持 S1(16 位地址)、S2(24 位地址)、S3(32 位地址)三种记录类型
  2. 构建字节缓冲区:以地址为 key,字节值为 value 的哈希表
  3. 生成 32 位字:按 4 字节对齐读取,处理空洞(gap)为零
  4. 字节交换:适配 E906 testbench 的 RAM 加载约定

A.2 完整源码与逐段注释

#!/usr/bin/env pwsh
<#
.SYNOPSIS
    Convert Motorola S-record hex file to $readmemh-compatible .pat file
    (Replacement for the Linux-only Srec2vmem binary)
.DESCRIPTION
    Reads an S-record file (generated by objcopy -O srec) and outputs
    a Verilog $readmemh-compatible .pat file with 32-bit hex words,
    byte-swapped for the E906 testbench's RAM loading convention.
.PARAMETER HexFile
    Input S-record hex file
.PARAMETER PatFile
    Output .pat file
#>
param(
    [Parameter(Mandatory=$true)]
    [string]$HexFile,
    [Parameter(Mandatory=$true)]
    [string]$PatFile
)

# ──────────────────────────────────────────────
# Step 1: 读取 S-record 文件到行数组
# ──────────────────────────────────────────────
$lines = Get-Content $HexFile

# Data buffer: address -> byte
# 使用哈希表存储每个地址对应的字节值
# key: 32-bit 地址(Int32),value: byte(0x00-0xFF)
$data = @{}

# ──────────────────────────────────────────────
# Step 2: 逐行解析 S-record
# ──────────────────────────────────────────────
# S-record 格式(Motorola):
#   S<type><count><addr><data...><checksum>
#   type: 0=header, 1=16bit addr data, 2=24bit addr data, 3=32bit addr data
#         5=data count, 7/8/9=termination
#   count: 后续字节数(含 addr + data + checksum)
#   addr:  2/3/4 字节地址
#   data:  数据字节
#   checksum: 1 字节校验和
foreach ($line in $lines) {
    $line = $line.Trim()
    if ($line.Length -lt 4) { continue }

    $type = $line.Substring(1, 1)

    # 跳过头记录、计数记录和终止记录
    if ($type -eq '0') { continue }  # header
    if ($type -eq '5' -or $type -eq '7' -or $type -eq '8' -or $type -eq '9') { continue }

    $byteCount = [Convert]::ToByte($line.Substring(2, 2), 16)

    # 根据类型确定地址长度(字节数)
    if ($type -eq '1') {
        # S1: 16-bit address (2 bytes)
        $addrLen = 2
    } elseif ($type -eq '2') {
        # S2: 24-bit address (3 bytes)
        $addrLen = 3
    } elseif ($type -eq '3') {
        # S3: 32-bit address (4 bytes)
        $addrLen = 4
    } else {
        continue
    }

    $addrStr = $line.Substring(4, $addrLen * 2)
    # ⚠️ 关键:必须显式转为 [int]!
    # PowerShell 哈希表的键类型严格匹配。
    # [Convert]::ToUInt32 返回 UInt32,而后续查找用 Int32,
    # 会导致 $data[0](Int32)找不到 $data[$uintKey](UInt32)的值。
    $addr = [int][Convert]::ToUInt32($addrStr, 16)

    # 数据长度 = 总字节数 - 地址字节数 - 1(校验和)
    $dataLen = $byteCount - $addrLen - 1
    if ($dataLen -le 0) { continue }

    $dataStart = 4 + $addrLen * 2
    $dataStr = $line.Substring($dataStart, $dataLen * 2)

    # 提取每个字节存入哈希表
    for ($i = 0; $i -lt $dataLen; $i++) {
        $byteVal = [Convert]::ToByte($dataStr.Substring($i * 2, 2), 16)
        $data[$addr + $i] = $byteVal
    }
}

if ($data.Count -eq 0) {
    Write-Warning "No data records found in $HexFile"
    return
}

# ──────────────────────────────────────────────
# Step 3: 确定地址范围
# ──────────────────────────────────────────────
$minAddr = [int]($data.Keys | Measure-Object -Minimum).Minimum
$maxAddr = [int]($data.Keys | Measure-Object -Maximum).Maximum

# ──────────────────────────────────────────────
# Step 4: 按 32-bit 字生成 .pat 内容
# ──────────────────────────────────────────────
$patLines = @()
for ($addr = $minAddr; $addr -le $maxAddr; $addr += 4) {
    # 读取 4 个连续字节(处理地址空洞)
    $b0 = if ($data.ContainsKey($addr))   { $data[$addr] }   else { 0 }
    $b1 = if ($data.ContainsKey($addr+1)) { $data[$addr+1] } else { 0 }
    $b2 = if ($data.ContainsKey($addr+2)) { $data[$addr+2] } else { 0 }
    $b3 = if ($data.ContainsKey($addr+3)) { $data[$addr+3] } else { 0 }

    # ⚠️ 关键:字节交换以适应 E906 testbench 的 RAM 加载机制
    #
    # Testbench 加载(直接写 RAM 阵列):
    #   ram0.mem[i] = readmemh_word[31:24]  ← 这条指令的 MSB
    #   ram3.mem[i] = readmemh_word[7:0]    ← 这条指令的 LSB
    #
    # RAM 控制器读取(组装回数据总线):
    #   dout = {ram3_dout, ram2_dout, ram1_dout, ram0_dout}
    #
    # 结果为:dout = {readmemh_word[7:0], readmemh_word[15:8],
    #                 readmemh_word[23:16], readmemh_word[31:24]}
    #
    # 即:写入的 32 位值被字节翻转了。
    # 所以 .pat 文件中要保存的是 字节翻转后的值。
    #
    # 设原始指令字为 A_B_C_D(A=MSB,D=LSB),
    # 则 .pat 文件应保存 D_C_B_A。
    # ────────────────────────────────────────────
    #
    # 原始地址 $addr 的 4 字节(小端序):b0 b1 b2 b3
    #   其中 b3 是 MSB(地址 addr+3),b0 是 LSB(地址 addr)
    #
    # 翻转后保存:b0 | b1 | b2 | b3 → 组成 32-bit 十六进制值
    #   $readmemh 读到 ram0,最终被读到 dout[7:0]
    #   而 CPU 期望 dout[7:0] 是原始指令的 LSB(b0)
    #
    # 验证:CPU 读到 {ram3, ram2, ram1, ram0} = {b3, b2, b1, b0}
    #       = 正确的 RISC-V 小端指令字 ✓

    # ⚠️ 关键:必须转为 [int] 再移位!
    # PowerShell 5.1 对 [byte] 类型做 -shl N 运算时,
    # 如果 N >= 8,中间结果超出 byte 范围(0-255),
    # 会被静默截断为 0。
    # 例如:[byte]0x17 -shl 24 → 0x00000000(❌ 错误)
    #        [int]0x17  -shl 24 → 0x17000000(✅ 正确)
    $b0i = [int]$b0; $b1i = [int]$b1; $b2i = [int]$b2; $b3i = [int]$b3

    $swapped = ($b0i -shl 24) -bor ($b1i -shl 16) -bor ($b2i -shl 8) -bor $b3i

    $patLines += "{0:X8}" -f $swapped
}

# ──────────────────────────────────────────────
# Step 5: 写入 .pat 文件
# ──────────────────────────────────────────────
# 每行一个 32 位十六进制数(8 字符),CRLF 换行
$patLines -join "`r`n" | Out-File -FilePath $PatFile -Encoding ascii
# 确保文件末尾有换行符
"`r`n" | Out-File -FilePath $PatFile -Encoding ascii -Append -NoNewline

Write-Host "Generated $($patLines.Count) words from $minAddr to $maxAddr -> $PatFile"

A.3 使用示例

# 基本用法
.\srec2pat.ps1 -HexFile hello_world_main.hex -PatFile case.pat

# 生成指令段和数据段
.\srec2pat.ps1 -HexFile hello_world_main_inst.hex -PatFile inst.pat
.\srec2pat.ps1 -HexFile hello_world_main_data.hex -PatFile data.pat

A.4 与原始 Srec2vmem 的对等性验证

对比项 原始 Srec2vmem srec2pat.ps1
运行平台 Linux x86_64 Windows (PowerShell 5.1+)
输入格式 Motorola S-record Motorola S-record
输出格式 $readmemh .pat $readmemh .pat
字节序处理 内置翻转 内置翻转(保持一致)
地址空洞 填充 0 填充 0
多段支持

验证方法:在 Linux 上分别用两者转换同一 .hex 文件,输出结果应逐行一致。

附录 B:关键文件路径速查

文件 路径
Testbench 顶层 smart_run/logical/tb/tb.v
SoC 顶层 smart_run/logical/system/soc.v
CPU 子系统 smart_run/logical/system/cpu_sub_system_ahb.v
指令存储器控制器 smart_run/logical/mem/iahb_mem_ctrl.v
数据存储器控制器 smart_run/logical/mem/dahb_mem_ctrl.v
AHB 总线仲裁器 smart_run/logical/ahb/ahb.v
链接脚本 smart_run/tests/lib/linker.lcf
启动代码 smart_run/tests/lib/crt0.s
库文件 Makefile smart_run/tests/lib/Makefile
RTL 文件列表 E906_RTL_FACTORY/gen_rtl/filelists/E906_asic_rtl.fl
仿真配置文件 smart_run/setup/smart_cfg.mk

附录 C:测试结果数据

测试用例 仿真时间 周期数 (≈) 结果
最小程序(5 条指令) 5,625 ns 562 ✅ PASS
hello_world 138,315 ns 13,831 ✅ PASS

仿真条件:Icarus Verilog 12.0,时钟周期 10ns(100MHz),-DNO_DUMP 模式。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐