玄铁 E906:Windows 环境下 RTL 仿真完整指南 - 基于Claude Code的探索

摘要:本文详细记录了在 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 写入 0x00000FFF 或 0xFFFF0000 |
PASS |
向 0x6000FFF8 写入 0x00000EEE 或 0xEEEE0000 |
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 指令字为 0x60010237(lui 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] 中的 0 是 Int32。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 仿真的可行性。关键结论:
-
工具链可行:Icarus Verilog + Xuantie GCC mingw 版本 + GNU Make 构成了完整的 Windows 仿真工具链。
-
字节序是核心问题:E906 testbench 的 RAM 直接加载方式存在字节翻转,所有 .pat 文件必须做字节交换。
srec2pat.ps1脚本封装了这个逻辑。 -
PowerShell 有隐藏陷阱:
[byte]类型的左移运算和哈希表键类型严格匹配是两个容易忽视的问题。 -
无需工具链也能验证:通过手工编码最小程序的机器码,可以在几分钟内验证仿真环境是否正常,便于快速排错。
十、后处理步骤与原创内容说明
本节说明在原始 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 上的问题:
-
Srec2vmem 是 Linux 二进制:这是最直接的 Windows 障碍。原始团队可能默认在 Linux EDA 环境下工作。
-
Makefile 使用 csh/tcsh:
setup/example_setup.csh用 csh 语法设置环境变量,Windows 需要手动翻译为 PowerShell 或 cmd。 -
字节序设计是刻意的:testbench 的字节翻转并非 bug,而是与 Srec2vmem 的 .pat 输出格式配套的设计。
.hex→.pat的转换过程中已经包含了字节交换。我们编写的srec2pat.ps1保持了相同的转换约定。 -
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,核心逻辑:
- 解析 S-record:支持 S1(16 位地址)、S2(24 位地址)、S3(32 位地址)三种记录类型
- 构建字节缓冲区:以地址为 key,字节值为 value 的哈希表
- 生成 32 位字:按 4 字节对齐读取,处理空洞(gap)为零
- 字节交换:适配 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 模式。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)