16个版本
0.4.0 | 2023年11月13日 |
---|---|
0.3.3 | 2023年2月2日 |
0.3.2 | 2023年1月22日 |
0.3.1 | 2022年6月11日 |
0.1.0 | 2020年9月8日 |
#190 in 嵌入式开发
每月176次下载
用于 stockbook
100KB
648 行
avr-progmem
AVR架构的Progmem实用工具。
此包提供用于在AVR微控制器的程序内存中工作的不安全实用工具。此外,它定义了一个“尽力而为”的安全包装结构 ProgMem
以简化其使用,以及一个用于字符串处理的包装器 PmString
。
此包仅在Rust和一些简短的汇编语言中实现,它不依赖于 avr-libc
或任何其他C库。然而,由于使用了内联汇编,此包只能使用 nightly Rust 编译器进行编译(截至2022年中,AVR的内联汇编仍然是“实验性的”)。
MSRV
此包与Rust nightly-2023-08-08
编译器一起工作。所有版本 0.4.x
将遵守与 nightly-2023-08-08
一起工作。其他Rust编译器版本(尤其是较新的版本)也可能工作,但由于使用了实验性编译器功能,某些未来的Rust编译器版本可能无法工作。
未来的版本,如 0.5.x
可能需要较新的Rust编译器版本。
AVR内存
此包专门用于 AVR基础微控制器,如Arduino Uno(以及一些其他Arduino板,但不是所有),这些微控制器具有修改后的Harvard架构,这意味着程序代码和数据之间有严格的分离,同时具有特殊的指令来读取和写入程序内存。
虽然当然,所有普通数据都存储在数据域中,那里它完全可用,但大多数AVR处理器的严格限制使得使用程序存储器(也称为progmem)来存储常量值非常吸引人。然而,由于哈佛架构,这些值不能使用常规指令(即来自常规Rust代码发出的指令)使用。相反,需要特殊的指令来从程序代码域中加载数据,例如lpm
(从程序存储器加载)指令。由于无法从Rust代码中发出它,因此此crate使用内联汇编来发出该指令。
然而,由于程序代码中的指针与普通数据指针无法区分,完全取决于程序员确保这些不同的'指针类型'不会意外混淆。换句话说,这在Rust的上下文中是unsafe
。
从程序存储器加载数据
此crate的第一部分简单地提供了一些函数(例如read_byte
),用于将常量数据(即不可变的Rust static
)从程序存储器加载到数据域中,以便随后它成为可正常使用的数据,即作为堆栈上的所有者数据。
因为,如前所述,Rust中的简单*const u8
并没有指定它是否位于程序代码域或数据域,因此所有从程序存储器加载给定指针的函数本质上都是unsafe
。
请注意,使用程序代码域的引用(例如&u8
)通常应避免,因为Rust中的引用应该是可解引用的,而程序代码域不是。
此外,引用可以很容易地由安全代码解引用,如果该引用指向程序存储器,这将是不确定的(UB)行为。因此,Rust中对存储在程序存储器中的static
的引用必须被认为是危险的(如果不是UB),并建议只使用这些static
的原始指针,例如通过addr_of!
宏,该宏直接创建原始指针而无需引用。
示例
use avr_progmem::raw::read_byte;
use core::ptr::addr_of;
// This `static` must never be directly dereferenced/accessed!
// So a `let data: u8 = P_BYTE;` ⚠️ is **undefined behavior**!!!
/// Static byte stored in progmem!
#[link_section = ".progmem.data"]
static P_BYTE: u8 = b'A';
// Load the byte from progmem
// Here, it is sound, because due to the link_section it is indeed in the
// program code memory.
let data: u8 = unsafe { read_byte(addr_of!(P_BYTE)) };
assert_eq!(b'A', data);
最佳努力包装器
由于与progmem数据一起工作本质上是不可安全的且很难正确进行,此crate引入了最佳努力的'safe'包装器ProgMem
,该包装器应仅包装progmem中的数据,从而仅提供使用上述引入的progmem加载函数来加载其内容的函数。如果包装器数据确实存储在程序存储器中,则使用这些函数是合理的。因此,为了强制执行此不变性,ProgMem
的构造函数是unsafe
。
此外,由于正确的Rust引用(与指针不同)有很多特殊要求,因此访问存储在程序内存中的数据的引用应被视为危险。相反,应该只保留此类数据的原始指针,例如通过addr_of!
宏创建。因此,ProgMem
只是将数据在progmem中的指针封装起来,而这个指针反过来必须存储在标记为static
的#[link_section = ".progmem.data"]
的标记中。然而,由于安全Rust可以始终创建对任何(可访问)static
的“正常”Rust引用,因此将此类static
暴露给安全Rust代码被认为是不安全的,甚至是不合理的。
为了使这个过程更容易(也更安全),这个crate提供了一个progmem!
宏,该宏将在程序内存中创建一个隐藏的static
,并用你提供的数据对其进行初始化,将它的指针封装在ProgMem
结构中,并将这个包装器放入另一个(普通RAM)静态中,以便你可以访问它。这将确保存储在程序内存中的static
不能被安全Rust代码引用(因为它不可访问),而可访问的ProgMem
包装器允许通过从程序内存正确加载来访问底层数据。
示例
use avr_progmem::progmem;
// It will be wrapped in the ProgMem struct and expand to:
// ```
// static P_BYTE: ProgMem<u8> = {
// #[link_section = ".progmem.data"]
// static INNER_HIDDEN: u8 = 42;
// unsafe { ProgMem::new(addr_of!(INNER_HIDDEN)) }
// };
// ```
// Thus it is impossible for safe Rust to directly access the progmem data!
progmem! {
/// Static byte stored in progmem!
static progmem P_BYTE: u8 = 42;
}
// Load the byte from progmem
// This is sound, because the `ProgMem` always uses the special operation to
// load the data from program memory.
let data: u8 = P_BYTE.load();
assert_eq!(42, data);
字符串
使用&str
与ProgMem
一起使用相当困难,如果需要Unicode支持,则更令人惊讶(参见问题 #3)。因此,为了使字符串的处理更方便,在ProgMem
之上提供了一个PmString
结构。
PmString
将任何给定的&str
存储为静态大小的UTF-8字节数组(具有完整的Unicode支持)。为了使其内容可用,它提供了一个Display
& uDisplay
实现,一个懒惰的chars
迭代器,以及类似于ProgMem
的load
函数,该函数返回一个LoadedString
,它随后将延迟到&str
。
有关更多详细信息,请参阅字符串模块。
示例
use avr_progmem::progmem;
progmem! {
// A simple Unicode string in progmem.
static progmem string TEXT = "Hello 大賢者";
}
// You can load it and use that as `&str`
let buffer = TEXT.load();
assert_eq!("Hello 大賢者", &*buffer);
// Or you use directly the `Display` impl
assert_eq!("Hello 大賢者", format!("{}", TEXT));
此外,还提供了两个类似于Arduino IDE中F
宏的特殊宏,允许将字符串标记为存储在progmem中,同时在此处作为已加载的&str
返回。
// Or you skip the static and use in-line progmem strings:
use avr_progmem::progmem_str as F;
use avr_progmem::progmem_display as D;
// Either as `&str`
assert_eq!("Foo 大賢者", F!("Foo 大賢者"));
// Or as some `impl Display + uDisplay`
assert_eq!("Bar 大賢者", format!("{}", D!("Bar 大賢者")));
如果你启用了ufmt
crate功能(这是一个默认功能),你也可以使用uDisplay
,除了Display
。
use avr_progmem::progmem;
use avr_progmem::progmem_str as F;
use avr_progmem::progmem_display as D;
fn foo<W: ufmt::uWrite>(writer: &mut W) {
progmem! {
// A simple Unicode string in progmem.
static progmem string TEXT = "Hello 大賢者";
}
// You can use the `uDisplay` impl
ufmt::uwriteln!(writer, "{}", TEXT);
// Or use the in-line `&str`
writer.write_str(F!("Foo 大賢者\n"));
// Or the in-line `impl uDisplay`
ufmt::uwriteln!(writer, "{}", D!("Bar 大賢者"));
}
//
其他架构
如前所述,这个包专门设计用于与AVR基础微控制器配合使用。但由于我们大多数人不在AVR系统上编写程序,而是在例如x86系统上编写,并且可能想在那些系统上测试它们(只要可能),因此这个包还提供了对所有非AVR架构的回退实现,回退到默认数据段中的简单Rust static
。所有数据加载函数都只是引用指向的数据,假设它们只存在于默认位置。
这个回退在x86及其友好的架构上非常安全,也应该在其他所有架构上都没有问题,否则正常的Rust static
可能会损坏。然而,当编写不限于AVR的库时,了解这一点是很重要的。
实现限制
除了已经讨论的内容之外,当前实现还有两个进一步的限制。
首先,由于这个包在8位架构上使用内联汇编循环,循环计数器只能允许值高达255。这意味着一次最多只能用这个包的任何方法加载255字节。然而,这仅适用于单个连续的加载操作,例如,ProgMem<[u8;1024]>::load()
将会引发panic,但是以较小的块访问这样的类型,例如ProgMem<[u8;1024]>::load_sub_array::<[u8;128]>(512)
是完全可以的,因为要加载的类型[u8;128]
只有128字节大小。请注意,同样的限制也适用于PmString<N>::load()
(即只有当N <= 255
成立时才能使用。另一方面,在PmString<N>::chars()
和PmString
的Display
/uDisplay
实现中,没有这样的限制,因为那些,只是单独加载每个char
(即每次最多不超过4字节)。
其次,由于这个包仅使用lpm
指令,该指令受16位指针的限制,因此这个包只能与存储在程序内存低64 kiB中的数据一起使用。由于尚未对该属性进行测试,因此不清楚它是否会导致panic或完全未定义的行为,因此当与具有超过64 kiB程序内存的AVR芯片一起工作时,请务必小心。
许可证
基于Apache许可证,版本2.0(LICENSE 或 https://www.apache.org/licenses/LICENSE-2.0)。
贡献
除非你明确表示,否则根据Apache-2.0许可证定义的,你有意提交的任何贡献,包括在本项目中包含的贡献,将按照上述方式授权,不附加任何额外条款或条件。
依赖项
~1.5MB
~37K SLoC