1. 玄铁 E906 在 Windows 下跑 RTL 仿真到底难在哪玄铁 E906 是平头哥开源的一款 32 位 RISC-V 嵌入式处理器核支持 RV32IMA[F]C[P] 指令集官方给了完整的 RTL 源码和 smart_run 仿真环境。但如果你打开它的 setup 脚本看一眼就会发现默认路径全是 Linux VCS/irun 那一套商业 EDA 工具链Windows 用户拿到手基本是懵的。我这次要解决的问题很具体在纯 Windows 11 环境下不装虚拟机、不碰 Linux 子系统用完全开源免费的工具链把 E906 从 RTL 编译到 C 程序仿真跑通。最终目标是看到Hello Friend! Welcome to T-HEAD World!从 UART 输出并且能生成 VCD 波形用 GTKWave 查看。适合谁看如果你是嵌入式方向的学生、刚接触 RISC-V 的验证工程师或者手头只有 Windows 机器但想跑通一个真实处理器核仿真的开发者这篇内容可以直接跟着做。整个流程涉及 iverilog 编译、Xuantie GCC 交叉编译、S-record 转 pat 文件、PowerShell 脚本处理字节序每一步我都会给可复制的命令和配置。过程中我会用 Claude Code 辅助梳理工具链依赖和报错排查同时用 TaoToken 统一管理调用凭据避免在多个工具之间反复切换 Key。下面从环境搭建开始一步步走完整个流程。2. 工具链选型与 TaoToken 凭据通道配置2.1 为什么选 Icarus Verilog 而不是商业工具Windows 上能跑 Verilog 仿真的开源方案不多Icarus Verilogiverilog vvp是最成熟的一个。它支持 Verilog-2005 和部分 SystemVerilogE906 的 RTL 代码用-g2012模式可以正常编译。安装方式有两种直接下预编译包解压到C:\iverilog\或者用 wingetwinget install Icarus.VerilogGTKWave 通常随 iverilog 一起安装在C:\iverilog\gtkwave\目录下。如果你需要看波形这个不能少。RISC-V 交叉编译工具链从平头哥 OCC 社区下载 mingw 版本注意别选 x86_64 版本——那是 Linux 二进制文件Windows 上跑不了。文件名类似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_selfGNU Make 用 winget 装就行winget install ezwinports.make。不过后面我会给不依赖 make 的手动编译流程因为 PowerShell 下 make 的路径处理经常出问题。2.2 TaoToken 统一 Key/API 通道的配置方式在开始之前先把调用凭据统一管起来。TaoToken 的作用是让你用一个 Key 走通模型对话、代码补全、Agent 调用等不同通道不用每个工具单独配一套。配置入口在控制台API 地址是https://taotoken.net/api。如果你用 Claude Code 做辅助开发需要在 settings 里配 Base URL 和 Key。具体操作是打开 Claude Code 的配置文件通常在用户目录下的.claude/settings.json写入{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: 你的TaoToken Key } }Model ID 根据你用的模型填比如claude-sonnet-4-20250514这类。配好之后 Claude Code 的请求就走 TaoToken 通道了后续排查 RTL 报错、生成 PowerShell 脚本都可以直接对话。如果你用 Cline 或 CC Switch 这类工具配置逻辑一样Base URL 填https://taotoken.net/apiKey 填控制台生成的Model ID 按需选。三件套缺一不可少一个都会报 401。Key 的生成在控制台的 API Keys 页面建议按项目分 Key方便后面排查是哪个环节出的问题。接入文档里有各工具的具体配置示例遇到不确定的格式可以直接对照。2.3 环境变量设置工具链装好后PowerShell 里设置环境变量。注意CODE_BASE_PATH必须指向E906_RTL_FACTORY目录也就是gen_rtl的父目录而且路径分隔符用/而不是\$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为什么路径分隔符这么讲究因为 Makefile 里用${CODE_BASE_PATH}/gen_rtl/...拼接路径如果CODE_BASE_PATH里带反斜杠在某些上下文里会被解释成转义字符导致文件找不到。这个坑我在后面排障部分会再展开。3. 可复制的仿真环境配置与项目结构3.1 顶层目录结构E906 的项目结构不算复杂但第一次看容易迷路。核心目录就两个E906_RTL_FACTORY放 RTL 源码smart_run放仿真环境。e906/ ├── E906_RTL_FACTORY/ # RTL 源代码 │ └── gen_rtl/ │ ├── cpu/rtl/ # 顶层openE906.v, pa_core.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/ # 启动代码、链接脚本、C 库 │ └── cases/ # 测试程序源码 └── doc/ # 文档3.2 E906 的三总线结构理解 E906 的总线结构对后面排查仿真问题很关键。处理器核有三种总线接口IAHBL 是指令 AHB-Lite 总线连到内部指令存储器iahb_mem_ctrlBMU 把地址0x00000000 - 0x001FFFFF映射到这条总线。DAHBL 是数据 AHB-Lite 总线连到内部数据存储器dahb_mem_ctrl地址范围0x20000000 - 0x2FFFFFFF。BIU 是外部 AHB 总线连到 SoC 级外设PASS 检测地址0x6000FFF8就在这条总线上。BMU 的配置参数在cpu_sub_system_ahb.v里.pad_bmu_iahbl_base (12h000), // IAHB 基地址 .pad_bmu_iahbl_mask (12he00), // IAHB 地址掩码 .pad_bmu_dahbl_base (12h200), // DAHB 基地址 .pad_bmu_dahbl_mask (12hfff), // DAHB 地址掩码3.3 PASS/FAIL 机制Testbench 通过监视 BIU 总线上的写操作来判断仿真结果。往0x6000FFF8写入0x00000FFF或0xFFFF0000就是 PASS写入0x00000EEE或0xEEEE0000就是 FAIL。如果连续 5000 周期没有指令退休或者仿真时间超过 700ms也会判 FAIL。这个机制意味着你写的测试程序最后必须往这个地址写 PASS 值否则仿真会超时。后面手工编码最小程序时就是利用这个规则。3.4 核心配置文件smart_run/setup/smart_cfg.mk是仿真配置文件里面定义了工具链路径、编译选项、测试用例列表等。如果你要改默认的 VCS 工具链为 iverilog需要确认这个文件里的SIM_TOOL变量。不过我们后面直接手动调 iverilog不依赖 Makefile 的工具链选择。RTL 文件列表在E906_RTL_FACTORY/gen_rtl/filelists/下E906_asic_rtl.fl是处理器核的文件列表tdt_dmi_top_rtl.fl是调试模块的。仿真时两个都要带上。4. 从编译到仿真的完整验证流程4.1 手工编码最小程序验证环境在装 RISC-V 工具链之前先用手工编码的机器码验证仿真环境是否正常。写一个最简单的 RV32I 程序往0x6000FFF8写入0xFFFF0000PASS 条件。指令机器码如下指令机器码说明lui x4, 0x600160010237x4 0x60010000addi x4, x4, -8FF820213x4 0x6000FFF8lui x3, 0xFFFF0FFFF01B7x3 0xFFFF0000sw x3, 0(x4)00322023写 0x6000FFF8 → PASSbeq x0, x0, 000000063无限循环创建work/case.pat注意这里要做字节交换37020160 130282FF B701FFFF 23203200 63000000为什么是字节交换后的值因为 testbench 用$readmemh读取 pat 文件后直接写入 RAM 阵列时做了字节翻转。写入时 ram0 拿到的是 MSB但读取时 ram0 被解释为 LSB整个指令字被字节翻转了。所以 pat 文件里要存字节交换后的值这样经过翻转才能还原成正确的指令。4.2 编译 RTL 并运行仿真$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 -Diverilog1 -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 * **********************************************如果看到这个输出说明仿真环境正常可以进入完整流程。4.3 完整 C 程序编译流程装好 Xuantie GCC 后编译 hello_world 程序。先设置环境变量$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编译 C/汇编源码$CFLAGS (-mtunee906, -marchrv32imafc_zifencei, -mabiilp32f, -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链接$LFLAGS (-T, linker.lcf, -nostartfiles, -mtunee906, -marchrv32imafc_zifencei, -mabiilp32f) $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生成 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.hex4.4 S-record 转 pat 文件原始项目用Srec2vmem做转换但那是 Linux x86_64 二进制文件Windows 上跑不了。我用 PowerShell 写了一个替代脚本srec2pat.ps1核心逻辑是解析 S-record、构建字节缓冲区、按 32 位字生成 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.pat4.5 编译 RTL 并仿真iverilog -o xuantie_core.vvp -Diverilog1 -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****** 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 * **********************************************4.6 生成波形去掉-DNO_DUMP重新编译就会生成test.vcdiverilog -o xuantie_core.vvp -Diverilog1 -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 vvp xuantie_core.vvp gtkwave test.vcd带-DNO_DUMP的仿真更快适合功能验证不带的话生成波形适合调试时序问题。5. 常见报错排查401、local proxy failed、reading choices 与 OAuth5.1 TaoToken 通道报错如果你在 Claude Code 里配了 TaoToken 但请求失败先看报错类型。401 Unauthorized通常是 Key 没填对或者 Base URL 写错了。检查settings.json里的ANTHROPIC_BASE_URL是不是https://taotoken.net/apiKey 是不是从控制台 API Keys 页面生成的。local proxy failed一般是本地代理配置冲突。如果你之前配过其他工具的代理环境变量里可能有残留的HTTP_PROXY或HTTPS_PROXY清掉再试Remove-Item Env:HTTP_PROXY -ErrorAction SilentlyContinue Remove-Item Env:HTTPS_PROXY -ErrorAction SilentlyContinuereading choices报错通常是 Model ID 填错了或者模型名不在 TaoToken 支持的列表里。去模型对话页面确认一下可用的模型 ID填到配置里。OAuth 相关报错一般是 Claude Code 的登录态问题。如果你用的是 API Key 模式不需要走 OAuth 流程确认配置里没有混用两种认证方式。5.2 PowerShell 5.1 的 byte 左移陷阱症状生成的 pat 文件全是00000000。原因PowerShell 5.1 里对[byte]类型做-shl N运算时如果 N 8中间结果超出 byte 范围255会被截断为 0。# 错误 $b [byte]0x17 $b -shl 24 # → 0x00000000被截断 # 正确 [int]$b -shl 24 # → 0x17000000解决方案所有位运算前先转[int]。这个坑我在 srec2pat.ps1 里已经处理了。5.3 PowerShell 哈希表键类型严格匹配症状哈希表有 24164 条数据但$data[0]返回$null。原因[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(0000, 16) # 转 Int32 $data[$addr] 0x17 $data[0] # → 0x175.4 新版本 GCC 的 fence.i 指令症状编译 crt0.s 时报错unrecognized opcode fence.i, extension zifencei required。原因较新的 RISC-V GCC14.x将fence.i从基础 ISA 分离到了zifencei扩展。解决方案在 march 中添加_zifencei# 旧 march会报错 -marchrv32imafc # 新 march -marchrv32imafc_zifencei5.5 iverilog 的 -c 标志症状iverilog: unknown option -c。说明smart_run 的 Makefile 使用-c标志读取命令文件这是较新版本 Icarus Verilog 才支持的特性。本文使用的 iverilog 12.0 支持此标志如果遇到问题可改用-f。5.6 make 未安装症状make buildcase CASEhello_world提示命令未找到。解决方案winget install ezwinports.make或者在没有 make 的情况下手动执行编译步骤详见第 4 节。5.7 路径分隔符问题症状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_FACTORY5.8 -mtunee906 不被识别症状cc1.exe: error: unknown cpu e906 for -mtune。说明-mtunee906是平头哥 GCC 中 TIER 特有的优化参数。如果使用上游 RISC-V GCC非 Xuantie 版本这个选项不会被识别。使用平头哥官方工具链则无此问题。6. 把 E906 仿真接入日常开发流跑通一次仿真只是开始真正要把它用起来得把流程固化下来。我自己的做法是在smart_run/work目录下放一个run_sim.ps1脚本把环境变量设置、编译、转换、仿真串起来每次改完 RTL 或 C 代码直接跑脚本。脚本里关键的一点是把 TaoToken 的 Key 从环境变量读取而不是硬编码在文件里。这样换机器或者分享脚本时不会泄露凭据$env:ANTHROPIC_API_KEY $env:TAOTOKEN_KEY如果你用 Claude Code 辅助排查 RTL 编译错误可以在对话里直接贴 iverilog 的报错信息让它帮你定位是文件列表缺失还是语法问题。我试过把iverilog: unknown option -c这个报错贴进去它直接给出了改用-f或者升级 iverilog 版本的建议比翻文档快。长期做 RTL 验证的话建议把常用测试用例的编译命令整理成表格需要哪个直接复制。比如 hello_world 验证基本流程coremark 看性能ISA_RV32IMAC 验证指令集覆盖。每次改完 RTL 先跑最小程序确认环境没坏再跑完整用例。波形文件会占不少磁盘空间-DNO_DUMP模式适合日常回归只在调试具体时序问题时才开波形。GTKWave 打开 VCD 后把关键信号时钟、复位、PC、指令、总线地址加到波形窗口保存成.gtkw文件下次直接加载不用每次重新找信号。最后说一个实际经验E906 的 testbench 字节序设计是刻意的不是 bug。原始项目的Srec2vmem在转换过程中已经包含了字节交换所以.hex → .pat的转换必须保持同样的约定。我写的srec2pat.ps1保持了和原始工具一致的转换逻辑如果你在 Linux 上用原始Srec2vmem转出来的 pat 文件和我的脚本转出来的不一致那说明字节序处理有问题需要检查脚本里的交换逻辑。