Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ws63-qemu 文档

本文档集按 Diátaxis 框架组织,分为四类,对应你阅读时的四种不同需求:

你想……去说明
第一次上手,跟着一步步把固件跑起来tutorials/学习导向:手把手的入门课
完成一个具体任务(构建 / 运行 / 调试 / 移植…)how-to/任务导向:照着做的操作配方
查某个事实(地址 / 寄存器 / 指令 / 选项…)reference/信息导向:精确、干燥的技术清单
理解某件事为什么这样(设计取舍 / 边界 / 可行性)explanation/理解导向:背景与权衡的讨论

项目是什么:在 QEMU 上仿真 HiSilicon WS63(RISC-V RV32IMFC,Wi-Fi 6 + BLE + SLE/星闪 SoC), 用于无硬件运行仓库内预构建 fbb_ws63 C SDK 厂商固件,也可运行 ws63-rs 裸机固件做开发验证。 项目级概览见仓库根的 README.md,规划见 ROADMAP.md,变更见 CHANGELOG.md。


教程(Tutorials)— 学习导向

操作指南(How-to)— 任务导向

参考(Reference)— 信息导向

解释(Explanation)— 理解导向

QEMU 官方文档

本项目是上游 QEMU 的 fork,通用知识以官方文档为准:

文档自身的检查(CI)

本文档集受 CI 守护(见仓库 .github/workflows/):

  • docs.yml(每次涉及 .md 的 push/PR):scripts/check-docs.py 校验内部链接 / 锚点 / Diátaxis 布局 / 无孤儿页 / 每页有 H1,markdownlint-cli2(配置 .markdownlint-cli2.yaml)做质量检查。
  • link-check.yml(每周 + 手动):lychee 校验外部 URL 可达性(与 PR 解耦,避免第三方站点抖动阻塞合并)。

本地可直接跑:python3 scripts/check-docs.py 与 npx markdownlint-cli2。


真值来源:内存布局 = ws63-rs/ws63-rt/{memory.x,layout.ld} 与 fbb_ws63 C SDK 板级配置相互对齐; 外设基址/寄存器 = ws63-rs/ws63-pac/ws63-svd/WS63.svd;寄存器行为 = fbb_ws63 C SDK hal_*_regs_def.h + ws63-rs HAL 交叉验证。

教程 · 学习导向

手把手的入门课。第一次上手 ws63-qemu 从这里开始——跟着每一步做即可,不需要预先理解内部原理。

教程 1:环境准备与安装

本教程带你第一次把环境装好:克隆本仓库、装系统依赖、从源码构建仿真器。 跑完后你就万事俱备,可以进入 教程 2:跑通第一个固件。

跟着每一步做即可,不需要预先理解内部原理。本教程只给「最小够用」的引导式安装;完整的安装选项 (下载预编译 Release、依赖明细表、各平台差异)见操作指南 安装与构建仿真器。

适用平台:本教程的默认 happy path 在仍处于官方维护周期内的 Ubuntu/Debian x86_64 上验证。 Ubuntu 仅包含标准维护期,不包含 Ubuntu Pro / ESM / Legacy add-on 等延长服务;Debian 不包含 ELTS 等延长支持。 其他平台也能构建,详见上面的操作指南。 你不需要 WS63 硬件(EVB)。

前置

  • git:第 1 步需要用 git clone 拉取本仓库。若系统还没有 git,请先用系统包管理器安装(如 sudo apt-get install git)。
  • sudo:第 2 步装系统依赖需要。

第 1 步:克隆仓库

入门路径只需要仿真器仓库;可运行的预构建 C SDK 固件已经提交在 tests/csdk/。

git clone https://github.com/hispark-rs/hisi-riscv-qemu.git
cd hisi-riscv-qemu

第 2 步:安装系统依赖

bash scripts/setup-deps.sh

它会用 apt-get 装上 git / build-essential / pkg-config / ninja / meson / glib / pixman / flex / bison / python3 / libslirp-dev 等。需要 sudo。

各依赖的作用、非 Debian 平台(macOS brew / Windows MSYS2)的等价包,见 安装与构建仿真器 §构建依赖明细。

第 3 步:构建仿真器

想跳过这 10–20 分钟的构建? Ubuntu 上可直接下载预编译二进制、解压即用,无需装构建依赖(第 2 步) 也无需源码构建:见 安装与构建仿真器 §方式 A:下载预编译 Release。 本教程走源码构建(可移植、任意平台);拿到 qemu-system-riscv32 后即可进入下一篇教程。

bash scripts/build.sh

这一步会浅克隆固定版 QEMU(默认 v10.0.0)到 ./qemu/,注入 WS63 板卡文件、应用 patch-series,然后只构建 riscv32-softmmu 单目标。首次约 10–20 分钟(取决于核数)。产物是 ./qemu/build/qemu-system-riscv32。

构建完成后,确认机器已注册:

./qemu/build/qemu-system-riscv32 -M help | grep ws63

应输出含 ws63 的一行。看到了就说明仿真器侧准备好了。

你装好了

现在你有:从源码构建的、带 WS63 机器的 QEMU,以及仓库内可直接运行的预构建固件。

下一步

→ 教程 2:跑通第一个固件 —— 运行预构建固件、看到串口输出。

想了解更完整的安装方式(预编译 Release、各平台、依赖表)→ 安装与构建仿真器。

教程 2:跑通第一个固件

本教程接着 教程 1:环境准备与安装——假设你已经构建好仿真器,且当前在 hisi-riscv-qemu 目录里。现在我们运行仓库内预构建固件,在终端看到输出。

你将得到什么

一个真实的厂商 C SDK 固件(默认 tests/csdk/dma.elf)在仿真的 WS63 上启动,跑通 DMA 内存搬运样例并打印成功标志。 这证明仿真器、机器模型、xlinx 指令和串口输出链路都能工作。

第 1 步:运行默认固件

scripts/run.sh 不带参数时会运行仓库自带的预构建 C SDK fixture:

bash scripts/run.sh

终端应出现:

dma memory copy test succ

看到这一行,说明C SDK 固件 → DMA 模型 → 自定义 HiSilicon UART → -serial → 你的终端这条链路全通了。

退出 QEMU:先按 Ctrl-A,松开,再按 X。

想看到 GPIO 翻转的痕迹,开追踪:

DEBUG=1 bash scripts/run.sh
#  随后查看 qemu.log,里面有中断/未建模访问等记录

第 2 步:换一个预构建固件

显式传入 ELF 路径即可运行其它 fixture。比如 ADC 样例:

bash scripts/run.sh tests/csdk/adc.elf

终端应打印:

voltage: N mv

N 是样例读到的模拟电压值。想临时改无参默认固件,也可以用:

DEFAULT_ELF=tests/csdk/tcxo.elf bash scripts/run.sh

你做到了

你已经:在无硬件、无外部固件仓库、无 Rust 工具链的情况下跑通了真实固件,并看到了串口输出。

下一步

操作指南 · 任务导向

照着做的操作配方,面向已经知道自己要完成什么任务的人。

安装与构建仿真器

本指南覆盖两条获得 qemu-system-riscv32 -M ws63 的路径:下载预编译 Release(最快)或从源码构建(可移植)。 任选其一。第一次上手建议直接走教程 环境准备与安装 → 跑通第一个固件。

系统要求

  • OS:Linux(主要在 Ubuntu/Debian x86_64 上验证);其他平台请走「从源码构建」。
  • 默认测试范围:happy path 只覆盖仍处于官方维护周期内的 Ubuntu/Debian。Ubuntu 以标准维护期为准, 不含 Ubuntu Pro / ESM / Legacy add-on 等延长服务;Debian 不含 ELTS 等延长支持。
  • 磁盘:QEMU 源码树 + 构建产物约 2 GB。
  • 首次构建耗时:约 10–20 分钟(build.sh 只构建 riscv32-softmmu 单目标)。
  • 运行时:仅需 qemu-system-riscv32 二进制(动态依赖 glib/pixman)。

方式 A:下载预编译 Release(最快)

从 Releases 下载对应版本资产:

资产说明
qemu-system-riscv32-ws63-<ver>Ubuntu 构建的仿真器二进制(动态链接 glibc/glib/pixman)
ws63-qemu-src-<ver>.tar.gz源码包(src/ patches/ scripts/ tests/ docs/ 等)——其他平台用它重建
SHA256SUMS校验和
sha256sum -c SHA256SUMS
chmod +x qemu-system-riscv32-ws63-<ver>
./qemu-system-riscv32-ws63-<ver> -M help | grep ws63   # 确认机器已注册

预编译二进制是在 Ubuntu 上链接的。若你的发行版 glibc/glib/pixman 版本不兼容(error while loading shared libraries),请改用方式 B 从源码构建。

方式 B:从源码构建(可移植)

# 1. 安装构建依赖(Debian/Ubuntu;需 sudo)
bash scripts/setup-deps.sh
#   等价手动:apt-get install git build-essential pkg-config ninja-build meson \
#             libglib2.0-dev libpixman-1-dev flex bison python3 python3-venv zlib1g-dev libslirp-dev

# 2. 克隆固定版 QEMU、注入 WS63 板卡 + xlinx ISA、构建
bash scripts/build.sh
#   产物:./qemu/build/qemu-system-riscv32

build.sh 做的事(幂等,可反复运行做增量构建):

  1. 浅克隆 QEMU $QEMU_TAG(默认 v10.0.0,可改)到 ./qemu/;若无 patches/$QEMU_TAG/ 则报错列出已支持版本;
  2. 拷入新文件:板卡源 src/hw/riscv/ws63.c、xlinx 解码器 trans_xlinx.c.inc、qtest ws63-test.c;
  3. 应用该版本的 patch-series patches/$QEMU_TAG/0001..*.patch(对既有 QEMU 文件的改动,见 patch-series 参考);
  4. ./configure --target-list=riscv32-softmmu --enable-slirp 后 make。

构建相关环境变量:QEMU_TAG(默认 v10.0.0;另维护 v10.2.3、v11.0.1、v9.2.4)、QEMU_DIR(默认 <repo>/qemu)、QEMU_REPO、JOBS(默认 nproc)。

构建依赖明细

scripts/setup-deps.sh 会自动识别宿主系统并用对应包管理器安装。下表是 QEMU 构建链各依赖的作用 (Debian/Ubuntu 包名;其它平台的等价包由脚本按平台映射):

依赖作用缺失时的症状
git浅克隆固定版 QEMU 源码树build.sh 无法 clone
build-essential(gcc/g++/make)C11 编译器 + make 驱动configure 报「no working compiler」
pkg-config定位 glib/pixman/slirp 的头与库configure 找不到 glib
meson + ninja-buildQEMU 的构建系统(meson 配置 → ninja 编译)configure 报 meson/ninja 版本过低或缺失
libglib2.0-devQEMU 核心依赖(GLib,对象/事件/数据结构)configure 致命错误:glib 必需
libpixman-1-dev像素操作库(显示后端)图形相关目标无法构建
libslirp-dev用户态网络栈,启用 --enable-slirp → -nic user连接性示例(net_ping)的 SLIRP 后端不可用
flex + bison构建期代码生成(词法/语法)部分生成步骤失败
python3 + python3-venvQEMU 配置期建 venv(mkvenv)跑构建脚本configure 报「found no usable distlib / venv」
zlib1g-dev压缩库(镜像/迁移等)链接期缺 -lz
patchelfRelease 打包阶段修正二进制 rpath(仅打包用)仅影响 release 工件,不影响本地构建

磁盘/耗时再次提醒:QEMU 源码树 + 构建产物约 2 GB,首次构建约 10–20 分钟。

各平台

setup-deps.sh 已覆盖三类宿主,自动选择包管理器(无需手动指定):

平台包管理器备注
Linux(Debian/Ubuntu,x86_64 / aarch64)apt-get主验证平台;需 sudo
macOSHomebrew(brew)装 meson ninja pkgconf glib pixman libslirp dylibbundler;脚本另为 QEMU 的 mkvenv 补 distlib
Windows(MSYS2 / MINGW64)pacman在 MSYS2 shell 内运行;CI 用 msys2/setup-msys2

其它发行版请手动安装上表依赖的等价包(参见 QEMU 官方构建说明,见文末「QEMU 官方文档」)。

验证安装:

./qemu/build/qemu-system-riscv32 -M help | grep ws63   # 应输出含 "ws63" 的一行

准备固件

仿真器运行任意 WS63 ELF。入门和测试不需要另准备固件:仓库已经带了预构建 fixture;自行构建 C SDK 或 Rust 固件是进阶路径。

(a) 仓库自带预构建 C SDK fixture —— tests/csdk/ 里提交了 fbb_ws63 C SDK 外设样例 ELF,可直接运行:

bash scripts/run.sh                 # 默认 tests/csdk/dma.elf
bash scripts/run.sh tests/csdk/adc.elf
DEFAULT_ELF=tests/csdk/tcxo.elf bash scripts/run.sh

这些 fixture 由 scripts/csdk-test.sh 在 CI 中使用,适合做 happy path 和外设模型回归。

(b) fbb_ws63 C SDK(厂商 gcc) —— 用 hispark-rs/fbb_ws63-qemu (fbb_ws63 C SDK 的 QEMU 适配 fork,已为本仿真器预裁剪 BT/WiFi),SDK 内置工具链:

# 在 fbb_ws63-qemu 仓库中
cd src && python3 build.py ws63-liteos-app -c -ninja
#   产物:output/ws63/acore/ws63-liteos-app/ws63-liteos-app.elf

重新生成本仓库测试 fixture:scripts/build-csdk-samples.sh(从 fbb_ws63 checkout 选一个 CONFIG_SAMPLE_SUPPORT_*、干净构建、strip 到约 400 KB)。

(c) ws63-rs(Rust 裸机) —— 需要 hisi-riscv 自定义 Rust 工具链(rv32imfc 硬浮点、无原子,内置为 builtin target)。先装好基础 Rust 环境(rustup)——本工具链以 rustup 工具链形式安装,依赖 rustup; 若已装过可跳过。安装方式与上游 hisi-riscv-rs 一致(解压进 rustup 的 toolchains 目录,自动识别,无需 toolchain link):

HOST=x86_64-unknown-linux-gnu   # 或 aarch64-unknown-linux-gnu / aarch64-apple-darwin / x86_64-pc-windows-msvc
curl -LO https://github.com/hispark-rs/hisi-riscv-rust-toolchain/releases/download/v1.96.0-2/hisi-riscv-rust-1.96.0-$HOST.tar.gz
mkdir -p ~/.rustup/toolchains/hisi-riscv
tar xzf hisi-riscv-rust-1.96.0-$HOST.tar.gz --strip-components=1 -C ~/.rustup/toolchains/hisi-riscv
rustup toolchain list | grep hisi-riscv   # 验证:应输出 hisi-riscv
# 在 ws63-rs 仓库中:
cargo build -p blinky --release
#   产物:target/riscv32imfc-unknown-none-elf/release/blinky

工具链安装的权威步骤与各平台细节以上游为准: hisi-riscv-rs · 安装 hisi-riscv 工具链。

QEMU 官方文档

本项目是上游 QEMU 的 fork,构建/运行的通用知识以 QEMU 官方文档为准:

相关

运行固件

本指南讲怎么在 ws63-qemu 上运行一个固件 ELF——包括基本运行、run.sh 各开关、仓库自带预构建 fixture,以及运行 fbb_ws63 C SDK 固件(含用 NV=1 回填分区表/NV)。run.sh 全部选项的清单见 运行选项参考。

基本运行

# 用包装脚本(推荐):不带参数默认跑 tests/csdk/dma.elf
bash scripts/run.sh
bash scripts/run.sh path/to/firmware.elf
DEFAULT_ELF=tests/csdk/tcxo.elf bash scripts/run.sh

# 直接调用 QEMU
./qemu/build/qemu-system-riscv32 -M ws63 -nographic -serial mon:stdio -kernel firmware.elf

退出 QEMU:Ctrl-A 然后 X。

串口约定:-serial mon:stdio 把 UART0 的 TX 输出到你的终端,终端输入送入 UART0 的 RX(中断使能时触发 IRQ 53)。mon: 表示同一通道复用 QEMU monitor(Ctrl-A C 切换)。

常用开关

通过环境变量开启(run.sh <elf> [额外 qemu 参数...],额外参数原样透传给 QEMU):

ICOUNT=1 bash scripts/run.sh fw.elf          # 可复现的确定性指令计时(IPC=1 近似,非周期精确)
NV=1 bash scripts/run.sh ws63-liteos-app.elf # C SDK app + 分区表/NV 回填
DEBUG=1 bash scripts/run.sh fw.elf           # 写 qemu.log 追踪(见调试与追踪指南)
SEMIHOST=1 bash scripts/run.sh selftest.elf  # 固件用 semihosting SYS_EXIT 设置 QEMU 退出码

各变量完整语义见 运行选项参考。DEBUG/trace 相关用法见 调试与追踪。

运行仓库自带 fixture

tests/csdk/ 里有预构建的 fbb_ws63 C SDK 外设样例。它们是当前文档 happy path,不需要相邻的 hisi-riscv-rs checkout,也不需要安装 Rust 或厂商 SDK 工具链。

bash scripts/run.sh                 # 默认 tests/csdk/dma.elf
bash scripts/run.sh tests/csdk/adc.elf
DEFAULT_ELF=tests/csdk/tcxo.elf bash scripts/run.sh

默认 dma.elf 的成功标志是:

dma memory copy test succ

批量断言这些 fixture 用 运行测试 里的 scripts/csdk-test.sh。

运行 fbb_ws63 C SDK 固件

仿真器能跑 fbb_ws63 C SDK 厂商 gcc 编译的固件(依赖 xlinx 自定义 ISA,已实现)。这类固件可由 hispark-rs/fbb_ws63-qemu 构建—— 它是 fbb_ws63 C SDK 的 QEMU 适配 fork,已为本仿真器预裁剪(注释掉 BT/WiFi 任务以干净启动),产出可直接 boot 的 固件。详见其 README-QEMU.md。

# 在 fbb_ws63-qemu 仓库中构建(厂商工具链已内置)
cd src && python3 build.py ws63-liteos-app -c -ninja
#   产物:output/ws63/acore/ws63-liteos-app/ws63-liteos-app.elf

# 运行
qemu-system-riscv32 -M ws63 -nographic -serial mon:stdio -kernel output/ws63/acore/ws63-liteos-app/ws63-liteos-app.elf

flashboot 会跑出时钟 bring-up → flash init 的 UART 输出;ws63-liteos-app(裁剪 BT+WiFi)会稳定启动到 cpu 0 entering scheduler 并空转运行。

用 NV=1 回填分区表 + NV

-kernel 启动直接把 app 装入 RAM、跳过 flashboot(真机里 flashboot 负责把分区表/NV 写进 flash),所以 flash XIP 窗口(0x200000,RAM 背靠)默认是空的,C SDK 的 uapi_partition_get_info() / NV 读取会失败 ([UPG] ...flash_start_addr fail、nv read sw fail)。

解法:NV=1 用 -device loader 把分区表 + NV 回填进 flash:

NV=1 bash scripts/run.sh ws63-liteos-app.elf

回填的镜像(tests/csdk/flash/,见其 manifest.txt):

镜像XIP 地址内容
partition_params.bin0x200000params 区,分区表在 0x200380(magic 0x4b87a54b)
nv.bin0x5FC000软件 NV 键值区
nv_factory.bin0x20C000出厂 NV 键值区

三者都落在 app 自身 XIP 区(~0x230300–0x294000)之外,故不冲突。回填后分区表解析成功、NV 读取成功, UPG 的 flash_start_addr fail 消除。

唯一残留:xo_trim(晶振温补标定)等逐芯片出厂标定键——生产时烧录,任何构建产物的 NV 都不含, 固有缺失、非缺陷。详见 已知边界与非目标。

含 BT/WiFi 的 app

含 BT/WiFi 任务的 C SDK app 会在子系统深层初始化崩于 ROM 数据墙(vtable/NV/efuse/RF 标定无 dump), 需在 config.py 注释 BGLE/BTH/WIFI_TASK_EXIST 裁剪这些任务。原因见 已知边界与非目标。

故障排查

现象原因 / 解法
QEMU not built: .../qemu-system-riscv32先 bash scripts/build.sh(见 安装与构建)
firmware ELF not foundrun.sh 第一个参数给对 ELF 路径;若是无参运行,确认 tests/csdk/dma.elf 存在或用 DEFAULT_ELF=... 覆盖
-M help 不含 ws63构建未注入成功;删 qemu/ 重跑 build.sh
固件一上来就 illegal instruction 大量陷阱多为 xlinx ISA 或 ROM 调用问题;DEBUG=1 看 qemu.log 的 pc 落点,ROM 区(0x109xxx)是预期被拦截的
C SDK app 反复打印 [UPG] ...flash_start_addr fail / nv read sw fail用 NV=1 回填;残留的 xo_trim 一行是固有的
C SDK BT/WiFi 任务崩溃预期(ROM 数据墙);裁剪 BGLE/BTH/WIFI_TASK_EXIST 任务
计时每次运行都不同默认虚拟时间自由运行;要可复现用 ICOUNT=1

更完整的语义边界与 FAQ 见 已知边界与非目标。

相关

调试与追踪

本指南讲怎么在 ws63-qemu 上调试固件、以及怎么追踪/记录仿真器的内部行为。

GDB 源码级调试

QEMU 内置 gdbstub 对本机器开箱即用。透传 -s -S(= 在 :1234 起 gdbstub 并冻结在复位):

bash scripts/run.sh fw.elf -s -S
# 另一终端:
riscv32-unknown-elf-gdb fw.elf   # 或 gdb-multiarch
(gdb) target remote :1234
(gdb) break main
(gdb) continue

注意:GDB 的反汇编器不认识 xlinx 自定义指令,落在 ROM/xlinx 区的指令会显示为未知。源码级 断点/单步/查看变量在标准指令上正常工作。xlinx 指令是什么见 xlinx ISA 参考。

追踪与日志

DEBUG=1 bash scripts/run.sh fw.elf   # 加 -d int,guest_errors,unimp -D qemu.log

或手动给 QEMU 传 -d:

  • -d unimp:打印对未建模/兜底外设的访问(按地址定位)。哪些是兜底见 内存映射。
  • -d int:打印每次中断投递(确认 IRQ 号/向量)。
  • -d guest_errors:guest 侧错误访问。
  • -D qemu.log:把上述写入文件。

WS63 模型的 trace 事件

WS63 模型用 QEMU 正规 trace 事件暴露关键行为:

-d trace:ws63_gpio_*    # GPIO 输出变化
-trace ws63_dma_xfer    # DMA 搬运(也可单独开启)
事件含义
ws63_gpio_set / ws63_gpio_clrGPIO 输出脚拉高 / 拉低
ws63_dma_xferDMA 通道搬运一次

Rust 进阶冒烟里的 smoke-test.sh 即据 ws63_gpio_* 断言 blinky 的 GPIO 翻转。

典型排查路径

  • 固件大量 illegal instruction:DEBUG=1 看 qemu.log 里陷阱 pc 的落点。落在 ROM 区(0x109000..0x14C000) 是预期会被 ws63_rom_call 拦截的(见 ROM 桩目录);落在别处才是真问题。
  • 中断没投递:-d int 看是否有对应 IRQ 号的投递行;中断机制见 中断控制器设计。
  • 访问到未建模外设:-d unimp 按地址定位,对照 内存映射 看是哪个外设。

QEMU 官方文档

相关

运行测试

ws63-qemu 有三层测试,互补:寄存器级 qtest(免启动、毫秒级)、C SDK 外设样例(仓库内预构建厂商固件)、 ws63-rs Rust 冒烟(进阶/CI 覆盖)。本指南讲怎么各自跑起来。每层验证了什么、覆盖到哪,见 验证覆盖范围参考。

寄存器级 qtest(免启动回归)

scripts/qtest.sh 用 libqtest 直接驱动外设寄存器(测试进程扮演 CPU 角色,不启动固件),毫秒级验证 GPIO/UART/timer/INTC/DMA 模型的寄存器语义。

bash scripts/qtest.sh          # 构建 tests/qtest/ws63-test 并运行(4 例,TAP 输出)

覆盖:GPIO 数据 set/clr/OEN/INT-EN 读写;UART FIFO/行状态复位值;timer 装载/使能/触发 + 经 INTC 投递 IRQ 26(qtest_irq_intercept_in);DMA 通道 0 mem→mem 搬运 + 完成位。

C SDK 外设样例

scripts/csdk-test.sh 启动 tests/csdk/ 里预编译的 fbb_ws63 C SDK 厂商固件并断言各自的 UART 成功标志—— 用真实厂商固件交叉验证外设模型。纯净 + 免 SDK/工具链(用已提交的 fixture):

bash scripts/csdk-test.sh      # 当前 5/5 绿

样例列表、成功标志、以及两个「已记录但未断言」样例(timer / watchdog)的诊断,见 验证覆盖范围 §C SDK 外设样例。

重新生成 fixture:scripts/build-csdk-samples.sh(从 fbb_ws63 checkout 选一个 CONFIG_SAMPLE_SUPPORT_*、 干净构建、strip 到 ~400 KB)。

ws63-rs(Rust)冒烟

scripts/smoke-test.sh 启动 ws63-rs Rust 固件,端到端断言串口/MMIO 标志。这是开发和 CI 验证路径,不是入门 happy path;运行前需要先 checkout 并构建 hisi-riscv-rs 示例固件。

WS63_RS=../ws63-rs bash scripts/smoke-test.sh

固件列表与成功判据见 验证覆盖范围 §ws63-rs 冒烟。

在 CI 里跑的是什么

ci.yml 每次 push/PR 跑上述全部三层(Ubuntu);qtest-matrix.yml 在每个有 patches/<tag>/ 的 QEMU 版本上跑 qtest。门禁清单见 验证覆盖范围 §持续验证。

QEMU 官方文档

相关

扩展掩膜 ROM 桩

当一个固件调用了尚未仿真的掩膜 ROM 函数时,它会走 default 分支返回 0(成功)——多数情况无碍,但有时你需要 真正仿真某个 ROM 函数。本指南讲怎么定位、确认、新增一个 ROM 桩。

背景(为什么需要这些桩、机制如何)见 ROM 桩目录参考 与 已知边界。

找一个新的 ROM 函数地址

ROM 函数地址落在 0x109000..0x14C000 区间。从 SDK 的符号表查:

grep '<name>' fbb_ws63/src/drivers/chips/ws63/rom_config/acore/acore.sym

地址落在该区间即为 ROM 函数,运行时会走 ws63_rom_call。

看哪些 ROM 地址被实际调到

在 ws63_rom_call 顶部临时加一行打印:

fprintf(stderr, "ROM pc=0x%x\n", pc);

注意 *printf_s 族很吵,按地址区间过滤再看。

新增一个仿真

在 ws63_rom_call(位于 target/riscv 补丁的 cpu_helper.c)的 switch 里按地址加 case,从 a0..a7 (gpr[10..17])取参,设返回值 ret:

case 0x10aeb4:  /* memcpy_s(dst, dmax, src, n) */
    /* 从 gpr[10..13] 取参,写内存,设 ret */
    break;

返回约定:a0(gpr[10]) = ret;pc = ra(gpr[1]),即直接返回到调用者。

把改动落回 patch-series

改完 target/riscv/ 后,需要重新生成 patch-series 的 0001:

  1. 在 qemu/ 树里把改动提交。
  2. git format-patch 出对应补丁,覆盖 patches/<QEMU_TAG>/0001-target-riscv-*.patch (或最简单:git -C qemu diff -- target/riscv/ hw/riscv/meson.build ... 后手工归入对应 000N 补丁)。
  3. scripts/build.sh 重新构建验证。

务必覆盖整个 target/riscv/ 改动——漏掉如 translate.c(xlinx 解码 hook)的 hunk,全新克隆套用残缺补丁后 无法解码 xlinx、C SDK 固件零输出。生成后用 git apply --check 对 pristine 基线验证整条序列可干净套用。

多版本下补丁怎么维护,见 移植到新的 QEMU 版本。

相关

移植到新的 QEMU 版本

WS63 模型是上游 QEMU 的树外 overlay:新增文件(hw/riscv/ws63.c、trans_xlinx.c.inc、ws63-test.c)由 build.sh 拷入;对既有 QEMU 文件的改动走按版本分目录的 git format-patch 序列 patches/<tag>/。

这些改动会随 QEMU 版本漂移(头文件搬家、结构体/字段偏移变化、惯用法变更),所以序列按版本维护。本指南讲怎么把序列 移植到一个新的 QEMU tag。序列的结构与各补丁职责见 patch-series 参考。

步骤

  1. 确认未移植:

    QEMU_TAG=<new> QEMU_DIR=/tmp/q bash scripts/build.sh
    

    若 patches/<new>/ 不存在,build.sh 会失败并列出已支持版本。

  2. 套用最近版本的序列并解决冲突:克隆该 tag,拷入 src/ 文件,用 git apply --reject(或 GNU patch --fuzz)套用最近版本的序列,解决 .rej,修掉 API 漂移,直到能构建且 scripts/qtest.sh 通过。

  3. 重新生成序列:把改动按 0001/0002/0003 的同样分组提交,git format-patch,把结果放进 patches/<new>/(若拷入的 ws63.c 需要适配旧/新 API,再加一个 0004 兼容补丁)。

  4. 接入 CI:把版本加进 qtest-matrix.yml;如需把它设为默认基线,调整 QEMU_TAG 的默认值。

漂移有多大(参考)

同一套支持在不同版本间的漂移很说明问题:

  • 10.0 → 10.2:insn_len 移到 internals.h、CPU 定义改声明式 DEFINE_RISCV_CPU、decode_opc 改表驱动、 CharBackend→CharFrontend、exec/→system/address-spaces.h。
  • 10.2 → 11:六个 hw/*.h→hw/core/*.h 头大迁移。

——正因如此才按版本分目录维护。各版本 0004/0005 补丁的具体差异见 patch-series 参考。

相关

参考 · 信息导向

精确、干燥的技术清单——查地址、寄存器、指令、选项、覆盖范围时来这里。

WS63 内存映射与外设基址

本文件记录 ws63-qemu 机器模型所依据的 WS63 SoC 地址布局。真值来源:

  • 内存区域(RAM/Flash/TCM):ws63-rs/ws63-rt/memory.x,它是 fbb_ws63 C SDK 板级内存配置 src/drivers/boards/ws63/evb/memory_config/include/memory_config_common.h 的忠实转写。
  • 外设基址:ws63-rs/ws63-pac/ws63-svd/WS63.svd(各 <peripheral> 的 baseAddress;svd 现为 ws63-pac 的嵌套子模块)。

✅ 与 C SDK 一致:上表地址逐项匹配 memory_config_common.h—— BOOTROM_START=0x100000、ROM_START=0x109000、APP_ITCM_ORIGIN=0x14C000、 APP_DTCM_ORIGIN=0x180000、APP_SRAM_ORIGIN=0xA00000、FLASH_START=0x200000、 APP_PROGRAM_ORIGIN=0x230000+0x300=0x230300。memory.x 与 C SDK 不冲突。

易混淆点:platform_core.h 里的 MPU_ITCM_ADDR_BASE=0x80000、MPU_L2RAM_ADDR0_BASE=0x100000 是 MPU 保护区窗口(供内存保护单元用的粗粒度地址范围),不是代码/数据的实际链接地址, 是另一层概念。应用固件的真实放置以 memory_config_common.h / memory.x 为准。

内存区域(machine 建模)

区域基址大小QEMU 建模说明
BOOTROM0x0010_000036 KiBRAM启动 ROM(QEMU 中跳过原厂 bootloader)
ROM0x0010_9000268 KiBRAM应用 ROM
ITCM0x0014_C00016 KiBRAM指令紧耦合内存
DTCM0x0018_000016 KiBRAM数据紧耦合内存
FLASH0x0020_00008 MiBRAMXIP SPI NOR;-kernel ELF 载入于此
PROGRAM0x0023_0300—(FLASH 内)应用代码段起点 = 复位 PC / ELF entry
SRAM0x00A0_0000576 KiBRAM (-m bank)主系统 RAM(data/bss/栈)
栈顶0x00A9_0000——ORIGIN(SRAM)+LENGTH(SRAM)

固件复位流程(ws63-rt/asm/startup.S → src/startup.rs):关 PMP → 设 mtvec → 关中断 → 开 FPU → 设 gp/sp → 跳 runtime_init(开 cache、flash→RAM 数据重定位、清 BSS)→ main。

外设基址(来自 WS63.svd)

状态:已建模 = 专有 sysbus 设备(寄存器语义 + 必要时中断/搬运);已建模(部分) = 关键位真实、 其余影子;吸收 = create_unimplemented_device 兜底(读 0、不崩)。行为细节见 外设建模矩阵。

外设基址ws63-qemu 状态
SYS_CTL00x4000_0000已建模(时钟状态 TCXO/PLL 锁 + 系统复位记录)
GLB_CTL_M0x4000_2000已建模(在 SYS_CTL0 窗口内:芯片复位触发)
WDT0x4000_6000已建模(倒计时→超时复位 SoC)
TCXO / SYS_CTL1 窗口0x4400_0000(计数器 0x4400_04C0)已建模(部分)(64 位单调计数 + count-valid;覆盖 SYS_CTL1 区)
CLDO_CRG0x4400_1100已建模(部分):时钟门控生效(清/置 CKEN_CTL0 冻结/恢复定时器)¹
TIMER0x4400_2000已建模(×3 下数计数器 + 中断 26/27/28)
RF_WB_CTL0x4400_4000吸收(配置影子;RF/PHY 无线电不仿真)
EFUSE0x4400_8000已建模(OTP:写=按位或、STS boot-done、数据窗读回)
LSADC0x4400_C000已建模(触发转换 → 弹 14-bit 采样 + IRQ 72)
IO_CONFIG (pinmux)0x4400_D000已建模(ws63-pinmux 引脚复用织构)
TSENSOR0x4400_E000已建模(start→rdy + 10-bit 温度码)
UART00x4401_0000已建模(自定义 HiSilicon UART 设备)
UART1/20x4401_1000 / 0x4401_2000已建模(同 UART0)
I2C0/10x4401_8000 / 0x4401_8100已建模(回环 FIFO:TXR→RXR、SR 完成位、IRQ 31/32)
SPI0/10x4402_0000 / 0x4402_1000已建模(回环 FIFO:DR 写→顺序读回、WSR、IRQ 43/52)
PWM0x4402_4000已建模(部分)(影子 + START 自清 + PERIODLOAD_FLAG)
I2S0x4402_5000已建模(LEFT/RIGHT TX→RX 回环)
GPIO0/1/20x4402_8000 / 0x4402_9000 / 0x4402_A000已建模(输出 set/clr、输入、边沿/电平中断、真实信号网)
SPACC / PKE / KM0x4410_0000 / 0x4411_0000 / 0x4411_2000吸收(寄存器影子)
TRNG0x4411_4000已建模(FIFO_READY + 伪随机 xorshift)
SFC0x4800_0000已建模(部分)(SPI 命令 RDID/RDSR + flash XIP 窗口)
DMA0x4A00_0000已建模(通道使能即真正搬运内存 + 完成位 + IRQ 59)
SDMA0x520A_0000已建模(同 DMA 引擎;逻辑通道 8–11)
RTC0x5702_4000已建模(周期触发 IRQ 29 + CURRENT_VALUE 计数)
ULP_GPIO0x5703_0000吸收(寄存器影子)

¹ boot-critical:clock_init::init_clocks() 读 SYS_CTL0(TCXO 检测、PLL 锁定)+ 写 CLDO_CRG (时钟门控)。SYS_CTL0 已建模为返回「TCXO 检出 + PLL 已锁」,故 init_clocks() 不空转;CLDO_CRG 的时钟门控位真实生效(清 CKEN_CTL0 bit21 冻结定时器、置位恢复),其余位影子。

建模 vs 吸收的实现:已建模外设是注册的 sysbus 设备,映射覆盖在三个 create_unimplemented_device 兜底窗口(0x4000_0000 256 MiB、0x5200_0000、0x5700_0000)之上;只有未建模子区落到兜底 (读 0、不崩,可用 -d unimp 追踪)。全部 35 个 SVD 外设均已覆盖(见 外设建模矩阵)。

CPU

  • 真实芯片:RV32IMFC_Zicsr(硬件单精度浮点 ilp32f,无原子扩展 A),240 MHz,单 hart。
  • QEMU 用命名 CPU -cpu ws63(默认型号),= I/M/F/C + Zicsr/Zcf,关闭 A、D,无 MMU——与 WS63 ISA 完全一致(非超集;Zcb/Zcmp 禁用以让位 xlinx 自定义压缩编码)。复位 PC = ELF entry(0x0023_0300), 无 OpenSBI / 无 FDT(裸机)。 注:zawrs 在基础核默认开启且依赖 A,故一并关闭以保持 A 关闭。

相关

机器模型参考

-M ws63 机器模型的组件清单、自定义 UART 寄存器布局、以及中断控制器的 CSR/IRQ 号。实现位于 src/hw/riscv/ws63.c + target/riscv 补丁。为什么这样设计见 设计取舍 与 中断控制器设计。

机器组件(hw/riscv/ws63.c)

组件实现
机器类型MACHINE_TYPE_NAME("ws63") → -M ws63
CPU单 hart,命名 CPU -cpu ws63(默认型号)= rv32imfc(I/M/F/C + Zicsr/Zcf,关 A/D,无 MMU,禁 Zcb/Zcmp 让位 xlinx)
复位resetvec = -kernel ELF 的 entry(缺省 0x230300);无 OpenSBI/FDT
内存BOOTROM/ROM/ITCM/DTCM/FLASH 作 RAM,SRAM 作 -m bank(见 内存映射)
固件载入load_elf(-kernel, …, EM_RISCV, …),按 ELF 物理地址落段
UART0/1/2自定义 ws63-uart SysBusDevice @ 0x4401_0000/1000/2000(见下)
TIMERws63-timer(3 个下数计数器 @ 0x4400_2000,到点产生中断 26/27/28)
GPIO0/1/2ws63-gpio(输出 set/clr、输入、中断寄存器 @ 0x4402_8000/9000/A000)
SYS_CTL0ws63-sysctl0(时钟状态:TCXO + PLL 已锁,使 init_clocks() 不空转)
中断控制器ws63-intc:自定义 LOCIxx CSR 状态 + IRQ 路由(见下)
其余外设create_unimplemented_device 吸收(三窗口),-d unimp 按地址可追踪

自定义 UART 设备

WS63 UART 不是 16550(这是关键,QEMU 自带 serial_mm 不可用)。它是 HiSilicon 定制布局, 经 WS63.svd UART0 + ws63-hal/src/uart.rs + SDK hal_uart_v151_regs_def.h 核实:

偏移寄存器16550?
0x00INTR_ID✗(16550 是 RBR/THR)
0x04DATA(读=RX,写=TX)✗(16550 DATA 在 0x00)
0x08UART_CTL✗
0x0C/0x10/0x14DIV_H / DIV_L / DIV_FRA(16 位整数 + 6 位小数分频)✗
0x34LINE_STATUS位定义不同于 16550 LSR
0x44FIFO_STATUS:tx_full[0]/tx_empty[1]/rx_full[2]/rx_empty[3]✗

HAL 的 TX 路径(uart.rs write_byte):轮询 FIFO_STATUS.tx_fifo_full(0x44 bit0)为 0 → 写 DATA(0x04)。模型据此:

  • 读 FIFO_STATUS → TX 永远「空且不满」(瞬时排空),RX 视收到的字节而定。
  • 写 DATA → qemu_chr_fe_write_all 输出到 chardev(-serial mon:stdio)。
  • 读 DATA → 弹出收到的字节(最小 RX,支持回显)。
  • 其余寄存器:接受写、读回 shadow / 0。

中断控制器(ws63-intc)

WS63 用 HiSilicon 自定义的「riscv31」核内 CLIC 式方案,不是 CLINT/PLIC。设备 IRQ 分两类。 投递机制的完整讨论见 中断控制器设计;这里只列事实。

IRQ 类范围使能机制投递路径
mie 类IRQ 26–31(TIMER_0/1/2、RTC、I2C0)标准 mie CSR 位riscv_cpu_update_mip() 拉高 mip[n] → 向量化 mtvec(mtvec + 4*n)
自定义本地类IRQ ≥32(GPIO=33、UART=53…LSADC=72)核内自定义 CSR LOCIEN/LOCIPDtarget/riscv 补丁核内投递,mcause = irq(32–72) → 向量化 mtvec

自定义本地类相关 CSR:

CSR地址作用
LOCIEN0-20xBE0本地中断使能位
LOCIPD0-20xBE8本地中断 pending 位
LOCIPCLR0xBF0清 pending
LOCIPRI0-150xBC0每 IRQ 4 位优先级(8 个/寄存器)
PRITHD0xBFE优先级阈值;仅当优先级 严格 > PRITHD 才投递

复位默认:每 IRQ 优先级 1、阈值 0(退化为按号投递,兼容旧行为);多候选取最高优先级,同级取最小 IRQ 号。

相关

WS53 RAM test machine

-M ws53 currently models the application-core MCU_MODE_1 RAM takeover environment, not a flash-bootable product or the vendor control-core firmware. Supported build pin: QEMU v10.0.0 (7c949c53e936aa3a658d84ab53bae5cadaa5d59c).

Mapped regions: acore ITCM 0x20000..0x30000, DTCM 0x20010000..0x20060000, three basic UARTs, two 32 MHz timers, three GPIO banks, MCU/AON/TZPC pad controls, NMI aggregation and local interrupt controller/custom CSRs. There are no catch-all MMIO absorbers, flash/ROM stubs, shared-memory aliases or fake control-core responses.

The 32 MHz rate follows the pinned SDK application dev_work_clock_init(); it overwrites the menuconfig’s initial 30 MHz value. This is a boot-profile assumption, not an oscillator measurement on the attached board.

The timer model supports the first milestone’s 32-bit path: one-shot/periodic, counter snapshot, mask/unmask, per-channel W1C, stop and reset. SDK profile CONFIG_TIMER_0_WIDTH_64=0 and CONFIG_TIMER_1_WIDTH_64=0. High-half/permission/ clock-management/low-power behavior is not yet modeled. UART uses a WS53-specific word-access model instead of the legacy family’s different receive flags. Full UART TX FIFO/timing/flow-control/DMA validation is outstanding. None of these results prove silicon timing, electrical properties or control-core coexistence.

The SDK common-register C structure and its comments disagree by 0x10. Neither common window is modeled: reads/writes are diagnosed as unimplemented. Tests use local raw/masked status at channel +0x18/+0x1c. The SDK writes one to channel EOI, but that does not establish read behavior; EOI reads also report unimplemented. All normal tests must run without these diagnostics. A separate negative test requires diagnostics for 11 unsupported reads and 3 unsupported writes, so an unknown register returning zero cannot silently satisfy a test. Zero state after model reset is deterministic simulation initialization, not a silicon reset claim.

The UART model owns a 64-byte RX FIFO for each of the three instances. It models FIFO order, drop-new-data FIFO overrun, overwrite-on-overrun non-FIFO mode, parity/framing test injection, read-clear line errors, RX thresholds, IRQ masks and pending delivery, FIFO reset and system reset. SDK FIFO access mode (FAR / RX_FIFO_WRITE) supplies controlled data and error inputs; DATA TX also supports loopback. FIFO_STATUS bit2 means not-empty and bit3 full; LINE_STATUS data-available is bit5. The RAM test’s optional --elf-uart-rx image (build with uart-rx-smoke) consumes ten actual chardev bytes through the Rust HAL and checks that the FIFO then reports empty.

The SDK does not establish the character-timeout duration. The model’s rx-timeout-ns property defaults to zero (no synthesized timeout); tests inject 100000 ns explicitly to exercise IRQ ID12 and cancellation. This is a test input, not a WS53 timing constant. Error-before-data IRQ priority is also a model policy awaiting hardware verification. UART reset supplies an explicitly configured console handoff, not measured silicon resets. Register width follows SDK word accesses; rejection of halfword accesses is the model contract, not a claim that silicon rejects them.

TX currently goes straight to the chardev: no TX FIFO, shift-register delay or baud-accurate serial line is claimed. Modem/TX IRQs, auto flow control, PRO fields, DMA and unsupported MMIO are diagnosed rather than absorbed. FIFO-mode changes without an explicit RX reset are also unmodeled. Preserved configuration values alone are not proof of framing/divider hardware behavior. The experimental HAL now has fallible receive APIs, but full clock/pin configuration and physical validation remain open; it must not be marked a complete UART driver.

Optional --elf-uart-errors runs the RAM example built with uart-error-smoke. It tests the actual Rust HAL on all three UARTs: simultaneous error flags, corrupt-head discard, preservation of queued bytes on overrun, a good prefix before an errored byte, deferred error delivery after a partial read, zero-length read preservation and recovery. This image deliberately resets RX FIFOs and injects errors through FAR; it is not the default board image. --elf-uart-combined expects both UART test features and additionally injects ten chardev bytes. Break decoding has host coverage only; no break-injection hardware proof is claimed.

--elf-uart-irq runs the uart-irq-smoke RAM variant. Each of IRQs 39, 41 and 42 enters the real Rust runtime/handler six times, testing queued data on unmask, data arriving after a one-shot notification, deferred parity/framing and overrun errors, injected character timeout, cancellation and recovery. The ISR captures IIR, masks RX/error notifications and disables/acknowledges the local IRQ. It does not read FIFO data or read-clear LSR, so the foreground HAL retains the error report. Rearming requires both peripheral and controller enable; an empty FIFO must not retrigger. The runner explicitly injects a 100000 ns timeout for this variant. This tests deferral, not hardware timeout timing or async support. --elf-uart-all expects all three test features and exercises them together; --elf-uart-combined retains the original RX-plus-errors two-feature meaning.

The machine also supplies three independent ws53-gpio basic banks at 0x5700c000/0x57028000/0x57010000, with 23/12/3 bit spans and IRQs 34/33/29. scripts/ws53_gpio_test.py (also called by test-ws53.py) checks input versus output-latch reads, word SET/CLR commands, output and drive-enable wires, rising/falling/dual edges, high/low levels, masking, selective EOI, isolation and system reset. Tests drive named pin inputs and observe out and output-enable signals: behavior is not satisfied by register storage alone.

GPIO model policies are explicit: edges latch only on enabled input/edge pins; masking retains a captured edge; disabling hides it without acknowledging it; EOI clears edge latches but cannot suppress an active level. Configuration changes alone do not fabricate an edge. Deterministic reset disables/masks interrupts and clears output latches while preserving external input levels. These are simulation contracts awaiting physical validation. The 23-bit bank is not a claim of 23 independently bonded pads: pad/SDK aliases, mux routing, SWD reservation, shared clocks and analog electrical effects are not integrated.

SW_CTL’s data-source encoding, LOCK/LP registers, MULTISYSTEM extensions and debounce timing are not established. Reads/writes of those unsupported controls, command reads and RO-status writes must report diagnostics; enabling debounce also reports unimplemented rather than silently pretending to filter input. Fifteen negative accesses/signals enforce this. Only debounce-disabled operation is exercised. These qtests alone are not Rust GPIO ISR proof.

The default machine connects the three GPIO banks through ws53-pinctrl. The SDK-derived fixture covers 31 canonical pads and 30 GPIO routes across MCU, AON and TZPC register domains. Mode 0 routes GPIO; input-enable gates sampling, pulls resolve undriven inputs, and an external driver overrides a weak pull. The model exposes level, drive-enable, floating and contention signals. A conflict deterministically resolves to the GPIO output and floating resolves low; neither rule predicts an analog voltage. Drive and Schmitt codes are retained without modeling current or hysteresis. Unsupported encodings/accesses are diagnosed and rejected. Valid alternate modes disconnect GPIO but diagnose the unimplemented peripheral route; UART chardevs remain independent test paths.

The reset profile selects GPIO, enables inputs and removes pulls, retaining external driver state. It is a RAM-test profile, not a silicon reset claim. RST_N chip reset, clock gating, low-power halfword access and alternate UART/SPI/ SDIO signal routing remain unimplemented. -M ws53,gpio-direct=on disconnects the pad model only for isolated GPIO register tests. The integrated tests check ten behavior classes plus eleven expected unsupported-access diagnostics.

The optional gpio-smoke RAM image first configures all 28 non-debug, non-reset pads through HAL PAC accessors. Each WS53 PAD NN READY handshake checks the target mux/pad addresses against the independently SDK-checked fixture. Input disabled rejects GPIO ownership transfer; enabled mode 0 allows it. These full pad sweeps are simulation-only and must never be run on the physical board.

The image then exercises the HAL’s borrowed Input/Output drivers and embedded-hal digital traits on GPIO0 bit2, GPIO1 bit3 and GPIO2 bit2. scripts/ws53_gpio_ram_test.py supplies external digital inputs through qtest and synchronizes with UART2. The 66 ordered stages verify output preload, set/clear/toggle, input sampling, unchanged neighboring configuration, six real ISR notifications per bank (IRQ34/33/29), and cancellation with an edge already pending. Both explicit release and Drop are checked; a held active level must not cause an interrupt storm after the one-shot handler masks the local IRQ.

uv run --script scripts/test-ws53.py --qemu /path/to/qemu-system-riscv32 \
  --elf-gpio /path/to/gpio-smoke.elf --elf-all /path/to/all-features.elf

--elf-all expects all UART and GPIO features together. --elf-uart-all still means only the three UART features. These explicit injection images are for QEMU, not the default hardware RAM test. No physical pad mux, clock handoff, electrical behavior, debounce timing or GPIO silicon qualification is implied.

The application NMI aggregation window is at 0x52000700, following hisi-registers 4939cd2 and the SDK’s selected non_os source profile. It accepts only halfword accesses at enable/raw/masked offsets 0/4/8. Five named source positions (bits1..5: WDT, XIP control, XIP cache, MDMA, SMDMA) drive raw status; enabled active sources produce the pending output. Reads do not acknowledge sources. Peripheral-side latching belongs to the source device and is not invented by this block. The deterministic RAM reset profile disables all enables but retains externally driven levels; this is not a silicon reset claim.

scripts/ws53_nmi_test.py checks all 1024 source/enable combinations, held sources across mask/reset, last-source release and 16 invalid accesses/signals. Wrong widths, holes, unmodeled bits and status writes are diagnosed and leave state unchanged. Tests inject source levels via /machine/nmi source; the WDT device now drives source bit1 and clears it on peripheral reset. The aggregate’s held-input reset test uses independent source bits2/4. These source names alone do not establish usable DMA engines or application ownership.

CPU NMI delivery is unimplemented. The output is deliberately not connected to the ordinary local IRQ controller, and the first asserted output emits an unimplemented CPU NMI delivery diagnostic. Only the named aggregation tests and the explicitly named watchdog model tests expect this diagnostic; RAM success paths still reject every unimplemented access. The model neither claims CPU entry/return, NMI nesting/maskability nor watchdog SoC reset behavior. It must not silently make NMI-using firmware pass.

Watchdog command and assumed-timing model

ws53-wdt at 0x5702b000 implements 32-bit command/status views from RDL 4939cd2. scripts/ws53_wdt_test.py is included in the normal model regression. Unlock/restart use 0x5a5a5a5a; EOI consumes a read and never infers an independent write-clear effect. Raw status survives masking and feeding. Snapshot requests clear validity until completion, and counter reads consume the captured value, not a continuously changing counter. The WDT output connects to NMI source bit1.

The SDK does not establish the initial low counter byte, zero-expiry edge, reload between stages, or the stage effect of EOI versus kick. Therefore the default device rejects reload/counting/snapshot requests with an unimplemented diagnostic. It must not silently supply guessed watchdog timing to firmware. Only explicit tests may select:

-global ws53-wdt.assume-timing=on -global ws53-wdt.assumed-load-low-byte=255

This opt-in profile uses an inherited 32000 Hz clock, expiry after count+1 ticks, reload after a non-reset expiry, kick restarting the first stage without clearing raw status, and EOI clearing raw status without restarting the stage. Enable reloads the counter; disable freezes its remaining interval. LOAD’s reserved low byte reads zero; the independently assumed counter low byte is 0..255 (default255), swept at both extremes by tests. These are explicit model assumptions, not new silicon facts. One diagnostic announces the profile on first use. Counts at u32::MAX, valid zero and fractional clock pause/resume are tested without wrapping the duration arithmetic.

Test-only named inputs /machine/wdt clock 0 and stall-snapshot 0 pause the input clock and block snapshot completion. Resuming the clock retains fractional tick phase. A stalled snapshot never returns ready; releasing the stall completes the pending request. System reset clears model state and owned interrupt outputs but retains external clock/stall levels. Initial locked/disabled/zero state is a deterministic fixture initialization, not a claimed hardware reset value.

The reset-request output is observable, but SoC reset topology and pulse timing are unimplemented. Assertion diagnoses that gap and stops counting until the fixture resets the device; it does not reset a CPU behind the guest’s back or pretend a reset completed. The pulse-length register is retained but not timed. CPU NMI delivery likewise remains explicitly diagnosed by the aggregation model. Wrong widths, holes, write-only reads, read-only writes, unsampled CNT, unknown commands, LPIF/busy state and writes under unknown lock protection are rejected with diagnostics. In particular this is not a complete low-power/reset model or permission to run a physical watchdog test. Named model tests accept only their exact expected diagnostics; ordinary RAM tests still reject them.

The shared Python qtest context now sends QMP quit with its qtest channel still open. This avoids racing socket closure/SIGTERM against QEMU’s log timer teardown; strict NMI diagnostic checks retain no unrelated host warnings.

Build with scripts/build.sh using a private QEMU source tree. Then:

uv run scripts/test-ws53.py --qemu /path/to/qemu-system-riscv32 \
  --elf /path/to/ws53_ram_smoke
QTEST_QEMU_BINARY=/path/to/qemu-system-riscv32 /path/to/build/tests/qtest/ws63-test

The test checks 9 named timer behaviors plus the unsupported-access diagnostics. With the RAM ELF it also checks data/BSS, UART2 output and three IRQ 53 entries and returns through the guest runtime. All are simulation tests; the 20-round physical-board RAM/IRQ acceptance remains outstanding.

Build host notes: this host’s system Python lacked distlib; a uv Python 3.12 virtualenv with distlib, meson 1.5.0, ninja and pycotap was passed to QEMU configure --python=.... GitLab dtc fetch failed; the exact same pinned commit was obtained from QEMU’s official GitHub mirror. The new host compiler reports additional warnings in QEMU 10 and dtc: meson configure -Dc_args=-Wno-error was used for this local build. These are build-environment adjustments, not changes to guest hardware behavior. They do not suppress actual compile errors.

外设建模矩阵

WS63.svd 的全部 35 个外设均已建模(无裸 catch-all 黑洞)。本表逐个列出建模状态。地址见 内存映射,机器组件见 机器模型,配置类为何是影子见文末。

外设状态说明
CPU (rv32imfc) / 内存 / 复位 / ELF 载入✅ 真实命名 CPU -cpu ws63(默认型号):I/M/F/C + Zicsr/Zcf,关 A/D、无 MMU,禁用 Zcb/Zcmp(让位 xlinx 压缩编码);见 target/riscv 补丁
xlinx 自定义 ISA✅ 真实HiSilicon riscv31 私有指令(l.li/*shf/b*i/muliadd/jal16/ldmia/push-pop/压缩 lbu-sb…),厂商 gcc 固件必需;见 xlinx ISA
UART0/1/2✅ 真实自定义 HiSilicon 寄存器;TX→chardev,RX←chardev(中断使能时触发 IRQ 53/54/55)
TIMER ×3✅ 真实下数计数器 + 中断(26/27/28),周期重载
GPIO0/1/2✅ 真实(行为完整)输出 set/clr、输入读、边沿/电平中断;引脚为真实信号网:bank 内输出→输入回环 + 跨 bank 板级连线(GPIO0 输出脚驱动 GPIO1 输入脚,可观测+中断,裸机验证)+ 可由 monitor/外部设备驱动
SYS_CTL0✅ 真实(部分)仅时钟状态(TCXO/PLL 锁);其余读 0
TCXO 时钟/计数器✅ 真实(部分)0x440004C0:bit4 count-valid + 64 位单调计数(+0x04/+0x08),供 bootloader us 级延时
PPB(核内私有外设总线)🟡 RAM 吸收0xE0000000 FlashPatch 单元 + Cortex-M 式 SCS(0xE000E000);加载已打补丁镜像故补丁单元无意义
中断控制器✅ 真实IRQ 26–31(mie 类)+ ≥32(自定义 LOCIxx,target/riscv 补丁)均完整投递;LOCIPRI 优先级 + PRITHD 阈值已强制(严格 >、最高优先级优先、同级取小号)
SFC(Flash 控制器)✅ 真实(部分)SPI 命令接口(RDID→W25Q16、RDSR→ready、命令完成);flash XIP 窗口(0x200000)为 RAM 背靠,默认空——可用 run.sh NV=1 回填分区表+NV(见 运行固件)
I2C0/1(0x44018000/0x44018100)✅ 真实(行为完整)真实回环 FIFO:TXR→RXR 多字节顺序回读、SR 完成位、COM 命令位自动清、IRQ 31/32
SPI0/1(0x44020000/0x44021000)部分行为模型DRNM 窗口 +0x2c..+0xb8 的 36 个对齐地址共享回环 FIFO;数据口支持字节/半字/字访问,WSR/RLR 反映队列。尚未实现 SPI IRQ、时钟/使能门控、FIFO 错误及 DMA 请求节拍;不能据回环通过宣称完整支持。BS2X 保留独立的 +0x60 数据口布局。
PWM(0x44024000)✅ 真实(部分)寄存器影子 + PERIODLOAD_FLAG=1 + START 自清
I2S(0x44025000)✅ 真实(行为完整)LEFT/RIGHT TX→RX 回环
LSADC(0x4400C000)✅ 真实(行为完整)CTRL_8 触发转换 → rne=1/bsy=0、CTRL_9 弹出 14-bit 采样 + 完成 IRQ 72(读清)
EFUSE(0x44008000)✅ 真实(行为完整)真实 OTP:STS boot-done + 数据窗读回,写=按位或(一次性熔丝只能置位,不可清零,裸机验证);标定内容无 dump 故为空白熔丝
TSENSOR(0x4400E000)✅ 真实(行为完整)start→sts rdy=1 + 10-bit 温度码(合成 ~25°C,按 HAL 转换公式)
WDT(0x40006000)✅ 真实(行为完整)QEMU 定时器倒计时;超时未喂狗则真复位 SoC(裸机测试验证)
RTC(0x57024000)✅ 真实(行为完整)QEMU 定时器周期触发 IRQ 29 + INT_STATUS/EOI;CURRENT_VALUE 计数
DMA/SDMA(0x4A000000/0x520A0000)✅ 真实(行为完整)通道使能即真正搬运内存(src→dst,按宽度/地址自增),置传输完成位,按 tc_int_en 触发 IRQ 59;INT_CLR 清除(裸机测试验证)
TRNG(0x44114000)✅ 真实FIFO_READY=ready、FIFO_DATA 伪随机(xorshift)
合成 Wi-Fi/以太 MAC(ws63-netmac @ 0x44210000)✅ 真实(行为完整,合成)软件在环连接性底座(路线图阶段 5):不仿 RF/PHY,在 ws63-rf-rs netif 缝合点暴露最小以太帧 MAC——TX_BUF+TX_GO→qemu_send_packet(接 -nic user SLIRP NAT),主机帧→.receive→RX_BUF + IRQ 45(WLMAC_INT);qtest 整帧收发回环验证。非厂商 WLMAC 寄存器级复刻
SPACC / PKE / KM(密码学)🟡 影子寄存器影子(未在启动路径:mbedtls 用 ROM 表软件 AES)。真实 AES/SHA/RSA 可经 QEMU crypto 库实现,但 SPACC v2 多通道描述符协议复杂且无固件触发,列为按需扩展
CLDO_CRG(时钟与复位生成)✅ 真实(部分)时钟门控生效:清 CKEN_CTL0 bit21 冻结定时器、置位恢复(已实测);CLK_SEL 源路由建模为状态;其余位影子
IO_CONFIG / SYS_CTL1 / PWM / RF_WB_CTL / SHARE_MEM / FAMA_REMAP / ULP_GPIO(影子)🟡 影子见下「配置类为何是影子」

覆盖度:WS63.svd 的全部 35 个外设现均有模型(无裸 catch-all 黑洞)。 「行为完整」= 真实数据搬运/计时/中断/转换(DMA/RTC/WDT/Timer/I2C/SPI/I2S/LSADC/GPIO/UART/TSENSOR/EFUSE)。

配置类为何是影子

配置寄存器本身没有“行为“,其行为是对别处的作用。作用可内部计算的已做成真实 (TSENSOR 出温度、EFUSE 走 OTP、LSADC 出采样);作用是物理/外部的则在仿真器里无可观测行为:

  • 引脚行为本身可仿真:QEMU 用信号网(qemu_irq)建模引脚——GPIO 引脚是真实信号网 (bank 内回环 + 跨 bank 板级连线 + 可外部驱动)。真正“不可观测“的只是悬空引脚(输出无连接对象)。
  • IO_CONFIG(引脚复用)= ws63-pinmux 设备(已做路由):GPIO_xx_SEL[2:0] 选功能(0=GPIO, 1-7=UART/SPI/I2C/PWM…)。GPIO0→GPIO1 的板级引脚网经 pinmux 路由:引脚复用为 GPIO 时信号导通、 复用为其他功能时被门控(引脚改由该外设承载)。裸机验证:复用 GPIO→读到 1,复用走→0,复用回→1。 非 GPIO 功能(UART TX/SPI CLK)的数据本身由各外设的 TX/RX/回环覆盖;pinmux 负责的是“哪根物理引脚承载它“。
  • RF_WB_CTL / WiFi / BT:射频 PHY,物理边界,不仿。
  • CLDO_CRG 时钟门控(已生效):定时器门(CKEN_CTL0 bit21)清零会冻结定时器、置位恢复(默认开, 故不显式开门的固件不受影响);CLK_SEL 源路由建模为状态。其复位位理论可复位目标外设,但位→外设映射 复杂且无固件触发,暂留影子。
  • SHARE_MEM_CTL:核间共享内存控制,单核下无意义。
  • SPACC/PKE/KM:见加密行,真实 AES/SHA/RSA 需复杂且未被触发的描述符协议,按需扩展。

相关

HiSilicon WS63 “xlinx” 自定义 RISC-V 指令扩展

WS63 的 riscv31 核在标准 RV32IMFC 之上实现了一组 HiSilicon 私有指令(厂商工具链 riscv32-linux-musl-gcc -march=rv32imfcxlinxma_xlinxmb_xlinxmc 默认发射)。真实 C SDK 固件(fbb_ws63)通篇使用它们,因此要在 QEMU 上跑厂商编译的固件,必须实现这些指令。

它们落在 RV32 保留 / RV64-only(0x1b/0x3b)与 custom-N(0x0b/0x5b/0x7b)的 32 位 opcode 空间、一种 48 位(marker 0x1f)形态、以及被复用的压缩编码空隙(funct3=100 的 push/pop/uxt,funct3=001/101 的 D-float 槽位)。在我们的 RV32 核上标准解码器都会落空,转交 src/target/riscv/insn_trans/trans_xlinx.c.inc。

编码为实测真值:用厂商汇编器逐操作数扫描(汇编 → 读原始字节 → 对每个字段做差分)得到; 语义经厂商代码生成(gcc -S)与 13 条单元自检(scripts//会话记录)核对。

32 位指令

助记符opcode[6:0]语义字段
l.li rd, imm3248 位,halfword0 & 0x3f == 0x1f,bits[15:12]=0rd = imm32rd=bits[11:7];其后 4 字节小端 = imm32
{add,sub,or,xor,and}shf rd,rs1,rs2,mode,sh0x1brd = rs1 OP (rs2 移位 sh)funct3: 0 add,1 sub,2 or,3 xor,4 and;rd[11:7] rs1[19:15] rs2[24:20];sh=bits[29:25];mode=bits[31:30]: 0 sll,1 srl,2 sra,3 ror
{beq,bne,blt,bge,bltu,bgeu}i rs1,imm8,off0x3b比较 rs1 与 8 位有符号 imm8,成立则跳转funct3: 0 eq,1 ne,4 lt,5 ge,6 ltu,7 geu;rs1[19:15];imm8=bits[31:24](符号);off: imm[1]=bit7, imm[5:2]=bits[11:8], imm[9:6]=bits[23:20](10 位有符号,±1KB)
muliadd rd,rs1,rs2,imm60x5b(bits[13:12]=01)rd = rs1 + rs2 * imm6rd[11:7] rs1[19:15] rs2[24:20];imm6 = bit14 | (bits[29:25]<<1)
jal16 / j16 target0x7bpc += off;jal16(bit7=0) 置 ra=pc+4,j16(bit7=1) 置 rd=x025 位有符号 off(±16MB):标准 J 型给 imm[20:1](imm[10:1]=bits[30:21], imm[11]=bit20, imm[19:12]=bits[19:12], imm[20]=bit31)外加 imm[24:21]=bits[11:8],符号位 imm[24]=bit11
ldmia/stmia {ra,s0-s11},(base)0x0b多寄存器 load/store,地址递增,base 不回写bit12: 0 ld / 1 st;base=bits[19:15];寄存器存在位图:ra=bit7, s0=bit9, s1=bit10, s2..s11=bits21..30

j16 a0e46e <memcpy> 这类调用偏移可达 ~9MB,远超标准 J 型 ±1MB —— 这正是 25 位立即数 (bits[11:8] 承载高位)存在的原因;曾因漏掉 imm[23] 导致目标地址丢 bit23(0xa0e46e→0x20e46e)。

16 位指令(复用压缩编码空隙)

助记符匹配语义字段
push/pop/popret {ra,s0-sN}, spimmfunct3=100,Q0:insn & 0xE003 == 0x8000Zcmp 式多寄存器入/出栈(popret 末尾 ret)bits[3:0]=op(8 push,0 pop,4 popret);bits[7:4]=rlist(1..13 ⇒ {ra,s0..s(N-2)});bits[12:8]=extra;stack_adj = round_up(n*4,16)+extra*16
uxtb/uxth rd'insn & 0xFC5F == 0x9C01rd' = zext8/zext16(rd') 原地rd’=bits[9:7](x8+);bit5: 0 字节 / 1 半字
压缩 lbu/lhu rd',imm(rs1')funct3=001,bit0=0:insn & 0xE001 == 0x2000无符号字节/半字 load(占用 C.FLD/C.FLDSP 空槽)rd’=bits[4:2], rs1’=bits[9:7](均 x8+);bit1: 0 字节 / 1 半字;imm[4:0]={bit11,bit10,bit6,bit5,bit12}(字节偏移,不缩放)
压缩 sb/sh rs2',imm(rs1')funct3=101,bit0=0:insn & 0xE001 == 0xA000字节/半字 store(占用 C.FSD/C.FSDSP 空槽)rs2’=bits[4:2], rs1’=bits[9:7];其余同上

在 QEMU 中的接入

  • 新文件 target/riscv/insn_trans/trans_xlinx.c.inc:decode_xlinx16/32/48 + 各 trans_*。
  • target/riscv/translate.c(见 patches/<QEMU_TAG>/0001-target-riscv-*.patch)最小 hook:
    • insn_len():(first_word & 0x3f) == 0x1f ⇒ 6 字节(48 位 l.li);MAX_INSN_LEN 4→6。
    • decode_opc():16/32 位标准解码落空后转 decode_xlinx16/32;新增 6 字节分支取 l.li。
  • 必须关闭 Zcb/Zcmp/Zcmt(由 target/riscv 的 ws63 命名 CPU rv32_ws63_cpu_init 设定——它是机器默认 CPU 型号):它们与 xlinx 的 funct3=100 push/pop/uxt 编码空间冲突,会误解码(曾把 push {ra},-16(0x8018) 误当 Zcb load → load fault)。保留 Zcf (压缩浮点 load/store,编码空间不冲突,trap 向量里 c.fsw 会用到)。

验证现状

  • 13/13 指令自检通过(l.li 保 bit23、shf 全族、muliadd、branch-imm、push/pop/popret、ldmia/stmia)。

  • 真实 ws63-liteos-app.elf 在 QEMU 上完成 relocation / dyn_mem_cfg / riscv_patch_init / 驱动初始化, 正确执行数百万条指令,直至命中掩膜 ROM 边界(见下)。

  • 真实 C SDK flashboot 已在 QEMU 上跑出 UART 输出(厂商 gcc 编译、自包含、零 ROM 依赖):

    Flash Init Fail! ret = 0x80001341
    SFC fix SR ret =0x80000002
    

    即 xlinx ISA + 内存映射 + 启动参数(a0) + TCXO 时钟 + UART 全链路打通。SFC Flash 控制器现已建模 (命令接口 RDID/RDSR + flash XIP 窗口,见 ROM 桩目录 §B / 内存映射); 完整 flash init 仍需真实分区表/flash 内容(run.sh NV=1 回填),否则 bootloader 优雅报错继续、不挂死。rs SDK 各例无回归。

掩膜 ROM 调用拦截(让主应用 ws63-liteos-app 跑起来)

ws63-liteos-app 调用 53 个固化在硅片掩膜 ROM(0x109000–0x14C000)的函数(vsnprintf_s/memcpy_s/ SFC/pin/timer/systick…,ABI 见 rom_config/acore/acore.sym),SDK 不提供 ROM 二进制。我们在 riscv_cpu_do_interrupt(补丁内)拦截:app 跳进 ROM 区 → 空 ROM 触发非法指令 → 用宿主 C 模拟该函数 并返回 ra。详见 ROM 桩目录。

结果:主应用启动 LiteOS、打印格式化日志、创建全部 14 个子系统任务(app/log/bt/wifi/radar/at…)、 进入调度器(cpu 0 entering scheduler),见 logs/ws63-liteos-app-boot.txt:

APP|dbg uart init ok.
[UPG] init OK!
los_at_plt_cmd_register EXCUTE
cpu 0 entering scheduler

随后在 bt 任务里崩(near-null load)——因 BT/NV/efuse 等ROM 数据 + 硬件状态未建模。

已知边界(非 ISA 问题)

  • 掩膜 ROM 数据墙:ROM 函数桩能让 app 跑到调度器,但 ROM 里的数据结构(vtable 内容、配置、NV/efuse、 BT/WiFi 标定)无 dump,深层子系统初始化(bt/wifi)会因 near-null/缺数据而崩。整应用跑到底需真实 ROM dump。
  • bootloader 启动参数 ABI:flashboot/loaderboot 自包含(零 ROM 依赖),但入口即 lw t3,0(a0) —— 期望上一级(loaderboot/ROM)经 a0 传入启动参数结构;直接 -kernel 引导时 a0=0 会 load fault。需伪造启动参数(后续工作)。
  • PPB / FlashPatch:0xE0000000 为核内私有外设总线(FlashPatch 重映射单元 + Cortex-M 式 SCS 0xE000E000)。我们加载完整已打补丁镜像,补丁单元无意义,用 RAM 吸收即可(见 ws63.c 的 ppb)。

相关

WS63 掩膜 ROM 桩与拦截目录

本文记录 ws63-qemu 中**所有与掩膜 ROM(mask ROM)相关的桩(stub)/拦截(interception)**的清单:机制、已仿真的 ROM 函数、相邻设备桩。真值来源:patches/<QEMU_TAG>/0001-target-riscv-*.patch(target/riscv/cpu_helper.c)与 src/hw/riscv/ws63.c。

为什么需要这些桩(简述)

WS63 的 RISC-V 应用核有一块片上掩膜 ROM(地址区间 0x109000 .. 0x14C000),固化了启动期与基础库代码/数据 (mem*_s / *printf_s、systick/tcxo 计时、各外设 HAL vtable、整套看门狗 API、BT/WiFi 标定数据)。HiSpark SDK 不发布这块 ROM 的二进制,固件 call 到这些地址时 QEMU 那块区域是空的(取指得 0 → 非法指令异常)。

我们用两类手段:(A) 掩膜 ROM 调用拦截(把 ROM 函数在宿主 C 里仿真)、(B) ROM/启动相邻的设备桩。

A. 掩膜 ROM 调用拦截(ws63_rom_call)

机制:riscv_cpu_do_interrupt() 里加了一个钩子——当一次非法指令异常的 pc 落在 ROM 区间 [0x109000, 0x14C000) 时,不进入正常陷阱,而是调用 ws63_rom_call(env),在宿主 C 里仿真该 ROM 函数, 然后:a0(gpr[10]) = 返回值;pc = ra(gpr[1]),即直接返回到调用者。靠 pc 分派到具体函数;未识别的 ROM 地址走 default 分支,返回 0(成功),让固件能调用未建模的 ROM 函数而不崩(优雅降级)。

已仿真的 ROM 函数:

ROM 地址函数仿真内容
0x10aeb4memcpy_s(dst,dmax,src,n)安全拷贝;n>dmax 返回 -1,否则逐字节拷贝、返回 0
0x10b790memmove_s带重叠安全(临时缓冲);n>dmax 返回 -1
0x10b7f6memset_s(dst,dmax,c,n)安全填充;n>dmax 返回 -1
0x10cc3csprintf_s经 ws63_vformat 解析 guest 变参格式化到 dst,返回长度
0x10cc0asnprintf_s同上,长度截到 min(count,dmax)
0x10d1d0vsnprintf_s同上,但变参取自 guest 的 va_list(use_va=true)
0x10ac44/0x10acba/0x10d34auapi_systick_get_count/_us / uapi_tcxo_get_us共用:静态 us 计数每次 +1000,a0=低 32 位、a1=高 32 位(保证单调递增,bootloader 的 us 级延时不会死等)
0x10ac94/0x10d32cuapi_systick_get_ms / uapi_tcxo_get_ms共用:静态 ms 计数每次 +1
0x10aa9ahal_timer_v150_get_funcs返回合成 vtable 指针(ws63_rom_vtable(0)):一块 256B 的 scratch(在 PPB 的 ~0xE000F000),预填 WS63_ROM_BASE 蹦床地址(被调到时再走 ROM 拦截→返回 0)
0x109ab8hal_sfc_v150_funcs_get同上(ws63_rom_vtable(1)),供 flashboot 的 SFC 初始化拿到非空 vtable
0x109f7euapi_watchdog_init(timeout_s)记录超时秒数(0→默认 2);见 §C
0x109fb6uapi_watchdog_enable(mode)ws63_wdt_rearm() 起一个虚拟 one-shot 定时器
0x10a0a4uapi_watchdog_kick重新装填定时器(喂狗)
0x109fda/0x10a00euapi_watchdog_disable/_deinittimer_del 取消定时器
(其它)default返回 0(成功),不崩

结果:ws63-liteos-app 能启动 LiteOS 到 cpu 0 entering scheduler、格式化日志正常打印 (*printf_s 走宿主 libc)、SFC/timer 的 vtable 获取不返回空指针。

B. ROM / 启动相邻的设备桩(src/hw/riscv/ws63.c)

这些不是“ROM 函数仿真“,而是为了让 bootloader/app 的 bring-up 寄存器序列走通而建模的设备:

设备/区域地址桩内容
ws63-tcxo0x440004C024 MHz 单调计数器(count-valid 位常 1,计数寄存器每次读保证步进)。QEMU 虚拟时钟在紧凑 TCG MMIO 轮询里会“冻结“,故必须保证每读必增,否则 flashboot 的 us 延时死等
PPB(核内私有外设总线)0xE0000000(64KB)RAM 背靠:FlashPatch 单元 + Cortex-M 式 SCS(0xE000E000);ROM vtable getter 返回的 scratch vtable 落在 ~0xE000F000
SYS_CTL00x40000000(16KB)时钟状态:HW_CTL 读 0(TCXO 24MHz)、REG_EXCEP_RO_RG@0x319C 的 bit12=1(PLL 已锁),使 init_clocks() 走通
ws63-sfc0x48000000SPI flash 命令接口:RDID→W25Q16(0x001560EF)、RDSR→ready/未保护、start 位自动清
Flash XIP 窗口0x200000(8MB)RAM 背靠,默认空;run.sh NV=1 用 -device loader 回填分区表(partition_params.bin@0x200000,表在 0x200380,magic 0x4b87a54b)+ NV(见 csdk 测试 fixtures)
低位 MMIO 兜底吸收0x40000000(256MB)create_unimplemented_device,吸收未建模外设;SYS_CTL0/SFC 等以更高优先级映射在其上

启动参数交接(ws63_cpu_reset):复位时 a0(gpr[10]) = WS63_SRAM_BASE(指向一个可读、清零的 SRAM 字)。 真机上每个启动级(mask ROM → loaderboot → flashboot → app)以 a0 = 启动参数块指针 进入;独立 -kernel 启动 没有上一级,a0 会是 0,而 bootloader 在设置 mtvec 之前就解引用 a0(lw t3,0(a0))→ 取数异常→双重异常→ pc=0。指 a0 到清零 SRAM 让“启动原因“读到 0(正常启动)。

C. 看门狗 ROM API 仿真(展开)

整套看门狗栈都在掩膜 ROM 里(uapi_watchdog_*、hal_watchdog_v151_*、watchdog_port_*),所以最初每个调用 都被 default 桩成 no-op。注意:仅“强行设 g_watchdog_regs“是不够的——调用方(uapi_watchdog_init 等)本身 就是 ROM 桩,根本走不到 HAL。

因此在 ws63_rom_call 里仿真整套 API:init 记超时、enable 用 QEMU_CLOCK_VIRTUAL 起一个 one-shot 定时器、 kick 重新装填、disable/deinit 取消;定时器到期(= 固件停止喂狗)时 ws63_wdt_fire() 调 qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET)——这正是看门狗的本职。已验证:C SDK watchdog 样例 持续喂狗 → 永不复位、干净跑到调度器,挂死才会触发复位请求。

未建模:中断模式的超时回调(样例里的 "watchdog kick timeout!")。从虚拟时钟定时器回调里改写 env->pc 去注入固件回调并不安全(定时器回调与 vCPU 取指边界不同步),会被静默丢弃;故仅建模“超时→复位“, 不建模“超时→中断回调→(未喂狗)再复位“。见 已知边界与非目标。

相关

运行选项参考

scripts/run.sh 与直接调用 QEMU 的选项清单。用法导向的讲解见 运行固件。

调用形式

bash scripts/run.sh [firmware.elf] [额外 qemu 参数...]
  • 不带 firmware.elf 时默认跑仓库内预构建 fixture:tests/csdk/dma.elf。
  • 额外参数原样透传给 QEMU(如 -s -S)。

直接调用:

./qemu/build/qemu-system-riscv32 -M ws63 -nographic -serial mon:stdio -kernel firmware.elf

退出 QEMU:Ctrl-A 然后 X。

行为开关(环境变量)

变量作用
DEBUG=1加 -d int,guest_errors,unimp -D qemu.log,把中断/guest 错误/未建模访问写入 qemu.log
ICOUNT=1确定性指令计时(-icount shift=2,约 250 MHz、IPC=1):同一固件每次运行计时完全一致。非周期精确
ICOUNT_SHIFT=N改 icount shift(默认 2→4 ns/insn≈250 MHz;3→125 MHz)
NV=1用分区表 + NV 镜像(tests/csdk/flash/)回填 flash XIP 窗口,使 C SDK 的分区/NV 读取成功
SEMIHOST=1加 -semihosting:固件可用 RISC-V semihosting SYS_EXIT 设置 QEMU 进程退出码(CI 免解析 UART 即得 pass/fail)、SYS_WRITE0 打印到控制台。见 ws63-rs ws63-examples/semihost_selftest

路径变量

变量作用默认
QEMU_DIR / QEMU_BIN仿真器位置<repo>/qemu
DEFAULT_ELF无参运行时使用的固件 ELFtests/csdk/dma.elf
FLASH_DIRNV overlay 目录tests/csdk/flash/
QEMU_TAG构建用的 QEMU tagv10.0.0
JOBS构建并行度nproc

常用 QEMU -d 标志

标志打印
-d unimp对未建模/兜底外设的访问(按地址定位)
-d int每次中断投递(IRQ 号/向量)
-d guest_errorsguest 侧错误访问
-d trace:ws63_gpio_*WS63 GPIO 输出变化 trace 事件
-trace ws63_dma_xferWS63 DMA 搬运 trace 事件
-D <file>把 -d 输出写入文件

追踪用法见 调试与追踪。

示例

bash scripts/run.sh                         # 运行默认预构建 C SDK fixture
bash scripts/run.sh tests/csdk/adc.elf      # 显式运行另一个 fixture
DEFAULT_ELF=tests/csdk/tcxo.elf bash scripts/run.sh
ICOUNT=1 bash scripts/run.sh fw.elf          # 可复现计时
NV=1 bash scripts/run.sh ws63-liteos-app.elf # C SDK app + 分区表/NV
DEBUG=1 bash scripts/run.sh fw.elf           # 写 qemu.log 追踪
bash scripts/run.sh fw.elf -s -S             # 起 gdbstub 并冻结在复位

QEMU 官方文档

这些开关多数透传给上游 QEMU,语义以官方文档为准:

验证覆盖范围

ws63-qemu「覆盖」分三层:持续 CI 验证(每次提交都跑)、外设建模覆盖(35/35 建模)、明确的边界 (已知不覆盖)。本文列出各层的具体清单。怎么自己跑这些测试见 运行测试。

持续验证(CI)

ci.yml 在每次 push / PR 上跑(Ubuntu),门禁全绿才算通过:

  1. 构建 qemu-system-riscv32(带 WS63 机器);
  2. 机器注册 sanity(-M help 含 ws63);
  3. C SDK 外设样例(csdk-test.sh,见下,5/5,使用仓库内预构建 fixture);
  4. ws63-rs 冒烟(smoke-test.sh,见下,进阶/CI 覆盖);
  5. 寄存器级 qtest(qtest.sh,免启动驱动 GPIO/UART/timer/INTC/DMA,4/4)。

release.yml(打 v* tag 时)额外做全新构建 + 冒烟,然后发布二进制。 qtest-matrix.yml 在每个有 patches/<tag>/ 的 QEMU 版本上跑 qtest(v9.2.4 + v10.0.0 + v10.2.3 + v11.0.1 全绿)。

C SDK 外设样例

scripts/csdk-test.sh 启动 tests/csdk/ 里预编译的 fbb_ws63 C SDK 厂商固件并断言各自的 UART 成功标志—— 用真实厂商固件交叉验证外设模型。纯净 + 免 SDK/工具链(用已提交的 fixture)。当前 5/5 绿:

样例验证的外设路径成功标志
tcxo.elfTCXO ms/us 计数器tcxo get ms work normall
systick.elfSysTick 计数器systick get ms work normall
adc.elfLSADC 标定 + RX-FIFO 转换读voltage: N mv
dma.elfDMA v151 内存搬运 + 完成dma memory copy test succ
(NV overlay)分区表解析 + NV 读启动到调度器且无 upg ...flash_start_addr fail

已记录但未断言的样例(tests/csdk/manifest.txt):

  • timer —— 非外设模型问题。硬件定时器模型正确(channel 0 / IRQ 26 即 LiteOS systick 正常触发), 但 LiteOS 软件定时器任务层没走到 uapi_timer_start,属上层任务问题。
  • watchdog —— 看门狗 API 功能可用(喂狗→重装、超时→复位;健康样例干净跑到调度器),但样例唯一的成功 标志是中断模式的 "watchdog kick timeout!" 回调,而该回调未建模(见 已知边界), 故无 UART marker 可断言。

重新生成 fixture:scripts/build-csdk-samples.sh(从 fbb_ws63 checkout 选一个 CONFIG_SAMPLE_SUPPORT_*、 干净构建、strip 到 ~400 KB)。

ws63-rs(Rust)冒烟

scripts/smoke-test.sh(真值见脚本)属于进阶/CI 验证路径,入门不需要相邻的 hisi-riscv-rs checkout。每条都是端到端、 断言串口/MMIO 标志:

固件验证什么成功判据
blinkyGPIO0 输出翻转 + xlinx/启动正确0 非法指令陷阱 + 观察到 GPIO 翻转(pin0 拉高)
uart_hello自定义 UART0 TX串口打印 Hello from WS63 on QEMU!
timer_irqTIMER_0→IRQ 26→ISR(mie 类中断)串口 timer irq #N 递增 + OK: timer interrupts delivered
gpio_irqGPIO0→IRQ 33→ISR(≥32 自定义本地中断)串口 gpio irq #N + OK: custom local IRQ (>=32) delivered
reset_demosoftware_reset() + reset_reason() 往返重启 ≥2 次 + OK: software reset observed(reset_reason=Software)
dma_loopbackmem↔SPI0 外设 DMA + SDMA 通道DMA LOOPBACK TEST: PASS
wifi_blob_link链接 libwifi_rom_data.a + 重定位BLOB LINK SPIKE: PASS
rf_port_demows63-rf-rs porting 层 + blob 经其链接RF PORT DEMO: PASS
sched_selftestws63-rf-rs 协作调度器(上下文切换 + 信号量)SCHED SELFTEST: PASS
semihost_selftestsemihosting 退出码(M/F/Zicsr 自检)QEMU 退出码 0(免解析 UART;见 运行选项 SEMIHOST)

外设建模覆盖

WS63.svd 的全部 35 个外设均已建模(无裸 catch-all 黑洞)。分两档,完整矩阵见 外设建模矩阵:

  • 行为完整(真实数据/计时/中断/回环):CPU+内存、xlinx ISA、UART0/1/2(TX+RX)、TIMER×3、GPIO+pinmux、 中断控制器(26–31 + ≥32,含 LOCIPRI/PRITHD)、DMA/SDMA、RTC、WDT、I2C0/1、SPI0/1、I2S、LSADC、TSENSOR、 EFUSE、TRNG、TCXO、SFC、CLDO_CRG 时钟门控。
  • 配置影子(可读回、无副作用):RF_WB_CTL、SHARE_MEM、SPACC/PKE/KM、部分 SYS_CTL1/IO_CONFIG 位等。

明确不在覆盖范围内

项状态原因
Wi-Fi / BT / SLE 射频(PHY/RF)❌ 不仿物理边界
C SDK 含 BT/WiFi app 的深层初始化❌ 崩于 ROM 数据墙vtable/NV/efuse/RF 标定无 dump,需裁剪任务
逐芯片出厂标定 NV(xo_trim 等)❌ 固有缺失生产时烧录,任何构建产物都不含
真实 AES/SHA/RSA(SPACC v2)❌ 影子描述符协议复杂且未被触发
看门狗中断模式超时回调❌ 未建模需 vCPU 同步的 PC 注入
周期精确时序❌ 近似TCG 非微架构;ICOUNT 仅 IPC=1 近似
双核 / 第二 hart❌ 不适用WS63 单核(已核实)
snapshot / migration❌ 未投入按需

这些边界的展开讨论见 已知边界与非目标。

相关

解释 · 理解导向

背景与权衡的讨论——理解某件事为什么这样设计、边界在哪。

设计取舍:为什么 fork QEMU

本文讨论 ws63-qemu 的几个核心设计决策为什么这么做——目标、为何 fork 而非树外插件、为何不用 -M virt、 以及为何按 QEMU 版本维护 patch-series。要查机器组件的事实清单见 机器模型。

目标

在 QEMU 上仿真 HiSilicon WS63(RISC-V)SoC,让 ws63-rs 裸机固件 无需真实硬件即可运行——为 ROADMAP 阶段 1「硬件在环 bring-up」提供一个软件在环替代信号:证明内存布局、 startup(PMP/FPU/cache/栈/数据重定位)、链接脚本在一个 WS63 地址空间模型上能正确跑起来。

它是 软件在环(SIL) 的驱动验证底座,不是周期精确的微架构模拟器。

方法:fork 一个固定版本的 QEMU(仿 esp-qemu)

QEMU 没有稳定的「树外板卡插件」ABI,因此自定义 SoC 的标准做法是 fork 并加一个 in-tree 板卡文件 ——Espressif 的 esp-qemu 即如此(hw/riscv/esp32c3.c + 自定义 CPU/外设)。本项目同样:

  • 默认基线 QEMU v10.0.0(scripts/build.sh 克隆该 tag;从 v9.2.4 升级而来)。同时维护 v10.2.3、v11.0.1 与 v9.2.4。
  • 新增单文件 hw/riscv/ws63.c(机器模型 + 外设设备)+ insn_trans/trans_xlinx.c.inc(xlinx 解码器)+ tests/qtest/ws63-test.c 作为新文件直接拷入(不冲突,保留在 src/ 便于编辑);对既有 QEMU 文件的改动 走按版本分目录的 patch-series(见 patch-series 参考)。
  • 只构建 riscv32-softmmu 一个目标,控制构建时间(~10–20 分钟)。

为什么不用树外补丁

board 没有稳定插件 ABI——树外加载自定义机器没有受支持的途径,所以必须 fork 进树。

为什么不用 -M virt

固件按 WS63 地址链接(外设基址、内存布局都是 WS63 特定的),首次访问 WS63 外设就会在 virt 上 fault—— virt 是一个通用虚拟机器,其地址布局与 WS63 完全不同。

为什么按 QEMU 版本维护 patch-series

对既有 QEMU 文件的改动会随版本漂移(头文件搬家、结构体/字段偏移变化、惯用法变更)。把这些改动凝固进单一补丁 会在每次 QEMU 升级时全线冲突。按版本分目录(patches/<tag>/)让每条序列对其基线干净可套,升级是「新建一个 目录」而非「改一份会到处冲突的补丁」。

漂移有多真实:10.0→10.2 就移动了 insn_len、把 CPU 定义改成声明式 DEFINE_RISCV_CPU、把 decode_opc 改成表 驱动、把 CharBackend 改名 CharFrontend;10.2→11 又把六个 hw/*.h 迁到 hw/core/*.h。移植流程见 移植到新的 QEMU 版本。

「可选确定性指令计时」而非周期精确

TCG 不模拟流水线/cache/逐指令周期,默认虚拟时间自由运行(每次运行计时随宿主时钟漂移)。ICOUNT=1 (= -icount shift=2,约 250 MHz、IPC=1)把虚拟时间绑定指令数,同一固件每次运行结果完全一致(实测 1e6 循环 3 次均 = 2,880,003 ticks)。这是 IPC=1 近似,不是真实微架构周期精确——真周期级需 gem5 等,非本仿真器 目标。详见 已知边界与非目标。

多角度交叉验证

仿真器既跑 ws63-rs(Rust,标准 rv32imfc),也跑 fbb_ws63 C SDK 厂商 gcc 编译的固件(需实现 xlinx 自定义 ISA)。 两侧固件在同一外设模型上对照,可交叉验证内存映射、启动、外设寄存器时序与驱动逻辑——用厂商实现反向核验 rs HAL。 方法与结论见 多角度对齐分析。

相关

中断控制器设计

本文讨论 WS63 中断控制器(ws63-intc)的设计与投递机制为什么这样。CSR 地址、IRQ 号等事实清单见 机器模型 §中断控制器。

WS63 用 HiSilicon 自定义的「riscv31」核内 CLIC 式方案,不是 CLINT/PLIC。设备 IRQ 分两类,这两类的差异是整个 设计的关键。

两类中断线

IRQ 26–31(mie 类):TIMER_0/1/2、RTC、I2C0

这些用标准 mie 位。固件经真实 mie CSR 使能;ws63-intc 收到外设 IRQ 线后用 riscv_cpu_update_mip(env, 1<<n) 拉高 mip[n],QEMU 经向量化 mtvec(mode 1)派发到 mtvec + 4*n。 完整保真、已实测(见 timer_irq 示例)。

这一类能直接复用 QEMU 既有的 RISC-V 中断机制,因为 IRQ 号 < 32,放得进 RV32 的 32 位 mip/mie。

IRQ ≥32(自定义本地类):GPIO=33、UART=53…LSADC=72

这一类是为什么需要 target/riscv 补丁的根本原因。它们用核内自定义 CSR LOCIEN0-2(0xBE0)/ LOCIPD0-2(0xBE8)/LOCIPCLR(0xBF0),且 mcause 取值 32–72 放不进 RV32 的 32 位 mip/mie——所以 QEMU 原生机制无法表达它们。

通过 target/riscv 补丁(patch-series 0001)核内投递:

  • CPUArchState 加 ws63_locien/locipd 状态。
  • riscv_cpu_local_irq_pending() 在标准 mip/mie 检查之后增查 locipd & locien(受 mstatus.MIE 门控), 返回 IRQ 号。
  • riscv_cpu_do_interrupt() 既有逻辑即以 mcause = irq 投递(向量化时 mtvec + 4*irq)。
  • 设备经新导出的 riscv_cpu_set_local_irq(env, irq, level) 置/清 locipd;LOCIEN/LOCIPCLR CSR 写更新 enable/清 pending。

完整投递、已实测(见 gpio_irq 示例,GPIO0 pin0→IRQ 33)。这条自定义 pending 通路绕开了 RV32 32 位 mip/mie,对应 ws63-rt 的 local_interrupt_handler。

优先级 / 阈值

ws63_local_irq_pending() 读 LOCIPRI(每 IRQ 4 位,8 个/寄存器)取优先级,仅当 优先级 > PRITHD(严格大于) 才投递;多个候选取最高优先级、同级取最小 IRQ 号。复位默认每 IRQ 优先级 1、 阈值 0(即退化为按号投递,兼容旧行为)。CSR 写经 ws63_loci_write 镜像入 env->ws63_locipri[]/ws63_prithd。 已用 LOCIPRI/PRITHD 探针实测(屏蔽 / 抢占 / 严格 > 边界 5/5 通过)。

投递时自动清 LOCIPD(边沿/一次性,匹配 C SDK 的 default_local_interrupt_handler 无 LOCIPCLR 通路)。

已知简化

被阈值屏蔽的「已挂起」IRQ 在仅写 CSR 降低阈值(无新边沿)时不会自动重投——需要一次新的中断源边沿。 固件一般「先配优先级/阈值、后开中断源」故不触及;已实测该常规顺序 5/5 通过。

小结

mie 类(26–31)与自定义本地类(≥32)两条中断线现均已端到端验证。前者复用 QEMU 原生机制,后者必须靠 target/riscv 补丁才能表达——这正是项目需要 fork QEMU 而非树外插件的具体例证之一(见 设计取舍)。

相关

已知边界与非目标

本文解释用 ws63-qemu 时必须知道的语义边界——它们大多不是缺陷,而是仿真器目标与物理/数据现实的边界。 不理解会误判结果。哪些外设建模到什么程度的事实表见 验证覆盖范围 与 外设建模矩阵。

语义边界(用之前必读)

1. 默认非周期精确,虚拟时间自由运行

TCG 不模拟流水线/cache/逐指令周期。需要可复现计时就用 ICOUNT=1,但那是 IPC=1 近似(≈250 MHz), 不是真实微架构周期。真周期级请用 gem5 等,非本仿真器目标。这条取舍的背景见 设计取舍。

2. 掩膜 ROM 是桩的

WS63 应用核有一块片上掩膜 ROM(0x109000..0x14C000,厂商不发布二进制)。仿真器在 ws63_rom_call 里 仿真用到的 ROM 函数,**未识别的 ROM 地址返回 0(成功)**让固件不崩。代价:依赖 ROM 数据(非代码)的 功能无法重建——见下「ROM 数据墙」。清单见 ROM 桩目录。

3. -kernel 启动跳过 bootloader

直接把 app 装入 RAM,不经 flashboot,所以 flash XIP 窗口默认是空的,C SDK 的分区表 / NV 读取会失败 ([UPG] ...flash_start_addr fail、nv read sw fail)。解法:NV=1 回填分区表 + NV(见 运行固件)。但逐芯片出厂标定键(如 xo_trim 晶振温补)在生产时烧录,任何 构建产物的 NV 都没有,故 xo_trim ... nv read sw fail 一行固有残留,非缺陷。

4. 配置类外设是「影子」

寄存器可读回但无副作用——典型是 RF / PHY / 晶体等本质模拟量或物理硬件(RF_WB_CTL、SHARE_MEM、 SPACC/PKE/KM 等)。行为可内部计算的外设是真实的。为什么配置类只能是影子,见 外设建模矩阵 §配置类为何是影子。

5. 目标核 = 单核 RV32IMFC

单浮点(F)、压缩(C)、无原子(A 关)、无 D;WS63 是单核(无第二 hart)。 WS63 单核已经 fbb_ws63 C SDK 核实(ch2_system.md 明确「系统提供一个自研 RISC-V 处理器作为主控 CPU」; Wi-Fi/BT 是链接进同一应用镜像的库,HMAC/DMAC 是 Wi-Fi 软件分层而非两颗物理核)。

6. 时钟树按标称建模,频率相关的「错时钟/错波特」症状无法复现

QEMU 把时钟树建模在标称(post-clock_init)值(CPU 240 MHz、UART 160 MHz PLL、定时器 24 MHz TCXO、PLL 报告已锁)。这是 QEMU 的固有边界,不是可建模能解的缺陷,原因有三:

  1. PLL 由掩膜 boot ROM 锁定——HAL clock_init 注释明确「CPU PLL(240 MHz)由 boot ROM 在 bootloader 之前配置」。-kernel 启动跳过 boot ROM,直接从 app 入口执行,此时已是「boot ROM 之后」状态,PLL 本就 应报已锁(恒报已锁是正确的;改成启动未锁反而错——probe_clocks() 会读到错值)。
  2. TCG 不建模 CPU 频率——指令以宿主速度执行(-icount 仅 IPC=1 近似)。所以依赖 CPU 真实频率的忙等延时 (如 delay_ms 在低频启动时钟下变慢)在 QEMU 里不会变慢——这正是「非周期精确」(见 §1)的同一根因。
  3. chardev 不限速——UART 字节无论分频器配成什么都照样发出,所以「错分频器→波特率乱码」在 QEMU 里也复现不了, 只能在写分频器时标注有效波特率(-d guest_errors,见 运行选项)。

能做与不能做:时钟源选择 / 门控已建模为状态并影响定时器/外设的模型频率(清定时器门会冻结定时器,已实测); 但「跳过 clock_init 就在 QEMU 里也算错时序」这一行为复现不了——固件在 QEMU 里跑得「时序正确」不代表在硅片上正确。 真周期级请用 gem5 等。对应 issue #3(已说明并 close as not planned);写分频器/CLK_SEL 时的诊断标注是现实上限。

ROM 数据墙——仍然受限的东西

ROM 里不仅有代码(可在宿主 C 里仿真),还有数据(vtable 数据、子系统常量、出厂标定),数据没有 dump 就无法重建。以下因「ROM 数据墙」仍受限(均非建模能解):

  • BT / WiFi / SLE 的射频(PHY/RF) —— 物理边界,不仿;C SDK 含 BT/WiFi 的 app 会在子系统深层初始化崩于 ROM 数据墙(vtable/NV/efuse/RF 标定无 dump),需在 config.py 注释 BGLE/BTH/WIFI_TASK_EXIST 裁剪这些任务。
  • 逐芯片出厂标定 NV 键(如 xo_trim 晶振温补)——生产时烧录,任何构建产物的 NV 都不含,固有残留一行 xo_trim_temp_comp::nv read sw fail(非缺陷)。
  • 看门狗中断模式回调(“kick timeout!”):WDT 超时→复位已建模,但超时→中断回调→再复位未建模 (从虚拟时钟定时器回调改写 env->pc 注入固件回调不安全,定时器回调与 vCPU 取指边界不同步)。
  • 真实密码学(AES/SHA/RSA 的 SPACC v2 描述符协议)——未在启动路径,按需扩展。

详见 ROM 桩目录 §C/§D。

冻结 / 非目标

均非建模能解或非本仿真器目标:

  • RF / PHY 无线电仿真——不做;连接性若推进只到 MAC / SLIRP 边界(同 esp-qemu)。BS21/BS2X 的 BLE/SLE 连接性 为何也是死胡同,见 BS21 连接性可行性。
  • 双核 / 第二 hart——WS63 单核(已核实),不做。
  • 周期精确时序——TCG 非微架构;-icount 仅 IPC=1 近似。真周期级请用 gem5 等。
  • snapshot / migration——按需,默认不投入。

FAQ

现象原因 / 解法
固件一上来就大量 illegal instruction多为 xlinx ISA 或 ROM 调用;DEBUG=1 看 qemu.log 的 pc 落点,ROM 区(0x109xxx)是预期被拦截的
C SDK app 反复打印 flash_start_addr fail / nv read sw fail用 NV=1 回填;残留的 xo_trim 一行是固有的
C SDK BT/WiFi 任务崩溃预期(ROM 数据墙);裁剪 BGLE/BTH/WIFI_TASK_EXIST 任务
计时每次运行都不同默认虚拟时间自由运行;要可复现用 ICOUNT=1
看门狗样例“没看到 timeout 打印“中断模式回调未建模;健康喂狗→不复位是正确行为

相关

多角度对齐分析:ws63-rs HAL ↔ C SDK ↔ SVD(QEMU 为交叉验证台)

目标:用 ws63-qemu 同时运行 ws63-rs(Rust 裸机)与 fbb_ws63 C SDK(厂商 gcc)两侧固件, 在「外设寄存器布局/时序」「内存映射」「启动」「中断」多个维度交叉验证 ws63-rs HAL 的正确性。

方法(四方对齐)

来源角色
ws63-pac/ws63-svd/WS63.svd寄存器布局的「名义真值」(厂商 SVD;svd 现为 ws63-pac 的嵌套子模块)
ws63-hal(Rust)被验证对象:ws63-rs 的 HAL 驱动
fbb_ws63(C SDK)权威参照:厂商在产硬件上验证过的驱动
ws63-qemu 运行时交叉验证台:QEMU 外设模型按 SVD + rs HAL 建模;若 C SDK 固件在同一模型上也能正确工作,则证明 rs HAL 的寄存器模型与 C SDK 一致

运行时交叉验证是最强证据:QEMU 的 UART/timer/中断/内存模型源自 rs HAL + SVD;而厂商 gcc 编译的 C SDK 固件(flashboot、ws63-liteos-app)在同一模型上跑出正确 UART 输出、定时器中断、启动流程 ——等于用厂商实现反向核验了 rs HAL。

UART(最强对齐,两侧均经此打印)

寄存器SVD / rs HAL (uart.rs)C SDK (hal_uart_v151_regs_def.h)QEMU 模型 (ws63.c)
data(RX 读/TX 写)0x040x04(intr_id@0x00, data@0x04)UART_DATA 0x04
line_status0x340x34UART_LINE_STATUS 0x34
fifo_status0x44(tx_full[0]/tx_empty[1]/rx_full[2]/rx_empty[3])0x44UART_FIFO_STATUS 0x44,同位定义
  • TX 时序一致:rs HAL write_byte 轮询 fifo_status.tx_fifo_full(0x44 bit0) 为 0 → 写 data(0x04); C SDK hal_uart_v151 经 hal_uart_is_tx_fifo_full(读 fifo_status)+ hal_uart_get_data_register(&->data) 同序。
  • 关键结论:WS63 UART 不是 16550(data 在 0x04 而非 0x00;自定义 fifo_status@0x44)—— rs HAL、C SDK、SVD 三方一致,且两侧固件在 QEMU 同一模型上打印成功 → rs HAL 的 UART 寄存器模型正确。

时钟 / TCXO

  • TCXO 计数器 0x440004C0(bit4 count-valid + 64 位计数 +0x04/+0x08):C SDK hal_tcxo(flashboot) 实际使用,QEMU 据此建模后 bootloader 时钟 bring-up 通过。rs HAL 已对接——ws63-hal/src/tcxo.rs 的 TcxoDriver 用同一「写 refresh 锁存 → 等 valid 标志 → 读 count0..count3」过程读 64 位计数, 与 C SDK / QEMU 模型一致。
  • SYS_CTL0 时钟状态(TCXO/PLL 锁,0x40000000):两侧 init_clocks 均经此,QEMU 模型同时满足。

定时器 / 中断

  • TIMER_0 → IRQ 26:rs 示例 timer_irq 与 C SDK radar_timer_init([RADAR_LOG] radar_timer_init succ) 都用 IRQ 26(标准 mie 位 + 向量化 mtvec)——两侧在 QEMU 同一中断模型上闭环,IRQ 号与投递路径对齐。
  • IRQ≥32(自定义 LOCIxx 本地中断):rs 示例 gpio_irq(GPIO0→IRQ 33)验证;C SDK 同走该通路。 机制见 中断控制器设计。

内存映射 / 启动

  • BOOTROM/ROM/ITCM/DTCM/FLASH/SRAM 布局(ws63-rt/memory.x ↔ C SDK memory_config_common.h)一致: 两侧固件在同一 QEMU 内存映射上正确 relocation/启动(rs blinky 等;C SDK app 完成 relocation/dyn_mem_cfg)。
  • 外设基址(UART 0x4401_x000、TIMER 0x4400_2000、GPIO 0x4402_8000、SFC 0x4800_0000、EFUSE 0x4400_8000) :SVD ↔ C SDK 头文件 ↔ rs HAL/PAC 三方一致。详见 内存映射。

发现与差异

  1. ✅ UART/timer/中断/内存映射/外设基址:rs HAL 与 C SDK 完全对齐(同寄存器、同位、同时序),运行时双向验证。
  2. ✅ TCXO 计数器:C SDK 与 rs HAL(ws63-hal/src/tcxo.rs 的 TcxoDriver)都用 0x440004C0 的 64 位 自由计数(同 refresh→valid→读过程),与 QEMU 模型一致。
  3. 🟡 指令集:C SDK 用 HiSilicon xlinx 自定义指令(GCC),rs 用标准 rv32imfc——功能等价,密度不同 (见 Rust 工具链是否需要 xlinx)。
  4. ⬜ flash/NV/efuse 内容:需真实分区/标定数据(无 dump),属数据墙而非寄存器布局差异(见 已知边界)。

总评:在已建模的外设维度上,ws63-rs HAL 的寄存器布局与时序与厂商 C SDK 一致,并经「两侧固件跑在 同一 QEMU 模型」运行时交叉验证(含 TCXO 64 位计数器,两侧均已对接 0x440004C0)。剩余差异是 xlinx 指令密度(功能等价)与 flash/NV/efuse 标定数据墙——均非寄存器布局问题。

Rust 工具链是否需要实现 xlinx 扩展?(含 HiSpark riscv-llvm 调研)

结论先行:不必要。ws63-rs 用标准 rv32imfc 即可正确运行(已在 ws63-qemu 上验证); 在 Rust 工具链里实现 xlinx 仅是「代码密度优化」,代价是把 Rust 重挂到 HiSilicon 的 LLVM fork, 收益有限且只能拿到一个很小的子集。

调研:gitee.com/HiSpark/riscv-llvm(LLVM 15.0.4 fork)

该 fork 在 llvm/lib/Target/RISCV/ 里确有 xlinx 扩展,但只是一个名为 “xlinxma GROUP1” 的小集合:

  • RISCV.td:SubtargetFeature<"xlinxma", "HasExtLinxma", "true", "LINXM GROUP1 Instructions">
  • 指令定义在 HiMCUGenInstrInfo.td / HiMCUGenImmOperands.td / HiMCUGenCompressPat.td,仅 5 条 16 位指令: c.neg、c.sbz/c.shz/c.swz(store-zero,把 x0 存到 byte/half/word)、c.xori。

关键发现:GCC 与 LLVM 的自定义 ISA 不一致

工具链自定义集合C SDK / ws63-rs 用哪个
GCC(riscv32-linux-musl-gcc,-march=rv32imfcxlinxma_xlinxmb_xlinxmc)完整集:l.li、{add,sub,or,xor,and}shf、b*i、muliadd、jal16/j16、ldmia/stmia、push/pop/popret、uxtb/uxth、压缩 lbu/lhu/sb/sh(见 xlinx ISA)C SDK(ws63-liteos-app/flashboot)用 GCC 全集
LLVM(HiSpark fork 15.0.4,xlinxma)GROUP1 小集:c.neg、c.sbz/shz/swz、c.xori(5 条)ws63-rs 未用(标准 rust = 标准 LLVM = 无 xlinx)

即:QEMU 里我实现的是 GCC 全集(C SDK 固件所需);LLVM fork 的 GROUP1 是另一套、且是早期/部分移植, 两者编码空间与指令都不同(如 LLVM c.sbz 在 funct3=0001/Q2,GCC 的 sb 在 funct3=101/Q0)。

ws63-rs 是否需要 xlinx?

  • 正确性:不需要。ws63-rs 由自定义 rust 工具链以 riscv32imfc(硬浮点、无原子)构建, blinky/uart_hello/timer_irq/gpio_irq 均在 ws63-qemu 上正确运行 —— 标准指令集足够。
  • 代码密度/性能:有边际收益。xlinx(尤其 GCC 的 l.li/push-pop/*shf)能压缩固件体积、减少指令数; 对 flash 受限的 MCU 是 nice-to-have,但非必需。
  • 代价很高:要让 rustc 发射 xlinx,必须把 Rust 的 LLVM 后端换成 HiSilicon 的 fork(15.0.4,相对当前 rustc 偏旧), 自行构建 rustc —— 工程量大;且即便如此,Rust 只能拿到 LLVM 的 GROUP1 小集(5 条),拿不到 GCC 的完整密度优化。

建议

  1. ws63-rs 维持标准 rv32imfc(现状),不引入 xlinx —— 正确、可用、工具链可维护。
  2. 若未来确需极致代码密度:评估把 GCC 的 xlinxma_mb_mc 通过 inline asm/intrinsic 局部使用(汇编器支持), 或推动 HiSilicon LLVM 把 GCC 全集补齐后再考虑 rustc 重挂 —— 优先级低。
  3. 对 ws63-qemu 的影响:当前 QEMU 实现 GCC 全集,足以跑 C SDK 与(未来若用 inline-asm-xlinx 的)rs。 若有人用 HiSpark LLVM 构建固件,需再补这 5 条 GROUP1 指令(编码已在 HiMCUGen*.td,工作量小)——目前无此需求。

BS21 / WS63 connectivity (BLE / SLE) emulation feasibility

Measured + reconnaissance verdict on whether the BLE 5.4 / SLE (NearLink) connectivity of the BS2X family (and WS63) can be simulated in QEMU. Short answer: not by emulating the radio, and not cheaply at HCI either — both sides of every useful boundary are closed binary blobs.

This doc records the conclusion so the dead end is not re-explored. The empirical probe code lives on the sle-radio-probe branch (a B_CTL trace+absorb model in src/hw/riscv/bs21.c, gated on BS21_BCTL_TRACE=1); it is deliberately not in master (see “Why no register absorber in master” below).

Architecture (both BS21 and WS63)

Single core. The BLE/SLE host and controller both run as LiteOS tasks on the app core — there is no separate BT core (the SLAVE_CPU_BT enum is dead; core:2 in a reboot dump is APPS_CORE). The stack splits at two boundaries:

  app (C source)
    │  high-level BLE/SLE API
  host stack blob   libbth_gle.a / libbg_common.a   (GAP/GATT/ATT/L2CAP/SMP, SLE, HCI encode)
    │  api_h2c_write()  ↓        ↑  event callbacks      ← the "HCI boundary" (in-memory, not UART)
  controller blob   libbgtp.a    (Link Layer FSM, HCI controller side)
    │  56 write-only PHY regs + IRQ 26 events             ← the "radio boundary" (B_CTL MMIO)
  B_CTL radio MMIO @0x59000000  +  closed analog PHY

Boundary 1 — radio MMIO (B_CTL @0x59000000): measured, dead end

Probed by tracing every B_CTL access while running the un-trimmed (BT-present) vendor firmware on -M bs21:

  • B_CTL low regs 0x59002008..0x5900388c: 56 writes, 0 reads. A flat, write-only PHY/RF constant table from one code site — no register-level poll or handshake. Trivially absorbed (~30 lines), but the values are opaque RF tuning constants meaningless without the analog model.
  • BT_SUB / BT_EM 0x59400000..0x59417fff: ~16.5k accesses = RAM (descriptors, buffers), not registers. RAM-backing it removes the near-NULL-deref crash that trace-absorb causes.
  • After PHY init + exchange-mem setup, the controller reboots core:2 (APPS_CORE), cause 0x2045, waiting for a PHY RX/TX event on IRQ 26 (BCPU_INT0_ID) that no analog model emits.

Verdict: the registers are shallow; the wall is the radio event/timing model, which is open-ended reverse engineering of the closed controller blob with no register-readback breadcrumbs. You can make the controller start but never work. This is where every radio-MMIO emulation attempt stops.

Boundary 2 — HCI (host ↔ controller): exists, but blob-on-blob

The host hands commands to the controller through an in-memory HCI interface (api_h2c_write() exported by libbgtp.a; controller→host via event callbacks), not over a UART. For BLE the packets are presumably standard Bluetooth-SIG HCI (host objects gle_hci_cmd, gle_hci_ev). SLE rides the same api_h2c_write boundary with HiSilicon-private command/event codes (GLE = unified BLE+SLE host).

Critically: both the host (libbth_gle.a) and the controller (libbgtp.a) are closed .a archives with no .c source. There is no DTM / HCI-over-UART controller firmware in this SDK, so BlueZ / standard H4 tooling cannot attach.

What this leaves:

  • Theoretically feasible for BLE: replace one blob with a synthetic that satisfies the other’s ABI — provide your own api_h2c_write + event callback, link the host blob against a synthetic controller. You do not need the host’s source, only its linker symbols and a valid message stream.
  • Substantial, not cheap: you must (1) reverse the exact packet ABI at api_h2c_write, and (2) satisfy the host blob’s other dependencies (LiteOS, timers, the controller’s remaining exported symbols) — a large surface.
  • SLE: effectively infeasible. Proprietary command/event set, no public reference, fused into the GLE blob.

Probe result (the “try it”): HCI is unreachable at runtime, and not raw H4

Two concrete findings from actually attempting the api_h2c_write trace:

  1. The HCI boundary is downstream of the radio wall in boot order. Located api_h2c_write in the BT-present firmware by prologue signature (0x901357ca; the trimmed build’s 0x9012bfe6 matches application.map). Running the un-trimmed firmware on -M bs21 with the B_CTL probe model, that PC is never executed (0 hits in the instruction trace) — the bt controller task brings up the radio and crashes/reboots (core:2, cause 0x2045) before the host ever sends an HCI command. So you cannot observe the HCI ABI by running the firmware without first solving the radio-event problem (the IRQ-26 PHY wall). The HCI boundary sits behind the dead end.

  2. The boundary is a HiSilicon-framed message ABI, not raw H4 HCI. Static disassembly of the host caller (gle_hci_send_data.c.obj → gle_hci_command_encode_send_tl) shows api_h2c_write(a0,a1,a2,a3) is not “pointer + length to a standard HCI packet”: a0 carries a message type in its high byte (a0>>16, e.g. 0xc), a1/a2 carry a code/length, and a3 points to a small payload assembled on the stack. So the “bridge the host blob to BlueZ/NimBLE over standard H4” shortcut does not apply — the framing is private and would itself have to be reverse-engineered.

Net: HCI-boundary virtualization is gated behind the same radio wall at runtime and uses a non-H4 private ABI, so neither runtime observation nor standard-tool bridging works without reverse-engineering the closed blobs.

Recommendation

  1. Do not pursue radio-MMIO emulation for functional connectivity — confirmed dead end (Boundary 1). Same conclusion for WS63 (identical architecture).
  2. The HCI-boundary probe was run and is refuted as a cheap path (see “Probe result” above): api_h2c_write is unreachable at runtime (radio wall first) and uses a private, non-H4 framing. So an HCI-boundary effort would require building a synthetic controller against the host blob’s ABI from static reverse engineering only — substantial, BLE-only, no runtime breadcrumbs.
  3. SLE stays out of scope — proprietary with no reference implementation.

Bottom line: every boundary that could be virtualized (radio MMIO, HCI) is either behind closed-blob analog/event timing or behind a private blob-to-blob ABI that is itself gated behind the radio wall. Connectivity emulation is not a viable next step; the functional-peripheral work is where the leverage is.

Reproduce the radio probe

git checkout sle-radio-probe
# build, then:
BS21_BCTL_TRACE=1 qemu-system-riscv32 -M bs21 -nographic -serial mon:stdio \
  -device loader,file=<flashboot-code>,addr=0x40000 \
  <flash1 64K chunks at 0x90100000..> \
  -device loader,addr=0x40000,cpu-num=0  2>bctl_trace.log

(flashboot code = tail code_size bytes of the signed flashboot image; flash1 = the pre-trim BT-present build.)

Why no register absorber in master

The B_CTL window is left to fault in the bs21/bs22/bs20 machines on purpose. No supported firmware touches 0x59000000 (the functional examples are BT-trimmed; the Rust drivers never reach the radio), so an absorber would have zero functional consumer, and a silent return-0 would let buggy firmware limp instead of failing loud. A clean fault at the recognizable radio base is the better signal. The trace+absorb model stays on sle-radio-probe for anyone who wants to re-run the probe.

Running BS21/BS2X vendor firmware on -M bs21

Status: loaderboot executes (the first vendor boot stage runs its full init on -M bs21). This documents the signed-image format, the BS21 ROM table, and the remaining boundaries — the BS21 analog of the WS63 C-SDK-on-QEMU work.

What works

scripts/bs21-vendor-boot.sh <loaderboot_sign.bin> boots the fbb_bs2x prebuilt loaderboot (src/interim_binary/bs21e/bin/boot_bin/loaderboot-bs21e-1100e/): it runs ~480 instructions of real vendor code — relocates the boot-param block to the DTCM, brings up PMU (0x57004600), and reaches its interrupt-driven download-mode idle spin (j . @0x4298e) — all standard RV32 + xlinx, zero illegal-instruction traps. The xlinx decoder (active for -M bs21 via the ws63 CPU type) handles the vendor compiler’s custom instructions transparently.

Signed-image format (loaderboot)

Multi-section HiSilicon signed image:

offsetsection
0x000image header — magic 0x4bd2f01e, header-len 0x40 (@0x0c), KeyAlg 0x2a13c812 (@0x18, = the sign cfg) + zero pad
0x100code-info header — magic 0x4bd2f02d; code size = u32 @ (code-info + 0x24); then a hash block
tailthe code itself (code_size bytes, ending at EOF) → for loaderboot: 0x5c20 bytes @ file 0x300

The mask ROM copies the code to ITCM and jumps to it. We instead load the extracted code at 0x40000 (APP_ITCM_ORIGIN, “use itcm start addr load loaderboot”) and set the reset PC there.

flashboot uses the SAME two-header structure, only with different magics (image header 0x4b1e3c1e = its ImageId, code-info header 0x4b1e3c2d @0x100, size @ code-info+0x24 = 0x8ab0, code at file 0x300). bs21-vendor-boot.sh accepts both magics, so it loads flashboot too. flashboot also links to 0x40000: it runs ~206 instrs (reset → ULP_AON clock config @0x5702c*** → a BSS/relocate clear loop → its main code @0x40552), then reaches an early halt (j . @0x40120) when it touches the SFC (Serial Flash Controller @ 0x90000000: it writes 0x509 to 0x90000210 and calls an SFC routine @0x4097a) — the SFC/flash is not modelled, so the flash read returns garbage and flashboot stops. flashboot’s source (flashboot_init + usb_download + upgrade_version_check) confirms it reads flash early. SFC modelling is the next boundary for flashboot to load the app and print its banner.

Memory-map fix (found by running firmware)

The loaderboot reset code copies the boot-param block to 0x20000000 — the real BS21 DTCM (APP_DTCM_ORIGIN, len 0x10000). bs21.c had wrongly placed DTCM at 0xF0000; loaderboot faulted writing to 0x20002d50. Fixed: DTCM → 0x20000000, ITCM window → 0x80000..0x100000. (M1 + WS63 qtests unaffected.)

bs21_rom_call — IMPLEMENTED (patches/v10.0.0/0005)

The ROM-call interceptor is now in place (mirrors ws63_rom_call, dispatched by the disjoint PC range). It emulates the secure-libc set the BS2X boot stages call: memset_s 0x3d80c, memcpy_s 0x3e07e, memmove_s 0x3e95c, sprintf_s 0x3ef18, snprintf_s 0x3ef60, vsnprintf_s 0x3ef92 (the printf family reuses the chip-neutral ws63_vformat()). Validated: a synthetic test that jalrs to 0x3d80c (memset_s) on -M bs21 is intercepted, the buffer is correctly filled, and execution resumes at ra — serial prints XA (X = UART, A = the memset’d byte). WS63 unchanged (5/5 qtests + M1 still pass). systick/tcxo/SFC/watchdog ROM APIs are not mapped yet (their BS21 addresses differ) — they fall through to the success stub. cpu_helper.c is version-volatile, so 0005 is on v10.0.0 (the build.sh default); other QEMU versions need it rebased.

Note on loaderboot: loaderboot is self-contained and makes zero ROM calls (it never executes below 0x40000), so bs21_rom_call does not change its behaviour — it reaches its download-mode idle spin either way. bs21_rom_call serves the later stages (flashboot/app), which call secure-libc heavily.

BS21 ROM table (source for bs21_rom_call)

Source: fbb_bs2x/src/drivers/chips/bs2x/rom/rom_config/acore/{acore_rom_n1200, romboot,remote_lib_boot}.sym (the BS21 acore.sym equivalents) + librom_callback.a. The ROM region is ~0x10000..0x42000 — disjoint from WS63’s 0x109000..0x14C000, so the interceptor can dispatch by PC range without a machine flag.

Secure-libc (the most-called by later stages), from acore_rom_n1200.sym:

functionBS21 addrfunctionBS21 addr
memset_s0x3d80csprintf_s0x3ef18
memcpy_s0x3e07esnprintf_s0x3ef60
memmove_s0x3e95cvsnprintf_s0x3ef92

SFC / boot (from remote_lib_boot.sym): uapi_sfc_init=0x1037a, uapi_sfc_init_rom=0x1044a, hal_sfc_init=0x1097c, … (romboot text 0x102ec..0x11e90).

To intercept, mirror WS63’s ws63_rom_call (patches/<tag>/0001): when env->pc is in the BS21 ROM range, emulate the function in host C (args in a0–a3, result in a0) and resume at ra.

Remaining boundaries (the deferred connectivity work)

  1. BS21 ROM-call table — DONE (bs21_rom_call, patches 0005; see above).

  2. flashboot image format — DONE (cracked; bs21-vendor-boot.sh runs it).

  3. SFC reg model — DONE. The ws63-sfc v150 model (RDID→JEDEC ID) is mapped at 0x90000000 in bs21.c; flashboot’s flash-ID init now succeeds (it runs 9 instrs further). M1 + WS63 unaffected.

  4. Flash CONTENT — partition table — DONE. flashboot reads the partition table via XIP at flash1 0x90100000 (its helper @0x406b4 just does lui a0,0x90100; ret) and checks the first word against magic 0x4b87a52d. bs21.c now maps a bs21.flash1 RAM region at 0x90100000; loading the prebuilt partition.bin (src/interim_binary/bs21e/bin/partition/.../partition.bin, magic at offset 0) there gets flashboot past the magic and ~940 instrs (4x) deep into its main path (partition parsing) before a new idle spin @0x4293a. Reproduce: bs21-vendor-boot.sh flashboot_sign_a.bin 5 0x40000 partition.bin.

  5. Full flash image — DONE. bs21-build-flash.sh unpacks the fbb_bs2x bs21e_all.fwpkg (loaderboot/partition/flashboot_a+b/application/nv) and lays each out at its partition flash offset (app @0x15000 → XIP 0x90115000); the boot script chunk-loads it at 0x90100000 (the generic loader caps a single raw load at ~0x10000). flashboot loads with the full flash present.

  6. The 0x4293a halt is a CRASH, not a boot-mode decision — it is the absent BS21 mask-ROM (cracked 2026-06-09). Earlier notes guessed a “boot-reason check” at 0x41730; that was wrong — it came from objdump misdecoding xlinx. The real mechanism (decoded with the vendor xlinx-aware objdump, see below):

    • The spin @0x4293a is flashboot’s panic tail: irq_lock() → record reason 0xdeadbeaf into DTCM @0x2000ffe8 (0x44f1c) → clear bit0 of BOOT_PORTING_RESET_REG 0x57004600 (0x42916) → j .. It is reached from flashboot’s exception handler (mtvec = 0x47bbc), which printf-dumps exception:/uwExcType=/mepc=/mstatus=/mtval=/mcause=/ra=/sp=… (strings @0x4871c+) via the log fn 0x43b40. So flashboot trapped, then panicked.
    • First trap: mcause=0x2 (illegal instr), mepc=0x0. flashboot validates the mask-ROM signature: 0x43c8a: lui a5,0x10; lw a4,32(a5) reads *(0x10020) and bne a4, 0xd4818193, 0x4403a. The ROM region (0x10000–0x40000) is zeroed RAM in -M bs21 (no mask-ROM dump exists), so *(0x10020)=0 ≠ 0xd4818193 → it tail- rets at 0x4403a with ra=0 → jumps to PC 0 → illegal-instruction trap.
    • Proof: inject the magic — -device loader,file=<0xd4818193>,addr=0x10020 — and flashboot stops branching to the crash (0 hits on 0x4403a), reaches the magic-OK path 0x43c98, and runs further (1260 vs 723 insns) before a second crash (mcause=0x5 load-access-fault @ 0x570004a0). So flashboot has many mask-ROM/peripheral dependencies; the magic is just the first.
    • Conclusion: like WS63, flashboot is tightly coupled to the silicon mask-ROM (signature + ROM data tables + ROM functions it tail-calls and registers callbacks into, e.g. ROM addr 0x1c200 at 0x43c9a). The SDK ships only librom_callback.a + .sym (no ROM image), so the path forward is emulating the BS21 mask-ROM (synthesize the signature/tables + extend bs21_rom_call for the functions it invokes) — the same scale as the WS63 ROM-on-QEMU work, i.e. the deferred connectivity-scale effort. This is NOT a single register to model.

    Tooling note (reusable): objdump misdecodes xlinx as fld/fsd/.insn/illegal. The fbb_ws63 vendor toolchain decodes it correctly — use …/cc_riscv32_musl_105/cc_riscv32_musl_fp/bin/riscv32-linux-musl-objdump -b binary -m riscv:rv32 -D --adjust-vma=0x40000 on the extracted code. It shows the real popret/push/pop/l.li/uxth/divu/... (BS2X linx131 == WS63 xlinx, same ISA). For a true linear trace + per-insn registers use QEMU -accel tcg,one-insn-per-tb=on -d in_asm,cpu.

  7. mask-ROM signature MODELLED → flashboot RUNS its init + prints its banner (2026-06-09). Two bs21.c-local fixes got flashboot from the 0x4293a panic to a clean, fully-diagnosed app-load attempt:

    • ROM signature: bs21.c pokes *(uint32_t*)0x10020 = 0xd4818193 into the RAM-backed ROM region at machine init (a small extensible rom_data[] table) — so the signature check passes natively (no -device loader injection).
    • TCXO collision fix: the shared ws63-tcxo model is mapped at 0x57000200 with a 0x1000 region (WS63 layout, where TCXO sits alone at 0x44000000) — on BS21 that swallowed the whole GLB_CTL_A/D block and faulted on flashboot’s 2-byte read of 0x570004a0 (TCXO ops require 4-byte). Per the SDK (TCXO_COUNT_BASE_ADDR 0x57000200, GLB_CTL_A 0x57000400) the TCXO_COUNT block is only 0x200, so bs21.c aliases just BS21_TCXO_SIZE 0x200 of the device — GLB_CTL_A/D fall through to the absorber. And BS21’s TCXO_COUNT sits at the region base (offset 0), not WS63’s +0x4C0, so ws63_tcxo_set_count_off(tcxo, 0) (new setter in hisi_riscv31.h) points the status/lo/hi there — flashboot’s *(0x57000200) bit-4 (count-valid) poll now passes (and BS21 uart_hello’s tick counter reads correctly too). WS63’s TCXO keeps its 0x4C0 default → 5/5 qtests unaffected.

    With these, flashboot runs 2953 insns (was 723) through its full init and prints:

    Flashboot Init! id = 0x0 / Power On / Reboot cause:0xF0F0 / Reboot count:0x0
    Flash Init ret = 0x80001341 / Load App Failed!
    
  8. SFC flash ID fixed → flashboot LOADS the app and JUMPS to it (2026-06-09). The Flash Init ret=0x80001341 was a flash-ID mismatch: BS21’s flashboot flash table is GigaDevice (sfc_config_info_porting.c: GD25LE80), and at 0x42122 it compares the read JEDEC ID against 0x1460C8 (mfr 0xC8 / type 0x60 / cap 0x14) — but the shared ws63-sfc model answered RDID with WS63’s Winbond W25Q16 (0x1560EF), so detection failed → error flag → 0x80001341. Made the SFC’s RDID ID per-machine (ws63_sfc_set_flash_id(), default W25Q16; bs21.c sets 0x1460C8). Now:

    Flashboot Init! / Power On / Reboot cause:0xF0F0 / Reboot count:0x0
    Flash Init ret = 0x0 / No need to upgrade / Jump to addr = 0x90115300
    

    With the full flash image present (bs21-build-flash.sh, app @ flash 0x15000 → XIP 0x90115000), flashboot’s flash detect ✓, upgrade-version check ✓, and it loads and jumps to the app at 0x90115300. The app’s RTOS startup then executes from XIP (sets mtvec, clears mstatus/mie, programs the LOCI custom CSRs 0x7c2/0x7c3, sets gp to DTCM 0x2000369c). flashboot’s job is complete. Reproduce: bs21-vendor-boot.sh flashboot_sign_a.bin 8 0x40000 <full-flash.bin> (the 4th arg is the bs21-build-flash.sh image). Running the full LiteOS BLE/SLE app from here is the broader connectivity work (full memory map / RAM init / all peripherals).

  9. App (LiteOS) startup runs — xlinx prefd decoded; app reaches its init-call table (2026-06-09). The app’s RTOS startup ran only 21 insns then trapped (mcause=2) on prefd (0x0003300b) in its .data copy loop — opcode 0x0b funct3=3, a cache-prefetch hint the xlinx decoder didn’t handle (it routed all 0x0b to ldmia/stmia, which matched no register bits → illegal). Added funct3 dispatch: 0x0b f3 0/1 = ldmia/stmia (already handled), f3 2/3 = pref/prefd → NOP (a prefetch is architecturally a no-op). With that, the app’s ldmia/stmia block copy runs and the app executes 934 insns of real LiteOS startup (copies .data flash→DTCM, sets up gp/sp, runs early init), then walks its init-call table (11 fn-ptrs @ 0x90115a10..0x90115a3c). Next blocker: init table[9] (0x90118a3a, a LiteOS task/thread registration — entry 0x90118cfe, stack 1536, prio 31, via the creator at 0x90118a2a) returns error 0x2000209 → the app logs + halts at 0x90128c62 (j .).

  10. xlinx muliadd immediate bug fixed → app passes LiteOS task creation, runs the full kernel init (2026-06-09). The init-call table[9] failure was a decoder bug, not a missing subsystem: LOS_TaskResume indexes the TCB array as base + id*92 via xlinx muliadd s0,s0,a0,92, but the decoder masked the immediate’s funct7 field to 5 bits (& 0x1f) instead of 7 (& 0x7f) — so any immediate > 63 was truncated (92 → 28). TCB[1] resolved to base+28 instead of base+92 → resume read the wrong TCB (status 0, not SUSPENDED) → 0x2000209. Verified against 82 muliadd instances with imm 64–192: the 5-bit mask got all 82 wrong, the 7-bit mask gets all 82 right (immediate = (funct7<<1) | bit14, 8 bits). With the fix the app passes task creation and runs the whole LiteOS kernel init (1663 app insns, was 934), reaching the app’s own reboot/idle path (APP|Reboot core:%d cause 0x%x, halts @0x90126206). Next boundary: that path reads a ULP_AON register (*0x5702c0f0 vs 0x10000, our absorber returns 0) and looks app/multi-core specific (core:2 — likely the BT slave core) — the deeper app/connectivity layer. (WS63 5/5 qtests + BS21 M1 unaffected by the decoder fix.)

  11. xlinx ldmia/stmia bank-selector bug fixed → the core:2 “multi-core” halt was an app CRASH, now gone (2026-06-10). Verified first whether multi-core was real: a BT slave core does exist (slave_cpu_t{SLAVE_CPU_BT, SLAVE_CPU_MAX_NUM}), but the APP|Reboot core:2 halt at 0x90126206 was not a multi-core boundary — core:2 is a hardcoded reboot-source code, and the path was reached via a NULL-pointer memcpy whose source pointer had been clobbered. Root cause: a third xlinx decoder bug. ldmia/stmia (opcode 0x0b) carries a 16-slot register presence bitmap, and bit31 selects which of two register banks each slot maps to; the old decoder only knew bank 0 ({ra,sp,s0,s1,a0,a1,s2..s11}). LiteOS’s 8-word memcpy block copy uses a bank-1 list ({ra,t0..t2,a0..a7,t3..t6}), so its ldmia/stmia decoded as bank-0 s-registers and clobbered the caller’s s0..s11 → a later memcpy(dst, s0=NULL, n) → crash → the reboot path. Added the bit31 bank selector + a [2][16] slot→GPR table (slot bits 7,8,9,10,11,20..30), derived and validated 100% against 164 real ldmia/stmia from the firmware (96 bank-0 + 68 bank-1). With the fix the crash is gone (0 traps, mcause=0) and the app advances past the reboot path into further init. Next blocker: the app now spins on a 32K-clock calibration poll at 0x901286b2 — while (!(*(u16*)0x57008488 & 1)), i.e. bit0 of HAL_CALIBRATION_32K_CLOCK_DET_STS (PMU2_CMU 0x57008000 + 0x488, hal_32k_clock.c:18); our generic absorber returns 0, so the wait never completes. Modeling that status bit as “calibration done” (bit0=1) is the next step. (WS63 5/5 qtests ×4 QEMU versions + BS21 M1 unaffected by the decoder fix.)

  12. 32K-clock calibration status modelled → app passes the poll, runs further bring-up (2026-06-10). Added a small bs21.clk32k MMIO region over the GLB absorber at the PMU2_CMU 32K-clock-detect block (0x57008480, size 0x20): DET_STS (+0x08 = 0x57008488) reads bit0=1 (DONE) / bit1=0 (not DOING), so hal_32k_clock_get_detect_ result()’s while (!(STS & 1)) completes immediately; CFG/VAL enable writes are no-ops. The detect result reads 0 — which is safe: calibration_get_clock_frq() (clock_calibration.c:55) guards if (result != 0) before dividing, so on 0 it keeps the default g_clock_32k = 32768 Hz. Verified the exact access sequence (WR VAL=0x40, DOING-check, clr/set ENABLE, DONE-poll=1, read RES=0) — 9 accesses, no loop. With this the app passes 0x901286b2 and runs far more init (memory pool, LOS task creation at 0x90118xxx, registration tables, 0x90122/0x90126/0x90128xxx) before the next blocker. Next blocker: the app enters a tight 20-PC ITCM loop (resident boot-stage service code at 0x41/0x42/0x45xxx) that polls a 2-bit field of 0x570007a0 (GLB_CTL_A region, via a descriptor-table register accessor) with TCXO delays between reads, interleaved with ULP_AON config writes (0x5702c934 = 0xc5, reads of 0x5702c330..340). Our generic absorber returns 0, so the field never reaches its ready value → infinite wait. This is the deeper clock/power-domain bring-up (a PLL/clock-switch or LDO “ready” status); modeling 0x570007a0’s ready field correctly needs the BS2X GLB_CTL_A register map. (Also fixed bs21-smoke-test.sh to find the examples under the post-regroup examples/bs21 path — it had silently SKIPped, a false PASS. WS63 5/5 qtests + BS21 M1 — uart_hello banner + 13 GPIO toggles, 0 illegal traps — both green.)

  13. Stuck loop traced to eFUSE (v151) + ULP_AON PMU bring-up (2026-06-10). Dug 0x570007a0 to its root. The app is truly stuck (identical 3723 app PCs / max PC 0x901662bc / 9 UART lines at both 8s and 25s), in a 20-PC ITCM loop with no app PCs — an init/handler-table iteration (table of {fn_ptr@0x9019Fxxx, id, 0, fn_ptr@0x90166xxx} at DTCM 0x20002ae4) whose one handler runs eFUSE + ULP_AON PMU register operations that never satisfy their exit condition. Decoded from live DTCM dumps: 0x425b6 = hal_efuse_switch_en_set(0xa5a5) (writes the eFUSE unlock magic 0xa5a5 to g_efuse_switch_en_addr = 0x5702C258); the per-byte loop 0x4269e iterates eFUSE bytes via g_efuse_base = 0x57028030, read-data 0x57028800+, boot-done status g_efuse_boot_done_addr = 0x5702802C (bit2 = efuse_boot2_done, EFUSE_BOOT_DONE_MASK 0x4). The eFUSE IP is v151 (full source in fbb_ws63/.../hal/efuse/v151/; HAL_EFUSE_SWITCH_EN 0xa5a5). Steady-state MMIO is a varied sequence (not one poll): eFUSE 0x57028030/0x5702887c + ULP_AON 0x5702c204/0x5702c520/0x5702c930/0x5702c938/0x5702c974 writes(0xa5a5/ 0x5a5a/configs)+reads. Root cause: neither the eFUSE controller (0x57028000) nor the ULP_AON PMU status block (0x5702c000) is modelled — the absorber returns 0, so the calibration/PMU “ready”/trim-data the handler waits for never appears. This is a distinct, larger phase: model the eFUSE v151 controller (boot-done bit2 set; switch-en accepts 0xa5a5; read-data returns blank/efuse image) + the ULP_AON PMU status registers. The exact ready-bit/expected-value needs the BS2X ULP_AON + eFUSE register maps, which are not in the open SDK source (precompiled hal libs / generated headers) — so it isn’t a quick single-register model like the 32K-clock one; it wants the register spec first (per “C SDK as sole ground truth”, not guessed bits). Reusable probes used: live DTCM dumps via QMP human-monitor-command "xp/Nxw addr"; -d in_asm app-PC-coverage diff across run lengths to prove stuck-vs-slow; -d unimp tail to read the steady-state MMIO footprint.

  14. Real root cause = TCXO v150 count layout (16-bit chunks); eFUSE v151 modelled → app runs all clock/PMU/calibration init to its stage-1 reboot (2026-06-10). §13’s “eFUSE/ULP_AON status” guess was wrong. Key discovery: the app overwrites ITCM (0x40000) with its own copied code, so the earlier flashboot disasm of the loop was of the wrong bytes — dumping the live ITCM (QMP pmemsave) revealed the truth. The 20-PC loop is a uapi_tcxo_delay_us busy-wait: a counter-read (0x42816) latches the TCXO and assembles a 64-bit count as (*0x57000204 & 0xffff) | (*0x57000208 << 16) (low) and (*0x5700020c & 0xffff) | (*0x57000210 << 16) (high). Per the SDK (hal_tcxo_v150_regs_ def.h, ground truth) the BS21 TCXO has four 16-bit count registers (count0[15:0] @+4, count1[31:16] @+8, count2[47:32] @+0C, count3[63:48] @+10; status valid = bit4). Our shared TCXO model used the WS63 2-register layout (count[31:0] @+4, count[63:32] @+8), so the app assembled only count[15:0] (wrapping every ~2 ms) with high = 0 → the delay’s now < target never held → infinite wait. Fix: ws63_tcxo_set_chunked16() — a BS21-only mode that splits the count into the four 16-bit chunks (WS63 path untouched, default off → byte-identical, 5/5 qtests green). Also modelled the eFUSE v151 controller (hal_efuse_v151, ground truth): boot-done sts asserted (0x2c → 0x1c), ctl/avdd/clock_period readback, a blank 128-byte (1024-bit) fuse array (trims read 0 → calibration uses defaults), and OR-on-write programming — faithful supporting infrastructure (it isn’t the gate, but it stops the absorber-returns-0 eFUSE boot-done from costing the bounded check_efuse_boot_done its 50×1 ms each call). With both, the app passes the delay loop and runs all its clock/PMU/eFUSE/calibration init (new code at 0x9012d5xx/0x9012fxxx), then deliberately reboots: reboot(core=2, cause=0x2003) via 0x9012d59c ("APP|Reboot core:%d cause 0x%x", reason stored at DTCM 0x2000ffd8, magic 0xdeadbeaf @0x2000ffe8) — core 2 = APPS_CORE; it clears bit0 of BOOT_PORTING_RESET_ REG 0x57004600 and spins j . waiting for the chip reset. This is the original “core:2” halt, now reached cleanly (not via a crash) after a full init pass — a stage-1 → stage-2 reboot. WS63 5/5 qtests + BS21 M1 (uart_hello banner + 13 GPIO toggles, 0 illegal traps) both green. Key probe: the app overwrites ITCM — always disassemble live memory (QMP pmemsave), not the loaded image, once past the app handoff.

  15. The cause=0x2003 reboot is a PANIC from an event-wait timeout = the connectivity boundary (2026-06-10). Classified the reboot (per §14’s open question): it is not a clean stage-1→2 reboot, it’s a software panic (no CPU trap — mtvec 0x44330 is never entered; mcause/mepc = 0). The panic handler 0x9012f8ec prints "APP|[panic]id:%d, code:0x%x,call:0x%x" then 0x9012f9a0 dumps context and calls reboot(core=2, cause=0x2003). The trigger is a queue-consume + timed-wait loop at 0x9013062e: it polls a software message queue (0x90130368 reads list entries at 0x4bed0/0x4bec4), checks a pending state (0x90122cd4 → 2 = pending), runs a timed wait (0x90122d64), and on timeout calls panic(id=9) (cause 0x2003 is not a watchdog — those are 0x2002/0x4002/0x8002 in reboot_porting.h). Root cause: the queue/event the task waits on is never produced — the producer needs the event/message-driven connectivity stack (interrupts/IPC from the BLE/SLE/radio + protocol/BT core), which is the deferred connectivity work, not modelled. So modelling the chip-reset trigger would NOT advance the boot — it would reboot-loop into the same panic (the missing event is structural, not first-boot state). This is the expected end of the single-core stage-1 bring-up: the app completes all of kernel init + clock/PMU/eFUSE/calibration, then blocks on the connectivity/event layer. Crossing it is the deferred BLE/SLE blob + multi-core/IPC + full-peripheral-event work (project north star), a multi-week effort — not a single register/decoder fix. WS63 5/5 qtests + BS21 M1 green (this entry is analysis only, no code change).

  16. BT trimmed at the SDK + securec ROM addresses fixed → a peripheral SAMPLE runs and PRINTS end-to-end (2026-06-10). Two changes, mirroring the WS63 approach (trim the connectivity stack, run a bare sample):

    • SDK BT trim (in fbb_bs2x, the “同 ws63 做法” fork): drop the build defines BGLE_TASK_EXIST + AT_COMMAND (config.py) so the SLE/BT/GLE platform stack is never inited and the AT task (prio 20, busy-polls its queue) no longer starves the app (prio 21); build a peripheral sample instead (CONFIG_SAMPLE_ENABLE + CONFIG_ENABLE_PERIPHERAL_SAMPLE, the tcxo demo). With BT gone the app no longer reaches §15’s connectivity-event panic — it boots clean into the sample task, which loops on uapi_tcxo_delay_ms/us + the TCXO getters.

    • securec ROM address fix (this repo): the sample’s logs were still silent despite the whole print path running. Root cause (traced via gdbstub): the bs21e target links securec against a different mask-ROM symbol file than bs20/bs22 — rom_info/acore/acore.sym (vsnprintf_s 0x3e962) vs acore_rom_n1200.sym (0x3ef92). bs21_rom_call used the latter, so the app’s real vsnprintf_s was unmapped → the success-stub returned 0 with an empty buffer → vsnprintf_s(...)="" → UartPuts returned early on len==0 → no byte ever reached the UART. Repointed the securec cases to acore.sym + added vsprintf_s. Now the tcxo sample prints its full banner and self-validates:

      Debug uart init succ! flash length:0x100000, version: bs21e 1.0.18
      APP|System Power On
      cpu 0 entering scheduler
      tcxo delay 1000ms!  ...  tcxo get ms work normall.
      tcxo delay 20000us! ...  tcxo get us work normall.   (loops)
      

      WS63 5/5 qtests + BS21 M1 green. Milestone: a BS2X vendor peripheral sample boots AND produces validated UART output on -M bs21. The print path is now unblocked for any future bare (non-connectivity) sample. Connectivity (§15) remains the deferred north-star work.

The infrastructure (CPU + xlinx [now incl. prefd + the muliadd imm fix + the ldmia/stmia bank selector] + memory map + UART/GPIO + SFC + flash1 + the disjoint-range ROM dispatch and bs21_rom_call + the mask-ROM signature + the TCXO fix + the GigaDevice flash ID + the 32K-clock-detect status + the eFUSE v151 controller + the TCXO v150 16-bit count layout) is in place; flashboot loads + jumps to the app, and the LiteOS app runs its full kernel init AND all clock/PMU/eFUSE/calibration bring-up crash-free, then deliberately reboots the apps core — the BS2X vendor boot chain (loaderboot → flashboot → app) runs end-to-end on -M bs21 through the application’s entire single-core stage-1 bring-up (kernel init + clock/PMU/eFUSE/calibration), then blocks on the event/connectivity layer: a task waits on a message queue that is never produced, times out, and panics (panic(id=9) → reboot(core=2, cause=0x2003); see §15). That boundary is the deferred BLE/SLE + multi-core IPC + full-peripheral-event work (the project north star), not a single fix — modelling the chip reset alone would only reboot-loop into the same panic.