Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

引言

这是 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。见生态贡献者路径

仓库

教程 · 选择你的路径

欢迎!本章是一组手把手的课程:给确切的命令、给你应当看到的输出,照着做就能成功。

在开始之前,先选一条适合你的路径——两条路径面向不同的人、起点也不同,但风格一样(学习导向、一条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 runjust flash“的最短上手路径。

你需要准备什么

  • 一台 Linux 电脑(本路径在 x86_64 Linux 上验证)。
  • 已安装 rustupgitcurl
  • 第 1 课会带你装好其余工具(自定义工具链、cargo-generatejust、烧录器、可选 QEMU)。
  • 开发板可选:QEMU 不需要硬件;只有要烧真机(第 2 课后半段)时才需要一块 WS63 开发板。

三节课

  1. 搭建环境(应用开发) —— 装好 hisi-riscv 工具链、cargo-generatejusthisi-fwpkg、烧录用的 probe-rs 分支,以及可选的 QEMU。
  2. 从模板创建你的第一个工程 —— cargo generate 生成 blinky 工程,just run 在 QEMU 里跑,再 just flash 烧到真板看 LED 闪。
  3. 改造成一个 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-gnuaarch64-apple-darwinx86_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 依次做了这些事:

  1. cargo build --release 编出 ELF;0x300 HiSilicon 启动头由 hisi-riscv-rtboot-header 特性在链接时直接烘进 ELF,无需单独的打包步骤;
  2. hisi-fwpkg patch-hash <elf> 在链接后就地补齐 ELF body 的 SHA-256(链接时占位是零);
  3. 用打补丁的 probe-rs 分支把这枚裸 ELF 直接 download 到应用分区 0x00230000(不经过 .img 中间文件);
  4. 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 appuart_hello

cargo generate --git https://github.com/hispark-rs/hisi-rs-template
  • 项目名:比如 my-uart
  • Target chipws63(默认)。
  • 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 上验证)。
  • 已安装 rustupgitcurl
  • 第 1 课会带你克隆monorepo(带子模块)、装好工具链、QEMU 和烧录器。
  • 第 3 课(HIL)需要一块 WS63 开发板;第 1、2 课只用 QEMU,无需硬件。

三节课

  1. 搭建环境(贡献生态) —— 克隆monorepo、装工具链/QEMU/probe-rs,并以一次成功的 cargo build -p blinky 收尾。
  2. 构建与运行示例集 —— 在 QEMU 里跑完整示例目录:GPIO、UART、中断、半主机退出码。
  3. 第一次硬件在环测试 —— 把 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 签名(efuse SEC_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 WS63timer_irq 应出现 timer irq #。 而 blinky 因为没有串口输出,脚本会特别提示“请用 LED / 逻辑分析仪人工确认“—— 正是你在第 2、3 步亲手做的事。

HIL 框架的整体设计、标记串约定、它和 QEMU 冒烟测试的关系,见 HIL 测试框架运行 HIL 冒烟测试

恭喜!你已经完成了贡献者路径的全部三课:克隆仓库、装好工具链,在 QEMU 里跑通了 示例集,最后在真正的 WS63 芯片上完成了第一次硬件在环测试。

接下来想做点什么?

  • 想完成具体任务(新建工程、加驱动、调试读内存)——看 操作指南
  • 想查命令、地址、API——看 参考
  • 想搞懂背后的原理——看 原理与背景

操作指南 · How-to Guides

这一章是任务导向的菜谱:每篇回答一个「如何做某件事」的具体问题,假设你已经掌握了基础(不懂概念请看原理与背景,要查字段/地址/标记串请看参考)。每篇都给出可照做的步骤,并尽量覆盖真实环境里的变体、坑和排错。

构建 · Build

打包与烧录 · Flash

测试 · Test

开发 · Develop

如何安装 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.tomltarget = "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 示例(blinkyuart_hellotimer_irqspi_loopbacki2c_scanasync_delayembassy_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.tomlmembers),和它在磁盘上的 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 烧录到真机

这是 2026-06-14 在真实 WS63 硅片上跑通的验证主路径blinky 上电启动 + 翻转 GPIO0)。WS63 用 hisi-riscv-rtboot-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-fwpkgChip::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.shhisi-fwpkg image.img,再 --binary-format bin --base-address 落位)——对 BS2X 正确,对 WS63 也仍能跑通(.img 与 boot-header ELF 的 body 一致)。WS63 的精简 route 2 推荐路径见上面「三步走」或模板 justfilepatch/flash recipe(hisi-fwpkg patch-hash + 直接烧 ELF)。

可用环境变量:

变量含义默认
PROBE_RS_YAMLfork 的芯片描述 yaml(必填
CHIPprobe-rs --chipWS63
CHIP_KINDws63/bs21(选默认 app 基址)ws63
BASE_ADDRESSapp 分区基址ws63 0x00230000 / bs21 0x00090000
PROBE_RSprobe-rs 二进制probe-rs

如要直接用本地编译出来的 fork:PROBE_RS=/home/.../probe-rs/target/debug/probe-rs

排错

  • 'probe-rs' not foundchip '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 落在头区,不会进你的程序。

之后

要边烧边看 UART、或让 cargo run 直接烧真机,见如何用硬件 runner 让 cargo run 烧真机;要 attach 调试/读内存见如何用 probe-rs 调试与读内存

如何用 hisiflash 烧录到真机

这是厂商串口 / YMODEM 路径:把程序打成 .fwpkg,用 hisiflash 经串口烧进去。它不需要 SWD/JTAG 探针,只要一根 UART 线——适合手上没有补丁版 probe-rs 探针、或就想用厂商工具的场景。

想用探针的验证主路径请看如何用 probe-rs 烧录。两者怎么选见文末。

前提

  • hisiflashcargo 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.shMETHOD=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_BAUDhisiflash 默认 921600
LOADERBOOT厂商 LoaderBoot bin(必填
ADDRESS程序写入偏移(必填
HISIFLASHhisiflash 二进制hisiflash

波特率注意:fwpkg/YMODEM 流程常见 230400,更稳的可降到 115200hisiflash 本身的 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-rtboot-header feature,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-rtboot-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-templatejustfilepatch / 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*.img0x300 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-templatejustfile——just patchcargo 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 forkhisi-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_RSprobe-rs 二进制PATH 里的 probe-rs
PROBE_CHIPprobe-rs --chipWS63
PROBE_YAML--chip-description-path yaml空 = 用内置数据库
HISI_FWPKGhisi-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-hashprobe-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 imagehisi-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_helloHello from WS63UART banner(验证 160 MHz 波特基准)
timer_irqtimer irq #OK: timer定时器中断投递(验证 24 MHz TCXO 时钟)
gpio_irqgpio irq #GPIO 中断投递
reset_demoreset_reason=Software软复位 + 复位原因
spi_loopbackSPI loopback OK阻塞 SPI0(真机需先短接 MOSI↔MISO
i2c_scanscan doneno devicesI2C0 总线扫描

两个示例不在自动断言里:blinky(GPIO0 翻转无 UART——用 LED / 逻辑分析仪看)、semihost_selftest(需要调试器的 semihosting——裸 HIL 跳过)。

读懂结果

  • 每个 check 打印 PASS: '<pat>' seenFAIL。FAIL 时会把串口最后几行 / flash 错误尾部打印出来帮你定位。
  • 末行汇总 HIL SMOKE: PASS(退出码 0)或 HIL SMOKE: FAIL(退出码 = 失败数 / 非零)。
  • 常见 FAIL 原因:
    • flash failed:烧录环境没配好(缺 yaml/LOADERBOOT/探针),看尾部错误。
    • 标记串没出现但板子像在跑UART_BAUD 不对(示例用 8N1,默认 115200),或 SETTLE 太短没等到输出——调大 SETTLE
    • spi_loopback FAIL:真机上没短接 MOSI↔MISO(QEMU 会自环,真机不会)。

封装与 CI

  • .claude/skills/hil-smoke 是这个脚本的 wrapper skill,给 agent 一键跑全套 HIL 冒烟。
  • .github/workflows/hil.ymlself-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

工程带一个 justfilecargo install just),封装了硬件验证过的流程:

配方做什么
just buildcargo 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 flashWS63:patch 后 probe-rs download 直接烧裸 ELF 再 reset;BS2X:image 后 probe-rs download*.img 烧进 app 分区再 reset
just fwpkghisi-fwpkg pack*.fwpkg(hisiflash/厂商路径)
just cleancargo clean + 删 img/fwpkg

烧录配方的前提(构建/run 不需要这些):hisi-fwpkg补丁版 probe-rs fork + 其 HiSilicon_WS63.yamlCHIP/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 patchhisi-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 验证,先对照分区表确认)。

之后

如何新增一个外设驱动

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.rspub 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.rsspi_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.rsembedded_hal_async::spi::SpiBus)。

3. Sealed trait(需要时)

如果你要引入「只能内部实现」的标记 trait(比如限定哪些类型能当某外设的输入/输出,或 DMA word),加在 private.rs:以 Sealed 为 supertrait,外部就无法实现。现有的有 DmaWordPeripheralInputPeripheralOutput不要复活已删掉的空 DriverMode/Blocking/Async 标记 trait——它们没有真实 async 后端时纯属误导。

4. 配一个带 PASS 标记的 HIL 示例

新建一个示例 crate(如 examples/ws63/myperiph_demo),用 UART0 打印一个HIL 能 grep 的标记串,并在根 Cargo.tomlmembers / 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.xcrates/hisi-riscv-rt/asm/startup.S。默认配置:576K SRAM、16K ITCM、16K DTCM。TCM 与 SRAM 大小可经 CONFIG 标志配置(参见 fbb_ws63)。

启动流程的“为什么“见 启动流程

内存区域(MEMORY{}

区域属性ORIGINLENGTH结束说明
BOOTROMrx0x1000000x9000 (36K)0x109000掩膜 ROM 启动码
ROMrx0x1090000x43000 (268K)0x14C000应用 ROM:SFC、pinmux、watchdog、timer、systick、TCXO、BT、WiFi
ITCMrwx0x14C0000x4000 (16K)0x150000指令 TCM(默认 16K,可配至 64K)
DTCMrw0x1800000x4000 (16K)0x184000数据 TCM(默认 16K,可配至 64K)
FLASHrx0x2000000x800000 (8MB)0xA00000外部 SPI NOR flash,XIP
PROGRAMrx0x2303000x240000 (~2.25MB)0x470300flash 内应用代码区(启动头之后)
SRAMrwx0xA000000x90000 (576K)0xA90000主系统 RAM(SRAM/L2RAM)
PRESERVErw0xA90000 - 0x100 = 0xA8FF000x100 (256B)0xA90000SRAM 尾部 256 字节,保留启动状态

BOOTROM + ROM 共 304K(36K + 268K),地址连续(0x1000000x14C000)。

关键地址

名称地址说明
app 分区0x230000flashboot 从此处加载 app 镜像
app 入口0x230300入口 = 分区地址 + 0x300(跳过 0x300 字节镜像头)
复位向量0x100000链接为程序内存中的第一项;reset_vector: j HandleResetstartup.S
栈顶 _stack_start0xA90000= ORIGIN(SRAM) + LENGTH(SRAM)

flashboot 无条件跳到 app_partition + 0x300,故 app 镜像必须带 0x300 字节 HiSilicon 镜像头。镜像头字段见 应用镜像格式

导出的链接符号(PROVIDE

区域符号(用于运行时重定位):

符号
__rom_startORIGIN(ROM) = 0x109000
__rom_lengthLENGTH(ROM) = 0x43000
__itcm_startORIGIN(ITCM) = 0x14C000
__itcm_lengthLENGTH(ITCM) = 0x4000
__dtcm_startORIGIN(DTCM) = 0x180000
__dtcm_lengthLENGTH(DTCM) = 0x4000
__sram_startORIGIN(SRAM) = 0xA00000
__sram_lengthLENGTH(SRAM) = 0x90000
__flash_startORIGIN(FLASH) = 0x200000
__flash_lengthLENGTH(FLASH) = 0x800000
__program_startORIGIN(PROGRAM) = 0x230300
__program_lengthLENGTH(PROGRAM) = 0x240000

riscv-rt v0.14 所需符号:

符号
_stack_startORIGIN(SRAM) + LENGTH(SRAM) = 0xA90000
_max_hart_id0
_hart_stack_size0x2000

数据/BSS 符号(memory.x 中为占位 0,权威值在 layout.ld):__sidata__sdata__edata__sbss__ebss

栈大小(可被用户覆盖)

符号默认值用途
__stack_size0x2000 (8K)用户栈
__irq_stack_size0x800 (2K)IRQ 栈
__exc_stack_size0x800 (2K)异常栈
__nmi_stack_size0x400 (1K)NMI 栈

IRQ/异常/NMI 栈顶在 layout.ld.stacks 段中权威定义(trap 处理器引用它们,KEEP.trap 段经 --gc-sections 保活)。

区域别名(riscv-rt v0.14 REGION_ALIAS

别名指向
REGION_TEXTPROGRAM
REGION_RODATAPROGRAM
REGION_DATASRAM
REGION_BSSSRAM
REGION_STACKSRAM
REGION_HEAPSRAM

复位向量

startup.Sreset_vector 位于 .text.entry 段,链接为程序内存第一项,内容为 j HandleResetHandleReset 依次:禁用 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 8N1semihost_selftest 走 RISC-V 半主机(semihosting),不走 UART。

如何构建/运行见 构建一个示例在 QEMU 里运行。HIL 标记串汇总见 HIL 标记串与环境变量

一览表

示例用途观测通道成功标记串需接线QEMU真机
blinkyGPIO0 1 Hz 闪灯(现代 Output 路径)GPIO无(GPIO0 翻转,逻辑分析仪/LED 观测)✅ (2026-06-14)
uart_helloUART hello + tick 计数UARTHello from WS63 on QEMU!⚠️⁹
gpio_irqGPIO0 pin0 上升沿 → IRQ 33(自定义 local IRQ ≥32)UARTOK: custom local IRQ (>=32) delivered否¹⚠️
i2c_scanI2C0 100 kHz 扫描 0x08..0x77UARTscan done / no devices acked否²⚠️³
spi_loopbackSPI0 Mode0 1 MHz 全双工自环UARTSPI loopback OK真机需短接 MOSI↔MISO⚠️⁴
dma_loopbackMDMA 外设环 + SDMA mem→memUARTDMA LOOPBACK TEST: PASS⚠️
timer_irqTIMER_0 周期 → IRQ 26(local trap)UARTOK: timer interrupts delivered⚠️
async_busasync SpiBus + I2c + LSADC(block_onUARTASYNC BUS: PASS⚠️
async_delayasync DelayNs(TIMER0 + wfiUARTASYNC DELAY: PASS⚠️
embassy_async_ioembassy GPIO Wait + async UART + TimerUARTEMBASSY ASYNC IO: PASS否¹⚠️
embassy_multitaskembassy 双任务 Timer::afterUARTEMBASSY MULTITASK: PASS⚠️
net_pingsmoltcp over ws63-netmac + SLIRP(ARP/ICMP/UDP)UARTNET PING: PASS否⁵❌⁶
reset_demosoftware_reset + reset_reason 端到端UARTOK: software reset observed⚠️
rf_port_demows63-rf-rs porting 层 + Wi-Fi ROM-data blob 链接UARTRF PORT DEMO: PASS否⁷⚠️
semihost_selftestCPU 自检(M/F 扩展、mcycle),半主机退出码semihosting退出码 0,console semihost_selftest: PASS否⁸❌⁸
custom_memory验证 per-example memory.x 覆盖 rt 自带UARTcustom_memory: OK (per-example memory.x in effect)⚠️
wifi_blob_link--whole-archive 链接 Wi-Fi ROM-data blob + 重定位证明UARTBLOB LINK SPIKE: PASS否⁷⚠️

图例(真机列):✅ 已在真实硅片上验证通过;⚠️ QEMU 通过、真机尚未逐一验证(bring-up 进行中);❌ 该观测通道真机不适用。截至 2026-06-14,只有 blinky 经硅片确认;其余 UART 类示例的真机标记串套件正在 bring-up(见 HIL 测试框架)。

注:

  1. gpio_irq / embassy_async_io 把 GPIO0 pin0 设为输出,依赖 ws63-qemu 建模的 输出→输入 自环产生边沿;真机需相应注入/接线。
  2. i2c_scan:QEMU 下无真从机,no devices acked 是正常结果而非失败。
  3. i2c_scan 真机需挂接真实 I2C 从机才会有 found device
  4. spi_loopback:QEMU 把 SPI0 TX FIFO 环回 RX,无需跳线;真机必须短接 MOSI↔MISO。
  5. net_ping 需 QEMU user netdev(-nic user,默认),纯软件/SLIRP,无需外部网络。
  6. net_ping 依赖 ws63-qemu 合成 MAC(ws63-netmac @ 0x4421_0000),真机无此通道。
  7. rf_port_demo / wifi_blob_link 需厂商 blob libwifi_rom_data.a(ws63-RF 子模块)链接到位。
  8. semihost_selftest 需 QEMU -semihosting;真机半主机陷阱为 no-op,exit 只自旋。
  9. uart_hello 真机上已确认能跑到 main 并运行(probe-rs 单步/采样验证),但 UART banner 在 115200 下暂不可读 —— 疑似该例不做时钟初始化、波特率基于 QEMU 默认时钟假设,真机 UART 时钟不同。属已知 bring-up 待修项。

成功标记串(逐字,用于 grep)

示例成功标记串(verbatim)
uart_helloHello from WS63 on QEMU!
gpio_irqOK: custom local IRQ (>=32) delivered
i2c_scanscan done(有从机时)或 no devices acked
spi_loopbackSPI loopback OK
dma_loopbackDMA LOOPBACK TEST: PASS
timer_irqOK: timer interrupts delivered
async_busASYNC BUS: PASS
async_delayASYNC DELAY: PASS
embassy_async_ioEMBASSY ASYNC IO: PASS
embassy_multitaskEMBASSY MULTITASK: PASS
net_pingNET PING: PASS
reset_demoOK: software reset observed
rf_port_demoRF PORT DEMO: PASS
semihost_selftestconsole semihost_selftest: PASS(半主机退出码 0)
custom_memorycustom_memory: OK (per-example memory.x in effect)
wifi_blob_linkBLOB LINK SPIKE: PASS

blinky 无 UART 输出,只能由 GPIO0 翻转观测。

失败标记串

示例失败/诊断标记串
spi_loopbackSPI loopback MISMATCH(rx≠tx);SPI error (timeout)
dma_loopback各阶段 FAIL;mismatch 诊断 mismatch @<idx> got=<x> want=<y>;末行 DMA LOOPBACK TEST: FAIL
async_busASYNC BUS: FAIL;SPI MISMATCH/spi error;ADC no sample(I2C err 不计失败)
net_pingNET PING: FAIL (no echo reply)(5000 ms 超时)
rf_port_demoRF PORT DEMO: FAILmemcpy_s/memset_s : FAIL
semihost_selftestconsole semihost_selftest: FAIL(退出码 1);semihost_selftest: PANIC(退出码 2)
custom_memorycustom_memory: FAIL (unexpected memory.x)
wifi_blob_linkBLOB LINK SPIKE: FAIL(验证少于 13/13)

其余示例(blinkygpio_irqtimer_irqreset_demoasync_delayembassy_*uart_hello)无显式 FAIL 串;失败表现为成功标记串始终不出现。各 UART 示例的 #[panic_handler] 仅静默自旋(不输出),唯一例外是 semihost_selftest(写 semihost_selftest: PANIC\nexit(2))。

semihost_selftest 退出码

退出码含义console 输出
0PASS — 全部 CPU 不变量成立(乘法、硬浮点 ilp32f、mcycle 推进)semihost_selftest: PASS\n
1FAIL — 某 CPU 不变量检查失败semihost_selftest: FAIL\n
2PANIC — 触达 Rust panic handlersemihost_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 约定

  • #![no_std]cfg(test) 下链接 std 供主机单测)。
  • 必须恰好选一个芯片特性:chip-ws63(默认)或 chip-bs21(二者互斥,否则 compile_error!)。
  • 依赖 embedded-hal 1.0embedded-hal-nb 1.0embedded-io 0.6portable-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>AnyPininit_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> 区分实例,构造函数按实例分开(每实例可能配置不同):

外设类型构造函数
UARTUart<'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)
DMADmaDriver<'d, T: DmaInstance>泛型于 Dma0 / Sdma0 标记

SPI Config { frequency, mode, data_bits };UART Config { baud, data, parity, stop }

单外设驱动(new() 模式)

多数驱动遵循 DriverName::new(peripheral)Watchdog::newTimerDriver::newTcxoDriver::newRtcDriver::newLsAdc::newI2sDriver::newPwmChannel::new(&Pwm, channel)SfcDriver::newPkeDriver::newSpaccDriver::newKmDriver::newTrngDriver::newTempSensor::newEfuseDriverSystem::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() 等)或 RAII PeripheralGuardAtomicU8 引用计数)。
  • Peripheral 枚举把每个外设映射到 (cken_register_index, bit_position)

多数外设访问寄存器前需经 CLDO_CRG 门控使能;复位默认即使能;WDT/RTC/TCXO 常开。

sealed trait(private.rs

Sealed 作为超 trait,阻止外部实现 DmaWordPeripheralInputPeripheralOutput。(旧的空 DriverMode/Blocking/Async 标记 trait 已移除。)

特性(features)

特性内容
chip-ws63(默认) / chip-bs21选芯片,互斥
asyncembedded-hal-async/embedded-io-async 实现 + asynch::block_on + IrqSignal + 各驱动 on_interrupt
embassyembassy-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芯片基地址主驱动类型示例覆盖裸板自检
gpioGPIO(19 引脚,3 块)两者0x4402_8000/9000/A000Input/Output/Flex/AnyPin/Ioblinkygpio_irq
ulp_gpioULP GPIO(8 引脚 GPIO107-114)两者0x5703_0000UlpGpioPin<MODE>
uartUART0/1/2(16C550)两者0x4401_0000/1000/2000Uart<T>uart_hello
i2cI2C 主机 v150(I2C0/1)WS63— (SCL 24 MHz)I2c<T>i2c_scanasync_bus⚠️ 需从机
i2c_v151I2C DesignWare SSI v151BS210x5208_3000 + idx*0x1000I2c<T>Speed⚠️ 需从机
spiSPI 主机 DesignWare SSI v151(SPI0/1)两者— (SSI_CLK 160 MHz)Spi<T>Configspi_loopbackasync_bus✅(QEMU 环回)
dmaDMA(MDMA) + SDMA v151两者MDMA 0x4A00_0000、SDMA 0x520A_0000DmaDriver<T>dma_loopback
pwmPWM(8 通道,32 位)两者PwmChannel
timer定时器(3× 32 位)两者— (24 MHz)TimerDriverTimerModetimer_irqasync_delay
tcxoTCXO 64 位自由计数两者0x4400_04C0TcxoDriver(embassy-time 间接)
time计时(Instant/Duration/Rate,基于 TCXO)两者TCXO 0x4400_04C0Instant/Duration多处间接
wdt看门狗(24 位降计数)两者0x4000_6000(lock magic 0x5A5A5A5AWatchdogreset_demo 间接
rtcRTC v100(48 位)WS63— (32768 Hz)RtcDriver
rtc_v150RTC v150(64 位)BS21RTC0 0x5702_4100RtcMode
lsadc低速 ADC v154WS630x4400_C000LsAdcAdcConfigasync_bus
gadc13 位 GADC v153BS21digital 0x5703_6000Gadc⚠️
tsensor温度传感器(10 位)WS63— (code 114..896)TempSensor
i2sI2S / PCM 音频WS63I2sDriverI2sConfig⚠️ 需外设
pdmPDM 麦克风前端 v150BS210x5208_E000Pdm⚠️ 需外设
keyscan键矩阵扫描器 v150BS210x5208_D000KeyscanKeyEvent⚠️ 需接线
qdec正交解码器 v150BS210x5200_0200Qdec⚠️ 需接线
usbUSB 2.0 OTG(DWC2 device)BS210x5800_0000UsbSpeed/UsbError⚠️ 需主机
sfcSPI Flash 控制器WS630x4800_0000(safety.rs)SfcDriverBusConfig
efuseeFuse / OTP v151WS63STS+0x2C / CTL+0x30 / data+0x800EfuseDriver✅(只读)
km密钥管理 KLAD/RKPWS63—(KEYSLOT_COUNT=8)KmDriver
pke公钥引擎(RSA/ECC/SM2)WS63PkeDriver
spacc安全加速器(AES/SM4/HASH)WS63Crypto 0x4410_0000(safety.rs)SpaccDriver
trngTRNG(FRO 熵源)WS63TrngDriverTrngError
trng_v1TRNG v1BS210x5200_9000TrngTrngError
system系统控制(时钟/复位/电源)WS63CHIP_RESET 0x4000_2110SystemResetReasonreset_demo
clock外设时钟门控参考(CLDO_CRG)WS63Peripheral 枚举(多处间接)n/a
clock_init时钟初始化 / PLL 切换WS63HW_CTL 0x4000_0014SystemClocksTcxoFreq(多处间接)
delay忙等阻塞延时两者Delayblinky 间接
interrupt自定义 local 中断控制器(无 PLIC)两者CSR LOCIEN0..2 0xBE0..2Prioritygpio_irqtimer_irq
io_config引脚复用配置WS63(多处间接)
safety编译期 MMIO/timer 断言(无外设)WS63MMIO 0x4000_0000..0x5704_0000PeripheralIndex/GpioPinIndexn/an/a
asynchasync 胶水(block_on/IrqSignalWS63 (async)IrqSignalasync_*n/a
embassyembassy-time Driver两者 (embassy)TCXO + TIMERembassy_*

裸板自检:✅ 可在裸板上自验(无需外接器件);⚠️ 需外接器件/接线/从机;n/a 非外设驱动。

DMA 控制器

控制器标记类型基地址通道
主 DMA / MDMADma00x4A00_00000–3
安全 DMA / SDMASdma00x520A_00000–3(逻辑 8–11)

Peripherals 实例(WS63,chip-ws63,35 个)

SYS_CTL0SYS_CTL1GLB_CTL_MCLDO_CRGIO_CONFIGGPIO0GPIO1GPIO2ULP_GPIOUART0UART1UART2I2C0I2C1SPI0SPI1PWMI2SLSADCDMASDMASFC_CFGTIMERWDTRTCTCXOTSENSOREFUSESPACCPKEKMTRNGRF_WB_CTLSHARE_MEM_CTLFAMA_REMAP

Peripherals 实例(BS21,chip-bs21,28 个)

GLB_CTL_MGPIO0GPIO4ULP_GPIOUART0UART2I2C0/I2C1SPI0SPI2PWMDMASDMATIMERWDTRTCTCXOTRNGGADCKEYSCANQDECPDMUSB

全部 PAC 外设均有 HAL 封装。寄存器行为以 fbb_ws63 / fbb_bs2x C SDK 为 ground-truth。

工具链与编译目标

WS63 用自定义 hisi-riscv 工具链构建。事实取自 rust-toolchain.toml.cargo/config.tomlCLAUDE.md

安装步骤见 安装 hisi-riscv 工具链;硬浮点选型原理见 硬浮点工具链

工具链 / 目标速查

rustup 通道名hisi-riscv
基础 rustc 版本stable 1.96.0
默认目标三元组riscv32imfc-unknown-none-elf
ISARV32IMFC_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-elfhisi-riscv 工具链里被烤进为 builtin 目标,工具链随附该目标的预编译 core/alloc,故构建不需要 nightly 的 -Z build-std。工具链是芯片中立的(ws63 + bs2x 都目标 riscv32imfc)。

无原子的处理:原子 load/store 降为普通 ld/st(单 hart);RMW 经 portable-atomiccritical-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] targetriscv32imfc-unknown-none-elf默认编译目标
runnergdb-multiarchcargo 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-gnuaarch64-unknown-linux-gnux86_64-apple-darwinaarch64-apple-darwinx86_64-pc-windows-msvc。链接目标为解压后的 stage2 目录。

应用镜像格式与签名

WS63 app 镜像(flashboot 从 flash 0x230000 加载的对象)的字段布局。事实逐字段取自 hisi-fwpkg/crates/hisi-fwpkg/src/image.rssrc/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_ID0x4B0F_2D1E
APP_CODE_INFO_IMAGE_ID0x4B0F_2D2D
KEY_AREA_LEN0x100
CODE_INFO_LEN0x200
IMAGE_HEADER_LEN0x300
STRUCTURE_VERSION0x0001_0000
SIG_LENBOOT_SIG_LEN0x40
KEY_ALG_ECC2560x2A13_C812(ECC256 / brainpoolP256r1)
ECC_CURVE_BP256R10x2A13_C812
PUB_KEY_LENBOOT_PUBLIC_KEY_LEN0x40
FLASH_NO_ENCRY_FLAG0x3C78_96E1
HASH_LEN32(SHA-256)

密钥区 image_key_area_t(偏移 0x000,长度 0x100)

偏移字段默认值
0x00image_id(magic)0x4B0F_2D1E
0x04structure_version0x0001_0000
0x08structure_length0x100
0x0Csignature_length0x40
0x10key_owner_id1(默认)
0x14key_id1(默认)
0x18key_alg0x2A13_C812
0x1Cecc_curve_type0x2A13_C812
0x20key_length0x40
0x24key_version_ext0(disabled 板)
0x28mask_key_version_ext0
0x2Cmsid_ext0
0x30mask_msid_ext0
0x34maintenance_mode0(关闭)
0x38..0x48die_id[16]dummy 0(仅维护模式检查)
0x48code_info_addr0(紧随其后)
ext_public_key_area[0x40]sig_key_area[0x40]dummy 0(ECC 签名/公钥 blob)

代码信息区 image_code_info_t(偏移 0x100,长度 0x200;下表偏移相对区起点)

偏移字段默认值
0x00image_id(magic)0x4B0F_2D2D
0x04structure_version0x0001_0000
0x08structure_length0x200
0x0Csignature_length0x40
0x10version_ext0
0x14mask_version_ext0
0x18msid_ext0
0x1Cmask_msid_ext0
0x20code_area_addr0(紧随头之后)
0x24code_area_lenbody.len()
0x28..0x48code_area_hash[32]body 的真实 SHA-256
0x48code_enc_flag0x3C78_96E1FLASH_NO_ENCRY_FLAG,未加密)
0x4C..0x5Cprotection_key_l1[16]0(加密关闭)
0x5C..0x6Cprotection_key_l2[16]0
0x6C..0x7Civ[16]0
0x7Ccode_compress_flag0(未压缩)
0x80code_uncompress_len= code_area_len
0x84text_segment_size0x0001_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 是非零哨兵:flashboot ws63_flash_encrypt_config()if (code_enc_flag == FLASH_NO_ENCRY_FLAG) return;,故零值反而会令 flashboot 尝试配置 on-the-fly 解密而启动失败。明文镜像必须用此值。

dummy-zero 字段:两个区的 ECC 签名 blob(sig_key_areasig_code_infosig_code_info_ext)与公钥(ext_public_key_areadie_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_V1flag0xEFBE_ADDF
HEADER_SIZEFWPKG_HEAD12
BIN_INFO_SIZEIMAGE_INFO52
NAME_SIZE(名字字段宽)32(名字须 < 32 字节)
PAYLOAD_SEPARATOR16(每负载后补的 0 字节数)

FWPKG_HEAD(12 字节)

偏移字段大小
0x00flag(magic)40xEFBE_ADDF
0x04crc2CRC16/XMODEM
0x06cnt(分区数)2parts.len()
0x08total_len4头 + 全部负载 + 全部分隔

IMAGE_INFO(每分区 52 字节)

偏移字段大小
0x00name[32]32
0x20offset4
0x24length(负载字节数,不含 16 字节分隔)4
0x28burn_addr4
0x2Cburn_size4
0x30type4

分区 type

类型说明
Loader0LoaderBoot(一级加载器)
Normal1ssb / flashboot / nv / params / app …
KvNv2Key-Value NV
Efuse3eFuse 配置
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.shhil/flash.shhil/pack.shhil/cargo-run-hw.sh

HIL 框架原理见 HIL 测试框架;运行步骤见 运行 HIL 冒烟测试

串口约定

串口用途参数
UART0 = /dev/ttyUSB0板子 UART0(示例输出)115200 8N1
ttyACM0J-Link VCOM

hil-smoke.sh 检查的标记串

hil-smoke.sh 逐例烧录后读 UART,用 grep -qE 匹配下列模式(check <example> <egrep> <desc>):

示例匹配的 egrep 模式描述
uart_helloHello from WS63UART banner(验证 160 MHz 波特基)
timer_irq`timer irq #OK: timer`
gpio_irqgpio irq #GPIO IRQ 投递
reset_demoOK: software reset observedsoftware_reset + reset_reason(第二次启动标记)
spi_loopbackSPI loopback OK阻塞 SPI0(先短接 MOSI↔MISO!)
i2c_scan`scan doneno devices`

blinky(GPIO 翻转无 UART,需 LED/逻辑分析仪)与 semihost_selftest(需 debugger 半主机)在裸 HIL 跳过。总结果:全过打印 HIL SMOKE: PASS,否则 HIL SMOKE: FAILexit 1

各示例标记串(HIL 期望)

示例成功标记串
uart_helloHello from WS63 on QEMU!
timer_irqOK: timer interrupts delivered(或周期性 timer irq #N
gpio_irqOK: custom local IRQ (>=32) delivered(或 gpio irq #N
reset_demoOK: software reset observed
spi_loopbackSPI loopback OK
i2c_scanscan done / no devices acked
blinky无(GPIO0 翻转)
semihost_selftest半主机退出码 0 / console semihost_selftest: PASS(裸 HIL 跳过)

完整 18 例标记串见 示例目录与验证标记串

环境变量

flash.sh

烧录方式选 METHOD=(默认 probe-rs)。

变量默认适用说明
METHODprobe-rsprobe-rs(验证主路径)或 hisiflash(厂商路径)
CHIP_KINDws63共享ws63|bs21,决定默认 app 分区地址
WS63_RS脚本父目录共享ws63-rs 检出根
CHIPWS63probe-rsprobe-rs --chip 目标
PROBE_RS_YAML(必填)probe-rsfork 的芯片描述 YAML(HiSilicon_WS63.yaml
BASE_ADDRESS0x00230000(ws63)/ 0x00090000(bs21)probe-rsapp 分区 flash 地址
PROBE_RSprobe-rsprobe-rsprobe-rs 二进制名
PORT(自动探测)hisiflash串口(导出为 HISIFLASH_PORT
BAUDhisiflash 默认 921600hisiflash烧录波特(HISIFLASH_BAUD
LOADERBOOT(必填)hisiflash厂商 LoaderBoot 二进制(取自 fbb_ws63 产物)
ADDRESS(必填)hisiflash程序写入 flash 偏移(对照分区表确认)
HISIFLASHhisiflashhisiflashhisiflash 二进制名

hil-smoke.sh(在 flash.sh 变量之外另加)

变量默认说明
PORT(必填)板子 UART0(/dev/ttyUSBx
SETTLE4每次烧录后读 UART 的秒数
UART_BAUD115200示例 UART0 波特(8N1)
MONITOR(raw read $PORT打印原始 UART 到 stdout 的命令(覆盖适配器读法)
HISIFLASHhisiflashhisiflash 二进制名

pack.sh

变量默认说明
CHIPws63目标芯片(ws63|bs21),决定 app 分区地址
APP_ADDR(未设)覆盖 app 分区 flash 地址(如 0x230000
FWPKG(未设)非空则同时产出 .fwpkg(厂商 hisiflash 路径)
HISI_FWPKGhisi-fwpkghisi-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_ADDR0x00230000(ws63)app 分区 flash 地址
PROBE_RSprobe-rsprobe-rs 二进制名
PROBE_CHIPWS63probe-rs --chip
PROBE_YAML(空 = 内置 DB)--chip-description-path YAML
HISI_FWPKGhisi-fwpkghisi-fwpkg 二进制名
PORT(无 = 不流式)复位后流式 UART0 的端口
UART_BAUD115200流式 UART 波特
MONITOR10流式 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 走 路线 2boot-header feature 已把 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>appfwpkg 内分区名
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
resetprobe-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 ws63WS63 机器模型(另有 bs21/bs21e/bs22/bs20
-nographic无图形,串口接终端
-bios none不加载默认固件
-kernel <elf>加载 ELF
-semihosting启用 RISC-V 半主机(semihost_selftest 必需)
-serial mon:stdio串口复用 stdio + monitor
-nic useruser netdev(SLIRP,net_ping 需要;默认即 user)

QEMU 模型原理见 QEMU 模型

hisiflash

厂商串口/YMODEM 烧录 CLI(@230400)。

cargo install hisiflash-cli
命令用法
write-programhisiflash write-program --loaderboot <loaderboot.bin> <program.bin> --address 0x230000
infohisiflash info <out.fwpkg>(静态校验 V1 / 分区 / CRC 结构)
flashhisiflash 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-templatecargo-generate 模板(WS63/BS2X 新工程脚手架)github.com/hispark-rs/hisi-rs-template
hisi-fwpkgapp 镜像 / fwpkg 打包工具(image/patch-hash/packgithub.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-qemuQEMU fork(-M ws63/bs21/bs21e/bs22/bs20github.com/hispark-rs/hisi-riscv-qemu
hisiflash串口/YMODEM 烧录 CLIgithub.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——阻塞驱动、async feature、embassy feature 三者的关系,以及它为什么能在一颗没有原子扩展的核上跑起来
  • 安全启动与签名——为什么开发片把 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-svdws63-pachisi-riscv-halhisi-riscv-rtws63-examples

为什么 PAC 必须只有一份

有一个容易被忽视、却会在链接期炸掉的约束:全仓库只能链接一个 PAC 实例。 PAC 里的 Peripherals::take() 依赖一个 DEVICE_PERIPHERALS 单例静态——如果链接进两份 PAC(比如一个来自 crates.io、一个来自本地 submodule),这个静态会重复、类型也不兼容。 所以根 Cargo.toml[patch.crates-io]ws63-pac 的 registry 依赖重定向到本地 submodule。这是“单一真值“原则在构建层面的体现:不只是源码单向依赖,连链接出的符号 也必须唯一。

所有权即安全:用生命周期泛型守住外设

HAL 的核心安全模型不是运行时检查,而是借 Rust 的类型系统把“外设被独占使用“编译期化。 机制有三层:

  1. 外设单例Peripherals::take() 在 critical-section 保护下只成功一次, 返回一组零大小(ZST)的外设令牌
  2. 生命周期参数化:每个令牌是 Peripheral<'d>。驱动构造器消费这个令牌 (Watchdog::new(wdt)),把 'd 借进驱动。于是“在外设令牌还活着时不能再拿到它“ 被编译器强制——use-after-drop 在编译期就过不了
  3. 多实例靠类型区分: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):DmaWordPeripheralInputPeripheralOutput 这些 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-bs21 feature 二选一区分, 条件编译外设模块。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 自有方法(Confignew*configureset_*)—— 不是 embedded-hal随便上类型。 校验 newtype + 可失败构造;角色用 type-state;驱动自起时钟门。
操作embedded-hal traits(SetDutyCycleSpiBusI2cRead/WriteDelayNs…)签名写死(u16/&[u8] + Result)。Result 就是 embedded-hal 表达「非法输入」的官方手段。不要改 trait 方法签名。

为什么操作面只能 Result:embedded-hal 1.0 的 trait 方法必须 fallible(见下「参考」),set_duty_cycle(u16) 这种签名你改不了 —— 越界就返回 Err,这是它的惯例,不是妥协。而配置/构造方法是 HAL 自有的 inherent 方法,不归 trait 管,可以放手编译期化。

缺陷分类法(给每个配置字段定级)

  • A —— 寄存器位宽溢出。 算出的值比硬件字段宽,被静默掩码/截断(& 0xFFFFas 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::PwmPeriodu16,因为 WS63 的 pwm_freq_h 高 16 位在硅片上根本不存值(实测:写 0x0001 读回 0,即便整条时钟树都拉起来),而 vendor regs_def 明明声明这个字段是 32 位。类型编码实测行为,而不是数据手册。 如果某字段的真实范围拿不准,先上板量,再定类型边界 —— 别只信 PAC/SDK 的位宽。

落地流程(docs-first)

  1. 先改文档 —— 本约定要求 docs-first:先更新该驱动的组件文档 + 本页 + ROADMAP,再写代码。
  2. 扫候选:bash .claude/skills/typed-config/scan.sh crates/hisi-riscv-hal/src/<driver>.rs
  3. 追到寄存器:从 PAC 拿字段真实位宽,从 vendor SDK 拿有效范围 + 时钟前提,标 file:line
  4. 定级 + 选方案(决策树),只动配置层,embedded-hal trait impl 的签名不碰。参考 pwm.rs
  5. 更新测试:host 单测/property(newtype 的接受/拒绝边界)+ tests/hil.rs
  6. 上板验证(硅片佐证):寄存器/轮询级事实可上板确认;示波器级行为(真实波形)和板级前提(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 完整性校验, 最后 transmuteaddr + 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 实际情况定制的事,大致顺序:

  1. PMP 清零——把物理内存保护配成不挡路(否则后续访问可能陷入);
  2. 设置 mtvec——安装中断/异常向量基址(向量化模式);
  3. 初始化 gp / sp——gp 用于 linker relaxation 的全局指针寻址,sp 指向栈顶;
  4. 栈染色(stack paint)——往栈区填已知图案,便于事后测高水位 / 检测溢出;
  5. runtime_init——把 .data 从 flash 拷到 RAM、清 .bss,让静态变量就位;
  6. 调用 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)作为权宜——但那会让编译器发原子指令、在真硅片上触发非法指令陷阱,所以被弃用

正解是两段配合:

  1. 目标本身声明为无原子——用 forced-atomics + no-CAS 配置,让原子 load/store 降级成 普通 ld/st(单核下这是安全的),而需要 RMW(compare-and-swap 之类)的操作不发 原子指令
  2. RMW 走 polyfill——portable-atomic(开 critical-section feature)把 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-stdnightly-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、不需要 nightlycargo 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 叠出了异步能力,于是同一套驱动有三档用法:

  1. 不开任何 feature——纯阻塞uart.write() 就在那儿自旋等 FIFO 有位。 简单、确定、没有执行器、没有 waker。绝大多数简单固件用这档就够。
  2. async feature——中断 + waker 驱动的 .await。多了 embedded-hal-async / embedded-io-async 的实现(DelayNsdigital::WaitSpiBusI2c、UART 的 Read/Write),外加一个极小的 block_on 执行器和一个 IrqSignal 桥。让你“不上 embassy 也能 .await“。
  3. 再开 embassy feature——完整的 embassy 时间生态。多了一个 embassy-time 的 Driver,于是 embassy-executor(platform-riscv32)能跑,Timer::after / Instant / Ticker 都可用。

这三档不是三套代码,而是同一套阻塞驱动上逐层叠加。这个分层本身是个设计取舍: 不想要异步复杂度的人完全感知不到它的存在,想要的人按需开 feature。

async feature:两块地基

block_on + IrqSignal

async 这一档的核心是两个极小的零件:

  • block_on(fut)——一个最朴素的 future 执行器:poll,遇到 Pendingwfi 休眠,硬件中断把核唤醒后再 poll。没有堆、没有全局执行器、没有任务队列。它存在的意义 正是“轻“——给只想偶尔 .await 一下、不愿背上 embassy 全套的场景。
  • IrqSignal——一座“ISR → future“的桥:一个 AtomicBool(fired 标志)加一个停在 critical_section::Mutex 里的 Waker。中断里调 signal(),future poll 时检查 fired、登记 waker。这是把“硬件中断这件异步的事“接到 Rust async 模型里的接缝。

一个关键克制:不抢中断向量

这套异步驱动有一条很重要的设计纪律——它不自动安装 ISR、不抢占中断向量。每个驱动只 导出一个 on_interrupt 钩子timer::on_interruptgpio::on_interruptuart::on_interrupt……),由应用自己的 trap 处理函数mcause 把中断路由过去。

为什么这么设计?因为 Rust 的 cargo 工作区会把 feature 并集——只要工作区里有一个 crate 开了 async,整个工作区都可能被打开。如果异步层一旦被开启就默认安装 ISR,那它会悄悄 改变那些根本没打算用异步的固件的中断行为。“只导出钩子、由应用显式路由“保证了: 开不开 async feature,对非异步固件的行为零影响。这是一条“不给用户埋雷“的边界。

为什么能跑在没有原子的核上

这是最反直觉的一点。WS63 是 riscv32imfc——没有 A 扩展lr.w/sc.w 会陷入 (详见 硬浮点工具链)。而异步执行器、waker 这些东西通常被认为 “当然要原子操作”。它怎么还能跑?

三件事让它成立:

  1. HAL 一直走 portable-atomic + critical-section。需要 CAS 的地方由 portable-atomic 用临界区 polyfill 实现,hisi-riscv-rt 提供单核的 critical-section-single-hart
  2. embassy-executor 本身就支持无 CAS 目标。它内部按编译期 cfgcore::sync::atomicportable_atomic 之间切换——这是它早就为 thumbv6m (Cortex-M0,同样无 CAS)准备好的能力。riscv32 平台模块里的 SIGNAL_WORK 只用 load/store(这颗核支持),不需要 CAS。所以无需改 embassy 一行
  3. 一个真实踩过的坑值得记一笔: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 在跳转前会做两件事:

  1. 真实性验签——用固化在 efuse 里的根密钥,对镜像头里的 ECC(bp256)/ SM2 数字签名做验证。只有用对应私钥签过的镜像才通过。攻击者就算能写 flash, 也伪造不出一个能通过验签的签名(他没有私钥)。
  2. 完整性校验——对 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-header feature 在 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 1hisi-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_lencode_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 自定义 LOCIxx CSR(经 target/riscv 补丁实现),并强制 LOCIPRI 优先级 + PRITHD 阈值。
  • -icount 确定性指令计时:开了之后虚拟时间绑定指令数,同一固件每次运行结果完全 一致(实测 1e6 循环三次都是同一个 tick 数)。这让 CI 回归可重现。

这是一个真值驱动的模型:外设基址/寄存器对着 WS63.svd,内存布局对着 hisi-riscv-rtmemory.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真硅片
镜像裸 ELFload_elf() 解析并按物理地址落段0x300 头部的镜像,烧到 app 分区
引导链没有 flashboot;复位向量直接设成 ELF entrymask 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。为什么选这么“土“的通道, 而不是用调试器读寄存器、或者别的什么?三个理由:

  1. QEMU 可证 + 硅片可观测。同一行标记串,QEMU 的 smoke-test 能看到、真硅片接串口也能 看到。这就让 parity 比对成立:QEMU baseline 打印什么、硅片就该打印什么, 两边逐字对照。换个调试器专属的通道,QEMU 那边就对不上了。
  2. 它恰好能抓住 QEMU 抓不到的 bug。一行 UART 输出要正确出现,背后牵连真实波特 (UART 时钟分频对不对)、真实定时(timer 时钟基对不对)、引导是否真的跑到了 main—— 这些全是物理现实。QEMU 的 chardev 不限速,所以波特算错它照样原样吐字节、根本不报错; 而真串口接收端波特不匹配就是一屏乱码或者干脆没有。也就是说,UART 这个通道天生能暴露 时钟/波特/定时类 bug,正好补上 QEMU 的盲区。
  3. 门槛低、确定。一根串口线、一个标记串比对,不需要逻辑分析仪也能跑出第一轮结论 (需要时再上逻辑分析仪做更细的诊断)。

三段式流程:qemu-smoke → hil-smoke → hil-triage

整套验证是三段接力,逐步逼近真机现实:

  • qemu-smoke——在 QEMU 里逐例跑,建立baseline:每个固件该打印什么标记串、按什么 顺序、什么节奏。这是“应该是什么样“的真值。
  • hil-smoke——在真硅片上逐例烧录 + 读 UART + 比对标记串,镜像 QEMU 的 smoke-test。 通过则该例的物理现实也对了;不通过则进下一步。
  • hil-triage——诊断单个失败步。它的工作假设很关键:板子跑的是 QEMU 已验证的固件, 所以失败通常意味着一个 QEMU 模拟不了的真实时钟/时序/外设/接线现实,而不是逻辑 bug。 triage 的任务是带证据点名最可能的那一类原因,而不是漫无目标地猜。

分歧的几类——先查这些

QEMU↔硅片的分歧高度集中在固定几类,triage 按这个清单逐项对照:

  1. UART 波特——乱码 / 没 banner ⇒ UART 时钟分频假设错。WS63 UART 从 160 MHz 基 分频;若按别的时钟算分频,波特就偏。(QEMU chardev 不限速,永远抓不到这类。)
  2. 定时器周期偏约 10×——timer_irq 来得太快/太慢 ⇒ 定时器还在按 240 MHz PLL 算、 而真值是 24 MHz TCXO(或反过来)。这是**最典型的“QEMU 过了、硅片不行“**的 bug。
  3. 引导挂死 / 全程静默——一点输出都没有 ⇒ 电源/PWR_ON、错的 LOADERBOOT、错的 flash ADDRESS、或启动时读了一个真硅片上永远锁不上的 PLL。
  4. IRQ 没投递——gpio_irq 静默 ⇒ LOCI 使能、触发沿、或引脚接线;核对 IRQ 号和 LOCIEN/mie 路径是否和 SDK 一致。
  5. 外设接线——spi_loopback 需要 MOSI↔MISO 短接、i2c_scan 需要真实上拉。 这一类“失败“可能是测试台架(rig)的问题,不是固件的问题——triage 必须把 “固件要改“和“台架要修“分开。

诊断时对时序类症状要做算术:从 HAL 的时钟常量算出期望周期/波特,再从实测值反推真实 时钟是多少,用数字说话而不是猜。

当前 bring-up 状态(诚实版,2026-06-14)

要诚实——这是进行中的工作,不是已完成的胜利:

  • blinky:已在真硅片上确认。 完整的 Rust → flash → 启动主流程 (cargo buildhisi-fwpkg patch-hash <elf>probe-rs run <elf>) 于 2026-06-14 在真 WS63 硅片上跑通,blinky 上电启动并翻转 GPIO0。这是第一个、也是目前 唯一一个端到端真机确认的例子。WS63 走 boot-header feature——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-time Driver、代码地图与上游化路线 (概念总览见 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-pacsubmoduleWS63 svd2rust 生成的寄存器访问层ws63-pac.md
crates/pac/ws63-pac/ws63-svd嵌套 submoduleWS63 SVD 真值 + 生成工具ws63-svd.md
crates/pac/bs2x-pacsubmoduleBS2X(BS21/BS20/BS22)svd2rust 生成的寄存器访问层ws63-pac.md
crates/pac/bs2x-pac/bs2x-svd嵌套 submoduleBS2X SVD 真值 + 生成工具ws63-svd.md
crates/hisi-riscv-halsubmodule多芯片 HAL(chip-ws63/chip-bs21 feature)+ 可选 async/embassyhisi-riscv-hal.md
crates/hisi-riscv-rtsubmodule运行时:启动、中断向量、链接脚本、critical-sectionhisi-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/flashbootin-tree实验性二级引导(非安全启动)ws63-flashboot.md
chips/ws63/rf/in-treeWS63 Wi-Fi porting 层 ws63-rf-rs
chips/ws63/rf/ws63-RFsubmodule(嵌套)WS63 闭源协议栈 blob + porting 接口ws63-RF.md
chips/ws63/guidesubmoduleWS63 中文硬件手册(Sphinx)ws63-guide.md
chips/bs2x/guidesubmoduleBS2X 中文硬件手册(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 traitprivate.rs):Sealed 超 trait 防外部实现 DmaWord/PeripheralInput/PeripheralOutput

  • #![no_std]:无堆、无 Vec,数据缓冲用定长数组。

  • 寄存器访问 unsafe:裸 PAC 写封装在驱动方法内。

  • 异步async/embassy feature,见 async-embassy.md):中断 + waker 驱动的 embedded-hal-async/embedded-io-async 驱动 + 一个 embassy-time Driver,跑在无原子的 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.toml pin channel = "hisi-riscv";安装见 https://github.com/hispark-rs/hisi-riscv-rust-toolchainrustup 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 链接做准备)。
  • 单一 PAC 实例:根 Cargo.toml[patch.crates-io]ws63-pac 的 registry 依赖重定向到本地 submodule, 保证全仓库只链接一个 PAC(否则 DEVICE_PERIPHERALS 单例静态重复、类型不兼容)。
  • default-members = 库 + blinkyws63-pac/hisi-riscv-hal/hisi-riscv-rt/examples/ws63/blinky)。 blinky 经 hisi-riscv-rt 导出的链接脚本可正常链接(hisi-riscv-rt/build.rscargo:rustc-link-search 导出脚本目录 + ws63-link.x 包装脚本,blinky 的 build.rs-Tws63-link.x 引入)。实验性的 ws63-flashboot 不在默认构建里, 仍是 membercargo 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)

已知的全局性问题(详见评审台账)

  1. 连接性状态
    • 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
  2. 示例无法链接 (已修,阶段 1)多芯片支持 (已实现):hisi-riscv-hal 用 chip-ws63/chip-bs21 feature 区分,二选一;examples 分为 WS63(submodule)、BS2X(in-tree 独立工作区)。
  3. 硬件在环(HIL)进度:QEMU 软件在环已成熟(WS63/BS21/BS22/BS20 均支持,全外设功能覆盖),HIL 脚手架已就位(烧录脚本 + 冒烟框架);待真机板卡到位进行 blinky/UART/中断冒烟。
  4. 正确性修复状态:中断(LOCIEN/LOCIPRI/LOCIPCLR)、SPI(两级时钟)、超时(wait_until 有界)、复位(GLB_CTL + SYS_RST_RECORD)等核心问题已修(ROADMAP 阶段 2);QEMU 软件在环验证已覆盖中断、复位、DMA、timer;上板验证仍待硬件(时钟精度、外设时序)。

参考资料

多芯片支持细节

  • PAC 组织crates/pac/ws63-paccrates/pac/bs2x-pac 各自独立(SVD 源→svd2rust 生成),root Cargo.toml[patch.crates-io] 统一链接到本地实例(保证单一 PAC 版本)。

  • HAL 多芯片hisi-riscv-hal 通过 chip-ws63(default)和 chip-bs21 feature 区分,条件编译外设模块(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.rsClockControl + 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 实现。
  • 不负责
    • 裸寄存器布局与地址映射(属 ws63-pac)。
    • 启动汇编、链接脚本、中断向量表(属 hisi-riscv-rt)。
    • 应用业务逻辑(属 ws63-examples)。
    • 连接性协议栈(WiFi/BLE/SLE)、porting 层、HCC IPC(尚未实现,见 ROADMAP 阶段 4-5)。

#![no_std]、无堆、无 Veclib.rs:20)。寄存器访问全部经 unsafe { w.bits(...) } 封装在驱动方法内部。

在依赖链中的位置

ws63-svd (XML)
   │ svd2rust 生成
   ▼
ws63-pac ──► hisi-riscv-hal ──► examples/ws63/*
                ▲
       hisi-riscv-rt(启动汇编 / 链接脚本 / 中断向量)并行提供运行期支撑

hisi-riscv-hal 是承上启下的核心层:向下消费 ws63-pacRegisterBlock,向上为示例提供驱动。它直接依赖 hisi-riscv-rt,但其中断子系统依赖 riscv crate 的 trap 模型,运行期向量表由 hisi-riscv-rtdevice.x 提供。

依赖:embedded-hal 1.0embedded-hal-nb 1.0embedded-io 0.6nbportable-atomicriscv

关键设计

类型化配置 — “能编译就能上板”(0.5.0)

0.5.0 把配置面全面收紧为「能写出来的值就是能在硅上跑的值」:不存在能编译却被静默 clamp / 截断 / 没接时钟的参数。约定与 A/B/C/D 缺陷分类见 类型化配置,验收见 docs/review/0.5.0-acceptance.md。 两层结构:

  • 配置/构造面(HAL 自有,可自由类型化):受校验 newtype + 可失败构造子返回 Option/ResultSpiHz/DataBits/BaudRate/WdtTimeout/SampleCount 等),越界 在构造点拒绝;角色用 type-state(I2S new_master(非零派生分频)/new_slave(),零分频 Master 不可表达);驱动在 new/configure自起本外设时钟门(construct→clocked, 如 PWM/I2S)。类型编码的是实测硅事实而非数据手册(如 pwm::PwmPeriodu16, 因 WS63 pwm_freq_h 高半字在硅上不 latch)。
  • 操作面(embedded-hal trait,固定签名)SetDutyCycle/SpiBus/I2c/Read/Write 保留标准 u16/&[u8] + ResultResult 即 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 被释放后仍持有驱动,是这一层的核心安全不变量(评审优点)。

时钟架构

两套并存:

  1. 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_RG bit 12 轮询 PLL 锁定(clock_init.rs:127-148)。TCXO 频率检测读 HW_CTL bit 0(clock_init.rs:103-107)。所有地址均注明 fbb_ws63 出处。
  2. 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>,统一到下面这一套):

  1. AnyPin<'d> — 类型擦除,经 unsafe steal(pin) 创建。
  2. Input / Output / Flex — 由 AnyPininit_input()/init_output()/init_flex() 派生;degrade() 可安全擦除回 AnyPinFlex::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)。DmaEligibledma.rs:428-431)+ DmaChannelFor<P>dma.rs:439)意图提供编译期通道-外设绑定安全(刻意不写 blanket impl 以保留约束语义)。

Sealed trait + 异步层

private.rs 定义 Sealed 超 trait,封印 DmaWordPeripheralInputPeripheralOutput。早先空壳的 DriverMode/Blocking/Async mode 标记(associated type 恒等、零消费者)已删除

真正的异步层已实现(feature async/embassy,详见 async-embassy.md):embedded-hal-async/embedded-io-asyncDelayNs/digital::Wait/spi::SpiBus/i2c::I2c/Read/Write,加上 asynch::block_on + IrqSignal(中断→waker 桥)+ 各驱动的 on_interrupt 钩子(不自动装 ISR);外加 LSADC/DMA 的自研异步。还提供一个 embassy-time Drivernow()=TCXO 64 位计数器、alarm=TIMER 通道),让 embassy-executor(platform-riscv32)跑 Timer::after + 多任务。全部跑在无原子的 WS63 上(portable-atomic + critical-section)。

embedded-hal trait 选型(评审优点)

  • SPI 实现 SpiBus 而非 SpiDevicespi.rs:135)— HAL 层不持有 CS,符合分层惯例。
  • I2C transaction 在操作间发 repeated-START、仅末尾发 STOP(i2c.rs:215-265),符合 embedded-hal 契约。NACK 映射为 NoAcknowledgei2c.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-74197-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:135i2c.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.rsi2c.rs✅ 阶段2已修:I2C/SPI 加 bounded 超时并真正返回 Timeout 等错误
正确性software_reset 执行 ebreak(非系统复位);reset_reason 恒返回 PowerOnsystem.rs✅ 阶段2已修:software_reset 置 GLB_CTL_M 复位位,reset_reason 解析 SYS_RST_RECORD;ws63-qemu reset_demo 往返验证
正确性GPIO InputConfig.pull 被静默忽略:init_input 只设 OENgpio.rs✅ 阶段2已修:init_input 经 IO_CONFIG pad 寄存器应用上下拉 + 中断触发模式
正确性eFuse / LSADC 寄存器布局为猜测,与 SDK 矛盾efuse.rslsadc.rs🟡 已对照 fbb_ws63 + ws63-qemu(eFuse 写=按位或、LSADC 转换 IRQ72) 验证读写序列;逐寄存器复核仍按阶段 2 推进
维护性safety.rs 多条 const_assert! 为恒真断言;模块头措辞夸大safety.rs✅ 阶段2已修:删除恒真断言 + 夸大措辞
架构零消费者死代码:RAII 时钟守卫、DMA 安全 trait、async markerclock.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-rtcargo:rustc-link-search + ws63-link.x,示例正常链接);✅ 恒真式测试已由 ws63-qemu 软件在环大幅替代(smoke-test.sh 跑真实固件 + C SDK 交叉验证);🟡 真机 HIL 冒烟仍待补。
  • 阶段 2(死代码清理 + 正确性修复):✅ 中断子系统已重写到 LOCIPRI/LOCIEN/LOCIPD CSR 模型;✅ I2C/SPI 超时并返回错误;✅ software_reset/reset_reason;✅ GPIO pull + 中断触发;✅ safety.rs 恒真断言 + 夸大措辞已删;✅ async marker / RAII 时钟守卫死代码已删。🟡 SPI trsm、eFuse/LSADC 逐寄存器复核仍在推进。
  • 新增(超出原评审):✅ 异步 HALasync/embassy feature,见 async-embassy.md)—— embedded-hal-async/embedded-io-async 全套 + embassy-time Driver,全部 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-26layout.ld:19ENTRY(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→symbols INCLUDE),下游 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,启用 riscvcritical-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 实例)、riscvriscv-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 裸机启动惯例:

  1. pmpcfg0..3startup.S:30-37,EDA/仿真 workaround)。
  2. la t0, trap_vector; csrw mtvec, t0startup.S:40-41)。
  3. 关中断:csrwi mstatus,0 + csrwi mie,0startup.S:44-45)。
  4. 开 FPU:mstatus.FS=0b11,清 fflagsstartup.S:48-50)。
  5. 初始化 gpnorelax 包裹,startup.S:53-56)与 sp = __stack_top__startup.S:59)。
  6. 栈金丝雀填充 0xefbeaddestartup.S:62-70)。
  7. tail runtime_initstartup.S:73)→ Rust 侧做重定位/清 BSS/再开 miesrc/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_entrystartup.S:320)用 save_all(36 字,含 mcause/mbadaddr/ccause 自定义 CSR)保存上下文,通过 mscratch 切到 __exc_stack_top__startup.S:327-328),按 mcause 索引 .rodata 中的 excp_vect_tablestartup.S:132-153、335-342)分发;M-mode ecall 单独走 handle_ecall_mstartup.S:356-361)。
  • NMI:切到 __nmi_stack_top__,调 nmi_handlerstartup.S:369-383)。
  • MIE 中断mie_interrupt_handler 宏生成 6 个(bits 26-31),切到 __irq_stack_top__call mie\n\()_interrupt_handlerstartup.S:389-410)。
  • local 中断:60 个向量统一进 local_interrupt_handler,调 local_isr_dispatchstartup.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。.startupKEEP(*(.text.entry)) 确保复位向量在 PROGRAM 区最前(layout.ld:125-129)。栈区在 .stacks (NOLOAD) 内自高地址向下生长(layout.ld:189-216)。

ISA / 原子性

build.rsRISCV_RT_BASE_ISA=rv32i(无原子扩展)。默认 target 是 builtin 的 riscv32imfc-unknown-none-elf(RV32IMFC,硬件单精度浮点 ilp32f);无 A 扩展,原子由 portable-atomic 的 critical-section polyfill 提供,hisi-riscv-rt 启用 riscvcritical-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-73src/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 fallbacklayout.ld:199-204(说明);memory.x:76-78(fallback)本轮已修
构建(已修)riscv 启用 critical-section-single-hart,为无原子扩展的 WS63 提供单 hart CS 实现,支撑 PAC take() 与 HAL portable-atomicCargo.tomlriscv features)本轮已修
构建/发布(已修)ws63-pac 依赖补充 versionversion = "0.1", path = ...)以便 cargo publishCargo.toml(ws63-pac 依赖)本轮已修

说明:构建完整性修复中与本组件相关的还包括——双 PAC 实例消除(根 [patch.crates-io] 指向本地,cargo tree 单一实例)、无原子 ISA 下实测产物零原子指令(lr/sc/amo)。默认 target 现为 ws63 工具链 builtin 的 riscv32imfc-unknown-none-elf(硬浮点;2026-05-31 曾过渡用 stable riscv32imc)。这些在仓库级评审中记录,本组件直接相关项已并入上表。

改进项与排期

  • 阶段 1(链接脚本集成 ✅ 已完成 / 上板待硬件):链接脚本不传播问题已解决(rustc-link-search + ws63-link.x 包装脚本 + blinky build.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;在 rt feature 下提供中断向量表 device.x
  • 不负责:任何驱动逻辑、时钟门控策略、引脚复用、外设初始化时序。这些全部上移到 hisi-riscv-hal。PAC 只暴露“裸寄存器 + 地址映射“,是 unsafe 寄存器写入的最底层封装边界。

crate 元数据齐全(Cargo.toml:1-9):license = "MIT"repositorykeywordscategories,具备发布到 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:1 doc 注释标注版本)。
  • 下游:hisi-riscv-hal(安全驱动)与 hisi-riscv-rt(启动/链接)均消费本 crate。两者通过 registry 版本依赖 version = "0.1" 声明(crates/hisi-riscv-hal/Cargo.toml:12crates/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 fnsrc/lib.rs:23-25),Deref 直接解到寄存器块(src/lib.rs:45-51)。这是新版 svd2rust 的 const-fn 访问器风格,零运行时开销。
  • Peripherals 单例static mut DEVICE_PERIPHERALS: boolsrc/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、双 spidma/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 = 26src/lib.rs:906)。rt feature 下 build.rsdevice.x 写入 OUT_DIR 并加入 link-search(build.rs:8-18),向量表用 PROVIDE(... = DefaultHandler) 提供弱默认(device.x:1-30)。
  • feature 设计default = ["critical-section"],外加 rtCargo.toml:16-18)。take() 仅在 critical-section 下编译(src/lib.rs:31758),符合 svd2rust 约定。
  • ISA 协同rt feature 下 build.rs 导出 RISCV_RT_BASE_ISA=rv32ibuild.rs:16),与本轮 ISA 修复(默认 target 切到 riscv32imc、产物零原子指令)一致。

评审发现

优点

  • svd2rust 0.37.1 现代 const-fn 访问器,generic 层零开销(src/lib.rs:14-51)。
  • 编译快(约 6s 过编译),单文件无复杂构建依赖。
  • 工程化完备:device.x 中断向量、critical-section/rt feature(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:2841528569CHANGELOG.md:13-21已排期(ROADMAP 阶段 2):应回填到 ws63-svd 源头由生成器产出
依赖版本曾停在 0.1.0 而 tag 后又追加了公开寄存器,违反 SemVerCargo.toml:3CHANGELOG.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:12Cargo.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.00.1.1(与 tag 后新增的 KM 寄存器对齐),其后随各仓自有流水线发布到 0.1.3
  • ISA 协同rt feature 导出 RISCV_RT_BASE_ISA=rv32ibuild.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:2schemaVersion="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-paclib.rs,再由 hisi-riscv-hal 封装为安全驱动,最终被 ws63-examples 使用。hisi-riscv-rt 提供启动代码与链接脚本,是与上述生成链平行的独立分支。

生成关系(2026-05-31 起可复现)WS63.svdregen.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 为 otherfpuPresent=true/fpuDP=falsewidth/size=32nvicPrioBits=3WS63.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_CFGport_sel/key_enc/key_dec 等,flush_hmac_kslot_ind 字段亦已建模)。

校验工具

validate.pyvalidate.py:1-29)从 ARM CMSIS_5 仓库下载 CMSIS-SVD.xsd 缓存到 /tmp,用 xmlschemaWS63.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。五步:

  1. svd2rust -i WS63.svd --target riscv --settings ws63-settings.yaml
  2. rustfmt(svd2rust 原始输出未格式化,后续正则后处理依赖多行格式)
  3. postprocess.py 两处确定性修补:删除 dim 重复生成的 5 个 TIMER 裸访问器(否则与索引访问器重复定义、编译失败)、#[no_mangle]#[unsafe(no_mangle)](edition 2024 硬错误)
  4. cargo fix 自动套 unsafe_op_in_unsafe_fnrt+critical-section 特性下 Peripherals::steal() 的 unsafe 包裹)
  5. 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 寄存器,enumeratedValuesderivedFromwriteConstraintaddressBlock 一应俱全,是一份结构完整、可被 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.pyprint(...) 桩,无 svd2rust 调用;ws63-settings.yamlbase_isa: rv32i 截断;CI 中无 SVD 引用main.py:1-6ws63-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)已落地大部分:

  1. 建立可复现生成流水线(已完成):regen.sh(svd2rust 0.37.1 + postprocess.py 后处理 + cargo fix/fmt)替代 main.py 桩,幂等、build+clippy 门禁。剩余:把“从 SVD 重生成并 diff“接入 CI;并加 validate.py XSD 校验门(脚本已就绪)。
  2. 以 SVD 为源重生成 PAC(已完成):regen.sh 即唯一生成路径,手补 lib.rs 被 PreToolUse hook 拦截;重生成恢复了历史手补遗漏的 KM keyslot 字段,消除 SVD↔PAC 漂移。
  3. eFuse/LSADC 寄存器修复(已完成):对照 hal_efuse_v151/hal_adc_v154 改 SVD 并重生成(详见上表)。剩余:KM *_FLUSH_BUSY(0xB10–0xB1C)转录缺口仍待补;其它外设逐个对照 fbb_ws63 *_reg.h 核覆盖。
  4. 补写 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-pacbs2x-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-membersCargo.toml:30-48),默认 cargo build 即构建:

示例演示内容
blinkyGPIO 点灯(最小裸机入口模板)
uart_helloUART 输出
timer_irqTimer 中断(WS63 自定义 LOCI* 中断模型)
gpio_irqGPIO 输入 + 边沿/电平中断
reset_demosoftware_reset / reset_reason 往返
dma_loopbackDMA mem-to-mem 搬运
semihost_selftestsemihosting exit()/print(CI 免解析 UART 即得 pass/fail)
custom_memory示例自带 memory.x 覆盖默认内存布局
async_delayembedded-hal-async DelayNs + asynch::block_on
async_bus异步 SPI/I2C 总线(SpiBus/I2c
embassy_multitaskembassy-executor 多任务 + embassy-time
embassy_async_ioembassy 下的异步 UART I/O
wifi_blob_link把 vendor RF blob 链入镜像(符号闭合冒烟)
rf_port_demows63-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-qemuscripts/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-rtcargo:rustc-link-search 导出 ws63-link.xhisi-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 使用方式blinkylegacy 类型态 GPIOcreate_output_pin + set_high()/set_low());gpio_irq 则演示新的输入 + 中断路径。HAL 的 OutputConfig/InputConfig 构建器 API 已落地,示例正逐步覆盖。
  • 延时实现blinkydelay_ms手写忙等(按 240 MHz 估算,绕过 HAL timer),属「最小可演示」而非最佳实践;async_delay / embassy_multitask 演示了正确的 DelayNs / Timer::after 路径。
  • 自定义内存布局custom_memory 演示用示例自带的 memory.x 覆盖 hisi-riscv-rt 的 bundled 链接脚本(hisi-riscv-rt 的默认 feature bundled-memory-x,关掉后由示例侧提供),从而不与 rt 冲突。
  • semihosting / CI 信号semihost_selftest 用 semihosting exit() 给 CI 一个免解析 UART 的 pass/fail 退出码。
  • 异步async_* / embassy_* 用 hisi-riscv-hal 的 async / embassy feature + embassy-executor(机制见 async-embassy.md)。
  • RF portingrf_port_demows63-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-memberscargo 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-22README.md:3Cargo.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.rssrc/main.rs:235-258)。
  • 看门狗、eFuse 时钟周期初始化、FAMA 重映射,最后跳转到 app 入口(src/main.rs:280-296192-202100-107135-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-halDEVICE_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),仅依赖 riscvcritical-sectionCargo.toml:20-22)。
  • 在工作区中它是 members 之一(cargo check --workspace 仍覆盖),但不在 default-members 中,默认 cargo build 不构建它(根 Cargo.toml default-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-48src/sfc.rs:9-27src/uart.rs:8-15),刻意不引入 ws63-pac,避免双份 PAC 链接冲突(Cargo.toml:17-19)。代价是与 HAL 重复造 UART/SFC/SHA256/startup(见评审)。
  • 汇编启动对照原厂asm/startup.S 注释声明基于 fbb_ws63 flashboot_ws63/startup/riscv_init.S,做 PMP 清零、清自定义 CSR 0x7d9、从 a0 保存 boot flag 到 __flash_boot_flagmtvec 向量模式(+1)、开 FPU(mstatus.FS=0b11)、清 BSS、tail flashboot_main
  • 单镜像启动(2026-06-01 整改):删除了对 0x4000_0024 的 A/B 误用。该寄存器是 flashboot 自身的备份恢复标志(0x5A5A5A5A ⇒ 从备份分区恢复 bootloader;vendor main.c:131-135 flashboot_need_recovery),不是 app 槽选择器。真实 app A/B 由 upg run-region 配置(PARTITION_FOTA_DATA 末尾 magic 0x70746C6Crun_region 0=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,按 vendor image_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_idstructure_version==0x0001_0000structure_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"/长输入测试)未经审计、仅作完整性用途。
  • SFCsfc_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 + 0x300 transmuteextern "C" fn() -> ! 并调用(src/main.rs:159-166),SAFETY 注释声明 app 入口同 ABI(RV32IMFC ilp32f)。
  • 本轮构建完整性修复(针对该 crate):banner 重写为“非安全启动“警告(src/main.rs:1-22)、publish = falseCargo.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-22Cargo.toml:11README.md)。

问题

严重度类别问题证据(file:line)状态
严重安全无真实性验签:只把算出的哈希与同一份未签名头里的哈希比对。能写 flash 的攻击者改镜像后重算 SHA256 写回头部即可以 M 态特权跳进任意代码,≠ secure boot(原厂用 efuse 根密钥 ECC-bp256/SM2 签名验签)src/main.rsverify_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/CodeInfoimage_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/Bsrc/main.rs;对照 vendor main.c:131-135flashboot_need_recovery✅ 已修(2026-06-01):删除该误用,改单镜像启动 + 如实注明真实 A/B = upg run-region(magic 0x70746C6C)+分区表(@0x200380)、0x40000024=bootloader 自恢复
方向重写原厂安全关键件(验签/启动链)属误导努力。生产应复用原厂 flashboot,本 crate 仅供学习src/main.rs:5-8README.md:22-26暂不修(定级实验性;定位为学习件,整体方向走复用原厂)
正确性关键子流程是桩:boot_clock_adapt() 为 TODO 空操作;read_partition_app_addr() 恒返回 FLASH_STARTcheck_upgrade_mode() 恒 falsesrc/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.rssrc/sfc.rssrc/sha256.rsasm/startup.SCargo.toml:17-19暂不修(为保持独立、规避双份 PAC 链接冲突的有意取舍)
工程化删除未用的 ws63-pac 依赖、publish=false、移出默认构建、banner 改为实验性警告Cargo.toml:11,17-19、根 Cargo.toml default-memberssrc/main.rs:1-22本轮已修

改进项与排期

  • 生产层面的结论是复用 fbb_ws63 原厂 flashboot(已做签名验签 / A/B / 升级 / 解压 / flash 加密),Rust 应用以 app 镜像形式由原厂 flashboot 加载(README.md:22-26)。本 crate 维持实验/学习定位。
  • 整改已落地(2026-06-01):镜像头布局对齐 secure_verify_boot.hcode_area_len/code_area_hash 偏移修正 + const 尺寸断言)、删除 0x40000024 的 A/B 误用改单镜像启动并如实注明真实 A/B 机制、verify_sha256verify_image_integrity 如实标注“仅完整性非真实性“、read_partition_app_addr 桩如实标注。flashboot 现已纳入 CI clippy 门禁(不再 --exclude)。真实 ECC/SM2 验签仍按冻结项复用原厂、不在本实验件投入。
  • 阶段 0 的构建完整性修复已落地:双份 PAC 消除(registry 版本依赖 + 根 [patch.crates-io] 指向本地)、无原子 ISA + portable-atomic critical-section polyfill(默认 target 现为 ws63 工具链 builtin 的 riscv32imfc-unknown-none-elf,硬浮点;2026-05-31 曾过渡用 stable riscv32imc)、CI/release gating 与发布顺序修复、hisi-riscv-rt MIE 中断宏 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)的载体。它负责两件事:

  1. 重分发 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)。
  2. 提供 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.tomlmembers / default-membersCargo.lock 均未引用 ws63-RF(已 grep 核实,0 结果)。因此当前它对 cargo check --workspace 完全不可见。
  • 不实现 porting 层本身——port_*.h 只是接口声明,实现由下游 in-tree crate ws63-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 Corerom_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 头

  • 闭源 .alib/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.alibbth_gle.a(821 KB,SLE/GLE)、libbth_sdk.alibbg_common.a。README 的“Library Catalog“表与磁盘实际大小逐项吻合(ls -la lib/ 核实)。
  • internal 头(include/internal/:blob 内部代码依赖的类型/消息定义(osal_types.hfrw_msg_rom.hwlan_msg.hhcc_*.h 等),porting 头里的不透明结构(如 struct frw_msg)正是在此定义。
  • porting 头(include/port/:调用方必须实现的 8 组接口(每个文件均以 port_*.h 命名)。

porting 接口分解(README 的 “Dependencies Count” 表,与头文件逐一核对)

头文件函数数职责(file 证据)
port_osal.h24OS 抽象:中断 osal_irq_*、线程 osal_kthread_*、内存 osal_kmalloc/kfree、等待 osal_wait_*osal_udelayosal_flush_cacheosal_printkport_osal.h:44-160
port_frw.h15Wi-Fi 消息分发框架 + 定时器:frw_main_initfrw_fetch_msg_nodefrw_send_msg_to_devicefrw_task_threadfrw_dmac_timer_*port_frw.h:28-99
port_wlan.h11共享内存 ring buffer + RF 时钟:wlan_open/close_wifi_abb_rf_clkwlan_msg_h2d_*oal_ring_write/readport_wlan.h:25-110
port_hcc.h6HCC IPC 传输:hcc_dmac_config_bus_inihcc_dmac_service_adapt_starthcc_wifi_msg_register/sendport_hcc.h:35-77
port_oal.h748 KB Wi-Fi packet 缓冲池:oal_memory_initoal_mem_rsvoal_get_netbuf_pool_lenport_oal.h:39-79
port_uapi.h3平台服务:uapi_nv_read(RF 校准/MAC)、uapi_tsensor_get_current_temp(热保护退避)、uapi_systick_get_msport_uapi.h:30-72
port_log.h7日志 + 安全 C 库:log_event_wifi_print0/1/2/4memset_s/memcpy_s/snprintf_sport_log.h:30-50
port_linker.h20+ 符号链接符号声明:__wifi_pkt_ram_begin__/end__、TCM/SRAM 区段、__divdi3/__udivdi3port_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.hoal_ring_ctrlport_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.shWi-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.hextern 符号与 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 blobchips/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.tomlchip-ws63(default)与 chip-bs21 feature,运行时可选;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.tomlpackage = 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:30ws63-guide/README.md:28)。根目录另有一份 ws63-guide/index.rstws63-guide/source/index.rst 两个 toctree 入口(root_doc = 'index'conf.py:46)。
  • 9 章 + 附录的 toctreesource/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}.mdch6_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_themeconf.py:55,注意 README.md:118 仍写的是 sphinx-rtd-theme,已过时);语言 zh_CN
  • PDF 中文支持靠 xeCJKconf.py:97-100xeCJK + 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_00000x4000_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/SFCsource/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.pysphinx_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/embassy feature)如何工作、代码在哪、以及之后如何上游化。 总体架构见 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-section feature)做 CAS 垫片;hisi-riscv-rt 提供 riscv/critical-section-single-hart 实现。
  • embassy-executor 在无 CAS 目标上也能跑(thumbv6m / riscv32imc 同理):它内部按编译期 cfg 在 core::sync::atomicportable_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,IRQ TIMER_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_interruptenable_global()。其余照 embassy 标准用法。

代码地图

文件内容
crates/hisi-riscv-hal/src/asynch.rsblock_on + IrqSignal(地基)
crates/hisi-riscv-hal/src/embassy.rsembassy-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.tomlasync / 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::SpiBusi2c::I2c、io Read/Write(UART);外加两个完成中断外设的自研异步(DMA IRQ 59、LSADC IRQ 72)。RTC/I2S/PWM 等无标准 async trait、语义为周期/流式/一次性 —— 保持阻塞,需要时按同一 IrqSignal+on_interrupt 模式加(RTC 的 IRQ 29 已建模)。

之后怎样上游化

按“上游“对象分四条线,都不需要改 embassy 本身:

  1. 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),天然适合树外。 所以上游化 = 把带 embassy feature 的 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 一个文件。
  2. embassy-executor 已是上游:我们直接用 platform-riscv32,零改动。无 CAS 支持是它已有能力(thumbv6m 同理)。无需上游任何东西。

  3. 工具链 / target(最大的“非上游“项):现在依赖自定义 ws63 rustc(把 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 是否上游正交。
  4. 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 主线 —— 三者互相独立。