range-set-blaze
移植到 no_std
中获得的实际经验
嵌入式设备上的 Rust 运行 — 来源: https://openai.com/dall-e-2/。其余图表由作者提供。
你想让你的 Rust 代码可以在任何地方运行——从大型服务器到网站、机器人,甚至手表?在这三篇文章的最后一篇中,我们将看到如何使用 Rust 在嵌入式设备上使用 no_std
模式来实现这一目标。
将你的 Rust 项目迁移到 no_std
环境下,可以让你在受限环境中创建高效软件,适用于微控制器和深度嵌入式系统。比如,我使用即将发布的 range-set-blaze
版本创建了一个 LED 动画编排器和合成器,该程序在 Raspberry Pi Pico 上运行。
一个展示Pico上LED动画的1分钟视频
不在标准库支持下运行Rust会带来独特的挑战。缺少操作系统支持的情况下,比如文件I/O、网络功能等,有时甚至无法进行动态内存分配。本文将介绍一些实用策略来应对这些限制。
将 Rust 移植到 no_std
环境需要仔细的步骤和选择,任何一步的疏忽都可能导致移植失败。我们将通过这九条规则来简化这个过程,下面我们将详细探讨这些规则。
- 确认您的项目可以在WASM WASI和WASM在浏览器中运行。
- 使用目标
thumbv7m-none-eabi
和cargo tree
来识别并解决与no_std
不兼容的依赖项问题。 - 将主要(非测试)代码标记为
no_std
和alloc
。将std::
替换为core::
和alloc::
。 - 使用Cargo功能使您的主要代码可选地使用
std
来执行与文件相关的等功能。 - 理解为什么测试代码总是使用标准库的原因。
- 创建一个简单的嵌入式测试项目,并使用QEMU运行它。
- 在
Cargo.toml
中,为与WASM和no_std
相关的项目添加关键字和类别。 - [可选] 使用预分配的数据类型以避免使用
alloc
。 - 在您的CI(持续集成)测试中添加对
thumbv7m-none-eabi
和QEMU的支持。
旁注:这些文章是基于我在蒙特利尔举行的RustConf24上进行的三小时工作坊。感谢参加那个工作坊的各位。特别感谢来自西雅图Rust Meetup的志愿者们,他们帮助测试了这些材料。这些文章替换了我去年撰写的文章,加入了更新的信息,该文章可以在这里找到。
与这一系列中的前两篇文章一样,在我们逐一讨论这些规则之前,让我们先定义一下术语吧。
- 原生: 您常用的系统(Linux, Windows, macOS)
- 标准库 (std) :提供 Rust 的核心功能,如
Vec
,String
,还有文件输入输出、网络和时间处理。 - WASM :WebAssembly (WASM) 是一种可以在大多数浏览器中运行的二进制指令集(以及浏览器之外的环境)。
- WASI :WebAssembly 系统接口 (WASI) 允许浏览器之外的 WASM 访问文件 I/O,网络(暂时不支持),和时间处理。
- no_std :指示 Rust 程序不使用完整的标准库,适用于小型嵌入式设备或资源有限的环境。
- alloc :在
no_std
环境中,它提供类似Vec
,String
的堆内存分配功能,对于动态内存管理非常重要。
根据我在 [range-set-blaze](https://github.com/CarlKCarlK/range-set-blaze)
,一个数据结构项目上的经验,我将依次推荐以下决策。为了避免含糊不清,我将把这些决策表述为规则。
在将 Rust 代码移植到嵌入式环境之前,确保它在 WASM WASI 和 浏览器中的 WebAssembly 中顺利运行。这些环境会暴露与脱离标准库相关的问题和挑战,并施加类似嵌入式系统的限制。通过尽早解决这些问题,你将更接近在嵌入式设备上运行你的项目。
我们希望代码运行的环境可以视为一个逐步增加限制条件的维恩图,这些限制越来越严格。
运行下面的命令以确认您的代码在WASM WASI和浏览器WASM环境下都能正常运行。
cargo test --target wasm32-wasip1
cargo test --target wasm32-unknown-unknown
如果测试失败或无法执行,请返回查看本系列之前的文章:WASM WASI 和 在浏览器中运行 WASM。
WASM WASI 文章也提供了重要的背景资料,有助于了解 Rust 目标、条件编译功能和 Cargo 功能。
一旦满足了这些先决条件,接下来就是看看我们的依赖项能否运行以及如何运行试试我们能否让它们在嵌入式系统上工作。
规则 2:使用thumbv7m-none-eabi
来识别并解决不兼容 no_std
的依赖项。
检查您的依赖项是否与嵌入式环境兼容,可以将项目编译成嵌入式目标环境。我建议您使用 thumbv7m-none-eabi
目标:
thumbv7m
— 表示ARM Cortex-M3 微控制器,这是一个流行的嵌入式处理器。none
— 表示没有可用的操作系统(OS)。在Rust中,这通常意味着我们不能依赖标准库(std
),所以我们使用no_std
。标准库通常提供的功能包括Vec
、String
、文件输入输出、网络和时间等。eabi
— 嵌入式应用二进制接口,定义了嵌入式应用程序的调用约定、数据类型和二进制布局的标准。
因为大多数嵌入式处理器都遵循no_std
规范,确保与这一目标的兼容性也有利于与其他嵌入式目标的兼容性。
安装目标程序并检查一下项目
运行 rustup target add thumbv7m-none-eabi
命令来添加目标支持,然后使用 cargo check --target thumbv7m-none-eabi
检查构建配置。
当我在这个 range-set-blaze
上做这个时,遇到了一些依赖项相关的错误,比如:
这说明我的项目依赖于num-traits
,它还依赖于either
,从而最终依赖于std
。
错误信息可能会让你感到困惑。要更清楚地了解情况,你可以运行这个 cargo tree
命令。
cargo tree --edges no-dev --format "{p} {f}"
它将展示项目依赖项及其启用的 Cargo 功能的递归列表。例如:
range-set-blaze v0.1.6 (C:\deldir\branches\rustconf24.nostd)
├── gen_ops v0.3.0
├── itertools v0.13.0 default,use_alloc,use_std
│ └── either v1.12.0 use_std
├── num-integer v0.1.46 default,std
│ └── num-traits v0.2.19 default,i128,std
│ [build-dependencies]
│ └── autocfg v1.3.0
└── num-traits v0.2.19 default,i128,std (*)
我们注意到多个名为 use_std
和 std
的 Cargo 特征,这强烈表明:
- 这些 Cargo 特性需要用到标准库。
- 我们可以关闭这些 Cargo 特性选项。
根据第 一篇文章 中的第六条规则,我们禁用了 use_std
和 std
这两个 Cargo 功能。回想一下,Cargo 特性是累加的,默认情况下是开启的。要关闭默认特性,我们需要将 default-features
设置为 false
。通过设置例如 features = ["use_alloc"]
来启用我们需要保留的 Cargo 功能。现在的 Cargo.toml
文件内容如下:
[dependencies]
gen_ops = "0.3.0" # 用于生成操作的库
itertools = { version = "0.13.0", features=["use_alloc"], default-features = false } # 提供迭代工具的库
num-integer = { version = "0.1.46", default-features = false } # 提供整数操作的库
num-traits = { version = "0.2.19", features=["i128"], default-features = false } # 提供数字特征的库
在某些情况下,关闭 Cargo 特性不一定能够让你的依赖项和 no_std
兼容。
例如,流行的 thiserror
crate 会将 std
引入你的代码,并且没有提供可用的 Cargo 功能来禁用它。然而,社区已经创建了 no_std
替代品。你可以通过类似 https://crates.io/search?q=thiserror+no_std 的搜索找到这些替代品。
在处理 range-set-blaze
这个问题时,仍然存在一个与 crate [gen_ops](https://crates.io/crates/gen_ops)
相关的问题——这是一个很好的 crate,可以方便地定义诸如 +
和 &
这样的操作符。该 crate 本不需要使用 std
,但确实使用了。我找到了一个简单的单行修改,并在我们即将介绍的 Rule 3 中提到的方法下进行了修改,然后提交了拉取请求。维护者接受了这个改动,并发布了新版本 0.4.0
。
有时,我们的项目无法禁用std
,因为在完整的操作系统上运行时,我们需要功能如文件访问。然而,在嵌入式系统上,我们确实必须放弃这类功能,甚至愿意这么做。在规则4中,我们将看到如何通过引入我们自己的Cargo特性来使std
的使用变得可选。
这些方法解决了 range-set-blaze
中所有的依赖错误。但是,解决这些错误后,在主代码中发现了281个错误。有进展了!
no_std
并启用 alloc
。将 std::
替换为 core::
,并将 alloc
替换为 alloc::
。
在你的项目中,lib.rs
(或main.rs
)文件的最上方添加:
#![no_std] // #[no_std] 表示不使用标准库
extern crate alloc; // extern crate alloc; 表示引入 alloc 依赖。
这意味着我们不会使用标准库中的功能,但我们仍然会进行内存分配。在 range-set-blaze
模块中,此更改将错误数量从281减少到52个。
剩下的许多错误是由于使用了在core
或alloc
中可找到的std
中的某些项造成的。由于std
的很大一部分只是core
和alloc
的重新导出,我们可以通过将std
引用切换到core
或alloc
来解决很多问题。这使我们能够在不依赖标准库的前提下保留基本功能。
例如,在这些情况下,我们为每一行得到了一个错误信息。
use std::cmp::max;
use std::cmp::Ordering;
use std::collections::BTreeMap;
将 std::
更改为 core::
或(如果与内存相关) alloc::
,可以解决这些错误:
use core::cmp::max;
use core::cmp::Ordering;
use alloc::collections::BTreeMap;
一些功能,比如文件访问,仅在 std
中才有——也就是说,这些功能不在 core
和 alloc
定义。幸运的是,对于 range-set-blaze
,切换到 core
和 alloc
之后,主代码中的 52 个错误全都被解决了。然而,这个修改也发现了测试代码中的 89 个错误。这又是一个进展!
我们将在规则5中解决测试代码里的错误,但首先,让我们弄清楚当需要文件访问等权限时我们该怎么办。在运行全功能操作系统时,这种情况会怎样处理。
规则 4:使用 Cargo 功能特性使主代码能够选择性地使用std
,进行与文件相关的函数(等等)的操作。
如果我们需要代码的两个版本——一个用于完整操作系统,另一个用于嵌入式系统——我们可以使用Cargo特性来实现(参见九条在WASI上运行Rust的规则中的规则6)。例如,让我们定义一个名为foo
的特性,它默认情况下启用。仅当启用foo
时,才包含demo_read_ranges_from_file
函数。
在 Cargo.toml
文件中:
[特性设置]
默认 = ["foo"]
foo = []
在 lib.rs
(暂定版本):
#[cfg(feature = "foo")]
// TODO: 尚未实现此功能。
pub fn demo_read_ranges_from_file<P, T>(path: P) -> std::io::Result<RangeSetBlaze<T>>
where
P: AsRef<std::path::Path>,
T: FromStr + Integer,
{
todo!("尚未实现此功能。");
}
只有在启用Cargo功能foo
时,才会定义函数demo_read_ranges_from_file
。现在我们能够检查代码的不同版本。
cargo check # 启用默认的 Cargo 功能 "foo"
cargo check --features foo # 同样启用 "foo"
cargo check --no-default-features # 禁用所有默认功能
现在让我们给 Cargo 特性赋予更有意义的名字,将 foo
重命名为 std
。Cargo.toml 文件内容如下,只是将其中的 foo
替换为 std
:
[package]
name = "std"
version = "0.1.0"
authors = ["Author Name"]
edition = "2018"
[dependencies]
[特性]
default = ["std"]
std = []
在我们的`lib.rs`文件中,我们在文件开头部分添加了这些行,以便在Cargo功能中的`std`被启用时引入`std`库。
#[cfg(feature = "std")]
extern crate std;
所以,`lib.rs` 最终是这样的:
#![no_std]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
// ...
#[cfg(feature = "std")]
pub fn demo_read_ranges_from_file<P, T>(path: P) -> std::io::Result<RangeSetBlaze<T>>
where
P: AsRef<std::path::Path>,
T: FromStr + Integer,
{
todo!("此功能尚未实现。");
}
我们还想对 `Cargo.toml` 文件做一些修改。我们想要用新的 Cargo 功能来管理依赖及其功能。
```toml
# Here, you can insert the actual `Cargo.toml` content
[features]
default = ["std"]
std = ["itertools/use_std", "num-traits/std", "num-integer/std"]
[dependencies]
itertools = { version = "0.13.0", features = ["use_alloc"], default-features = false }
num-integer = { version = "0.1.46", default-features = false }
num-traits = { version = "0.2.19", features = ["i128"], default-features = false }
gen_ops = "0.4.0"
# 说明:以下内容是Cargo.toml配置文件的一部分,定义了依赖项及其特性。
# "default" 特性默认为 "std",这些配置确保了项目中相关的依赖项被正确加载。
# 对于每个依赖项,指定了其版本号、特性以及是否启用默认特性。
旁注:如果你对
Cargo.toml
中指定依赖和功能的格式感到困惑,可以参阅我最近的文章:Nine Rust Cargo.toml Wats and Wat Nots: 掌握 Cargo.toml 格式规则,避免烦恼 在 Towards Data Science 上。
要检查你的项目能否使用标准库 (std
) 编译,也能不使用标准库编译,可以使用如下命令。
cargo check # 使用 std 标准库
cargo check --no-default-features # 不使用 std 标准库
cargo check
能正常工作,你可能以为cargo test
也应该是简单直接的。可惜的是,情况并非如此。我们下次再来看看这个。
当我们用 --no-default-features
编译项目时,它会运行在 no_std
环境中。然而,即使在 no_std
项目中,Rust 的测试框架仍然包含标准库。这是因为 cargo test
需要标准库;例如,#[test]
属性和测试套件本身都是在标准库中定义的。
因此,运行如下:
// 不启用 `no_std` 测试
cargo test --no-default-features
实际上并不会测试你的代码的 no_std
版本,这意味着在真正的 no_std
环境中不可用的 std
函数在测试时仍然可以访问。例如,下面的测试即使使用 --no-default-features
编译和运行,仍然会成功,尽管它使用了 std::fs
。
#[test]
fn test_read_file_metadata() {
// 测试读取文件元数据
let metadata = std::fs::metadata("./").unwrap(); // 获取当前目录的元数据
assert!(metadata.is_dir()); // 确保元数据是目录
}
此外,在测试时使用 std
模式,您可能需要显式地导入标准库中的特性。这是因为,尽管在测试期间可以使用 std
,但您的项目仍然以 #![no_std]
编译,这意味着标准库的预导入项不会自动生效。例如,您通常需要在测试代码中添加如下导入:
#![cfg(test)]
use std::prelude::v1::*;
use std::{format, print, println, vec};
这些导入从标准库中引入了必要的实用程序和工具,这样在测试时就可以使用它们了。
要想真正地在不使用标准库的情况下测试你的代码,你需要使用一些不依赖于cargo test
的替代方法。接下来我们将探讨如何在这一规则中运行no_std
测试。
你不能在嵌入式环境中运行常规的测试程序。然而,你可以——也应该——运行至少一个嵌入式测试。我的理念是,即使只有一个测试也远远好过没有测试。由于“如果编译通过,就认为可以工作”这一说法在no_std
项目中通常成立,一个或几个精心挑选的测试可以非常有效。
要运行此测试,我们使用QEMU(快速仿真器,发音为“cue-em-you”),它使我们能够在主操作系统(Linux、Windows 或 macOS)上模拟thumbv7m-none-eabi
代码。
请参阅 QEMU 更多信息 获取完整信息:
Linux/WSL(Windows子系统Linux)
- Ubuntu: 在Ubuntu中,你可以通过以下命令安装QEMU:
sudo apt-get install qemu-system
。 - Arch: 在Arch中,你可以通过以下命令安装QEMU系统:
sudo pacman -S qemu-system-arm
。 - Fedora: 在Fedora中,你可以通过以下命令安装QEMU系统:
sudo dnf install qemu-system-arm
。
zh: Windows
- 方法1: https://qemu.weilnetz.de/w64。运行安装程序(告诉 Windows 这是可信任的安装程序)。将路径
"C:\Program Files\qemu\"
添加到系统环境变量中。 - 方法2: 从 https://www.msys2.org/ 下载并安装 MSYS2。打开 MSYS2 UCRT64 终端程序。
pacman -S mingw-w64-x86_64-qemu
。将路径C:\msys64\mingw64\bin\
添加到系统环境变量中。
Mac电脑
brew install qemu
,或sudo port install qemu
测试安装时,请使用:
qemu-system-arm --version
查看QEMU ARM版本号
创建一个新的嵌入子项目。创建一个子项目,用于嵌入测试:
cargo new tests/embedded
// 创建一个新的Rust项目名为embedded,位于tests文件夹下
这个命令创建一个新的子项目,包括位于 tests/embedded/Cargo.toml
的配置文件。
此命令还会更新你的顶层
Cargo.toml
文件,将子项目添加到你的工作区内。在 Rust 中,工作区是一组相关包,定义在顶层Cargo.toml
文件的[workspace] 部分
。工作区内的所有包共享一个Cargo.lock
文件,确保依赖版本的一致性。
编辑 tests/embedded/Cargo.toml
并将其内容替换如下,但将 "range-set-blaze"
替换为你的主项目名称:
[package]
name = "embedded"
version = "0.1.0"
edition = "2021"
[dependencies]
alloc-cortex-m = "0.4.4"
cortex-m = "0.7.7"
cortex-m-rt = "0.7.3"
cortex-m-semihosting = "0.5.0"
panic-halt = "0.2.0"
# 请将此更改为指向您的顶级项目
range-set-blaze = { path = "../..", default-features = false }
更新测试代码吧。
将 tests/embedded/src/main.rs
文件的内容替换为:
// 基于 https://github.com/rust-embedded/cortex-m-quickstart/blob/master/examples/allocator.rs
// 和 https://github.com/rust-lang/rust/issues/51540
#![feature(alloc_error_handler)]
#![no_main]
#![no_std]
extern crate alloc;
use alloc::string::ToString;
use alloc_cortex_m::CortexMHeap;
use core::{alloc::Layout, iter::FromIterator};
use cortex_m::asm;
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
use panic_halt as _;
#[global_allocator]
static ALLOCATOR: CortexMHeap = CortexMHeap::empty();
const HEAP_SIZE: usize = 1024; // 字节(B)
#[alloc_error_handler]
fn alloc_error(_layout: Layout) -> ! {
asm::bkpt();
loop {}
}
#[entry]
fn main() -> ! {
unsafe { ALLOCATOR.init(cortex_m_rt::heap_start() as usize, HEAP_SIZE) }
// 仅在仿真环境中执行
use range_set_blaze::RangeSetBlaze;
let range_set_blaze = RangeSetBlaze::from_iter([100, 103, 101, 102, -3, -4]);
hprintln!("{:?}", range_set_blaze.to_string());
if range_set_blaze.to_string() != "-4..=-3, 100..=103" {
debug::exit(debug::EXIT_FAILURE); // 退出失败
}
debug::exit(debug::EXIT_SUCCESS); // 退出成功
loop {} // 无限循环
}
这段 main.rs
代码的大部分是嵌入式系统的常见启动代码。实际的测试代码如下:
use range_set_blaze::RangeSetBlaze;
let range_set_blaze = RangeSetBlaze::from_iter([100, 103, 101, 102, -3, -4]);
hprintln!("{:?}", range_set_blaze.to_string());
if range_set_blaze.to_string() != "-4..=-3, 100..=103" {
debug::exit(debug::EXIT_FAILURE);
}
如果测试失败,它将返回 EXIT_FAILURE
;否则,它将返回 EXIT_SUCCESS
。我们使用 hprintln!
宏在模拟时向控制台打印消息。由于这是一个嵌入式系统,代码最后进入一个无限循环。
在运行测试之前,需要将两个文件添加到子项目中:build.rs
和 memory.x
(来自 Cortex-M 快速入门仓库页面)。
Linux/WSL/macOS
cd tests/embedded # 切换到tests/embedded目录,并从GitHub下载两个文件。
wget https://raw.githubusercontent.com/rust-embedded/cortex-m-quickstart/master/build.rs
wget https://raw.githubusercontent.com/rust-embedded/cortex-m-quickstart/master/memory.
Windows (PowerShell)
cd tests/embedded # 切换到tests/embedded目录
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/rust-embedded/cortex-m-quickstart/master/build.rs' -OutFile 'build.rs' # 从GitHub下载build.rs文件并保存为build.rs
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/rust-embedded/cortex-m-quickstart/master/memory.x' -OutFile 'memory.x' # 从GitHub下载memory.x文件并保存为memory.x
同时创建文件 tests/embedded/.cargo/config.toml
,内容如下:
[目标.thumbv7m-none-eabi 段]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
[构建目标]
target = "thumbv7m-none-eabi"
此配置指示Cargo使用QEMU来运行嵌入式程序代码,并将thumbv7m-none-eabi
设为子模块的默认目标。
用 cargo run
(而不是 cargo test
)运行测试。
# 设置
# 将此子项目设置为 'nightly' 版本以支持 #![feature(alloc_error_handler)]
rustup override set nightly # 设置为 nightly 版本
rustup target add thumbv7m-none-eabi # 添加目标支持
# 如果需要,可以切换到 tests/embedded 文件夹下
运行 `cargo run`
你应该能看到日志信息,进程应该正常退出。在我这边,我看到的是:"-4..=-3, 100..=103"
。
这些步骤可能看起来像是为了运行一个(或几个)测试而做了大量的工作。然而,这主要是复制粘贴的一次性工作。此外,它允许在CI环境中运行测试(参见规则9)。相比之下,这样做,仅仅声称代码能在no_std
环境中工作,却从未真正运行过no_std
环境下的代码,可能会忽略关键问题。
接下来的规则就非常简单。
规则 7:在Cargo.toml
文件中为 WASM 和 无标准
添加关键字和类别。
一旦您的包成功编译并通过了额外的嵌入测试,您可以将其发布到crates.io,这是Rust的包仓库。为了告知他人该包与WASM和no_std
环境兼容,请在您的Cargo.toml
文件中添加以下关键词和类别:
[package]
# ...
categories = ["no-std", "wasm", "embedded"]#以及其他特定于您包的类别
keywords = ["no_std", "wasm"]#以及其他特定于您包的关键字
请注意,对于类别,我们使用破折号形式如 no-std
。对于关键词,no_std
(带下划线)比 no-std
更受欢迎,您的软件包最多可以有五个关键词和五个类别。
以下是一些可能感兴趣的,分类和关键词列表,以及每个术语对应的crate数量。
- 类别 no_std (6884)
- 类别 embedded (3455)
- 类别 wasm (2026)
- 类别 no_std::no_alloc (581)
- 标签 wasm (1686)
- 标签 no_std (1351)
- 标签 no-std (1157)
- 标签 embedded (925)
- 标签 webassembly (804)
合适的分类和关键词可以帮助人们找到您的包,但系统对此没有严格要求。没有检查分类和关键词准确性的机制,您也无需提供这些分类和关键词。
接下来,我们将探索你最可能遇到的最受限的环境。
规则八:[可选] 使用预先分配的数据类型以避免使用alloc
。
我的项目 range-set-blaze
实现了一个动态数据结构,这个结构需要通过 alloc
从堆中分配内存。但是,如果你的项目不需要动态内存分配的话,在这种情况下,它可以在内存完全预先分配的更严格的嵌入式环境中运行,比如程序加载时即分配好所有内存。
尽量避免使用 alloc
的原因:
- 完全确定的内存使用量
- 降低了运行时失败的风险(通常由内存碎片造成)
- 减少功耗
有些 crate 可以帮助你替换类似 Vec
, String
, 和 HashMap
的动态数据结构。这些替代品通常需要你指定一个固定的容量,这样在编译时就能确定其大小。具体选择哪个 crate 取决于你的具体需求。下表显示了一些常用的 crate:
arrayvec (固定大小的向量) |
提供固定大小的向量 |
---|---|
smallvec (小型向量,提升缓存效率) |
提供小向量,可以提升缓存效率 |
im (不可变的数据结构,如不可变的向量和哈希映射) |
提供不可变的数据结构,如不可变的向量和哈希映射 |
petgraph (用于图的数据结构,例如邻接矩阵和邻接列表) |
提供图数据结构,包括邻接矩阵和邻接列表 |
我推荐使用heapless
库,因为它提供了一组可以很好地配合使用的数据结构。
这里是一个使用heapless
的与LED显示相关的代码示例。此代码创建了一个将字节映射到整数列表的映射。我们将映射中的项目数量以及整数列表的长度限制为DIGIT_COUNT
(在这里是4)。
使用heapless库中的LinearMap和Vec;
// …
let mut map: LinearMap<u8, Vec<usize, DIGIT_COUNT>, DIGIT_COUNT> = LinearMap::new();
// …
let mut vec = Vec::default();
vec.push(index).unwrap();
map.insert(*byte, vec).unwrap(); // 实际上复制了一份
关于如何创建一个 no_alloc
项目,超出了我的经验范围,所以我不太清楚如何操作。但是,第一步是删除你根据规则三添加的这行代码:你在 lib.rs
或 main.rs
文件中的这一行。
// 请删除此行
规则九:在您的 CI(持续集成)测试中添加 thumbv7m-none-eabi
和 QEMU。
你的项目现在正在编译为 no_std
,并且已经通过了一个与嵌入式相关的测试。完成了吗?还没有呢。正如我在前面提到的两篇文章中所说:
要是不在持续集成(CI)里,那它就不存在。
记得持续集成(CI)是一个系统,它可以在你每次更新代码时自动运行测试。我使用GitHub Actions作为我的CI平台。我在.github/workflows/ci.yml
中添加了以下配置,用于在我的项目中对嵌入式系统进行测试:
test_thumbv7m_none_eabi:
name: 设置和检查嵌入式
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
target: thumbv7m-none-eabi
- name: 安装稳定版和夜间版工具链并检查
run: |
cargo check --target thumbv7m-none-eabi --no-default-features
rustup override set nightly
rustup target add thumbv7m-none-eabi
cargo check --target thumbv7m-none-eabi --no-default-features
sudo apt-get update && sudo apt-get install qemu qemu-system-arm
- name: 使用夜间版进行嵌入式测试
timeout-minutes: 超时分钟
run: |
cd tests/embedded
切换到嵌入式测试目录
cargo run
通过 CI 对嵌入式以及 no_std
进行测试,我可以放心我的代码将继续支持嵌入式平台。
所以,就是这样——九条将你的 Rust 代码移植到嵌入式环境的规则。要查看应用所有规则后的 range-set-blaze
项目快照,请参见该 Github 分支。
这里是我将代码移植到嵌入式设备时感到惊讶的几点:
不好的地方:
- 我们无法在嵌入式系统上运行现有测试。相反地,我们必须创建一个新的子项目并编写一些新的测试用例。
- 许多流行库依赖于
std
,因此,找到或适配与no_std
兼容的依赖库可能会很困难。
好的方面:
- Rust 中的谚语“编译通过就差不多能跑了”在嵌入式开发中也成立。这使我们对代码的正确性充满信心,而无需进行大量的新测试。
- 虽然
no_std
去除了我们对标准库的直接访问,但许多项目仍然可以通过core
和alloc
获得。 - 多亏了仿真技术,您就可以不依赖硬件来开发嵌入式系统。
感谢您与我一同踏上这段从WASI到WebAssembly在浏览器中的旅程,最终进入嵌入式开发的旅程。Rust 一直以其在各种环境中的高效和安全性给我留下了深刻的印象。在探索这些不同领域时,我希望您也会感受到 Rust 的灵活性和强大功能如同我一样令人信服。无论您是在云端服务器、浏览器环境,还是微控制器上开发,我们讨论的这些工具将帮助您自信地迎接接下来的挑战。
对未来的文章感兴趣吗?请在Medium上关注我https://medium.com/@carlmkadie。我写Rust和Python编程、机器学习和统计相关的内容。我每个月大概写一篇。
共同学习,写下你的评论
评论加载中...
作者其他优质文章