在51单片机为主流的时期,常常会有个问题困扰嵌入式应用的初学者,是先学习汇编语言,还是直接学习C语言。
在51时代,可以毫不犹豫的说,不懂汇编就不是个好的开发者。51指令系统开发与70年代末,因此,相关资料极为详细。而主流的51教科书都无一例外的会从汇编指令表展开教学。
在针对51开发的C语言普及之前,汇编几乎是51开发的唯一手段,加上51单片机资源有限,因此在C语言只能用在扩展了存储器的51单片机系统电路.中或者是增强型(在芯片上增加了存储器等)芯片上。对于资源很少的AT1051、AT2051芯片,除非功能特别简单,否则,用C语言进行开发是不可能的事情,因此,可以说不懂汇编,就不是一个好的开发者。
当嵌入式开发进入到ARM芯片时代,先学汇编还是直接学习C语言已经不再是问题了。ARM芯片上的存储资源尤其是RAM比标准51的要大上10倍以上,为C语言开发奠定了基础。在弃繁就简习性的推动下,C语言成了为数不多甚至是唯一的选择。在今天,即使你想学习ARM的汇编也困难重重了。首先是ARM的汇编指令表,几乎没有书籍和资料做专门的介绍。其次,所有开发平台虽然都可以支持汇编(相对C语言,汇编编译器要简单很多,并且是高级语言编译的基础)但都没有做专门介绍,也不提供汇编的例程。因此现在学习汇编要比以前艰难许多。
和C语言比起来,汇编比较繁琐,但它确无法绕开的。许多初学者甚至许多业内“高手”认为C语言可以做到汇编语言能做到的一切。我以为这个不但目前做不到,在可以未来预见的未来也不可能做到,除非计算机在结构上发生重大进步。
现在所有所谓“高级”语言,对CPU来讲都不是“自然”语言,在这些源代码由编译器翻译成CPU可执行的机器语言之前是不可能被CPU执行的。因此,有一种指令系统就必须配备一款专门的编译器,就像微软的PC操作系统和Mac操作系统不同是一个道理。所以,编译器的完成者是必须懂得汇编语言。
ARM公司最成功的思想就是统一了CPU的汇编指令体系,它自己不生产芯片,但它使ARM体系的编译器发挥最大的作用。各个芯片生产公司都使用ARM体系内核,再加上各自擅长的外部设备集成芯片。这样就为各个公司的MCU推广提前铺平了道路,ARM公司也在这种开发系统应用最大化中获得了可观的利益。
即使有了最好的编译器。也并不能使高级语言解决一切问题。这个做PC软件的工程师应该最有体会,因为一旦涉及到底层设备的操作,就需要调用一个叫做“驱动库文件”的东西,而这个东西最核心的代码只能是用汇编程序做的。
在嵌入式开发进入到C语言时代后,芯片生产厂一般都会为芯片上比较复杂的设备如:USB、Ether等设备配上专门的驱动库以便芯片应用者使用。这虽然解决了复杂设备的一些应用问题,但不等于解决了所有问题。做过汇编程序的人都知道,对于一个计算功能的实现,不同的编程者所写的代码一般是不同的,虽然都能完成任务,但有些程序会更好一些。造成这种情况的原因是汇编指令中有很多功能相同但又有些许差别的指令,例如,51单片机的累加器加1这个功能,既可以用“ADD A,#1”来完成,也可以用“INC A”这条指令,就像每个人有自己的习惯用语一样,每个程序员都有自己使用指令的习惯。程序员不用清楚每条汇编指令的含义,照样可以编出很棒的程序,就像作家不用认识每个字照样写出好文章一样。
微处理器技术发展到现在,增加了许多原先没有的操作细节,如增加速度的流水线操作等,这些指令的使用有很高的的条件要求,高级语言编译器是无法将这些细节操作通过编译产生的,各种编译器其实也有自己的“习惯用语”,所以,也不可能用到所有的汇编指令。因此,当高级语言程序需要做复杂操作时,是必须调用“库”文件的。
当我们把这些任务交给“库”来完成后,我们就不能再有另外的选择,无论它是好一点或者差一点。
再者即使对于像GPIO这样简单的设备,在许多情况下我们也希望它们的操作判断越快越好,而能使这些口操作最快的手段,依然是汇编程序。
也许有些人要问了,C(包活其它高级)语言的程序不是经过编译器最后编译成了和汇编语言对应的机器语言吗,最后都是机器语言,怎么会说汇编编的程序快些而C语言的编译过的程序就慢些呢?
这里举一个简单的例子说明。作为项目的完成者,编写程序的人清楚的知道自己要完成的计算是什么规模的,比如要完成一个一百以内的数目运算使用一个字节就够了,因为一个字节里的最大数目是255;完成一万以内的数目运算只需要一个字就够(一个字里的最大数目是65535)。
如果是用汇编编程,程序员直接使用字节或字取数后,编写运算程序即可,不再占用运算需求以外的任何存储资源;但如果使用C语言,情况就不大一样,因为在C语言编程时你首先要确定数据类型,你需要用一个unsigned char a或者 unsigned int a来定义数据类型。如果你把这个C程序编译后所生成的汇编语言拿来看,你就会发现用汇编直接取数的一条指令,在C编译后的过程是先判断类型字节的数字是表示比特、字节、字还是长字,然后四中选一再进行相对应的取数操作。汇编编程直接的一条机器代码(与汇编是一一对应的)用C完成需要10条甚至20条以上的机器代码才能够完成。
所以,汇编程序编写的代码会比包括C语言在内的所有高级语言快得多,也是从硬件角度讲最便宜的选择。那为什么这种多快好省的开发方式会被边缘化呢?这是因为学习汇编需要相应的硬件基础,而且不容易被掌握。从这个角度讲,汇编才是真真的“高级”语言。
C语言的流行得益于芯片性价比的飞速提升,随着MCU的主频和资源的大幅提升,高级语言的硬件消耗越来越便宜,相反程序开发的劳务费用却在不断上升,因此,好懂易用的C语言在各种多用途多样化的产品中占了主导地位。尽管如此,各种高级语言并未能真正能摆脱汇编语言,前面提到的各种驱动“库”文件,就是在各种应用中不能缺少的。因为在真正需要高速操作的地方,是不能依靠高级语言自身去完成的。
所以,嵌入式的研发行业内仍然需要一定数量懂得汇编语言的人,尤其是在高要求又需要自主开发的地方,这也是本文作者写下该文的初衷。
闲话到此,下面就来谈谈ARM的汇编指令,由于ARM内核有多个,并且还在不断的变化,因此,本文只能以相对简单的Cotex M0指令系统作介绍,尽管如此,只要真弄清楚了这些指令的规范和结构,学习其它ARM指令会变得非常容易。熟悉51(或其它任何一种CPU、MCU)汇编指令系统的人,对本文理解会容易很多,对不懂汇编建议找一本有51指令系统介绍的书籍或先资料熟悉一下,然后再读本文。
本文的主要资料是来源于广州周立功单片机发展有限公司的公开资料《LPC1100系列微控制器用户手册(中文版)》的第二十二章“ARM Cortex-M0参考资料”,Cortex-M0 指令表中的指令代码,则是利用现有编译器,使用反汇编原理推导及验证的结果。由于可用的资料和作者能力的限制,文中难免有些存在少许误解和失误,在此先做个免责声明并敬请各位读者谅解。
先回顾一下51指令的数据格式,8051指令系统使用的是标准8位存储器,即每个地址单元有8个比特位。寻址空间则为16个比特位(共64K)。用二进制可以直观的表达为:
地址 对应地址存储数据
0000 0000 0000 0000 bbbb bbbb (注:b=0或者1)
0000 0000 0000 0001 bbbb bbbb
0000 0000 0000 0010 bbbb bbbb
…… …… ……
1111 1111 1111 1111 bbbb bbbb
地址表示使用具体数字是因为地址是常量,每个地址都是明确的,就像一个大旅馆面的房间编号,是固定不动的;数据使用b表示意味着数据是变量,就像旅馆房间一样,今天住这个人,明天住那个人。而编程者就是所有房间的安排者,确定每个地址应该存放什么样的数据。
对按照指令执行命令的CPU来讲,必须有个明确的执行起始点。而且这个起始点有对应的硬件操作端复位键,51是高电平复位,因此,给它的复位端一个高脉冲,它就会从0000 0000 0000 0000这个地址开始执行指令。51最大的地址是二进制1111 1111 1111 1111B,用十六进制表示为0FFFFH,也就是业内人常说的64K。
51是复杂指令集,因此,它的指令按所占地址分为单字节指令,双字节指令和单字节指令。也就是说,如果51的指令即可能只占一个地址,也可能会占两或3个地址,依照所使用的指令占用字节数的不同而不一样。
另外,为了使硬件效率最大化,51指令从00H-0FFH的所有256个数据都是指令。顺便提一下,0EA也是指令,虽然该指令在标准51中是双字节空指令,但这只不过是为了给今后的扩展预留,有些品牌的扩展51核用该指令扩展出4字节指令。
而ARM是使用的精简指令集,精简指令有2个特点,一是指令数目少,其指令数据位在理论上只占5个比特;二是它按每个的地址只占一条指令的规则安排存储器。下面以Cotex M0(一下简称为M0)指令系统为例,看看ARM指令的特点。
Cotex M0是16位指令系统,但由于它是32位的运算器,而且寻址空间也是32位,因此,把它说成是32位机也能说的过去。
依照前面的二进制表示法,M0的指令格式可表示为如下方式:
地址 对应地址存储数据
0000 0000 0000 0000 0000 0000 0000 0000 bbbb bbbb bbbb bbbb
0000 0000 0000 0000 0000 0000 0000 0001 bbbb bbbb bbbb bbbb
0000 0000 0000 0000 0000 0000 0000 0010 bbbb bbbb bbbb bbbb
…… …… ……
1111 1111 1111 1111 1111 1111 1111 1111 bbbb bbbb bbbb bbbb
和51的64K个地址相比,32位ARM的地址是51的64K倍数,就目前的嵌入式应用来讲,是不需要使用如此大的存储空间的。但就像大多数51芯片并不用满64K空间一样,大多数ARM芯片只用其空间的一小部分。
回到具体指令上看,M0是精简指令集,也就是说每条指令只能占一个地址,但每个地址有2个字节的容量(51每个地址只占一个地址容量)。
现在以51和M0指令为例,比较一下复杂指令集和精简指令集的数据格式。先看51指令的3中格式:
单字节指令:占一个地址,格式为“指令本身”。没有操作数,功能是寄存器之间的数据传递,寄存器左、右移动一位,加一等。例句:“MOV A,Rn”;
双字节指令:占2个地址,格式为“指令+八位操作数”。例句:“MOV A,#DATA”;
三字节指令:占3个地址,格式为“指令+8位操作数+8位操作数”或“指令+16位操作数”。例句:“MOV SBUF,B”、“MOV DPTR,#DATA(16位)”。
相比51的复杂指令集,M0的指令格式看起很简单,即:cccc cddd dddd dddd。其中c表示伪指令,d则为数据,前面5个比特位是精简指令集的标准指令位,共32条指令,而后面有数据位是11个,加在一起是16个比特位。精简指令集明确规定每个地址位置只放1条指令。
虽然精简指令集看起来好像很简单,其实真正了解的话并不比复杂指令集少多少而且不大容易理解,在存储器的使用上也不会更节约。2者性能其实差不太多,只是在产业联盟上存在竞争关系,有竞争对用户总是有利。
下面来看看为什么精简指令集的指令不少,从格式上看,精简指令集只有5个指令比特位,理论上只有32条指令。但由于它的每条指令必须占据一个地址位置,所以不能像复杂指令集中的单字节指令那样节约数据位(参见51的单指令)。比如M0中每个地址为占16个比特,如果是要实现一个空造作(NOP),51只需要8个比特位(一个字节)而M0则需要16个比特位,所以为避免浪费精简指令**尽可能的利用数据位的空余比特,形成许多子指令,即在不需使用数据位时会在32条指令的下面扩展出多条指令,最终的结果是精简指令集的指令数目远大于32条。
其次,因为软件应用已经习惯于以字节单位,所以顺应应用实际也必须规范数据格式。M0指令可以进行8位,16位和32位运算,而它的数据位是11位,只能满足8位取数。因此,要进行16位和32位的取数必须使指令超出一个地址位,事实上精简指令集也不是绝对的每条指令只占一个地址位置,它还是有多个一个以上地址位置指令存在的。
附件是Cortex-M0指令表,看过指令表后你就可以发现,M0的通用寄存器里面并没有包含R8至R12的高位寄存器,因为指令表中没有包含这几个寄存器如何寻址。类似的许多问题,都可以从指令表中找到。