「经历了十多年的单片机开发,站在我现在的高度来回看单片机,可谓望眼欲穿。」
下面振南要介绍的是“单片机的体系架构模型”,是超脱于任何一种具体型号的单片机芯片之上的(我感觉我要成仙
),它具有很强的普适性。几乎所有的单片机,或是ARM、DSP以及更为高端的处理器都遵循这一模型。或者说,这一模型中的几大要素是必需的。
看穿单片机
CPU模型
CPU,即中央处理单元,「它是计算机系统的核心,占有至高无上的地位,拥有绝对的管理权与控制权,」 如图1.6。
图1.6 CPU在计算机系统中占有核心地位
CPU的核心要务是执行指令
,比如计算两个数的和、读写寄存器、操作总线读写内存等等。每一个CPU都有自己事先设计好的一套指令集
,或称指令系统,每一条指令完成一项具体的操作和功能。但是指令集并不是凭空存在的,每条指令必然都对应着一套电路
。当CPU执行一条指令时,其实就是相应的电路在工作。所以,一个CPU的性能是否优异,一部分因素就在于指令集是否丰富,指令功能是否强大,指令电路是否强大而高效。
从复杂程度上来说,CPU指令集主要分为两种:复杂指令集(CISC)与精简指令集(RISC)
。大多数的嵌入式CPU都是RISC的,这一方面表现在指令的数量上:指令少,则对应的电路就少,可以很大程度上降低CPU设计的难度,同时也降低了功耗;另一方面则表现在指令的功能量级上:指令本身一般不宜实现过于复杂的功能,这使得指令执行效率比较高。CISC则不同(x86就是最为经典的CISC指令集),它的指令数量庞大(少的有300条左右,多的甚至超过500条,而RISC通常不超过100条),而且指令的功能都比较强大。这意味着采用CISC指令集的CPU在电路设计上的难度很大,研发周期比较长。但是它在功能和性能上都是RISC所无法企及的(一条CISC指令所完成的工作可能需要若干条RISC指令才能完成)。「所以在大型服务器、工作站这些计算机系统中大多使用CISC指令CPU。」
实际上,CISC与RISC只是为了适应不同的需求而产生的,它们并非对立,反而是相互促进,取长补短的关系。CISC中已经开始加入部分RISC指令,而在嵌入式领域中也出现了一些CISC指令的CPU。融合了CISC与RISC双重指令集的新型CPU将是以后的发展趋势。
上面是振南对CPU指令集的简要介绍,其实与指令集密切相关还有一些关键技术,比如流水线、指令预取、乱序执行等等,是它们让CPU的性能有了更大的提升(振南早期就职于Intel中国研究院,主要就是研究这方面技术,所以深有感触)。不过在这里振南不对其进行讲解,有兴趣的读者可以自行研究。
直到现在,仍然有很多人向我咨询关于计算机基本原理、体系架构、硬件组成等等方面的问题,我在解答之余,也在问他们:“你们对计算机基础如此感兴趣,为什么起初不学计算机专业呢?”我其实明白,很多人在高考报志愿的时候,都是有些盲目的。
指令的实质是什么?是C语言中的a=0?是汇编语言中的MOV?不,大家看到的这些语句只是指令的一种表达形式而已。「指令的实质上是一个有一定长度的二进制序列(比如0101111010101010或1011010111011011等)。」 CPU在得到指令之后,「首先由指令译码电路从中分离出操作码、操作数」,如图1.7所示(以51的MOV指令为例进行说明)。
图1.7 对指令码的译码
01110100即为指令74H,它的功能是将后面的操作数(00010000,即10H)传送到A寄存器(51 CPU中的累加器)。这条指令如果使用汇编语言来表示就是MOV A,#10H,它通过汇编器翻译之后,就是上图中的16位指令码「汇编语言的提出,只是对最原始的CPU二进制指令进行了封装,用一些便于记忆的标识,比如MOV、ADD、INC等对指令进行表示,经过汇编器翻译后,就是可直接进入CPU进行执行的指令码序列了。」
振南经常想像在CPU问世初期人们是如何向CPU输入指令的—“打孔纸带”,如图1.8。
图1.8 人们使用纸带打孔方式向CPU输入指令
在汇编语言产生之前,程序指令的编制都要完全靠人工来完成。人们将编好的若干条指令通过纸带打孔方式输入到CPU中,让它可以依次执行,最终完成整个计算任务
(纸带上的‘孔’与‘实’代表了1和0)。从某种意义上来说,“纸带”才是第一代编程语言,然后“汇编语言”是第二代编程语言。它们都是离CPU指令最近的语言,所以我们称之为“低级语言”。最后才产生了C语言,它与我们人类日常使用的自然语言(英语)已经非常接近,这意味着它离CPU指令很远了。它需要经过专门的编译器进行预处理、语义分析、编译等加工处理,生成中间代码(汇编),然后再进一步进行汇编、连接等处理才能得到真正可由CPU执行的指令码。所以,C语言被称为“高级语言”。
综上所述,我们可以认为CPU就是一个取指令执行的机器
,这就是CPU的主要功能和工作。但是CPU的体系结构又并非仅仅这么简单,如何协调取指令的过程,防止出错?指令存储在哪里?CPU如何从存储器中取出指令?
这些问题我们都要深刻理解,否则C语言和单片机是无法真正精通的。
存储器模型
存储器对于整个计算机系统来说是至关重要的:「供CPU执行的程序指令、程序运行过程中的变量和数据……」,它们都要以存储器作为载体。所以在实际的应用和开发中,人们总是希望单片机芯片的RAM和ROM容量能尽量大一些。这样就可以存储更多的代码指令,运行规模更大更为复杂的程序。另外,存储器本身的读写速度也就成为了CPU性能的最大瓶颈之一。更为形象的描述如图1.9所示。
图1.9 CPU从存储器取指令以及进行变量、数据的存储
ROM,即只读存储器,「也就是说在它上面存储的内容是无法被CPU直接修改的」。(通常只能使用专用的烧录器来修改其中的数据,不过现在一些新型的单片机芯片已经可以在CPU运行过程中去修改ROM数据了,这种技术被称为“IAP”)所以,ROM通常被用来固化存储程序指令代码和一些无需修改的数据,比如字模字库、常数等等。
RAM与ROM不同,它是可读可写的,因此被称为随机读写存储器。CPU在运行过程中,可以对RAM中的任何数据进行读写修改操作。这就是C语言中赋值语句在底层得以实现的物理基础,比如“int a; a=0”,就是将RAM中的某一个存储单元写入了一个数值0。但如果是“code int a; a=0”的话,编译的时候就一定会报错。(code关键字在51单片机C语言中是用来说明“变量”的位置在ROM中,同样的定义在ARM上使用const)而且,RAM比ROM在读写速度上要快得多,所以CPU在运行程序的时候,通常都会把一些代码指令拷贝到RAM中来,尤其是那些会被频繁执行的部分
(这就是C语言中的.text段,即代码段)。但是RAM通常比ROM要昂贵的多(关于这一点大家应该有宏观的感知,一个U盘16G才10块钱,但是电脑内存条却要好几百),这也就是生产厂商为什么在单片机芯片中对于RAM的配比显得很吝啬,而对于ROM则略显慷慨的原因。
为了更好地讲解后面的内容,对于存储器大家必须明确一点,它也算是一个常识:它是由很多的地址连续的存储单元构成的,如图1.10所示。
图1.10 存储器是由地址连续的存储单元构成
总体来说,「存储器就是一个指令和数据的容器,它与CPU相互依存」,这才使得整个计算机系统得以正常运作。此时一个极其重要的问题便应运而生:「CPU是如何精准地从存储器中取出指令和数据的,又是如何将数据写入到存储器中的?」 这一问题说起来简单,但它却引申出了一个关键的技术—“总线”!
总线模型
如果把CPU看作“帝都”,存储器看作是“卫城”,它们之间要互通往来,就必然要修建道路,而这条道路又可以不断延伸分支,将很多城市串连起来。这样,「城市两两之间便均可通行。这条“道路”就是总线!」 如图1.11所示。(这些被串连起来的“城市”就犹如振南后面要讲到的“CPU外设”)。
图1.11 总线的结构模型
好,现在CPU与存储器之间的这条通路有了。此时,CPU如果要读取存储器中地址为addr位置上的一个字节,该如何作呢?这个过程主要分三步:(是不是想起了“把大象装冰箱总共分几步?”)
CPU首先告诉存储器要读取的地址;
等待存储器将相应地址上的数据取出来;
CPU将数据取走。
更为形象地说明如图1.12所示。
图1.12 CPU访问存储器的主要过程
仔细想一下,这个过程的实现其实涉及几个问题:CPU如何将地址给存储器?
CPU如何知道存储器已将数据准备好?
CPU又如何将数据取走?
……总结起来,主要是地址和数据的传输,以及它们之间的协调与控制。为了解决这一问题,我们提出了这一模型,请看图1.13。
图1.13 CPU与存储器之间的总线模型(读数据)
图中所看到的连线就是实实在在的用于传输二进制信号(0或1)的导线。CPU首先将地址输出到地址总线上(很显然地址线的数量决定了CPU可以寻址的空间范围),然后再将RD信号置为0(RD平时为1),告诉存储器地址已经给出,请准备好数据并将其输出到数据总线上
(数据线的数量决定了CPU的数据吞吐量,这也是衡量CPU位数的标准,51单片机是8位单片机,则它每次只能读到一个字节的数据,ARM是32位的,所以它可以一次性读取一个字
)。CPU对数据总线进行读取,再将RD信号置1,整个过程便完成了。
那CPU如何向存储器写入数据呢?其实道理是一样的,如图1.14所示。
图1.14 CPU与存储器之间的总线模型(写数据)
仍然是由CPU先给出地址,再向数据总线给出要写入的数据,然后将WR信号置0,告诉存储器地址与数据已经就绪,请予以处理。最后将WR信号置1即可。综上所述,CPU中有三大总线:地址总线、数据总线与控制总线。这一模型最终如图1.15所示。
图1.15 CPU与存储器之间的总线模型
如果我们把CPU访存过程中,各总线信号上的电平随时间变化的示意图画出来的话,它将是这样的,如图1.16所示。
图1.16 CPU总线操作的时序图
上图就是CPU总线操作的时序图(Timing Digram)。它是描述接口时序与信号协议最为直观的形式。「看懂时序图是我们学习电子和单片机技术,使用C语言正确编写底层驱动程序的根本基础。」
外设模型
我们已经知道了CPU如何通过总线进行存储器的读写,也知道地址总线的宽度决定了CPU的寻址空间,数据总线的宽度则决定了CPU的位数(单次能够读写的数据量),而控制总线在一定程度上影响了访存的速度(WR与RD为0的时间越短,访存速度越快,当然也要存储器速度跟得上才行)。有了CPU和存储器,以及连接它们的总线,这就足以构成一个完整的、可正常运行的计算机系统。「我们可以把一些算法放在其中来运行,但是单片机(嵌入式处理器)并不仅仅只是用来作计算的,它更大的作用在于控制」(所以单片机的英文缩写是MCU,即Micro Controller Unit,微控制单元)。IO是最直接、最常用的控制接口,我们可以将它置1或清0来输出高电平或低电平,从而实现对外部电路或机构的控制。
对前面振南的困惑从更基础的层面进行解释:「C语言是如何实现对物理世界产生影响的?」
在图1.6中,以CPU为核心,周边除了存储器(RAM与ROM)之外,还有很多控制器,比如IO控制器、串口控制器等等,这些就是所谓的“CPU外设”。外设其实就是一些电路,它用于实现某一特定的功能。这些电路肯定要受CPU的控制,因此在电路设计上留出了专门的接口(寄存器)。这个接口的读写是符合CPU总线时序的,所以它可以直接挂接在CPU总线上,与存储器、其它外设并存(但是分属于不同的地址区间,CPU向这些地址读写数据将对应于外设电路的不同功能)。更形象的说明请看图1.17。
图1.17 CPU外设的结构模型
很明显,「CPU的整个寻址空间(它的大小由地址总线宽度决定)并不都是由存储器独占的。存储器只是占用其中的某一段而已,其它的地址空间一部分分配给各个外设,而更多的可能只是空闲保留。」 有人可能会问:“既然这样,那我们完全可以把自己作的电路接到CPU的总线上,为CPU扩展外设。”没错,只要CPU芯片把总线通过外部的引脚开放出来,我们就可以挂接自己的电路,让CPU直接去访问控制,比如挂接一个8080接口的液晶屏等。(51的xdata、STM32的FMC都是CPU内核将总线对外开放的实例,在后面大家会看到一些单片机外部总线巧妙的应用实例)。
单片机跑起来
「好,有了CPU、存储器、总线以及外设,我们把它们有机地组合封装在一起,再把各个外设、总线的信号,以及供电和地通过引脚引出来,这就是一片完整的单片机芯片。等等,要让单片机跑起来似乎还少了些什么?对,还有时钟!」
时钟系统
如果上面所说的这些都只是单片机芯片的躯壳的话,那么时钟就是在其中流动的血液和跳动的脉搏
。时钟对于单片机来说是至关重要的,它是整个系统的激励。它是否稳定、是否精确、是否高速都直接影响了单片机中所有电路的运作
,包括CPU、总线、外设等等。从本质上来说时钟就是一个方波信号,如图1.18所示。
图1.18 时钟在单片机中占有至关重要的地位
有人说:“既然时钟的快慢决定了单片机的速度,那只要尽量提高时钟频率就可以让单片机的性能得到飞跃了!?”原理上来说确实是这样的,但是因为很多因素,比如半导体材料的特性、芯片制造工艺等,导致时钟频率并不能无限制地提高(过高频率的时钟信号会导致单片机电路工作异常,问题大多出现在信号的完全性上)。关于这一问题的解释请见图1.19。
图1.19 时钟频率过高将导致信号完整性受损
更形象的说,这就如同人的心跳不能太快
,否则血液还没来得及将氧气和养份送到各个组织,就已经急逝而过了。同时,血管也无法承载如此高的血压和血液速度,最终导致人体整体机能的紊乱。相反,心跳又不能过缓,否则血液同样也无法完成输送给养的工作(单片机的时钟过慢可能无法满足我们的应用需求,所以实际应用过程中,选取一个合适的工作时钟是非常重要的
)。
不过又说回来了,我们很多时候确实希望单片机运行得越快越好,比如一些计算量很大的实时算法、信号采集、音视频的录制与播放等等。所以无数的工程师、科学家都在致力于提高硬件性能、提高时钟频率、提高加工工艺水平,甚至是尝试新的半导体材料或是改进电路结构。但是尽管如此,时钟频率仍然会有一个无法逾越的顶线。而且人们发现时钟越快,电路工作时的功耗越大。这些窘境以及对高性能低功耗的不断追求,催生了CPU芯片向着多核的方向开始发展(时钟频率不再提升,而是通过增加芯片中CPU内核的数量来提高整体的性能)。
振南上面说,时钟越快,功耗越大。这不难理解,电路不工作时功耗一定最小;一旦有了时钟,它开始工作了,那功耗必然变大。这一原理是现在很多单片机芯片中实现低功耗的根本基础。「单片机的设计者为每一个外设电路都配置了一个时钟开关」(这些开关也是一些挂接在CPU总线上的特殊功能寄存器,因此可由CPU直接控制),从而可以控制外设停机还是工作。这在很大程度上降低了单片机的整体功耗。这就是现在一些高级单片机中的一个新概念—时钟配置(CC)。它实际上可能会更加强大,「不光可以关闭某个外设的时钟供应,而且还可以调节时钟的频率,让外设可以工作在最为适宜的功耗水平下。」
二进制
将“二进制”单独拿出来作为一节来讲,是因为它是一个极为基础的概念。但是很多人对二进制并没有形象的认识,甚至有一些已经入门、稍有开发经验的人对它的理解仍然比较模糊。所以振南认为有必要将它以一种更为形象、通俗而又深刻的方式着重来进行阐述,以便给我们以后的学习打下坚实的基础。
我们人类自古以来都在沿用一种被认为非常自然的计数方式,即十进制。它的原理非常简单,即“满十进一”(为什么是十进制,究其根源是因为我们有十根手指)。如果”XY”是一个十进制的2位数的话,那么它的每一个位上将可能出现0~9这十个数字。某一位当前是9,如果再+1便会归0,同时向它的更高一位进1。这就是计数的基础原理(不论进制如何都是如此)。
既然人类已经习惯了使用了十进制,那为什么要在单片机中使用二进制呢?把它设计成十进制不好吗?在计算机问世的初期,或是在一些技术狂热分子中确实有人尝试制造出其它进制的计算机。但不论使用何种进制,振南前面所介绍的CPU体系中的各种基本内容都是必须要遵循和实现的,比如寻指令与执行、总线的操作等等。好,那振南就以总线操作中的一个环节—“CPU向地址总线给出要访问的存储器中的存储单元的地址”为例,用十进制来进行实现。
假设要访问的地址是(3456)10,请看图1.20。
图1.20 CPU以十进制方式向存储器产生地址
很明显,要以十进制方式传输3456这个数值,我们就需要用4条地址线,每一条地址线上分别传输3、4、5、6这四个数字。敢问大家,这该如何传呢?一条线如何能表达0~9这十个数字呢?有人说:“可以啊!我把5V等分为10份,0V~0.5V代表0,0.5V~1V代表1,依此类推,4.5V~5V代表9。”不错,很聪明,这就是传说中的“模拟计算机”的作法。它的信号线上传输的是模拟电压信号,而非数字信号。
虽然上面所说的方法是可行的,但有很多因素决定了人们不会去这样作:
电路的实现上难度比较大,模拟电路的设计比数据逻辑电路要复杂的多;
传输速度不高,模拟信号的产生与采集接收比数字信号要慢;
稳定性和抗干扰能力比较弱,仅仅靠0.5V的压差来确定传输的数值,极易出现错误;
功耗很难降低,模拟电路的复杂度和规模以及其它因素注定其功耗较大。
……
针对于第3条,有人曾经提出过疑问:“我可以把电压抬高啊,可以将5V定义为10V或20V,这样压差不就拉开了吗?”聪明,不过你考虑过功耗的问题吗?
我们还是用二进制的方式来进行实现吧。(3456)10转化为二进制是(110110000000)2,请看图1.21。
图1.21 CPU以二进制方式向存储器产生地址
二进制是满二进一,将一个十进制的数值转化为二进制其位数一定会变多,所以我们就需要更多的地址线。二进制数的每一位上只能表示0和1这两个数字,这对应于地址线上使用两个电平即可实现,比如0V和5V(实际可能是0V~2.xV表示0,2.xV~5V表示1)。这样作的好处是显而易见的,电路设计的难度下降了很多,而且抗干扰能力也比较强。更重要的是,信号的传输速率可以作到比较高,最终实现计算机系统整体性能的提升。另外,二进制也使得芯片的功耗可以大幅度地降低,因为我们可以将高电平定义为3.3V、1.8V,甚至是1.2V。(高电平电压定义得越低,单片机信号从低电平爬升到高电平的速度越快。因此,降低电平电压将有利于时钟频率的提高。
)
综上所述,大家应该已经比较深刻地认识到计算机系统中使用二进制的重要意义了。二进制是计算机的根基,是底层CPU硬件以及很多相关电路实现的基础。所以,在我们所作的与单片机相关的很多开发和研究工作中,会大量涉及到二进制的概念和应用。
中断机制
中断机制在单片机及嵌入式系统中是重中之重,我们必须深入理解。首先我们要明白一点:CPU执行指令代码,并非一直顺序地逐条执行,而是可能突然跳到某段代码上去的
。因为这段代码的优先级更高,或者说它更加紧迫,CPU必须暂时放下手上的的工作,立即去执行它,否则就可能导致不良的后果,甚至是严重的事故。这个“突然跳转”有时是可以人为预见的,或者是设计人员故意使然,但有些时候却是随机的,无法事先断定它发生的具体时间。这就是“中断”最为通俗的表述,如图1.22所示。
图1.22 对“中断机制”的表述
有人说:“中断似乎是在给CPU捣乱嘛,它总是在打断程序的正常执行。”不错,但是不能说是“捣乱”,因为中断的存在是合理的,是为了解决实实在在的问题而产生的。比如说,一个单片机正在正常工作,它同时还要接收来自于串口的数据,但是它又不知道数据何时会到,为了解决这一问题,我们可以采用CPU轮询方式,即不停地查看是否有新的数据到来,如果有则进行接收。这样作的最大问题在于浪费CPU的运行时间,这可能会影响到其它任务的执行效率。「如果使用中断方式,将使CPU得以解放,在没有数据到来之前它可以安心地去作其它工作。」 串口控制器(CPU外设)在接收到新的数据后主动通知CPU(这个通知的过程依赖于专门的中断控制电路以及CPU的中断源),CPU立即跳到事先设计好的处理代码(ISR,中断服务程序)去执行,完成数据的接收和处理。最后再跳回到原来的“断点”处继续完成手上的工作。
关于中断,其实包含有非常多的内容,比如现场维护、中断向量、中断优先级、中断响应速度、中断的嵌套等等。要将这一切融会贯通,我们才能在实际的开发过程中游刃有余。不过,中断很多时候要比我们想像得更加复杂一些:如果主程序在顺序执行过程中产生了中断,CPU立即转向中断服务程序,那如果在执行中断服务程序的过程中,再一次产生了中断,CPU又该何去何从呢?这就是上面所说的“中断嵌套”所要解决的问题(这里只是简单说说,让大家心中有初步的认识)。
好,本章到这里就告一段落了。振南希望通过此章能够让大家从根本上实实在在地认识到CPU与单片机体系结构以及运作机理的本质,在脑中建立起一个形象的模型。有了这个基础,大家对单片机的理解才能真正作到入木三分,学习和领悟才能事半功倍。
「单片机和C语言其实不难,从某种意义上来说,它只不过是一个“熟练工种”,最重要的是“入门”。基础加上我们的聪明才智,每一个人都能成为高手!」
历时筹备一年,于振南老师结合多年实战经验,全新系统性的打造了一套课程《十天登顶嵌入式C语言之巅(高手C)》,一共100讲,总课时2000多分钟,为我们解密了很多嵌入式C语言中你所不知道、经常出错的问题。
希望这套课程,可以让那些已经入门C语言,还在【半山腰】徘徊的人们,能够再提升一个层次,最终登顶嵌入式C语言之巅峰!!
点击阅读原文注册,跟小伙伴们一起学习吧!