RP2350上的 Rust 开发体验
JP's Website
RP2350 发布博客
发布于 2024-08-08
目录
- Intro
- 它能运行 Rust 吗?
- 先别管细节,给我看看 Demo
- RP2040 上 Rust 的回顾
- 今天 Rust 能做什么
- RP2040 启动的回顾
- 启动 RP235x
- 关于地址转换的一点说明
- 其他很酷的东西 - ROM 和 OTP
- Flash 是否加密?
- 激发的 DVI 输出 - HSTX 和 TMDS
- DCP 上的快速(相对而言)f64 计算
- RTC 去哪儿了?
- 今天 Rust 有哪些不足
- 最后的思考
Intro
今天发布了 Raspberry Pi 微控制器系列的最新产品 - RP2350 系列。 我已经有一段时间的原型单元,你今天就可以在它上面运行 Rust 代码。 据我所知,这是有史以来第一个开箱即用支持 Rust 的微控制器发布。 如果你还没看过其他地方的规格,让我来介绍一下: RP2040| RP235x ---|--- 主 CPU| 2x Arm Cortex-M0+ (Armv6-M)| 2x Arm Cortex-M33 with FPU (Armv8-M) 备选 CPU| None| 2x Hazard3 (RISC-V RV32IMAC) 原子比较和交换| No| Yes (Arm 和 RISC-V 模式) 双精度浮点协处理器| No| Yes (仅 Arm 模式) 标称时钟| 133 MHz| 150 MHz 片上 SRAM| 264 KiB| 520 KiB 内部 Flash| None| 要么 None (RP2350x),要么 2 MiB (RP2354x) 内部 OTP| None| 64 页,每页 64 个 24 位字(可读为 16 位 + ECC) 安全启动| No| Yes 外部存储器| 1x QSPI Flash (最大 16 MiB)| 2x QSPI Flash 和/或 PSRAM (每个最大 16 MiB) Flash 分区| 不支持| ROM 最多支持 16 个,带有地址转换 地址转换| None| 每个芯片选择四个 4 MiB 窗口,可映射到任何 4 KiB 偏移 GPIO| 30x 3.3V| 30x (xA) 或 48x (xB) 3.3V PIO 块| 2| 3 PWM 通道| 16| 24 ADC 通道| 4| 4 (xA) 或 8 (xB) DVI 输出| 软件实现 (使用 2x 超频)| 硬件 TMDS 编码器 & 高速双倍泵浦串行器 USB 设备| USB 1.1 全速| USB 1.1 全速 USB 主机| USB 1.1 低速/全速| USB 1.1 低速/全速 睡眠电流| 180 μA| 低至 10 μA 正如你所看到的,我们几乎拥有了一切 - 加上可以在启动时或运行时将两个 Cortex-M33 中的一个(或两个)换成开源[1] Hazard3 RISC-V 核心。 在我看来,唯一没有真正改善的是我们仍然卡在 USB 1.1 的 11 Mbps 全速 上,而不是升级到 USB 2.0 的 480 Mbps 高速 模式 - 这很可惜。 但是,嘿,它仍然只有一美元! 另外! 不再有单一 SKU - 而是有四种 RP235x 变体可供发布。 RP2040| RP2350A| RP2350B| RP2354A| RP2354B ---|---|---|---|--- 封装| QFN56| QFN60| QFN80| QFN60| QFN80 内部 Flash| None| None| None| 2 MiB| 2 MB GPIO| 30| 30| 48| 30| 48 ADC 引脚| 4| 4| 8| 4| 8 在这篇博客中,当我说 RP235x 时,我指的是 RP2350A、RP2350B、RP2354A 或 RP2354B 中的任何一个。 当我说 RP2350x 时,我指的是没有 flash 的部件之一。 当我说 RP235xA 或 xA 时,我指的是任何 QFN60 RP235x 部件,对于 RP235xB 或 xB,我指的是任何 QFN80 RP235x 部件。 所有 RP235x 都有相同的硅芯片,所以好消息是我们在考虑软件时可以很大程度上忽略这些。 即使是内部 Flash 实际上也是 封装中的 Flash,因此工作方式与外部 flash 完全相同。 由于焊盘在内部键合的方式,60 针封装上的 GPIO 实际上不是芯片上的前 30 个 GPIO,但是芯片内的重新映射功能意味着你可以假装它们是前 30 个 GPIO。 这仅仅是因为硅芯片勘误表意味着重新映射在 Arm 非安全模式下不起作用,但这没关系。 大多数代码都将在安全模式下运行。 我可以访问 RP2350A 的各种早期版本,这些版本安装在预生产的 Raspberry Pi Pico 2 上。
它能运行 Rust 吗?
当然,你可以为它编译 Rust 代码。 对于 Arm Cortex-M33 核心,使用 thumbv8m.main-none-eabihf
。 对于 Hazard3 RV32IMAC 核心,使用 riscv32imac-unknown-none-elf
。 我已经尝试过这两种方法,从编译器的角度来看没有任何问题。
我的实验性 HAL 位于 https://github.com/thejpster/rp-hal-rp2350-public。 截至今天,该存储库是公开的,但已存档。 我希望这些更改将在 https://github.com/rp-rs 上游采用,但我即将去度假,所以不会发送 PR。 此外,还有一些悬而未决的问题,我希望社区参与进来:
- 它应该与 RP2040 HAL 位于同一个存储库中,还是不同的存储库中?
- 当芯片具有不同的 PAC crates,因此具有不同的 PAC UART 类型时,你能否为 RP2040 和 RP2350 编写一个通用驱动程序,例如,对于 UART?
- 我们应该投入多少工作来使编写可以为 RP2040 或 RP2350 编译的应用程序变得容易?
- 示例是否应该从 HAL 存储库中移出,因为其中一些示例无法为 RISC-V 编译,这使得 CI 管道变得棘手?
- 你如何编写 RISC-V 中断处理程序,以便为 RISC-V RP2350 代码提供与 Arm RP2350 代码相同的
#[interrupt]
宏? - 谁将首先引爆 SECURE_MODE OTP 位,看看我们是否可以在此设备上启动签名的 Rust 固件?
编辑: 好的,几个月后我回到了这里,a) 修复了一些示例中的错别字,以及 b) 让你知道 RP2350 HAL 支持现在位于 https://github.com/rp-rs/rp-hal,所以你应该去使用该版本。 没有太多变化,但可能有一些有用的错误修复。
先别管细节,给我看看 Demo
我已经将 https://github.com/rp-rs HAL 移植到 RP235x,以及一些示例:
rustup target add thumbv8m.main-none-eabihf
rustup target add riscv32imac-unknown-none-elf
git clone https://github.com/thejpster/rp-hal-rp2350-public
cd rp-hal-rp2350-public/rp235x-hal
# 它为 Arm 构建
cargo build --example pwm_blink --target thumbv8m.main-none-eabihf --all-features
picotool load -t elf ./target/thumbv8m.main-none-eabihf/debug/pwm_blink
# 一些示例(没有中断的那些)也为 RISC-V 构建!
cargo build --example pwm_blink --target riscv32imac-unknown-none-elf --all-features
picotool load -t elf ./target/riscv32imac-unknown-none-elf/debug/pwm_blink
你需要按照 Raspberry Pi 的说明,了解如何使用他们的 Pico SDK 编译 picotool
。 遗憾的是,在有人添加 Arm Debug Interface v6 支持之前,标准的 Rust 工具(如 probe-rs
)将无法与 RP2350 配合使用,这非常重要(并且远远超出了我的技能)。
RP2040 上 Rust 的回顾
RP2040 有两个 Arm Cortex-M0+ CPU,因此合适的 Rust 目标是 thumbv6m-none-eabi
。
RP2040 有两个独立的 Rust 堆栈实现:
- 来自 https://github.com/rp-rs 的
rp2040-hal
- 来自 https://github.com/embassy-rs 的
embassy-rp
rp2040-hal
是我一直在使用的,但我听到了关于 embassy-rp
的好评 - 特别是如果你正在寻找为你的 RP2040 编写 Async Rust。 两者都让你为你的 RP2040 编写 Rust 代码,并使用所有集成的外围设备与漂亮的、高级的驱动程序。
今天 Rust 能做什么
RP235x 有两个 Arm Cortex-M33 CPU,因此 Arm 模式的合适 Rust 目标是 thumbv8m.main-none-eabihf
。 RISC-V 模式的合适目标是 riscv32imac-unknown-none-elf
。
到目前为止,我已经:
- 在 Arm 模式 和 RISC-V 模式下,在生产硅芯片上测试了以下示例:
- 仅在 Arm 模式下,在生产硅芯片上测试了以下示例:
- Multicore FIFO Blink
- Float Test (RP235x 的新增功能,具有 DCP 支持)
- POWMAN Test
- 在 Arm 模式下,在生产硅芯片上运行了 Neotron Pico BIOS 和 Neotron OS,具有:
- SD 卡支持
- 使用 PIO 的 VGA 视频(640x480,16 色)
- 使用 PIO 的 I2S 数字音频
- I2C 和 SPI
- 490,752 字节的可用 SRAM
- 由于更大的 CPU 核心和更丰富的 Armv8-M 指令集,即使在相同的时钟速度下,性能也有所提高
- 从 flash 的开头和分区内部启动了 Rust 应用程序。
这些更改位于 rp-rs
存储库的一个分支中,我今天将其公开。 我可以确认,从应用程序的角度来看,可能只需要进行少量更改。 HAL 中最大的 API 更改是围绕删除 RTC 外围设备 - 最接近的功能现在是 POWMAN
外围设备的“始终在线定时器”,其具有 64 位 1 毫秒的滴答声。 还有现在有两个标准 Timer 外设,因此你必须明确你要使用哪一个。
RP2040 启动的回顾
RP2040 从其内部 Mask ROM 启动。 这个 ROM 查找并执行 flash 开头的特殊 256 字节 启动块,它在你的代码启动之前将外部 flash 控制器重新编程为高速 QSPI 模式。 然后,它使用位于启动块之后的向量表(位于 flash 地址 0x100
或内存地址 0x1000_0100
)来启动你的应用程序,就像“常规”Arm CPU 一样。
启动 RP235x
这是我在移植我们现有的 Rust 代码以在 RP235x 上运行时面临的最大挑战,因此我想在此处详细介绍一下,以帮助那些沿着这条路跟随我的人。 基本上,RP235x 上的事情变得 有点复杂:
- 有两个“核心”,每个“核心”可以是 Arm (Cortex-M33) 模式或 RISC-V (Hazard3) 模式。
- 此选择会在核心重置后记住。
- Arm Cortex-M33 核心可以在 安全模式 或 非安全模式 下运行代码,并且外围设备/内存范围可以锁定为只能从 安全模式 访问。
- RISC-V Hazard3 核心可以在 机器模式 或 用户模式 下运行代码,并且外围设备/内存范围可以锁定为只能从 机器模式 访问。
- 启动 ROM 是核心运行的第一件事。
- 因此,既有 RISC-V ROM,也有 Armv8-M ROM。 实际上,我认为 RISC-V ROM 运行一个微小的 Arm 模拟器来执行 Arm 版本的代码 - 或者至少在早期版本中是这样。
- ROM 需要弄清楚你的代码在哪里,以及你是否想在 Cortex-M33 上运行 Arm 代码,或者在 Hazard3 上运行 RISC-V 代码。
- ROM 还处理固件签名、分区表、安全/非安全模式、OTP 启动、UART 启动、从 SRAM 执行等
- 片上有 一次可编程 Flash,它可以通过包含以下内容来影响启动:
- 哈希公钥,用于验证签名密钥是否有效,
- 自定义引导加载程序,或
- 自定义 QSPI flash 配置。
在 RP235x 上,256 字节的启动块消失了 - ROM 现在可以自动对外部 Flash 控制器进行编程,以使用各种常见的 QSPI flash(欢呼!)。 它有一个要尝试的命令和时钟分频器列表,但是如果你的 flash 芯片非常奇怪,ROM 无法使其工作,则可以将你的自定义 flash 配置放入 OTP 中。 虽然旧的启动块消失了,但你现在确实需要提供一个名为 Image Definition 的新数据结构,它位于 Flash 的前 4 KiB 内的某个位置。 这个 Image Definition 是称为 Block 的结构的特定形式。 Image Definition 告诉 ROM(或其他工具)关于你的应用程序的信息。 Block 必须形成为一个闭合的链表,称为 Block Loop。 如果你有一个 Block,它必须指向自身。
┏━━━━━━━━━━━━━━━━━━━┓
┃ 0xffffded3 ┃ 魔术 32 位起始值 } 标头
┣━━━━━━━━━━━━━━━━━━━┫
┃ 项目 0 ┃
┣━━━━━━━━━━━━━━━━━━━┫
┃ ... ┃
┣━━━━━━━━━━━━━━━━━━━┫
┃ 项目 N━1 ┃
┣━━━━━━━━━━━━━━━━━━━┫
┃ 字数 ┃ 此块中有多少个字 }
┣━━━━━━━━━━━━━━━━━━━┫ }
┃ 下一个块偏移 ┃ 到列表中下一个块的相对偏移 } 页脚
┣━━━━━━━━━━━━━━━━━━━┫ }
┃ 0xab123579 ┃ 魔术 32 位结束值 }
┗━━━━━━━━━━━━━━━━━━━┛
支持的项目包括(但不限于):
IMAGE_DEF
- 指定代码应在其上/之中运行的 CPU 核心和模式(Arm 安全模式、Arm 非安全模式或 RISC-V)VERSION
- 主要/次要版本信息,以及可选的回滚保护HASH_DEF
和HASH_VALUE
- 固件或 Block 的哈希SIGNATURE
- 固件或 Block 的非对称 (secp256k1
) 签名LOAD_MAP
- 代码是从 Flash 执行,还是复制到 RAM 并执行,还是因为其中包含数据而不是代码而被忽略VECTOR_TABLE
- 在 Arm 模式下设置中断向量表的特定位置ENTRY_POINT
- 设置自定义堆栈指针和_start
函数地址PARTITION_TABLE
- 将 flash 分割成 Partitions
为了简单起见,我认为你需要启动 RP235x 的最小 Image Definition 是:
┏━━━━━━━━━━━━━━━┓
┃ 0xffff_ded3 ┃ 魔术 32 位起始值 } 标头
┣━━━━━━━━━━━━━━━┫
┃ 0x1021_0142 ┃ 以 Arm 安全模式启动
┣━━━━━━━━━━━━━━━┫
┃ 0x0000_01ff ┃ 此块长 1 个字 }
┣━━━━━━━━━━━━━━━┫ }
┃ 0x0000_0000 ┃ 到下一个块的偏移量(指向自身) } 页脚
┣━━━━━━━━━━━━━━━┫ }
┃ 0xab12_3579 ┃ 魔术 32 位结束值 }
┗━━━━━━━━━━━━━━━┛
这些 32 位值应以小端格式存储。
单个块通过偏移量零指向自身,但你可能想在 flash 映像的末尾添加第二个 Block,其中包含该映像的哈希或签名。 实际上,picotool
可以 密封 ELF 文件或 UF2 文件,并为你添加这样一个新的 Block,将其连接到先前仅在 flash 开头包含 Image Definition 的 Block Loop 中。
要使用哈希 密封 文件,你可以执行以下操作:
picotool seal \
--verbose \
--major 1 --minor 0 \
--hash \
./target/thumbv8m.main-none-eabihf/release/examples/rom_funcs -t elf \
./target/rom_funcs_hashed.elf
要使用签名(受密码保护的哈希)密封 文件,你可以执行以下操作:
# 创建一个椭圆曲线密钥对
openssl ecparam -name secp256k1 -genkey -noout -out ec-secp256k1-priv-key.pem
# 使用我刚刚创建的密钥对二进制文件进行签名
picotool seal \
--verbose \
--major 1 --minor 0 \
--sign \
./target/thumbv8m.main-none-eabihf/release/examples/rom_funcs -t elf \
./target/rom_funcs_signed.elf \
./ec-secp256k1-priv-key.pem \
./target/rom_funcs_signed_otp.json
第二个命令同时生成一个签名文件和一个需要设置的 OTP 值的 JSON 列表(picotool
可以为你执行此操作)。
只有当 RP235x 处于 安全启动 模式时才会检查这些签名,并且该模式由 OTP 中的一位控制。 我还没有足够勇敢地打开我唯一的 Pico 2 板上的那个位。
除了 Image Definitions 之外,你还可以创建一个 Partition Table,这是另一种 Block。 Partition Table 可以描述 Flash 中最多 16 个分区,并控制它们是用于代码还是数据、它们是 A 分区还是 B 分区(用于 OTA 升级和先试后买),以及它们是用于 Arm 安全模式、Arm 非安全模式还是 RISC-V 代码。
你不必哈希你的 Partition Table,但这可能是一个好主意,这样你的 RP235x 就永远不会尝试使用损坏的分区表。 并且请注意,安全模式代码始终可以做任何它喜欢的事情并覆盖分区表,但非安全模式代码和 ROM 例程将遵守它,picotool
也会遵守它。 在运行时,你当前的的 Partition Table 位于它自己的特殊 RAM 片段中(我认为在 0x4000_0000
附近)。 如果你有一个 Partition Table,那么当你将 UF2 文件拖放到 USB 大容量存储接口时,它将根据 UF2 系列 ID 自动写入到相应的分区。
让我们使用 picotool
将此分区表添加到 Pico 2:
{
"version": [1, 0],
"unpartitioned": {
"families": ["absolute"],
"permissions": {
"secure": "rw",
"nonsecure": "rw",
"bootloader": "rw"
}
},
"partitions": [
{
"name": "Armageddon",
"id": 0,
"size": "2044K",
"families": ["rp2350-arm-s"],
"permissions": {
"secure": "rw",