引言
这是 HiSilicon WS63(Hi3863,RISC-V RV32IMFC,Wi-Fi 6 + SLE/SparkLink + BLE)的 Rust 嵌入式生态开发手册。
整套生态包含:
hisi-riscv-hal—— 手写的安全外设驱动(GPIO/UART/I2C/SPI/DMA/Timer…),基于embedded-hal 1.0,可选async/embassy。ws63-pac—— svd2rust 生成的寄存器访问层。hisi-riscv-rt—— 启动汇编、链接脚本、中断向量。hisi-riscv工具链 —— 内置riscv32imfc-unknown-none-elf(硬浮点、无原子)目标的定制 stable rustc。hisi-fwpkg—— 把 ELF 打包成可被 flashboot 加载的应用镜像(0x300 头)。- patched
probe-rs—— 支持 WS63 的 J-Link/SWD 烧录与调试。 hisi-riscv-qemu—— 跑得动 vendor C SDK 与 Rust 固件的 QEMU 模型。- HIL 测试框架 —— 在真实芯片上构建→烧录→运行→断言 UART 标记串。
本手册如何组织(Diátaxis)
本手册按 Diátaxis 框架分为四个象限,各自服务不同目的:
| 象限 | 面向 | 什么时候看 |
|---|---|---|
| 教程 | 学习 | 你是新手,想从零跑通第一个程序 |
| 操作指南 | 解决问题 | 你知道要做什么,需要一份可照做的步骤 |
| 参考 | 查信息 | 你需要准确的事实:地址、标记串、API、命令行参数 |
| 原理与背景 | 理解 | 你想搞懂“为什么这样设计“ |
如果你是第一次接触,先到教程导读选择适合你的路径——本手册的教程分两条:
- 应用开发者:用
cargo generate从模板脚手架出你自己的 WS63 应用(依赖来自 crates.io,无需克隆本仓库)。见应用开发者路径。 - 生态贡献者:克隆本 monorepo(含子模块),构建/运行完整示例集、改 HAL/PAC/运行时、跑完整 HIL。见生态贡献者路径。
仓库
- 在线手册:https://hispark-rs.github.io/hisi-riscv-rs/(本书)
- API 文档(rustdoc):https://hispark-rs.github.io/hisi-riscv-rs/api/(hisi-riscv-hal / ws63-pac / hisi-riscv-rt)
- 主仓库:https://github.com/hispark-rs/hisi-riscv-rs
- 工程模板:https://github.com/hispark-rs/hisi-rs-template(
cargo generate) - 其它仓库见 CLI 工具速查 与各组件文档。
教程 · 选择你的路径
欢迎!本章是一组手把手的课程:给确切的命令、给你应当看到的输出,照着做就能成功。
在开始之前,先选一条适合你的路径——两条路径面向不同的人、起点也不同,但风格一样(学习导向、一条happy path)。
你是哪一类?
应用开发者(用 WS63 写你自己的 App)
你想用我们已发布到 crates.io 的库(hisi-riscv-hal / hisi-riscv-rt / ws63-pac)
开发自己的 WS63 程序。你不需要克隆这个monorepo——
起点是用 cargo generate 从模板 hisi-rs-template
生成一个自包含的工程。有没有开发板都行(QEMU 不需要硬件)。
→ 走 应用开发者路径
生态贡献者(开发 HAL / PAC / rt / QEMU / 示例)
你想给 HAL、PAC、运行时、QEMU 模型或示例目录贡献代码。你需要克隆 带子模块的monorepo,构建并运行完整的示例集,并做硬件在环(HIL)测试。
→ 走 生态贡献者路径
教程只求带你跑通,不展开讲原理。想知道“为什么“看 原理与背景; 想查命令和参数看 参考;想完成某个具体任务看 操作指南。
应用开发者路径 · 导读
本路径面向用 WS63 写自己 App 的开发者。你会用已发布到 crates.io 的库
(hisi-riscv-hal / hisi-riscv-rt / ws63-pac),从模板
hisi-rs-template 生成一个
自包含的工程,在 QEMU 里跑起来,再烧到真板。
你不需要克隆这个monorepo——所有依赖都来自 crates.io,生成的工程自带 justfile。
适合谁
- 想基于现成的库快速做一个 WS63 应用,而不是改 HAL/PAC 本身。
- 想要一条“生成工程 →
just run→just flash“的最短上手路径。
你需要准备什么
- 一台 Linux 电脑(本路径在 x86_64 Linux 上验证)。
- 已安装
rustup、git、curl。 - 第 1 课会带你装好其余工具(自定义工具链、
cargo-generate、just、烧录器、可选 QEMU)。 - 开发板可选:QEMU 不需要硬件;只有要烧真机(第 2 课后半段)时才需要一块 WS63 开发板。
三节课
- 搭建环境(应用开发) —— 装好
hisi-riscv工具链、cargo-generate、just、hisi-fwpkg、烧录用的 probe-rs 分支,以及可选的 QEMU。 - 从模板创建你的第一个工程 ——
cargo generate生成 blinky 工程,just run在 QEMU 里跑,再just flash烧到真板看 LED 闪。 - 改造成一个 UART 程序 —— 用
uart_hello起手,在 QEMU 里看到Hello from WS63 ...。
学完这三课,你就有了一个自己的、能跑能烧的 WS63 工程。开始吧 —— 搭建环境(应用开发)。
搭建环境(应用开发)
本课带你装好开发 WS63 应用所需的全部工具。注意:你不需要克隆monorepo——
所有库依赖都来自 crates.io,工程将在下一课用 cargo generate 生成。
本课只求“把工具装上“。每个工具的深入说明与故障排查见 安装 hisi-riscv 工具链。
第 1 步:安装 hisi-riscv 工具链
WS63 应用核是 riscv32imfc-unknown-none-elf(硬件单精度浮点、无原子扩展)。
这个目标被内建进了一个自定义的 hisi-riscv 工具链——它不是 rustup 频道,需要手动下载并链接。
下载与你主机匹配的压缩包(这里以 x86_64 Linux 为例),解压,然后链接:
HOST=x86_64-unknown-linux-gnu
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
tar xzf hisi-riscv-rust-1.96.0-$HOST.tar.gz
rustup toolchain link hisi-riscv "$PWD/stage2"
其它主机把
HOST换成对应三元组即可:aarch64-unknown-linux-gnu、aarch64-apple-darwin、x86_64-pc-windows-msvc。
确认链接成功:
rustup toolchain list | grep hisi-riscv
你应当看到:
hisi-riscv
第 2 步:安装 cargo-generate 与 just
下一课用 cargo generate 从模板生成工程;生成出来的工程用 just 跑各种命令:
cargo install cargo-generate just
第 3 步:安装打包工具 hisi-fwpkg(烧真机用)
烧到真板时,flashboot 期望一个带 0x300 启动头的应用镜像,hisi-fwpkg 负责打包:
cargo install --git https://github.com/hispark-rs/hisi-fwpkg
也可以克隆 github.com/hispark-rs/hisi-fwpkg 自行构建。确认就位:
hisi-fwpkg --help。
第 4 步:安装打过补丁的 probe-rs 分支(烧真机用)
上游 probe-rs 不认识 WS63,必须用打过补丁的分支,并配上它自带的
HiSilicon_WS63.yaml 芯片描述文件:
cargo install --git https://github.com/hispark-rs/probe-rs \
--branch add-hisilicon-ws63-bs21 probe-rs-tools
确认就位:probe-rs --version。深入说明见
用 probe-rs 烧录到真机。
只想在 QEMU 里跑、暂时不烧真机,可以先跳过第 3、4 步。
第 5 步:安装 QEMU(可选,just run 用)
想用 just run 在模拟器里跑,需要 hisi-riscv-qemu——
一个带 -M ws63 机器模型的 QEMU 分支。克隆并构建,把它的 qemu-system-riscv32 放进 PATH:
git clone https://github.com/hispark-rs/hisi-riscv-qemu && cd hisi-riscv-qemu
./scripts/build.sh
确认 ws63 机器可用:
qemu-system-riscv32 -M help | grep ws63
第 6 步:验证工具链
hisi-riscv 工具链内建了 WS63 目标,确认它在目标列表里:
rustc +hisi-riscv --print target-list | grep riscv32imfc
你应当看到:
riscv32imfc-unknown-none-elf
看到这一行就说明工具链装好了。下一课生成的工程里有
rust-toolchain.toml, 会自动选用hisi-riscv,所以在工程目录里直接敲cargo即可,无需+hisi-riscv。
工具齐了!下一课我们生成你的第一个工程 —— 从模板创建你的第一个工程。
从模板创建你的第一个工程
这是应用开发者路径最关键的一课。我们用 cargo generate 从模板生成一个
你自己的 blinky 工程,先在 QEMU 里跑通,再烧到真正的 WS63 开发板,
亲眼看到板载 LED 闪烁。
这条 blinky 路径已在真实芯片上验证通过(2026-06-14,GPIO0 翻转正常)。照做即可成功。
第 1 步:从模板生成工程
用 cargo generate 拉取模板并回答几个提示:
cargo generate --git https://github.com/hispark-rs/hisi-rs-template
按提示回答(默认值就是我们要的):
- 项目名(
Project Name):随便起一个,比如my-blinky。 - Target chip:选
ws63(默认)。 - Starter app:选
blinky(默认)。 - App partition flash address:保持默认
0x00230000(WS63 的应用分区地址,已验证)。
生成完成后进入工程目录:
cd my-blinky
这个工程是自包含的:它的依赖(hisi-riscv-hal / hisi-riscv-rt / ws63-pac)
都来自 crates.io,它自带 rust-toolchain.toml(钉死 hisi-riscv 工具链)、
.cargo/config.toml(设好目标和 QEMU runner)和一个 justfile。
你不需要克隆任何monorepo。
第 2 步:在 QEMU 里跑
先用 QEMU 确认程序能正常启动:
just run
这会 cargo build --release 再用 -M ws63 启动 QEMU。blinky 只翻转 GPIO0、
没有串口输出,所以控制台不会打印东西——这是预期的:程序正在安静地循环翻转引脚
(机器 trace 里能看到 GPIO0 每 500 ms 变一次)。
按 Ctrl-A 然后按 X 退出 QEMU。
第 3 步:烧到真正的开发板
插上 WS63 开发板,一条命令完成“编译 → 打包 → 下载 → 复位“:
just flash
just flash 依次做了这些事:
cargo build --release编出 ELF;0x300HiSilicon 启动头由hisi-riscv-rt的boot-header特性在链接时直接烘进 ELF,无需单独的打包步骤;hisi-fwpkg patch-hash <elf>在链接后就地补齐 ELF body 的 SHA-256(链接时占位是零);- 用打补丁的 probe-rs 分支把这枚裸 ELF 直接
download到应用分区0x00230000(不经过.img中间文件); probe-rs reset复位,flashboot 跳进应用入口(app + 0x300)。
不需要真正签名,但需要真实 hash:开发芯片的安全验证是关闭的(efuse
SEC_VERIFY_ENABLE == 0),但 flashboot 在硬件上仍会校验 body 的 SHA-256—— secure-off 只是跳过 ECC 签名,不跳过 hash。所以镜像需要的是0x300头 + 真实 body SHA-256(这正是patch-hash在做的事),而不存在一个能让它启动的“占位签名“。 镜像格式细节见 应用镜像格式与签名。BS2X 仍走“route 1“:链接时没有
boot-header,用编译后的hisi-fwpkg image -o app.img <elf>打包再烧.img;WS63 用的是上面的patch-hash+ 裸 ELF 路径。如果
HiSilicon_WS63.yaml不在当前目录,指给它:just CHIP_DESC=/path/to/HiSilicon_WS63.yaml flash。 probe-rs 分支与 YAML 的细节见 用 probe-rs 烧录到真机。
想在烧录的同时顺便看 UART0 的启动日志,可以用:
just run-hw PORT=/dev/ttyUSB0
它会先 flash,再把 UART0(CH340 串口,/dev/ttyUSB0 @ 115200 8N1)接到终端。
第 4 步:看 LED 闪烁
复位之后,看你的开发板:板载 LED(GPIO0)开始闪烁,亮 0.5 秒、灭 0.5 秒, 不断循环。
成功了!你刚刚用一个完全属于你自己的 Rust 工程,让一块真正的 WS63 芯片 亮起了第一个 LED。
下一课我们把它改造成一个会“说话“的 UART 程序 —— 改造成一个 UART 程序。
改造成一个 UART 程序
上一课你的工程只会闪灯,这一课我们让它开口打印。最简单的办法是用
uart_hello 起手重新生成一个工程,在 QEMU 里看到它打印 Hello from WS63 ...。
QEMU 是本课可靠的成功路径。
uart_hello就是为 QEMU 设计的:它故意不初始化时钟, 只碰 UART0 寄存器。
第 1 步:用 uart_hello 起手生成工程
再跑一次 cargo generate,这次 Starter app 选 uart_hello:
cargo generate --git https://github.com/hispark-rs/hisi-rs-template
- 项目名:比如
my-uart。 - Target chip:
ws63(默认)。 - Starter app:选
uart_hello。 - App partition flash address:默认
0x00230000。
进入工程目录:
cd my-uart
第 2 步:在 QEMU 里运行
just run
-nographic 会把 UART0 接到你的终端。
第 3 步:看到它说话
控制台上你应当立刻看到 banner,随后是不断递增的 tick 计数:
Hello from WS63 on QEMU!
UART0 @ 0x44010000 is alive.
tick 0
tick 1
tick 2
...
计数器会一直涨下去。看到这些输出,说明你的 Rust 程序成功通过 UART0 打印了文本。
按 Ctrl-A 然后按 X 退出 QEMU。
成功了!你刚刚让一个属于你的工程打印出了第一行串口日志。
关于真机
在真正的硬件上,串口 banner 的点亮工作仍在进行中——真机需要先初始化时钟,
让波特率分频与 PLL 匹配,而 uart_hello 为了适配 QEMU 故意省去了这一步。
所以本课不承诺真机上能看到这条 banner;要在真板上稳定看到串口输出,
请关注 HIL 测试框架 的进展。
想在真机上看到稳定可观测的行为,最稳妥的仍是上一课的 blinky(GPIO 翻转,已验证)。
接下来想做点什么?
生态贡献者路径 · 导读
本路径面向给生态本身贡献代码的人:改 HAL、PAC、运行时(rt)、QEMU 模型, 或者维护示例目录。你会克隆带子模块的monorepo,构建并运行完整的示例集, 最后做硬件在环(HIL)测试。
适合谁
- 想给
hisi-riscv-hal/ws63-pac/hisi-riscv-rt/hisi-riscv-qemu提交改动。 - 想新增或调试示例(
examples/ws63/*),并用 QEMU + HIL 验证它们。
你需要准备什么
- 一台 Linux 电脑(本路径在 x86_64 Linux 上验证)。
- 已安装
rustup、git、curl。 - 第 1 课会带你克隆monorepo(带子模块)、装好工具链、QEMU 和烧录器。
- 第 3 课(HIL)需要一块 WS63 开发板;第 1、2 课只用 QEMU,无需硬件。
三节课
- 搭建环境(贡献生态) —— 克隆monorepo、装工具链/QEMU/probe-rs,并以一次成功的
cargo build -p blinky收尾。 - 构建与运行示例集 —— 在 QEMU 里跑完整示例目录:GPIO、UART、中断、半主机退出码。
- 第一次硬件在环测试 —— 把 blinky 烧到真板,观察 GPIO 翻转,认识
hil/hil-smoke.sh。
学完这三课,你就能在这个仓库里构建、运行并验证示例与改动了。开始吧 —— 搭建环境(贡献生态)。
搭建环境(贡献生态)
本课带你装好全部工具、克隆带子模块的monorepo,并以一次成功的编译收尾。 请逐步执行,每一步都有可见的结果。
本课只求“把工具跑起来“。每个工具的深入安装与故障排查见 安装 hisi-riscv 工具链。
第 1 步:安装 hisi-riscv 工具链
WS63 应用核是 riscv32imfc-unknown-none-elf(硬件单精度浮点、无原子扩展)。
这个目标被内建进了一个自定义的 hisi-riscv 工具链——它不是 rustup 频道,需要手动下载并链接。
下载与你主机匹配的压缩包(这里以 x86_64 Linux 为例),解压,然后链接:
curl -LO https://github.com/hispark-rs/hisi-riscv-rust-toolchain/releases/download/v1.96.0-2/hisi-riscv-rust-1.96.0-x86_64-unknown-linux-gnu.tar.gz
tar xzf hisi-riscv-rust-1.96.0-*.tar.gz
rustup toolchain link hisi-riscv "$PWD/stage2"
确认链接成功:
rustup toolchain list | grep hisi-riscv
你应当看到:
hisi-riscv
第 2 步:克隆仓库(带子模块)
示例、HAL、PAC、运行时都是子模块,务必带 --recurse-submodules 克隆:
git clone --recurse-submodules https://github.com/hispark-rs/hisi-riscv-rs.git
cd hisi-riscv-rs
如果你已经克隆但忘了子模块,补一句:
git submodule update --init --recursive。
仓库根目录的 rust-toolchain.toml 已经把频道钉成了 hisi-riscv,
所以在本仓库内执行的所有 cargo 命令都会自动用上刚装好的工具链。
第 3 步:安装 QEMU 模拟器
第 2、3 课要用 hisi-riscv-qemu——
一个带 WS63 机器模型(-M ws63)的 QEMU 分支。在仓库同级目录里克隆并构建它:
cd ..
git clone https://github.com/hispark-rs/hisi-riscv-qemu.git
cd hisi-riscv-qemu
bash scripts/build.sh
构建完成后,确认 qemu-system-riscv32 可用并支持 ws63 机器:
./build/qemu-system-riscv32 -M help | grep ws63
你应当看到 ws63 出现在机器列表中。把这个二进制加入 PATH,
或记下它的路径——第 2 课会用到。详细步骤见
QEMU 模型。
第 4 步:安装烧录工具(真机用)
第 3 课要烧到真板,需要两个工具:
hisi-fwpkg:把 ELF 打包成可启动镜像(加0x300启动头)。- 打过补丁的 probe-rs 分支(
hispark-rs/probe-rs,分支add-hisilicon-ws63-bs21): 上游 probe-rs 不认识 WS63,必须用这个分支,并配上HiSilicon_WS63.yaml。
安装方法(深入说明见 安装工具链 与 用 probe-rs 烧录到真机):
# hisi-fwpkg
cargo install --git https://github.com/hispark-rs/hisi-fwpkg
# 打过补丁的 probe-rs 分支
cargo install --git https://github.com/hispark-rs/probe-rs --branch add-hisilicon-ws63-bs21 probe-rs-tools
确认两者就位:
hisi-fwpkg --help
probe-rs --version
第 2 课只用 QEMU,可以暂时跳过本步;等到第 3 课要烧真机时再装也行。
第 5 步:验证你的环境
回到仓库根目录,编译 blinky 示例——这是检验工具链是否就绪的最快办法:
cd ../hisi-riscv-rs
cargo build -p blinky --release
第一次编译会拉取依赖、编译 HAL,需要几分钟。结束时你应当看到类似:
Finished `release` profile [optimized + debuginfo] target(s) in ...
产物在这里:
ls target/riscv32imfc-unknown-none-elf/release/blinky
看到这个文件,就说明工具链、目标、仓库都配好了。
编译过程中会有一些
.weak StorePageFault之类的汇编 warning,这是正常的,可以忽略。
环境就绪!下一课我们构建并运行完整的示例集 —— 构建与运行示例集。
构建与运行示例集
QEMU 是不用硬件就能跑 WS63 固件的“软件在环“环境。作为生态贡献者,你会反复
构建并运行 examples/ws63/* 里的示例来验证改动。本课带你跑通示例目录里的几个
代表:blinky(GPIO trace)、uart_hello(banner)、timer_irq / gpio_irq(中断串口输出)、
semihost_selftest(半主机退出码)。每一步都有可见结果。
QEMU 模型的原理见 QEMU 模型。
示例都是根工作区的成员,所以一律从仓库根目录用 -p <name> 构建,再用同一条
命令模板运行:
cargo build -p <name> --release
qemu-system-riscv32 -M ws63 -nographic -bios none \
-kernel target/riscv32imfc-unknown-none-elf/release/<name>
-M ws63:WS63 机器模型。-nographic:无图形界面,把 UART0 接到当前终端。-bios none:不加载额外固件,直接跑我们的-kernel。-kernel <elf>:要运行的示例 ELF。
退出 QEMU 始终是:按 Ctrl-A,再按 X。
第 1 步:blinky(GPIO 翻转,无串口)
cargo build -p blinky --release
qemu-system-riscv32 -M ws63 -nographic -bios none \
-kernel target/riscv32imfc-unknown-none-elf/release/blinky
blinky 把 GPIO0 配成推挽输出,死循环里拉高、延时、拉低、延时。它没有串口输出, 所以控制台不会打印东西——这是预期的:程序在安静地翻转引脚(机器 trace 里能看到 GPIO0 每 500 ms 变一次)。想“看见“可观测行为,用下面带串口的示例。
关于构建目标和产物路径的细节,见 构建一个示例。
第 2 步:uart_hello(串口 banner)
cargo build -p uart_hello --release
qemu-system-riscv32 -M ws63 -nographic -bios none \
-kernel target/riscv32imfc-unknown-none-elf/release/uart_hello
uart_hello 为 QEMU 设计:它故意不初始化时钟,只碰 UART0 寄存器(0x4401_0000)。
控制台上你应当立刻看到 banner,随后是不断递增的 tick 计数:
Hello from WS63 on QEMU!
ws63-qemu: UART0 @ 0x44010000 is alive.
tick 0
tick 1
tick 2
...
真机上的串口 banner 仍在打磨(需要先初始化时钟),本课只承诺 QEMU。第 3 课的 blinky 才是当前确认可观测的真机行为。
第 3 步:timer_irq(定时器中断)
cargo build -p timer_irq --release
qemu-system-riscv32 -M ws63 -nographic -bios none \
-kernel target/riscv32imfc-unknown-none-elf/release/timer_irq
TIMER_0 周期性触发 IRQ 26,处理函数每次累加计数并打印。你应当看到:
WS63 timer-IRQ test (TIMER_0 -> IRQ 26)
timer irq #0
timer irq #1
timer irq #2
...
OK: timer interrupts delivered
看到 timer irq # 不断递增、最后出现 OK: timer interrupts delivered,
说明 QEMU 的中断投递闭环正常。
第 4 步:gpio_irq(GPIO 中断)
cargo build -p gpio_irq --release
qemu-system-riscv32 -M ws63 -nographic -bios none \
-kernel target/riscv32imfc-unknown-none-elf/release/gpio_irq
这个示例把 GPIO0 的边沿映射到一个自定义本地 IRQ(≥32)。你应当看到:
WS63 GPIO-IRQ test (GPIO0 pin0 -> IRQ 33, custom local)
gpio irq #0
gpio irq #1
...
OK: custom local IRQ (>=32) delivered
第 5 步:semihost_selftest(半主机退出码)
有些示例不靠串口打印,而是通过 RISC-V 半主机把结果报告给宿主——
semihost_selftest 跑完后会用半主机的“退出“操作返回退出码:PASS 返回 0,FAIL 返回 1,
panic 返回 2。这个退出码会变成 QEMU 进程自己的退出码,非常适合写进自动化脚本。
要让半主机生效,必须加上 -semihosting:
cargo build -p semihost_selftest --release
qemu-system-riscv32 -M ws63 -nographic -bios none -semihosting \
-kernel target/riscv32imfc-unknown-none-elf/release/semihost_selftest
控制台会打印(通过半主机控制台):
semihost_selftest: PASS
随后 QEMU 自行退出。检查它的退出码:
echo $?
你应当看到:
0
0 就代表自检通过——脚本可以直接据此判定成败,无需解析串口文本。
各示例的预期标记串汇总见 示例目录与验证标记串; 半主机相关的环境变量见 HIL 标记串与环境变量。
你现在已经能从仓库根目录构建并在 QEMU 里跑示例、读中断输出、用退出码做自检了。 下一课我们走出模拟器,做第一次真机测试 —— 第一次硬件在环测试。
第一次硬件在环测试
“硬件在环”(HIL,Hardware-In-the-Loop)就是:把程序烧进真芯片、让它真的跑起来,
再从外部观察它的行为,确认真实硬件上一切正常。本课我们用 blinky——
那个已经在真实芯片上验证通过的示例——完成你的第一次 HIL 测试,保证成功。
我们特意选 blinky:它的 GPIO0 翻转是当前确认可观测的真机行为 (上一课提过,真机串口 banner 还在打磨中)。
你需要准备
- 一块 WS63 开发板,已连上电脑。
- 第 1 课装好的
hisi-fwpkg和打过补丁的 probe-rs 分支。 - 一根 USB 串口线连到板子的 UART0,在系统里通常是
/dev/ttyUSB0(CH340 适配器),波特率115200 8N1。
注意:
/dev/ttyACM0是 J-Link 的 VCOM,不是应用 UART,别接错了。
第 1 步:把 blinky 烧进真板
从仓库根目录走“编译 → 补哈希 → 下载 → 复位“四步:
cargo build -p blinky --release
# WS63 用 hisi-riscv-rt 的 boot-header 特性:0x300 启动头在链接期就已烘进 ELF,
# 链接后只需补上 body 的 SHA-256(在原文件就地填写),无中间 .img、无 image 步骤。
hisi-fwpkg patch-hash \
target/riscv32imfc-unknown-none-elf/release/blinky
probe-rs download --chip WS63 \
--chip-description-path HiSilicon_WS63.yaml \
target/riscv32imfc-unknown-none-elf/release/blinky
probe-rs reset
hisi-fwpkg patch-hash把 ELF 里0x300启动头之后的 body SHA-256 就地补全; 头已在链接期烘入,flashboot 复位后跳进app + 0x300。安全启动关闭时,flashboot 仍校验 body hash,只跳过 ECC 签名(efuseSEC_VERIFY_ENABLE==0)——所以镜像需要 一份真实 body SHA-256,没有“占位签名“能让它启动。HiSilicon_WS63.yaml来自打补丁的 probe-rs 分支仓库——上游 probe-rs 没有 WS63 支持。 (BS2X 还没有链接期 boot-header,仍走hisi-fwpkg image -o app.img的 route 1 路线。) 细节见 用 probe-rs 烧录到真机 与 应用镜像格式与签名。
第 2 步:观察 GPIO 翻转
复位后看开发板:板载 LED(GPIO0)开始闪烁,亮 0.5 秒、灭 0.5 秒。
这就是一次成功的硬件在环观测——程序确实在真芯片上运行,并按预期驱动了真实引脚。 如果手边有逻辑分析仪或万用表,也可以直接量 GPIO0 引脚看到方波。
第 3 步:在串口上看启动日志
打开一个串口监视器,盯住 UART0,就能在烧录/复位时看到 flashboot 的启动日志, 确认芯片确实重启并跳进了你的应用:
stty -F /dev/ttyUSB0 115200 raw -echo
cat /dev/ttyUSB0
再做一次 probe-rs reset,监视器里就会滚出 flashboot 的启动信息。
看完按 Ctrl-C 退出 cat。
blinky 自身不打印串口(它只翻转 GPIO),所以这里看到的是 flashboot 的启动日志, 不是应用输出。UART0 接线与端口的细节见 HIL 标记串与环境变量。
第 4 步:认识 HIL 冒烟测试
手动烧一个示例、肉眼看一个结果,是理解 HIL 的好起点。但当示例越来越多时,
我们希望自动把每个示例烧上去、读 UART、断言它打印了预期的标记串。
仓库里的 hil/hil-smoke.sh 就是干这个的——它是 QEMU 冒烟测试在真硅片上的对应物。
它大致这样工作(概念示意,本课不要求你真的运行):
PORT=/dev/ttyUSB0 hil/hil-smoke.sh
脚本会逐个示例:用 hil/flash.sh 烧录 → 读几秒 UART → 用 grep 检查预期标记串,
比如 uart_hello 应出现 Hello from WS63、timer_irq 应出现 timer irq #。
而 blinky 因为没有串口输出,脚本会特别提示“请用 LED / 逻辑分析仪人工确认“——
正是你在第 2、3 步亲手做的事。
HIL 框架的整体设计、标记串约定、它和 QEMU 冒烟测试的关系,见 HIL 测试框架 与 运行 HIL 冒烟测试。
恭喜!你已经完成了贡献者路径的全部三课:克隆仓库、装好工具链,在 QEMU 里跑通了 示例集,最后在真正的 WS63 芯片上完成了第一次硬件在环测试。
接下来想做点什么?
操作指南 · How-to Guides
这一章是任务导向的菜谱:每篇回答一个「如何做某件事」的具体问题,假设你已经掌握了基础(不懂概念请看原理与背景,要查字段/地址/标记串请看参考)。每篇都给出可照做的步骤,并尽量覆盖真实环境里的变体、坑和排错。
构建 · Build
- 如何安装 hisi-riscv 工具链 —— 下载/链接(或源码构建)自定义硬浮点 rustc 工具链,并验证
riscv32imfctarget。 - 如何构建一个示例 —— 从仓库根工作区
cargo build -p <name> --release,ELF 落点,release/debug 与 objcopy 到 bin。
打包与烧录 · Flash
- 如何打包成可启动镜像(hisi-fwpkg) ——
image(裸 0x300 镜像)vspack(fwpkg),0x300 header 是干嘛的。 - 如何用 probe-rs 烧录到真机 —— 验证主路径:
image→probe-rs download→probe-rs reset,补丁版 fork + yaml + 各芯片基址 + 排错。 - 如何用 hisiflash 烧录到真机 —— 厂商 YMODEM 路径:
pack→.fwpkg→hisiflash flash,何时用它。 - 如何用硬件 runner 让
cargo run烧真机 —— 用hil/cargo-run-hw.sh把cargo run从 QEMU 改成烧真机;全部环境变量。
测试 · Test
- 如何运行 HIL 冒烟测试 ——
hil/hil-smoke.sh逐示例的 UART 标记断言、环境变量、读懂通过/失败。 - 如何用 probe-rs 调试与读内存 —— 用补丁版 probe-rs
read、reset_and_halt、读 CSR/内存、用 HW 断点抓应用入口、dump ROM。
开发 · Develop
- 如何从模板新建一个工程 ——
cargo generate从 hisi-rs-template 起步,用生成的 justfile 完成首次构建+烧录。 - 如何新增一个外设驱动 —— HAL 驱动模块范式、外设单例宏、sealed trait,以及配一个带 PASS 标记的 HIL 示例。
如何安装 hisi-riscv 工具链
WS63 应用核是 RV32IMFC_Zicsr:硬件单精度浮点(ilp32f)、没有原子(‘a’)扩展。仓库用一套自定义的 hisi-riscv 工具链来构建——一套 stable rustc(1.96.0),把 target riscv32imfc-unknown-none-elf 作为 builtin 烤进去,并随附预编译的 core/alloc,因此不需要 -Z build-std。它不是可分发的 rustup channel,必须先手动安装并 link。
工具链的来历、ABI 取舍见硬浮点工具链;target 命名细节见工具链与编译目标。
方式一:下载预编译 tarball(推荐)
发布页 https://github.com/hispark-rs/hisi-riscv-rust-toolchain/releases 为每个 host 提供 tarball(Linux x86_64/aarch64、macOS x86_64/aarch64、Windows x86_64)。挑你 host 对应的那个:
# 以 Linux x86_64 为例(换成你 host 对应的文件名)
curl -fLO https://github.com/hispark-rs/hisi-riscv-rust-toolchain/releases/download/v1.96.0-2/hisi-riscv-rust-1.96.0-x86_64-unknown-linux-gnu.tar.gz
tar xzf hisi-riscv-rust-1.96.0-*.tar.gz
# 把解压出来的 stage2/ 作为名为 hisi-riscv 的 rustup 工具链链接进去
rustup toolchain link hisi-riscv "$PWD/stage2"
rustup toolchain link 只是把 hisi-riscv 这个名字指向 stage2/ 目录,不会拷贝——所以别在 link 之后删/移动这个目录(要换位置就重新 link)。
方式二:从源码构建
源码与构建配方都在工具链仓库 https://github.com/hispark-rs/hisi-riscv-rust-toolchain(它本质是带 WS63 target spec 的 rustc 分支 + 一份 config.toml)。构建很重(需要完整的 rustc bootstrap,几十分钟到数小时、十几 GB 磁盘),照仓库 README 跑 x.py build 即可。构建产物同样是一个 stage2/,照方式一末尾 rustup toolchain link 进去。
大多数人不需要从源码构建——只有你要改 target spec / 编译器本身时才需要。
验证
确认 target 已 builtin(这是关键——没有它说明链接的工具链不对):
rustc +hisi-riscv --print target-list | grep riscv32imfc
# 期望输出: riscv32imfc-unknown-none-elf
确认 core 预编译可用(直接试构建,下一篇如何构建一个示例):
rustc +hisi-riscv --version # 应打印 1.96.0 系列版本
rust-toolchain.toml 会自动选它
仓库根的 rust-toolchain.toml 写着:
[toolchain]
channel = "hisi-riscv"
只要你在仓库目录里跑 cargo,rustup 就会自动用 hisi-riscv 工具链,不用每条命令都加 +hisi-riscv。换句话说:链接好之后,在仓库里普通 cargo build 就走对了。 默认 target 由 .cargo/config.toml 的 target = "riscv32imfc-unknown-none-elf" 指定。
排错
error: toolchain 'hisi-riscv' is not installed:还没rustup toolchain link,或stage2/被移动/删除了——重新 link。error: target 'riscv32imfc-unknown-none-elf' not found/ 触发 build-std:你用的是普通 stable 而不是hisi-riscv。检查rustc +hisi-riscv --print target-list | grep riscv32imfc是否有输出;在仓库外构建时记得cargo +hisi-riscv ...或带上rust-toolchain.toml。- 下错 host tarball(如在 aarch64 上用了 x86_64 包):
rustc跑不起来。按uname -m重新挑文件名。
如何构建一个示例
仓库带了一批 WS63 示例(blinky、uart_hello、timer_irq、spi_loopback、i2c_scan、async_delay、embassy_multitask…,完整清单见示例目录与验证标记串)。本篇讲怎么把它们编出来。
前提:已安装 hisi-riscv 工具链。在仓库目录里
rust-toolchain.toml会自动选它,默认 target 是riscv32imfc-unknown-none-elf,无需+hisi-riscv或--target。
从仓库根工作区构建(推荐)
在仓库根目录用 -p <包名> 构建任意示例:
cargo build -p blinky --release
cargo build -p uart_hello --release
cargo build -p spi_loopback --release
包名就是 crate 名(见根 Cargo.toml 的 members),和它在磁盘上的 examples/ws63/<name> 路径无关。一次构建全部库 + 默认示例:
cargo build --release # 构建 default-members(库 + 全部 ws63 示例)
坑:别在
examples/ws63/里构建全部示例。examples/ws63/自带一个嵌套工作区,但它的members只列了blinky一个。所以在那个目录里cargo build -p timer_irq会失败(不是它的成员)。从仓库根构建,根工作区才把全部示例列全。
ELF 落点
根工作区共用根 target/,release ELF 在:
target/riscv32imfc-unknown-none-elf/release/<name>
例如 target/riscv32imfc-unknown-none-elf/release/blinky。注意是无扩展名的 ELF(cargo 按 [[bin]]/crate 名命名产物)。
如果你是在
examples/ws63/嵌套工作区里单独构建(只有blinky),它的产物在examples/ws63/target/riscv32imfc-unknown-none-elf/release/——hil/里的脚本默认就找这个目录。两个target/不要混。
--release vs debug
--release:默认就用它。优化后体积小(blinky 约 48 KB),是真机/HIL 烧录用的产物。- debug(去掉
--release):体积大很多、有完整调试信息,适合 GDB 调试,但可能因为体积/布局在受限的 app 分区里不合适。烧真机一律用--release。
objcopy 成裸 bin
打包成可启动镜像时 hisi-fwpkg 直接吃 ELF(见如何打包镜像),通常不需要手动 objcopy。但若某条工具链确实要裸 bin,用工具链自带的 rust-objcopy:
OBJCOPY="$(rustc +hisi-riscv --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/rust-objcopy"
"$OBJCOPY" -O binary \
target/riscv32imfc-unknown-none-elf/release/blinky \
blinky.bin
(host 三元组那段按你的 host 改;hil/flash.sh 的 hisiflash 路径就是这么从 ELF 生成 bin 的。)
下一步
- 打包成可启动镜像 → 如何打包成可启动镜像
- 烧到真机 → 用 probe-rs / 用 hisiflash
- 在 QEMU 里跑 → 见教程 在 QEMU 里运行与调试
如何用 probe-rs 烧录到真机
这是 2026-06-14 在真实 WS63 硅片上跑通的验证主路径(blinky 上电启动 + 翻转 GPIO0)。WS63 用 hisi-riscv-rt 的 boot-header feature 在链接期就把 0x300 HiSilicon 头烤进 ELF,所以裸 ELF 本身就可启动——无需 hisi-fwpkg image 那一步、也没有中间 .img 文件。流程三步:链接后用 hisi-fwpkg patch-hash 就地把 body SHA-256 填进头里(secure-off 仍会校验 hash,只跳过 ECC 签名),用 probe-rs download 把 ELF 直接写进 XIP flash 的 app 分区,再 probe-rs reset 复位运行。
用串口/YMODEM 而不是 SWD/JTAG 探针的话,走厂商 hisiflash 路径。
前提:补丁版 probe-rs fork(必须)
上游 probe-rs 还没有 WS63 target,也没有 ws63-sfc flash 算法,用 mainline 烧不了。必须装补丁版 fork:
cargo install --git https://github.com/hispark-rs/probe-rs \
--branch add-hisilicon-ws63-bs21 probe-rs-tools
同时需要该 fork 随附的芯片描述 HiSilicon_WS63.yaml(在 fork 仓库 probe-rs/targets/HiSilicon_WS63.yaml)。烧录时用 --chip-description-path 指向它。该端口的来历见probe-rs 端口说明。
三步走(手动,WS63)
# 1. 链接后填充 body SHA-256(boot-header 已把 0x300 头烤进 ELF,
# 这一步把头里的 body hash 就地补齐;secure-off 仍校验 hash,只跳过 ECC 签名)
hisi-fwpkg patch-hash \
target/riscv32imfc-unknown-none-elf/release/blinky
# 2. 直接把 ELF 下载进 app 分区(基址来自 ELF 里 boot-header 的链接地址,
# 无需 --base-address;fork 的 ws63-sfc 算法会按 ELF 段地址落位)
probe-rs download --chip WS63 \
--chip-description-path HiSilicon_WS63.yaml \
target/riscv32imfc-unknown-none-elf/release/blinky
# 3. 复位运行
probe-rs reset --chip WS63 --chip-description-path HiSilicon_WS63.yaml
关键:WS63 烧的是带 boot-header 的 ELF 本身,不是裸二进制,所以不需要 --binary-format bin + --base-address——probe-rs 按 ELF 段地址落位即可。patch-hash 是必须的后链接步骤,少了它 flashboot 校验 body hash 不过、不会进你的程序。
BS2X(bs21/bs20…)走 route 1: 还没有链接期 boot-header,要先
hisi-fwpkg image -o app.img <elf>打出 0x300 头镜像(裸二进制),再用--binary-format bin --base-address <app 基址>把.img落到 app 分区。下面「各芯片基址」表给的就是 BS2X 这条路要用的基址。
各芯片基址(route 1 / BS2X 用)
WS63 走 route 2 烧 ELF,地址由 boot-header 链接进去,不需要手填基址。下表是 route 1(hisi-fwpkg image + --base-address)落 .img 时用的 app 分区基址——目前主要给 BS2X,也可作为 WS63 boot-header 链接地址的参考:
| 芯片 | app 分区基址 |
|---|---|
| WS63(boot-header 链接地址 / route 1 参考) | 0x00230000 |
| BS2X(bs21/bs20…,route 1) | 0x00090000 |
BS2X 基址来自
hisi-fwpkg的Chip::Bs21默认值,尚未 HIL 验证——烧 BS2X 前对照你的 fbb_bs2x 分区表确认。自定义分区表时用--base-address覆盖。
用脚本一把梭
hil/flash.sh 默认 METHOD=probe-rs,封装了 download + reset 一条龙:
PROBE_RS_YAML=/path/HiSilicon_WS63.yaml hil/flash.sh blinky
注意:当前
hil/flash.sh仍走 route 1 老流程(内部调hil/pack.sh跑hisi-fwpkg image出.img,再--binary-format bin --base-address落位)——对 BS2X 正确,对 WS63 也仍能跑通(.img与 boot-header ELF 的 body 一致)。WS63 的精简 route 2 推荐路径见上面「三步走」或模板justfile的patch/flashrecipe(hisi-fwpkg patch-hash+ 直接烧 ELF)。
可用环境变量:
| 变量 | 含义 | 默认 |
|---|---|---|
PROBE_RS_YAML | fork 的芯片描述 yaml(必填) | — |
CHIP | probe-rs --chip 值 | WS63 |
CHIP_KIND | ws63/bs21(选默认 app 基址) | ws63 |
BASE_ADDRESS | app 分区基址 | ws63 0x00230000 / bs21 0x00090000 |
PROBE_RS | probe-rs 二进制 | probe-rs |
如要直接用本地编译出来的 fork:PROBE_RS=/home/.../probe-rs/target/debug/probe-rs。
排错
'probe-rs' not found或chip 'WS63' not found:装的是上游 probe-rs,不是补丁版 fork。重装上面那条--branch add-hisilicon-ws63-bs21。PROBE_RS_YAML not found:忘了给 yaml 路径,或路径错。yaml 在 fork 仓库probe-rs/targets/HiSilicon_WS63.yaml。"Flash Init Fail"之类的提示:在本端口里通常非致命——download 仍会继续并成功。先看最终是否Finished/写入成功,再决定是否当真问题。- 写入卡住 / 校验失败:很可能是 flash **block protect(块保护)**没解。先确认 app 分区不在保护区(厂商工具或 SFC 寄存器层面解保护),再重试。
- download 成功但 reset 后没反应:
- WS63(route 2):八成是忘了跑
hisi-fwpkg patch-hash——头里 body SHA-256 没填,flashboot 校验 hash 不过,不会进你的程序(secure-off 只跳过 ECC 签名,hash 仍校验,没有“假签名/dummy 签名”能让它启动,必须是真实 body hash)。也要确认烧的是带 boot-header 的 ELF,不是cargo直接产出但没 patch 的 ELF。 - BS2X(route 1):确认烧的是
hisi-fwpkg image出的.img(0x300 头镜像)而不是裸 ELF/bin——裸文件复位后 PC 落在头区,不会进你的程序。
- WS63(route 2):八成是忘了跑
之后
要边烧边看 UART、或让 cargo run 直接烧真机,见如何用硬件 runner 让 cargo run 烧真机;要 attach 调试/读内存见如何用 probe-rs 调试与读内存。
如何用 hisiflash 烧录到真机
这是厂商串口 / YMODEM 路径:把程序打成 .fwpkg,用 hisiflash 经串口烧进去。它不需要 SWD/JTAG 探针,只要一根 UART 线——适合手上没有补丁版 probe-rs 探针、或就想用厂商工具的场景。
想用探针的验证主路径请看如何用 probe-rs 烧录。两者怎么选见文末。
前提
hisiflash:cargo install hisiflash-cli(或自行构建 hisiflash 仓库)。LOADERBOOT:厂商 LoaderBoot 二进制。hisiflash会先把它推进 SRAM,再让它接管 flash 写入。取自 fbb_ws63 构建产物(src/output/ws63/.../*loaderboot*.bin)。必填。ADDRESS:程序写入的 flash 偏移(典型 app 分区偏移0x230000)。对照板子的分区表确认——写错可能烧不进或烧错位置。
两步走
1. 打成 .fwpkg
hisi-fwpkg pack -o blinky.fwpkg --chip ws63 \
target/riscv32imfc-unknown-none-elf/release/blinky
# 或用脚本:
FWPKG=1 hil/pack.sh blinky # -> examples/ws63/target/.../blinky.fwpkg
.fwpkg 是单分区容器(V1 + CRC),内含已带 0x300 头的 app 镜像(见如何打包镜像)。
2. 用 hisiflash 烧
hisiflash 直接吃 .fwpkg:
hisiflash flash blinky.fwpkg
或者走 hil/flash.sh 的 METHOD=hisiflash 分支(它写程序而非 fwpkg,先推 LoaderBoot 再 write-program):
METHOD=hisiflash PORT=/dev/ttyUSB0 \
LOADERBOOT=/path/loaderboot.bin ADDRESS=0x230000 \
hil/flash.sh blinky
环境变量(hisiflash 路径):
| 变量 | 含义 | 默认 |
|---|---|---|
PORT | 串口(导出为 HISIFLASH_PORT) | 自动探测 |
BAUD | 烧录波特率(HISIFLASH_BAUD) | hisiflash 默认 921600 |
LOADERBOOT | 厂商 LoaderBoot bin(必填) | — |
ADDRESS | 程序写入偏移(必填) | — |
HISIFLASH | hisiflash 二进制 | hisiflash |
波特率注意:fwpkg/YMODEM 流程常见 230400,更稳的可降到 115200;
hisiflash本身的write-program默认 921600。波特率太高在差线材上易丢包,烧不进就降速重试。
何时用 hisiflash vs probe-rs
| probe-rs(验证主路径) | hisiflash(厂商路径) | |
|---|---|---|
| 接线 | SWD/JTAG 探针 | 一根 UART |
| 依赖 | 补丁版 probe-rs fork + yaml | 厂商 LoaderBoot + hisiflash |
| 调试 | 能 attach、读内存、下断点 | 仅烧录 |
| 验证状态 | 真机验证 | 厂商成熟路径 |
优先 probe-rs(能顺带调试);没有探针、或只想用厂商成熟链路时用 hisiflash。
如何打包成可启动镜像(hisi-fwpkg)
裸 ELF/bin 烧进 app 分区不会启动。flashboot 会无条件跳到 app 分区 + 0x300(WS63 上 app 分区 = flash 0x230000,故入口 = 0x230300)。所以 app 分区开头必须放一段 0x300 字节的 HiSilicon 镜像头,后面才是你的代码。镜像头的字段布局见应用镜像格式与签名,启动流程见启动流程。
补这层 0x300 头有两条路线,按芯片不同:
- WS63(route 2,当前主路径):用
hisi-riscv-rt的boot-headerfeature,0x300 头在链接期就烧进 ELF,裸 ELF 本身即可启动——不需要hisi-fwpkg image。链接后只需补一步 body SHA-256(hisi-fwpkg patch-hash),再用probe-rs download/run直接烧裸 ELF,没有中间.img。详见下面的 WS63:boot-header+patch-hash。 - BS21/BS2X(route 1):尚无链接期 boot-header,仍走
hisi-fwpkg image -o app.img <elf>(编译后),再把.img烧到 app 分区。
安装:
cargo install --git https://github.com/hispark-rs/hisi-fwpkg(或cargo install hisi-fwpkg-cli)。
WS63:boot-header + patch-hash
WS63 用 hisi-riscv-rt 的 boot-header feature,把 0x300 HiSilicon 头在链接期直接放进 ELF,裸 ELF 即可启动。链接后再补一步 body 哈希即可:
# 编译(boot-header feature 会把 0x300 头烧进 ELF)
cargo build --release
# 补 body SHA-256(就地改写 ELF;secure-off 时 flashboot 仍校验 hash)
hisi-fwpkg patch-hash \
target/riscv32imfc-unknown-none-elf/release/blinky
# 直接烧裸 ELF(无中间 .img),再复位运行
probe-rs download target/riscv32imfc-unknown-none-elf/release/blinky
probe-rs run target/riscv32imfc-unknown-none-elf/release/blinky
patch-hash 只接受一个位置参数 <ELF>,就地填回 body 的 SHA-256(不产新文件)。注意 cargo flash 不适用于 WS63 boot-header——它没有插入这步强制 patch-hash 的 runner 槽位,无法保证烧进去的 ELF 带正确 body hash。烧录细节见如何用 probe-rs 烧录,新工程脚手架见 hisi-rs-template 的 justfile(patch / flash / run-hw recipe)。
BS2X:hisi-fwpkg 的两个子命令:image vs pack
仅 BS2X 走
image。 WS63 已改用上面的boot-header+patch-hash(route 2),不要再对 WS63 的 ELF 跑image。下面的image子命令针对 BS21/BS2X(route 1);pack子命令 WS63/BS2X 都可用(厂商 fwpkg 路径)。
hisi-fwpkg 自动从 magic 识别输入是 ELF 还是裸 bin,两个子命令各产一种产物:
| 子命令 | 产物 | 内容 | 谁用 |
|---|---|---|---|
image | *.img | 0x300 HiSilicon 头 ‖ body(含 body 的 SHA-256) | BS2X probe-rs download 路径(route 1) |
pack | *.fwpkg | 把上面的 image 再包进单分区 fwpkg(V1 容器 + CRC) | 厂商 hisiflash / YMODEM 路径(WS63/BS2X 通用) |
产 *.img(BS2X probe-rs 路径用)
hisi-fwpkg image -o blinky.img \
target/riscv32imfc-unknown-none-elf/release/blinky
image 只有 -o/--output <OUTPUT> 和一个位置参数 <INPUT>(ELF 或裸 bin)。app 基址在烧录时由 probe-rs --base-address 给(见如何用 probe-rs 烧录),所以 image 自身不需要芯片/地址参数。
产 *.fwpkg(hisiflash 路径用)
hisi-fwpkg pack -o blinky.fwpkg --chip ws63 \
target/riscv32imfc-unknown-none-elf/release/blinky
pack 多几个选项:
-c/--chip <ws63|bs21>(默认ws63):决定 app 分区基址(ws63 = 0x230000,bs2x = 0x90000)。--app-addr <APP_ADDR>:覆盖 app 分区烧录地址(接受十六进制,如0x230000),自定义分区表时用。--name <NAME>:fwpkg 里分区名(默认app)。
用脚本一把梭
- WS63(route 2):用
hisi-rs-template的justfile——just patch(cargo build+hisi-fwpkg patch-hash)、just flash(patch +probe-rs download/reset)、just run-hw(patch +probe-rs run)。 - BS2X(route 1):
hil/pack.sh封装了image(+ 可选pack),按示例名解析 ELF:
CHIP=bs21 hil/pack.sh blinky # -> examples/.../blinky.img(默认只产 .img)
FWPKG=1 hil/pack.sh blinky # 额外再产一个 blinky.fwpkg
CHIP 决定 app 基址(APP_ADDR= 可覆盖),脚本跑完会把两条烧录命令(probe-rs / hisiflash)打印出来供复制。pack/fwpkg(厂商 hisiflash 路径)对 WS63 同样可用。
关于签名:本片不需要真签名(但需要真实 body hash)
镜像头里有签名字段,但开发芯片 secure boot 是关的(efuse SEC_VERIFY_ENABLE == 0)。注意 secure-off 只跳过 ECC 签名,不跳过 body 哈希——flashboot 在硅片上仍会校验 body SHA-256。所以一个能启动的镜像需要 0x300 头 + 真实 body SHA-256(secure-off 仍校验 hash,只跳过 ECC 签名),并不需要真实签名密钥;hisi-fwpkg image(route 1)/ patch-hash(route 2)填的就是这份真实 hash。要打开 secure boot 的代价与做法见安全启动与签名。
如何用硬件 runner 让 cargo run 烧真机
平时 cargo run 走的是 QEMU runner(在模拟器里跑)。本篇让 cargo run 改成「编译 → 打包 → 烧进真机 → 复位 → 串口看输出」,靠的是 cargo 的 per-target runner 机制 + hil/cargo-run-hw.sh。
这只影响你显式覆盖 runner 的那一次(或那个 shell)。不覆盖时,普通
cargo run仍然走 QEMU,互不影响。
原理
cargo 调用 runner 的方式是 <runner> <编译出的ELF路径> [args...]。hil/cargo-run-hw.sh 接住 $1 这个 ELF。WS63 用 hisi-riscv-rt 的 boot-header feature,0x300 HiSilicon 头在链接期就烤进了 ELF,裸 ELF 直接可引导——没有 hisi-fwpkg image 步骤、也没有中间 .img。脚本只用 hisi-fwpkg patch-hash <elf> 就地补上 body SHA-256(flashboot 即便关了 secure-verify 也仍校验 hash,secure-off 只跳过 ECC 签名,不跳过 hash),然后用补丁版 probe-rs download 把这个补好 hash 的 ELF 直接写进 flash,reset 复位,并(若设了 PORT)在复位前就开始抓 UART0 输出。
它依赖补丁版 probe-rs fork 和
hisi-fwpkg——脚本启动时会检查这两个二进制在不在。
用法
用 per-target runner 环境变量覆盖(target 是 riscv32imfc-unknown-none-elf,转成大写下划线即环境变量名):
CARGO_TARGET_RISCV32IMFC_UNKNOWN_NONE_ELF_RUNNER=hil/cargo-run-hw.sh \
cargo run -p blinky --release
要边烧边看串口,再加 PORT:
CARGO_TARGET_RISCV32IMFC_UNKNOWN_NONE_ELF_RUNNER=hil/cargo-run-hw.sh \
PORT=/dev/ttyUSB0 \
cargo run -p uart_hello --release
环境变量
脚本全部参数都有合理默认:
| 变量 | 含义 | 默认 |
|---|---|---|
PROBE_RS | probe-rs 二进制 | PATH 里的 probe-rs |
PROBE_CHIP | probe-rs --chip 值 | WS63 |
PROBE_YAML | --chip-description-path yaml | 空 = 用内置数据库 |
HISI_FWPKG | hisi-fwpkg 二进制 | PATH 里的 hisi-fwpkg |
PORT | 复位后要抓的板子 UART0 | 空 = 不抓串口 |
UART_BAUD | 抓串口的波特率 | 115200 |
MONITOR | 抓串口的秒数 | 10 |
装的 probe-rs 内置库里若没有 WS63 描述,必须显式给
PROBE_YAML=/path/HiSilicon_WS63.yaml(fork 自带)。本地编译的 fork 用PROBE_RS=/home/.../probe-rs/target/debug/probe-rs。
典型一条龙(指定 fork 二进制 + yaml + 抓串口 15 秒):
CARGO_TARGET_RISCV32IMFC_UNKNOWN_NONE_ELF_RUNNER=hil/cargo-run-hw.sh \
PROBE_RS=/home/me/probe-rs/target/debug/probe-rs \
PROBE_YAML=/home/me/probe-rs/targets/HiSilicon_WS63.yaml \
PORT=/dev/ttyUSB0 UART_BAUD=115200 MONITOR=15 \
cargo run -p uart_hello --release
与模板 justfile 的对应
从模板生成的工程(见如何从模板新建一个工程)用 just 封装了同样的流程:
- WS63:
just flash≈ 这里的「补 hash + download + reset」(hisi-fwpkg patch-hash→probe-rs download <elf>→probe-rs reset);just run-hw则等价于在补好 hash 的 ELF 上跑probe-rs run(顺带抓 RTT/semihosting)。 - 要让
cargo run/just run-hw烧真机而非 QEMU,是同一套机制:模板的just run走 QEMU,烧真机用just flash/just run-hw(或在工程里照本篇加一条run-hw配方,把CARGO_TARGET_..._RUNNER指向cargo-run-hw.sh)。
WS63 的 just flash 实现就是上面三步的等价命令,区别只是用 justfile 变量(CHIP/CHIP_DESC)代替环境变量。
BS2X(BS21/BS20)没有链接期 boot-header,仍走「route 1」:
just image用hisi-fwpkg image -o app.img <elf>后打包,just flash再把.img按--binary-format bin --base-address {{APP_ADDR}}写到 app 分区。本篇的 WS63 runner 不适用于 BS2X。
如何运行 HIL 冒烟测试
hil/hil-smoke.sh 把每个示例逐个烧到真机、读串口、断言它打印了预期的标记串。它验证 QEMU 证明不了的部分——真实时钟/时序、真实外设(尤其是修正后的 24 MHz TCXO 定时器和 160 MHz UART 波特基准)。HIL 框架背景见HIL 测试框架,全部标记串见HIL 标记串与环境变量。
前提:板子接好、UART0 接到 host、烧录环境就绪(见用 probe-rs 烧录,hil-smoke 默认通过
hil/flash.sh烧录,即默认METHOD=probe-rs)。
运行
PORT=/dev/ttyUSB0 PROBE_RS_YAML=/path/HiSilicon_WS63.yaml hil/hil-smoke.sh
走厂商烧录路径时把 flash.sh 那套环境变量带上:
METHOD=hisiflash PORT=/dev/ttyUSB0 \
LOADERBOOT=/path/loaderboot.bin ADDRESS=0x230000 \
hil/hil-smoke.sh
环境变量(与 hil/flash.sh 同:PORT/BAUD/LOADERBOOT/ADDRESS/HISIFLASH/PROBE_RS_YAML…),外加:
| 变量 | 含义 | 默认 |
|---|---|---|
PORT | 板子 UART0(必填) | — |
UART_BAUD | 示例的 UART0 波特率(8N1) | 115200 |
SETTLE | 每次烧完读串口的秒数 | 4 |
MONITOR | 自定义「打印原始 UART 到 stdout」的命令 | 直接 cat $PORT |
它检查哪些标记串
脚本逐示例烧录后,在 SETTLE 秒内 grep -E 串口输出找下面的模式(命中即 PASS):
| 示例 | 期望标记串(egrep) | 验证什么 |
|---|---|---|
uart_hello | Hello from WS63 | UART banner(验证 160 MHz 波特基准) |
timer_irq | timer irq # 或 OK: timer | 定时器中断投递(验证 24 MHz TCXO 时钟) |
gpio_irq | gpio irq # | GPIO 中断投递 |
reset_demo | reset_reason=Software | 软复位 + 复位原因 |
spi_loopback | SPI loopback OK | 阻塞 SPI0(真机需先短接 MOSI↔MISO) |
i2c_scan | scan done 或 no devices | I2C0 总线扫描 |
两个示例不在自动断言里:blinky(GPIO0 翻转无 UART——用 LED / 逻辑分析仪看)、semihost_selftest(需要调试器的 semihosting——裸 HIL 跳过)。
读懂结果
- 每个
check打印PASS: '<pat>' seen或FAIL。FAIL 时会把串口最后几行 / flash 错误尾部打印出来帮你定位。 - 末行汇总
HIL SMOKE: PASS(退出码 0)或HIL SMOKE: FAIL(退出码 = 失败数 / 非零)。 - 常见 FAIL 原因:
- flash failed:烧录环境没配好(缺 yaml/LOADERBOOT/探针),看尾部错误。
- 标记串没出现但板子像在跑:
UART_BAUD不对(示例用 8N1,默认 115200),或SETTLE太短没等到输出——调大SETTLE。 spi_loopbackFAIL:真机上没短接 MOSI↔MISO(QEMU 会自环,真机不会)。
封装与 CI
.claude/skills/hil-smoke是这个脚本的 wrapper skill,给 agent 一键跑全套 HIL 冒烟。.github/workflows/hil.yml在 self-hosted runner(接了真板子的机器)上跑同一脚本,把真机回归纳入 CI。
如何用 probe-rs 调试与读内存
烧录之外,补丁版 probe-rs fork 还能 attach 上去读内存、读 CSR、复位到指定状态、下硬件断点。本篇是真机诊断的常用招式。
全部命令都需要 fork(
--branch add-hisilicon-ws63-bs21)+ 其HiSilicon_WS63.yaml。下面为简洁省略了--chip-description-path HiSilicon_WS63.yaml,实跑时按需补上(或用PROBE_RS_YAML/--chip-description-path)。
读内存 / flash
# 读 app 镜像头开头 16 个 word(验证 0x300 头烧对没)
probe-rs read --chip WS63 b32 0x00230000 16
# 读 app 入口前几条指令
probe-rs read --chip WS63 b32 0x00230300 8
read <宽度> <地址> <个数>,宽度 b8/b32 等。地址直接给绝对物理地址(内存映射见内存映射)。
复位行为:reset_and_halt 落在复位向量
本端口修了 resethaltreq,所以 reset_and_halt 现在真的停在复位向量 0x100000(而不是任意位置)。这让「复位后第一条指令开始单步」成为可能:
probe-rs reset --chip WS63 # 复位并运行
# attach + halt 在复位向量(GUI/脚本里用 reset_and_halt)
复位后 core 从 mask ROM(0x100000)起跑,ROM 再跳 flashboot、flashboot 跳 app(0x230300)。整条链见启动流程。
读 CSR
attach 后可读 RISC-V CSR(mstatus/mepc/mcause/mtvec…)定位 trap:
# 在 halt 状态下读(具体子命令依 fork 版本,常见为 probe-rs read 的 CSR 形式或 GUI)
probe-rs read --chip WS63 b32 0x00230300 4 # 读应用代码确认在跑你的程序
mcause/mepc对排「跑飞到 ROM」最有用:若 halt 时 PC 在0x10xxxx区间,说明根本没进 app(多半是 0x300 头没烧对,见排错)。
抓住 app 入口:在 0x230300 下硬件断点
复位后直接 halt 经常停在 mask ROM 里,而不是你的程序——因为 ROM/flashboot 要先跑一段。要抓到「应用刚开始执行」的那一刻,在 app 入口 0x230300 下一个硬件断点,再复位运行,core 会停在你的第一条指令而不是 ROM:
设硬件断点 @ 0x230300 → reset run → 命中断点(已在 app 入口)
这正是 HIL 诊断里 examples/trapdump.rs 那类「trap dump」模式的做法:上电后在 app 入口设硬件断点,命中后 dump 寄存器/CSR/栈,确保你看到的是应用状态而不是落在 mask ROM 里的假象。把它当成「真机版 panic backtrace」——QEMU 给不了真实时序下的现场。
Dump ROM
mask ROM 在 0x100000,可整段读出来离线分析(启动早期行为、ROM 边界,见启动流程):
# 读 ROM 头部若干 word(按需扩大个数 / 写文件留存)
probe-rs read --chip WS63 b32 0x00100000 64
ROM 是 mask ROM——只读、不可改。dump 出来是为读懂启动链和定位「PC 卡在 ROM」的问题;mask ROM + SFC 是 QEMU 复刻不了的两处真机边界。
排错
- attach 不上 / 找不到芯片:装的是上游 probe-rs 不是 fork;或没给 yaml。见用 probe-rs 烧录的排错。
- halt 后 PC 一直在
0x10xxxx:还在 mask ROM,没进 app——用上面的「app 入口硬件断点」抓应用,并确认烧的是 0x300 头镜像。 "Flash Init Fail"类提示:本端口里通常非致命,不影响read/reset。
如何从模板新建一个工程
要从零起一个 WS63/BS2X 应用,用 cargo generate 从模板仓库 hisi-rs-template 生成——它帮你配好工具链、链接脚本、依赖和一份 justfile,开箱即可构建+烧录。
前提:已安装 hisi-riscv 工具链;
cargo install cargo-generate。
生成
cargo generate --git https://github.com/hispark-rs/hisi-rs-template
交互式会问两个选项:
- chip(目标芯片):
ws63/bs21/bs21e/bs22/bs20(默认ws63)。BS2X 几个 SKU 在 HAL 里是同一颗芯片(chip-bs21),差别只在 L2RAM 大小(bs20=128K,其余 160K,写在memory.x)和 QEMU machine 名。 - starter(起步应用):
blinky/uart_hello/async(默认blinky)。 - 还会问 app 分区 flash 地址(WS63 默认
0x00230000,BS2X 默认0x00090000)——没有自定义分区表就用默认。
非交互式可一把给定:
cargo generate --git https://github.com/hispark-rs/hisi-rs-template \
--name my-app --define chip=ws63 --define starter=blinky --silent
WS63 的内存布局来自 hisi-riscv-rt 自带的链接脚本,所以模板不为 WS63 生成
memory.x;BS2X 才需要工程级memory.x(模板会带)。
生成的 justfile
工程带一个 justfile(cargo install just),封装了硬件验证过的流程:
| 配方 | 做什么 |
|---|---|
just build | cargo build --release 编出 ELF |
just run | 在 QEMU 里跑(cargo run --release) |
just patch(WS63) | build 后 hisi-fwpkg patch-hash {{elf}} 补 body 的 SHA-256(0x300 头已由 boot-header feature 在链接期嵌进 ELF,无需 image 步骤) |
just image(BS2X) | build 后 hisi-fwpkg image 补 0x300 头 → *.img(BS2X 暂无链接期 boot-header,仍走 image 路径) |
just flash | WS63:patch 后 probe-rs download 直接烧裸 ELF 再 reset;BS2X:image 后 probe-rs download 把 *.img 烧进 app 分区再 reset |
just fwpkg | hisi-fwpkg pack 产 *.fwpkg(hisiflash/厂商路径) |
just clean | cargo clean + 删 img/fwpkg |
烧录配方的前提(构建/run 不需要这些):hisi-fwpkg、补丁版 probe-rs fork + 其 HiSilicon_WS63.yaml。CHIP/CHIP_DESC/APP_ADDR 可在命令行覆盖,例如:
just CHIP_DESC=~/probe-rs/HiSilicon_WS63.yaml flash
第一次构建 + 烧录
WS63(route 2,链接期已带 0x300 头):
cd my-app
just build # 编出 release ELF(boot-header feature 已把 0x300 头嵌进 ELF)
just patch # hisi-fwpkg patch-hash 补 body 的 SHA-256
just flash # probe-rs download 直接烧裸 ELF 并复位(需 probe-rs fork + yaml)
just flash依次做了这些事:先just patch(hisi-fwpkg patch-hash {{elf}}把 body 的 SHA-256 填进链接期已嵌好的 0x300 头),再probe-rs download --chip WS63 ... {{elf}}把这份裸 ELF 直接烧到芯片(无中间.img),最后probe-rs reset复位运行。
BS2X(route 1,build 后才补 0x300 头打成 .img):
cd my-app
just build # 编出 release ELF
just image # hisi-fwpkg image 补 0x300 头 → *.img
just flash # probe-rs download 把 *.img 烧进 app 分区并复位
烧 BS2X 时把 CHIP/APP_ADDR 调成对应值(BS2X 基址 0x00090000,尚未 HIL 验证,先对照分区表确认)。
之后
- 不想用
just、想理解每一步 → 如何打包镜像 + 如何用 probe-rs 烧录。 - 想让
cargo run直接烧真机 → 如何用硬件 runner。 - 要加自己的外设驱动 → 如何新增一个外设驱动。
如何新增一个外设驱动
给 hisi-riscv-hal 加一个新外设驱动,照仓库统一的「驱动模块范式」走,再配一个带 UART PASS 标记的示例让 HIL 能验证它。本篇是配方;范式背后的设计取舍见 HAL API 总览 和外设清单与覆盖情况,也对照仓库 CLAUDE.md 的「Driver Module Pattern」一节。
配置接口必须遵守类型化配置约定 ——「能编译就能在硅片上跑」是本项目头号 API 约定:配置面用校验 newtype / type-state / 自起时钟收紧,不留「能写出来却被静默 clamp/截断/没接时钟」的参数;操作面保持 embedded-hal 的
Result。落地按 docs-first(先改文档再写代码),用.claude/skills/typed-config/scan.sh扫候选,参考pwm.rs。
0. 确认外设单例存在
驱动消费的是 crates/hisi-riscv-hal/src/peripherals.rs 里用宏生成的外设单例。文件里两个宏:
peripheral!($name, $pac_ty)—— 为某个 PAC 类型生成带生命周期的 ZST$name<'d>,附steal()、ptr()、register_block()。peripherals!(...)—— 生成Peripherals结构体,带take()(安全单例)和steal()(unsafe)。
若你的外设的 PAC 类型还没被 peripheral! 包过,先加一行(注意按芯片放进对应的 #[cfg(feature = "chip-ws63")] / chip-bs21 块),并在对应的 peripherals!(...) 列表里加 字段 => 类型。例如已有的:
peripheral!(Spi0, crate::soc::pac::Spi0);
// ...
peripherals!(
// ...
SPI0 => Spi0,
// ...
);
1. 写驱动模块
在 crates/hisi-riscv-hal/src/ 加 <name>.rs,并在 lib.rs 加 pub mod <name>;(按芯片可加 #[cfg(...)])。模块结构:
//! <Name> driver for WS63.
use crate::peripherals::MyPeriph;
use core::marker::PhantomData;
/// 配置项:用一个 `Config` 结构体 + `Default`(对齐 spi/uart 的 `Config`)。
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub frequency: u32,
// ...
}
impl Default for Config {
fn default() -> Self { Self { frequency: 1_000_000 } }
}
pub struct MyDriver<'d> {
_peripheral: MyPeriph<'d>,
}
impl<'d> MyDriver<'d> {
/// 构造即配置:消费外设单例(保证独占 + 防 use-after-drop)。
pub fn new(peripheral: MyPeriph<'d>, config: Config) -> Self {
let me = Self { _peripheral: peripheral };
// ...用 me.regs() 配置硬件...
me
}
/// 拿到 PAC 寄存器块。指针是静态物理 MMIO 地址,恒有效。
fn regs(&self) -> &'static crate::soc::pac::myperiph::RegisterBlock {
// SAFETY: PAC 指针是静态物理 MMIO 地址,始终有效。
unsafe { &*MyPeriph::ptr() }
}
// ...API 方法...
}
要点:
- 构造函数消费外设单例(
MyPeriph<'d>),靠生命周期'd防止 token 被 drop 后还用——这是仓库的安全主线。 regs()返回&'static RegisterBlock,内部unsafe { &*MyPeriph::ptr() },把 unsafe 寄存器读写收口在驱动方法里。#![no_std]:不用堆 /Vec,要缓冲区用定长数组。
多实例外设(UART/I2C/SPI/DMA 那种)
同一类外设有多个实例时,用 PhantomData<&'d T> 区分,并为每个实例给独立构造函数(不是统一 new(),因为每个实例配置可能不同):
pub struct MyBus<'d, T> { idx: u8, _peripheral: PhantomData<&'d T> }
impl<'d> MyBus<'d, Inst0<'d>> { pub fn new_inst0(_p: Inst0<'d>, c: Config) -> Self { /* ... */ } }
impl<'d> MyBus<'d, Inst1<'d>> { pub fn new_inst1(_p: Inst1<'d>, c: Config) -> Self { /* ... */ } }
实例到寄存器块的映射用一个按 idx 分发的小函数(参考 spi.rs 的 spi_regs(idx))。
2. 实现 embedded-hal trait
在模块末尾为驱动实现对应的 embedded-hal 1.0 trait(SPI 实现 spi::SpiBus、I2C 实现 i2c::I2c、串口实现 embedded-io 等),先 ErrorType 再具体 trait。对照 spi.rs 底部:
impl embedded_hal::spi::Error for SpiError { /* kind() */ }
impl embedded_hal::spi::ErrorType for Spi<'_, Spi0<'_>> { type Error = SpiError; }
impl embedded_hal::spi::SpiBus for Spi<'_, Spi0<'_>> { /* read/write/transfer/flush */ }
开了 async feature 还可以补 embedded-hal-async 的对应实现(多以阻塞版兜底,见 spi.rs 的 embedded_hal_async::spi::SpiBus)。
3. Sealed trait(需要时)
如果你要引入「只能内部实现」的标记 trait(比如限定哪些类型能当某外设的输入/输出,或 DMA word),加在 private.rs:以 Sealed 为 supertrait,外部就无法实现。现有的有 DmaWord、PeripheralInput、PeripheralOutput。不要复活已删掉的空 DriverMode/Blocking/Async 标记 trait——它们没有真实 async 后端时纯属误导。
4. 配一个带 PASS 标记的 HIL 示例
新建一个示例 crate(如 examples/ws63/myperiph_demo),用 UART0 打印一个HIL 能 grep 的标记串,并在根 Cargo.toml 的 members / default-members 里登记它。骨架(仿 spi_loopback):
#![no_std]
#![no_main]
use hisi_riscv_hal::Peripherals;
use hisi_riscv_hal::uart::{Config as UartConfig, Uart};
use hisi_riscv_rt::entry;
#[entry]
fn main() -> ! {
let p = Peripherals::take().unwrap();
let uart = Uart::new_uart0(p.UART0, UartConfig::default());
let mut dev = hisi_riscv_hal::myperiph::MyDriver::new(p.MYPERIPH, Default::default());
match dev.do_thing() {
Ok(_) => uart.write(0, b" MyPeriph OK\r\n"), // <- HIL PASS 标记
Err(_) => uart.write(0, b" MyPeriph FAIL\r\n"),
}
loop { core::hint::spin_loop(); }
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! { loop { core::hint::spin_loop() } }
标记串约定:用一个稳定、唯一、好 grep 的短语(如 MyPeriph OK)。然后把它登进 HIL 冒烟脚本,让 hil/hil-smoke.sh 自动断言它(脚本里加一行 check myperiph_demo "MyPeriph OK" "...",标记串清单见HIL 标记串与环境变量)。
5. 验证
cargo check -p hisi-riscv-hal # 驱动能编过(host 上 check)
cargo build -p myperiph_demo --release # 示例能编出 ELF
再按如何运行 HIL 冒烟测试烧到真机看标记串。
提交时记得:HAL 与示例若在 submodule 里,先在 submodule 内 commit,再更新父仓库的 submodule 指针(见
CLAUDE.md)。
本章导读 · Reference
参考章节是信息导向的查阅资料:精确、结构化、可逐项检索。这里只陈述事实(地址、大小、标志位、字段偏移、签名、默认值),不讲教程,不讲原理。需要“怎么做“请看 操作指南,需要“为什么“请看 原理与背景。
本章所有事实均直接取自源码(memory.x、HAL 源文件、hisi-fwpkg 源、HIL 脚本、工具链配置)。
速查入口
| 页面 | 内容 |
|---|---|
| 内存映射 | WS63 内存区域、导出的链接符号、栈大小、复位向量与入口 |
| 示例目录与验证标记串 | 18 个示例的用途、观测通道、精确的成功标记串、是否需接线、QEMU/真机状态 |
| HAL API 总览 | hisi-riscv-hal 公开 API 结构图:驱动模块、构造函数、单例/GPIO/多实例/sealed/特性 |
| 完整 API 文档(rustdoc)↗ | hal/pac/rt 的逐项 API;与本手册同站部署在 /api/,CI 自动构建 |
| 外设清单与覆盖情况 | 全部 HAL 驱动模块、外设、基地址、示例覆盖、可否裸板自检 |
| 工具链与编译目标 | hisi-riscv 工具链通道、目标三元组、rust-toolchain.toml、.cargo/config.toml |
| 应用镜像格式与签名 | 0x300 镜像头字段布局、fwpkg V1 容器、CRC16 |
| HIL 标记串与环境变量 | 每个示例的 HIL 标记串、HIL 脚本消费的全部环境变量 |
| CLI 工具速查 | hisi-fwpkg、补丁版 probe-rs、QEMU、hisiflash 命令与仓库清单 |
内存映射
本页复现 WS63 的内存布局,事实取自 crates/hisi-riscv-rt/memory.x 与 crates/hisi-riscv-rt/asm/startup.S。默认配置:576K SRAM、16K ITCM、16K DTCM。TCM 与 SRAM 大小可经 CONFIG 标志配置(参见 fbb_ws63)。
启动流程的“为什么“见 启动流程。
内存区域(MEMORY{})
| 区域 | 属性 | ORIGIN | LENGTH | 结束 | 说明 |
|---|---|---|---|---|---|
BOOTROM | rx | 0x100000 | 0x9000 (36K) | 0x109000 | 掩膜 ROM 启动码 |
ROM | rx | 0x109000 | 0x43000 (268K) | 0x14C000 | 应用 ROM:SFC、pinmux、watchdog、timer、systick、TCXO、BT、WiFi |
ITCM | rwx | 0x14C000 | 0x4000 (16K) | 0x150000 | 指令 TCM(默认 16K,可配至 64K) |
DTCM | rw | 0x180000 | 0x4000 (16K) | 0x184000 | 数据 TCM(默认 16K,可配至 64K) |
FLASH | rx | 0x200000 | 0x800000 (8MB) | 0xA00000 | 外部 SPI NOR flash,XIP |
PROGRAM | rx | 0x230300 | 0x240000 (~2.25MB) | 0x470300 | flash 内应用代码区(启动头之后) |
SRAM | rwx | 0xA00000 | 0x90000 (576K) | 0xA90000 | 主系统 RAM(SRAM/L2RAM) |
PRESERVE | rw | 0xA90000 - 0x100 = 0xA8FF00 | 0x100 (256B) | 0xA90000 | SRAM 尾部 256 字节,保留启动状态 |
BOOTROM+ROM共 304K(36K + 268K),地址连续(0x100000–0x14C000)。
关键地址
| 名称 | 地址 | 说明 |
|---|---|---|
| app 分区 | 0x230000 | flashboot 从此处加载 app 镜像 |
| app 入口 | 0x230300 | 入口 = 分区地址 + 0x300(跳过 0x300 字节镜像头) |
| 复位向量 | 0x100000 | 链接为程序内存中的第一项;reset_vector: j HandleReset(startup.S) |
栈顶 _stack_start | 0xA90000 | = ORIGIN(SRAM) + LENGTH(SRAM) |
flashboot 无条件跳到
app_partition + 0x300,故 app 镜像必须带 0x300 字节 HiSilicon 镜像头。镜像头字段见 应用镜像格式。
导出的链接符号(PROVIDE)
区域符号(用于运行时重定位):
| 符号 | 值 |
|---|---|
__rom_start | ORIGIN(ROM) = 0x109000 |
__rom_length | LENGTH(ROM) = 0x43000 |
__itcm_start | ORIGIN(ITCM) = 0x14C000 |
__itcm_length | LENGTH(ITCM) = 0x4000 |
__dtcm_start | ORIGIN(DTCM) = 0x180000 |
__dtcm_length | LENGTH(DTCM) = 0x4000 |
__sram_start | ORIGIN(SRAM) = 0xA00000 |
__sram_length | LENGTH(SRAM) = 0x90000 |
__flash_start | ORIGIN(FLASH) = 0x200000 |
__flash_length | LENGTH(FLASH) = 0x800000 |
__program_start | ORIGIN(PROGRAM) = 0x230300 |
__program_length | LENGTH(PROGRAM) = 0x240000 |
riscv-rt v0.14 所需符号:
| 符号 | 值 |
|---|---|
_stack_start | ORIGIN(SRAM) + LENGTH(SRAM) = 0xA90000 |
_max_hart_id | 0 |
_hart_stack_size | 0x2000 |
数据/BSS 符号(memory.x 中为占位 0,权威值在 layout.ld):__sidata、__sdata、__edata、__sbss、__ebss。
栈大小(可被用户覆盖)
| 符号 | 默认值 | 用途 |
|---|---|---|
__stack_size | 0x2000 (8K) | 用户栈 |
__irq_stack_size | 0x800 (2K) | IRQ 栈 |
__exc_stack_size | 0x800 (2K) | 异常栈 |
__nmi_stack_size | 0x400 (1K) | NMI 栈 |
IRQ/异常/NMI 栈顶在
layout.ld的.stacks段中权威定义(trap 处理器引用它们,KEEP的.trap段经--gc-sections保活)。
区域别名(riscv-rt v0.14 REGION_ALIAS)
| 别名 | 指向 |
|---|---|
REGION_TEXT | PROGRAM |
REGION_RODATA | PROGRAM |
REGION_DATA | SRAM |
REGION_BSS | SRAM |
REGION_STACK | SRAM |
REGION_HEAP | SRAM |
复位向量
startup.S 中 reset_vector 位于 .text.entry 段,链接为程序内存第一项,内容为 j HandleReset。HandleReset 依次:禁用 PMP(pmpcfg0..3 = 0)、以 vectored 模式(mtvec[1:0]=01)装载 trap_vector、关中断、使能 FPU(mstatus.FS = 0b11)、初始化 gp。
复位地址
0x100000是掩膜 ROM 入口;上电后由 ROM 经 flashboot 转交到 app 入口0x230300。
示例目录与验证标记串
examples/ws63/ 下 18 个示例。下表的成功标记串、失败标记串、是否需接线均直接取自各 examples/ws63/<name>/src/main.rs。所有 UART 输出走 UART0 @ 115200 8N1;semihost_selftest 走 RISC-V 半主机(semihosting),不走 UART。
如何构建/运行见 构建一个示例 与 在 QEMU 里运行。HIL 标记串汇总见 HIL 标记串与环境变量。
一览表
| 示例 | 用途 | 观测通道 | 成功标记串 | 需接线 | QEMU | 真机 |
|---|---|---|---|---|---|---|
blinky | GPIO0 1 Hz 闪灯(现代 Output 路径) | GPIO | 无(GPIO0 翻转,逻辑分析仪/LED 观测) | 否 | ✅ | ✅ (2026-06-14) |
uart_hello | UART hello + tick 计数 | UART | Hello from WS63 on QEMU! | 否 | ✅ | ⚠️⁹ |
gpio_irq | GPIO0 pin0 上升沿 → IRQ 33(自定义 local IRQ ≥32) | UART | OK: custom local IRQ (>=32) delivered | 否¹ | ✅ | ⚠️ |
i2c_scan | I2C0 100 kHz 扫描 0x08..0x77 | UART | scan done / no devices acked | 否² | ✅ | ⚠️³ |
spi_loopback | SPI0 Mode0 1 MHz 全双工自环 | UART | SPI loopback OK | 真机需短接 MOSI↔MISO | ✅ | ⚠️⁴ |
dma_loopback | MDMA 外设环 + SDMA mem→mem | UART | DMA LOOPBACK TEST: PASS | 否 | ✅ | ⚠️ |
timer_irq | TIMER_0 周期 → IRQ 26(local trap) | UART | OK: timer interrupts delivered | 否 | ✅ | ⚠️ |
async_bus | async SpiBus + I2c + LSADC(block_on) | UART | ASYNC BUS: PASS | 否 | ✅ | ⚠️ |
async_delay | async DelayNs(TIMER0 + wfi) | UART | ASYNC DELAY: PASS | 否 | ✅ | ⚠️ |
embassy_async_io | embassy GPIO Wait + async UART + Timer | UART | EMBASSY ASYNC IO: PASS | 否¹ | ✅ | ⚠️ |
embassy_multitask | embassy 双任务 Timer::after | UART | EMBASSY MULTITASK: PASS | 否 | ✅ | ⚠️ |
net_ping | smoltcp over ws63-netmac + SLIRP(ARP/ICMP/UDP) | UART | NET PING: PASS | 否⁵ | ✅ | ❌⁶ |
reset_demo | software_reset + reset_reason 端到端 | UART | OK: software reset observed | 否 | ✅ | ⚠️ |
rf_port_demo | ws63-rf-rs porting 层 + Wi-Fi ROM-data blob 链接 | UART | RF PORT DEMO: PASS | 否⁷ | ✅ | ⚠️ |
semihost_selftest | CPU 自检(M/F 扩展、mcycle),半主机退出码 | semihosting | 退出码 0,console semihost_selftest: PASS | 否⁸ | ✅ | ❌⁸ |
custom_memory | 验证 per-example memory.x 覆盖 rt 自带 | UART | custom_memory: OK (per-example memory.x in effect) | 否 | ✅ | ⚠️ |
wifi_blob_link | --whole-archive 链接 Wi-Fi ROM-data blob + 重定位证明 | UART | BLOB LINK SPIKE: PASS | 否⁷ | ✅ | ⚠️ |
图例(真机列):✅ 已在真实硅片上验证通过;⚠️ QEMU 通过、真机尚未逐一验证(bring-up 进行中);❌ 该观测通道真机不适用。截至 2026-06-14,只有 blinky 经硅片确认;其余 UART 类示例的真机标记串套件正在 bring-up(见 HIL 测试框架)。
注:
gpio_irq/embassy_async_io把 GPIO0 pin0 设为输出,依赖 ws63-qemu 建模的 输出→输入 自环产生边沿;真机需相应注入/接线。i2c_scan:QEMU 下无真从机,no devices acked是正常结果而非失败。i2c_scan真机需挂接真实 I2C 从机才会有found device。spi_loopback:QEMU 把 SPI0 TX FIFO 环回 RX,无需跳线;真机必须短接 MOSI↔MISO。net_ping需 QEMU user netdev(-nic user,默认),纯软件/SLIRP,无需外部网络。net_ping依赖 ws63-qemu 合成 MAC(ws63-netmac @ 0x4421_0000),真机无此通道。rf_port_demo/wifi_blob_link需厂商 bloblibwifi_rom_data.a(ws63-RF 子模块)链接到位。semihost_selftest需 QEMU-semihosting;真机半主机陷阱为 no-op,exit只自旋。uart_hello真机上已确认能跑到main并运行(probe-rs 单步/采样验证),但 UART banner 在 115200 下暂不可读 —— 疑似该例不做时钟初始化、波特率基于 QEMU 默认时钟假设,真机 UART 时钟不同。属已知 bring-up 待修项。
成功标记串(逐字,用于 grep)
| 示例 | 成功标记串(verbatim) |
|---|---|
uart_hello | Hello from WS63 on QEMU! |
gpio_irq | OK: custom local IRQ (>=32) delivered |
i2c_scan | scan done(有从机时)或 no devices acked |
spi_loopback | SPI loopback OK |
dma_loopback | DMA LOOPBACK TEST: PASS |
timer_irq | OK: timer interrupts delivered |
async_bus | ASYNC BUS: PASS |
async_delay | ASYNC DELAY: PASS |
embassy_async_io | EMBASSY ASYNC IO: PASS |
embassy_multitask | EMBASSY MULTITASK: PASS |
net_ping | NET PING: PASS |
reset_demo | OK: software reset observed |
rf_port_demo | RF PORT DEMO: PASS |
semihost_selftest | console semihost_selftest: PASS(半主机退出码 0) |
custom_memory | custom_memory: OK (per-example memory.x in effect) |
wifi_blob_link | BLOB LINK SPIKE: PASS |
blinky无 UART 输出,只能由 GPIO0 翻转观测。
失败标记串
| 示例 | 失败/诊断标记串 |
|---|---|
spi_loopback | SPI loopback MISMATCH(rx≠tx);SPI error (timeout) |
dma_loopback | 各阶段 FAIL;mismatch 诊断 mismatch @<idx> got=<x> want=<y>;末行 DMA LOOPBACK TEST: FAIL |
async_bus | ASYNC BUS: FAIL;SPI MISMATCH/spi error;ADC no sample(I2C err 不计失败) |
net_ping | NET PING: FAIL (no echo reply)(5000 ms 超时) |
rf_port_demo | RF PORT DEMO: FAIL;memcpy_s/memset_s : FAIL |
semihost_selftest | console semihost_selftest: FAIL(退出码 1);semihost_selftest: PANIC(退出码 2) |
custom_memory | custom_memory: FAIL (unexpected memory.x) |
wifi_blob_link | BLOB LINK SPIKE: FAIL(验证少于 13/13) |
其余示例(blinky、gpio_irq、timer_irq、reset_demo、async_delay、embassy_*、uart_hello)无显式 FAIL 串;失败表现为成功标记串始终不出现。各 UART 示例的 #[panic_handler] 仅静默自旋(不输出),唯一例外是 semihost_selftest(写 semihost_selftest: PANIC\n 并 exit(2))。
semihost_selftest 退出码
| 退出码 | 含义 | console 输出 |
|---|---|---|
0 | PASS — 全部 CPU 不变量成立(乘法、硬浮点 ilp32f、mcycle 推进) | semihost_selftest: PASS\n |
1 | FAIL — 某 CPU 不变量检查失败 | semihost_selftest: FAIL\n |
2 | PANIC — 触达 Rust panic handler | semihost_selftest: PANIC\n |
机制:exit(code) 发 SYS_EXIT_EXTENDED (0x20) + ADP_STOPPED_APPLICATION_EXIT (0x2_0026) 块 [reason, code],使 QEMU 进程退出码等于 code。console 写经 SYS_WRITE0,串末需 NUL(\0)。
HAL API 总览
hisi-riscv-hal 是手写的安全驱动层,建模自 esp-hal 模式。本页给出公开 API 的结构图。
完整 API 文档(rustdoc)↗ —— 在线:https://hispark-rs.github.io/hisi-riscv-rs/api/(
hisi-riscv-hal/ws63-pac/hisi-riscv-rt,与本手册同站部署,CI 自动构建);本地:cargo doc -p hisi-riscv-hal --open。本页只是结构图,逐项 API 以 rustdoc 为准。
事实取自 crates/hisi-riscv-hal/src/lib.rs 及各模块头。
模块全清单与外设映射见 外设清单;async/embassy 的原理见 async 与 embassy。
crate 约定
#下链接std供主机单测)。- 必须恰好选一个芯片特性:
chip-ws63(默认)或chip-bs21(二者互斥,否则compile_error!)。 - 依赖
embedded-hal 1.0、embedded-hal-nb 1.0、embedded-io 0.6、portable-atomic。
顶层导出
| 项 | 说明 |
|---|---|
hal::Peripherals | 外设单例(peripherals 模块) |
hal::System | 系统控制(仅 chip-ws63) |
hal::prelude | 常用 trait/类型再导出 |
单例模式(peripherals.rs)
两个宏生成单例:
peripheral!($name, $pac_ty)— 生成生命周期参数化 ZST$name<'d>,带steal()、ptr()、register_block()。peripherals!(...)— 生成Peripherals结构,带take() -> Option<Self>(安全,仅一次)与steal()(unsafe)。
let p = hal::Peripherals::take().unwrap(); // 一次性安全取得
let uart = Uart::new_uart0(p.UART0, Config::default());
每个驱动经构造函数消费其外设 token('d 生命周期参数防止 Peripherals 失效后再用)。
GPIO 三级驱动(gpio.rs)
| 级别 | 类型 | 创建方式 |
|---|---|---|
| 1 类型擦除 | AnyPin<'d> | unsafe AnyPin::steal(pin_number) |
| 2 类型化驱动 | Input<'d> / Output<'d> / Flex<'d> | 由 AnyPin 经 init_input() / init_output() / init_flex() |
| 3 旧式类型态 | GpioPin<'d, MODE> | 向后兼容 |
配置结构:InputConfig { pull }、OutputConfig { open_drain, initial_high }。另有 Io::new(IoConfig) 顶层封装。
多实例外设
UART / I2C / SPI / DMA 用 PhantomData<&'d T> 区分实例,构造函数按实例分开(每实例可能配置不同):
| 外设 | 类型 | 构造函数 |
|---|---|---|
| UART | Uart<'d, T> | new_uart0(UART0, Config)、new_uart1(...)、new_uart2(...) |
| I2C (WS63 v150) | I2c<'d, T> | new_i2c0(I2C0, freq)、new_i2c1(...) |
| SPI (DesignWare SSI v151) | Spi<'d, T> | new_spi0(SPI0, Config)、new_spi1(SPI1, Config) |
| DMA | DmaDriver<'d, T: DmaInstance> | 泛型于 Dma0 / Sdma0 标记 |
SPI
Config { frequency, mode, data_bits };UARTConfig { baud, data, parity, stop }。
单外设驱动(new() 模式)
多数驱动遵循 DriverName::new(peripheral):Watchdog::new、TimerDriver::new、TcxoDriver::new、RtcDriver::new、LsAdc::new、I2sDriver::new、PwmChannel::new(&Pwm, channel)、SfcDriver::new、PkeDriver::new、SpaccDriver::new、KmDriver::new、TrngDriver::new、TempSensor::new、EfuseDriver、System::new(SysCtl0, GlbCtlM, CldoCrg)。完整签名见各模块 rustdoc。
时钟(clock.rs / clock_init.rs,仅 chip-ws63)
clock_init::init_clocks(&sys_ctl0, &cldo_crg) -> SystemClocks— 为不经 flashboot 启动的固件初始化系统时钟。ClockControl包裹CldoCrg,两种访问:直接方法(enable_uart()等)或 RAIIPeripheralGuard(AtomicU8引用计数)。Peripheral枚举把每个外设映射到(cken_register_index, bit_position)。
多数外设访问寄存器前需经 CLDO_CRG 门控使能;复位默认即使能;WDT/RTC/TCXO 常开。
sealed trait(private.rs)
Sealed 作为超 trait,阻止外部实现 DmaWord、PeripheralInput、PeripheralOutput。(旧的空 DriverMode/Blocking/Async 标记 trait 已移除。)
特性(features)
| 特性 | 内容 |
|---|---|
chip-ws63(默认) / chip-bs21 | 选芯片,互斥 |
async | embedded-hal-async/embedded-io-async 实现 + asynch::block_on + IrqSignal + 各驱动 on_interrupt |
embassy | embassy-time Driver,使 embassy-executor (platform-riscv32) 可跑 Timer::after |
async/embassy在无原子的 WS63 上经portable-atomic+critical-section工作。
外设清单与覆盖情况
crates/hisi-riscv-hal/src/*.rs 下的全部驱动模块。基地址取自各模块的 doc 注释/常量或 safety.rs 断言;标 “—” 者源文件头未列。“芯片“列:默认构建为 chip-ws63,标注 BS21 的模块仅在 chip-bs21 下编译。
完整 API 见 HAL API 总览;驱动如何新增见 新增一个外设驱动。
驱动模块表
| 模块 | 外设 / IP | 芯片 | 基地址 | 主驱动类型 | 示例覆盖 | 裸板自检 |
|---|---|---|---|---|---|---|
gpio | GPIO(19 引脚,3 块) | 两者 | 0x4402_8000/9000/A000 | Input/Output/Flex/AnyPin/Io | blinky、gpio_irq | ✅ |
ulp_gpio | ULP GPIO(8 引脚 GPIO107-114) | 两者 | 0x5703_0000 | UlpGpioPin<MODE> | 无 | ✅ |
uart | UART0/1/2(16C550) | 两者 | 0x4401_0000/1000/2000 | Uart<T> | uart_hello | ✅ |
i2c | I2C 主机 v150(I2C0/1) | WS63 | — (SCL 24 MHz) | I2c<T> | i2c_scan、async_bus | ⚠️ 需从机 |
i2c_v151 | I2C DesignWare SSI v151 | BS21 | 0x5208_3000 + idx*0x1000 | I2c<T>(Speed) | 无 | ⚠️ 需从机 |
spi | SPI 主机 DesignWare SSI v151(SPI0/1) | 两者 | — (SSI_CLK 160 MHz) | Spi<T>(Config) | spi_loopback、async_bus | ✅(QEMU 环回) |
dma | DMA(MDMA) + SDMA v151 | 两者 | MDMA 0x4A00_0000、SDMA 0x520A_0000 | DmaDriver<T> | dma_loopback | ✅ |
pwm | PWM(8 通道,32 位) | 两者 | — | PwmChannel | 无 | ✅ |
timer | 定时器(3× 32 位) | 两者 | — (24 MHz) | TimerDriver(TimerMode) | timer_irq、async_delay | ✅ |
tcxo | TCXO 64 位自由计数 | 两者 | 0x4400_04C0 | TcxoDriver | (embassy-time 间接) | ✅ |
time | 计时(Instant/Duration/Rate,基于 TCXO) | 两者 | TCXO 0x4400_04C0 | Instant/Duration | 多处间接 | ✅ |
wdt | 看门狗(24 位降计数) | 两者 | 0x4000_6000(lock magic 0x5A5A5A5A) | Watchdog | reset_demo 间接 | ✅ |
rtc | RTC v100(48 位) | WS63 | — (32768 Hz) | RtcDriver | 无 | ✅ |
rtc_v150 | RTC v150(64 位) | BS21 | RTC0 0x5702_4100 | Rtc(Mode) | 无 | ✅ |
lsadc | 低速 ADC v154 | WS63 | 0x4400_C000 | LsAdc(AdcConfig) | async_bus | ✅ |
gadc | 13 位 GADC v153 | BS21 | digital 0x5703_6000 等 | Gadc | 无 | ⚠️ |
tsensor | 温度传感器(10 位) | WS63 | — (code 114..896) | TempSensor | 无 | ✅ |
i2s | I2S / PCM 音频 | WS63 | — | I2sDriver(I2sConfig) | 无 | ⚠️ 需外设 |
pdm | PDM 麦克风前端 v150 | BS21 | 0x5208_E000 | Pdm | 无 | ⚠️ 需外设 |
keyscan | 键矩阵扫描器 v150 | BS21 | 0x5208_D000 | Keyscan(KeyEvent) | 无 | ⚠️ 需接线 |
qdec | 正交解码器 v150 | BS21 | 0x5200_0200 | Qdec | 无 | ⚠️ 需接线 |
usb | USB 2.0 OTG(DWC2 device) | BS21 | 0x5800_0000 | Usb(Speed/UsbError) | 无 | ⚠️ 需主机 |
sfc | SPI Flash 控制器 | WS63 | 0x4800_0000(safety.rs) | SfcDriver(BusConfig) | 无 | ✅ |
efuse | eFuse / OTP v151 | WS63 | STS+0x2C / CTL+0x30 / data+0x800 | EfuseDriver | 无 | ✅(只读) |
km | 密钥管理 KLAD/RKP | WS63 | —(KEYSLOT_COUNT=8) | KmDriver | 无 | ✅ |
pke | 公钥引擎(RSA/ECC/SM2) | WS63 | — | PkeDriver | 无 | ✅ |
spacc | 安全加速器(AES/SM4/HASH) | WS63 | Crypto 0x4410_0000(safety.rs) | SpaccDriver | 无 | ✅ |
trng | TRNG(FRO 熵源) | WS63 | — | TrngDriver(TrngError) | 无 | ✅ |
trng_v1 | TRNG v1 | BS21 | 0x5200_9000 | Trng(TrngError) | 无 | ✅ |
system | 系统控制(时钟/复位/电源) | WS63 | CHIP_RESET 0x4000_2110 等 | System、ResetReason | reset_demo | ✅ |
clock | 外设时钟门控参考(CLDO_CRG) | WS63 | — | Peripheral 枚举 | (多处间接) | n/a |
clock_init | 时钟初始化 / PLL 切换 | WS63 | HW_CTL 0x4000_0014 等 | SystemClocks、TcxoFreq | (多处间接) | ✅ |
delay | 忙等阻塞延时 | 两者 | — | Delay | blinky 间接 | ✅ |
interrupt | 自定义 local 中断控制器(无 PLIC) | 两者 | CSR LOCIEN0..2 0xBE0..2 等 | Priority | gpio_irq、timer_irq | ✅ |
io_config | 引脚复用配置 | WS63 | — | — | (多处间接) | ✅ |
safety | 编译期 MMIO/timer 断言(无外设) | WS63 | MMIO 0x4000_0000..0x5704_0000 | PeripheralIndex/GpioPinIndex | n/a | n/a |
asynch | async 胶水(block_on/IrqSignal) | WS63 (async) | — | IrqSignal | async_* | n/a |
embassy | embassy-time Driver | 两者 (embassy) | TCXO + TIMER | — | embassy_* | ✅ |
裸板自检:✅ 可在裸板上自验(无需外接器件);⚠️ 需外接器件/接线/从机;n/a 非外设驱动。
DMA 控制器
| 控制器 | 标记类型 | 基地址 | 通道 |
|---|---|---|---|
| 主 DMA / MDMA | Dma0 | 0x4A00_0000 | 0–3 |
| 安全 DMA / SDMA | Sdma0 | 0x520A_0000 | 0–3(逻辑 8–11) |
Peripherals 实例(WS63,chip-ws63,35 个)
SYS_CTL0、SYS_CTL1、GLB_CTL_M、CLDO_CRG、IO_CONFIG、GPIO0、GPIO1、GPIO2、ULP_GPIO、UART0、UART1、UART2、I2C0、I2C1、SPI0、SPI1、PWM、I2S、LSADC、DMA、SDMA、SFC_CFG、TIMER、WDT、RTC、TCXO、TSENSOR、EFUSE、SPACC、PKE、KM、TRNG、RF_WB_CTL、SHARE_MEM_CTL、FAMA_REMAP。
Peripherals 实例(BS21,chip-bs21,28 个)
GLB_CTL_M、GPIO0–GPIO4、ULP_GPIO、UART0–UART2、I2C0/I2C1、SPI0–SPI2、PWM、DMA、SDMA、TIMER、WDT、RTC、TCXO、TRNG、GADC、KEYSCAN、QDEC、PDM、USB。
全部 PAC 外设均有 HAL 封装。寄存器行为以 fbb_ws63 / fbb_bs2x C SDK 为 ground-truth。
工具链与编译目标
WS63 用自定义 hisi-riscv 工具链构建。事实取自 rust-toolchain.toml、.cargo/config.toml、CLAUDE.md。
安装步骤见 安装 hisi-riscv 工具链;硬浮点选型原理见 硬浮点工具链。
工具链 / 目标速查
| 项 | 值 |
|---|---|
| rustup 通道名 | hisi-riscv |
| 基础 rustc 版本 | stable 1.96.0 |
| 默认目标三元组 | riscv32imfc-unknown-none-elf |
| ISA | RV32IMFC_Zicsr |
| 浮点 | 硬件单精度,ABI ilp32f |
| 原子 | 无 a 扩展(forced-atomics + no-CAS) |
| build-std | 不需要(目标为 builtin,工具链自带预编译 core/alloc) |
| 工具链仓库 | github.com/hispark-rs/hisi-riscv-rust-toolchain |
| 当前发布 | v1.96.0-2 |
目标三元组写法
riscv32imfc(注意是imfc,含硬浮点f、不含原子a)。CLAUDE.md中出现的riscv32imafc-unknown-none-elf是--target覆盖示例,并非默认目标。
builtin 目标(无需 -Z build-std)
riscv32imfc-unknown-none-elf 在 hisi-riscv 工具链里被烤进为 builtin 目标,工具链随附该目标的预编译 core/alloc,故构建不需要 nightly 的 -Z build-std。工具链是芯片中立的(ws63 + bs2x 都目标 riscv32imfc)。
无原子的处理:原子 load/store 降为普通 ld/st(单 hart);RMW 经 portable-atomic 的 critical-section polyfill。不发射 lr.w/sc.w/amo*。
rust-toolchain.toml
[toolchain]
channel = "hisi-riscv"
该工具链不是可分发的 rustup channel,必须先手动安装并
rustup toolchain link hisi-riscv(见下“安装“)。
.cargo/config.toml
[build]
target = "riscv32imfc-unknown-none-elf"
[target.riscv32imfc-unknown-none-elf]
runner = "gdb-multiarch"
rustflags = ["-C", "link-arg=--no-relax"]
| 字段 | 值 | 说明 |
|---|---|---|
[build] target | riscv32imfc-unknown-none-elf | 默认编译目标 |
runner | gdb-multiarch | cargo run 默认 runner(QEMU/真机 runner 经 env 覆盖,见 硬件 runner) |
rustflags | -C link-arg=--no-relax | 关闭 RISC-V 链接器松弛,匹配厂商 C SDK 流,避免 gp 相对松弛与自定义链接脚本冲突 |
安装(release URL 形态)
按主机选 tarball(linux x86_64/aarch64、macOS x86_64/aarch64、windows x86_64):
curl -fLO https://github.com/hispark-rs/hisi-riscv-rust-toolchain/releases/download/v1.96.0-2/hisi-riscv-rust-1.96.0-x86_64-unknown-linux-gnu.tar.gz
tar xzf hisi-riscv-rust-1.96.0-*.tar.gz
rustup toolchain link hisi-riscv "$PWD/stage2"
release URL 形态:
https://github.com/hispark-rs/hisi-riscv-rust-toolchain/releases/download/<tag>/hisi-riscv-rust-1.96.0-<host-triple>.tar.gz
当前 <tag> = v1.96.0-2;<host-triple> 如 x86_64-unknown-linux-gnu、aarch64-unknown-linux-gnu、x86_64-apple-darwin、aarch64-apple-darwin、x86_64-pc-windows-msvc。链接目标为解压后的 stage2 目录。
应用镜像格式与签名
WS63 app 镜像(flashboot 从 flash 0x230000 加载的对象)的字段布局。事实逐字段取自 hisi-fwpkg/crates/hisi-fwpkg/src/image.rs 与 src/fwpkg.rs。所有多字节字段为小端。
构建命令见 打包成可启动镜像;安全启动原理见 安全启动与签名。
整体布局
+--------------------------------------+ 0x000
| image_key_area_t (0x100 字节) | magic = 0x4B0F2D1E
+--------------------------------------+ 0x100
| image_code_info_t (0x200 字节) | magic = 0x4B0F2D2D
+--------------------------------------+ 0x300 = APP_IMAGE_HEADER_LEN
| code body(原始 .text/.rodata/...) | 链接到 0x230300 运行
+--------------------------------------+
0x300 字节前缀为定长镜像头。secure boot 关闭(efuse SEC_VERIFY_ENABLE == 0)时,flashboot 的 verify_image_* 在检查任何签名/body hash 之前短路成功,故签名字段从不被读。关键只有两点:头恰好 0x300 字节、其后是链接到 0x230300 的真实代码。
常量
| 常量 | 值 |
|---|---|
APP_KEY_AREA_IMAGE_ID | 0x4B0F_2D1E |
APP_CODE_INFO_IMAGE_ID | 0x4B0F_2D2D |
KEY_AREA_LEN | 0x100 |
CODE_INFO_LEN | 0x200 |
IMAGE_HEADER_LEN | 0x300 |
STRUCTURE_VERSION | 0x0001_0000 |
SIG_LEN(BOOT_SIG_LEN) | 0x40 |
KEY_ALG_ECC256 | 0x2A13_C812(ECC256 / brainpoolP256r1) |
ECC_CURVE_BP256R1 | 0x2A13_C812 |
PUB_KEY_LEN(BOOT_PUBLIC_KEY_LEN) | 0x40 |
FLASH_NO_ENCRY_FLAG | 0x3C78_96E1 |
HASH_LEN | 32(SHA-256) |
密钥区 image_key_area_t(偏移 0x000,长度 0x100)
| 偏移 | 字段 | 默认值 |
|---|---|---|
0x00 | image_id(magic) | 0x4B0F_2D1E |
0x04 | structure_version | 0x0001_0000 |
0x08 | structure_length | 0x100 |
0x0C | signature_length | 0x40 |
0x10 | key_owner_id | 1(默认) |
0x14 | key_id | 1(默认) |
0x18 | key_alg | 0x2A13_C812 |
0x1C | ecc_curve_type | 0x2A13_C812 |
0x20 | key_length | 0x40 |
0x24 | key_version_ext | 0(disabled 板) |
0x28 | mask_key_version_ext | 0 |
0x2C | msid_ext | 0 |
0x30 | mask_msid_ext | 0 |
0x34 | maintenance_mode | 0(关闭) |
0x38..0x48 | die_id[16] | dummy 0(仅维护模式检查) |
0x48 | code_info_addr | 0(紧随其后) |
| — | ext_public_key_area[0x40]、sig_key_area[0x40] | dummy 0(ECC 签名/公钥 blob) |
代码信息区 image_code_info_t(偏移 0x100,长度 0x200;下表偏移相对区起点)
| 偏移 | 字段 | 默认值 |
|---|---|---|
0x00 | image_id(magic) | 0x4B0F_2D2D |
0x04 | structure_version | 0x0001_0000 |
0x08 | structure_length | 0x200 |
0x0C | signature_length | 0x40 |
0x10 | version_ext | 0 |
0x14 | mask_version_ext | 0 |
0x18 | msid_ext | 0 |
0x1C | mask_msid_ext | 0 |
0x20 | code_area_addr | 0(紧随头之后) |
0x24 | code_area_len | body.len() |
0x28..0x48 | code_area_hash[32] | body 的真实 SHA-256 |
0x48 | code_enc_flag | 0x3C78_96E1(FLASH_NO_ENCRY_FLAG,未加密) |
0x4C..0x5C | protection_key_l1[16] | 0(加密关闭) |
0x5C..0x6C | protection_key_l2[16] | 0 |
0x6C..0x7C | iv[16] | 0 |
0x7C | code_compress_flag | 0(未压缩) |
0x80 | code_uncompress_len | = code_area_len |
0x84 | text_segment_size | 0x0001_0000(默认,仅信息) |
| — | sig_code_info[0x40] + sig_code_info_ext[0x40] | dummy 0 |
code_area_hash(区内偏移0x28,即镜像绝对偏移0x128)是 body 的真实 SHA-256,与厂商sign_tool一致。
code_enc_flag(区内0x48,绝对0x148)=0x3C7896E1是非零哨兵:flashbootws63_flash_encrypt_config()做if (code_enc_flag == FLASH_NO_ENCRY_FLAG) return;,故零值反而会令 flashboot 尝试配置 on-the-fly 解密而启动失败。明文镜像必须用此值。dummy-zero 字段:两个区的 ECC 签名 blob(
sig_key_area、sig_code_info、sig_code_info_ext)与公钥(ext_public_key_area、die_id、protection key、iv)。
fwpkg V1 容器(fwpkg.rs)
“all-in-one” 固件包:小头 + 每分区描述符表 + 串接的分区负载。布局是 hisiflash 解析器的逆,匹配厂商 packet_create.py create_allinone()。
+----------------------------------+ 0x000
| FWPKG_HEAD (12 字节) | flag(4) crc(2) cnt(2) total_len(4)
+----------------------------------+ 0x00C
| IMAGE_INFO[0] (52 字节) | name[32] off(4) len(4) burn_addr(4)
| IMAGE_INFO[1] ... | burn_size(4) type(4)
+----------------------------------+
| payload[0] || 16 个 0 字节 |
| payload[1] || 16 个 0 字节 |
+----------------------------------+
常量
| 常量 | 值 |
|---|---|
FWPKG_MAGIC_V1(flag) | 0xEFBE_ADDF |
HEADER_SIZE(FWPKG_HEAD) | 12 |
BIN_INFO_SIZE(IMAGE_INFO) | 52 |
NAME_SIZE(名字字段宽) | 32(名字须 < 32 字节) |
PAYLOAD_SEPARATOR | 16(每负载后补的 0 字节数) |
FWPKG_HEAD(12 字节)
| 偏移 | 字段 | 大小 | 值 |
|---|---|---|---|
0x00 | flag(magic) | 4 | 0xEFBE_ADDF |
0x04 | crc | 2 | CRC16/XMODEM |
0x06 | cnt(分区数) | 2 | parts.len() |
0x08 | total_len | 4 | 头 + 全部负载 + 全部分隔 |
IMAGE_INFO(每分区 52 字节)
| 偏移 | 字段 | 大小 |
|---|---|---|
0x00 | name[32] | 32 |
0x20 | offset | 4 |
0x24 | length(负载字节数,不含 16 字节分隔) | 4 |
0x28 | burn_addr | 4 |
0x2C | burn_size | 4 |
0x30 | type | 4 |
分区 type
| 类型 | 值 | 说明 |
|---|---|---|
Loader | 0 | LoaderBoot(一级加载器) |
Normal | 1 | ssb / flashboot / nv / params / app … |
KvNv | 2 | Key-Value NV |
Efuse | 3 | eFuse 配置 |
Other(v) | v | 其它原始类型码 |
CRC
crc = CRC16/XMODEM(poly 0x1021,init 0x0000),覆盖范围为从偏移 6(cnt 字段)到描述符表末尾的字节(即 out[6..head_len],head_len = 12 + cnt*52)。已知向量:crc16_xmodem("123456789") == 0x31C3。
每个负载后跟 16 个 0 字节分隔,计入
total_len但不计入描述符length。
HIL 标记串与环境变量
HIL(hardware-in-the-loop)框架的标记串与环境变量参考。事实取自 hil/hil-smoke.sh、hil/flash.sh、hil/pack.sh、hil/cargo-run-hw.sh。
HIL 框架原理见 HIL 测试框架;运行步骤见 运行 HIL 冒烟测试。
串口约定
| 串口 | 用途 | 参数 |
|---|---|---|
UART0 = /dev/ttyUSB0 | 板子 UART0(示例输出) | 115200 8N1 |
ttyACM0 | J-Link VCOM | — |
hil-smoke.sh 检查的标记串
hil-smoke.sh 逐例烧录后读 UART,用 grep -qE 匹配下列模式(check <example> <egrep> <desc>):
| 示例 | 匹配的 egrep 模式 | 描述 |
|---|---|---|
uart_hello | Hello from WS63 | UART banner(验证 160 MHz 波特基) |
timer_irq | `timer irq # | OK: timer` |
gpio_irq | gpio irq # | GPIO IRQ 投递 |
reset_demo | OK: software reset observed | software_reset + reset_reason(第二次启动标记) |
spi_loopback | SPI loopback OK | 阻塞 SPI0(先短接 MOSI↔MISO!) |
i2c_scan | `scan done | no devices` |
blinky(GPIO 翻转无 UART,需 LED/逻辑分析仪)与 semihost_selftest(需 debugger 半主机)在裸 HIL 跳过。总结果:全过打印 HIL SMOKE: PASS,否则 HIL SMOKE: FAIL 并 exit 1。
各示例标记串(HIL 期望)
| 示例 | 成功标记串 |
|---|---|
uart_hello | Hello from WS63 on QEMU! |
timer_irq | OK: timer interrupts delivered(或周期性 timer irq #N) |
gpio_irq | OK: custom local IRQ (>=32) delivered(或 gpio irq #N) |
reset_demo | OK: software reset observed |
spi_loopback | SPI loopback OK |
i2c_scan | scan done / no devices acked |
blinky | 无(GPIO0 翻转) |
semihost_selftest | 半主机退出码 0 / console semihost_selftest: PASS(裸 HIL 跳过) |
完整 18 例标记串见 示例目录与验证标记串。
环境变量
flash.sh
烧录方式选 METHOD=(默认 probe-rs)。
| 变量 | 默认 | 适用 | 说明 |
|---|---|---|---|
METHOD | probe-rs | — | probe-rs(验证主路径)或 hisiflash(厂商路径) |
CHIP_KIND | ws63 | 共享 | ws63|bs21,决定默认 app 分区地址 |
WS63_RS | 脚本父目录 | 共享 | ws63-rs 检出根 |
CHIP | WS63 | probe-rs | probe-rs --chip 目标 |
PROBE_RS_YAML | (必填) | probe-rs | fork 的芯片描述 YAML(HiSilicon_WS63.yaml) |
BASE_ADDRESS | 0x00230000(ws63)/ 0x00090000(bs21) | probe-rs | app 分区 flash 地址 |
PROBE_RS | probe-rs | probe-rs | probe-rs 二进制名 |
PORT | (自动探测) | hisiflash | 串口(导出为 HISIFLASH_PORT) |
BAUD | hisiflash 默认 921600 | hisiflash | 烧录波特(HISIFLASH_BAUD) |
LOADERBOOT | (必填) | hisiflash | 厂商 LoaderBoot 二进制(取自 fbb_ws63 产物) |
ADDRESS | (必填) | hisiflash | 程序写入 flash 偏移(对照分区表确认) |
HISIFLASH | hisiflash | hisiflash | hisiflash 二进制名 |
hil-smoke.sh(在 flash.sh 变量之外另加)
| 变量 | 默认 | 说明 |
|---|---|---|
PORT | (必填) | 板子 UART0(/dev/ttyUSBx) |
SETTLE | 4 | 每次烧录后读 UART 的秒数 |
UART_BAUD | 115200 | 示例 UART0 波特(8N1) |
MONITOR | (raw read $PORT) | 打印原始 UART 到 stdout 的命令(覆盖适配器读法) |
HISIFLASH | hisiflash | hisiflash 二进制名 |
pack.sh
| 变量 | 默认 | 说明 |
|---|---|---|
CHIP | ws63 | 目标芯片(ws63|bs21),决定 app 分区地址 |
APP_ADDR | (未设) | 覆盖 app 分区 flash 地址(如 0x230000) |
FWPKG | (未设) | 非空则同时产出 .fwpkg(厂商 hisiflash 路径) |
HISI_FWPKG | hisi-fwpkg | hisi-fwpkg 二进制名 |
WS63_RS | 脚本父目录 | ws63-rs 检出根 |
默认 app 分区地址:ws63 0x00230000、bs21 0x00090000。
cargo-run-hw.sh(cargo runner)
cargo 以 <runner> <built-elf> 调用,脚本把 ELF 打包成 0x300-header 镜像、probe-rs download、复位、(设了 PORT 则)流式 UART0。
| 变量 | 默认 | 说明 |
|---|---|---|
APP_ADDR | 0x00230000(ws63) | app 分区 flash 地址 |
PROBE_RS | probe-rs | probe-rs 二进制名 |
PROBE_CHIP | WS63 | probe-rs --chip 值 |
PROBE_YAML | (空 = 内置 DB) | --chip-description-path YAML |
HISI_FWPKG | hisi-fwpkg | hisi-fwpkg 二进制名 |
PORT | (无 = 不流式) | 复位后流式 UART0 的端口 |
UART_BAUD | 115200 | 流式 UART 波特 |
MONITOR | 10 | 流式 UART 秒数 |
启用:
CARGO_TARGET_RISCV32IMFC_UNKNOWN_NONE_ELF_RUNNER=hil/cargo-run-hw.sh cargo run -p blinky --release(或just run-hw)。
CLI 工具速查
本项目工具的命令参考:hisi-fwpkg、补丁版 probe-rs、QEMU、hisiflash。事实取自 hisi-fwpkg-cli/src/main.rs、HIL 脚本、tutorials。
镜像字段布局见 应用镜像格式;烧录步骤见 用 probe-rs 烧录、用 hisiflash 烧录。
hisi-fwpkg
把编译产物(ELF 或裸 bin,按 magic 自动识别)打包成 HiSilicon app 镜像 / fwpkg。
cargo install hisi-fwpkg-cli
hisi-fwpkg image(路线 1 / BS2X)
ELF/bin → app 镜像(0x300 header || body,含真实 body SHA-256)。这是 BS2X 的路线 1 产物:BS2X 暂无 link-time boot-header,构建后用 image 单独生成可启动 .img,再烧到 app 分区。
WS63 走 路线 2:
boot-headerfeature 已把0x300头烤进 ELF(链接期),构建后只需hisi-fwpkg patch-hash <elf>(见下)补 body hash,直接烧裸 ELF——不再有中间.img,也不走image。
| 参数 | 说明 |
|---|---|
<input> | 输入 ELF 或裸 .bin(位置参数) |
-o, --output <PATH> | 输出镜像路径(必填) |
# BS2X 路线 1:
hisi-fwpkg image app -o app.img
hisi-fwpkg patch-hash(路线 2 / WS63)
WS63 的 路线 2 post-link 步骤:原地把裸 ELF(已含 link-time 0x300 头)的 body SHA-256 填回头部。无输出文件、无 .img,补好后直接 probe-rs download <elf> 烧、probe-rs run <elf> 跑。
硅片上 flashboot 始终校验 body hash:即便 efuse
SEC_VERIFY_ENABLE==0(secure-off)也只跳过 ECC 签名,不跳过 hash——所以镜像需要 真实 body hash,没有任何「dummy 签名」能让它启动。patch-hash正是用来填这个真实 hash 的。
| 参数 | 说明 |
|---|---|
<input> | 输入裸 ELF(含 boot-header 烤入的 0x300 头,位置参数;原地修改) |
# WS63 路线 2:
hisi-fwpkg patch-hash blinky
hisi-fwpkg pack
上面的镜像再包进单分区 fwpkg(V1 容器 + CRC),供厂商 hisiflash 烧录。
| 参数 | 默认 | 说明 |
|---|---|---|
<input> | — | 输入 ELF 或裸 .bin(位置参数) |
-o, --output <PATH> | — | 输出 .fwpkg 路径(必填) |
-c, --chip <ws63|bs21> | ws63 | 目标芯片(决定 app 分区地址) |
--app-addr <ADDR> | (芯片默认) | 覆盖 app 分区 burn 地址(接受 0x 十六进制) |
--name <NAME> | app | fwpkg 内分区名 |
hisi-fwpkg pack blinky -o blinky.fwpkg --chip ws63 --name app
probe-rs(补丁版 fork)
需补丁版 fork hispark-rs/probe-rs(branch add-hisilicon-ws63-bs21)——上游 probe-rs 尚无 WS63 target 与 ws63-sfc flash 算法。需 fork 提供的 HiSilicon_WS63.yaml 芯片描述。
本项目用到的子命令与标志:
| 命令 | 用法 |
|---|---|
download(WS63 / 路线 2) | probe-rs download --chip WS63 --chip-description-path HiSilicon_WS63.yaml <elf>(裸 ELF 已含 0x300 头 + patch-hash 补好的真实 body hash) |
download(BS2X / 路线 1) | probe-rs download --chip BS21 --chip-description-path HiSilicon_WS63.yaml --binary-format bin --base-address 0x00090000 <app.img> |
run(WS63 / 路线 2) | probe-rs run --chip WS63 --chip-description-path HiSilicon_WS63.yaml <elf>——just run 的硅片版,烧+跑+抓 RTT/semihosting |
reset | probe-rs reset --chip WS63 --chip-description-path HiSilicon_WS63.yaml |
read | 读内存/外设(调试) |
gdb | 启 GDB stub |
debug | 交互调试 |
| 标志 | 说明 |
|---|---|
--chip <NAME> | 目标芯片(WS63;bs21 用 BS21) |
--chip-description-path <YAML> | fork 的 HiSilicon_WS63.yaml |
--binary-format bin | 仅路线 1(BS2X .img)需要:输入为裸 bin。WS63 路线 2 直接烧 ELF,不加此标志 |
--base-address <ADDR> | 仅路线 1 需要:app 分区 flash 地址(bs21 0x00090000)。WS63 路线 2 的地址由 ELF 内 0x300 头自带,无需此标志 |
调试与读内存细节见 用 probe-rs 调试与读内存。
QEMU
姊妹仓 hisi-riscv-qemu 的 QEMU fork,提供 -M ws63 / bs21 / bs21e / bs22 / bs20 机器。软件在环,无需硅片。
qemu-system-riscv32 -M ws63 -nographic -bios none -kernel <elf>
| 标志 | 说明 |
|---|---|
-M ws63 | WS63 机器模型(另有 bs21/bs21e/bs22/bs20) |
-nographic | 无图形,串口接终端 |
-bios none | 不加载默认固件 |
-kernel <elf> | 加载 ELF |
-semihosting | 启用 RISC-V 半主机(semihost_selftest 必需) |
-serial mon:stdio | 串口复用 stdio + monitor |
-nic user | user netdev(SLIRP,net_ping 需要;默认即 user) |
QEMU 模型原理见 QEMU 模型。
hisiflash
厂商串口/YMODEM 烧录 CLI(@230400)。
cargo install hisiflash-cli
| 命令 | 用法 |
|---|---|
write-program | hisiflash write-program --loaderboot <loaderboot.bin> <program.bin> --address 0x230000 |
info | hisiflash info <out.fwpkg>(静态校验 V1 / 分区 / CRC 结构) |
flash | hisiflash flash <out.fwpkg> |
环境变量:HISIFLASH_PORT(串口)、HISIFLASH_BAUD(烧录波特,默认 921600)。
仓库清单
全部位于 GitHub 组织 github.com/hispark-rs(旧 sanchuanhehe/* URL 已重定向)。
| 仓库 | 一句话 | URL |
|---|---|---|
hisi-riscv-rs | 主 monorepo(crates、examples、guides、SVD 均为子模块) | github.com/hispark-rs/hisi-riscv-rs |
hisi-rs-template | cargo-generate 模板(WS63/BS2X 新工程脚手架) | github.com/hispark-rs/hisi-rs-template |
hisi-fwpkg | app 镜像 / fwpkg 打包工具(image/patch-hash/pack) | github.com/hispark-rs/hisi-fwpkg |
probe-rs(fork) | 补丁版 probe-rs(WS63/BS21 target + ws63-sfc flash 算法) | github.com/hispark-rs/probe-rs(branch add-hisilicon-ws63-bs21) |
hisi-riscv-rust-toolchain | 自定义 rustc(riscv32imfc builtin,硬浮点) | github.com/hispark-rs/hisi-riscv-rust-toolchain |
hisi-riscv-qemu | QEMU fork(-M ws63/bs21/bs21e/bs22/bs20) | github.com/hispark-rs/hisi-riscv-qemu |
hisiflash | 串口/YMODEM 烧录 CLI | github.com/hispark-rs/hisiflash |
原理与背景 · Explanation
这一章不是教你“怎么做“,也不是给你查“叫什么“的索引——它讲的是为什么: 为什么栈这样分层、为什么要一条自定义工具链、为什么一个裸 ELF 在硅片上不会启动、 为什么我们用 UART 里的一行字符串来判定一次硬件测试是否通过。
如果你想动手,去 教程 或 操作指南; 如果你想查一个确切的地址、字段或 API,去 参考。 本章是给你靠在椅背上读的——读完你会理解这套生态为什么长成现在这样, 也就更容易判断某个改动是否合理、某个故障应该往哪个方向想。
概念章节
这些章节自顶向下,把分散在各组件里的事实串成一个整体:
- 系统架构总览——crate 依赖链(svd → pac → hal → 示例,rt 管启动)、
以及那些贯穿全栈的设计取舍:
no_std、用生命周期泛型保证安全、把unsafe寄存器访问 封进驱动、用 sealed trait 锁住扩展点。为什么这样分层。 - 类型化配置:能编译就能在硅片上跑——本项目 HAL API 的头号约定:配置面
用校验 newtype / type-state / 自起时钟收紧,操作面保持 embedded-hal 的
Result, 为什么「能写出来的值就该能跑」、以及 A/B/C/D 缺陷分类法与逐字段决策树。 - 启动流程:mask ROM → flashboot → app——从上电到
main()的整条引导链, 以及为什么“补 0x300 头部、烧到 app 分区“是必须的,为什么一个裸 ELF 不会启动。 - 硬浮点工具链——为什么是一条把
riscv32imfc烤进 builtin 的 自定义 rustc,而不是-Z build-std;hard-float ABI、无原子、code model 的来龙去脉。 - async 与 embassy——阻塞驱动、
asyncfeature、embassyfeature 三者的关系,以及它为什么能在一颗没有原子扩展的核上跑起来。 - 安全启动与签名——为什么开发片把 secure boot 关掉、这意味着什么、 为什么一个全零“假签名“镜像照样能启动、真正签名又需要什么。
- QEMU 模型——hisi-riscv-qemu 为什么存在、它能模拟什么、不能 模拟什么,以及 QEMU 里跑和真硅片上跑的根本差别。
- HIL 测试框架——硬件在环测试的哲学:为什么用 UART 标记串做验证通道、 QEMU↔硅片的几类分歧、以及诚实的当前 bring-up 状态。
组件深入文档
组件深入文档索引 列出了 10 篇逐组件的权威深入文档 (HAL、rt、pac、svd、示例、flashboot、RF、guide、async-embassy)。 本章的概念章节会链接进这些深入文档:概念章讲“为什么、怎么串起来“, 深入文档讲“这一个组件内部到底怎么实现“。两者互补——读概念建立全局,读深入查实现。
系统架构总览
这一篇讲的是为什么这套 Rust 代码长成这个形状——不是逐个 API 的清单(那是 HAL API 总览),也不是逐组件的实现细节(那是 组件深入文档),而是把分层、所有权模型、安全边界这几件事串成一个 能自洽解释的整体。读完你应该能回答:“如果我要加一个外设驱动,它该放在哪一层、长什么样、 为什么不能直接写寄存器。”
一条单向的依赖链
整个库栈是一条严格单向的依赖链,每一层只依赖它下面的一层:
ws63-svd (XML 真值)
│ svd2rust 生成
▼
ws63-pac ── 裸寄存器访问层(~1.5 MB lib.rs,35 个外设的 RegisterBlock)
│
▼
hisi-riscv-hal ── 手写的安全驱动(35 个源文件 + 可选 async/embassy)
│
▼
examples/ws63/* ── 应用
hisi-riscv-rt ── 运行时(启动汇编、链接脚本、中断向量):横切,被示例链接
这条链不是随手画的,它对应着一个明确的抽象递进:SVD 是芯片寄存器的机器可读真值,
PAC 把它机械地翻成 Rust 类型,HAL 在 PAC 之上用人手写出安全、符合 embedded-hal 的驱动,
示例再在 HAL 之上写业务。每往上一层,unsafe 越少、类型越强、离硬件越远。
为什么要这么分?因为这三层的变化频率和变化原因完全不同。SVD/PAC 跟着芯片走,
芯片定了就几乎不动;HAL 跟着 Rust 嵌入式生态(embedded-hal 版本、esp-hal 的模式演进)走;
示例跟着用户需求走。把它们拆开,任何一层换代都不会逼着另外两层跟着改。
逐层的实现细节见各自的深入文档:ws63-svd、
ws63-pac、hisi-riscv-hal、
hisi-riscv-rt、ws63-examples。
为什么 PAC 必须只有一份
有一个容易被忽视、却会在链接期炸掉的约束:全仓库只能链接一个 PAC 实例。
PAC 里的 Peripherals::take() 依赖一个 DEVICE_PERIPHERALS 单例静态——如果链接进两份
PAC(比如一个来自 crates.io、一个来自本地 submodule),这个静态会重复、类型也不兼容。
所以根 Cargo.toml 用 [patch.crates-io] 把 ws63-pac 的 registry 依赖重定向到本地
submodule。这是“单一真值“原则在构建层面的体现:不只是源码单向依赖,连链接出的符号
也必须唯一。
所有权即安全:用生命周期泛型守住外设
HAL 的核心安全模型不是运行时检查,而是借 Rust 的类型系统把“外设被独占使用“编译期化。 机制有三层:
- 外设单例:
Peripherals::take()在 critical-section 保护下只成功一次, 返回一组零大小(ZST)的外设令牌。 - 生命周期参数化:每个令牌是
Peripheral<'d>。驱动构造器消费这个令牌 (Watchdog::new(wdt)),把'd借进驱动。于是“在外设令牌还活着时不能再拿到它“ 被编译器强制——use-after-drop 在编译期就过不了。 - 多实例靠类型区分:UART/I2C/SPI/DMA 这些有多个实例的外设,用
PhantomData<&'d T>+ 每实例构造器(new_uart0/new_uart1)把实例编进类型, 避免“两段代码同时以为自己独占 UART0“。
这套模式直接借鉴了 esp-hal——不是为了好看,而是因为它把资源冲突这类最难调试的嵌入式
bug 挡在了编译期。代价是 API 略繁(不能用统一的 new()),但换来的是“能编译过就不会有两个
驱动抢同一个外设“。
unsafe 的边界:把它关进驱动里
裸寄存器访问本质上是 unsafe 的——你在往任意物理地址写值,编译器无从知道这是否合法。
这套架构的处理方式不是消灭 unsafe,而是把它收敛:
- PAC 层暴露的
reg.write(|w| w.bits(val))是unsafe的; - HAL 的每个驱动方法在内部
unsafe { ... }这一句,外部 API 全是安全的; - 应用层(示例)完全不写
unsafe。
也就是说,unsafe 被压缩成 HAL 里一条条短小、可审计的语句。一次架构评审
(见各组件文档里的“评审发现“)的隐含目标就是:让每一处 unsafe 都对应一个经人核对过
寄存器手册的写入,而不是散落在应用代码里无人负责。
sealed trait:留扩展点,但不让外人乱接
HAL 用了一批 sealed trait(private.rs 里的 Sealed 超 trait):DmaWord、
PeripheralInput、PeripheralOutput 这些 trait 外部 crate 实现不了。这是有意的——
这些 trait 表达的是“哪些类型是合法的 DMA 字宽 / 合法的引脚功能“,它们的完整集合由硬件
决定,不该让下游随便加。sealed 让 HAL 可以放心地用这些 trait 做编译期约束
(比如 DmaChannelFor<P> 保证某外设只能配对它真正支持的 DMA 通道),而不必担心
有人实现出一个硬件根本不支持的组合。
贯穿全栈的几个决定
有几条决定不属于某一层,而是整个栈共享的前提:
#![no_std]:无堆、无Vec、无String。需要缓冲就用定长数组。这不是洁癖—— WS63 是资源受限的裸机环境,引入分配器会带来确定性和体积代价,而嵌入式代码几乎总能用 定长缓冲解决。- 目标是
riscv32imfc-unknown-none-elf(硬浮点 ilp32f,无原子),由自定义hisi-riscv工具链提供。为什么是它而不是软浮点、为什么是自定义工具链而不是-Z build-std——这件事本身就是一篇 硬浮点工具链。 - 无原子怎么办:这颗核没有 A 扩展,
lr/sc/amo会陷入。所以 RMW 原子全部走portable-atomic的 critical-section polyfill,hisi-riscv-rt提供critical-section-single-hart实现。这一条让 async/embassy 能在这颗核上跑—— 详见 async 与 embassy。 - 多芯片:同一套 HAL 用
chip-ws63/chip-bs21feature 二选一区分, 条件编译外设模块。WS63 含 Wi-Fi 相关,BS2X 含 GADC/KEYSCAN/QDEC/RTC/TRNG 等 M1 外设。
这套架构想达到的最终目的
把上面几条放在一起看,会发现它们都服务于同一个目标:让“写应用“这一层完全安全、完全
no_std、完全不碰 unsafe,同时不牺牲对硬件的精确控制。精确控制被压进 PAC(机械生成、
对着 SVD)和 HAL 的 unsafe 短句;安全被生命周期和 sealed trait 守住;启动和链接这些
最底层、最容易出错的事被隔离进 hisi-riscv-rt。
而这套栈服务的北极星是连接性(WS63 的 Wi-Fi / BLE / SLE)。分层之所以值得, 是因为连接性那一层(RF blob 的 porting)最复杂、最容易把下面搅乱——清晰的分层正是 为了在引入那层巨大复杂度时,下面的 PAC/HAL/rt 不被污染。连接性的可行性与现状见 ws63-RF 深入文档 与 HIL 框架。
类型化配置:「能编译就能在硅片上跑」
这是本项目 HAL API 的头号约定:配置面被设计成 ——你能写出来的值,就是能在真硅片上跑起来的值。不存在「编译通过、却被静默 clamp / 截断 / 没接时钟」的参数。
本篇讲为什么这样设计、它怎么和 embedded-hal 分层、以及落地时该怎么判断。配方见 如何新增一个外设驱动;仓库内的可调用清单见 .claude/skills/typed-config/(含缺陷分类法 + 候选扫描器)。
问题:能写出但跑不了
很多嵌入式 HAL 的配置接口会接受一个结构上合法、却在硅片上跑不通的值,而且不报错:
- 算出来的分频/周期/计数超过寄存器位宽 → 被静默掩码截断,频率/波特悄悄错;
- 角色与分频的组合合法但不产生时钟(比如 I2S Master 配了零分频);
- 需要一个没被强制的前提(时钟门没开、板上没焊晶振、模拟 AFE 没上电);
- 越界被悄悄
.clamp()/saturating_/if x == 0 { 1 }掉,而不是报错。
这类 bug 的代价极高:编译、烧录、上板,全过,但行为是错的,且无任何信号 —— 往往要拿逻辑分析仪或半天 debug 才发现。本约定的目标就是把这类错误从「上板才暴露」提前到「根本写不出来」。
两层:config 面收紧,操作面保持 embedded-hal
关键边界:出问题的是配置面,而 embedded-hal 的 trait 根本不约束配置面。 所以两层互不打架。
| 层 | 谁 | 规则 |
|---|---|---|
| 配置 / 构造 | HAL 自有方法(Config、new*、configure、set_*)—— 不是 embedded-hal | 随便上类型。 校验 newtype + 可失败构造;角色用 type-state;驱动自起时钟门。 |
| 操作 | embedded-hal traits(SetDutyCycle、SpiBus、I2c、Read/Write、DelayNs…) | 签名写死(u16/&[u8] + Result)。Result 就是 embedded-hal 表达「非法输入」的官方手段。不要改 trait 方法签名。 |
为什么操作面只能 Result:embedded-hal 1.0 的 trait 方法必须 fallible(见下「参考」),set_duty_cycle(u16) 这种签名你改不了 —— 越界就返回 Err,这是它的惯例,不是妥协。而配置/构造方法是 HAL 自有的 inherent 方法,不归 trait 管,可以放手编译期化。
缺陷分类法(给每个配置字段定级)
- A —— 寄存器位宽溢出。 算出的值比硬件字段宽,被静默掩码/截断(
& 0xFFFF、as u16)。 - B —— 合法但死的组合。 结构合法却不产生可用时钟/输出(如零分频的 I2S Master)。
- C —— 未强制的前提。 必须先开的时钟门、必须焊的晶振 / 上电的模拟 AFE、XIP-unsafe 上下文。
- D —— 静默 clamp/wrap。 越界被悄悄夹/饱和/
if x==0 {1},而不是报错。
决策树:每类字段怎么收
- 频率 / 波特 / 周期 / 超时(从运行时值算出来的)→ 校验 newtype +
const fn try_from_hz(u32) -> Option<Self>/from_count/try_new,越出可达寄存器范围就返回None。拒绝,不要 clamp。(治 A、D) - 角色相关配置(合法字段取决于模式)→ type-state:需要额外参数的那个状态在构造函数里强制要求它,非法组合在类型上不可表达。(治 B)
- 小的有限选择 → enum(本就装不下非法值;除非现在是裸整数)。
- 时钟门没开 → 驱动在
configure/new里自起自己的时钟门(照搬 vendor*_porting的 CKEN + DIV_CTL 分频 + LOAD_DIV 序列)。(治 C) - 板级/模拟前提(RTC 32 kHz 晶振、ADC AFE/LDO 上电)类型治不了 → doc + 守护:命名明确 /
cfg/ feature 门控的构造,有界轮询(绝不用会拖死总线的无界轮询),加一行# 硬件要求文档。(治 C) - 本就是全宽 32 位寄存器 / 本就是 enum → 不动。 不要无中生有造约束,只收真缺陷。
类型编码的是实测硅片现实,不是手册
最有教育意义的一例:pwm::PwmPeriod 是 u16,因为 WS63 的 pwm_freq_h 高 16 位在硅片上根本不存值(实测:写 0x0001 读回 0,即便整条时钟树都拉起来),而 vendor regs_def 明明声明这个字段是 32 位。类型编码实测行为,而不是数据手册。 如果某字段的真实范围拿不准,先上板量,再定类型边界 —— 别只信 PAC/SDK 的位宽。
落地流程(docs-first)
- 先改文档 —— 本约定要求 docs-first:先更新该驱动的组件文档 + 本页 + ROADMAP,再写代码。
- 扫候选:
bash .claude/skills/typed-config/scan.sh crates/hisi-riscv-hal/src/<driver>.rs。 - 追到寄存器:从 PAC 拿字段真实位宽,从 vendor SDK 拿有效范围 + 时钟前提,标
file:line。 - 定级 + 选方案(决策树),只动配置层,embedded-hal trait impl 的签名不碰。参考
pwm.rs。 - 更新测试:host 单测/property(newtype 的接受/拒绝边界)+
tests/hil.rs。 - 上板验证(硅片佐证):寄存器/轮询级事实可上板确认;示波器级行为(真实波形)和板级前提(RTC 晶振)不能 —— 如实说明。
参考实现与依据
- 参考实现:
crates/hisi-riscv-hal/src/pwm.rs——PwmPeriod(u16,from_count/try_from_hz)、Duty(0..=100)、configure自起时钟树、保留SetDutyCycle。 - 仓库约定:
CLAUDE.md的「Typed config — if it compiles, it runs on silicon」一节 +.claude/skills/typed-config/skill。 - 业界依据:
- esp-hal API 准则:「prefer compile-time checks over runtime checks; prefer a fallible API over panics」—— 本 HAL 本就仿照 esp-hal。
- Parse, don’t validate(Alexis King):只给可失败构造,值要么解析成功要么不存在。
- Typestate pattern(The Embedded Rust Book):把运行时状态编码进编译期类型,零运行时开销。
启动流程:mask ROM → flashboot → app
这一篇回答一个看似简单、却让很多人第一次烧 WS63 时困惑的问题:为什么我 cargo build
出来的那个 ELF,直接烧上去不会跑? 答案藏在一整条引导链里——从上电的第一条指令,到你的
main() 拿到控制权,中间隔着好几道关卡,每一道都对镜像格式有要求。理解这条链,
你就理解了为什么必须“打包 + 烧到特定地址“(操作步骤见
打包成可启动镜像 与 用 probe-rs 烧录,
确切地址见 内存映射)。
整条链:四级接力
WS63 上电后,控制权像接力棒一样在四个阶段间传递,每一级都把芯片往“能跑应用“的状态推一步:
上电
│
▼
① mask ROM @ 0x100000 复位向量 `j 0x100024`,固化在硅片里、不可改
│ 最底层 bring-up,随后把控制交给 flash 里的 loaderboot
▼
② loaderboot 一级引导:最早的时钟/外设 bring-up、烧录通道(YMODEM)
│
▼
③ flashboot 二级引导:时钟切到 PLL、SFC 初始化、(可选)校验镜像,
│ 然后【无条件】跳到 app 分区 + 0x300
▼
④ app @ 0x230300 你的 Rust 程序,从 0x300 头部之后的入口开始
hisi-riscv-rt 的启动代码接管 → 最终调用 main()
每一级“为什么存在“都不一样:mask ROM 解决“硅片上电后第一条指令从哪来“, loaderboot/flashboot 解决“flash 里的东西怎么被搬起来跑、镜像合不合法“, 而 app 这一级才是你的代码。前三级里有两级(mask ROM、app ROM)是厂商固化的 ROM—— 它们的真实内容是从硅片上读出来的 dump,专有、仅本地可见,不会进仓库; 我们对它们的理解来自对照 fbb_ws63 C SDK 和实测行为。
① mask ROM:硅片里固化的第一步
复位时 PC 落在 mask ROM 的 0x100000,那里是一条 j 0x100024——跳过最前面几个字的头部,
进到真正的 bring-up 代码。这段代码无法修改(它就是硅片的一部分),职责是把芯片从“刚上电、
什么都没配“的状态拉到“能从 flash 取下一级“的状态。除了 0x100000 的 mask ROM,
还有一块 app ROM @ 0x109000——厂商固化的运行时支持例程(C SDK 的某些底层函数会调到它)。
对纯 Rust 的裸机应用来说,app ROM 基本不在路径上;但理解连接性(RF blob)时它很关键,
因为厂商协议栈会跳进这些 ROM 地址——这正是 blob 难以脱离真硅片的原因之一
(见 ws63-RF 深入文档)。
②③ loaderboot 与 flashboot:把镜像搬起来、跳进去
loaderboot 是一级引导,flashboot 是二级引导。对“跑一个 Rust 应用“这件事, 最关键的是 flashboot 的最后一跳:
flashboot 无条件跳到
app 分区起址 + 0x300。WS63 的 app 分区在 flash 的0x230000,所以入口固定是0x230300。
注意“无条件“三个字——flashboot 不去解析 ELF 头、不去找 entry point、不做任何重定位。
它只是把 PC 设到 0x230300 然后一跳了事。这就直接解释了下一节那个核心问题。
仓库里有一个实验性、学习用途的 Rust 版 flashboot(chips/ws63/flashboot),
它对照原厂 flashboot_ws63 重写了这条流程:汇编启动(PMP 清零、mtvec、开 FPU、清 BSS)、
时钟从 TCXO 切到 PLL、SFC 四线读初始化、镜像头边界校验 + 软件 SHA-256 完整性校验,
最后 transmute 到 addr + 0x300 跳进去。它有意不依赖 PAC/HAL(裸 MMIO),
以免链接进第二份 PAC。生产上不该用它——生产应复用原厂 flashboot,
它有真实签名验签、A/B 槽、FOTA、解压。详见 flashboot 深入文档。
为什么 0x300 头部必须存在——以及为什么裸 ELF 不会启动
把上面两件事拼起来,答案就清楚了:
- flashboot 无条件跳到
app 分区 + 0x300; - 它不解析 ELF。
所以 app 分区开头那 0x300(768)字节必须是一段 HiSilicon 镜像头——一个 0x100 字节的
KeyArea(签名/密钥区)加一个 0x200 字节的 CodeInfo(含 body 长度、body 的 SHA-256 等)。
flashboot 跳到 +0x300 时,正好落在这段头部之后、也就是你程序真正的第一条指令上。
如果你把 cargo build 出来的裸 ELF 直接写到 0x230000,会发生什么?
flashboot 照样无条件跳到 0x230300——但那里现在是 ELF 文件里偏移 0x300 处的某段数据
或节内容,不是入口指令。PC 落在一堆并非代码的字节上(或者 SRAM 残留),于是跑飞。
你的程序明明被烧进去了,却一条指令都没执行到。
这就是为什么必须用 hisi-fwpkg 打包:它把 ELF/bin 转成
“0x300 头部 + body“的镜像,把入口对齐到 +0x300,并把 body 的 SHA-256 填进 CodeInfo。
头部各字段的精确布局见 应用镜像格式与签名。
XIP:app 直接在 flash 里执行
还有一个值得理解的点:WS63 的应用是 XIP(execute in place) 的——代码段不被搬进 RAM,
而是直接从 flash 的 XIP 窗口(映射在 0x200000 区域)取指执行。app 分区 0x230000
就落在这个窗口里。这意味着 flashboot 跳进 0x230300 后,CPU 是直接对着 flash 取指的,
SFC(flash 控制器)必须已经被初始化成可读状态——这正是 flashboot 在跳转前要做 SFC 四线读
初始化的原因。
④ app:hisi-riscv-rt 接管
控制权落到 0x230300 你的程序入口后,并不是直接进 main(),而是先经过
hisi-riscv-rt 的启动序列。这段代码做的是每个裸机 Rust 程序
都需要、但又必须按 WS63 实际情况定制的事,大致顺序:
- PMP 清零——把物理内存保护配成不挡路(否则后续访问可能陷入);
- 设置
mtvec——安装中断/异常向量基址(向量化模式); - 初始化
gp/sp——gp用于 linker relaxation 的全局指针寻址,sp指向栈顶; - 栈染色(stack paint)——往栈区填已知图案,便于事后测高水位 / 检测溢出;
runtime_init——把.data从 flash 拷到 RAM、清.bss,让静态变量就位;- 调用
main()——到这里你的代码才真正开始跑。
这套序列为什么不能省、为什么 gp/sp/PMP 这些必须由 rt 而不是应用来做,
属于 rt 这一层的职责;它的链接脚本(memory.x / layout.ld)如何把段摆到正确地址、
又如何把脚本传播给下游的 bin,见 hisi-riscv-rt 深入文档。
与 QEMU 的差别:为什么 QEMU 里裸 ELF 反而能跑
一个会让人困惑的对照:在 QEMU 里,你直接 -kernel blinky.elf
就能跑,根本不需要 0x300 头部、不需要 flashboot。这不矛盾——QEMU 用 load_elf()
解析 ELF 并按 ELF 的物理地址落段,再把复位向量设成 ELF 的 entry。也就是说 QEMU
替你做了“理解 ELF、跳到正确入口“这件 flashboot 不做的事。
所以记住这条分界:QEMU 跑的是裸 ELF(无头部、无 flashboot、时钟取标称值);真硅片跑的是 带 0x300 头部、烧到 app 分区、经 flashboot 跳入的镜像。 这正是 QEMU 能验证逻辑、却验证不了 “镜像格式 / 引导链 / 真实时钟“的根本原因——详见 QEMU 模型 和 HIL 框架。
硬浮点工具链
这一篇解释一个看起来“过度工程“的决定:为什么 WS63 要用一条自定义的 rustc——把
riscv32imfc-unknown-none-elf 烤进 builtin 的 hisi-riscv 工具链——而不是用现成的
stable rustc 加 -Z build-std? 这背后串着三个互相牵连的约束:硬浮点 ABI、没有原子扩展、
以及 code model。逐项的安装与版本细节见
工具链与编译目标 和
安装 hisi-riscv 工具链;这里只讲为什么是这条路。
这颗核到底是什么
WS63 的核是 RV32IMFC:
- I(基础整数)、M(乘除)、C(压缩指令)——常规;
- F(单精度浮点)——有硬件浮点单元;
- 没有 A(原子扩展)——
lr.w/sc.w/amo*这些指令会陷入非法指令。
这两个非常规点(有 F、没有 A)合起来,把“选哪个编译目标“这件事从“随手挑个标准 target“ 变成了一道需要权衡的题。
为什么硬浮点(ilp32f ABI)
既然硅片有 FPU,最自然的选择就是让它用起来——也就是 ilp32f ABI:浮点参数走浮点
寄存器、浮点运算发真正的 f* 指令,而不是软件模拟。软浮点(ilp32)当然也能跑(编译器把
f32 运算翻成调用 libgcc/compiler-builtins 里的软件例程),但那是在一颗有 FPU 的核上
白白浪费硬件、还更慢更大。
但硬浮点 ABI 的真正分量不只在性能。ilp32f 是一条 ABI 边界——用 ilp32f 编的代码和用 ilp32 编的代码不能直接链接(浮点参数的传递约定不同)。而 WS63 的北极星是连接性, 连接性意味着最终要和厂商的闭源 blob 链接,那些 blob 是用厂商 gcc 按 ilp32f 编的。 所以选 ilp32f 不仅是“用上 FPU“,更是“为了将来能和 vendor blob 在同一个 ABI 上对接“ ——这是阶段 3(blob 链接)的前置条件。这一层动机,使硬浮点从“优化“升级成“必需“。
为什么没有原子是个真问题
RV32IMFC 缺 A 扩展,意味着任何会发 lr/sc/amo 的代码在硅片上都会陷入。
而 Rust 的 core::sync::atomic 默认假设有原子指令。历史上一度用过 riscv32imafc
(带 A)作为权宜——但那会让编译器发原子指令、在真硅片上触发非法指令陷阱,所以被弃用。
正解是两段配合:
- 目标本身声明为无原子——用 forced-atomics + no-CAS 配置,让原子 load/store 降级成
普通
ld/st(单核下这是安全的),而需要 RMW(compare-and-swap 之类)的操作不发 原子指令; - RMW 走 polyfill——
portable-atomic(开critical-sectionfeature)把 CAS 实现成“关中断 → 读改写 → 开中断“的临界区,hisi-riscv-rt提供critical-section-single-hart这个单核实现。
这套机制正是 async/embassy 能在这颗核上跑的地基(见 async 与 embassy)。 它和“用不用自定义工具链“正交——但目标必须被正确声明为无原子,否则 polyfill 也救不了, 编译器照样会在别处发出原子指令。
核心抉择:自定义 builtin target,还是 -Z build-std?
到这里问题收敛成:我们需要一个标准 rustc 里没有的目标(riscv32imfc,硬浮点、无原子)。
Rust 提供两条路拿到一个非标准 target,二者是真正的取舍:
路线 A:-Z build-std(用现成 stable rustc + nightly 特性)
写一个 *.json 自定义 target spec,然后让 cargo 用 -Z build-std 从源码现编 core/alloc。
- 好处:不用自己造工具链,跟着官方 rustc 走。
- 代价:
-Z build-std是 nightly-only 的不稳定特性。整条工具链就被钉死在 nightly 上——nightly 每天变、偶尔回归,CI 的可重现性变差,用户也得装 nightly + rust-src。 对一个要给别人用、要长期维护的嵌入式 SDK,“必须 nightly“是个不小的负担。
路线 B:自定义 rustc,把 target 烤成 builtin(现在走的路)
构建一条 hisi-riscv 工具链:一个 stable rustc,但在编译它的时候就把
riscv32imfc-unknown-none-elf 这个 target spec 编进 rustc 内部成为 builtin,
并预编译好 core/alloc 一起分发。
- 好处:用户拿到的是一条稳定、自带预编译 core/alloc 的工具链,
.cargo/config.toml里设好默认 target 就行,完全不需要-Z build-std、不需要 nightly。cargo build直接出 RV32IMFC ilp32f 固件,可重现、好分发。 - 代价:得自己维护这条工具链——跟 rustc 版本、出多平台预编译包、走自己的 CI。 这是实打实的工程量,也是这套生态接受的那笔账。
权衡的结论很清楚:用户体验和可重现性 > 维护方自己省事。对一个嵌入式 SDK,“装好工具链
就能稳定 cargo build“远比“维护方不用管工具链、但每个用户都得忍 nightly“更值。
所以选了 B。工具链通过 rust-toolchain.toml pin 住 channel = "hisi-riscv",
用 rustup toolchain link 接进 rustup。
code model:medlow 还是 medany
还有一个容易被忽略、但在裸机上会真出问题的旋钮:code model,它决定编译器怎么寻址 全局符号。
- medlow:假设代码和数据都落在地址空间低 2 GiB 以内,用更短的寻址序列。
- medany:用 PC 相对寻址,可以放在地址空间任意位置,序列略长。
WS63 的地址布局把外设、flash、SRAM 散布在很高的地址(比如外设在 0x4400_0000 一带、
SRAM 更高),全局符号未必落在低 2 GiB。所以这条工具链用 medany——这样不管链接脚本把
段摆到哪个高地址,PC 相对寻址都能正确指到。如果误用 medlow,链接期或运行期会因为
“地址放不进 medlow 的寻址范围“而出错。这件事和硬浮点、无原子一样,是“WS63 的地址空间
不像教科书 RISC-V“逼出来的细节。
一段不算短的历史
这条路不是一步到位的:
- 2026-05-31,阶段 0:先用 stable rustc 里已有的 builtin
riscv32imc(软浮点、 stable、免 build-std)做过渡——目的是先让整条构建/链接跑通,把“无原子 + critical-section“ 这套机制验证出来。 - 随后切到硬浮点工具链:为了和 vendor blob 的 ilp32f ABI 对齐(阶段 3 的前置),
把目标换成
riscv32imfc,并为此造了hisi-riscv工具链。
理解这段历史有助于读懂仓库里偶尔还能见到 riscv32imc 字样的地方——那是过渡期的遗存,
现在的默认与正解是 riscv32imfc + hisi-riscv 工具链。
这件事对其他部分的影响
值得强调的是:异步/embassy 这块完全不在乎工具链是否上游。异步只依赖
portable-atomic + critical-section,与“target 是 builtin 还是 build-std“正交。
真正被自定义工具链“绑住“的是上游化——只要还依赖自定义 rustc,hisi-riscv-hal 就难以
进 embassy 那种“基于标准 stable target 构建“的 in-tree CI。所以“摆脱自定义工具链“
(短期改用标准 target + build-std,长期推 target 进 rustc 主线)被列为一条独立的上游化
工作线,详见 async 与 embassy 深入文档 里的上游化讨论。
async 与 embassy
这一篇是异步故事的概念总览——它讲三种用 HAL 的方式(纯阻塞、async feature、
embassy feature)各自是什么、为什么并存、以及一个反直觉的事实:这一切跑在一颗连原子
扩展都没有的核上。 想看代码在哪个文件、每个 trait 怎么实现、怎么上游化,去
async-embassy 深入文档;这里建立的是全局直觉。
同一套 HAL,三种用法
hisi-riscv-hal 默认是一套阻塞驱动(符合 embedded-hal 1.0)。在它之上,
两个 feature 叠出了异步能力,于是同一套驱动有三档用法:
- 不开任何 feature——纯阻塞。
uart.write()就在那儿自旋等 FIFO 有位。 简单、确定、没有执行器、没有 waker。绝大多数简单固件用这档就够。 - 开
asyncfeature——中断 + waker 驱动的.await。多了embedded-hal-async/embedded-io-async的实现(DelayNs、digital::Wait、SpiBus、I2c、UART 的Read/Write),外加一个极小的block_on执行器和一个IrqSignal桥。让你“不上 embassy 也能.await“。 - 再开
embassyfeature——完整的 embassy 时间生态。多了一个 embassy-time 的Driver,于是embassy-executor(platform-riscv32)能跑,Timer::after/Instant/Ticker都可用。
这三档不是三套代码,而是同一套阻塞驱动上逐层叠加。这个分层本身是个设计取舍: 不想要异步复杂度的人完全感知不到它的存在,想要的人按需开 feature。
async feature:两块地基
block_on + IrqSignal
async 这一档的核心是两个极小的零件:
block_on(fut)——一个最朴素的 future 执行器:poll,遇到Pending就wfi休眠,硬件中断把核唤醒后再 poll。没有堆、没有全局执行器、没有任务队列。它存在的意义 正是“轻“——给只想偶尔.await一下、不愿背上 embassy 全套的场景。IrqSignal——一座“ISR → future“的桥:一个AtomicBool(fired 标志)加一个停在critical_section::Mutex里的Waker。中断里调signal(),future poll 时检查 fired、登记 waker。这是把“硬件中断这件异步的事“接到 Rust async 模型里的接缝。
一个关键克制:不抢中断向量
这套异步驱动有一条很重要的设计纪律——它不自动安装 ISR、不抢占中断向量。每个驱动只
导出一个 on_interrupt 钩子(timer::on_interrupt、gpio::on_interrupt、
uart::on_interrupt……),由应用自己的 trap 处理函数按 mcause 把中断路由过去。
为什么这么设计?因为 Rust 的 cargo 工作区会把 feature 并集——只要工作区里有一个 crate
开了 async,整个工作区都可能被打开。如果异步层一旦被开启就默认安装 ISR,那它会悄悄
改变那些根本没打算用异步的固件的中断行为。“只导出钩子、由应用显式路由“保证了:
开不开 async feature,对非异步固件的行为零影响。这是一条“不给用户埋雷“的边界。
为什么能跑在没有原子的核上
这是最反直觉的一点。WS63 是 riscv32imfc——没有 A 扩展,lr.w/sc.w 会陷入
(详见 硬浮点工具链)。而异步执行器、waker 这些东西通常被认为
“当然要原子操作”。它怎么还能跑?
三件事让它成立:
- HAL 一直走
portable-atomic+critical-section。需要 CAS 的地方由portable-atomic用临界区 polyfill 实现,hisi-riscv-rt提供单核的critical-section-single-hart。 - embassy-executor 本身就支持无 CAS 目标。它内部按编译期
cfg在core::sync::atomic和portable_atomic之间切换——这是它早就为thumbv6m(Cortex-M0,同样无 CAS)准备好的能力。riscv32 平台模块里的SIGNAL_WORK只用 load/store(这颗核支持),不需要 CAS。所以无需改 embassy 一行。 - 一个真实踩过的坑值得记一笔:
target/里陈旧的 host proc-macro 工件会让 embassy 宏构建莫名失败——cargo clean后全量通过。这不是逻辑问题,是构建缓存问题。
也就是说,“无原子“在这里没有变成异步的拦路虎——它早被 portable-atomic +
critical-section 这层垫片吸收掉了,而 embassy 恰好已经为这种核留好了路。
embassy feature:让 WS63 成为时间提供者
embassy feature 做的事可以一句话概括:让 WS63 成为 embassy-time 的时间源。
具体是实现一个 embassy-time Driver:
now()读 TCXO 的 64 位自由计数器(24 MHz),缩放到 embassy-time 的 1 MHz tick。单调、跟随真实(QEMU 上是虚拟)时间流逝。schedule_wake(at, waker)把 waker 入队,并用一个 TIMER 通道编程一次性闹钟。- 闹钟 IRQ 触发时排空到期 waker、重新武装下一个截止时间。
这里有个和 HIL 框架 直接相关的细节:时间的真值来自 TCXO(24 MHz),
不是 PLL(240 MHz)。如果时间源算错了时钟基,所有 Timer::after 都会偏 10×——
这正是 QEMU 验证不了、必须上板验的那类时钟假设。
这一档该用哪个
把三档放在一起,选择其实很自然:
- 简单顺序逻辑、不在乎并发 → 纯阻塞。
- 想
.await个别 IO、不想背 embassy →async+block_on。 - 要多任务、要
Timer::after、要 embassy 生态 →embassy。
覆盖范围、每个 trait 落在哪个文件、以及“为什么走 esp-hal 那种 out-of-tree 上游模型而不是 塞进 embassy monorepo“这些更深的讨论,都在 async-embassy 深入文档 里。那篇是权威;本篇负责让你先有 全局图景。
安全启动与签名
这一篇解释一个让很多人第一次成功烧片时困惑的事实:为什么一个签名字段全是零的“假“镜像, 居然能在真硅片上启动? 答案是——开发片的 secure boot 是关掉的。理解这一点, 既能让你明白当前开发流为什么这么顺,也能让你清楚这条流绝不是生产流、以及生产要补什么。 镜像头里签名/哈希字段的精确布局见 应用镜像格式与签名; 本篇讲“为什么“。
secure boot 想解决的问题
安全启动要回答一个问题:flashboot 凭什么相信它即将跳进去的那段 app 是可信的、 没被篡改的? 在一个开了 secure boot 的设备上,flashboot 在跳转前会做两件事:
- 真实性验签——用固化在 efuse 里的根密钥,对镜像头里的 ECC(bp256)/ SM2 数字签名做验证。只有用对应私钥签过的镜像才通过。攻击者就算能写 flash, 也伪造不出一个能通过验签的签名(他没有私钥)。
- 完整性校验——对 app body 算哈希,和签过名的头里的哈希比对,确保 body 没被改。
关键在第 1 点:真实性靠的是“攻击者拿不到私钥“,这是密码学保证。光有完整性(哈希) 是不够的——因为哈希就在镜像头里,能写 flash 的人改完 body 重算哈希写回头部即可绕过。 没有验签的“安全启动“根本不安全。
开发片为什么把它关掉
WS63 用一个 efuse 位 SEC_VERIFY_ENABLE 控制是否启用安全启动。
在开发片上这个 efuse 位是 0,意味着:
flashboot 跳过 ECC/SM2 签名验签,但仍然校验 body 的 SHA-256 哈希,校验通过才跳进 app。
于是引导链对镜像头的签名字段不做检查——但哈希字段照样比对。这就是为什么——
- 签名区全零的镜像能启动,但 body 哈希必须是真的:打包时签名区填的是全零的占位符 (dummy zero signature),flashboot 既然不验签,全零的签名照样放行;但镜像头里的 body SHA-256 哈希必须算对——secure-off 只跳过 ECC 签名,不跳过 hash,哈希对不上同样拒绝启动。 所以根本不存在某个“假签名“能让任意 body 启动;需要的是 0x300 头 + 真实 body SHA-256。 镜像头的结构当然也要正确(0x300 字节、KeyArea + CodeInfo、body 的 SHA-256 字段位置对——见 启动流程),只是签名的内容无所谓。
- 整条开发流因此非常顺:WS63 走 route 2——靠 hisi-riscv-rt 的
boot-headerfeature 在 link 时把 0x300 头烤进 ELF,再用hisi-fwpkg patch-hash <elf>补上真实的 body SHA-256 (这一步不可省:secure-off 仍校验 hash,只跳过 ECC 签名),然后直接probe-rs download <elf>/probe-rs run <elf>→ reset,就启动了。没有中间.img、 也没有hisi-fwpkg image这一步。不需要任何私钥、不需要厂商签名工具。 (BS21/BS2X 暂无 link-time 头,仍走 route 1:hisi-fwpkg image -o app.img <elf>后烧.img。)
这是一个有意的、便利的开发态选择:开发片关掉验签,让 Rust 固件能自由迭代烧录, 不被“每次都得找厂商签名“卡住。
efuse 是一次性的——这件事的份量
要理解为什么“开发片关、量产片开“是条单向门,得知道 efuse 是一次性可编程(OTP)—— 熔丝只能从 0 烧成 1,烧了不可逆。所以:
- 开发片出厂时
SEC_VERIFY_ENABLE == 0,安全启动关; - 一旦在量产环节把这个位烧成 1,这颗片子就永久进入“必须验签才启动“的状态, 回不去了。从那以后,只有用对应私钥签过的镜像才能在它上面跑。
这也是为什么仓库里那个实验性 Rust flashboot 只做完整性校验、明确标注“非真实性验签“ (见 flashboot 深入文档):它对着同一份未签名的头里的哈希 比对——这在关掉 secure boot 的开发片上够用,但绝不是 secure boot,文档里如实写明了 这一点,不假装安全。
真正签名需要什么
如果要把固件跑在一颗开了 secure boot 的量产片上,dummy 全零签名就过不了了,需要:
- 厂商闭源的
sign_tool——用真正的私钥对镜像生成 ECC-bp256 / SM2 签名,填进 KeyArea。 这个工具是厂商闭源的,不在本仓库、也不可能在本仓库重写(重写一个签名工具没有意义—— 没有对应的、烧进 efuse 的根密钥,签出来的东西在那颗片子上也过不了)。 - 因此生产推荐复用 fbb_ws63 原厂 flashboot 的完整签名/打包/烧录流程——它有真实验签、 A/B 槽、FOTA、解压、flash 在线加密。本仓库的 Rust 应用在生产里应作为被原厂 flashboot 加载的 app 镜像,按原厂流程签名后烧到 app 分区。
安全含义:别把开发流当生产流
把上面拼起来,要诚实说清楚的边界是:
- 当前这套 Rust 烧录流是 DEV 流——它工作,是因为开发片关掉了验签。它不提供任何 真实性保证:能写到这片 flash 的人就能换掉你的固件。
- 在开发/评估阶段这完全没问题——你要的是迭代速度,不是抗篡改。
- 但不要把“全零签名也能启动“误读成“WS63 的安全启动是摆设“。恰恰相反:
量产片烧了
SEC_VERIFY_ENABLE之后,验签是真的、靠 efuse 根密钥 + 私钥签名, 全零镜像会被拒绝。是开发片主动关掉了它,不是它不存在。
一句话:当前的便利来自一个被有意关掉的安全特性;上生产就得把它打开,并接入厂商签名链。
两者不能混为一谈。镜像头里每个字段(结构版本、签名长度、code_area_len、
code_area_hash……)的确切位置见 应用镜像格式与签名。
QEMU 模型
这一篇讲 hisi-riscv-qemu(WS63/BS2X 的 QEMU 仿真)为什么存在、它的能力边界在哪。一句话先说结论:它让你在没有真硅片、没有 RF 的情况下把绝大多数开发——内存布局、启动、外设逻辑、中断投递——都验证掉, 但它模拟不了的那部分恰恰是 HIL 必须上板验的部分。把这两件事的边界 看清楚,就理解了整套验证策略。
为什么要造一个 QEMU 板卡
最朴素的理由:真硅片稀缺、RF 难搞、迭代慢。如果每改一行 HAL 都得打包、烧片、接串口看
输出,开发会被硬件可用性卡死。QEMU 给的是一个软件在环的快速反馈环——cargo build 完
直接 -kernel firmware.elf 就能看它跑、能 GDB 调、能跑确定性回归。
为什么不用现成的 -M virt?因为固件是按 WS63 的真实地址链接的——外设在 0x4400_xxxx、
flash 在 0x200000、SRAM 在特定高地址。在 virt 上,固件第一次访问 WS63 外设就会
fault。所以必须有一个地址布局和 WS63 一致的板卡。
为什么不做“树外插件“?因为 QEMU 没有稳定的树外板卡 ABI。自定义 SoC 的标准做法
(Espressif 的 esp-qemu 也是如此)是 fork 一个固定版本的 QEMU、加一个 in-tree 板卡文件。
hisi-riscv-qemu 正是这么做的:加 hw/riscv/ws63.c,只构建 riscv32-softmmu。
它建模了什么
这个模型的覆盖面相当完整,远不止“能跑起来“:
- CPU:命名核
-cpu ws63= RV32IMFC(I/M/F/C + Zicsr/Zcf,关 A/D、无 MMU), 和真硅片的 ISA 一致——包括“没有原子“这一点。 - xlinx 自定义 ISA:HiSilicon riscv31 的一批私有指令。这是为了能跑厂商 gcc 编的 C SDK 固件——有了它,Rust 固件(标准 RV32IMFC)和厂商 C SDK 固件可以对照交叉验证 内存映射、启动、外设时序。
- 全部 35 个 SVD 外设:不是“catch-all 黑洞“,而是逐个建模。其中很多是行为完整的—— DMA 真的搬内存、Timer/RTC/WDT 真的计时和触发中断、I2C/SPI/I2S 真的回环 FIFO、 LSADC 真的出采样、EFUSE 真的走 OTP 按位或、GPIO 是真实信号网(bank 内回环 + 跨 bank 板级连线 + 可外部驱动)。少数配置类寄存器(晶振/RF/PHY 相关)是读回影子。
- 时钟树:时钟门控生效(清门会冻结定时器、置位恢复)、源路由(TCXO/PLL 选择)建模为状态。
- 中断:两类都端到端投递——IRQ 26–31 走标准
mie,IRQ ≥32 走 HiSilicon 自定义LOCIxxCSR(经 target/riscv 补丁实现),并强制LOCIPRI优先级 +PRITHD阈值。 -icount确定性指令计时:开了之后虚拟时间绑定指令数,同一固件每次运行结果完全 一致(实测 1e6 循环三次都是同一个 tick 数)。这让 CI 回归可重现。
这是一个真值驱动的模型:外设基址/寄存器对着 WS63.svd,内存布局对着
hisi-riscv-rt 的 memory.x/layout.ld,UART 行为对着 HAL 的 uart.rs + SDK 头文件。
也就是说它和真硅片共享同一批真值来源——这是它能当“软件在环替身“的前提。
它模拟不了什么
诚实地划出边界,比夸大覆盖面重要得多。QEMU 本质上模拟不了这几类东西:
- RF / PHY / 模拟量:射频前端、PHY 事件、真实无线收发——这是物理边界,仿真器里没有可 观测行为。(连接性因此走“合成 MAC 在 netif 缝合点“的软件在环底座,而不仿 RF; BS2X 的 BLE/SLE 在 radio-MMIO 层模拟已被论证是死胡同。)
- 真实时钟频率与时序:TCG 不模拟流水线/cache/逐指令周期。
-icount给的是 IPC=1 的 确定性近似,不是真实微架构周期精确。更要命的是——QEMU 的 chardev 不限速, 所以 UART 哪怕波特率算错了,它照样把字节原样吐出来,根本不会暴露波特错误。 - 真实 flash 内容:flash XIP 窗口是 RAM 背靠的,默认空白。分区表、NV、出厂标定
(
xo_trim这类逐芯片烧录的键)QEMU 里天然没有——只能用-device loader回填一份 构建出来的,而出厂标定值任何构建产物都不含。 - 掩膜 ROM / app ROM 的真实内容:那些是从硅片读出的专有 dump,不在仓库。厂商 blob 里 会跳进这些 ROM 地址,所以连接性的 blob 难以脱离真硅片——这也是 QEMU 的天花板之一。
QEMU 跑 vs 真硅片跑:根本差别
这是最该记住的一张对照表——它直接解释了为什么“QEMU 过了“不等于“硅片能跑“:
QEMU(-kernel ELF) | 真硅片 | |
|---|---|---|
| 镜像 | 裸 ELF,load_elf() 解析并按物理地址落段 | 带 0x300 头部的镜像,烧到 app 分区 |
| 引导链 | 没有 flashboot;复位向量直接设成 ELF entry | mask ROM → loaderboot → flashboot 跳 +0x300 |
| 时钟 | 取标称值(240 MHz PLL / 24 MHz TCXO),chardev 不限速 | 真实频率,会暴露分频/波特/PLL 锁定的真相 |
| RF | 不仿 | 真实射频 |
换句话说,QEMU 替你做了 flashboot 不做的事(理解 ELF、跳对入口),又省略了 flashboot 做的事(镜像格式、引导链)——所以“裸 ELF 在 QEMU 里能跑、在硅片上不能跑“ 不是矛盾,而是这张表的直接推论(详见 启动流程)。
QEMU↔HIL 的 parity 思路
把上面拼起来,整套验证策略就清楚了:QEMU 负责证明逻辑对——内存布局、启动序列、 外设行为、中断投递、DMA 搬运;HIL 负责证明 QEMU 证明不了的物理现实对——真实时钟、真实波特、 真实外设时序、引导链、RF。 两者验的是不同的东西,不是冗余。
理想形态是 parity:同一个固件(比如 uart_hello),QEMU 里看到什么标记串、硅片上就该
看到同样的标记串。QEMU 先把逻辑钉死,硅片再把物理现实钉死,两边比对。一旦硅片上的输出和
QEMU baseline 分歧,那个分歧几乎必然落在 QEMU 模拟不了的那几类——波特、时钟 10× 偏差、
引导挂死、IRQ 投递、外设接线。这正是 HIL 框架 那套 triage 的出发点。
模型的逐外设建模矩阵、xlinx ISA 细节、NV 回填等见 hisi-riscv-qemu 仓库的
docs/design.md。
HIL 测试框架
这一篇讲硬件在环(hardware-in-the-loop, HIL)测试的哲学——为什么我们用 UART 里的一行 字符串来判定一次真机测试通过、QEMU 已经验过的东西为什么还要上板再验一遍、以及一份诚实的 当前 bring-up 状态。操作步骤见 运行 HIL 冒烟测试, 标记串与环境变量见 HIL 标记串与环境变量;本篇讲“为什么这样测“。
HIL 存在的意义:验 QEMU 验不了的那部分
QEMU 已经把逻辑钉死了——内存布局、启动序列、外设行为、中断投递、DMA 搬运 都在软件在环里过了。那为什么还要 HIL?因为 QEMU 本质上模拟不了真实的物理现实: 真实时钟频率、真实波特、真实外设时序、真实引导链、RF。
所以 HIL 的定位很明确:它不是再验一遍逻辑,而是验 QEMU 验不了的物理现实。 一个固件如果在 QEMU 里跑通、却在硅片上出问题,那个问题几乎必然落在“真实时钟/时序/外设/ 接线“这几类——而不是逻辑 bug(逻辑 QEMU 已经替你筛过了)。这个前提决定了 HIL 故障诊断 (triage)的整个思路:先怀疑物理现实,而不是先怀疑代码逻辑。
为什么用 UART 标记串做验证通道
HIL 的验证通道是串口里打印的标记字符串——比如 uart_hello 该打印
Hello from WS63 ...、timer_irq 该周期打印 timer irq #N。为什么选这么“土“的通道,
而不是用调试器读寄存器、或者别的什么?三个理由:
- QEMU 可证 + 硅片可观测。同一行标记串,QEMU 的 smoke-test 能看到、真硅片接串口也能 看到。这就让 parity 比对成立:QEMU baseline 打印什么、硅片就该打印什么, 两边逐字对照。换个调试器专属的通道,QEMU 那边就对不上了。
- 它恰好能抓住 QEMU 抓不到的 bug。一行 UART 输出要正确出现,背后牵连真实波特 (UART 时钟分频对不对)、真实定时(timer 时钟基对不对)、引导是否真的跑到了 main—— 这些全是物理现实。QEMU 的 chardev 不限速,所以波特算错它照样原样吐字节、根本不报错; 而真串口接收端波特不匹配就是一屏乱码或者干脆没有。也就是说,UART 这个通道天生能暴露 时钟/波特/定时类 bug,正好补上 QEMU 的盲区。
- 门槛低、确定。一根串口线、一个标记串比对,不需要逻辑分析仪也能跑出第一轮结论 (需要时再上逻辑分析仪做更细的诊断)。
三段式流程:qemu-smoke → hil-smoke → hil-triage
整套验证是三段接力,逐步逼近真机现实:
- qemu-smoke——在 QEMU 里逐例跑,建立baseline:每个固件该打印什么标记串、按什么 顺序、什么节奏。这是“应该是什么样“的真值。
- hil-smoke——在真硅片上逐例烧录 + 读 UART + 比对标记串,镜像 QEMU 的 smoke-test。 通过则该例的物理现实也对了;不通过则进下一步。
- hil-triage——诊断单个失败步。它的工作假设很关键:板子跑的是 QEMU 已验证的固件, 所以失败通常意味着一个 QEMU 模拟不了的真实时钟/时序/外设/接线现实,而不是逻辑 bug。 triage 的任务是带证据点名最可能的那一类原因,而不是漫无目标地猜。
分歧的几类——先查这些
QEMU↔硅片的分歧高度集中在固定几类,triage 按这个清单逐项对照:
- UART 波特——乱码 / 没 banner ⇒ UART 时钟分频假设错。WS63 UART 从 160 MHz 基 分频;若按别的时钟算分频,波特就偏。(QEMU chardev 不限速,永远抓不到这类。)
- 定时器周期偏约 10×——
timer_irq来得太快/太慢 ⇒ 定时器还在按 240 MHz PLL 算、 而真值是 24 MHz TCXO(或反过来)。这是**最典型的“QEMU 过了、硅片不行“**的 bug。 - 引导挂死 / 全程静默——一点输出都没有 ⇒ 电源/PWR_ON、错的
LOADERBOOT、错的 flashADDRESS、或启动时读了一个真硅片上永远锁不上的 PLL。 - IRQ 没投递——
gpio_irq静默 ⇒ LOCI 使能、触发沿、或引脚接线;核对 IRQ 号和 LOCIEN/mie 路径是否和 SDK 一致。 - 外设接线——
spi_loopback需要 MOSI↔MISO 短接、i2c_scan需要真实上拉。 这一类“失败“可能是测试台架(rig)的问题,不是固件的问题——triage 必须把 “固件要改“和“台架要修“分开。
诊断时对时序类症状要做算术:从 HAL 的时钟常量算出期望周期/波特,再从实测值反推真实 时钟是多少,用数字说话而不是猜。
当前 bring-up 状态(诚实版,2026-06-14)
要诚实——这是进行中的工作,不是已完成的胜利:
- blinky:已在真硅片上确认。 完整的 Rust → flash → 启动主流程
(
cargo build→hisi-fwpkg patch-hash <elf>→probe-rs run <elf>) 于 2026-06-14 在真 WS63 硅片上跑通,blinky 上电启动并翻转 GPIO0。这是第一个、也是目前 唯一一个端到端真机确认的例子。WS63 走boot-headerfeature——0x300 头在链接期就烤进 ELF,链接后只需hisi-fwpkg patch-hash补上真实 body SHA-256(secure-off 仍校验 hash, 只跳过 ECC 签名),裸 ELF 即可直接probe-rs download/probe-rs run,没有中间.img、 也没有hisi-fwpkg image步骤。(BS2X 暂无链接期 boot-header,仍走 route 1 的hisi-fwpkg image -o app.img <elf>→ 烧到 app 分区。) - uart_hello:跑到了 main 并在运行,但 banner 还读不出来。 固件确实启动、确实进了
main、确实在跑——但它的 UART banner 目前在真硅片上还读不到。怀疑是波特/时钟 假设的问题(对照上面的分歧类 1:UART 的 160 MHz 时钟基),正在排查中。 这正是 QEMU 抓不到、必须上板才暴露的那类——也正是为什么我们要 HIL。 - 其余例子(timer_irq / gpio_irq / reset_demo / SPI / I2C / DMA 等):QEMU 已验证,
真机 bring-up 进行中。 QEMU 端这些的逻辑(中断投递、复位记录、DMA 握手、回环)都验过了;
真机这一侧在等逐例把
LOADERBOOT/串口监控参数按板填实、逐步推进。 - 连接性(阶段 4/5):WS63 Wi-Fi 的 porting + 链接 + netif→smoltcp 已在 QEMU 软件在环 自测、符号闭合达成;真机连通仍待 HIL。BS2X 的 BLE/SLE 在 radio 层已论证不可行, 走 HCI 边界。
首板的第一目标就是跑通 uart_hello → timer_irq → reset_demo 这几步,确认“本轮的时钟修复 在真硅片上准确“(24 MHz 定时器、160 MHz UART 波特、SPI/I2C、GPIO/复位中断)——这正是 QEMU 数字验证不了、必须上板验的核心。uart_hello 的 banner 问题就是这条路上的当前关卡。
不夸大、不假装:逻辑这一层 QEMU 已经替我们筛得很干净,物理现实这一层才刚踩上第一块硅。 这恰恰是 HIL 这套框架存在的全部理由。
组件深入文档
这一节是 10 篇逐组件深入文档的索引。它们是每个组件的权威实现说明——讲这一个组件 内部到底怎么实现、设计上踩过哪些坑、架构评审发现了什么。和上一层的概念章节分工明确:
- 概念章节(架构 / 启动 / 工具链 / async / 安全启动 / QEMU / HIL)讲 “为什么、各组件怎么串成一个整体”;
- 这里的深入文档(deep dive)讲 “这一个组件内部怎么实现”。
概念章节会链接进这些深入文档;想建立全局图景先读概念章,想查某个组件的实现细节就来这里。
从这里开始
- 总体架构 overview.md——整个栈的全景:依赖链、核心设计模式、构建与目标、 多芯片支持、已知的全局性问题。想先看一篇就看这篇。
核心库栈(依赖链自下而上)
- ws63-svd.md——SVD 真值(CMSIS-SVD XML)+ 生成工具,整条链的最底层真值来源。
- ws63-pac.md——svd2rust 生成的裸寄存器访问层(含 BS2X 的 bs2x-pac)。
- hisi-riscv-hal.md——手写的安全驱动层,多芯片、可选 async/embassy。
- hisi-riscv-rt.md——运行时:启动汇编、中断向量、链接脚本、 critical-section 实现。
- async-embassy.md——HAL 异步层的实现细节:
block_on/IrqSignal、 每驱动on_interrupt钩子、embassy-timeDriver、代码地图与上游化路线 (概念总览见 async 与 embassy)。
应用与芯片支持
- ws63-examples.md——示例集合(blinky/uart/timer/gpio/dma/async/ embassy/连接性等)的组织与验证方式。
- ws63-flashboot.md——实验性 Rust 二级引导的架构与评审 (概念上的引导链见 启动流程)。
- ws63-RF.md——WS63 闭源 Wi-Fi/BT/BLE/SLE blob 与 Rust porting 层。
硬件手册
- ws63-guide.md——WS63 中文硬件手册(Sphinx)的说明(讲芯片,与讲代码的 overview.md 互补)。
ws63-rs 总体架构
这是 ws63-rs 的 Rust 代码架构文档(与硬件手册
ws63-guide互补:手册讲芯片,本文讲代码)。 完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
这是什么
ws63-rs 是面向 HiSilicon WS63 + BS2X(BS21/BS20/BS22)RISC-V SoC 族的 Rust 嵌入式生态。WS63 覆盖 Wi-Fi 6 / BLE / SLE/星闪,BS2X 覆盖 BLE/SLE(M1/M2)。 采用多仓库(git submodule)+ 单一 Cargo workspace 的组织方式。
组件与依赖链
┌─ crates/pac ────────────────────────────────────┐
│ ├─ ws63-pac/ws63-svd (CMSIS-SVD) ──svd2rust──┐ │
│ └─ bs2x-pac/bs2x-svd (CMSIS-SVD) ─────────────┤ │
│ ▼ ▼ │
│ hisi-riscv-hal (多芯片 HAL,chip-ws63/chip-bs21 feature) ◀── embedded-hal 1.0
│ │ ├─ feature "async" ◀── embedded-hal-async / embedded-io-async
│ │ │ asynch::block_on + IrqSignal + 各驱动 on_interrupt(中断→waker)
│ │ └─ feature "embassy" ◀── embassy-time-driver / -queue-utils
│ │ embassy::Driver (now()=TCXO 64位计数器, alarm=TIMER 通道)
│ ▼
│ examples/ws63/* (blinky/uart_hello/timer_irq/gpio_irq/reset_demo/dma_loopback/
│ examples/bs21/* (blinky/spi_loopback/i2c_scan/gadc_read/hid_demo/pwm_wdt/clock_rng/dma_mem)
│ │ async_delay/async_bus/embassy_multitask/embassy_async_io/wifi_blob_link/rf_port_demo/semihost…
│ ▲
│ └── embassy-executor (platform-riscv32, thread mode) ← embassy 示例
│ hisi-riscv-rt (启动/中断向量/链接脚本 + critical-section + 工具链原子垫片) ─┘(运行时)
│ chips/ws63/flashboot (实验性二级引导,独立,裸 MMIO)
│ chips/ws63/rf + ws63-RF (WS63 Wi-Fi/BT/BLE/SLE blob + Rust porting 层)
│ chips/ws63/guide (WS63 中文硬件手册,Sphinx)
│ chips/bs2x/guide (BS2X 中文硬件手册,Sphinx)
│
│ ws63-qemu (姊妹仓:`-M ws63/bs21/bs22/bs20` QEMU,WS63/BS2X 软件在环验证)
│ probe-rs fork hispark-rs/add-hisilicon-ws63-bs21(RISC-V-DM + HiSilicon DebugSequence + flash-algorithm)
└──────────────────────────────────────────────────┘
| 组件 | 类型 | 角色 | 架构文档 |
|---|---|---|---|
crates/pac/ws63-pac | submodule | WS63 svd2rust 生成的寄存器访问层 | ws63-pac.md |
crates/pac/ws63-pac/ws63-svd | 嵌套 submodule | WS63 SVD 真值 + 生成工具 | ws63-svd.md |
crates/pac/bs2x-pac | submodule | BS2X(BS21/BS20/BS22)svd2rust 生成的寄存器访问层 | ws63-pac.md |
crates/pac/bs2x-pac/bs2x-svd | 嵌套 submodule | BS2X SVD 真值 + 生成工具 | ws63-svd.md |
crates/hisi-riscv-hal | submodule | 多芯片 HAL(chip-ws63/chip-bs21 feature)+ 可选 async/embassy | hisi-riscv-hal.md |
crates/hisi-riscv-rt | submodule | 运行时:启动、中断向量、链接脚本、critical-section | hisi-riscv-rt.md |
examples/ws63/* | in-tree 独立工作区 | WS63 应用示例(blinky/uart/timer/gpio/dma/reset/async/embassy/wifi_blob_link/rf_port_demo…) | ws63-examples.md |
examples/bs21/* | in-tree 独立工作区 | BS2X 应用示例(blinky/spi/i2c/gadc/keyscan/qdec/rtc/trng/wdt/dma/pdm/usb…,全外设功能覆盖) | ws63-examples.md |
examples/bs20/ | in-tree 独立工作区 | BS20(M1)示例 | — |
chips/ws63/flashboot | in-tree | 实验性二级引导(非安全启动) | ws63-flashboot.md |
chips/ws63/rf/ | in-tree | WS63 Wi-Fi porting 层 ws63-rf-rs | — |
chips/ws63/rf/ws63-RF | submodule(嵌套) | WS63 闭源协议栈 blob + porting 接口 | ws63-RF.md |
chips/ws63/guide | submodule | WS63 中文硬件手册(Sphinx) | ws63-guide.md |
chips/bs2x/guide | submodule | BS2X 中文硬件手册(Sphinx) | — |
核心设计模式
调试支持
-
probe-rs(新):fork
hispark-rs/probe-rs分支add-hisilicon-ws63-bs21,实现 RISC-V Debug Module + HiSilicon 厂商 DebugSequence(mem-AP DTM)+ flash-algorithm crate。软件完整,待硅片真机验证。用法:probe-rs run --chip ws63 <bin>进行实时调试与 on-silicon 烧录。 -
外设单例 +
'd生命周期:Peripherals::take()(PAC 单例,critical-section 保护)分发'd参数化的 ZST 外设令牌; 驱动经构造器消费令牌,借生命周期防 use-after-drop。 -
多实例外设(UART/I2C/SPI/DMA):用
PhantomData<&'d T>+ 每实例构造器(new_uart0/new_uart1…)区分。 -
sealed trait(
private.rs):Sealed超 trait 防外部实现DmaWord/PeripheralInput/PeripheralOutput。 -
#![no_std]:无堆、无Vec,数据缓冲用定长数组。 -
寄存器访问
unsafe:裸 PAC 写封装在驱动方法内。 -
异步(
async/embassyfeature,见 async-embassy.md):中断 + waker 驱动的embedded-hal-async/embedded-io-async驱动 + 一个 embassy-timeDriver,跑在无原子的 WS63 上 (portable-atomic + critical-section 垫片)。驱动只暴露on_interrupt钩子、不自动装 ISR。
注意:早先评审里的“零消费者脚手架“(DMA 安全 trait、空的 async marker)已处理 —— async marker 已删, 真正的异步层已实现并验证(见上);RAII 时钟守卫等仍按 ROADMAP 阶段 2 评估。详见各组件文档。
构建与目标(target)
- 默认 target / 工具链:
riscv32imfc-unknown-none-elf(RV32IMFC,硬件单精度浮点ilp32f,无原子), 由自定义hisi-riscv工具链提供(stable rustc 把该 target 烤成 builtin,故无需-Z build-std, 工具链自带预编译 core/alloc)。rust-toolchain.tomlpinchannel = "hisi-riscv";安装见 https://github.com/hispark-rs/hisi-riscv-rust-toolchain(rustup toolchain link hisi-riscv …)。- WS63 核无原子(A)扩展:该 target 用 forced-atomics + no-CAS,原子 load/store 降为 ld/st、
RMW 走
portable-atomic的 critical-section polyfill,不发lr/sc/amo。原默认riscv32imafc会发原子指令、在硅片上触发非法指令陷阱,已弃用。 - 历史:2026-05-31 阶段 0 曾先用 builtin
riscv32imc(软浮点、stable、免 build-std)做过渡; 随后切到ws63硬浮点工具链(与 ilp32f vendor blob ABI 一致,为阶段 3 链接做准备)。
- WS63 核无原子(A)扩展:该 target 用 forced-atomics + no-CAS,原子 load/store 降为 ld/st、
RMW 走
- 单一 PAC 实例:根
Cargo.toml用[patch.crates-io]把ws63-pac的 registry 依赖重定向到本地 submodule, 保证全仓库只链接一个 PAC(否则DEVICE_PERIPHERALS单例静态重复、类型不兼容)。 - default-members = 库 + blinky(
ws63-pac/hisi-riscv-hal/hisi-riscv-rt/examples/ws63/blinky)。 blinky 经 hisi-riscv-rt 导出的链接脚本可正常链接(hisi-riscv-rt/build.rs用cargo:rustc-link-search导出脚本目录 +ws63-link.x包装脚本,blinky 的build.rs以-Tws63-link.x引入)。实验性的ws63-flashboot不在默认构建里, 仍是member,cargo check --workspace覆盖。
常用命令:
cargo build --release # 构建库(default-members)
cargo check --workspace # 检查全部(含 blinky/flashboot,不链接)
cargo clippy --workspace --exclude ws63-flashboot -- -D warnings
cargo build -p blinky # 示例(已可链接;包含在默认构建中)
cargo build -p ws63-flashboot # 显式构建实验性 flashboot(包名是 ws63-flashboot)
已知的全局性问题(详见评审台账)
- 连接性状态:
- WS63 Wi-Fi(ROADMAP 阶段 3-5):porting 层 + 链接 + netif→smoltcp 已实现并在 QEMU 自测(阶段 4),符号闭合已达成;真机连通待 HIL(阶段 5)。
- BS2X BLE/SLE(已评估):radio MMIO 模拟是死胡同(B_CTL 0x59000000 为 56 个写只 PHY 寄存器 + IRQ-26 blob 事件墙),HCI 边界为 blob-on-blob(无法干预);完整分析见
hisi-riscv-qemu/docs/bs21-connectivity-feasibility.md。
示例无法链接(已修,阶段 1);多芯片支持(已实现):hisi-riscv-hal 用chip-ws63/chip-bs21feature 区分,二选一;examples 分为 WS63(submodule)、BS2X(in-tree 独立工作区)。- 硬件在环(HIL)进度:QEMU 软件在环已成熟(WS63/BS21/BS22/BS20 均支持,全外设功能覆盖),HIL 脚手架已就位(烧录脚本 + 冒烟框架);待真机板卡到位进行 blinky/UART/中断冒烟。
- 正确性修复状态:中断(LOCIEN/LOCIPRI/LOCIPCLR)、SPI(两级时钟)、超时(wait_until 有界)、复位(GLB_CTL + SYS_RST_RECORD)等核心问题已修(ROADMAP 阶段 2);QEMU 软件在环验证已覆盖中断、复位、DMA、timer;上板验证仍待硬件(时钟精度、外设时序)。
参考资料
多芯片支持细节
-
PAC 组织:
crates/pac/ws63-pac和crates/pac/bs2x-pac各自独立(SVD 源→svd2rust 生成),rootCargo.toml经[patch.crates-io]统一链接到本地实例(保证单一 PAC 版本)。 -
HAL 多芯片:
hisi-riscv-hal通过chip-ws63(default)和chip-bs21feature 区分,条件编译外设模块(WS63 含 Wi-Fi 相关,BS2X 含 GADC/KEYSCAN/QDEC/RTC/TRNG 等 M1 外设)。 -
示例组织:WS63 示例遵循原 submodule 路径
examples/ws63/;BS2X 示例为 in-tree 独立工作区examples/bs21/和examples/bs20/(避免 submodule 膨胀)。 -
QEMU 支持:ws63-qemu 已支持
-M ws63(8 GB 地址空间)、-M bs21(不同时钟/外设)、-M bs22/-M bs20(M2/M1),完整的 QEMU 外设仿真(UART/GPIO/Timer/DMA/SDMA/SPI/I2C/WDT/PDM/USB DWC OTG 等)。 -
fbb_ws63(
/root/fbb_ws63):官方 C SDK,寄存器/外设行为的真值来源。 -
esp-hal(
/root/esp-hal):成熟 Rust HAL 参照(esp-radio/esp-rtos/embassy/众多示例)——WS63 的连接性轨迹可对标。
hisi-riscv-hal 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
2026-06 更新:HAL 现为多芯片 ——
chip-ws63(默认)/chip-bs21特性。后者基于bs2x-pac服务 BS21/BS2X(BLE 5.4 + SLE/星闪)家族;BS2X 全部功能外设(SPI/GADC/I2C/KEYSCAN/QDEC/RTC/TRNG/WDT/DMA/PDM/USB)已在 QEMU-M bs21/bs22/bs20上验证。crate 路径crates/hisi-riscv-hal。
职责与边界
hisi-riscv-hal 是 WS63 SoC 的硬件抽象层(HAL),在 ws63-pac 的裸寄存器之上手写安全、符合 embedded-hal 习惯的驱动 API。
- 负责:
- 为 35 个 PAC 外设提供生命周期化的安全单例封装(
peripherals.rs),并在其上实现 35 个外设驱动模块(GPIO、UART、SPI、I2C、DMA、PWM、Timer、WDT、RTC、TRNG、Tsensor、SFC、I2S、LSADC、eFuse、以及 KM/PKE/SPACC 等加密外设)。 - 时钟架构:时钟门控(
clock.rs的ClockControl+Peripheral枚举)、引导期时钟树初始化(clock_init.rs)。 - GPIO 三层驱动模型、DMA 双控制器抽象、sealed trait 体系(
private.rs)。 - embedded-hal 1.0 / embedded-hal-nb 1.0 / embedded-io 0.6 / nb 的 trait 实现。
- 为 35 个 PAC 外设提供生命周期化的安全单例封装(
- 不负责:
- 裸寄存器布局与地址映射(属
ws63-pac)。 - 启动汇编、链接脚本、中断向量表(属
hisi-riscv-rt)。 - 应用业务逻辑(属
ws63-examples)。 - 连接性协议栈(WiFi/BLE/SLE)、porting 层、HCC IPC(尚未实现,见 ROADMAP 阶段 4-5)。
- 裸寄存器布局与地址映射(属
#![no_std]、无堆、无 Vec(lib.rs:20)。寄存器访问全部经 unsafe { w.bits(...) } 封装在驱动方法内部。
在依赖链中的位置
ws63-svd (XML)
│ svd2rust 生成
▼
ws63-pac ──► hisi-riscv-hal ──► examples/ws63/*
▲
hisi-riscv-rt(启动汇编 / 链接脚本 / 中断向量)并行提供运行期支撑
hisi-riscv-hal 是承上启下的核心层:向下消费 ws63-pac 的 RegisterBlock,向上为示例提供驱动。它不直接依赖 hisi-riscv-rt,但其中断子系统依赖 riscv crate 的 trap 模型,运行期向量表由 hisi-riscv-rt 的 device.x 提供。
依赖:embedded-hal 1.0、embedded-hal-nb 1.0、embedded-io 0.6、nb、portable-atomic、riscv。
关键设计
类型化配置 — “能编译就能上板”(0.5.0)
0.5.0 把配置面全面收紧为「能写出来的值就是能在硅上跑的值」:不存在能编译却被静默
clamp / 截断 / 没接时钟的参数。约定与 A/B/C/D 缺陷分类见
类型化配置,验收见 docs/review/0.5.0-acceptance.md。
两层结构:
- 配置/构造面(HAL 自有,可自由类型化):受校验 newtype + 可失败构造子返回
Option/Result(SpiHz/DataBits/BaudRate/WdtTimeout/SampleCount等),越界 在构造点拒绝;角色用 type-state(I2Snew_master(非零派生分频)/new_slave(),零分频 Master 不可表达);驱动在new/configure里自起本外设时钟门(construct→clocked, 如 PWM/I2S)。类型编码的是实测硅事实而非数据手册(如pwm::PwmPeriod是u16, 因 WS63pwm_freq_h高半字在硅上不 latch)。 - 操作面(embedded-hal trait,固定签名):
SetDutyCycle/SpiBus/I2c/Read/Write保留标准u16/&[u8]+Result(Result即 embedded-hal 的非法输入惯用法),不改 trait 签名。
危险外设(Wdt/PwmChannel/Output)实现 scoped Drop(停表/关输出/回高阻),逃生口
into_armed/into_running/into_latched(消费 self→零大小 marker)。DMA 提供拥有缓冲区的
Transfer guard(embedded_dma bound + 缓存维护折进类型),safe 代码里 use-after-free 不可表达。
每个收紧面都有 host newtype/property 测试,并在连接的真机经 HIL 套件(tests/hil.rs)复验。
外设单例 + 'd 生命周期
peripherals.rs 用两个宏生成全套封装:
peripheral!($name, $pac_ty)(peripherals.rs:10-48)— 为每个外设生成零大小、'd参数化的 ZST,提供unsafe steal()、ptr()、register_block()。peripherals!(...)(peripherals.rs:50-87)— 生成Peripherals结构体,take()经 PAC 单例校验(peripherals.rs:61-64),unsafe steal()绕过校验。
全部 35 个 PAC 外设都有 HAL 封装(peripherals.rs:157-193)。'd 生命周期防止 Peripherals token 被释放后仍持有驱动,是这一层的核心安全不变量(评审优点)。
时钟架构
两套并存:
clock_init.rs(标杆) — 逐寄存器对照 fbb_ws63 C SDK 的启动时钟序列核实。文件头部完整记录了CLDO_CRG_CLK_SEL位图、寄存器地址映射、时钟树(clock_init.rs:1-74)。init_clocks()(clock_init.rs:197-253)实现 flash→PLL(bit 18)、UART0/1/2→PLL(bits 1/2/3)、SPI→PLL(bit 6)的切换,并经REG_EXCEP_RO_RGbit 12 轮询 PLL 锁定(clock_init.rs:127-148)。TCXO 频率检测读HW_CTLbit 0(clock_init.rs:103-107)。所有地址均注明 fbb_ws63 出处。clock.rs的 RAII 时钟门控 —ClockControl封装CldoCrg,提供enable_uart()/enable_spi()等直接方法(clock.rs:192-260),以及PeripheralGuard引用计数守卫(clock.rs:86-125),用static REF_COUNTS: [AtomicU8; 17](clock.rs:74-78)做并发安全的开/关计数。Peripheral::cken_info()(clock.rs:45-67)将每个外设映射到(cken 寄存器索引, 位),PWM 的 9 位连续门控(bits 2:10)特殊处理。
GPIO 三层模型
19 个引脚分布在 3 个 block(GPIO0 bits 0-7、GPIO1 bits 8-15、GPIO2 bits 16-18),block 映射为 pin / 8、位为 pin % 8(评审确认正确)。两层(0.5.0 删除了遗留的 type-state GpioPin<MODE>,统一到下面这一套):
AnyPin<'d>— 类型擦除,经unsafe steal(pin)创建。Input/Output/Flex— 由AnyPin经init_input()/init_output()/init_flex()派生;degrade()可安全擦除回AnyPin,Flex::set_as_input/set_as_output提供显式方向。
InputConfig { pull } / OutputConfig { open_drain, initial_high } 为配置入口(均有 with_* builder)。Output 实现 scoped Drop(回 input/高阻),逃生口 into_latched()/into_flex()。embedded-hal digital trait 用 Infallible 错误类型实现。
DMA 双控制器
Dma0(0x4A00_0000)与 Sdma0(0x520A_0000)共享 dma::RegisterBlock,经 DmaInstance trait 提供 ptr()(dma.rs:25-44)。DmaDriver<'d, T: DmaInstance> 泛型于控制器(dma.rs:144)。DmaEligible(dma.rs:428-431)+ DmaChannelFor<P>(dma.rs:439)意图提供编译期通道-外设绑定安全(刻意不写 blanket impl 以保留约束语义)。
Sealed trait + 异步层
private.rs 定义 Sealed 超 trait,封印 DmaWord、PeripheralInput、PeripheralOutput。早先空壳的 DriverMode/Blocking/Async mode 标记(associated type 恒等、零消费者)已删除。
真正的异步层已实现(feature async/embassy,详见 async-embassy.md):embedded-hal-async/embedded-io-async 的 DelayNs/digital::Wait/spi::SpiBus/i2c::I2c/Read/Write,加上 asynch::block_on + IrqSignal(中断→waker 桥)+ 各驱动的 on_interrupt 钩子(不自动装 ISR);外加 LSADC/DMA 的自研异步。还提供一个 embassy-time Driver(now()=TCXO 64 位计数器、alarm=TIMER 通道),让 embassy-executor(platform-riscv32)跑 Timer::after + 多任务。全部跑在无原子的 WS63 上(portable-atomic + critical-section)。
embedded-hal trait 选型(评审优点)
- SPI 实现
SpiBus而非SpiDevice(spi.rs:135)— HAL 层不持有 CS,符合分层惯例。 - I2C
transaction在操作间发 repeated-START、仅末尾发 STOP(i2c.rs:215-265),符合 embedded-hal 契约。NACK 映射为NoAcknowledge(i2c.rs:278-280)。 - UART 同时实现 embedded-io
Read/Write与 embedded-hal-nb serial(uart.rs:172-293)。
编译期断言(safety.rs)
const_assert! 宏(safety.rs:11-20)校验 MMIO 地址范围、PERIPHERAL_COUNT == 17、各类外设/通道计数常量。注意此文件的多数断言为恒真(见评审问题)。
评审发现
优点
clock_init.rs是全仓标杆:逐寄存器、逐位对照 fbb_ws63 C SDK 核实,地址与位含义均注明出处(clock_init.rs:36-74、197-253)。- 外设单例 +
'd生命周期健全:宏生成统一、take()经 PAC 单例校验,生命周期防 use-after-drop(peripherals.rs:10-87)。 - embedded-hal/embedded-io/nb trait 选型正确:
SpiBus(非SpiDevice)、I2C repeated-START、ACK→NoAcknowledge均符合各 trait 契约(spi.rs:135、i2c.rs:215-280)。 - GPIO block 映射正确:
pin/8分 block、pin%8取位,与 3 block × 8 位的硬件布局一致(gpio.rs:86-88)。
问题
下表为 2026-05 评审快照;其后多数阶段 2 项已修(见各行状态),权威进度以 评审台账 为准。全部修复在姊妹仓
ws63-qemu软件在环验证。
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 严重 | 正确性 | 中断子系统曾建在不存在的 PLIC 模型上。WS63 用自定义 CSR(LOCIPRI=0xBC0 / LOCIEN=0xBE0 / LOCIPD=0xBE8) | interrupt.rs | ✅ 阶段2已修:重写为 LOCIPRI/LOCIEN/LOCIPD CSR 模型 + 优先级/阈值;ws63-qemu timer_irq/gpio_irq(IRQ≥32) 端到端验证 |
| 严重 | 正确性 | SPI ctra 写入 trsm=3(bits 19:18),该值是 EEPROM-Read 模式;全双工 TX+RX 应为 0。注释误写“TX+RX mode“导致 transfer/SpiBus 全双工语义不成立 | spi.rs:76 | 已排期(ROADMAP 阶段 2) |
| 高 | 正确性 | I2C/SPI 多处无超时死循环;错误码定义却从不返回 | spi.rs、i2c.rs | ✅ 阶段2已修:I2C/SPI 加 bounded 超时并真正返回 Timeout 等错误 |
| 高 | 正确性 | software_reset 执行 ebreak(非系统复位);reset_reason 恒返回 PowerOn | system.rs | ✅ 阶段2已修:software_reset 置 GLB_CTL_M 复位位,reset_reason 解析 SYS_RST_RECORD;ws63-qemu reset_demo 往返验证 |
| 中 | 正确性 | GPIO InputConfig.pull 被静默忽略:init_input 只设 OEN | gpio.rs | ✅ 阶段2已修:init_input 经 IO_CONFIG pad 寄存器应用上下拉 + 中断触发模式 |
| 高 | 正确性 | eFuse / LSADC 寄存器布局为猜测,与 SDK 矛盾 | efuse.rs、lsadc.rs | 🟡 已对照 fbb_ws63 + ws63-qemu(eFuse 写=按位或、LSADC 转换 IRQ72) 验证读写序列;逐寄存器复核仍按阶段 2 推进 |
| 中 | 维护性 | safety.rs 多条 const_assert! 为恒真断言;模块头措辞夸大 | safety.rs | ✅ 阶段2已修:删除恒真断言 + 夸大措辞 |
| 中 | 架构 | 零消费者死代码:RAII 时钟守卫、DMA 安全 trait、async marker | clock.rs/dma.rs/private.rs | ✅ 大部已清:async marker(Blocking/Async) 与 RAII 时钟守卫已删;并已实现真正的异步层(见上「Sealed trait + 异步层」);DMA DmaEligible/DmaChannelFor 保留约束语义 |
| 高 | 维护性 | 测试为恒真式(重抄被测公式再断言),从未上板验证 | spi.rs/i2c.rs/clock.rs/safety.rs | 🟡 已大幅缓解:ws63-qemu smoke-test.sh 用真实固件端到端验证(含异步/embassy 示例 + C SDK 交叉验证);真机 HIL 冒烟仍待补(阶段 1 尾) |
改进项与排期
按 ROADMAP(多数已完成,下记现状):
- 阶段 1(bring-up + 链接脚本集成):✅ 链接脚本集成已打通(
hisi-riscv-rt经cargo:rustc-link-search+ws63-link.x,示例正常链接);✅ 恒真式测试已由 ws63-qemu 软件在环大幅替代(smoke-test.sh跑真实固件 + C SDK 交叉验证);🟡 真机 HIL 冒烟仍待补。 - 阶段 2(死代码清理 + 正确性修复):✅ 中断子系统已重写到
LOCIPRI/LOCIEN/LOCIPDCSR 模型;✅ I2C/SPI 超时并返回错误;✅software_reset/reset_reason;✅ GPIO pull + 中断触发;✅safety.rs恒真断言 + 夸大措辞已删;✅ async marker / RAII 时钟守卫死代码已删。🟡 SPItrsm、eFuse/LSADC 逐寄存器复核仍在推进。 - 新增(超出原评审):✅ 异步 HAL(
async/embassyfeature,见 async-embassy.md)——embedded-hal-async/embedded-io-async全套 + embassy-timeDriver,全部 ws63-qemu 验证。 - 阶段 4-5(porting 层 + HCC IPC + 连接性):HAL 之上接入 WiFi/BLE/SLE 协议栈所需的 porting 与 IPC 通道。
- 阶段 6(async):在确有异步消费者后再恢复
Blocking/Async类型状态(阶段 2 已先删除空壳)。
hisi-riscv-rt 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
2026-06 更新:同一 runtime 服务 WS63 与 BS2X(BS21/BS20)。BS2X 示例自带按芯片的
memory.x(BS21E/BS22 160K、BS20 128K L2RAM),见examples/bs21/examples/bs20。
职责与边界
hisi-riscv-rt 是 WS63(RISC-V RV32IMFC_Zicsr)的最小运行时(runtime),负责把芯片从复位状态带到可执行 Rust main() 的环境。
负责:
- 复位向量:
reset_vector作为链接到 PROGRAM 区最前端的入口(asm/startup.S:18-26、layout.ld:19的ENTRY(reset_vector))。 - CPU 早期初始化:清 PMP、设
mtvec、关中断、开 FPU、清fcsr、初始化gp/sp、栈金丝雀填充(asm/startup.S:28-73)。 - trap/中断向量与汇编分发:异常入口
trap_entry、NMI、6 个 MIE 中断、60 个 local 中断的向量与寄存器保存/恢复(asm/startup.S:76-428)。 - 段重定位:ROM data/BSS、TCM text/data/BSS、SRAM text、
.data、.bss从 flash 拷到 RAM 并清零(src/startup.rs:75-193)。 - 缓存与 PMP:I/D cache 使能(
src/startup.rs:59-69),PMP 由 startup.S 在复位时清零(doc 注释提到 PMP 配置,但当前仅做禁用)。 - 链接脚本:内存布局(
memory.x)、段布局(layout.ld)、中断符号默认值(device.x)。build.rs生成ws63-link.x包装脚本(按 memory→layout→device→symbolsINCLUDE),下游 bin 用一个-Tws63-link.x引入。bundled-memory-x默认 feature:hisi-riscv-rt 默认把自己的memory.x放上链接搜索路径(零配置);需要自定义布局的 bin 设default-features = false自带memory.x(见examples/ws63/custom_memory)。 - 入口属性与 prelude:re-export
riscv_rt::entry与 PAC 中断类型(src/lib.rs:44-64)。 - 临界区基础设施:作为持有单一应用 hart 的 crate,启用
riscv的critical-section-single-hart,为全固件提供唯一的critical-section实现(Cargo.toml依赖注释;支撑 PAC 的Peripherals::take()与 HAL 的 portable-atomic polyfill)。
不负责:
- 不实现中断控制器逻辑(SYS_CTL1 / LOCIPRI / LOCIEN 的优先级与使能仅在 device.x 给出占位符,实际派发模型有误,见评审)。
- 不提供堆分配器(
.heap段仅预留地址,无 allocator)。 - 不做镜像头/验签/AB 切换(属 flashboot 与下游范畴)。
- 不做 porting/HCC/blob 连接性相关初始化。
在依赖链中的位置
ws63-svd (XML) → ws63-pac (svd2rust 生成) → hisi-riscv-hal → examples/ws63/*
hisi-riscv-rt ─┘ 提供启动/向量/链接脚本
hisi-riscv-rt 是“横切”运行时:它不在 PAC→HAL→examples 这条数据流主线上,而是为最终的 bin(examples) 提供入口、trap 向量与链接脚本。它依赖 ws63-pac(仅为 re-export 中断类型与共享单一 PAC 实例)、riscv 与 riscv-rt。
链接脚本传播(已解决):lib 依赖的
cargo:rustc-link-arg不传播到下游 bin。早先这导致示例无法链接;现已修——build.rs改为cargo:rustc-link-search导出 OUT_DIR + 生成ws63-link.x,bin 用-Tws63-link.x引入(rustc-link-search会传播)。见评审“问题”表「本轮已修」条。
关键设计
启动序列(标准 RV32 bring-up)
asm/startup.S 的复位流程对照 fbb_ws63 SDK 的 start.S,符合标准 RV32 裸机启动惯例:
- 清
pmpcfg0..3(startup.S:30-37,EDA/仿真 workaround)。 la t0, trap_vector; csrw mtvec, t0(startup.S:40-41)。- 关中断:
csrwi mstatus,0+csrwi mie,0(startup.S:44-45)。 - 开 FPU:
mstatus.FS=0b11,清fflags(startup.S:48-50)。 - 初始化
gp(norelax包裹,startup.S:53-56)与sp = __stack_top__(startup.S:59)。 - 栈金丝雀填充
0xefbeadde(startup.S:62-70)。 tail runtime_init(startup.S:73)→ Rust 侧做重定位/清 BSS/再开mie(src/startup.rs:21-50)。
内存地址(BOOTROM 0x100000、ROM 0x109000、ITCM 0x14C000、DTCM 0x180000、FLASH 0x200000、PROGRAM 0x230300、SRAM 0xA00000)与 fbb_ws63 一致(memory.x:16-41)。
trap/异常/中断汇编分发
- 异常:
trap_entry(startup.S:320)用save_all(36 字,含mcause/mbadaddr/ccause自定义 CSR)保存上下文,通过mscratch切到__exc_stack_top__(startup.S:327-328),按mcause索引.rodata中的excp_vect_table(startup.S:132-153、335-342)分发;M-mode ecall 单独走handle_ecall_m(startup.S:356-361)。 - NMI:切到
__nmi_stack_top__,调nmi_handler(startup.S:369-383)。 - MIE 中断:
mie_interrupt_handler宏生成 6 个(bits 26-31),切到__irq_stack_top__,call mie\n\()_interrupt_handler(startup.S:389-410)。 - local 中断:60 个向量统一进
local_interrupt_handler,调local_isr_dispatch(startup.S:418-428)。
每条 trap 路径都做了 mscratch 栈切换 + 上下文保存,异常路径还按 mcause 做表驱动派发,结构清晰。
链接脚本布局
layout.ld 改编自 fbb_ws63 的 linker.prelds:ITCM 放 patch 表/ROM-RAM 回调/TCM text;DTCM 放 ROM data/BSS、TCM data/BSS;SRAM 放 SRAM text、.data、.bss、栈、堆;FLASH(PROGRAM) 放 .text/.rodata 与各初始化段 LMA。.startup 段 KEEP(*(.text.entry)) 确保复位向量在 PROGRAM 区最前(layout.ld:125-129)。栈区在 .stacks (NOLOAD) 内自高地址向下生长(layout.ld:189-216)。
ISA / 原子性
build.rs 设 RISCV_RT_BASE_ISA=rv32i(无原子扩展)。默认 target 是 builtin 的 riscv32imfc-unknown-none-elf(RV32IMFC,硬件单精度浮点 ilp32f);无 A 扩展,原子由 portable-atomic 的 critical-section polyfill 提供,hisi-riscv-rt 启用 riscv 的 critical-section-single-hart 作为整个固件唯一的 CS 实现(Cargo.toml)。这套 CS 也支撑 hisi-riscv-hal 的 async/embassy 异步层。
评审发现
优点
- 标准 RV32 启动:PMP 清零、
mtvec、关中断、FPU、gp/sp、栈金丝雀、BSS/data 重定位齐备,流程对照 fbb_ws63(asm/startup.S:28-73、src/startup.rs:75-193)。 - 内存地址权威:
memory.x各区起始/长度与 fbb_ws63 SDK 对齐(memory.x:16-41)。 - trap 汇编质量高:异常/IRQ/NMI 均有
mscratch栈切换 + 分栈(exc/irq/nmi 独立栈),异常按mcause索引excp_vect_table表驱动分发(asm/startup.S:318-428、132-153)。 - 单一 CS 实现的依赖边界清晰:由持有 hart 的
hisi-riscv-rt独家启用critical-section-single-hart,避免多处重复实现(Cargo.toml注释)。
问题
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 高 | 正确性 | mtvec 以 Direct 模式写入(la t0,trap_vector; csrw mtvec,t0,未 ori 设置 MODE=Vectored),但同时构建了完整的 Vectored 跳转表(含 NMI/MIE/local 各项),导致除 trap_entry 外的向量项全部失效——所有 trap 都落到偏移 0 的异常入口 | asm/startup.S:40-41(Direct 写法)vs asm/startup.S:88-127(Vectored 表) | 已排期(ROADMAP 阶段 2,随中断子系统重构修正模式/表) |
| 中 | 正确性 | trap 相关段(.trap/.trap.exception/.trap.nmi/.trap.mie*/.trap.local)在 layout.ld 无显式输出段放置,成为孤立段(orphan),布局/对齐依赖链接器默认行为 | asm/startup.S:85,318,367,390,416(段声明);layout.ld:28-224(无对应 *(.trap*) 放置) | 已排期(ROADMAP 阶段 2) |
| 高 | 构建 | (已修)链接脚本不传播到下游二进制:build.rs 原用 cargo:rustc-link-arg=-T... 注入,但该 arg 来自 lib 依赖、不传递到 bin;示例改用 lld 默认布局、__exc/nmi/irq_stack_top__ 未定义 → blinky 链接失败。现改为 cargo:rustc-link-search 导出 OUT_DIR + 生成 ws63-link.x 包装脚本(按 memory→layout→device→symbols INCLUDE),blinky build.rs 以 -Tws63-link.x 引入 → blinky 现可链接 | build.rs(link-search + ws63-link.x);examples/ws63/blinky/build.rs | 本轮已修 |
| 高 | 构建 | (已修)MIE 中断宏 typo:call mie\()_interrupt_handler 缺少 \n,宏展开后符号名错误 | asm/startup.S:397(现为 call mie\n\()_interrupt_handler) | 本轮已修 |
| 中 | 构建 | (已修)栈顶符号 __irq/exc/nmi_stack_top__ 在 .stacks (NOLOAD) 仅符号区被 --gc-sections 回收 → 链接期未定义;已在 memory.x 顶层加 GC-safe fallback | layout.ld:199-204(说明);memory.x:76-78(fallback) | 本轮已修 |
| 中 | 构建 | (已修)riscv 启用 critical-section-single-hart,为无原子扩展的 WS63 提供单 hart CS 实现,支撑 PAC take() 与 HAL portable-atomic | Cargo.toml(riscv features) | 本轮已修 |
| 低 | 构建/发布 | (已修)ws63-pac 依赖补充 version(version = "0.1", path = ...)以便 cargo publish | Cargo.toml(ws63-pac 依赖) | 本轮已修 |
说明:构建完整性修复中与本组件相关的还包括——双 PAC 实例消除(根
[patch.crates-io]指向本地,cargo tree 单一实例)、无原子 ISA 下实测产物零原子指令(lr/sc/amo)。默认 target 现为 ws63 工具链 builtin 的riscv32imfc-unknown-none-elf(硬浮点;2026-05-31 曾过渡用 stableriscv32imc)。这些在仓库级评审中记录,本组件直接相关项已并入上表。
改进项与排期
- 阶段 1(链接脚本集成 ✅ 已完成 / 上板待硬件):链接脚本不传播问题已解决(
rustc-link-search+ws63-link.x包装脚本 + blinkybuild.rs引入),blinky 现可链接并产出.bin。剩余:真机上板冒烟、用readelf核实 WS63 布局生效。 - 阶段 2(死代码清理 + 正确性修复):修正
mtvec模式与向量表的不一致(Direct vs Vectored);为 trap 段在layout.ld增加显式输出段放置;统一.stacks布局与memory.x栈顶 fallback;并随中断子系统模型纠正(PLIC vs LOCIPRI/LOCIEN)一并处理。对应上表前两行。 - 其余仓库级排期(efuse/lsadc、flashboot 镜像头/验签/AB、porting+HCC+blob 连接性、async)见 ROADMAP 阶段 2-6,与本运行时组件无直接耦合。
ws63-pac 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
2026-06 更新:PAC crate 现归并在
crates/pac/ws63-pac(内嵌生成源ws63-svd)。其 BS2X 同胞crates/pac/bs2x-pac(由bs2x-svd生成)以同样的 svd2rust 流水线服务 BS21/BS2X 家族。
职责与边界
ws63-pac 是 WS63 SoC 的外设访问层(Peripheral Access Crate),由 svd2rust 从 SVD 描述生成。它的职责非常聚焦:
- 负责:为芯片上的 35 个外设提供
RegisterBlock结构体与类型安全的寄存器读/写/改访问器;提供Peripherals单例(take()/steal());提供外部中断枚举ExternalInterrupt;在rtfeature 下提供中断向量表device.x。 - 不负责:任何驱动逻辑、时钟门控策略、引脚复用、外设初始化时序。这些全部上移到
hisi-riscv-hal。PAC 只暴露“裸寄存器 + 地址映射“,是unsafe寄存器写入的最底层封装边界。
crate 元数据齐全(Cargo.toml:1-9):license = "MIT"、repository、keywords、categories,具备发布到 crates.io 的条件。
在依赖链中的位置
ws63-svd (XML)
│ svd2rust 0.37.1 生成
▼
ws63-pac ──► hisi-riscv-hal ──► examples/ws63/*
│
└──► hisi-riscv-rt(rt feature 提供 device.x 中断向量 + RISCV_RT_BASE_ISA)
- 上游:
ws63-svd的 XML 描述,经svd2rust v0.37.1一次性生成(src/lib.rs:1doc 注释标注版本)。 - 下游:
hisi-riscv-hal(安全驱动)与hisi-riscv-rt(启动/链接)均消费本 crate。两者通过 registry 版本依赖version = "0.1"声明(crates/hisi-riscv-hal/Cargo.toml:12、crates/hisi-riscv-rt/Cargo.toml:21),在 monorepo 内由根Cargo.toml的[patch.crates-io]重定向到本地路径(Cargo.toml:50-51),保证全工作区只链接单一 PAC 实例。
关键设计
- svd2rust 0.37.1 现代访问器:generic 层用
Periph<RB, const A: usize>把外设基址作为 const 泛型参数编码(src/lib.rs:14-20),ptr()是const fn(src/lib.rs:23-25),Deref直接解到寄存器块(src/lib.rs:45-51)。这是新版 svd2rust 的 const-fn 访问器风格,零运行时开销。 - Peripherals 单例:
static mut DEVICE_PERIPHERALS: bool(src/lib.rs:31681)作为一次性标志;take()在critical-section内检查并返回Option<Self>(src/lib.rs:31760-31767),steal()为unsafe无检查版本(src/lib.rs:31774-31813)。Peripherals结构体逐字段持有 35 个外设的 ZST 句柄(src/lib.rs:31684-31755)。 - 35 外设覆盖:从
sys_ctl1、三路gpio0/1/2、三路uart0/1/2、双i2c、双spi、dma/sdma,到安全引擎spacc/pke/km/trng与时钟复位cldo_crg等全部映射(src/lib.rs:31685-31754)。 - 中断模型:
ExternalInterrupt枚举用#[riscv::pac_enum(unsafe ExternalInterruptNumber)]标注(src/lib.rs:902-904),中断号从 26 起(TIMER_INT0 = 26,src/lib.rs:906)。rtfeature 下build.rs把device.x写入OUT_DIR并加入 link-search(build.rs:8-18),向量表用PROVIDE(... = DefaultHandler)提供弱默认(device.x:1-30)。 - feature 设计:
default = ["critical-section"],外加rt(Cargo.toml:16-18)。take()仅在critical-section下编译(src/lib.rs:31758),符合 svd2rust 约定。 - ISA 协同:
rtfeature 下build.rs导出RISCV_RT_BASE_ISA=rv32i(build.rs:16),与本轮 ISA 修复(默认 target 切到riscv32imc、产物零原子指令)一致。
评审发现
优点
- svd2rust 0.37.1 现代 const-fn 访问器,generic 层零开销(
src/lib.rs:14-51)。 - 编译快(约 6s 过编译),单文件无复杂构建依赖。
- 工程化完备:
device.x中断向量、critical-section/rtfeature(Cargo.toml:16-18)、crates.io 元数据齐全(Cargo.toml:1-9)。 - 单例语义正确:
take()/steal()配合DEVICE_PERIPHERALS标志在临界区内做一次性保护(src/lib.rs:31760-31775)。
问题
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 中 | 维护性 | 单文件 lib.rs 体积约 1.8MB / 31814 行,难以审阅与定位 | src/lib.rs(1797361 字节、31814 行) | 暂不修(svd2rust 生成产物,按惯例不拆分;通过 CHANGELOG + grep 定位缓解) |
| 高 | 维护性 | 寄存器手补进生成代码:KM keyslot 寄存器(KC_REECPU_LOCK_CMD 等)在生成后人工添加,下次重生成会被覆盖 | src/lib.rs:28415、28569;CHANGELOG.md:13-21 | 已排期(ROADMAP 阶段 2):应回填到 ws63-svd 源头由生成器产出 |
| 中 | 依赖 | 版本曾停在 0.1.0 而 tag 后又追加了公开寄存器,违反 SemVer | Cargo.toml:3、CHANGELOG.md | 已修:bump 0.1.0 →(经 0.1.1/0.1.2)现 0.1.3,由 ws63-pac 自有仓库流水线发布 |
| 中 | 依赖 | 曾被 hisi-riscv-hal 以 git 依赖引入,导致工作区出现双 PAC 实例 | crates/hisi-riscv-hal/Cargo.toml:12、Cargo.toml:45-51 | 本轮已修:改 registry 版本依赖 + 根 [patch.crates-io] 指向本地,cargo tree 仅单一 ws63-pac 实例 |
改进项与排期
本轮(2026-05-31,ROADMAP 阶段 0)已完成的构建完整性修复中,与本 crate 直接相关:
- 双 PAC 消除:
hisi-riscv-hal/ws63-flashboot改为 registry 版本依赖,根Cargo.toml用[patch.crates-io]统一指向本地(Cargo.toml:50-51),全工作区单实例。 - 版本对齐:
0.1.0→0.1.1(与 tag 后新增的 KM 寄存器对齐),其后随各仓自有流水线发布到0.1.3。 - ISA 协同:
rtfeature 导出RISCV_RT_BASE_ISA=rv32i(build.rs:16),配合默认 target = builtin、无原子的riscv32imfc-unknown-none-elf(硬件单精度浮点 ilp32f,原子由 portable-atomic critical-section polyfill 提供)。
仍需后续处理(指向 ROADMAP 对应阶段):
- 手补寄存器回源(阶段 2):把 KM keyslot 等人工添加的寄存器回填到
ws63-svd,使其由 svd2rust 重生成产出,消除“生成产物被手改“的维护风险;同阶段一并补齐 efuse / lsadc 等外设寄存器的正确性。 - 单文件体积:作为生成产物,按 svd2rust 惯例暂不拆分;若后续 SVD 重构,可评估按外设分模块生成。
ws63-svd 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
职责与边界
ws63-svd 是整个 ws63-rs 寄存器抽象链的上游真值(source of truth)。它由一份手写的 CMSIS-SVD 描述文件 WS63.svd 加少量 Python 工具构成,负责:
- 用 CMSIS-SVD 1.3 schema 描述 WS63 SoC 的外设、寄存器、字段、枚举值与地址布局(
WS63.svd:2,schemaVersion="1.3")。 - 提供针对官方 CMSIS XSD 的格式校验脚本(
validate.py)。 - 承载 svd2rust 目标配置(
ws63-settings.yaml,RV32IMFC_Zicsr 等 RISC-V 目标参数)。 - 驱动可复现的 SVD→PAC 生成流水线(
regen.sh+postprocess.py,2026-05-31 起;见“关键设计/生成流水线”)。
它不负责:
- 托管 Rust 生成产物(产物
lib.rs落在下游ws63-pac;本组件提供并驱动生成流水线regen.sh,产物归 PAC 仓)。 - 任何运行时逻辑或驱动语义(那是
hisi-riscv-hal的职责)。 - 中断控制器的运行时模型(SVD 仅声明
<interrupt>编号,控制器建模问题归 HAL/RT 层)。
寄存器定义来源为公开的 ws63-guide 文档与 fbb_ws63 HAL 头文件转录(WS63.svd:5-7 的 <licenseText>),属于手工建模而非厂商官方 SVD。
在依赖链中的位置
flowchart LR
SVD["ws63-svd<br/>(WS63.svd 手写真值)"] -->|svd2rust 生成| PAC["ws63-pac<br/>(寄存器 RegisterBlock)"]
PAC --> HAL["hisi-riscv-hal<br/>(安全驱动)"]
HAL --> EX["examples/ws63/*"]
RT["hisi-riscv-rt<br/>(启动/链接/向量)"] --> EX
ws63-svd 处于链条最上游:WS63.svd 经 svd2rust 生成 ws63-pac 的 lib.rs,再由 hisi-riscv-hal 封装为安全驱动,最终被 ws63-examples 使用。hisi-riscv-rt 提供启动代码与链接脚本,是与上述生成链平行的独立分支。
生成关系(2026-05-31 起可复现):WS63.svd 经 regen.sh(svd2rust 0.37.1 + 确定性后处理 + cargo fix/fmt)生成 crates/pac/ws63-pac/src/lib.rs,幂等(同 SVD → 字节一致产物),内建 build+clippy 门禁。SVD 与 PAC 之间已是可复现的生成关系,不再“人工对照”。
关键设计
SVD 文件结构与建模质量
WS63.svd 约 1.07 万行(精确 10744 行)(WS63.svd:1-10744),device 头声明了 CPU 为 other、fpuPresent=true/fpuDP=false、width/size=32、nvicPrioBits=3(WS63.svd:9-18),description 中记录了 ISA rv32i2p1_m2p0_f2p2_c2p0_zicsr2p0 与 512KB ITCM / 288KB DTCM / 640KB 共享 SRAM 的内存规格(WS63.svd:4)。
建模规模与完整度(实测 2026-06-11):
- 36 个
<peripheral>元素(grep -c "<peripheral"),覆盖 SYS_CTL1、IO_CONFIG、GPIO0/1/2、UART0/1/2、I2C0/1、PWM、DMA、SFC_CFG、SPI0/1、I2S、LSADC、TSENSOR、TIMER、WDT、RTC、EFUSE、SYS_CTL0、GLB_CTL_M、SPACC、PKE、KM、TRNG、TCXO、CLDO_CRG、SDMA、ULP_GPIO、RF_WB_CTL、SHARE_MEM_CTL、FAMA_REMAP。 - 501 个非派生
<register>定义(grep -c "<register>";评审时为 497,本轮 eFuse/LSADC 修复 +4:eFuse 数据窗口 +1、LSADC 重写 +3;含derivedFrom展开后逻辑实例更多)。 - 920 个
<field>、44 处<enumeratedValues>、36 个<addressBlock>、2 处<writeConstraint>、190 处read-only访问限定。 - 8 处
derivedFrom复用。
UART/GPIO/KM 等外设建模质量较高:字段拆分、枚举值与访问属性齐全。例如 KM(Key Management,WS63.svd:9410,baseAddress 0x44112000)对 KLAD 派生、keyslot 锁定、RKP 根密钥保护建模到了字段级(KL_KEY_CFG 的 port_sel/key_enc/key_dec 等,flush_hmac_kslot_ind 字段亦已建模)。
校验工具
validate.py(validate.py:1-29)从 ARM CMSIS_5 仓库下载 CMSIS-SVD.xsd 缓存到 /tmp,用 xmlschema 对 WS63.svd 做 XSD 校验,PASS/FAIL 返回码区分。依赖在 pyproject.toml 声明为 xmlschema>=4.3.1,由 uv.lock 锁定。这是目前唯一真实可用的工具。
生成流水线(regen.sh,可复现)
regen.sh + postprocess.py(2026-05-31)把 WS63.svd 可复现地生成为 crates/pac/ws63-pac/src/lib.rs,固定工具版本 svd2rust@0.37.1 / form@0.13.0。五步:
svd2rust -i WS63.svd --target riscv --settings ws63-settings.yamlrustfmt(svd2rust 原始输出未格式化,后续正则后处理依赖多行格式)postprocess.py两处确定性修补:删除dim重复生成的 5 个 TIMER 裸访问器(否则与索引访问器重复定义、编译失败)、#[no_mangle]→#[unsafe(no_mangle)](edition 2024 硬错误)cargo fix自动套unsafe_op_in_unsafe_fn(rt+critical-section特性下Peripherals::steal()的 unsafe 包裹)cargo fmt,随后 build + clippy 作为门禁
流水线幂等:同一 SVD 重跑产出字节一致的 lib.rs。ws63-settings.yaml 提供 svd2rust 目标设置(RV32IMFC_Zicsr、自定义中断控制器 SYS_CTL1 无标准 CLINT/PLIC、单 hart、240MHz)。main.py 仍是 uv 占位入口,实际生成走 regen.sh。主仓 PreToolUse hook 拦截对 crates/pac/ws63-pac/src/lib.rs 的手改,强制走重生成。
评审发现
优点
- 建模覆盖广:36 外设 / 497 寄存器,
enumeratedValues、derivedFrom、writeConstraint、addressBlock一应俱全,是一份结构完整、可被 svd2rust 直接消费的 1.3 版 SVD。 - 通过
derivedFrom对同构外设(GPIO/UART/I2C/SPI/SDMA)做了正确复用,降低了维护面。 - 提供了针对官方 CMSIS XSD 的格式校验脚本,建模本身有质量门可依。
- UART/GPIO/KM 等关键外设建模到字段+枚举级,下游 HAL 可直接获得类型安全的位域访问。
问题
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 高 | 维护性 | 手补代码曾被手工补进已格式化的 PAC 生成代码,而非回填 SVD 后重生成,clean regen 会丢失或冲突。 | 历史提交 df35d69「add missing KM keyslot registers」;该批字段在 WS63.svd KM 外设中存在但生成链曾未联动 | ✅ 已修(2026-05-31):建立 regen.sh、停止手补;重生成时 PAC 反而恢复了手补遗漏的 KM keyslot 字段(flush_hmac_kslot_ind/tscipher_ind/lock_cmd/key_slot_num) |
| 中 | 维护性 | 无可复现生成流水线:main.py 是 print(...) 桩,无 svd2rust 调用;ws63-settings.yaml 在 base_isa: rv32i 截断;CI 中无 SVD 引用 | main.py:1-6;ws63-settings.yaml;.github/workflows/ 无 SVD 引用 | ✅ 已修(2026-05-31):regen.sh+postprocess.py 幂等可复现、build+clippy 门禁;CI 接入(“重生成并 diff”)为剩余小项 |
| 高 | 正确性 | eFuse/LSADC 外设建模错误:eFuse 控制寄存器偏移错位(0x00 段)、wr_rd 建成单 bit 而非 16 位魔数、缺 0x800 数据窗口;LSADC 寄存器整块错位(使能/启停/FIFO 寄存器选错) | 评审台账 + 本轮对照 hal_efuse_v151/hal_adc_v154 | ✅ 已修(2026-05-31):eFuse 控制块移到 base+0x30、16 位魔数、加 0x800 窗口;LSADC 重写为连续 adc_regs_t(CTRL_8/9/11、CFG_* @0xDC..0xEC)。偏移已在生成 PAC 中逐一核验 |
| 中 | 正确性 | 覆盖不全:KM 的 *_FLUSH_BUSY 状态寄存器(偏移 0xB10–0xB1C)缺失,KM 偏移从 0x1B0C 直接跳到 0x1B30,存在转录静默缺口 | WS63.svd KM 外设;addressOffset 序列断档;grep FLUSH_BUSY 无命中 | 已排期(ROADMAP 阶段 2):flush_hmac_kslot_ind 字段已建模,但 BUSY 查询寄存器本身仍未补 |
| 低 | 文档 | README.md 为空文件(0 字节),组件无任何使用/维护说明 | README.md(0 bytes) | ✅ 已修:README 已补写(含 regen.sh 用法、流水线步骤、校验命令、维护约定) |
说明:本组件已从“几乎全部已排期”转为四项中三项已修(仅 KM
*_FLUSH_BUSY转录缺口待补)。这些都是静态对照 fbb_ws63 C SDK 的修复,eFuse/LSADC 驱动仍未上板验证(验证归 ROADMAP 阶段 1 门禁)。下游ws63-pac也已随regen.sh重生成。
改进项与排期
ws63-svd 的整改核心是把 SVD 重新确立为唯一真值。本轮(2026-05-31)已落地大部分:
- ✅ 建立可复现生成流水线(已完成):
regen.sh(svd2rust 0.37.1 +postprocess.py后处理 + cargo fix/fmt)替代main.py桩,幂等、build+clippy 门禁。剩余:把“从 SVD 重生成并 diff“接入 CI;并加validate.pyXSD 校验门(脚本已就绪)。 - ✅ 以 SVD 为源重生成 PAC(已完成):
regen.sh即唯一生成路径,手补 lib.rs 被 PreToolUse hook 拦截;重生成恢复了历史手补遗漏的 KM keyslot 字段,消除 SVD↔PAC 漂移。 - ✅ eFuse/LSADC 寄存器修复(已完成):对照
hal_efuse_v151/hal_adc_v154改 SVD 并重生成(详见上表)。剩余:KM*_FLUSH_BUSY(0xB10–0xB1C)转录缺口仍待补;其它外设逐个对照 fbb_ws63*_reg.h核覆盖。 - ✅ 补写 README(已完成):含
regen.sh用法、五步流水线、校验命令与“勿手改 lib.rs“约定。
阶段编号参考:阶段 0 为构建完整性修复(2026-05-31 已完成);阶段 1 为硬件在环 bring-up 与链接脚本集成;阶段 2 为本组件
相关架构
BS2X SVD: bs2x-svd 强自前和永久性存搬,两份 SVD 准生政之不同厨特针 PAC。probe-rs 调试: hispark-rs fork 各和 RISC-V DM/CoreSight,待板级。连接: Wi-Fi 于剖地,BLE 是 blob。主要落点(本轮已完成上述大部分,KM 缺口 + CI 接入为剩余)。详见 ROADMAP。
ws63-examples 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
职责与边界
ws63-examples 是面向最终用户的应用示例集合,演示 WS63、BS21 等多芯片的固件组合。例子展示如何把 hisi-riscv-rt(启动)+ hisi-riscv-hal(驱动,支持 chip-ws63/chip-bs21 特性)+ PAC(ws63-pac 或 bs2x-pac,见 crates/pac/)+ 连接性场景下的 ws63-rf-rs(RF porting),组合成可烧录的裸机固件。
- 负责:提供可参考的
#![no_std]/#![no_main]入口,以及各外设/子系统的最小调用示例(GPIO/UART/Timer/DMA、中断、复位、semihosting、自定义内存布局、async/embassy、RF porting)。 - 不负责:实现任何驱动或运行时逻辑(这些属于
hisi-riscv-hal/hisi-riscv-rt/ws63-rf-rs);不承担系统测试覆盖职责(单测在各 crate 内)。
当前含 14 个工作区示例,全部在 default-members(Cargo.toml:30-48),默认 cargo build 即构建:
| 示例 | 演示内容 |
|---|---|
blinky | GPIO 点灯(最小裸机入口模板) |
uart_hello | UART 输出 |
timer_irq | Timer 中断(WS63 自定义 LOCI* 中断模型) |
gpio_irq | GPIO 输入 + 边沿/电平中断 |
reset_demo | software_reset / reset_reason 往返 |
dma_loopback | DMA mem-to-mem 搬运 |
semihost_selftest | semihosting exit()/print(CI 免解析 UART 即得 pass/fail) |
custom_memory | 示例自带 memory.x 覆盖默认内存布局 |
async_delay | embedded-hal-async DelayNs + asynch::block_on |
async_bus | 异步 SPI/I2C 总线(SpiBus/I2c) |
embassy_multitask | embassy-executor 多任务 + embassy-time |
embassy_async_io | embassy 下的异步 UART I/O |
wifi_blob_link | 把 vendor RF blob 链入镜像(符号闭合冒烟) |
rf_port_demo | 经 ws63-rf-rs 调用 porting 层 + FRW/HCC 数据通路 |
另有 2 个 crate 内自测示例(在 chips/ws63/rf/examples/):sched_selftest(协作调度器自测)、net_selftest(netif→smoltcp 自测)。此外 examples/bs21(BS21 examples,隔离工作区)和 examples/bs20(BS20 examples,隔离工作区)提供多芯片变种。所有示例全部在姊妹仓 ws63-qemu 经 scripts/smoke-test.sh 端到端验证。仍缺真实连接性(Wi-Fi/BLE/SLE 实际链路)示例(北极星,待 blob 上板 HIL)。
在依赖链中的位置
examples 位于整条依赖链的最下游(叶子节点),消费上游各 crate:
crates/pac/ws63-pac/ws63-svd (XML) crates/pac/bs2x-pac/bs2x-svd (XML)
│ │
└─> ws63-pac (svd2rust) └─> bs2x-pac (svd2rust)
│ │
└─> hisi-riscv-hal (手写安全驱动;chip-ws63/chip-bs21、async/embassy feature)
│
├─> examples/ws63/* (WS63 示例)
├─> examples/bs21/* (BS21 示例,隔离)
└─> examples/bs20/* (BS20 示例,隔离)
hisi-riscv-rt (启动汇编 / 链接脚本 / 中断向量) ──#[entry] + 导出 ws63-link.x──┘
ws63-rf-rs (RF porting 层) ──仅 rf_port_demo / wifi_blob_link 用──┘
每个示例的 Cargo.toml 直接依赖其所需 crate(典型为 hisi-riscv-hal + hisi-riscv-rt;async 示例再加 embassy-*;RF 示例加 ws63-rf-rs)。
链接脚本传播问题已修:hisi-riscv-rt 经 cargo:rustc-link-search 导出 ws63-link.x(hisi-riscv-rt/build.rs),各二进制以自己的 build.rs 用 -Tws63-link.x 引入。因此全部 14 个示例现已可链接并都在 default-members,默认 cargo build 即构建并产 .bin(仅 ws63-flashboot 仍单独排除——它是实验性、非 secure boot,见其 README)。注:blinky/Cargo.toml 历史上多声明了一条 ws63-pac 直接依赖而源码未用(死代码,排期阶段 2 清理)。
关键设计
以 blinky 为最小模板说明裸机入口形态,其余示例在此之上各增量演示一个子系统:
- 入口与运行时集成:用
#[entry](来自hisi_riscv_rt)声明fn main() -> !,并自带#[panic_handler](自旋空转)。这是riscv-rt体系下的标准裸机入口形态。 - GPIO 使用方式:
blinky用 legacy 类型态 GPIO(create_output_pin+set_high()/set_low());gpio_irq则演示新的输入 + 中断路径。HAL 的OutputConfig/InputConfig构建器 API 已落地,示例正逐步覆盖。 - 延时实现:
blinky的delay_ms是手写忙等(按 240 MHz 估算,绕过 HAL timer),属「最小可演示」而非最佳实践;async_delay/embassy_multitask演示了正确的DelayNs/Timer::after路径。 - 自定义内存布局:
custom_memory演示用示例自带的memory.x覆盖hisi-riscv-rt的 bundled 链接脚本(hisi-riscv-rt的默认 featurebundled-memory-x,关掉后由示例侧提供),从而不与 rt 冲突。 - semihosting / CI 信号:
semihost_selftest用 semihostingexit()给 CI 一个免解析 UART 的 pass/fail 退出码。 - 异步:
async_*/embassy_*用 hisi-riscv-hal 的async/embassyfeature +embassy-executor(机制见 async-embassy.md)。 - RF porting:
rf_port_demo经ws63-rf-rs行使 porting 函数,并把 vendor ROM-data blob 链入镜像(g_dmac_alg_main/g_mac_res_etc在 rf-rs 解析)。
与参考实现的关系:esp-hal 示例普遍调用 Delay / embedded-hal trait;ws63 示例集现已从「单一点灯」扩展为覆盖各外设 + async + RF porting 的一组最小演示。
评审发现
优点
- 入口形态正确:
#[entry]+#[panic_handler]的裸机骨架完整,blinky可作后续示例的模板。 - 覆盖面已大幅扩展:GPIO / UART / Timer / DMA + 中断 + 复位 + semihosting + 自定义内存 + async/embassy + RF porting,14 例全部在 ws63-qemu 端到端冒烟。
- 链接已打通且诚实标注:14 例全部在
default-members,cargo build默认即构建;ws63-flashboot的排除附了原因注释。
问题
| 严重度 | 类别 | 问题 | 状态 |
|---|---|---|---|
| 高 | 构建 | (曾)blinky 无法链接:lib 依赖的 cargo:rustc-link-arg 不传播到下游二进制 | ✅ 已修:hisi-riscv-rt 导出 ws63-link.x + 各 build.rs 用 -Tws63-link.x,14 例全部可链接并回到 default-members |
| 高 | 方向 | (曾)唯一示例(blinky)+ 手写忙等,无法证明其余驱动可用 | ✅ 大部已破:现有 UART/Timer/GPIO/DMA + async SPI/I2C 等 13 个额外示例 |
| 中 | 演示覆盖 | blinky 仍用 legacy create_output_pin,未直接演示 OutputConfig/InputConfig | 🟡 gpio_irq 已演示输入/中断路径;blinky 升级待排期 |
| 中 | 文档 | 旧构建指引曾指向自定义 JSON target | ✅ 已统一为 builtin riscv32imfc-unknown-none-elf(硬浮点 ilp32f、无原子;2026-05-31 曾过渡用 stable riscv32imc) |
| 低 | 依赖 | blinky/Cargo.toml 多声明 ws63-pac 直接依赖,源码未用 | 🟡 排期阶段 2 死代码清理 |
| — | 连接性 | 缺真实 Wi-Fi/BLE/SLE 链路示例 | 🔴 待 blob 上板 HIL(阶段 5) |
改进项与排期
- ROADMAP 阶段 1(已大部完成):链接脚本传播已修、示例覆盖面已扩。剩余:把
blinky升级为使用OutputConfig/InputConfig配置 API;真机上板点灯验证。 - ROADMAP 阶段 2(死代码清理):清理
blinky冗余的ws63-pac直接依赖等。 - ROADMAP 阶段 5(连接性示例) 🔴:在 blob 上板(HIL)后新增 Wi-Fi/BLE/SLE 真实链路示例,使示例集真正覆盖 SoC 核心能力。
- ROADMAP 阶段 6(async) ✅ 已完成:
async_delay/async_bus/embassy_multitask/embassy_async_io四个异步示例已落地(依赖 HAL 的async/embassy支持,见 async-embassy.md)。
ws63-flashboot 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
职责与边界
ws63-flashboot 是一个实验性 / 学习用途的 Rust 二级引导(second-stage bootloader),对标 fbb_ws63 原厂 flashboot_ws63/startup/main.c。它本轮(2026-05-31)已被明确标注为实验性、非安全启动、不可用于生产(src/main.rs:1-22、README.md:3、Cargo.toml:5)。当前专为 WS63 设计;BS2X 系列(BS21/BS22/BS20)的引导加载另行开发(见下),复用原厂 flashboot 是生产推荐。
负责(最小化的引导流程):
- 汇编启动:PMP 清零、
mtvec向量模式、关中断、开 FPU、初始化gp/sp、清 BSS、跳flashboot_main()(asm/startup.S)。 - 时钟切换:Flash/UART 从 TCXO 切到 PLL(
src/main.rs:262-278);TCXO 频率检测(24/40 MHz,src/main.rs:65-69)。 - SFC(SPI Flash Controller)四线读初始化与按块读取(
src/sfc.rs:83-171)。 - 镜像头边界校验(
src/image.rs:9-19)与软件 SHA256 完整性校验(src/sha256.rs、src/main.rs:235-258)。 - 看门狗、eFuse 时钟周期初始化、FAMA 重映射,最后跳转到 app 入口(
src/main.rs:280-296、192-202、100-107、135-167)。 - 独立的只写 UART0 调试输出(
src/uart.rs)。
不负责 / 当前不具备:
- 真实性验签(secure boot) —— 没有基于 efuse 根密钥的 ECC-bp256 / SM2 签名校验。
- 分区表解析、A/B app 槽选择、FOTA / 升级、镜像解压、flash 在线加密 —— 这些在原厂 flashboot 中存在,本 crate 为桩或缺失(
src/main.rs:206-231)。 - 不依赖
ws63-pac/hisi-riscv-hal:有意用裸 MMIO 保持独立、避免第二份 PAC 在链接期与hisi-riscv-hal的DEVICE_PERIPHERALS冲突(Cargo.toml:17-19)。
生产正确做法:复用 fbb_ws63 原厂 flashboot,把本仓库构建的 Rust 应用按原厂打包/签名流程烧到原厂 flashboot 加载的 APP 分区(README.md:22-26)。
在依赖链中的位置
ws63-flashboot 不在 主依赖链(SVD → PAC → HAL → examples)上,是一条独立的二进制旁支:
SVD → ws63-pac → hisi-riscv-hal → examples/ws63/* (主链,hisi-riscv-rt 提供启动)
ws63-flashboot (独立 bin,自带 startup.S / uart / sfc / sha256,裸 MMIO,
不依赖 pac/hal/rt;被排除在默认构建之外)
- 它是一个
[[bin]](Cargo.toml:13-15,产物名flashboot),仅依赖riscv与critical-section(Cargo.toml:20-22)。 - 在工作区中它是
members之一(cargo check --workspace仍覆盖),但不在default-members中,默认cargo build不构建它(根Cargo.tomldefault-members仅含ws63-pac/hisi-riscv-hal/hisi-riscv-rt)。 - 它逻辑上位于 PAC/HAL 之“下“:在硬件上电后、Rust 应用(用
hisi-riscv-rt启动 +hisi-riscv-hal驱动)运行之“前“运行,但在代码上与三者完全解耦。
关键设计
- 裸 MMIO 而非 PAC:所有外设地址硬编码为
*mut u32/*const u32常量(src/main.rs:40-48,src/sfc.rs:9-27,src/uart.rs:8-15),刻意不引入ws63-pac,避免双份 PAC 链接冲突(Cargo.toml:17-19)。代价是与 HAL 重复造 UART/SFC/SHA256/startup(见评审)。 - 汇编启动对照原厂:
asm/startup.S注释声明基于 fbb_ws63flashboot_ws63/startup/riscv_init.S,做 PMP 清零、清自定义 CSR0x7d9、从a0保存 boot flag 到__flash_boot_flag、mtvec向量模式(+1)、开 FPU(mstatus.FS=0b11)、清 BSS、tail flashboot_main。 - 单镜像启动(2026-06-01 整改):删除了对
0x4000_0024的 A/B 误用。该寄存器是 flashboot 自身的备份恢复标志(0x5A5A5A5A⇒ 从备份分区恢复 bootloader;vendormain.c:131-135flashboot_need_recovery),不是 app 槽选择器。真实 app A/B 由 upg run-region 配置(PARTITION_FOTA_DATA末尾 magic0x70746C6C、run_region0=A/1=B)+ 分区表(@0x200380)决定 —— 本实验 loader 不解析这些,仅启动单一 app 镜像,A/B/恢复/FOTA 交给原厂 flashboot(src/main.rs:110-131)。 - 镜像头数据结构(整改:对齐 secure_verify_boot.h):
ImageHeader = KeyArea(0x100) + CodeInfo(0x200) = 0x300,按 vendorimage_key_area_t/image_code_info_t(ECC256/SM2 构建)逐字段重排(src/sfc.rs)。CodeInfo的关键字段现在正确:code_area_len在 +0x24(旧代码错读mask_version_ext@+0x14 当长度)、code_area_hash在 +0x28(旧代码错读 +0x1C)。const断言锁定size_of= 0x100/0x200/0x300。 - 校验流程:
validate()做结构边界检查(image_id、structure_version==0x0001_0000、structure_length∈{0x200,0x400}、signature_length∈(0,512]、code_area_len∈(0,8MB),src/image.rs),随后verify_image_integrity()(原verify_sha256)分 256 字节块读 app body、软件 SHA256、与头里的code_area_hash比对(src/main.rs)。这是完整性校验、非真实性验签:哈希在同一份未签名头里,能写 flash 的攻击者可重算 —— 函数名/文档已如实标注。SHA256 软件实现(src/sha256.rs,含""/"abc"/长输入测试)未经审计、仅作完整性用途。 - SFC:
sfc_init()配置四线快读(rd_ins=0xEB Quad I/O,src/sfc.rs:99-104);sfc_read_data()以 16 字(64 字节)为硬件上限分块、轮询SFC_INT_STATUS完成位(src/sfc.rs:137-171)。 - 跳转:清
mie、喂狗后将addr + 0x300transmute为extern "C" fn() -> !并调用(src/main.rs:159-166),SAFETY 注释声明 app 入口同 ABI(RV32IMFC ilp32f)。 - 本轮构建完整性修复(针对该 crate):banner 重写为“非安全启动“警告(
src/main.rs:1-22)、publish = false(Cargo.toml:11)、移出default-members、删除未用的ws63-pac依赖、新增README.md。
评审发现
已对照 fbb_ws63 与 esp-hal、按 file:line 验证,0 条被驳回。
优点
- SHA256 软件实现正确,常量与填充无误,含已知向量单测(
src/sha256.rs:14-141、:148-175)。 startup.S对照原厂riscv_init.S,PMP/FPU/BSS/boot flag 处理到位(asm/startup.S)。- 关键地址(SFC/UART/WDT/FAMA/efuse 寄存器、
FLASHBOOT_RAM语义)与镜像头 magic/版本对照 SDK 一致;整改后镜像头布局对齐secure_verify_boot.h。 - 镜像头边界校验有较完整的拒绝/接受边界单测(
src/image.rs:52-135)。 - 本轮已正确自我定级为实验性:banner、
publish=false、移出默认构建、README 说明(src/main.rs:1-22、Cargo.toml:11、README.md)。
问题
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 严重 | 安全 | 无真实性验签:只把算出的哈希与同一份未签名头里的哈希比对。能写 flash 的攻击者改镜像后重算 SHA256 写回头部即可以 M 态特权跳进任意代码,≠ secure boot(原厂用 efuse 根密钥 ECC-bp256/SM2 签名验签) | src/main.rs、verify_image_integrity();对照 vendor secure_verify_boot.c | ✅ 已如实标注(2026-06-01):函数改名 verify_image_integrity、文档明确“仅完整性、非真实性“;真实 ECC/SM2 验签属 ROADMAP 冻结项(复用原厂,不在本实验件投入) |
| 严重 | 正确性 | ImageHeader/CodeInfo 布局对不上真实 WS63 镜像:image_length(+0x114)/image_hash(+0x11C) 偏移读错 → 会拒绝真镜像 | src/sfc.rs;对照 vendor secure_verify_boot.h:156-178 | ✅ 已修(2026-06-01):sfc.rs KeyArea/CodeInfo 按 image_key_area_t/image_code_info_t(ECC256) 逐字段重排,code_area_len@+0x24、code_area_hash@+0x28,const 断言锁定 0x100/0x200/0x300;评审(layout) ok |
| 高 | 正确性 | A/B 误用 0x4000_0024:该寄存器是 flashboot 自身的备份恢复标志,并非 app 槽选择器。代码却用它选 app 区 A/B | src/main.rs;对照 vendor main.c:131-135(flashboot_need_recovery) | ✅ 已修(2026-06-01):删除该误用,改单镜像启动 + 如实注明真实 A/B = upg run-region(magic 0x70746C6C)+分区表(@0x200380)、0x40000024=bootloader 自恢复 |
| 高 | 方向 | 重写原厂安全关键件(验签/启动链)属误导努力。生产应复用原厂 flashboot,本 crate 仅供学习 | src/main.rs:5-8、README.md:22-26 | 暂不修(定级实验性;定位为学习件,整体方向走复用原厂) |
| 高 | 正确性 | 关键子流程是桩:boot_clock_adapt() 为 TODO 空操作;read_partition_app_addr() 恒返回 FLASH_START;check_upgrade_mode() 恒 false | src/main.rs | 🟡 部分(2026-06-01):read_partition_app_addr() 改为如实标注的桩(注明不解析分区表、真实查表在 @0x200380 magic 0x4b87a54b);boot_clock_adapt/check_upgrade_mode 仍为桩(实验定位,生产复用原厂) |
| 中 | 维护性 | 重复造轮子:UART/SFC/SHA256/startup 与 hisi-riscv-hal/hisi-riscv-rt 重复(因刻意不依赖 PAC/HAL) | src/uart.rs、src/sfc.rs、src/sha256.rs、asm/startup.S、Cargo.toml:17-19 | 暂不修(为保持独立、规避双份 PAC 链接冲突的有意取舍) |
| 中 | 工程化 | 删除未用的 ws63-pac 依赖、publish=false、移出默认构建、banner 改为实验性警告 | Cargo.toml:11,17-19、根 Cargo.toml default-members、src/main.rs:1-22 | 本轮已修 |
改进项与排期
- 生产层面的结论是复用 fbb_ws63 原厂 flashboot(已做签名验签 / A/B / 升级 / 解压 / flash 加密),Rust 应用以 app 镜像形式由原厂 flashboot 加载(
README.md:22-26)。本 crate 维持实验/学习定位。 - 整改已落地(2026-06-01):镜像头布局对齐
secure_verify_boot.h(code_area_len/code_area_hash偏移修正 + const 尺寸断言)、删除0x40000024的 A/B 误用改单镜像启动并如实注明真实 A/B 机制、verify_sha256→verify_image_integrity如实标注“仅完整性非真实性“、read_partition_app_addr桩如实标注。flashboot 现已纳入 CI clippy 门禁(不再--exclude)。真实 ECC/SM2 验签仍按冻结项复用原厂、不在本实验件投入。 - 阶段 0 的构建完整性修复已落地:双份 PAC 消除(registry 版本依赖 + 根
[patch.crates-io]指向本地)、无原子 ISA +portable-atomiccritical-section polyfill(默认 target 现为 ws63 工具链 builtin 的riscv32imfc-unknown-none-elf,硬浮点;2026-05-31 曾过渡用 stableriscv32imc)、CI/release gating 与发布顺序修复、hisi-riscv-rtMIE 中断宏 typo 与栈顶符号 GC fallback 修复。 - 尚未解决并已排期:示例链接(
hisi-riscv-rt链接脚本不传播到下游 bin)见 阶段 1;中断模型(PLIC vs LOCIPRI/LOCIEN)、SPI/I2C/SPI 超时、system reset、GPIO pull、死代码清理见 阶段 2;porting 层 + HCC IPC + blob 链接的连接性见 阶段 3–5;async 见 阶段 6。详见 ROADMAP。
注记:BS2X 引导加载(BS21/BS22/BS20)
WS63-flashboot 当前专为 WS63 SoC 实现。BS2X 系列(BS21/BS22/BS20)作为独立芯片系列,有自己的:
- ROM 代码:不同的掩膜 ROM 版本与启动流程(相似但非完全兼容)。
- 原厂 flashboot:fbb_bs2x 中的 flashboot_bs2x(结构类似但地址/配置寄存器有差异)。
- 推荐方案:复用 fbb_bs2x 的原厂 flashboot 加载 Rust 应用镜像;若需自研,按 WS63-flashboot 模式(对照 secure_verify_boot.h 等)另行实现。
QEMU 验证侧,-M bs21/bs22/bs20 已支持硬件仿真;vendor 的 LiteOS 栈由 hisi-riscv-qemu 虚拟;BS2X 真机引导加载与连接性由 BS2X 团队后续跟进(见 ROADMAP)。
ws63-RF 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
职责与边界
ws63-RF 是 ws63-rs monorepo 的一个 git 子模块(.gitmodules:10-12,URL 指向独立仓库 ws63-RF.git),定位是连接性(Wi-Fi/BT/BLE/SLE)的载体。它负责两件事:
- 重分发 vendor 闭源协议栈——7 个从 HiSilicon WS63 SDK 抽取的预编译 RISC-V 静态库
lib/*.a(合计约 3.1 MB),包含完整的 Wi-Fi MAC 协议栈(HMAC + DMAC + RF 前端控制)与 BLE/SLE 主机协议栈(GAP/GATT/SMP/L2CAP、SLE/GLE)。 - 提供 porting 接口契约——
include/port/port_*.h共 8 个头文件,约 70 个 OS/IPC/缓冲管理抽象函数,外加include/api/(公开 API 头)与include/internal/(blob 内部依赖的类型头)。
它不负责:
- 不提供任何 Rust 绑定、链接胶水或可编译产物——目录中无
.rs/build.rs/Cargo.toml/ bindgen(已find核实,0 结果)。 - 不是 Cargo workspace 成员——根
Cargo.toml的members/default-members与Cargo.lock均未引用ws63-RF(已 grep 核实,0 结果)。因此当前它对cargo check --workspace完全不可见。 - 不实现 porting 层本身——
port_*.h只是接口声明,实现由下游 in-tree cratews63-rf-rs填充,现已实现(见下)。 - 不提供链接脚本——
port_linker.h仅以extern声明 blob 期望的链接符号,实际的SECTIONS/ 内存区段需调用方在 linker script 中给出。
子模块内自带 ARCHITECTURE.md。注意其“连接性 0% / 尚无 Rust 绑定“的旧结论已过时:in-tree crate ws63-rf-rs 现已实现 porting 契约(osal/oal/log/uapi/frw/hcc + 协作调度器 + 软件计时器 + netif→smoltcp 桥),且 Wi-Fi-init 的符号闭合已达成(whole-archive 0 重复符号,--gc-sections rooted at uapi_wifi_init 残留仅 2 个 __wifi_pkt_ram_* defsym)。剩余是真机 bring-up(掩膜 ROM 地址只在硅片上可执行 + 厂商自定义重定位)。
在依赖链中的位置
ws63-RF(blob)经 in-tree crate ws63-rf-rs 接入主链 —— rf-rs 实现 porting 契约、把 blob 链进镜像:
graph TD
subgraph 主链["已接入的 Rust 主链"]
SVD[crates/pac/ws63-pac/ws63-svd] --> PAC[ws63-pac]
PAC --> HAL[hisi-riscv-hal]
HAL --> EX[ws63-examples]
RT[hisi-riscv-rt] -.启动/链接脚本.-> EX
end
subgraph RF["chips/ws63/rf/ws63-RF(闭源 blob 交付)"]
BLOB["lib/*.a 闭源协议栈"]
PORT["include/port/port_*.h<br/>~77 个 porting 接口契约"]
PORT -.接口契约.-> BLOB
end
PLATFORM["ws63-rf-rs(已实现)<br/>osal/oal/log/uapi/frw/hcc + 调度器 + netif→smoltcp"]
HAL --> PLATFORM
PORT --> PLATFORM
PLATFORM -->|实现契约 + 链接 blob| BLOB
BLOB -.HIL:真机连接性示例.-> EX
classDef done fill:#d5f5d5,stroke:#2a2;
classDef hil fill:#fff3cd,stroke:#cc9;
class SVD,PAC,HAL,EX,RT,PORT,PLATFORM done;
class BLOB hil;
- 上游:blob 由 vendor SDK(
fbb_ws63,参考实现在src/drivers/chips/ws63/porting/)编译而来,本仓库只做重分发。 - 下游:blob 的 Wi-Fi/BLE 公开 API 最终供连接性示例调用。中间两层桥已实现(
ws63-rf-rs的 porting + FRW/HCC 数据路径);真正剩下的是真机 HIL(ROM 地址 + 自定义重定位只在硅片上成立)。 - 架构上 WS63 是单核 RISC-V(一个自研应用核——核过 fbb_ws63:
ch2_system.md「系统提供一个自研 RISC-V 处理器作为主控 CPU」、platform_core.h标题 Application Core、rom_config/仅acore、全 SDK 无dcore)。Wi-Fi 协议栈的 HMAC(上层/host MAC)与 DMAC(下层/device MAC)是链接进同一应用镜像的软件库(libwifi_driver_hmac.a/libwifi_driver_dmac.a同在ws63-liteos-app/),都跑在这一颗核上、驱动 Wi-Fi MAC/PHY 硬件。HCC 的 host/device-CPU 语义是 HiSilicon 跨产品线的通用框架模型——真正两颗 CPU 是「外接主控 MCU + WS63 模组」拓扑,不是 WS63 片内有第二颗 RISC-V 核。(更正:早期 README/本文曾写成「ACORE/DCORE 双核」,不准确。)
关键设计
三层产物:blob / 内部头 / porting 头
- 闭源
.a(lib/):libwifi_driver_dmac.a(629 KB,Wi-Fi device MAC + HAL + RF 前端)、libwifi_rom_data.a(3 KB)、libbt_host.a(1.1 MB,BLE host)、libbt_app.a、libbth_gle.a(821 KB,SLE/GLE)、libbth_sdk.a、libbg_common.a。README 的“Library Catalog“表与磁盘实际大小逐项吻合(ls -la lib/核实)。 - internal 头(
include/internal/):blob 内部代码依赖的类型/消息定义(osal_types.h、frw_msg_rom.h、wlan_msg.h、hcc_*.h等),porting 头里的不透明结构(如struct frw_msg)正是在此定义。 - porting 头(
include/port/):调用方必须实现的 8 组接口(每个文件均以port_*.h命名)。
porting 接口分解(README 的 “Dependencies Count” 表,与头文件逐一核对)
| 头文件 | 函数数 | 职责(file 证据) |
|---|---|---|
port_osal.h | 24 | OS 抽象:中断 osal_irq_*、线程 osal_kthread_*、内存 osal_kmalloc/kfree、等待 osal_wait_*、osal_udelay、osal_flush_cache、osal_printk(port_osal.h:44-160) |
port_frw.h | 15 | Wi-Fi 消息分发框架 + 定时器:frw_main_init、frw_fetch_msg_node、frw_send_msg_to_device、frw_task_thread、frw_dmac_timer_*(port_frw.h:28-99) |
port_wlan.h | 11 | 共享内存 ring buffer + RF 时钟:wlan_open/close_wifi_abb_rf_clk、wlan_msg_h2d_*、oal_ring_write/read(port_wlan.h:25-110) |
port_hcc.h | 6 | HCC IPC 传输:hcc_dmac_config_bus_ini、hcc_dmac_service_adapt_start、hcc_wifi_msg_register/send(port_hcc.h:35-77) |
port_oal.h | 7 | 48 KB Wi-Fi packet 缓冲池:oal_memory_init、oal_mem_rsv、oal_get_netbuf_pool_len(port_oal.h:39-79) |
port_uapi.h | 3 | 平台服务:uapi_nv_read(RF 校准/MAC)、uapi_tsensor_get_current_temp(热保护退避)、uapi_systick_get_ms(port_uapi.h:30-72) |
port_log.h | 7 | 日志 + 安全 C 库:log_event_wifi_print0/1/2/4、memset_s/memcpy_s/snprintf_s(port_log.h:30-50) |
port_linker.h | 20+ 符号 | 链接符号声明:__wifi_pkt_ram_begin__/end__、TCM/SRAM 区段、__divdi3/__udivdi3(port_linker.h:38-77) |
合计约 70 个外部符号——README 的 “Key insight”(“所有硬件寄存器访问 hal_/fe_hal_/hh503_* 自包含于 libwifi_driver_dmac.a,~70 个外部符号都是标准 OS 抽象/IPC/缓冲管理”)方向正确。
HCC 共享内存 IPC(连接性的核心机制)
port_hcc.h:8-22:HCC 是 host(HMAC/BLE host)与 device(DMAC/BT controller)之间的传输抽象,是 HiSilicon 跨产品线的通用模型。两种拓扑要分清:(a) WS63 作为模组接在外部主控 MCU 后面时,host=外部 MCU、device=WS63,走 SDIO/SPI bus driver——这才是「两颗 CPU」;(b) WS63 独立运行(ws63-rs 的场景),HMAC 与 DMAC 都在这一颗应用核上,HCC 退化为片内软件层间 + 到 Wi-Fi MAC 硬件的消息通路,没有第二颗核。port_wlan.h 的 oal_ring_ctrl(port_wlan.h:78-84,带 read_idx_addr/write_idx_addr/ring_depth)是该 HCC 传输的无锁环形缓冲控制块。
与参考实现的关系
porting 层的语义对标 vendor SDK fbb_ws63/src/drivers/chips/ws63/porting/(README “References” 明确指向)。这与本仓库其余部分对标 esp-hal 的取向不同——RF 连接性不重写协议栈,而是复用 blob + 移植 OS/IPC 抽象,这一战略判断是正确的(数千行 MAC/BLE 状态机用 Rust 重写既无必要也不现实)。
评审发现
优点
- 战略方向正确:复用经过现场验证的闭源协议栈、只移植 ~70 个 OS/IPC 抽象函数,而非用 Rust 重写 Wi-Fi MAC / BLE host,是务实且正确的判断。
- 依赖面识别准确:README 准确指出“硬件寄存器访问自包含于 blob,外部符号都是 OS 抽象 / IPC / 缓冲管理“,并准确量化为约 70 个 porting 函数。(注:README 同时把 HMAC/DMAC 描述为「ACORE/DCORE 双核」——此点不准确,WS63 单核,见「在依赖链中的位置」;但「依赖面 = OS/IPC/缓冲抽象」这一核心判断方向正确。)
- 接口文档化完整:8 个
port_*.h每个函数都有 doc 注释、返回语义与移植难度评级,port_linker.h给出了内存布局与区段符号清单,为后续移植提供了清晰契约。 - 文档与代码一致:README 的库目录表、依赖计数表与磁盘实际
.a大小、头文件函数数逐项吻合,无夸大。
问题
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 严重 | 方向 | (曾)纯 blob + C 头,无 Rust/链接配置,连接性 0% | — | ✅ 已修:in-tree crate ws63-rf-rs 提供完整 Rust porting + build.rs + 链接搜索;blob 经它链入镜像(wifi_blob_link/rf_port_demo 在 ws63-qemu 验证) |
| 高 | 方向 | (曾)porting 层完全未实现:osal/oal/log/HCC 无一行实现 | chips/ws63/rf/src/* | ✅ 已实现:osal_adapt_*(33 符号) + oal/log/uapi + 协作调度器 + FRW 工作线程 + HCC 传输 + 软件计时器 + netif→smoltcp 桥;frw_hcc_selftest/sched_selftest/netif_smoltcp_selftest 自测通过 |
| 高 | 链接 | (曾)blob 数千未定义符号无一被满足 | mac-link-residual.sh | ✅ Wi-Fi-init 符号闭合达成:whole-archive 0 重复符号;--gc-sections rooted at uapi_wifi_init 残留仅 2 个(__wifi_pkt_ram_begin__/end__ defsym)。早先“~3126/~96 missing“是 whole-archive 上界,被 off-path BT/alt-OS 代码主导(可达路径 0 BT 符号) |
| 高 | 工具链 | 链接 blob 需 ilp32f rv32imfc 目标 | .cargo/config.toml | ✅ 已就位:默认 target 就是 builtin 的 riscv32imfc-unknown-none-elf(硬浮点 ilp32f),原子由 portable-atomic critical-section 垫片提供(之前文档误写 imc) |
| 中 | 集成 | port_linker.h 的 extern 符号与 hisi-riscv-rt 链接脚本的衔接 | hisi-riscv-rt/ws63-rf-rs | 🟡 hisi-riscv-rt 提供 __wifi_pkt_ram_* 的 scaffold --defsym;真机前需把 netif pbuf 布局 pin 到 WiFi 构建的 lwipopts.h、TX sink 指向 blob 真实发送符号(见 ws63-rf-rs README) |
改进项与排期
本组件是 ws63-rs 通往“可用产品“的最大缺口。多数前置已完成,现状如下:
- 阶段 0(已完成):消除双 PAC;默认 target = builtin
riscv32imfc(硬浮点 ilp32f,blob 所需)+ critical-section polyfill。工具链前置已清。 - 阶段 3(链接 blob 尖刺) ✅ 已完成:
chips/ws63/rf/build.rs+ 链接搜索把lib/*.a喂给链接器;hisi-riscv-rt 提供__wifi_pkt_ram_*defsym;Wi-Fi-init 符号闭合达成(残留 2)。wifi_blob_link/rf_port_demo验证。 - 阶段 4(porting + HCC) ✅ 大部已实现:
osal_adapt_*(33) +oal/log/uapi+ 协作调度器 + FRW 工作线程 + HCC 传输 + 软件计时器 + netif→smoltcp 桥,均在 ws63-qemu 自测。剩余是把 pbuf 布局/TX sink pin 到真实 blob + 真机执行。 - 阶段 5(连接性示例) 🔴 待真机:ROM 地址 + 厂商自定义重定位只在硅片上成立,故是 HIL(硬件在环);QEMU 无法跑真 Wi-Fi 链路。
- 阶段 6(async) ✅ 通用异步已就绪:hisi-riscv-hal 的
async/embassy(见 async-embassy.md)已实现并验证;连接性专属的异步包装待 blob 上板后再做。
详见 ROADMAP。
注记:BS2X 多芯片支持(BS21/BS22/BS20)
WS63-RF 中的 blob + porting 层当前专为 WS63 设计(所有路径、符号、校准数据指向 WS63 HMAC/DMAC/RF 前端)。BS2X 系列(BS21/BS22/BS20 统称为 BS2X,含不同内核配置与外设集的变体)有独立的 blob (bs2x-pac + chips/bs2x/ 目录结构)。
- WS63 blob:
chips/ws63/rf/ws63-RF/lib/libwifi_driver_dmac.a等 7 个库,Wi-Fi MAC/RF/BLE/SLE 完整堆栈。 - BS2X blob:QEMU
-M bs21/bs22/bs20及其上的 vendor LiteOS 栈(部分由 hisi-riscv-qemu 虚拟)。QEMU 侧已验证 SPI/GADC/I2C/KEYSCAN/QDEC/RTC/TRNG/WDT/DMA/PDM audio/USB enumeration 的完整功能外设覆盖;真机 BS2X 连接性与 WS63 路线并行,后续由 BS2X 团队跟进。 - hisi-riscv-hal 多芯片:
Cargo.toml有chip-ws63(default)与chip-bs21feature,运行时可选;porting 层(ws63-rf-rs)当前绑 WS63,BS2X 连接性交付另行规划。
详见 ROADMAP 阶段 3-5(WS63 北极星)与 overview.md。
ws63-guide 架构与评审
本文是 ws63-rs 架构文档的一部分。完整评审台账见 架构评审 2026-05,整改排期见 ROADMAP。
职责与边界
ws63-guide 是 WS63 系列 SoC(Wi-Fi 6 / BLE / SLE 星闪 Combo 芯片)的中文硬件手册,使用 Sphinx + MyST 构建,逆向自 vendor(HiSilicon)文档。它以子模块形式挂在 ws63-rs monorepo 下(chips/ws63/guide/)。同时,BS2X 系列(BS21/BS22/BS20)有独立的 chips/bs2x/guide 手册,采用相同 Sphinx 工程化。
负责:
- 用人类可读的中文描述芯片的硬件行为:系统/复位/时钟/低功耗、存储器地址空间映射、中断系统、QSPI(SFC)控制器、Wi-Fi/BLE/SLE 的 RF/ABB/PHY/MAC、安全子系统、外设寄存器(GPIO/UART/I2C/SPI/PWM/DMA/IOMUX/ADC/TSENSOR/I2S)、JTAG。
- 提供逆向得到的内存图、中断编号表、寄存器位描述等“原始 IP“,供 PAC/HAL 开发时核对硬件语义。
- 产出 HTML(GitHub Pages)与 PDF 两种交付物。
不负责:
- 不描述 Rust 代码架构。代码侧的设计与评审由
docs/(本架构文档体系)承担。 - 不参与 Cargo workspace 构建,不是 crate;它是独立的 Python/Sphinx 项目(
pyproject.toml中package = false),有自己的uv.lock与.github/workflows/docs.yml。
关键边界判断:本手册与 docs/ 互补而非重复——本手册讲硬件(寄存器、电气、协议层硬件块),docs/ 讲 Rust 代码架构(crate 职责、依赖链、设计模式、评审)。两者受众不同、构建链不同,内容零重叠。
在依赖链中的位置
ws63-guide 不在 Rust crate 编译依赖链(SVD → PAC → HAL → examples,rt 提供启动)之内,而是横向的知识来源:
flowchart LR
vendor[HiSilicon vendor 文档] -->|逆向/MinerU 提取| guide[ws63-guide 硬件手册]
guide -.参考硬件语义.-> svd[ws63-svd]
guide -.参考寄存器位/中断/内存图.-> hal[hisi-riscv-hal]
svd --> pac[ws63-pac] --> hal --> ex[ws63-examples]
rt[hisi-riscv-rt] -. 启动/链接 .-> ex
文字版:vendor 文档经逆向(手册自述基于 MinerU 提取的 Markdown 重建,见 ws63-guide/index.rst:20-22)形成本手册;本手册的内存图、中断表、寄存器描述是 SVD/PAC/HAL 实现的事实依据,但二者无编译期耦合,独立演进。
关键设计
- 独立 Sphinx + MyST 工程,配置不在仓库根。Sphinx 配置位于
ws63-guide/source/conf.py,因此所有构建命令都需-c source标志(ws63-guide/CLAUDE.md:30,ws63-guide/README.md:28)。根目录另有一份ws63-guide/index.rst与ws63-guide/source/index.rst两个 toctree 入口(root_doc = 'index',conf.py:46)。 - 9 章 + 附录的 toctree。
source/index.rst:4-16定义章节顺序:preface、ch1_overview、ch2_system、ch3_qspi、ch4_wifi、ch5_security、ch6_peripherals、ch7_jtag、appendix。其中 ch3/ch4/ch6 是子目录(含各自index.md),如ch4_wifi/{rf,abb,phy,mac,ble_sle,radar}.md、ch6_peripherals/{gpio,uart,i2c,spi,pwm,dma,iomux,adc,tsensor,i2s,qspi}.md。 - MyST 扩展与中文渲染。
conf.py:23-38启用colon_fence/deflist/html_image/dollarmath/substitution/replacements/smartquotes,并把mermaid/list-table/figure/danger等当作 directive 处理。主题为sphinx_book_theme(conf.py:55,注意 README.md:118 仍写的是 sphinx-rtd-theme,已过时);语言zh_CN。 - PDF 中文支持靠 xeCJK。
conf.py:97-100用xeCJK+Droid Sans Fallback渲染中文,CI 安装完整 TeX Live(ws63-guide/CLAUDE.md:32)。numfig中文编号格式见conf.py:158-165(图 %s / 表 %s)。 - 构建后拷贝 Markdown 源。
conf.py:179-194注册build-finished钩子,把source/**/*.md原样复制到 HTML 输出目录,便于 AI/爬虫访问原始 Markdown。 - 有价值的逆向 IP(与参考实现的关系):
- 存储器地址空间映射:
source/ch2_system.md:162“表2-4 存储器地址空间映射”,逐段列出0x0010_0000–0x4000_3FFF等区间,是 hisi-riscv-rt 链接脚本/ws63-svd 基址的事实依据。 - 中断系统模型:
source/ch2_system.md:328-424描述真实硅片的中断模型——支持向量/直接模式、优先级配置寄存器共 3bit 可配 7 级(ch2_system.md:332)、1 个 nmi + 64 个非标准外部中断(ch2_system.md:152),并给出“表2-5 非标准中断编号列表“(Timer/RTC/I2C/GPIO 组合中断/SPI/WLAN PHY&MAC/BLE/SLE/TSENSOR 等,ch2_system.md:339-424)。这正是 HAL 中断子系统建模错误(误用 PLIC,应为 LOCIPRI/LOCIEN)的正确参照系。 - RF/ABB 逆向:
source/ch4_wifi/rf.md描述 2.4G RX/TX/PLL、校准(RX DC、TX LO Leakage、TX Power、TRX IQ)等,对 undocumented 的 RF blob 极有价值。 - 安全子系统寄存器:
source/ch5_security.md描述对称(AES/SM4,ECB/CBC/CTR/CCM/GCM 等模式)、HASH(SHA1/SHA2/SM3)、PBKDF2、非对称、RNG 模块。 - QSPI/SFC:
source/ch3_qspi/registers.md(约 21KB)逆向了 SFC 寄存器,配images/fig-3-*读写时序流程图。
- 存储器地址空间映射:
- 图片资产:
source/images/共 18 张 JPEG(芯片框图、典型应用、SFC 框图与读写流程、RF/ABB 框图、UART 数据格式、I2C 收发时序、危险/提示图标)。
评审发现
优点
- 独特的逆向 IP:RF/外设/安全寄存器描述、存储器地址映射、中断编号表,对一颗 undocumented 的芯片极具价值,是 PAC/HAL 核对硬件语义的权威中文参照(如中断模型、内存图)。
- 与代码文档清晰分工:硬件手册(本组件)与 Rust 架构文档(
docs/)受众不同、内容零重叠,互补关系明确(见ws63-guide/ARCHITECTURE.md:5-7)。 - 工程化完善:
uv.lock锁定依赖、-c source配置隔离、HTML/PDF/linkcheck 三类构建、构建后拷贝 Markdown 源供机器读取,自带 CI/CD。 - 覆盖完整:9 章 + 附录覆盖系统/QSPI/Wi-Fi&BLE&SLE/安全/外设/JTAG,子目录拆分粒度合理。
问题
| 严重度 | 类别 | 问题 | 证据(file:line) | 状态 |
|---|---|---|---|---|
| 低 | 方向 | 与 Rust 代码架构文档(docs/)零重叠、受众不同;独立 Sphinx 构建链与 workspace 分离,维护面双倍 | ws63-guide/source/conf.py:30(-c source)、ws63-guide/pyproject.toml(独立工程)、ws63-guide/ARCHITECTURE.md:5-7 | 暂不修(这是互补关系而非缺陷,刻意分离) |
| 低(方向) | 范围 | 手册应冻结扩张、聚焦连接性:当前价值已确立,继续扩章节会分散到连接性里程碑的精力 | ROADMAP.md:138(CI/文档/SVD 持续扩张冻结)、ROADMAP.md:140(保留 ws63-guide 独特逆向 IP 但停止扩张) | 已排期(ROADMAP “冻结/降优先级”:保留、停止扩张) |
| 低 | 文档一致性 | README 技术栈列 sphinx-rtd-theme,实际 conf.py 用 sphinx_book_theme,记述过时 | ws63-guide/README.md:118 vs ws63-guide/source/conf.py:55 | 暂不修(不影响构建,留作小修;非本轮整改范围) |
说明:本组件无被驳回项,评审要点已对照 fbb_ws63 / esp-hal 与 file:line 验证。手册内容本身(中断模型、内存图、寄存器位)经核实与真实硅片一致,恰好是 HAL 侧若干正确性问题(中断 PLIC 误建模等)的纠偏依据。
改进项与排期
本组件无本轮(阶段 0)整改项——阶段 0 的构建完整性修复(双 PAC 消除、默认 target ISA 改 riscv32imc、flashboot 实验化、CI/release 修复、hisi-riscv-rt MIE 宏 typo)均落在 Rust crate 侧,不涉及本手册。
注记:BS2X 多芯片手册(BS21/BS22/BS20)
本手册专门为 WS63 编写。BS2X 系列(BS21/BS22/BS20,不同内核配置与外设集)有独立的硬件手册 (chips/bs2x/guide/source/),独立 Sphinx 工程、相同的逆向工艺与工程化标准:
-
覆盖范围:BS2X 系统/复位/时钟/存储映射/中断系统、QSPI、BLE/SLE(无 Wi-Fi MAC,RF 由 PHY 直驱)、安全/外设/JTAG。
-
与 WS63 的异同:共享大部分 IP(I2C/SPI/UART/DMA/GPIO/ADC 等),但核心 (RISC-V 配置)、RF (BLE/SLE PHY)、部分外设(如音频链路)有差异。
-
维护:两份手册独立演进;冻结扩张的方针同样应用于 BS2X 手册(ROADMAP “冻结/降优先级”)。
-
冻结扩张、聚焦连接性(ROADMAP “冻结/降优先级”):手册保留为独特逆向 IP,停止新增章节,把精力投向连接性北极星(在真实 EVB 上连上 AP 并 ping 通)。
-
作为下游纠偏的事实依据:手册的中断编号表与优先级模型(
source/ch2_system.md:328-424)应在 ROADMAP 阶段 2 用于修正 HAL 的中断子系统建模错误(PLIC → LOCIPRI/LOCIEN);内存图(ch2_system.md:162)服务于 阶段 1 的链接脚本集成;RF/ABB 章节(ch4_wifi/)服务于 阶段 3–5 的 blob 链接与连接性。 -
小修(非阻断):将 README 技术栈中的
sphinx-rtd-theme更正为sphinx_book_theme,与conf.py:55对齐。
异步与 embassy 适配
hisi-riscv-hal 的异步层(
async/embassyfeature)如何工作、代码在哪、以及之后如何上游化。 总体架构见 overview.md。
一句话
hisi-riscv-hal 在阻塞驱动之上加了一层中断 + waker 驱动的异步驱动(async feature) 和一个 embassy-time
Driver(embassy feature)。于是同一套 HAL 既能阻塞用,也能在 embedded-hal-async / embassy-executor
下 .await。全部跑在单核、无原子扩展的 WS63 上,靠 portable-atomic + critical-section 垫片。
三块地基
1. asynch::block_on + IrqSignal(crates/hisi-riscv-hal/src/asynch.rs)
block_on(fut):极简单 future 执行器 —— poll,Pending 就wfi休眠,硬件中断唤醒后重 poll。无堆、无全局执行器。给“不上 embassy 也想.await“的场景用。IrqSignal:const可构造的「ISR → future」桥。一个portable_atomic::AtomicBool(fired 标志)+ 一个critical_section::Mutex停放的Waker。驱动把它放进static;ISR 调signal(),future poll 时take_fired()/register(waker)。
2. 每驱动的 on_interrupt 钩子(不自动装 ISR)
关键设计:异步驱动不抢占中断向量。每个驱动导出一个 on_interrupt(timer::on_interrupt(ch)、gpio::on_interrupt(bank)、uart::on_interrupt(idx)、lsadc::on_interrupt()、dma::on_interrupt()、embassy::on_alarm_interrupt()),由应用的 trap 处理函数按 mcause 路由过去(见示例)。
这样开 async/embassy feature(哪怕被 cargo 工作区特性合并全局打开)绝不改变非异步固件的行为 —— 因为没有任何 ISR 被默认安装。
3. WS63 没有原子扩展 —— 怎么跑起来的
WS63 是 riscv32imfc(无 A 扩展,lr.w/sc.w 会陷入)。
- hisi-riscv-hal 一直用
portable-atomic(开critical-sectionfeature)做 CAS 垫片;hisi-riscv-rt提供riscv/critical-section-single-hart实现。 - embassy-executor 在无 CAS 目标上也能跑(thumbv6m / riscv32imc 同理):它内部按编译期 cfg 在
core::sync::atomic与portable_atomic间切换,riscv 平台模块的SIGNAL_WORK只用 load/store(WS63 支持)。所以无需改 embassy。 - 一个真实踩过的坑:
target/里陈旧的 host proc-macro 工件(syn/quote 来自旧 rustc)会让 embassy 宏构建莫名失败 →cargo clean后全量通过。
embassy-time Driver(crates/hisi-riscv-hal/src/embassy.rs)
让 WS63 成为 embassy-time 的时间提供者,于是 Timer::after/Instant/Ticker 在 embassy-executor 下可用:
now():读 TCXO 64 位自由计数器(24 MHz),缩放到 embassy-time 的 1 MHz tick(微秒)。单调、跟随真实(QEMU 上是虚拟)流逝时间。schedule_wake(at, waker):把 waker 入embassy-time-queue-utils::Queue;若最早截止变了,用一个 TIMER 通道(ALARM_CH,IRQTIMER_INT0)编程一次性闹钟。on_alarm_interrupt():闹钟 IRQ 触发时排空到期 waker、重新武装下一个截止。- 经
embassy_time_driver::time_driver_impl!注册为全局 driver(导出_embassy_time_now/_embassy_time_schedule_wake,embassy-time 链接它们)。
应用侧:开 embassy-time/tick-hz-1_000_000(对齐 TICK_HZ)、把闹钟通道的 trap 路由到 on_alarm_interrupt、enable_global()。其余照 embassy 标准用法。
代码地图
| 文件 | 内容 |
|---|---|
crates/hisi-riscv-hal/src/asynch.rs | block_on + IrqSignal(地基) |
crates/hisi-riscv-hal/src/embassy.rs | embassy-time Driver(now/alarm/queue) |
crates/hisi-riscv-hal/src/timer.rs (末尾) | AsyncDelay(DelayNs)+ on_interrupt |
crates/hisi-riscv-hal/src/gpio.rs (末尾) | Wait(GPIO 边沿/电平)+ on_interrupt |
crates/hisi-riscv-hal/src/uart.rs (末尾) | embedded_io_async::{Read,Write} + on_interrupt |
crates/hisi-riscv-hal/src/spi.rs (末尾) | embedded_hal_async::spi::SpiBus(包装阻塞) |
crates/hisi-riscv-hal/src/i2c.rs (末尾) | embedded_hal_async::i2c::I2c(包装阻塞) |
crates/hisi-riscv-hal/src/lsadc.rs (末尾) | read_async(自研;IRQ 72) |
crates/hisi-riscv-hal/src/dma.rs (末尾) | wait_transfer_done(自研;IRQ 59) |
crates/hisi-riscv-hal/Cargo.toml | async / embassy feature + 可选依赖 |
示例(均在 ws63-qemu smoke-test 验证):
examples/ws63/async_delay(block_on + DelayNs)、async_bus(SPI/I2C/LSADC)、
embassy_multitask(embassy 多任务 + embassy-time)、embassy_async_io(capstone:embassy + GPIO Wait + async UART)。
覆盖范围
实现了 embedded-hal-async / embedded-io-async 对 WS63 适用的全部 trait:DelayNs(timer)、digital::Wait(GPIO)、spi::SpiBus、i2c::I2c、io Read/Write(UART);外加两个完成中断外设的自研异步(DMA IRQ 59、LSADC IRQ 72)。RTC/I2S/PWM 等无标准 async trait、语义为周期/流式/一次性 —— 保持阻塞,需要时按同一 IrqSignal+on_interrupt 模式加(RTC 的 IRQ 29 已建模)。
之后怎样上游化
按“上游“对象分四条线,都不需要改 embassy 本身:
-
embassy 支持 —— 两种正规模型,WS63 走 out-of-tree 那条。 embassy 仓库确实收录了一批 in-tree HAL(
embassy-nrf/-stm32/-rp/-nxp/-imxrt/-microchip/-mspm0/-mcxa…,主要是主流 Cortex-M),它们由 embassy 维护者承诺维护、与 embassy 内部同步演进。 但 embassy 同时提供一套给树外 HAL 用的接缝(embassy-time-driver+embassy-time-queue-utils+embassy-executor的 platform 抽象)—— 树外 HAL 只实现这些 trait 即可,无需进 embassy 仓库。最大的例子是 esp-hal(Espressif 自己维护、在 esp-rs/esp-hal,不在 embassy 仓库),社区还有 ch32-hal/py32-hal 等几十个。 WS63 属于后者(esp-hal 模型),原因:① in-tree 要 embassy 维护者采纳并长期维护该芯片——对一颗 niche 的 HiSilicon 厂商芯片门槛极高;② embassy 的 in-tree HAL 全部基于 stable rustc 标准 target 构建,而 WS63 现在依赖自定义ws63工具链(无原子 target 烤进 builtin)——这是进 embassy CI 的硬阻塞(见第 3 点);③ hisi-riscv-hal 本就是独立 HAL(阻塞 embedded-hal + 可选 embassy),天然适合树外。 所以上游化 = 把带embassyfeature 的 hisi-riscv-hal 发布到 crates.io(版本结构已就绪),而不是塞进 embassy monorepo。想更解耦可拆embassy-time-ws63,但非必需。- 跟版:盯
embassy-time-driver(现 0.2)/embassy-time-queue-utils(0.3)/embassy-executor(0.10)的 semver;破坏性改动(如Driver从 alarm-handle 改成schedule_wake+queue)集中在embassy.rs一个文件。
- 跟版:盯
-
embassy-executor 已是上游:我们直接用
platform-riscv32,零改动。无 CAS 支持是它已有能力(thumbv6m 同理)。无需上游任何东西。 -
工具链 / target(最大的“非上游“项):现在依赖自定义
ws63rustc(把riscv32imfc-unknown-none-elf无原子 target 烤成 builtin)。两条上游路:- 短期:改用 rustc 已有的稳定 target(如
riscv32imc/riscv32imac)+-Z build-std+build-std-features,去掉自定义工具链依赖 —— 代价是需要 nightly/-Z。 - 长期:把这个 target spec 提交进 rustc(niche,门槛高),或推动官方加
riscv32imfc-*。 - 现状对异步无影响:异步只依赖
portable-atomic+critical-section,与 target 是否上游正交。
- 短期:改用 rustc 已有的稳定 target(如
-
QEMU 模型(ws63-qemu):把
-M ws63板卡 +-cpu ws63命名核 + xlinx 自定义 ISA 解码上游到 QEMU —— 这是 ws63-qemu ROADMAP 阶段 6(github.com/hispark-rs/hisi-riscv-qemu)。与本仓异步无直接关系,但能让 CI 不依赖 fork 的 QEMU。
简言之:异步/embassy 这块本身已经是「按上游约定正确实现」,真正的上游化工作量在 ① 把 hisi-riscv-hal(含 embassy feature)发版到 crates.io、② 摆脱自定义 rustc 工具链、③ ws63-qemu 进 QEMU 主线 —— 三者互相独立。