This commit is contained in:
Xingyu Wang 2020-01-28 12:22:46 +08:00
parent d17cd544d3
commit 0826a4308b

View File

@ -124,13 +124,13 @@ fn enable_register(&mut reg) {
}
```
使用 Rust你可以使用数据结构来表示字段将它们附加到特定的寄存器,并在与硬件交互时提供简洁明了的工效。这个例子使用了 Rust 提供的最基本的功能。无论如何,添加的结构都会减轻上述 C 示例中的某些晦涩的地方。现在,字段是个带有名字的事物,而不是从模糊的按位运算符派生而来的数字,并且寄存器是具有状态的类型 —— 这在硬件上多了一层抽象。
使用 Rust你可以使用数据结构来表示字段将它们与特定的寄存器联系起来,并在与硬件交互时提供简洁明了的工效。这个例子使用了 Rust 提供的最基本的功能。无论如何,添加的结构都会减轻上述 C 示例中的某些晦涩的地方。现在,字段是个带有名字的事物,而不是从模糊的按位运算符派生而来的数字,并且寄存器是具有状态的类型 —— 这在硬件上多了一层抽象。
### 一个易用的 Rust 实现
用 Rust 重写的第一个版本很好,但是并不理想。你必须记住要带上掩码和偏移量,并且要手工进行临时计算,这容易出错。人类不擅长精确且重复的任务 —— 我们往往会感到疲劳或失去专注力,这会导致错误。一次一个寄存器地手动记录掩码和偏移量几乎可以肯定会以糟糕的结局而告终。这是最好留给机器的任务。
其次,从结构上进行思考:如果有一种方法可以让字段的类型携带掩码和偏移信息呢?如果可以在编译时就发现所实现的硬件寄存器的访问和交互中存在错误,而不是在运行时才发现,该怎么办?也许你可以依靠一种常用的策略在编译时解决问题,例如类型。
其次,从结构上进行思考:如果有一种方法可以让字段的类型携带掩码和偏移信息呢?如果可以在编译时就发现硬件寄存器的访问和交互的实现代码中存在错误,而不是在运行时才发现,该怎么办?也许你可以依靠一种在编译时解决问题的常用策略,例如类型。
你可以使用 [typenum][6] 来修改前面的示例,该库在类型级别提供数字和算术。在这里,你将使用掩码和偏移量对 `Field` 类型进行参数化,使其可用于任何 `Field` 实例,而无需将其包括在调用处:
@ -175,7 +175,7 @@ fn enable_register(&mut reg) {
}
```
看起来不错,但是……如果你在给定的值是否*适合*字段方面犯了错误,会发生什么?考虑一个简单的输入错误,你在其中放置了 `10` 而不是 `1`
看起来不错,但是……如果你在给定的值是否*适合*字段方面犯了错误,会发生什么?考虑一个简单的输入错误,你在其中放置了 `10` 而不是 `1`
```
fn enable_register(&mut reg) {
@ -183,7 +183,7 @@ fn enable_register(&mut reg) {
}
```
在上面的代码中,预期结果是什么?好吧,代码会将启用位设置为 0因为 `101 = 0`。那真不幸;最好在尝试写入之前知道你要写入字段的值是否适合该字段。事实上,我会考虑放弃错误字段值的高位*未定义行为*(喘气)。
在上面的代码中,预期结果是什么?好吧,代码会将启用位设置为 0因为 `101 = 0`。那真不幸;最好在尝试写入之前知道你要写入字段的值是否适合该字段。事实上,我认为截掉错误字段值的高位是一种 1*未定义的行为*(哈)。
### 出于安全考虑使用 Rust
@ -219,11 +219,11 @@ impl<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> Field<Width, Mask, Offse
}
```
现在,只有给定值适合时,你才能构造一个 `Field`!否则,你将得到 `None` 信号,该信号指示发生了错误,而不是放弃该值的高位并静默写入意外的值。
现在,只有给定值适合时,你才能构造一个 `Field` !否则,你将得到 `None` 信号,该信号指示发生了错误,而不是截掉该值的高位并静默写入意外的值。
但是请注意,这将在运行时环境中引发错误。但是,我们知道我们想事先写入的值,还记得吗?鉴于此,我们可以教编译器完全拒绝具有无效字段值的程序 —— 我们不必等到运行它!
但是请注意,这将在运行时环境中引发错误。但是,我们事先知道我们想写入的值,还记得吗?鉴于此,我们可以教编译器完全拒绝具有无效字段值的程序 —— 我们不必等到运行它!
这次,你将向 `new` 的新实现 `new_checked` 中添加一个特征绑定(`where` 子句),该函数要求输入值小于或等于给定字段用 `Width` 所能控制的最大可能值:
这次,你将向 `new` 的新实现 `new_checked` 中添加一个特征绑定(`where` 子句),该函数要求输入值小于或等于给定字段用 `Width` 所能容纳的最大可能值:
```
struct Field<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> {
@ -252,7 +252,7 @@ impl<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> Field<Width, Mask, Offse
}
```
只有此属性保存的数字才具有此特征的实现,因此,如果使用不适合的数字,它将无法编译。让我们看一看!
只有拥有此属性的数字才实现此特征,因此,如果使用不适合的数字,它将无法编译。让我们看一看!
```
fn enable_register(&mut reg) {
@ -265,15 +265,15 @@ fn enable_register(&mut reg) {
found type `typenum::B1`
```
`new_checked` 将无法生成一个程序,因为该字段的值错误地过高。你的输入错误不会在运行时环境中爆炸,因为你永远无法获得一个可以运行的工件。
`new_checked` 将无法生成一个程序,因为该字段的值有错误的高位。你的输入错误不会在运行时环境中才爆炸,因为你永远无法获得一个可以运行的工件。
就使内存映射的硬件进行交互的安全性而言,你已经接近 Rust 的极致。但是,你在 C 的第一个示例中所写的内容比最终得到的一锅粥类型参数更简洁。当你谈论潜在的数百甚至数千个寄存器时,这样做是否容易处理?
就使内存映射的硬件进行交互的安全性而言,你已经接近 Rust 的极致。但是,你在 C 的第一个示例中所写的内容比最终得到的一锅粥的类型参数更简洁。当你谈论潜在可能有数百甚至数千个寄存器时,这样做是否容易处理?
### Rust 恰到好处:既安全又方便使用
### Rust 恰到好处:既安全又方便使用
早些时候,我认为手工计算掩码有问题,但我只是做了同样有问题的事情 —— 尽管是在类型级别。虽然使用这种方法很不错,但要达到编写任何代码的地步,则需要大量样板和手动转录(我在这里谈论的是类型同义词)。
早些时候,我认为手工计算掩码有问题,但我做了同样有问题的事情 —— 尽管是在类型级别。虽然使用这种方法很不错,但要达到编写任何代码的地步,则需要大量样板和手动转录(我在这里谈论的是类型同义词)。
我们的团队想要像 [TockOS mmio 寄存器][7]之类的东西,但是他们会以最少的手动转录生成类型安全的实现。我们得出的结果是一个宏,该宏生成必要的样板以获得类似 Tock 的 API 以及基于类型的边界检查。 要使用它,请写下一些有关寄存器的信息,其字段、宽度和偏移量以及可选的[枚举][8]类的值(你应该为字段可能具有的值赋予“含义”):
我们的团队想要像 [TockOS mmio 寄存器][7]之类的东西,以最少的手动转录生成类型安全的实现。我们得出的结果是一个宏,该宏生成必要的样板以获得类似 Tock 的 API 以及基于类型的边界检查。要使用它,请写下一些有关寄存器的信息,其字段、宽度和偏移量以及可选的[枚举][8]类的值(你应该为字段可能具有的值赋予“含义”):
```
register! {
@ -297,7 +297,7 @@ register! {
}
```
由此,你可以生成寄存器和字段类型,如上例所示,其中索引:`Width`、`Mask` 和 `Offset` 是从一个字段定义的 `WIDTH``OFFSET` 部分的输入值派生的。另外,请注意,所有这些数字都是 “类型数字”;们将直接进入你的 `Field` 定义!
由此,你可以生成寄存器和字段类型,如上例所示,其中索引:`Width`、`Mask` 和 `Offset` 是从一个字段定义的 `WIDTH``OFFSET` 部分的输入值派生的。另外,请注意,所有这些数字都是 “类型数字”;们将直接进入你的 `Field` 定义!
生成的代码通过为寄存器及字段指定名称来为寄存器及其相关字段提供名称空间。这很绕口,看起来是这样的:
@ -318,15 +318,15 @@ mod Status {
}
```
生成的 API 包含名义上期望的读取和写入的原语,以获取原始寄存器的值,但它也有办法获取单个字段的值、执行集体操作以及确定是否有任何(或全部)位集合的方法。你可以阅读[完整生成的 API][9]上的文档。
生成的 API 包含名义上期望的读取和写入的原语,以获取原始寄存器的值,但它也有办法获取单个字段的值、执行集合操作以及确定是否设置了任何(或全部)位集合的方法。你可以阅读[完整生成的 API][9]上的文档。
### 粗略检查
将这些定义用于实际设备会是什么样?代码中是否会充斥着类型参数,从而掩盖了视图中的实际逻辑?
不会!通过使用类型同义词和类型推断,你实际上根本不必考虑程序的类型级别部分。你可以直接与硬件交互,并自动获得与边界相关的保证。
不会!通过使用类型同义词和类型推断,你实际上根本不必考虑程序的类型层面部分。你可以直接与硬件交互,并自动获得与边界相关的保证。
这是一个 [UART] [10] 寄存器块的示例。我将跳过寄存器本身的声明,因为包括在这里就太多了。而是从寄存器“块”开始,然后帮助编译器知道如何从指向该块开头的指针中查找寄存器。我们通过实现 `Deref``DerefMut` 来做到这一点:
这是一个 [UART][10] 寄存器块的示例。我会跳过寄存器本身的声明,因为包括在这里就太多了。而是从寄存器“块”开始,然后帮助编译器知道如何从指向该块开头的指针中查找寄存器。我们通过实现 `Deref``DerefMut` 来做到这一点:
```
#[repr(C)]
@ -381,7 +381,7 @@ fn main() {
}
```
当我们使用运行时值时,我们使用如前所述的 `Option`。这里我使用的是 `unwrap`,但是在一个实际的输入未知的程序中,你可能想检查一下从新调用中返回的 “Some” [^1] [^2]
当我们使用运行时值时,我们使用如前所述的**选项**。这里我使用的是 `unwrap`,但是在一个输入未知的真实程序中,你可能想检查一下从新调用中返回的**某些东西** [^1] [^2]
```
fn main() {
@ -402,7 +402,7 @@ fn main() {
### 解码失败条件
根据你的个人痛苦阈值,你可能已经注意到错误几乎是无法理解的。看一下我在说什么的微妙提醒:
根据你的个人痛苦忍耐程度,你可能已经注意到这些错误几乎是无法理解的。看一下我所说的不那么微妙的提醒:
```
error[E0271]: type mismatch resolving `<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>, typenum::B0> as typenum::IsLessOrEqual<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>>>::Output == typenum::B1`
@ -415,9 +415,9 @@ error[E0271]: type mismatch resolving `<typenum::UInt<typenum::UInt<typenum::UIn
found type `typenum::B1`
```
` expected typenum::B0 found typenum::B1` 部分是有意义的,但是 ` typenum::UInt<typenum::UInt, typenum::UInt` 到底是什么呢?好吧,`typenum` 将数字表示为二进制[cons][13] 单元!像这样的错误使操作变得很困难,尤其是当你将多个这些类型级别的数字限制在狭窄的范围内时,你很难知道它在说哪个数字。当然,除非你将巴洛克式二进制表示形式转换为十进制表示形式是第二天性
`expected struct typenum::B0, found struct typenum::B1` 部分是有意义的,但是 ` typenum::UInt<typenum::UInt, typenum::UInt...` 到底是什么呢?好吧,`typenum` 将数字表示为二进制 [cons][13] 单元!像这样的错误使操作变得很困难,尤其是当你将多个这些类型级别的数字限制在狭窄的范围内时,你很难知道它在说哪个数字。当然,除非你一眼就能将巴洛克式二进制表示形式转换为十进制表示形式。
在第 U100 次试图从这个混乱中破译任何含义之后,一个队友简直《疯了,地狱了,不再要再忍受了》,并做了一个小工具,`tnfilt`,从痛苦中解脱了含义,它是命名空间的二进制约束单元。`tnfilt` 采用 cons 单元格样式表示法,并将其替换为明智的十进制数字。我们认为其他人也会遇到类似的困难,所以我们分享了 [tnfilt][14]。你可以像这样使用它:
在第 U100 次试图从这个混乱中破译出某些含义之后,我们的一个队友简直《<ruby>疯了,地狱了,不要再忍受了<rt>Mad As Hell And Wasn't Going To Take It Anymore</rt></ruby>》,并做了一个小工具 `tnfilt`,从这种命名空间的二进制 cons 单元的痛苦中解脱出来。`tnfilt` 将 cons 单元格式的表示法替换为可让人看懂的十进制数字。我们认为其他人也会遇到类似的困难,所以我们分享了 [tnfilt][14]。你可以像这样使用它:
```
$ cargo build 2>&1 | tnfilt
@ -429,22 +429,22 @@ $ cargo build 2>&1 | tnfilt
error[E0271]: type mismatch resolving `<U20 as typenum::IsLessOrEqual<U10>>::Output == typenum::B1`
```
现在*这*有意义!
现在*这*有意义!
### 结论
与软件中的硬件进行交互时,普遍使用内存映射寄存器,并且有无数种方法来描述这些交互,每种方法在易用性和安全性上都有不同的权衡。我们发现使用类型级编程来取内存映射寄存器交互的编译时检查为我们提供制作更安全软件的必要信息。该代码可在 [bounded-registers][15] crateRust包中找到。
在软件与硬件进行交互时,普遍使用内存映射寄存器,并且有无数种方法来描述这些交互,每种方法在易用性和安全性上都有不同的权衡。我们发现使用类型级编程来取内存映射寄存器交互的编译时检查可以为我们提供制作更安全软件的必要信息。该代码可在 [bounded-registers][15] crateRust 包)中找到。
我们的团队从安全性较高的一面开始,然后尝试找出如何将易用滑块移近易用端。从这些雄心壮志中,“边界寄存器”就诞生了,我们在 Auxon 冒险中遇到遇到内存映射设备的任何时候都可以使用它。
我们的团队从安全性较高的一面开始,然后尝试找出如何将易用滑块移近易用端。从这些雄心壮志中,“边界寄存器”就诞生了,我们在 Auxon 公司的冒险中遇到内存映射设备的任何时候都可以使用它。
* * *
[^1]: 从技术上讲,从定义上看,从寄存器字段读取的值只能在规定的范围内,但是我们当中没有一个人生活在一个纯净的世界中,而且你永远都不知道外部系统发挥作用时会发生什么。你是在这里接受硬件之神的命令,因此与其强迫你进入“可能的恐慌”状态,还不如给你提供处理“这将永远不会发生”的机会。
[^2]: `get_field` 看起来有点奇怪。我正在专门查看 `Field::Read` 部分。`Field` 是一种类型,你需要该类型的实例才能传递给 `get_field`。更干净的 API 可能类似于:`regs.rx.get_field::<UartRx::Data::Field>();`但是请记住,`Field` 是类型的同义词,它具有固定的宽度、偏移量等索引。要像这样对 `get_field` 进行参数化,你需要使用更高级的类型。
[^2]: `get_field` 看起来有点奇怪。我正在专门查看 `Field::Read` 部分。`Field` 是一种类型,你需要该类型的实例才能传递给 `get_field`。更干净的 API 可能类似于:`regs.rx.get_field::<UartRx::Data::Field>();` 但是请记住,`Field` 是一种具有固定的宽度、偏移量等索引的类型的同义词。要像这样对 `get_field` 进行参数化,你需要使用更高级的类型。
* * *
此内容最初出现在 [Auxon Engineering 博客] [16]上,并经许可进行编辑和重新发布。
此内容最初发布在 [Auxon Engineering 博客][16]上,并经许可进行编辑和重新发布。
--------------------------------------------------------------------------------