来源:https://zhuanlan.zhihu.com/p/468286383
RTOS在项目中使用得很广泛,要想更好的理解RTOS的工作原理就要知道CPU运行的大体流程是什么样的。
为什么要引用RTOS裸机跑程序它不香?刚开始使用单片机都是直接在main函数写一个while死循环然后在while循环中添加我们需要的代码。最终,实现我们的功能。
反正能跑,代码就没问题这种方式俗称"代码裸奔"。如果while循环需要实现的功能为4个电机转动不同角度,同时OLED显示电机的具体角度。裸机的执行步骤是4电机都转动后,将数据发给OLED屏(或者转动一个个数据给OLED屏幕)。
我们知道电机转动的原理是采用了调整高低电平的占空比。也就是开PWM,开PWM就要用到延时,那么对于最后的一个电机,它必须要等到前面的电机的程序完全运行完了才运行。这就出现了延时,有没有一种方式可以让所有的电机能够同时运行?RTOS就应运而生。
当然,对于单个点灯等过于简单的程序就不需要RTOS了,裸机能够实现就行,杀鸡焉用牛刀。RTOS主要针对多任务的程序。
要想更好的理解RTOS的工作原理就要知道CPU运行的大体流程是什么样的:
中央处理器 (英语:Central Processing Unit,缩写:CPU)是计算机的主要设备之一,功能主要是解释计算机指令以及处理计算机软件中的数据计算。
简介写到CPU的功能为指令处理以及数据的计算,所以CPU里面肯定是有计算部分以及指令分析部分。
ALU为CPU中的逻辑运算单元主要是对数据进行处理。它的概念图如下所示。
逻辑运算单元:CPU最大的功能就是逻辑运算了。在CPU中逻辑运算单元负责CPU的所有逻辑运算。逻辑运算单元又称为ALU,它由下面几个部分组成。以A+B=C为例子。
• 输入数据(Input Data);对于一个运算至少要由两个数组成,所以ALU的输入端由两个操作数组成(如上图的A和B)。
• 指令(OP):运算指令(operation),有了操作数A与B,那么我们要对这个数进行什么操作?是加减乘除的其中一种还是混合使用?运算指令就决定了A与B进行了什么逻辑运算。
• 输出结果:运算结果的输出。
• 标志位:标志位的作用是什么?假如现在要运行逻辑的运算的A+B,CPU的操作位为16位的,也就是两个数最大的计算值为15,如果A=10,B=6,A+B就超出了ALU的计算范围了。那么超出计算范围的数该怎么运算?对于超出的计算范围ALU会通过标志位产生一个Overflow的标志,然后将超出的部分存于寄存器组中作为下一次输入。进而存储起来。当然,ALU也不仅仅是进行加减乘除,也可以进行两个操作数大小的比较。比如输入的A=10,B=6,OP是运算指令A与B的大小比较,OP就用减法操作。A-B的结果(Result)就会产生一个标志位0/1通过查看计算后的标志就能知道A与B的大小。
从上面可知ALU运算输出结果保存在寄存器组中(当然也可以保存在RAM中),那么标志位存储在哪里?
ALU运算产生的标志位通常放在一个专用寄存器中,这个寄存器叫做"状态寄存器"(Program Status Register)PSR。每执行一条指令,相应的状态位都会进行更新,每条指令影响到的状态位时不一样的,需要查看芯片手册。
上图中状态寄存器要存什么状态?尽管各种芯片都有自家类型的标志位,但是不论哪种芯片都一定有以下的标志位。
• Z:(Zero)零标志位:A-B=0,如果计算的结果为0就会产生一个0的标志位。
• N:(Negative)负数标志位:计算结果为负数。
• O;(Overflow)溢出标志位:如上面的A+B=16>15超出了计算的范围就会产生一个溢出标志位额。
• C:(Carry)进位标志位。
我们知道ALU运算需要两个操作数作为输入,那么这个数是从哪里来的?
ALU操作数的来历有很多,一般有以下几种来源。
在设计CPU的时候会在CPU的里面,用电路设计一些寄存器,实现若干个临时存储器。寄存器的性质如下。
• 用于临时保存/获取操作数。比如ALU的输入数据可以来源于CPU内部的通用寄存器组,ALU运算的结果也可以保存在寄存器中(注意CPU内部的寄存器保存只是临时保存。)
• 任何CPU都包含若干个通用/专用寄存器。
• 寄存器的数据和宽度时衡量CPU的重要指标。
对于CPU内部的寄存器,将其比作考试中的草稿纸最适合不过了。每一次运算的中间结果都可以写在草稿纸上(ALU结果输出的保存),运算的中间结果又可以作为下一步的计算提供来源(为ALU提供输入操作数)。当你把最终的结果算出来后就会将结果填在试卷里面。草稿纸中的数据在交卷的时候就被你丢弃了(临时保存。)对于每个人的草稿纸都是独一无二的存在(专用寄存器),但是大家都在草稿纸中写下中间计算的结果(通用寄存器)。然而,总有一些人时特殊的喜欢自带草稿纸的它用的草稿纸比你的大(寄存器的宽度),而且还有两张。(寄存器的数量)所以它能存中间计算数据就越多。但,考试成绩不一定比你多。
草稿纸上最初的计算数据是从试卷上面来的(最初的操作数),寄存器组的数据是临时保存,草稿纸计算完的数据要写在试卷上的。(最终的结果),这里的试卷就是外部存储器。
外部存储器存储的是什么内容?上面说到CPU内部的寄存器有临时存储数据(比作为考试中的草稿纸),而最终要上交的是试卷(比作为外部储存器对于储存器的大体内容文章的开头已经详细的介绍了,这里我将RAM与ROM统一称为外部存储器。)如果将整一个CPU运行的过程比作为是一场数学的考试,那么CPU内部的ALU的功能就要帮助我们计算运算过程的结果。运算的数据A与B是最初的源头是从哪里来的?
那就是数学试卷嘛。试卷中的题目要求我们计算A+B的结果,这是试卷要求我们要干的事。(这里的要求就是CPU要执行的指令)经过你在草稿纸的一番运算后的结果被你写到了试卷里面了(这个过程简称了"数据保存到试卷")计算的结果保存到试卷,同理类比得ALU运行后得结果被保存到了外部存储器中。经过上面得过程得出外部储存器中有:
• CPU运行的指令。
• CPU运行的数据的保存。
同样以上面的试卷为例子,试卷中有一条题目为:A=1,B=1求A+B的和C的值是多少。当你看到题目的时候,你就会根据题目的"要求"计算出结果。同理,CPU运行的时候也要人给他出题目(那个出题目的就是用户),用户写好的代码的程序经过通过编译器处理后就会将程序转换为机械码指令(也就是CPU的试卷),指令编好后要存储器起来然后再交给CPU执行。(老师出完试卷后要封装起来然后装入档案带里面密封保存)。CPU从存储器拿到这段机械码就会分析,用户究竟要干嘛。经过分析后就会知道原来你要拿到A+B的运算结果。
上面我们将外部存储器存储数据存储ALU运算结果比作为(将草稿纸运算的结果写到试卷)说明存储器具有存储用户需要的最终数据。但对于CPU的外部存储器来说,除了存储最终的数据结果外,也可以存储临时的数据。
CPU内的寄存器能够临时存储数据,外部存储器也能够临时存储数据。那么这两者没有冲突么?
CPU内的寄存器能够临时存储数据,外部存储器也能够临时存储数据。那么这两者没有冲突么?
两者的临时存储的时间是不同的,由于内核寄存器在CPU内部近水流台先得月,所以CPU取内部寄存器的数据是最快的,一些需要频繁调用的数据,最好就存储在内部的通用寄存器中。但是对于一些过了几秒才用调用不是很频繁就需要存储在外部存储器中。当然这不是绝对的,用户可以通过去掉编译优化,将频繁调用的数据存在外部存储器。但是这种做法显然是会降低数据的处理速度。
指令在外部的存储器中CPU是如何取指令的?
书接上文,我们说到老师讲出好的试卷装入密封袋中。但是很不巧,第二天老师发现试卷少了一张,生气的他直接决定当场出一套新的试卷,他在黑板上出一道题,学生答一道题。这时老师的脑子就存着试卷内容,学生要计算老师出的题目。学生拿到试卷题目的方式是通过黑板获取的。
同理,CPU要拿到指令,是不是也要有一块黑板来存着这些指令呢?在CPU中这块黑板就是程序计数器PC与上述的黑板不同的是PC寄存器中存的并不是指令(题目)而是指令的地址。
程序计数器(英语:Program Counter,PC)是CPU内部的特殊寄存器,它总是指向下一条指令的地址。如上图,外部存储器实质上是一个个格子组成的,每个格子都有一个地址编号,每个格子里面存储着CPU要执行的指令,PC总是指向下一条指令的地址。如果当前是NOP那么此时PC指令指向的地址就是0001。
为什么PC寄存器总是指向下一条指令而不是当前的指令?
假设当前外部存储器被你的数据装满了,你的PC指向了顶端(下一步就要溢出了)下一步有可能发生一些不可预估的错误,如果PC指向了下一个地址当你的数据刚好存到顶端的时候,也可以通过CPU设置PC指向异常来反馈。
有了PC寄存器,存储器的指令就可以被CPU取到了,如上图在程序计数器(PC指针)的指引下将指令存储器中的指令加载到指令寄存器,经过指令的解析后将数据交给控制单元,实现数据存储器数据的加载以及ALU单元和要执行什么样的运算等等。PC指针寄存器是程序运行非常重要的机制。我们可以通过堆栈进一步加深对它的理解。
我们知道CPU的指令与数据本源来着外部的存储器,那么对于CPU其存储器主要由以下的分类。
只能读出无法写入信息。信息一旦写入后就固定下来,即使切断电源,信息也不会丢失,所以又称为固定存储器。在单片机中芯片上电复位的瞬间,CPU执行的第一条指令就是从ROM中获取的。在上电的过程中CPU究竟从ROM中读了什么内容?
• 为全局变量分配地址空间:将初始值从ROM中拷贝到RAM中,如果没有赋初值,则这个全局变量所对应的地址下的初值为0或者是不确定的
• 设置堆栈段的长度及地址:用C语言开发的单片机程序里面,普遍都没有涉及到堆栈段长度的设置,但这不意味着不用设置。堆栈段主要是用来在中断处理时起“保存现场”及“现场还原”的作用,其重要性不言而喻。
• 分配数据段data,常量段const,代码段code的起始地址。代码段与常量段的地址可以不管,它们都是固定在ROM里面的,无论它们怎么排列,都不会对程序产生影响。但是数据段的地址就必须得关心。数据段的数据时要从ROM拷贝到RAM中去的,而在RAM中,既有数据段data,也有堆栈段stack,还有通用的工作寄存器组。通常,工作寄存器组的地址是固定的,这就要求在绝对定址数据段时,不能使数据段覆盖所有的工作寄存器组的地址。
既然说ROM是只读存储器就是不可以写入那么为什么说代码中的数据能够写进去?
首先,ROM是只读储存器只读不写没错。这只是对与早期的单片机来说。早期的ROM在芯片出厂的时候数据与程序就已经固化在芯片里面了,不能更改。强行更改就会直接报废。既然ROM是出厂的时候就固化,无法写入,也就无法进行编程,那么它的用途是什么?ROM虽然无法编程但它适合大量生产。
不如有一些大型的工业设备,芯片要控制成千上百的器件,不可能人为的一遍一遍的将代码进行编程,烧录到设备中。如果在出厂前就已经将程序固化在了ROM当中,那么只要将机器组装起来上电就行。这不仅简化了生产,而且制作便宜适合大量生产。当然ROM在当时也不仅仅在大型的工艺上面用到,这只是在工业上用的比较普遍而已。
ROM无法存储,生活中的数据中的编程数据是如何读写?
由于早期的的ROM的储存器不可编程的缺点被大多人诟病,随着技术的发展出现了PROM
可编程的ROM,出厂的时候是一种空白的ROM,用户可以根据需要写入信息,但写入后信息就不能更改。PROM是通过20V的电压下熔断里面的熔丝,实现0/1的写入。由此可知PROM是一次性的编程,因为熔丝熔断后就不能恢复了,如果要写入新的数据就要用一块新的PROM。PROM最有代表的那就是任天堂游戏卡带了
可编程只读存储器,出厂的时候是一种空白的ROM,用户可以根据需要写入信息,但写入后信息就不能更改。PROM是通过20V的电压下熔断里面的熔丝,实现0/1的写入。由此可知PROM是一次性的编程,因为熔丝熔断后就不能恢复了,如果要写入新的数据就要用一块新的PROM。PROM编程的工作电压高、只能一次编程也不适合长久发展。
可编程可擦除的存储器。EPROM工作原理是在高压的条件电子跃迁实现编程,再利用紫外光照射芯片使得EPROM里面发生电子飞跃,实现擦除的。这种方式为高压编程紫外线擦除,可以重复使用。但是,成本太高也不能普及。
电可擦除可编程,常规电压下进行擦除与编程。可以实现3.3V-5V的情况下编程与擦除,符合多次编程与擦除。
以块擦除与写入,优点质量轻,能耗低、体积小。缺点需要先查出后写入,块擦除次数有限,读写干扰。常见的有USB、SD卡。图1框图中,如果用户将程序代码的二进制文件下载到外部的USB当中,CPU通过启动设备控制器中的USB控制线就可以实现通过USB启动设备。也就是实现了CPU通电的时候,第一条指令是通过USB中的文件启动。当然,在写入的过程中要保证USB内部的数据干净除了,仅仅能够装载CPU所要的程序。
在PC的重装电脑的过程中,首先要把WIN系统装在一个干净的U盘中,将操作系统的数据写入硬盘,计算机上电后通过运行硬盘的操作系统数据,实现操作系统的初始化。该过程的USB与硬盘都属于Flsh memory。
回到最初的问题,ROM无法存储,生活中的数据中的编程数据是如何读写?比如STM32又是如何存储数据的?ROM就是程序存储器,掉电后数据不会丢失,但在程序运行过程中其数据不会改变.早期的单片机的ROM因为擦写修改麻烦,价格昂贵或者价格低廉的OTP型无法修改数据等原因已经被现在的FLASH存储器替代了。
因为FLASH的擦写很容易,现在的部分单片机支持在线内部编程,通过特定的程序执行方式可以修改FALSH的内容,而实现在线修改程序存储器.这与上面说的程序存储器的内容在运行的时候不可被改变是不冲突的,因为在程序正常运行时,其内容不会改变,只工作在只读状态下的.
随机存取存储器,是与 CPU 直接交换数据的内部存储器它可以随时读写而且速度很快,通常作为操作系统或其他正在运行中的程序的临时资料存储介质。RAM 存储器可以进一步分为静态随机存取存储器(SRAM)和动态随机存取存储器(DRAM)两大类。SRAM 具有快速访问的优点,但生产成本较为昂贵,一个典型的应用是缓存。而 DRAM 由于具有较低的单位容量价格,所以被大量的采用作为系统的主存。
DRAM特点:
• 随机存取:所谓“随机存取”,指的是当存储器中的消息被读取或写入时,所需要的时间与这段信息所在的位置无关。相对地,有串行访问存储器包括顺序访问存储器(如:磁带)和直接访问存储器(如:磁盘)。
• 易失性:当电源关闭时 RAM 不能保留数据。如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。RAM 和 ROM 相比,两者的最大区别是 RAM 在断电以后保存在上面的数据会自动消失,而 ROM 则不会。
• 较高的访问速度:现代的随机存取存储器几乎是所有访问设备中写入和读取速度最快的,访问延迟也和其他涉及机械运作的存储设备(如硬盘、光盘驱动器)相比,也显得微不足道。但速度仍然不如作为 CPU 缓存用的 SRAM。
• 需要刷新:现代的随机存取存储器依赖存储器存储资料。电容器充满电后代表 1(二进制),未充电的代表 0。由于电容器或多或少有漏电的情形,若不作特别处理,电荷会渐渐随时间流失而使资料发生错误。刷新是指重新为电容器充电,弥补流失了的电荷。DRAM 的读取即有刷新的功效,但一般的定时刷新并不需要作完整的读取,只需作该芯片的一个列(Row)选择,整列的资料即可获得刷新,而同一时间内,所有相关记忆芯片均可同时作同一列选择,因此,在一段期间内逐一做完所有列的刷新,即可完成所有存储器的刷新。需要刷新正好解释了随机存取存储器的易失性。
• 对静电敏感:正如其他精细的集成电路,随机存取存储器对环境的静电荷非常敏感。静电会干扰存储器内电容器的电荷,引致资料流失,甚至烧坏电路。故此触碰随机存取存储器前,应先用手触摸金属接地。
静态随机存取存储器(Static Random Access Memory,SRAM)是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。相对之下,动态随机存取存储器(DRAM)里面所储存的数据就需要周期性地更新。然而,当电力供应停止时,SRAM储存的数据还是会消失(被称为易失性存储器),这与在断电后还能储存资料的ROM或闪存是不同的。SRAM是比DRAM更为昂贵,但更为快速、非常低功耗(特别是在空闲状态)。因此SRAM首选用于带宽要求高,或者功耗要求低,或者二者兼而有之。
SRAM比起DRAM更为容易控制,也更是随机访问。由于复杂的内部结构,SRAM比DRAM的占用面积更大,因而不适合用于更高储存密度低成本的应用,如PC内存。
上面说到CPU上电的时候,第一条指令是从ROM中获得的。其过程如上图红色线路所示。磁盘中包含了操作系统的初始化(如果是纯裸机就是中断向量表和一些值得初始化。)接着就CPU接到指令后就会将这些初始化数据写到RAM当中(如下图黄色线所示),然后从RAM中读取数据(下图绿色线所示),执行指令。其过程如下图所示。
从上图中,我们不难发现一个问题,为什么CPU要把ROM的数据写进RAM中再读取到CPU中,不是说从ROM中读取数据很慢么, 直接从ROM读取数据的时间不是少于ROM->CPU->RAM->CPU的时间少?
举个例子:假如你电脑的机械硬潘里面有1T大小的视频你要与你的朋友分享。现在有两种方案:第一种:你带上你的电脑坐上一个小时的车去到朋友家,将视频拷贝到他的电脑里你坐车要1个小时,拷贝要1个小时,你要花费两个小时你的朋友才能看视频。第二种方案你通过百度网盘将链接发给你的朋友,你朋友将视频下载到他电脑要11天时间,这确实比直接到他家将视频慢,但是你的朋友却可以一边下载视频,一边看已经缓存的部分视频。
上面的数据直接从ROM->CPU的使用的时间确实比ROM->CPU->RAM->所需要的时间长,但是,从RAM取数据的速度远比ROM->CPU处理的速度快。既然,ROM到CPU消耗极大的时间,有没有什么方法能够直接将ROM的数据直接写到ROM当中?
现代计算机为了避免ROM->CPU的时间消耗,采用了DMA技术,如上图红线所示。此外也可以在ROM->CPU之间加一个高速缓存,将一部分数据写到高速缓存中加快CPU读取程序的速度。这个高速缓存又称为"固态硬盘"。
上述的过程,我们理清了CPU读取数据的大体过程。CPU上电的一瞬间ROM中放着我们的程序数据、操作系统。CPU读取到ROM的数据后就会将数据写到RAM中,再从RAM中读取数据。如果用户按下保存按键,CPU就会将RAM的数据读取,然后将数据写到ROM中(这里的ROM是flash磁盘,可读可写)。有了上面的基础我们就可以深入理解CPU的三大过程以及PC(程序计数器的作用了)。
对于存储器提及到最多就是堆栈的概念。那么什么是堆栈?堆栈的作用是什么?
堆栈(英语:stack)又称为栈或堆叠,堆栈是一种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场。由程序员来定义使用的大小。一般程序员在说往"堆栈中放数据"这里的堆栈一般指"栈区"(因为栈区是存数据的)只不过仅说"栈"过于单调。
说白了,堆栈的目的就是如何更好的分配存储器(RAM与ROM),由于存一个数据再存储器器中都有一个地址与之相对应,而C语言中有一类特殊的数据就是指针。它一般指向了数据存在的地址。
把存储器比作小黑屋里面关着数据每间小黑屋都有一个锁(地址),指针就是钥匙,用户带着指针就能把小黑屋中的数据释放出来(指针指向哪里的地址就可以拿到地址里面的数据)。存储器的容量是巨大的(小黑屋的数量是很多的)所以将存储进行合理的规划管理是非常有必要的,堆栈管理无疑是最好的方法。
从上述可知PC指针寄存是从指令寄存器中将指令一条一条的加载进来的,但是在C语言中程序不仅仅是只有一条一条的运行。有时候有可能从一个函数跳转到另一个函数。那么跳转后原来函数的地址应该如何保存?
方法一:利用CPU内部的寄存器。
可以在CPU内部的增加一个寄存器专门用一个寄存器来存储跳转前函数的地址(这里的寄存器暂称为"地址寄存器"),在子函数运行完以后通过将PC指针寄存器指向地址寄存器。就可以完成函数的跳转 3 方法一是能够解决函数的调用,但是如果子函数里面又嵌套了子函数,子子函数又嵌套函数的话,那套用N个子函数是不是要使用N的地址寄存器,显然利用地址寄存器只能少量的函数跳转对于函数嵌套比较多的函数又该怎么办?
方法二:利用堆栈来实现函数嵌套的时候的地址存储
为了更好理解这个过程需要对堆栈有一些基本的了解
如上图所示,因为存储器是一种连续地址的存储空间,所以堆栈也是一段连续的存储空间。其次,堆栈中存储着大量数据要被CPU使用,CPU要用到数据的时候就要从堆栈中取出数据CPU运算的结果也要存于堆栈中。这"取"与"存"的过程中势必要会改变的数据的数量。因此,有必要设定一种机制来确定堆栈中数据的数量还有多少,这就是堆栈指针寄存器SP。其作用如下图所示:
我们知道,堆栈是存储器中的一段存储数据的空间。如上图,堆栈是底部是高地址顶部是低地址。堆栈就像一边封住一边开放的储存罐,SP是一个指针寄存器,它指向的是当前堆栈的地址。上图中,在数据1111111没有存进堆栈中的时候,堆栈中没有数据,所以当前SP指向底部。
当,CPU写入堆栈中写入1111111数据SP递减(为11111存储空出一个位置),SP指向1111111的地址这个过程称之为"压栈",同理当2222222压入堆栈的时候SP指针指向222222的地址。从111111到222222压入栈的时候SP总是指向当前数据的地址。当111111要弹出的时候,首先要将SP的地址递增(将数据的空间收回)将2222222数据弹出,接着SP继续递增将1111111弹栈,实现CPU取到1111111。要取111111就要先取出2222222,这就是堆栈的先入后出的概念。
经过分析上面的分析可以总结堆栈有以下的特点
• 堆栈是一段连续的储存空间。
• 堆栈是按照后入先出的工作方式。
• 只能向堆栈顶部加入或着取出数据。
• 堆栈能够保持数据的顺序。
堆栈有两种基本的操作方式
• PUSH:将内容加入到堆栈顶端
• PULL: 将堆栈顶端的内容取出
为什么堆栈的特点与操作与操作方式是要上面规定?堆栈是一段连续的存储空间。首先,PC是CPU内部的程序计数器,它总是指向下一条指令的地址,他规定了CPU内部的指令寄存器IR拿到指令的顺序,IR指令寄存器都是一条一条的执行的,不难反推在没有发生跳转指令下PC指向的地址也是按顺序的来(当然,)
堆栈是按照后入先出的工作方式。前面我们提出一个问题,在函数发生嵌套的时候可以通过CPU内部的寄存器来保存地址来保护现场,但是对于多个函数嵌套的时候,利用CPU内部寄存器来存储地址显然不合适。在堆栈的空间是连续存储的提到跳转指令的概念,那么跳转指令与函数嵌套之间又有什么联系?
在函数发生多重函数嵌套的时候,利用堆栈来存储跳转前的地址是不是就可以解决——因多重函数嵌套导致的CPU内核没有足够的内核存储的问题了么。为了更好理解这个过程我们将概念图分析。
如上图,假设主函数中调用子函数1,子函数数1中又有子函数2。所以,这个函数运行的过程是这样的:主函数在运行的过程中碰到了子函数1的调用然后跳到子函数1运行子函数1,在运行子函数1的时候又发生了子函数2的调用。于是,子函数就调转到子函数2中运行子函数2中的数据。当子函数2运行完后就必须回到主函数(主函数还有数据没有运行完)。所以,它必须跳转回子函数1然后再跳回主函数。
在主函数调用子函数的时候将主函数数当前的地址压入堆栈中,然后跳转取去运行子函数1,子函数1调用子函数2的时候也将子函数当前运行的地址压入栈中。在子函数2运行完的时候,就会通过弹栈将之前堆栈保存子函数的地址弹出交给CPU,这时CPU就回回到子函数中,接着指令继续执行就会获得主函数跳转前的地址,然后弹栈最终实现回到主函数。堆栈的先入后出的概念根本上就是实现函数调用与返回的过程。
同时也很好回答上面的疑问,处理CPU内部的地址寄存器可以存储函数跳转的前的地址外堆栈也可以实现该功能。
同过上面的过程可以进一步可以知道堆栈更多的作用
• C语言编译器使用堆栈来完成参数传递和返回值的传递。(C预压函数的调用)
• 汇编程序可以使用堆栈来保存局部变量,寄存器值。
• CPU硬件使用堆栈来保存返回地址和寄存器上下文。(中断)
• 堆栈顶端位置通过CPU内的堆栈指针寄存器(SP)确定。
• 堆栈的初始位置有程序代码决定,指向预先划定的堆栈空间的底部。
• 如果要自己操作堆栈,记住:往堆栈放进了什么,就要取出什么。后面放进的数据要先拿出来。
上述的概念很多我们可以通过举个例子将CPU与堆栈配合的过程回更好的归纳总结上面所述的知识点。
#include
void SubFunc()
{
}
int main()
{
int A=1;
int B=2;
int C;
SubFunc();
C=A;
A=B;
B=C;
return 0;
}
A与B值互换代码经过编译器后变成汇编如上图所示,当然堆栈中并不是存着汇编指令而是机械码,为了方便理解直接将汇编指令画在了堆栈里面。因为刚开始的时候没有指令运行所以刚开始的时候PC指令指向NOP(空指令)。SP为undefine。
• 第一步:PC指向下一条指令的地址(SubFunc),SP递减,将A,B的值从片内的通用寄存器压入堆栈中。如上图2所示。
• 第二步:PC指针指向SubFunc函数的首地址,SP指向的地址继续递减将PULLA的地址压入堆栈中,以便后续返回使用。如上图3所示
• 第三步:PC继续指向函数里的返回指令的地址,因为子函数为空函数所以SP指向的地址没有发生改变,如图4所示
• 第四步:PC指针指向PULLA指令的地址,CPU获得RTS指令后,SP指令递增将跳转前的的地址弹栈。如图5所示。
• 第五步:PC指向PULLB,SP递增将B的数值22存于堆栈的值弹栈,CPU拿到数据后覆盖内部存A的寄存器。实现了将B的值赋值给A.
• 第六步:PC的值继续指向下一指令地址,SP递增将A的值弹出,CPU拿到数据后覆盖内部存B的寄存器。实现了将A的值赋值给B。
经过上面的步骤实现了A与B的数值交换,我们可以发现在这个过程中C语言中的C的数值没有出现过,因为C仅仅是定义了但是却没有实质的大小,所以它被编译器优化了。
上面的例子中,我们不难发现一个问题,指令和程序都是存在堆栈中,那么指令数据该如何管理。针对程序与数据管理一般有两种,分别是冯诺依曼体系与哈佛结构。
早期的计算机是由各种门电路组成的,这些门电路通过组装出一个固定的电路板,来执行一个特定的程序,一旦需要修改程序功能,就要重新组装电路板,所以早期的计算机程序是硬件化的!在早期的计算机中程序与数据是两个截然不同的概念,数据放在存储器中,而程序作为控制器的一部分。
这样计算机计算的效率极低,后来冯诺依曼提出了将程序编码,然后将数据与编码后的程序一同放在储存器中这样计算机就可以调用存储器中的程序来处理数据了。意味着,无论什么程序,最终都是会转换为数据的形式存储在存储器中,要执行相应的程序只需要从存储器中依次取出指令、执行,冯.诺依曼结构的灵魂所在正是这里:减少了硬件的连接,这种设计思想导致了硬件和软件的分离,即硬件设计和程序设计可以分开执行。冯诺依曼结构的核心思想如下。
• 程序、数据的最终形态都是二进制编码,程序和数据都是以二进制方式存储在存储器中的,二进制编码也是计算机能够所识别和执行的编码。(可执行二进制文件:.bin文件)
• 程序、数据和指令序列,都是事先存在主(内)存储器中,以便于计算机在工作时能够高速地从存储器中提取指令并加以分析和执行。
• 确定了计算机的五个基本组成部分:运算器、控制器、存储器、输入设备、输出设备
通过下面得代码分析冯诺依曼结构运行整个过程。
本次分析跳过启动时候将ROM数据加载到RAM得过程(也就是上述得ROM->CPU->RAM)分析CPU与RAM的取指令、分析指令、执行指令的大体过程。
• 第一步:上电时候PC获取最初的指令存于地址寄存器MAR中。
• 第二步:MAR寄存器通过地址去存储体中寻找地址为0的数据并存于MDR(数据寄存器)中
• 第三步:MDR通过数据总线将本身的数存入IR(指令寄存器)中。
• 第四步:IR将操作码发送到CU,CU分析后得知这是取数指令就会调用MAR寄存器去取数。
• 第五步:MAR到寄存体中寻找数据放到MDR中,在CU的控制下放进ACC寄存器中。
• 第六步:PC计数器自动加一,重复1-4步
• 第七步:因为执行乘法操作,CU会把MDR中的b的值放入MQ中CU把ACC中a的值放入X中,通过CU告诉算术逻辑单元,通过ALU进行计算,然后放入ACC中,如果乘积过大还需要MQ做辅存。最终实现y=a*b+c。
。从上图的CPU指令的操作过程不难发现,存储体中的数据由16位组成,前6位为操作码后10位位地址码,通过CU单元的译码后会得出相应的前6位的操作码是"读"、"写"或者"跳转"等指令,后10位为地址码主要是从存储体的地址中找出相应的数进行指令操作。指令中包含了指令于地址,而指令又是存于储存体中的所以刚好验证了对应了冯诺依曼结构中数据与指令存于同一个寄存器的思想。
从上述过程我们也解答了上面所提出的疑问。PC寄存器是用于存储下一指令的地址的(其实也可以存储当前指令的地址,只不过寄存器都是暂存的作用所以要具体从当前角度看还是下一步的角度看。)
冯诺依曼结构广泛应用于当今的计算机中。但是,我们不能理所当然的任务所有的计算机结构都是如此。当今社会除了计算机以外还有高性能的微处理器如:89C51、STM32 、ARM等。他们于前面提到的计算机最大的不同笔者认为主要有两点。
在计算机中对于RAM有对应着电脑的内存条、对于ROM对于计算机的磁盘。计算机的RAM一般都是8G以上,磁盘一般都有500G以上,对于微处理器来说无论RAM还是ROM最多不会超过KB,对于ARM的RAM可能有几百M到1G,ROM也有几G级别。对于微处理器来说CPU内部都会集成了ROM、RAM、SPI、ADC、USB、NVIC等驱动。构成了CPU+的模式。
其次,指令操作的过程中数据于指令不是在同一个储存器中的。指令有指令存储器,数据又数据存储器。
指令于数据分开存储互不干扰的结构就是哈佛结构最大的特点。
如上图,这就是典型的哈佛结构。程序的指令与数据都是分开存储的。与冯诺依曼相比哈佛结构无需先译码让后再操作数据,指令与分开使用。这无疑是提高了运行效率。
CPU为什么要有时钟?
首先对上图的逻辑电路进行分析:当A=B=1时,C=0。当输入信号发生变化时,逻辑元件不会立即对输入变化做出反应,会有一个传播时延(propagation delay)。当B变化为0时,由于B也作为XOR的直接输入,所以XOR异或门会立即感知一个输入变为0的状态变化,XOR输出变为了1。但是由于传播时延的作用,AND与门的输出会过一小段时间才变为0,XOR的输出会在变为1后隔一小段时间重现变为0。表现为下图就是这样:
上面这种现象叫作空翻(race condition),即指输出中出现了一个不希望有的脉冲信号。一个简单的办法就是在输出端放置一个边沿触发器.
边沿触发器的作用就是只有当CLK端输入从0变到1时,数据端D的输入才会影响边沿触发器的输出。这样,所有的传播时延都会被边沿触发器所隐藏掉,这时C端的输出将变得稳定。比如
其中虚线的部分代表没有边沿触发器时的C端输出状态。我们可以看出,当有了边沿触发器后,C端的输出变得稳定,基本消除了传播时延。
从上面的例子我们可以看出CPU为什么要时钟:目前绝大多数的微处理器都是被同步时序电路所驱动,而时序电路由各种逻辑门组成。正如上面说的那样,逻辑门需要一小段时间对输入的变化做出反应(propagation delay)。所以需要时钟周期来容纳传播时延,并且时钟周期应当大到需要容纳所有逻辑门的传播时延。
当然,目前也有Asynchronous sequential logic,即不需要时钟信号做同步。但是这种异步逻辑电路虽然速度比同步时序电路快,然而设计起来比同步时序电路复杂的多,并且会遇到上面说的空翻现象(race condition),所以,现在绝大多数的CPU还是需要时钟做信号同步的。
上面主要讲述了CPU的运行过程,下一篇会进一步讲述ARM的中断、总线、等概念,一切是为了更好的理解RTOS打下基础。
版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。
分享一个OTA升级相关的应用实践!
一个应用于单片机的按键处理模块!
实用 | 分享几个非常实用的开源项目
STM32如何收发float类型数据?
如何查看Linux命令工具的源码?
分享一款嵌入式人必备绘图工具!
在公众号聊天界面回复1024,可获取嵌入式资源;回复 m ,可查看文章汇总。