首发:裸机思维
作者: GorgonMeducer 傻孩子
【前言其一—— 你听说过 Memory Model 么】
不知道读者中有多少人听说过 Memory Model 这个概念,中文通常翻译成存储器模型,实际上,这种直接对英文单词的机械翻译为大部分人带来了巨大的误解——很多没有没有接触过Memory Model实际内容的小伙伴可能都会像我当年一样望文生义,以为它是关于:
- 描述存储器结构的一种模型
- 描述各类ROM、RAM特点和差异的数学或者设计模型
- 描述各种常见存储器分配的算法模型,比如栈(STACK)、堆(HEAP)以及池(POOL)等等
然而 Memory Model 和上述内容一点关系都没有。不得不说,这是一个由不走心翻译所毁掉的概念。Memory 在这里并不能直接翻译为“存储器” ,它实际上是一个类似行业黑话(Jargon)的缩略语,而Memory Model真实想讨论的议题是:当我们把目标对象(比如外设等)“当做普通存储器”一样来访问时,这个存储器有啥特点——在这里,“当做普通存储器” 对应 Memory 这个词,而 Model 则更接近“为理解和体现某些特点而建立的模型”——用大白话来说就是:
- 有一个本身可能不是存储器的外设或者加速器;
- 为了方便类似C/C++这样的高级语言来访问这样的设备,我们把这个设备映射到一段地址空间中,从而把它当成一个存储器一样来访问;
- 毕竟,这是一个模拟出来的存储器——那么这个模拟出来的存储器有什么特点?有什么注意事项?我们访问的时候会遇到什么问题呢?——不妨用一个模型来描述一下这些特点和行为方便更为广泛的讨论。
一言以蔽之:当我们把某个东西映射到地址空间中、当成一个普通存储器一样的来访问的时候:
- 有哪些坑在前方等着我们?
- 它们的现象是什么?
- 后果是什么?
- 由什么导致?
- 如何解决?
- 它跟我们常见的各类Hardfault有什么关系?
- 它跟系统性能优化有什么关系?
如果你对上述问题一头雾水,甚至不知从何谈起,那就不妨随着本系列一点一点抽丝剥茧——慢慢来学吧。
首先,可能浮现在你脑海里的第一个问题就是:你说这里的 Memory 是“把目标对象当做存储器一样来访问”——就以寄存器为例子——难道还有“不把目标对象当做存储器一样”来访问的方式么?
还真的有!回想一下,你怎么访问普通存储器?是不是C语言里只要知道目标变量的地址就可以用指针进行访问了?——通过地址进行访问就是“像存储器一样访问”的最大特点。当我们用指针访问外设寄存器的时候,可以说:我们是把这些寄存器映射在地址空间中——当做普通存储器一样通过地址来访问它们。与之相对,有一类寄存器必须借助特殊的指令或者其它手段才能访问,比如CONTRL寄存器、PRIMASK寄存器等等——这些Cortex-M的“特殊功能寄存器”只能通过专门的指令“MSR和MRS”以指名道姓的方式进行读写——它们就不属于一般意义上Memory Model所要讨论的范畴,而是开发者模型(Programmers' Model)所要关心的内容。
【前言其二 —— Memory Model 对我们真的有意义么?】
从实践经验来看,很多从事嵌入式开发不下3年甚至更久的小伙伴可能会心生疑惑:我搞了这么久的嵌入式开发,不懂所谓的 Memory Model 似乎也没觉得有啥不方便啊?
其实,这是因为很多我们常见的微控制器,比如各类Cortex-M0/M0+/M3/M4 使用的都是最简单的 Memory Model,而这类最简Memory Model的特点就是简单易用——你之所谓感觉不到它的存在,是因为这类简单 Memory Model 中很少存在陷阱——当然也不是没有陷阱,只不过我们已经非常熟悉他们了,比如:
- 对齐和非对齐访问的问题
- volatile 的使用问题
- 地址空间布局的问题(linkscript相关的问题)
是的,以上问题其实都是 Memory Model 所涉及的一些非常具体的点,所谓“不闻庐山真面目,只缘身在此山中”是也。对上述话题感兴趣的小伙伴,可以单击这里查看相关的文章。
Memory Model 绝非仅有这些内容而已,实际上,你们所接触的部分仅仅是简单MCU所使用的简化 Memory Model,而随着芯片资源愈发丰富:
- 更大的FLASH和SRAM
- 更多类型的存储器,比如 TCM、Cache、XIP、DDR
- 更复杂系统结构,比如多个Cortex-M核,或者Cortex-M与其它类型的加速器放置在同一个SoC里,甚至是Cortex-M与Cortex-A/R混搭的结构等等
我们的面前事实上出现了一座名为 Memory Model 的冰山——如果不能了解掩藏在水面之下的全貌,当我们拿到新一代的芯片,比如 NXP的 i.MX系列,ST的STM32F7/H7/M1系列;面对新一代的内核Cortex-M7/M33/M55,即便不全军覆没,也必然是损失惨重。
这里,有的小伙伴会问了:Memory Model 复杂么?有什么书可以看么?你这个系列会介绍到什么程度呢?
Memory Model 本身相当庞杂,但具体每一个知识点却非常简单,也容易理解;遗憾的是,Memory Model 最终所要解决的问题却不是 1+1=2 这样的简单。
我们不妨打个比方:
- 小时候,爸妈给了你几百块压岁钱,让你自己支配,你决定为自己组装一个心怡的四驱车;
- 四驱车看似是跑得越快越好,但实际上要考虑很多细节——具体包括但不限于:如何才能过弯道的时候不直接冲出赛道?马达的加速能力和轮胎材质如何配合?如何取舍重量和各类金属框架结构带来的好处等等……
- 你可以从小店老板、同伴和杂志上了解和学习了每一个配件的特点,价格——甚至做到了如指掌,如数家珍
- 但你在最终如何做出最优的排列组合,最大化的发挥预算获得最优性能的问题上仍然会纠结良久——甚至无法说服明天的自己。
- *
在一个嵌入式系统中,Memory Model 所关心的是访问的功能正确性与数据吞吐量之间的平衡问题,而在这一过程中,即便你了解了每一个简单的知识点,在面对最终的性能和功能正确性的平衡问题时,也会同样纠结良久——问题没有标准答案,只有较优解和更优解之间的区别。
在了解了Memory Model所要解决问题的复杂性和开放性后,你一定能想象出对其进行介绍和讨论的书籍与文章可谓灿若星海。你甚至可以认为经典的研究生教材《Computer Architecture》很大的篇幅其实就在介绍流水线、Memory Model以及流水线和Memory Model之间的各种“爱恨情仇”。
本系列的目的就是以连载的方式、通过一系列专题文章,慢慢为您介绍Memory Model 所涉及的一些简单知识点、讨论它们在实际嵌入式开发中会给我们带来怎样的麻烦、以及如何解决这些问题。我当然不是要写教科书,所以选题可能会有所偏重,无法面面俱到,但相信我所偏重点的也是大家所最关心的——
**作为一个普通嵌入式程序员:
**
- **有哪些必须知道的 Memory Model的知识?
如何使用这些知识解决遇到的问题?** - 如何写出正确而可靠的代码?
- 如何提升代码的性能?
闲话少说,让我们开始吧。
【总线——故事开始的地方】
应该没有哪个从事嵌入式开发的软件工程师没听说过总线这个概念吧?然而,大部分情况也仅限于此——也就只知道总线是用来传输数据的,存储器啊、外设啊、内核啊、DMA都连接在上面。受到芯片数据手册的影响,在大部分人的心中,总线是一个盘根错节的复杂网络,如“何首乌和木莲藤”那般“缠络着”。
一方面,看到Cortex-M的官方文档和权威指南有提到一些诸如AMBA、AHB、APB、PPB之类半懂不懂的名词;另一方面,总线本身的存在对日常的软件开发来说似乎就是个小透明——这一切的一切加在一起,给人营造了一种“不要问,问就是一团乱麻;不要提,提就一堆高大上的名词”的难以名状的复杂心情——你说她犹抱琵琶半遮面很诱人吧,它又经常“洒家装逼关你鸟事?”
其实,作为本系列故事开始的地方,有几个关于总线的误解还是需要澄清的,即,就软件开发工程师来说:
- 并不需要关心总线的时序是怎样的,但一定需要知道特定总线的“能耐”和“忌讳”;
- 并不需要知道总线工作的细节,但一定要知道总线有哪些特殊的功能:
- 这些功能是解决什么问题的?
- 有什么特殊的脾气?
- 编写软件的时候有哪些注意?
- 总线和时钟系统是什么关系
- 总线和功耗是什么关系
- 总线和性能是什么关系
- 总线和地址区间是什么关系
【主从“关系”可不是你想的那样“简单”】
从模型上来说,首先要抛弃“总线是一个网络”的想法——不是说总线不是网络,而是说实际上不能“直接”将总线看做一个网络。为什么这么说呢?
首先,需要明确区分,我们这里所讨论的总线并不包括一般的外设总线,比如SPI、I2C等等。我们说的是用于芯片内部数据通信的总线。
其次,这里所说的总线实际上有主机(Master)和从机(Slave)两种角色——多嘴一句,目前国外受到政治正确风潮的影响,主流的架构和芯片公司正在寻求使用新的单词来替代过去的Master和Slave这样的说法,而具体用哪些单词来替代,目前仍无定论,唯一确定的是,Master和Slave这样的敏感单词是肯定不会继续使用了。这里,主机的特权是可以主动的发起数据传输。每一个主机都直接与自己可能会访问的从机相连——从主机看来,自己就是一个小的星状结构的核心——自己位于正中间,而那些从机“你们都是我的翅膀”。
然而,一个系统中不可能只有一个主机,且不说多核MCU了,普通的单核MCU也有多个主机——比如,DMA至少拥有一个总线主机、以太网控制器、USB控制器都有总线主机接口,换句话说,它们都能主动的发起数据通信。这里需要特别强调的是,主机与主机之间是不可能进行通信的——你可以认为这帮“极端自我中心主义”的主机们“老死不相往来”;它们甚至人人都武断的认为自己独占整个芯片,所有的从机都是它鱼塘里的鱼。
然而,“小丑居然是我自己”,主机自己为是后宫之主,其实想想也知道,每个从机其实都脚踏n之船——谁还不是个海王?
在每个从机看来,不光自己是“女神”、拥有一个独占的逆后宫——同样拥有一个星状的网络——而且她切实的拥有一个所有主机都不曾享有的特权:当多个主机都主动发出(通信)邀请时,她可以一边说“你们不要争了啦~”,一边任性的决定“今天想临幸谁就临幸谁”——是的,总线仲裁的大权是在从机手里的。
另外补充一句,从机之间也是极端自我,老死不相往来的。
现在,聪明的你一定明白了:单独从每一个个体看来(无论是主机还是从机),自己都拥有一个极其简单的星状连接关系,而且自己都是这个结构的中心。站在上帝视角的我们会发现,正是这种“互为鱼塘”的混乱关系造就了所谓的总线网络——学名叫 Bus Matrix——八卦一点来说就是谁跟谁有一腿,谁跟谁不对付的一张图。是不是突然就无法直视数据手册中的总线矩阵了呢?
主机:我想追谁就追谁
从机:我想选谁就选谁
【说个笑话:在SRAM里执行代码】
现代32位的微控制器通常都有流水线的概念,无论结构多复杂,可以近似的认为在流水线的起点有一个负责指令读取(Instruction Fetch)阶段叫IF;而在流水线后半部分,负责指令执行的阶段,也会有从地址空间中读写数据的需求。简单来说,一个流水线温饱的最基本要求就是两个:通过总线读取指令,以及通过总线读写数据。
- 如果处理器在满足这两个需求的时候,给它们一人分配了一个独立的主机,则称为“改进的哈佛架构”:
- 如果处理器单纯为了追求自身结构的简单(同时也为了降低功耗),让它们共享同一个主机,则称为“冯诺依曼架构”:
对比下来,你会发现,由于冯诺依曼结构让“指令读取”和“数据访问”在“窝里”就开始“斗”了——还没有上总线就开始争抢主机的使用权,因而从一开始就遇到了“自古指令与数据不可两全”的系统性难题。这样的结构从一开始就存在性能瓶颈。
然而,拥有两个主机接口的哈佛总架构也只是理论上可能进行指令读取和数据访问同时进行,回想一下,如果这两个主机同时向同一个从机“示好”,你说从机还不是要“二选一”?具体到实践中,比如芯片里只有一个SRAM从机,如果你把程序也放在SRAM里运行,显然就相当于直接把哈佛结构当冯诺依曼用了……
当然,换个角度思考,如果一款芯片有多个SRAM从机(而且他们的地址不是交错在一起的),如果程序放在一个独立的SRAM从机里,而数据放在另外的SRAM从机上,本质上就是为两个主机各自分配了一个老婆,自然皆大欢喜。
更进一步,一款芯片虽然FLASH较慢,但考虑到FLASH拥有独立的从机,而且假设该从机配备有自己的高速缓存(因此速度也不是大问题),此时,如果强行把代码拷贝到系统唯一的SRAM里运行——并美其名曰:SRAM速度比FLASH快,这样我可以提高性能——所有这里看懂了的小伙伴就都要要呵呵了。
【大师,我悟了,世上时钟只有两种……】
不管你之前听说过没有——总线上是有时钟的,而时钟的作用自然是帮助数据传输的。为了访问存储器或者映射在总线上的外设寄存器,总线都为它们提供了一个时钟源——如果主机想读取存储器中的内容,或者是想操作寄存器,则目标从机都必须使用该时钟源,或者是该时钟源整数分频后的结果。
然而,从机有的选么?是的从机有的选——对于总线所提供的时钟源,外设可以选择我用还是不用——或者说使能还是不使能:
- 如果使能了,则主机就可以正确的访问目标存储器或者寄存器的内容;
- 如果不使能(或者说拒绝了),则主机就无法正确访问从机,其结果是“不确定的”——即可能是bus fault,也可能是读了个寂寞(但没有触发错误);
在我们开始后续内容之前,请首先在大脑中默念:
“总线时钟的作用只是为了数据访问”
“总线时钟的作用只是为了数据访问”
“总线时钟的作用只是为了数据访问”
……
然而,外设和存储器工作也是需要时钟的啊(我们称为外设时钟),这时候有些外设就动了歪脑筋:干嘛辛辛苦的去考虑时钟源从哪里来的问题,咱们直接用总线时钟就好了嘛——概念上,我们把总线时钟,以及总线时钟分频后的时钟都称为同步时钟。这里的同步就是“跟总线时钟保持同步”的意思。
虽然很羡慕,但有一些外设处于各种跟样的原因,没法直接使用同步时钟来完成自己的工作,因此它必须要另外寻求一个额外的时钟源来作为自己的外设时钟,正因为“此时的”时钟往往无法直接从同步时钟分频而来,因此,我们又把这类额外提供给外设实现功能的时钟称为异步时钟——这里的异步强调的就是时钟源与总线时钟不同步。
对于这种拥有异步时钟的外设,同步时钟的作用就单纯只是用来实现寄存器访问了——外设的功能完全由异步时钟来确保。这就造就了以下两种奇观:
- 同步时钟开启,但异步始终关闭——可能是为了低功耗,由于外设暂时不需要工作,但是却需要访问寄存器,因此关闭了异步时钟,而开启了同步时钟;
- 同步时钟关闭,但开启异步时钟——仍然可能是为了低功耗,由于寄存器连接到同步时钟时会产生动态功耗(后面会介绍),如果应用确认不会在外设工作的时候修改外设寄存器的内容,就可以干脆把该外设的同步时钟关闭了,从而实现了降低功耗的目的。这样做还有一个功能,就是实现了寄存器的只读化——没有同步时钟,你根本没法修改寄存器的内容;
- 同步时钟和异步时钟都关闭——彻底没有动态功耗了。
- 同步时钟和异步时钟都开启——马力全开,功耗最大。
总结来说,芯片里的时钟以总线为中心实际上只有两种:与总线同步的,以及与总线异步的。外设时钟既可以直接使用同步时钟——比如很多定时器就是这么做的——也可以使用额外的时钟源,比如USB、I2S等就是这么做的。
芯片复位后,为了降低功耗,很多外设的时钟默认都处于关闭状态,在访问它们之前,不光要开启同步时钟,如果存在异步时钟,一样不要忘记正确的配置哦。最后,千万不要混淆外设时钟(Peripheral Clock)和总线时钟的概念。
【功耗管理的半壁江山】
现代的微控制器,往往可以对外设和存储器的功耗进行精细的控制。之所以可以这么做是因为,一个外设的功耗分为两大部分:
- 只要通电就一定会存在的静态电流——也就是传说中的静态功耗,要想关闭它只有一个办法,就是拉闸断电。芯片设计上,一般把芯片拆分成很多区域,这些区域可以独立的控制供电——我们称为电源域(Power Domain)。通过控制电源域来控制功耗的方法称为Power-Gating(供电管理);
- 在通电的情况下,只要有时钟翻转就会产生额外的功耗——也就是传说中的动态功耗。动态功耗是由于逻辑门翻转产生的,因而跟时钟频率存在二次方的关系。降低功耗的方法就是降低频率,甚至直接关闭时钟。芯片设计上,一般把数字电路拆分成很多区域,这些区域可以独立的控制时钟的频率和开关——我们称为时钟域(Clock Domain)。通过控制时钟开关来实现低功耗的方法称为Power-Gating(时钟管理)。
如大家所见,时钟管理实际上是整个功耗管理的半壁江山,因此按需开关外设的同步和异步时钟就是降低芯片总体功耗的关键之一。由于本系列着重介绍 Memory Model,这里功耗管理的部分只是在讨论总线时钟的时候顺带提及,因此将不做进一步展开。
【后记】
Memory Model中存在大量面向软件的抽象概念,而这些概念的硬件基础就是总线及主/从机间的各类错综复杂的“八卦”关系。实际上,在芯片设计者看来,总线(及配套的时钟和电源管理设施)才是整个芯片平台的本体:你要啥内核、要多大的存储器、要什么外设集合都可以像组装电脑一样——你要啥我就给你装啥。
从结构上来看,一个芯片系统本质上就像葫芦藤——总线是支撑整个系统的藤蔓,而扮演各种功能角色的模块则像葫芦一样挂在上面,熙熙攘攘的叫着“爷爷”,等着属于他们的高光时刻。
下一篇,我们将从最基本的数据传输开始,聊一聊总线和系统性能之间不得不说的那些事儿。
原创不易,
如果你喜欢我的思维、觉得我的文章对你有所启发,
请务必 “点赞、收藏、转发” 三连,这对我很重要!谢谢!
欢迎订阅 裸机思维
专栏推荐文章
如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。