能够在MCU内直接执行的指令序列是用机器语言编写的,采用助记符号来表示机器指令,便于记忆,这就形成了汇编语言。因此,用汇编语言写成的程序不能直接放入MCU的程序存储器中去执行,必须先转为机器指令。把用汇编语言写成的源程序“翻译”成机器语言的工具称为汇编程序或汇编器(Assembler),以下统一称为汇编器。
在汇编编程时推荐使用GNU v4.9.3汇编器,汇编语言格式满足GNU汇编语法,下面简称ARM-GNU汇编。为了有助于理解有关汇编指令,下面介绍一些汇编语法的基本信息 。
汇编语言源程序可以用通用的文本编辑软件编辑,以ASCII码形式保存。不同的汇编器对汇编语言源程序的格式有不同的要求。除了识别MCU的指令系统,为了能够正确地产生目标代码和方便汇编语言的编写,汇编器还提供了一些在汇编时使用的命令和操作符号,在编写汇编程序时,也必须正确使用它们。由于汇编器提供的指令仅是为了更好地做好“翻译”工作,并不产生具体的机器指令,因此这些指令被称为伪指令(Pseudo Instruction)。伪指令告诉汇编器:从哪里开始编译,到何处结束,汇编后的程序如何放置等相关信息。当然,这些相关信息必须包含在汇编源程序中,否则汇编器就难以编译源程序和生成正确的目标代码。
汇编源程序以行为单位进行设计,每一行最多可以包含以下四个部分:
对于标号(Labels)有下列要求及说明。
(1)如果一个语句有标号,那么标号必须书写在汇编语句的开头部分。
(2)可以组成标号的字符有字母A~Z、字母a~z、数字0~9、下划线“_”、美元符号“$”,但开头的第一个符号不能为数字和$。
(3)汇编器对标号中字母的大小写敏感,但指令不区分大小写。
(4)标号长度基本上不受限制,但实际使用时通常不要超过20个字符。若希望更多的汇编器能够识别,建议标号的长度小于8个字符。
(5)标号后必须带冒号“:”。
(6)一个标号在一个文件(程序)中只能定义一次,否则重复定义,不能通过编译。
(7)一行语句只能有一个标号,汇编器将把当前PC值赋给该标号。
操作码(Opcodes)包括指令码和伪指令,其中伪指令是指开发环境(ARM Cortex-M的汇编器)可以识别的伪指令。对于有标号的行,必须用至少一个空格或制表符(TAB)将标号与操作码隔开。对于没有标号的行,不能从第一列开始写指令码,应以空格或制表符开头。汇编器不区分操作码中字母的大小写。
操作数(Operands)可以是地址、标号或指令码定义的常数,也可以是由伪运算符构成的表达式。若一条指令或伪指令有操作数,则操作数与操作码之间必须用空格隔开书写。操作数多于一个时,操作数之间用逗号“,”分隔。操作数也可以来自ARM Cortex-M的内部寄存器或者另一条指令的特定参数。操作数中一般都有一个存放结果的寄存器,这个寄存器在操作数的最前面。
1)常数标识
汇编器识别的常数有十进制(默认不需要前缀标识)、十六进制(0x前缀标识)、二进制(用0b前缀标识)。
2)“#”表示立即数
一个常数前添加“#”表示一个立即数,不加“#”时,表示一个地址。
特别说明:初学时常常会将立即数前的“#”遗漏,如果该操作数只能是立即数时,汇编器会提示错误,如:
编译时会提示“immediate expression requires a # prefix -- 'mov R3,1'”,应该改为:
3)圆点“.”
若圆点“.”单独出现在语句的操作码之后的操作数位置上,则代表当前PC值被放置在圆点的位置。例如,“b.”指令代表转向本身,相当于永久循环,在调试时希望程序停留在某个地方可以添加这种语句,调试之后应删除。
4)伪运算符
表2-2列出了一系列的GNU汇编器识别的伪运算符。
表2-2 GNU汇编器识别的伪运算符
注释(Comments)即说明文字,类似于C语言的注释,多行注释以“/*”开始,以“*/”结束。这种注释可以包含多行,也可以独占一行。在ARM Cortex-M处理器的汇编语言中,单行注释以“#”引导或者用“//”引导。用“#”引导时,“#”必须为单行的第一个字符。
不同集成开发环境下的伪指令稍有不同,伪指令书写格式与所使用的开发环境有关,参照具体的工程样例,可以“照葫芦画瓢”。
伪指令主要有常量定义、宏定义、条件判断、文件包含等。在这里给出的GNU汇编器环境中的伪指令都是以“.”开头。
C语言程序经过gcc汇编器最终生成.elf格式的可执行文件。.elf可执行程序是以段为单位来组织文件的,通常划分为以下几个段:.text、.data和.bss。其中,.text是只读的代码区,.data是可读可写的数据区,而.bss则是可读可写且没有初始化的数据区。.text段开始地址为0x0,接着分别是.data段和.bss段。
汇编代码常用的功能之一为常量定义。使用常量定义,能够提高程序代码的可读性,并且使代码维护更加简单。常量定义可以使用.equ汇编指令,下面是GNU汇编器的一个常量定义的例子:
常量定义还可以使用.set汇编指令,其语法结构与.equ相同。
对大多数汇编工具来说,一个典型特性为可以在程序中插入数据。GNU汇编器的语法可以写作:
为了在程序中插入不同类型的常量,GNU汇编器中包含许多不同的伪指令。表2-3中列出了用于程序中插入不同类型常量的常用伪指令。
表2-3 用于程序中插入不同类型常量的常用伪指令
.if条件伪指令后面紧跟着一个恒定的表达式(即该表达式的值为真),并且要以.endif结尾。中间如果有其他条件,可以用.else填写汇编语句。
.ifdef标号,表示如果标号被定义,执行下面的代码。
.include是一个附加文件的链接指示命令,利用它可以把另一个源文件插入当前的源文件中一起进行汇编,成为一个完整的源程序。filename是一个文件名,可以包含文件的绝对路径或相对路径,但建议一个工程的相关文件放到同一个文件夹中,所以更多的时候使用相对路径。
除了上述伪指令,GNU汇编器还有其他常用伪指令。
(1).section伪指令:用户可以通过.section伪指令来自定义一个段。例如:
(2).global伪指令:用来定义一个全局符号。例如:
(3).extern伪指令:用来定义一个外部符号,其语法格式为“.extern_symbol”,声明symbol为外部函数,调用的时候可以遍历所有文件找到该函数并使用它。例如:
(4).align伪指令:可以通过添加填充字节使当前位置满足一定的对齐方式,其语法格式为“.align[exp[,fill]]”。其中,exp为0~16之间的数字,表示下一条指令对齐至2 exp 位置,若未指定,则将当前位置对齐到下一个字的位置;fill给出为对齐而填充的字节值,可省略,默认为0x00。例如:
(5).end伪指令:声明汇编文件的结束。
此外,还包含有限循环、宏定义和宏调用等伪指令,具体可参考GNU汇编语法文档。