如上所述,如下所示,第2部分:裸露金属锈蚀泛型

2020-08-31 23:52:54

在上一集中,我们游览了令人愉快的乏味的闪存世界,起草了一些驱动程序接口,并玩弄了机箱可见性的两个级别的泛型的想法。明显缺少的是,嗯,有效的代码。建立数据结构是一回事,完全在裸机固件的原始1和0之间编织泛型是另一回事。今天,我们将凝视空虚,希望空虚不会再凝视我们。

在关于闪存的一节中,我们借助一个不靠谱的类比,探讨了如何用0直接覆盖1,而不是反之亦然。正是这种怪癖使得高效的Flash编写器变得有点棘手。当被要求重写内存范围时,一个天真的驱动程序会简单地擦除与其重叠的每个扇区,然后写回合并了所需字节的原始数据。我们希望我们的驱动程序比这更智能一些;它应该首先检查是否可以直接写入字节。换句话说,我们希望确保目标写入中的每个1也是目标空间中的1。

Pub CharacterBitSubset{/检查右手边的每个';1';位是否都是';1';位。Fn is_subset_of(&;self,rhs:&;self)->;bool;}。

这看起来是一个直接、简单的特征来表达我们想要检查的条件。这就是不必要的部分:我们将一般性地实施它。让我们坦诚地说,我们都知道我们希望在[U8]上实现这一点。除了字节片之外,它应该没有太多用处。然而,这样做将有助于伸展我们的普通肌肉,并说明几个重要的点。

实施<;T:COPY+EQ+BitOr<;Output=T>;>;BitSubset for[T]{fn is_subset_of(&;self,rhs:&;self)->;bool{if sel.。LEN()>;RHS。LEN(){FALSE}ELSE{SELF.。ITER()。Zip(RHS。ITER())。All(|(a,b)|(*a|*b)==*b)}}。

是的,我也感觉到了恐惧。上面的神印足够召唤半个所罗门的小钥匙。我们有五种类型的括号(<;(|[{)],我们才刚刚开始。幸运的是,符号很像蚂蚁入侵--忍受它们足够久,你就不会注意到它们了。)(=。他们甚至可以很讨人喜欢。

几年前,当我开始使用Rust泛型时,它经常帮助我按部分处理函数;首先将重点放在约束上,只有当我清楚地描述了所涉及的元素时,才会转到实现上。我发现,破译约束有点像例行公事,就像臭名昭著的声明螺旋规则一样。对我来说,它的形式是用简单的英语伪码来描述它们。对于上面的内容,它看起来如下所示:

对于更复杂的列表,我会在脑海中反复演唱这些条件,直到抽象类型融合成更容易管理的东西,然后才有可能把符号汤推开,专注于行为。

在这些约束中总结了这些类型参数的所有行为。没有什么是含蓄的。那真是太酷了。

在这种情况下,剩下就是一些迭代器魔术和位争执。Self.iter().zip(rhs.iter()).all(Dition)";zips";两个迭代器成对组合在一起,然后验证每对迭代器是否满足一个条件。在我们的例子中,条件是闭包|(a,b)|(*a|*b)==*b。要使条件成立,元素*a中的所有1位也必须是1 in*b。

正如我们预期的那样,我们稍后将使用它来比较字节片段。我们从泛泛地做这件事中得到了什么?

尽管T:copy+eq+BitOr<;output=T>;肯定比U8简单得多,但从某种意义上说,它也更纯粹,因为它更准确地概括了我们关心的东西。奇怪的是,较短的U8背负着更多无关紧要、令人分心的包袱:

相比之下,我们的柏拉图式的T令人耳目一新地简单:只做实现需要的东西。因此,现在和以后的实施将是正确的,这将导致下一点:

当我们的约束与实现的要求如此紧密地匹配时,我们可以求助于一揽子实现,因为我们知道,如果未来的用户定义了与这些要求匹配的类型,他们将自动选择此行为。像令人印象深刻的优雅的bevy这样的箱子更进一步,利用它们的前奏,在不让用户接触到特性的情况下,将毯子实现带到了广泛的类型上。我认为,这导致了非常符合人体工程学的API。

这并不是说我们有任何理由怀疑不同的情况,但具体版本和通用版本会产生相同的程序集。

我们已经增加了警徽的数量,越来越接近召唤地狱侯爵基马里斯,20个恶魔军团的统治者,语法,逻辑和修辞的老师。也许他最终会给单子下一个直观的定义。

在最后一个条目的末尾,我们勾勒出了我们对地址和区域的最小概念的限制:

Pub特征地址:Ord+Copy+Add<;usize,output=self&>;{}pub特征区域<;A:address>;{fn包含(&;self,address:A)->;bool;}。

但库埃沃,你会问,这不是上周看起来的样子。您添加了添加约束。好了,假想的细心读者,你抓到我了。我可能忘了复制它,因为我正在策划代码示例。是的,core::ops::add binding是必要的,我们稍后会看到。这给了我们一个唱泛型歌曲的借口,这首歌对超级定义同样有效:

地址是可以排序和复制的东西。它可以与偏移量相加,从而产生另一个地址。

我们能用这个做什么呢?我们知道我们的闪存驱动器的目标之一是巧妙地协商不同的写入和擦除粒度,以便对于任何给定的缓冲区写入,只擦除所需的区域。为了做到这一点,我们可以这样准确地概括一个要求:

对于给定长度的缓冲区和目标内存映射中的基址,我们需要将每个缓冲区段与其重叠的每个区域相关联。

0 5 10 10 15 20 25 30 35。

给定上述两个输入,我们需要在缓冲区中生成以下四个视图:

0 5 10 15 20 25 30 35-|ooooo[ooooo][ooooo]ooooo|-视图1视图2视图3视图4。

我们在这一节的目标是一般性地定义这样的操作,并一般性地实现它,这样它就可以与上周的部门、子部门和来自两个驱动程序的页面一起工作。

现在,如果你对符号过敏,请注意:龙在这里。从此以后龙龟,一路走下坡路。我们即将到达古埃及人在秋叶原购物的标志过载水平。相信我,过一段时间,他们中的任何一个都不会感到多余或过度紧张。与人类语言一样,精确的定义会带来更好的理解。

/生成块-区域对的迭代器,/其中每个内存块映射到每个区域发布结构覆盖迭代程序';a,A,R,I&>;其中A:地址,R:区域;A&>,I:迭代器&Item=R&>;,{Memory:&;&39;A[U8],Regions:I,Base_Address:A,}pub特征迭代其中A:地址,R:地区<;A>;,I:Iterator<;Item=R>;,{fn重叠(自身,块:&;&39;a[U8],base_address:A)->;重叠迭代器<;A,R,I>;}。

嗯。到目前为止还不算太差。我们有必要的工具来分解WHERE语句,这对于结构和特征都是相同的。我们正在处理的是:

显式生存期是所有包含引用的结构的必要条件,它简单地表示内存字段是对存储在其他地方的数据的引用。

我们上面定义的是迭代器结构,以及可以以某种方式迭代的类型的特征。如果你来自C语言,迭代器可能有点麻烦。它们感觉很重,但归根结底它们是非常简单的。我喜欢把迭代器结构看作是纸质书的书签;进度跟踪器的状态刚好足以找到您在序列中的位置。

查看上面的迭代器结构,我们可以通过内部区域迭代器跟踪进度,直到耗尽所有区域。我们也有一种生成这种迭代器的方法--IterableByOverlaps特性中的Overlaps函数--所以剩下的就是定义迭代规则了。我将向您展示的实现可以进行一些手动优化,事实上,在最终的Loadstone代码中,它看起来并不完全像这样。为了更好地说明本文中的观点,我选择了可读性稍高的选项。

实施重叠迭代器迭代器&39;a,A,R,I&>;迭代器,其中A:地址,R:区域<;A&>,I:迭代器<;Item=R&>,{type item=(&;&39;A[U8],R,A);FN Next(&;mut self)-&。{而设Some(Region)=self.Regions。Next(){让mut block_range=(0.。自我记忆。LEN())。SKIP_WHILE(|索引|!地区。包含(self.base_address+*index))。Take_While(|index|Region。CONTAINS(self.base_address+*index));如果让Some(Start)=BLOCK_RANGE。Next(){let end=block_range。最后一个()。UNWRAP_OR(START)+1;返回一些((&;self.memory[START..。End],region,self.base_address+start));}}无}}实施<;';a,A,R,I>;IterableByOverlaps<;';a,A,R,I&>;对于I,其中A:Address,R:Region<;A&>,I:迭代器<;Item=R&>,{fn重叠(自身,内存:&;&A[U8],BASE_ADDRESS:A)->;重叠迭代器<;A,R,I>;{重叠迭代器{Memory,Regions:Self,Base_Address}。

实施重叠迭代器的迭代器(&39;a,A,R,I&>;其中A:地址,R:区域;A&>,I:迭代器<;Item=R>;,);其中A:Address,R:Region<;A>;,I:Iterator<;Item=R&>;;其中A:Address,R:Region<;A&>;I:Iterator<;Item=R&>;,

到目前为止,我们的结构只是名义上的迭代器。在这里,我们实际上正在实现它的核心::ITER::迭代器特征。我们已经熟悉了这里的泛型,WHERE子句与我们在迭代器结构和特征定义中看到的子句相同。继续前进!

这是我们将要迭代的关联类型。我们的迭代器将产生三个元素的元组:

具有已经熟悉的生存期的子片。也就是说,我们要编写的内存缓冲区上的视图。

Fn Next(&;mut self)->;选项<;self::item>;{同时让某些(Region)=self.Regions。Next(){//[...]}无}。

以上是包装内部迭代器时的常见模式。这意味着我们将贪婪地使用区域迭代器,最终只有在区域耗尽时才会产生任何结果。

这里是天真的一点,它对我们的探索来说已经足够好了。我们通过跳过区域外的所有地址并取区域内的所有地址来推导出我们的片的缓冲区索引的范围。

如果让SOME(START)=BLOCK_RANGE。Next(){let end=block_range。最后一个()。UNWRAP_OR(START)+1;返回一些((&;self.memory[START..。End],region,self.base_address+start));}。

最后,我们对这些索引进行切片,并对边例进行一些配置。下次调用下一个函数时,它将从它停止的地方继续。

实施&39;a,A,R,I&>;IterableByOverlaps<;&39;a,A,R,I&>;其中,A:Address,R:Region;A&>,I:Iterator<;Item=R&>;,{fn重叠(自身,内存:&;&39;A[U8],Base_Address:A)->;Overlt;,{FN Overlaps(Self,Memory:&;&39;A[U8],Base_Address:A)->;Overlt;,{fn overaps(Self,Memory:&;&39;A[U8],Base_Address:A)。{Overlay Iterator{内存,区域:自身,BASE_ADDRESS}。

这是将其粘合在一起的部分:区域上任何迭代器的全面实现。这使得将您的映射、压缩和链转换为我们所关心的元组迭代器成为可能。所有这些功能只需实现简单得多的区域和地址特性即可使用。

我的同事们对着这些单字母类型的参数名称流下了眼泪,咬牙切齿。在BlueFruit Software我们通常选择的语言中,我们总是喜欢描述性的、透彻的,而不是简明扼要的。然而,我相信保持类型参数简短有一个强有力的论据:它突出了它们灵活、无形的本质,并引起了人们对WHERE子句和特征界限的注意。

编写一个尊重位子集、透明地合并数据并仅在需要时才在C语言中创建扇区的闪存驱动程序是相当繁琐的。它经常涉及多个嵌套循环,一个接一个地容易操作,混乱,痛苦,恐惧和严重的错误。这一次,我们已经制作了足够的通用粘合剂,解决我们两个闪存驱动程序上的写入方法将是一件比较愉快的事情。

回到上周的特质定义,我们希望为我们的两位司机实现这一功能:

我们将从内部MCU闪存开始,您还记得吗,它有一个编码为常量扇区数组的内存映射,其形式如下:

在MCU FLASH的背景下,这是适合我们地区特点的具体类型。我们通过一揽子实施免费获取地址,因此让我们手动实施地区:

实施扇区{fn包含(&;self,address:address)->;bool{(sel.。开始()<;=地址)&;&;(自我。End()>;address)}}实施扇区{const fn start(&;self)->;address{self.location}const fn end(&;self)->;address{address(sel.。START()。0+self.size为u32)}}。

这就是我们要访问之前编写的所有功能强大的迭代器所需做的全部工作。Write方法干净地展开:

Fn写入(&;mut self,address:address,bytes:&;[U8])->;Result<;(),self::error>;{//[..]。此处对MemoryMap::Sectors()中的(块、扇区、地址)进行一些枯燥的边界/范围/对齐检查。重叠(字节,地址){let merge_buffer=&;mut[0 U8;max_sector_size()][0..。Sector.size];让OFFSET_INTO_SECTOR=ADDRESS。0。饱和_SUB(扇区。START()。0)as usize;self.。读取(扇区。Start(),merge_buffer)?;如果是块。Is_subset_of(&;合并缓冲区[OFFSET_INTO_SECTOR.。Sector.size]){自身。WRITE_BYTES(块,&;扇区,地址)?;}其他{自身。擦除(&;扇区)?;MERGE_BUFFER。Iter_mut()。跳过(OFFSET_INTO_SECTOR)。Zip(数据块)。FOR_EACH(|(字节,输入)|*字节=*输入);SELF。WRITE_BYTES(MERGE_BUFFER,&;Sector,sector.location)?;}}OK(())}。

WRITE_BYTES、ERASE和READ是非常低级的芯片特定功能,用于处理实际操作闪存所需的命令和寄存器写入。试图笼统地处理它们不会有太大好处,所以我不会在这里扩展它们。

MAX_SECTOR_SIZE()是在内存映射中的所有扇区上循环的常量FN,而不是可能与内存映射不同步或存在复制粘贴错误风险的常量值。

我们的ReadWrite特征中的其他函数比Write简单得多,在Write中,所有有趣的读写周期逻辑都会发生,因此它们不值得进行太多分析。

现在我们已经组装了写函数,让我们移回外部MicronN25Q128。上周,我们将此驱动程序的内存映射建模为返回存在迭代器类型的函数集合。另一个不同之处是,我们不再关心扇区,我们关心的是子扇区和页面(分别是新的擦除和写入粒度),我们的建模如下:

嗯,别着急!它们看起来可能非常不同,但在内心深处,它们是一个等待发生的地区。让我们这样做吧:

子扇区{fn包含(&;self,address:address)->;bool{let start=address((SUBSECTOR_SIZE*SELF.。0)as u32);(address>;=start)&;&;(address<;start+subsector_size)}}为页面{fn包含(&;self,address:address)->;bool{let start=address((page_size*sel.。0)作为u32);(地址&>;=开始)&;&;(地址<;开始+页面大小)}}。

再一次,这个实现就是解锁我们的通用权力所需的全部。有了这一点,我们终于可以使用Write方法了:

Fn Write(&;mut Self,Address:Address,Bytes:&;[U8])->;Result<;(),Self::Error>;{for(Block,SubSector,Address)in MemoryMap::SubSector()。重叠(字节,地址){让OFFSET_INTO_SUBSCADE=ADDRESS。0。饱和_SUB(分区。位置()。0)作为usize;让mut merge_buffer=[0x00 U8;SUBSCRATE_SIZE];SELF。阅读(界别分组。Location(),&;mut Merge_Buffer)?;如果是块。IS_SUBSET_OF(&;合并缓冲区[OFFSET_INTO_SUBSCADE.。]){界别分组的(区块、页面、地址)。页面()。重叠(块,地址){自我。WRITE_PAGE(&;PAGE,BLOCK,ADDRESS)?;}}其他{自我。ERASE_SUBSCRATE(&;SUBSCADE)?;MERGE_BUFFER。Iter_mut()。跳过(Offset_Into_Subsector)。Zip(数据块)。For_each(|(byte,input)|*byte=*input);for(block,page,address)in subsector。页面()。重叠(&;MERGE_BUFFER,子扇区。位置()){自身。WRITE_PAGE(&;页,块,地址)?;}确定(())}。

如您所见,此方法看起来与我们的MCU闪存方法非常相似。然而,也有一些不同之处。这一次我们的写入粒度与擦除粒度不同,因此不是直接写入子扇区,而是在subsector.ages().overaps(block,address)中使用(block,page,address)形式的内部迭代。

同样,WRITE_PAGE、ERASE_SUBSCADE和READ是低级芯片特定功能(在本例中通过QSPI发送一些命令),超出了本系列的范围。

在这里,我们看到了具体/通用混合方法的真正好处。我们将芯片特定的逻辑--subsector.ages()级联内存映射样式--与我们的通用重叠函数无缝地编织在一起,而不牺牲两边的表现力。

就是这样。我们已经做到了。我们与恶魔、龙、乌龟战斗过,增加了对多种形状括号的容忍度,并且变得更具通用性。我希望我们从这件事中恢复过来时比失去的更理智。

我知道我漏掉了很多细节。毕竟,这只是一个更大的项目的一小部分,我毫无歉意地忽略了模块可见性、数十个帮助器函数以及许多小决策的合理性。我也认为--或者更确切地说,我知道--我做了很多不太理想的事情。有那么多要学的东西,所以在最后的任何一个链接里都让我知道!说到这里,这里有一个小附录,涵盖了我在上次参赛后收到的一些问题:

如果你有机会,你能写一篇关于你的环境和工具的帖子吗?我已经有一段时间没有涉足嵌入式开发了,我很好奇嵌入式锈蚀的过程和开发环境是什么样子的。-/u/ricree。

如果我提到的召唤恶魔的天堂并没有让我有一点书呆子的感觉,所以我喜欢坚持使用nevim,使用coc-rust分析器forditing(效果非常好)和原始命令行gdb来调试一个SVDread插件。调试体验并不理想,所以我正在开发我自己的名为beak的TUI调试器。我以后会更多地谈到它,但在这里,你可以偷偷地看一看它的样子(如果嵌入物看起来太小,试着点击下面的链接):

我不是在骗你,上周我根本不知道RSS是什么。谢天谢地,Google叔叔做到了,所以现在我有了一个全球RSS提要(链接在主页底部)和每个类别的RSS提要,以防您想要。

.