我的MMIO抽象历险记

2020-09-12 15:48:51

几年前,我在Reddit上看到一个简单的流氓游戏,叫做coreRR。它非常简单;关卡只是一个有两面墙的盒子,只有一个敌人类型,有基本的人工智能,没有健康或性格属性,唯一的目标是看看你在死之前能走多远。由于没有更好的事情可做,我想为Arduino Nano编写一个端口会是一个有趣的小项目。所需的唯一输入是四个移动键,显示器可以只是一个基本的SSD1306驱动的128x64 OLED面板。

当然,我可以用C++来做这件事。语言是为Arduino Nano提供动力的ATmega328P的已知数量,工具链已经成熟,用于与板载外围设备交互的抽象也是成熟的。还有用于SSD1306驱动显示器的库。这是显而易见的选择。我真正要做的就是写游戏逻辑。

要真正做任何有用的事情,微控制器需要与外部世界交互。ATmega328P有四个我将在这个项目中使用的外设:IO端口、定时器、双线接口(TWI)总线和用于将调试信息发送回我的PC的USART。

它们由操作内存映射IO(MMIO)寄存器使用。通常,当你读或写记忆地址时,你是在访问某种,嗯……。记忆。内存映射IO顾名思义就是:它是映射到内存地址的一块硬件,因此您可以访问连接的硬件,而不是简单地访问内存中的值。具体是如何实现的取决于所讨论的硬件。例如,写入IO端口只需设置几个触发器。但其他设备将更加复杂。

为了说明如何使用这些,我将从微控制器的问候世界开始:打开LED。ArduinoNano有一个LED连接到PB5针脚。名称告诉我们操作哪组寄存器以及需要操作的位:端口B,位5。328P上的IO端口非常简单:使用DDRn寄存器设置数据方向(将一位设置为0表示输入,将位设置为1表示输出),而输出电平由PORTn寄存器控制(将一位设置为0表示低电平,将位设置为1表示高电平)。

我们需要端口B,因此这将是DDRB和PORTB寄存器。其地址为:

然而,这一部分让拉斯特感到有点尴尬。生锈是非常..。这里的问题是,对拉斯特来说,这些是我凭空变出来的任意内存地址。它对它们一无所知,当然也不知道它们是IO外围设备。这意味着我需要将它们作为原始指针访问。操纵原始指针需要不安全。此外,为了防止过度热心的优化器删除我们(显然)未使用的内存访问,您需要使用易失性读写。在C++中,这很简单:您只需将整个指针类型标记为易失性,并且每个访问都被正确处理。铁锈是不同的。在Rust中,访问是否是易失性的取决于访问的站点,而不是指针的类型。

因此,考虑到这一点,下面是我如何打开LED的方法。我首先定义一些常量:

常量PORTB:*MUT U8=0x25 AS*MUT_;常量DDRB:*MUT U8=0x24 AS*MUT_;常量PB5:U8=5;

这显然会很快变得非常混乱。所有这些围绕内存访问的噪音将最终导致与寄存器交互的代码难以读取。难以阅读的代码是很难理解的、有缺陷的代码。如果您只想设置一个位而不更改其他任何位,情况会变得更糟:

我想要做的是隐藏位的闲置细节,只留给我更高级别的概念。为此,我考虑了这些寄存器将具有哪些通用功能:

有几种方法可以做到这一点,但我最终确定的是这一特点:

PUB特征寄存器{const ADDR:*MUT U8;不安全的FN SET_VALUE(VAL:U8){SELF::Addr.。Write_Volatile(Val);}不安全的FN GET_VALUE()->;U8{Self::Addr.。Read_volatile()}不安全的FN get_bit(bit:u8)->;bool{let bit=1<;<;bit;(self::get_value()&;bit)!=0}不安全的fn set_bit(bit:u8){let bit=1<;<;bit;let val=self::get_value();self::set_value(val|bit);}不安全的FN CLEAR_BIT(BIT:U8){let bit=1<;<;bit;let val=self::get_value();self::set_value(val&;!bit);}}。

每个寄存器的函数实现都是相同的,所以我只有一个默认实现。这些功能应该是不安全的,因为这里没有办法证明我们正在做的事情实际上是正确的。这将取决于打电话的人。有了这个抽象,我就可以将我的寄存器定义更改为:

Struct PORTB;用于PORTB的IMPL寄存器{const ADDR:*MUT U8=0x25 AS*MUT_;}struct DDRB;IMPL寄存器用于DDRB{const ADDR:*MUT U8=0x24 AS*MUT_;}。

目前的实现有两个问题,虽然不是很严重,但确实困扰着我。首先,没有任何寄存器是8位的;有些寄存器是16位的。现在,我只需分别定义寄存器的高字节和低字节,这就是AVR C标头所做的工作,但我更希望能够在一次操作中对其进行寻址。

第二,位操作的所有输入都是纯U8。这意味着我可以执行PORTB::SET_BIT(30),并让编译器接受不正确的输入。现在也不清楚我是应该传入一个位ID值,还是应该传递一个预移位值。还有一个额外的问题:并不是寄存器中的所有位都有意义。例如,TWI控制寄存器(TWCR)位1没有任何功能。不过,我可以不出问题地将1传递给set_bit函数。这可能是文档的一部分,但如果我一开始就不能做错,不是更好吗?

第一个比较容易处理,所以我会先做这件事。我需要寄存器在其存储类型上是通用的。这相当简单:只需引入一个泛型类型T,并将U8的所有实例替换为T。如果这样做,您将会遇到一系列编译错误,如下所示:

错误[E0277]:没有实现`{INTEGER}<;<;T`-->;src\register.rs:16:21|16|let bit=1<;<;bit;|^^没有实现`{INTEGER}<;<;T`|=Help:特性`core::ops::shl<;T>;`没有实现`{INTEGER}`。

我需要约束T。编译器错误告诉我们需要什么特性:Shl、BitAnd、BitOr、Not和Eq。我还应该将其限制为寄存器可以是的类型:u8和u16。它也应该是复制的,首先是因为我们正在隐式复制值,但也是因为我正在进行指针读写,这与丢弃实现和与之相关的健壮性不能很好地配合,所以它应该强制不存在复杂的丢弃行为。Copy也可以做到这一点,因为不能对实现Drop的类型实现Copy。

除此之外,我还使用了两个常量:0和1。编译器在这些函数实现中不知道0和1的文字是t。我需要关联的常量。

为了满足这些要求,我引入了另一个特征RegisterType,它需要上面列出的类型,并在u8和u16中实现它:

Pub特征寄存器类型:Copy+BitAnd<;Output=Self&>;+BitOr<;Output=Self&>;+Not<;Output=Self&>;+Eq+PartialEq{Const Zero:Self;Const One:Self;}U8的Impl寄存器类型{Const Zero:Self=0;Const One:Self=1;}Impll RegisterType for U8{Const Zero:Self=0;Const One:Self=1;}Impll RegisterType for U8{Const Zero:Self=0;Const One:Self=1;}Impll RegisterType。

现在来解决另一个问题。可以和不能使用的位是特定于寄存器的,也有名称(例如,TWCR寄存器的位5称为TWSTA)。因此,我们所拥有的是一组固定的特定的、命名的值。枚举非常适合于此。我们可以这样表示TWCR的有效位:

然后,我想确保位翻转函数只能接受表示该寄存器位的枚举。因为我需要在Register特征上有一个关联的类型,所以我可以在函数签名中对其进行命名:

Pub特征寄存器<;T:RegisterType>;{const addr:*mut T;type BitType;...。不安全的FN GET_BIT(BIT:Self::BitType)->;bool{...}不安全的FN SET_BIT(BIT:Self::BitType){...}不安全的FN CLEAR_BIT(BIT:Self::BitType){...}}。

因为这里的所有逻辑都是基于移位的,所以我需要能够从BitType中获得一个T,告诉我们变量代表的是哪一位。所以BitType需要实现一个返回该值的函数。此函数还需要返回u8或u16,具体取决于寄存器。输入NamedBits特征:

然后,我在Register中更改关联的类型,以便将位类型适当地限制为仅实现NamedBits的位类型,并更新位缠绕函数以在其输入上调用bit_id函数:

Pub特征寄存器<;T:RegisterType>;{const addr:*mut T;type BitType:NamedBits<;dataType=T>;;...。不安全的fn get_bit(bit:self::BitType)->;bool{let bit=T::one<;<;bit。Bit_id();(self::get_value()&;bit)!=T::Zero}不安全的FN set_bit(bit:self::BitType){let bit=T::one<;<;bit。Bit_id();let val=self::get_value();self::set_value(val|bit);}不安全的FN Clear_bit(bit:self::BitType){let bit=T::one<;<;bit。Bit_id();let val=self::get_value();self::set_value(val&;!bit);}}。

#[派生(复制,克隆)]枚举PortBits{PB0,PB1,PB2,PB3,PB4,PB5,PB6,PB7,}为PortBits实施命名位{type dataType=U8;fn bit_id(Self)->;self::dataType{use PortBits::*;Match Self{PB0=>;0,PB1=>;1,PB2=>;6,Pb7=>;7,}针对PORTB的struct PORTB;实施寄存器U8&>{const ADDR:*mut U8=0x25 as*mut_;type BitType=PortBits;}struct DDRB;Iml Register<;U8&>;for DDRB{const ADDR:*mut U8=0x24 as*mut_;type BitType=PortBits;}。

无论输入是不是预移位值,都不再有任何模棱两可的地方,我也不能像以前那样随意输入数字。

还有最后一个问题:有些寄存器只是数据存储。这方面的一个例子是USART的UDR0寄存器,它存储通过总线发送或接收的字节。在这种情况下,寄存器是一个字节的数据,而不是控制位的集合,所以能够设置特定的位没有意义。然而,抽象在这里需要表示位的类型。

我的解决方案是创建一个名为NoBits的结构,它有一个私有字段,这样它就不能在其父模块之外构造:

#[派生(复制,克隆)]pub struct NoBits<;T>;(PhantomData<;T>;);Iml<;T:RegisterType>;NamedBits for NoBits<;T>;{type dataType=T;fn bit_id(Self)->;self::dataType{T::Zero}}。

我之所以选择结构而不是没有变体的枚举,是因为它需要对8位和16位寄存器都可用,这意味着它确实需要是泛型的,而不使用泛型类型参数是编译错误。这意味着我现在可以定义UDR0寄存器,并且仍然使用GET_VALUE和SET_VALUE函数,但不使用与位相关的函数:

用于UDR0的struct UDR0;Iml寄存器<;U8>;{const ADDR:*mut U8=0xC6 as*mut_;type BitType=NoBits<;U8>;;}。

在这一点上,那些患有锈病的朋友可能会想,为什么不直接使用Corelib的FROM或INTO特征来将位枚举转换为T呢?起初我确实尝试过这些方法,但很快就发现,由于某些原因,它们并不能很好地进行优化。即使所有的输入在编译时都是已知的,它最终也不会内联into调用,因此您将在最终的二进制文件中得到一个不必要的函数调用。定义我自己的特定转换特性导致了内联的位置,这意味着整个事情都得到了优化。

好的,我可以用一种容易阅读、更难出错的方式来润色一下。但我也希望在一次操作中设置(或清除)多个位。我可以通过连续调用set_bit(或clear_bit)来做到这一点,但是易失性访问开始成为一个问题。到目前为止,根据我所拥有的,将PORTB的PB5位设置为以下优化:

因为访问是易失性的,所以编译器不知道它是否可以将所有位收集在一起并一次设置所有位,所以我需要自己来做这件事。和前面一样,我不想暴露手动的位旋转,所以我将实现两个函数(set_bits和clear_bits)来为我做这件事。铁锈没有各种各样的功能,但它确实有切片,所以我会让它们取一片:

酒吧特性寄存器<;T:RegisterType>;{...。不安全的FN SET_BITS(BITS:&;[Self::BitType]){//通过将移位的位一起或来构造最终的位模式。设BITS=BITS。ITER()。已复制()。Map(NamedBits::bit_id)。Fold(T::Zero,|acc,b|acc|(T::one<;<;b));let val=self::get_value();self::set_value(val|bits);}不安全的FN CLEAR_BITS(BITS:&;[Self::BitType]){let Bits=BITS。ITER()。已复制()。Map(NamedBits::bit_id)。Fold(T::零,|acc,b|acc|(T::one<;<;b));let val=self::get_value();self::set_value(val&;!bits);}}

并将其优化为单个操作(0xEC是将以上位进行或运算后得到的结果):

到目前为止,我所得到的在一次操作中就可以很好地进行设置或清除。但有一件事经常出现,那就是当您想要替换某些位的值,但保持其他位的值不变时。这可以通过连续调用CLEAR_BITS,然后调用SET_BITS来完成,但这会遇到我们之前遇到的易失性寄存器问题,因为实际清除寄存器中的位可能会对更复杂的外设造成意外情况。

LET BITS_TO_REPLACE=(1<;<;2)|(1<;<;4)|(1<;<;7);LET_REPLACE_VAL=(1<;<;2)|(1<;<;7);LET_VAL=REG::GET_VALE();LET MASTED=REG_VAL&;!BITS_TO_REPLACE;LET NEW_REG=。Reg::set_value(Reg_Val);

但是有一个为我做这件事的函数会让这件事变得更容易,并减少出错的机会。这里的逻辑是SET_BITS和CLEAR_BITS函数的组合,所以就让我们这样做吧。它将需要两组位:一组表示要替换的位,另一组表示用来替换它们的值。

不安全的FN REPLACE_BITS(掩码:&;[Self::BitType],值:&;[Self::BitType]){let MASK=MASK。ITER()。已复制()。Map(NamedBits::bit_id)。Fold(T::Zero,|acc,b|acc|(T::one<;<;b));let value=value。ITER()。已复制()。Map(NamedBits::bit_id)。Fold(T::Zero,|acc,b|acc|(T::one<;<;b));let masked_value=value&;ask;let masked_reg=self::get_value()&;ask;self::set_value(mask_reg|masked_value);}。

这里我们应该注意的一件事是确保传入的值也被屏蔽(但不是用反转的屏蔽!),这样它就不会损坏屏蔽区域之外的位。所以现在我可以这样做:

到目前为止,这看起来还可以用于比特闲置。需要获取多位操作的切片并不重要,还有一个事实是,我还需要显式地导入位枚举,并知道它的名称。这两件事都有点让人恼火,但不是什么大问题。然而,这些问题是可以解决的。第一个问题,我们稍后再来讨论,但第二个问题很容易处理。

解决方案非常简单:关联常量。我们只需在寄存器上声明一组关联常量,它们指向枚举变量:

Pub struct TWCR;Iml TWCR{pub const twie::twie;pub const TWEN:TWCRBits=TWCRBits::TWen;pub const TWWC:TWCRBits=TWCRBits::TWWC;pub const TWSTO:TWCRBits=TWCRBits::TWSTO;pub const TWSTA。

现在,当我想对TWCR寄存器进行位旋转时,只需通过寄存器本身获取位名:

当涉及到设置寄存器值时,还有另一个更大的问题。目前,我只能用原始整数设置寄存器值。但是,想要根据一组位来设置值是完全合理的。例如,在配置TWI总线时。有几个点需要替换整个值;例如,在仲裁失败后释放总线时,当前如下所示:

当我们回到那个问题上时,做所有这些抽象有什么意义呢?我可以让set_value函数接受一些位,比如set_bits等函数,但是仍然有需要设置整数值的情况。我选择的方法是另一个特点:

然后,我为所有寄存器类型和寄存器位片段实现了它,并更新了set_value函数:

实施<;T:RegisterType>;SetValueType<;T>;for T{fn as_value(Self)->;T{self}}实施<;T:NamedBits>;SetValueType<;T::DataType>;for&;[T]{FN AS_Value(Self)->;T::DataType{sel.。ITER()。已复制()。Map(NamedBits::bit_id)。Fold(T::DataType::Zero,|acc,b|acc|(T::DataType::one<;<;b))}}Pub特征寄存器<;T:RegisterType>;{...。不安全的FN SET_VALUE<;V:SetValueType<;T>;>;(val:v){设val=val。As_value();self::addr。Write_Volatile(Val);}...}。

这允许我以两种方式设置值(尽管在这里传入切片需要as_ref,这不是很好):

我不喜欢所有这些切片。他们看起来既怪异又笨拙。在操作TWI总线时,IF语句的两个分支都替换了TWCR寄存器,但差别只有一位。例如,在处理数据包时,您需要配置寄存器,但可能不想发送ACK信号。目前您需要这样做:

如果我可以构建该值,选择性地设置TWEA位,然后设置寄存器,那就太好了。事实上,如果我能做这样的事情,同时还能保留一定程度的白痴保护,那就太好了:

你可能会注意到,我想要做的事情和我开始时相当难看的烂摊子很相似:

所以我需要的是用某种方式来做同样的事情,但是要保留关于它们来自哪个寄存器的信息。我的解决方案是BitBuilder。它应该保留有关它用于哪种位类型的信息,因此它需要是泛型的。因为位类型现在是整体类型的一部分,所以我们可以使内部字段成为寄存器的数据类型。我们将使该字段成为私有字段,这样它就不能被任意值替换。

#[派生(复制,克隆)]pub struct BitBuilder<;T:NamedBits>;(T::DataType);Iml<;T:NamedBits>;BitBuilder<;T>;{pub FN new()->;self{BitBuilder(T::DataType::Zero)}fn set_bit(&;mut self,b:t)。0=自我。0|(T::dataType::one<;<;B.bit_id());}}。

现在,要获得我想要的ORing行为,我可以简单地实现BitOr和BitOrAssign特征,用于当右侧都与BitBuilder的位类型有点相同时,以及当它是同一位类型上的另一个BitBuilder时:

对于BitBuilder<;T>;实施<;T:NamedBits>;BitOr<;Self>;{type output=BitBuilder<;T>;;FN BITOR(mut Self,RHS:Self)->;Self::Output{Self.。0=自我。0|RHS。0;Self}}实施<;T:NamedBits>;Bito。

.