本文共 22794 字,大约阅读时间需要 75 分钟。
rust编程
Rust是一种越来越流行的编程语言,被定位为硬件接口的最佳选择。 通常将其与C的抽象级别进行比较。 本文介绍了Rust如何以多种方式处理按位运算,并提供了既安全又易于使用的解决方案。
语言 | 起源 | 官方说明 | 总览 |
---|---|---|---|
C | 1972年 | C是一种通用编程语言,具有表达经济,现代控制流和数据结构以及丰富的运算符集的特点。 (来源: ) | C是命令式语言,旨在以相对简单的方式进行编译,从而提供对内存的低级访问。 (来源: ) |
Rust | 2010 | 一种使所有人都能构建可靠,高效的软件的语言(来源: ) | Rust是一种专注于安全性(尤其是安全并发性)的多范式系统编程语言。 (来源: ) |
在系统编程领域,您可能会发现自己在编写硬件驱动程序或直接与内存映射的设备进行交互,而交互几乎总是通过硬件提供的内存映射寄存器来完成的。 通常,您通过对某些固定宽度的数字类型进行按位运算来与这些事物进行交互。
例如,假设一个具有三个字段的8位寄存器:
+----------+------+-----------+---------+ | (unused) | Kind | Interrupt | Enabled | +----------+------+-----------+---------+ 5-7 2-4 1 0
字段名称下面的数字规定了该字段在寄存器中使用的位。 要启用该寄存器,您将写入值1 (以二进制表示为0000_0001 )以设置启用字段的位。 但是,通常,您也不想干扰寄存器中的现有配置。 假设您要在设备上启用中断,但也要确保设备保持启用状态。 为此,必须将“中断”字段的值与“已启用”字段的值结合起来。 您可以通过按位操作来做到这一点:
1 | (1 << 1)
这将二进制值0000_0011乘以2 或-将其与1左移1得到。您可以将其写入寄存器,使其保持启用状态,但也允许中断。
有很多事情要记住,特别是当您要为一个完整的系统处理数百个寄存器时。 实际上,您可以使用助记符来执行此操作,助记符可跟踪字段在寄存器中的位置以及字段的宽度(即,字段的上限是多少)?
这是这些助记符之一的示例。 它们是C宏,它们用右侧的代码替换它们的出现。 这是上面列出的寄存器的简写。 在&让你在位置的那场,和右手边界限的左手边你只字段的位:
#define REG_ENABLED_FIELD(x) (x << 0) & 1 #define REG_INTERRUPT_FIELD(x) (x << 1) & 2 #define REG_KIND_FIELD(x) (x << 2) & (7 << 2)
然后,您可以使用这些来抽象出寄存器值的推导,例如:
void set_reg_val ( reg * u8 , val u8 ) ; fn enable_reg_with_interrupt ( reg * u8 ) { set_reg_val ( reg , REG_ENABLED_FIELD ( 1 ) | REG_INTERRUPT_FIELD ( 1 ) ) ; }
这是最新技术。 实际上,这就是大多数驱动程序出现在Linux内核中的方式。
有没有更好的办法? 如果类型系统是基于对现代编程语言的研究得出的,则考虑对安全性和可表达性的好处。 也就是说,如何使用更丰富,更具表现力的类型系统使此过程更安全,更持久?
以上面的寄存器为例:
+----------+------+-----------+---------+ | (unused) | Kind | Interrupt | Enabled | +----------+------+-----------+---------+ 5-7 2-4 1 0
您可能想如何用Rust类型表示这种事情?
您将以类似的方式开始,为每个字段的偏移量(即距最低有效位有多远)及其掩码定义常数。 掩码是一个值,其二进制表示形式可用于更新或读取寄存器内部的字段:
const ENABLED_MASK: u8 = 1 ; const ENABLED_OFFSET: u8 = 0 ; const INTERRUPT_MASK: u8 = 2 ; const INTERRUPT_OFFSET: u8 = 1 ; const KIND_MASK: u8 = 7 << 2 ; const KIND_OFFSET: u8 = 2 ;
接下来,您将声明一个字段类型,并执行将给定值转换为其位置相关值以在寄存器内使用的操作:
struct Field { value : u8 , } impl Field { fn new ( mask : u8 , offset : u8 , val : u8 ) -> Self { Field { value : ( val << offset ) & mask , } } }
最后,您将使用一个寄存器类型,该类型将一个与您的寄存器宽度匹配的数字类型包裹起来。 Register具有更新功能,可使用给定字段更新寄存器:
struct Register ( u8 ) ; impl Register { fn update ( & mut self , val : Field ) { self .0 = self .0 | field.value ; } } fn enable_register ( & mut reg ) { reg.update ( Field :: new ( ENABLED_MASK , ENABLED_OFFSET , 1 ) ) ; }
使用Rust,您可以使用数据结构来表示字段,将它们附加到特定的寄存器,并在与硬件交互时提供简洁明了的人机工程学。 这个例子使用了Rust提供的最基本的功能。 无论如何,添加的结构都可以减轻上述C示例中的某些密度。 现在,字段是已命名的事物,而不是从模糊的按位运算符派生的数字,而寄存器是具有状态的类型-硬件上的额外抽象层。
Rust中的第一个重写很好,但是并不理想。 您必须记住要带上遮罩和偏移量,并且要手工进行临时计算,这容易出错。 人类不擅长精确且重复的任务-我们往往会感到疲劳或失去专注力,这会导致错误。 每次手动记录一次掩膜和偏移量几乎肯定会很糟糕。 这是最好留给机器的任务。
其次,从结构上进行思考:如果有一种方法可以让字段的类型携带掩码和偏移量信息,该怎么办? 如果您在实现过程中遇到错误,而不是在运行时发现硬件寄存器并与之交互,该怎么办? 也许您可以依靠一种通常用于在编译时解决问题的策略,例如类型。
您可以使用修改前面的示例,该库在类型级别提供数字和算术。 在这里,您将使用其掩码和偏移量对Field类型进行参数化,使其可用于Field的任何实例,而不必在调用站点中将其包括在内:
# [ macro_use ] extern crate typenum ; use core :: marker :: PhantomData ; use typenum ::*; // Now we'll add Mask and Offset to Field's type struct Field < Mask : Unsigned , Offset : Unsigned > { value : u8 , _mask : PhantomData < Mask >, _offset : PhantomData < Offset >, } // We can use type aliases to give meaningful names to // our fields (and not have to remember their offsets and masks). type RegEnabled = Field < U1 , U0 >; type RegInterrupt = Field < U2 , U1 >; type RegKind = Field < op ! ( U7 << U2 ) , U2 >;
现在,当重新访问Field的构造函数时,您可以忽略mask和offset参数,因为类型包含该信息:
impl < Mask : Unsigned , Offset : Unsigned > Field < Mask , Offset > { fn new ( val : u8 ) -> Self { Field { value : ( val << Offset :: U8 ) & Mask :: U8 , _mask : PhantomData , _offset : PhantomData , } } } // And to enable our register... fn enable_register ( & mut reg ) { reg.update ( RegEnabled :: new ( 1 ) ) ; }
看起来不错,但是……如果您对给定的值是否适合某个字段犯了错误,该怎么办? 考虑一个简单的错字,您输入10而不是1 :
fn enable_register ( & mut reg ) { reg.update ( RegEnabled :: new ( 10 ) ) ; }
在上面的代码中,预期结果是什么? 好的,代码会将启用位设置为0,因为10&1 = 0 。 那真不幸; 最好在尝试写入之前知道您要写入字段的值是否适合该字段。 事实上,我会考虑放弃错误字段值的高位未定义行为 (喘气)。
如何以一般方式检查字段的值是否适合其规定的位置? 更多类型级别的数字!
您可以将Width参数添加到Field并使用它来验证给定的值可以适合该字段:
struct Field < Width : Unsigned , Mask : Unsigned , Offset : Unsigned > { value : u8 , _mask : PhantomData < Mask >, _offset : PhantomData < Offset >, _width : PhantomData < Width >, } type RegEnabled = Field < U1 , U1 , U0 >; type RegInterrupt = Field < U1 , U2 , U1 >; type RegKind = Field < U3 , op ! ( U7 << U2 ) , U2 >; impl < Width : Unsigned , Mask : Unsigned , Offset : Unsigned > Field < Width , Mask , Offset > { fn new ( val : u8 ) -> Option < Self > { if val <= ( 1 << Width :: U8 ) - 1 { Some ( Field { value : ( val << Offset :: U8 ) & Mask :: U8 , _mask : PhantomData , _offset : PhantomData , _width : PhantomData , } ) } else { None } } }
现在,仅当给定值适合时才可以构造一个字段 ! 否则,您将显示None ,这表示发生了错误,而不是放弃该值的高位并静默写入一个意外值。
但是请注意,这将在运行时引发错误。 但是,我们知道我们想事先编写的价值,还记得吗? 因此,我们可以教编译器完全拒绝具有无效字段值的程序-我们不必等到运行它!
这次,您将向新的新实现(称为new_checked )添加特征绑定 ( where子句),该实现要求输入值小于或等于具有给定Width的字段可以容纳的最大可能值:
struct Field < Width : Unsigned , Mask : Unsigned , Offset : Unsigned > { value : u8 , _mask : PhantomData < Mask >, _offset : PhantomData < Offset >, _width : PhantomData < Width >, } type RegEnabled = Field < U1 , U1 , U0 >; type RegInterrupt = Field < U1 , U2 , U1 >; type RegKind = Field < U3 , op ! ( U7 << U2 ) , U2 >; impl < Width : Unsigned , Mask : Unsigned , Offset : Unsigned > Field < Width , Mask , Offset > { const fn new_checked < V : Unsigned > ( ) -> Self where V : IsLessOrEqual < op ! ( ( U1 << Width ) - U1 ) , Output = True >, { Field { value : ( V :: U8 << Offset :: U8 ) & Mask :: U8 , _mask : PhantomData , _offset : PhantomData , _width : PhantomData , } } }
只有拥有此属性的数字才具有此特征的实现,因此,如果使用不适合的数字,它将无法编译。 看一看!
fn enable_register ( & mut reg ) { reg.update ( RegEnabled :: new_checked ::< U10 > ( ) ) ; } 12 | reg.update ( RegEnabled :: new_checked ::< U10 > ( ) ) ; | ^^^^^^^^^^^^^^^^ expected struct `typenum :: B0 ` , found struct `typenum :: B1 ` | = note : expected type `typenum :: B0 ` found type `typenum :: B1 `
new_checked将无法生成一个程序,该程序的某个字段的值过高。 您的错字不会在运行时爆炸,因为您永远无法获得工件来运行。
就内存映射硬件交互的安全性而言,您已经接近Peak Rust。 但是,您在C的第一个示例中所写的内容比最终得到的类型参数沙拉更简洁。 当您谈论潜在的数百甚至数千个寄存器时,这样做是否容易处理?
早些时候,我认为手工计算蒙版有问题,但我只是做了同样有问题的事情-尽管在类型级别。 虽然使用这种方法很不错,但是要达到编写任何代码的地步,需要大量样板和手动转录(我在这里谈论类型同义词)。
我们的团队希望使用类的东西,但是要使用尽可能少的手动转录生成类型安全的实现。 我们得出的结果是一个宏,该宏生成必要的样板以获得类似Tock的API以及基于类型的边界检查。 要使用它,请写下一些有关寄存器的信息,其字段,其宽度和偏移量以及可选的的值(您应该为字段可能具有的值赋予“含义”):
register ! { // The register's name Status , // The type which represents the whole register. u8 , // The register's mode, ReadOnly, ReadWrite, or WriteOnly. RW , // And the fields in this register. Fields [ On WIDTH ( U1 ) OFFSET ( U0 ) , Dead WIDTH ( U1 ) OFFSET ( U1 ) , Color WIDTH ( U3 ) OFFSET ( U2 ) [ Red = U1 , Blue = U2 , Green = U3 , Yellow = U4 ] ] }
由此,您可以生成寄存器和字段类型,如上一个示例,其中的索引Width , Mask和Offset从字段定义的WIDTH和OFFSET部分中输入的值派生。 另外,请注意,所有这些数字都是typenums ; 他们将直接进入您的字段定义!
生成的代码通过为寄存器及其字段指定的名称为寄存器及其相关字段提供名称空间。 那是一口 看起来是这样的:
mod Status { struct Register ( u8 ) ; mod On { struct Field ; // There is of course more to this definition } mod Dead { struct Field ; } mod Color { struct Field ; pub const Red : Field = Field ::< U1 > new ( ) ; // &c. } }
生成的API包含名义上期望的读取和写入基元,以获取原始寄存器的值,但它也具有获取单个字段的值,执行集体操作以及确定是否有任何(或全部)位集合的方法。组。 您可以阅读有关的文档。
在实际设备上使用这些定义是什么样子? 代码中是否会充斥着类型参数,从而掩盖了视图中的任何真实逻辑?
没有! 通过使用类型同义词和类型推断,您实际上根本不必考虑程序的类型级别部分。 您可以直接与硬件交互,并自动获得与范围相关的保证。
这是一个寄存器块的示例。 我将跳过寄存器本身的声明,因为在此不包括在内。 而是从寄存器“块”开始,然后帮助编译器知道如何从指向该块开头的指针中查找寄存器。 我们通过实现Deref和DerefMut来做到这一点 :
# [ repr ( C ) ] pub struct UartBlock { rx : UartRX :: Register , _padding1 : [ u32 ; 15 ] , tx : UartTX :: Register , _padding2 : [ u32 ; 15 ] , control1 : UartControl1 :: Register , } pub struct Regs { addr : usize , } impl Deref for Regs { type Target = UartBlock ; fn deref ( & self ) -> & UartBlock { unsafe { &* ( self .addr as * const UartBlock ) } } } impl DerefMut for Regs { fn deref_mut ( & mut self ) -> & mut UartBlock { unsafe { & mut * ( self .addr as * mut UartBlock ) } } }
一旦到位,使用这些寄存器就像read()和Modify()一样简单:
fn main ( ) { // A pretend register block. let mut x = [ 0 _u32 ; 33 ] ; let mut regs = Regs { // Some shenanigans to get at `x` as though it were a // pointer. Normally you'd be given some address like // `0xDEADBEEF` over which you'd instantiate a `Regs`. addr : & mut x as * mut [ u32 ; 33 ] as usize , } ; assert_eq ! ( regs.rx.read ( ) , 0 ) ; regs.control1 .modify ( UartControl1 :: Enable :: Set + UartControl1 :: RecvReadyInterrupt :: Set ) ; // The first bit and the 10th bit should be set. assert_eq ! ( regs.control1.read ( ) , 0b_10_0000_0001 ) ; }
当我们使用运行时值时,我们使用如前所述的Option 。 在这里我使用的展开 ,但与未知输入一个真正的程序,你可能要检查你接到了新的调用一些回去: ,
fn main ( ) { // A pretend register block. let mut x = [ 0 _u32 ; 33 ] ; let mut regs = Regs { // Some shenanigans to get at `x` as though it were a // pointer. Normally you'd be given some address like // `0xDEADBEEF` over which you'd instantiate a `Regs`. addr : & mut x as * mut [ u32 ; 33 ] as usize , } ; let input = regs.rx.get_field ( UartRX :: Data :: Field :: Read ) .unwrap ( ) ; regs.tx.modify ( UartTX :: Data :: Field :: new ( input ) .unwrap ( ) ) ; }
根据您的个人痛苦阈值,您可能已经注意到错误几乎是无法理解的。 看一下我在说什么的微妙提醒:
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 ` --> src / main.rs : 12 : 5 | 12 | less_than_ten ::< U20 > ( ) ; | ^^^^^^^^^^^^^^^^^^^^ expected struct `typenum :: B0 ` , found struct `typenum :: B1 ` | = note : expected type `typenum :: B0 ` found type `typenum :: B1 `
预期的typenum :: B0发现typenum :: B1部分有意义,但typenum :: UInt <typenum :: UInt,typenum :: UInt到底是什么呢? 好吧, typenum将数字表示为二进制单元! 像这样的错误使操作变得很困难,尤其是当您将多个这些类型级别的数字限制在狭窄的范围内时,您很难知道它在说哪个数字。 当然,除非您将巴洛克式二进制表示形式转换为十进制表示形式是第二天性。
在U100试图从这个混乱中解开任何含义之后,一个队友得到了《疯了,地狱了,不再要接受它》了,并做了一个小工具tnfilt ,从命名空间二元缺点的痛苦中解析出了含义。细胞。 tnfilt采用con单元格样式表示法,并将其替换为明智的十进制数字。 我们认为其他人也会遇到类似的困难,因此我们共享了 。 您可以像这样使用它:
$ cargo build 2>&1 | tnfilt
它将上面的输出转换为如下所示:
error[E0271]: type mismatch resolving `>::Output == typenum::B1`
现在这很有意义!
我们的团队从安全性较安全的一面开始,然后尝试找出如何将易用滑块移近易用端。 从这些雄心壮志中, 有界寄存器就诞生了,在Auxon冒险中遇到内存映射设备的任何时候,我们都会使用它。
从技术上讲,从定义上看,从寄存器字段读取的值只能在规定的范围内,但是我们当中没有一个人生活在一个纯净的世界中,而且您永远都不知道外部系统起作用时会发生什么。 您是在这里接受“硬件之神”的命令,因此与其强迫您进入“可能的恐慌”状态,还不如让您选择处理“这将永远不会发生”的情况。
get_field看起来有点奇怪 。 我正在专门研究Field :: Read部分。 字段是一种类型,您需要该类型的实例才能传递给get_field 。 较干净的API可能类似于:
regs.rx.get_field ::< UartRx :: Data :: Field > ( ) ;
但是请记住, Field是类型的同义词,它具有固定的宽度,偏移量等索引。为了能够像这样对get_field进行参数化 ,您需要使用类型较高的类型。
它最初出现在 ,并经许可进行编辑和重新发布。
翻译自:
rust编程
转载地址:http://tmizd.baihongyu.com/