ARM 汇编程序设计

时间:2021-10-18 00:53:25

A.5.1  文件格式
        ARM 源程序文件(即源文件)为文件格式,可以使用任一文本编辑器编写程序代码。


        在一个项目中,至少要有一个汇编源文件或C 程序文件,可以有多个汇编源文件或多个C 程序文件,或者C 程序文件和汇编文件两者的组合。

A.5.2  ARM 汇编的一些规范
        (1)汇编语句格式
        ARM 汇编中,所有标号必须在一行的顶格书写,其后面不要添加“:”,而所有指令均不能顶格书写。ARM 汇编器对标识符大小写敏感,书写标号及指令时字母大小写要一致,在ARM 汇编程序中,一个ARM 指令、伪指令、寄存器名可以全部为大写字母,也可以全部为小写字母,但不要大小写混合使用。注释使用“;”,注释内容由“;”开始到此行结束,注释可以在一行的顶格书写。
        格式:[标号] <指令|条件|S> <操作数>[;注释]
        源程序中允许有空行,适当地插入空行可以提高源代码的可读性。如果单行太长,可以使用字符“”将其分行,“”后不能有任何字符,包括空格和制表符等。对于变量的设置,常量的定义,其标识符必须在一行的顶格书写。
        汇编指令正确的例子和错误的例子如下:
        正确的例子:
        …
        Str1 SETS My string1.”     ;设置字符串变量Str1
        Count RN R0                 ;定义寄存器名Count
        USR_STACK EQU 64             ;定义常量
        START LDR R0,=0x1123456     ;R0=0x123456H
        MOV R1,#0
        LOOP
        MOV R2,#3
        …
        错误的例子:
        START MOV R0,#1             ;标号START 没有顶格写
        ABC: MOV R1,#2             ;标号后不能带:
        MOV R2,#3                     ;命令不允许顶格书写
        loop Mov R2,#3                 ;指令中大小写混合
        B Loop                         ;无法跳转到Loop 标号
        (2)标号
        在ARM 汇编中,标号代表一个地址,段内标号的地址在汇编时确定,而段外标号的地址值在连接时确定。根据标号的生成方式,可以有以下3 钟:
        基于PC 的标号
        基于PC 的标号时位于目标指令前的标号或程序中的数据定义伪指令前的标号,这种标号在汇编时将被处理成PC 值加上或减去一个数字常量。它常用于表示跳转指令的目标地址,或者代码段中所嵌入的少量数据。
        基于寄存器的标号
        基于寄存器的标号通常用MAP 和FILED 伪指令定义,也可以用于EQU 伪指令定义,这种标号在汇编时被处理成寄存器的值加上或减去一个数字常量。它常用于访问位于数据段中的数据。
        绝对地址
        绝对地址是一个32 为的数字量,它可以寻址的范围为0~232-1,可以直接寻址整个内存空间。
        (3)局部标号
        局部标号主要用于局部范围代码中,在宏定义也是很有用的。局部标号是一个0~99 之间的十进制数字,可重复定义,局部标号后面可以紧接一个通常表示该局部变量作用范围的符号。局部变量的作用范围为当前段,也可以用伪指令ROUT 来定义局部标号的作用范围。
        局部标号定义格式:N{routname}
        其中:N 局部标号,为0~99。
        routname 局部标号作用范围的名称,由ROUT 伪指令定义。
        局部标号引用格式:
        %{F|B}{A|T} N{routname}
        其中: % 表示局部标号引用操作。
        F 指示编译器只向前搜索
        B 指示编译器只向后搜索
        A 指示编译器搜索宏的所有嵌套层次
        T 指示编译器搜索宏的当前层
        如果F 和B 都没有指定,则编译器先向前搜索,再向后搜索。如果A 和T 都没有指定,编译器搜索所有从宏的当前层次到宏的最高层次,比当前层次的层次不再搜索。
        如果指定了routname,编译器向前搜索最近的ROUT 伪指令,若routname 与该ROUT伪指令定义的名称不匹配,编译器报告错误,汇编失败。
        示例如下:
        routintA ROUT
        …
        3routineA
        BEQ %4routineA
        BGE %3
        4routineA
        …
        otherstuff ROUT
        …
        (4)符号
        在ARM 汇编中,符号可以代表地址、变量、数字常量。当符号代表地址时又称为标号,符号就是变量的变量名、数字常量的名称、标号,符号的命名规则如下:
        a. 符号由大小写字母、数字以及下划线组成;
        b. 除局部标号以数字开头外,其它的符号不能以数字开头;
        c. 符号区分大小写,且所有字符都是有意义的;
        d. 符号在其作用域范围你必须是唯一的;
        e. 符号不能与系统内部或系统预定义的符号同名;
        f. 符号不要与指令助记符、伪指令同名。
        (5)常量
          数字常数
        数字常量有三种表示方式:
        十进制数,如:12,5,876,0。
        十六进制数,如0x4387,0xFF0, 0x1。
        n 进制数,用n-XXX 表示,其中n 为2~9,XXX 为具体的数。如2-010111,8-4363156等。
        字符常量
        字符常量由一对单引号及中间字符串表示,标准C 语言中的转义符也可使用。如果需要包含双引号或“$”,必须使用“”和$$代替。如下示例:
        Hello SETS “Hello World!”
        Errorl SETS “The parameter ““VFH””error$$2”
        布尔常量
        布尔常量的逻辑真为{TRUE},逻辑假为{FALSE}。如下示例:
        testno SETS {FALSE}
        (6)段定义
        ARM 汇编程序设计采用分段式设计,一个ARM 源程序至少需要一个代码段,大的程序可以包含多个代码段及数据段。
        ARM 汇编程序经过汇编处理后生成一个可执行的映象文件,该文件通常包含下面3部分内容:
  • 一个或多代码段。代码段通常是只读的。
  • 零个或多个包含初始化值的数据段。这些数据段通常是可读写的。
  • 零个或多个不包含初始值的数据段。这些数据被初始化为0,通常中可读写的。

        连接器根据一定的规则将各个段安排到内存中的相应位置。源程序中段之间的相邻关系与执行的映象文件中段之间的相邻关系并不一定相同。
        代码段的例子如下:
        AREA Hello,CODE,READONLY ;声明代码段Hello
        ENTRY ;程序入口(调试用)
        START MOV R7,#10
        MOV R6,#5
        ADD R6,R6,R7 ;R6=R6+R7
        B ;死循环
        END
        每一个汇编文件都要以END 结束,包括*INC 文件,否则编译会有警告。
        数据段的例子如下:
        AREA DataArea,DATA,NOINIT,ALLGN=2
        DISPBUF SPACE 100
        RCVBUF SPACE 100
        …
        (7)宏定义及其作用
        使用宏定义可以提高程序的可读性,简化程序代码和同步修改。ARM 宏定义与标准C的#define 相似,只在源程序中进行字符代换。宏定义从MACRO 伪指令开始,到MEND 结束,并可以使用参数。
        宏要先定义,然后再使用。使用时直接书写宏名,并根据对应的宏定义格式设置输入参数或书写标号等。当源程序被汇编时,汇编编译器将展开每一个宏调用,用宏定义体代替程序中的宏调用,并使用实际的参数值代替宏定义时的形式参数。
        程序程序清单见后,程序中定义了一个宏CALL,用于调用子程序,调用时设置所要调用的子程序名$Function 及两个入口参数$dat1 和$dat2。由于宏定义体中使用的是MOV 指令,所以$dat1 参数只能为8 位图的立即数或通用寄存器。
        宏应用的例子:
        …
        MACRO ;宏定义
        CALL $Function,$dat1,$dat2 ;宏名称为CALL,带3 个参数
        IMPORT $Function ;声明外部子程序
        MOV R0,$dat1 ;设置子程序参数,R0=$dat1
        MOV R1,$dat2
        BL Function ;调用子程序
        MEND ;宏定义结束
        …
        CALL FADD1,#3,#2 ;宏调用
        …
        汇编预处理后,宏调用将被展开,程序清单如下:
        …
        IMPORT FADD1
        MOV R0,#3
        MOV R1,#3
        BL FADD1
        …

A.5.3  子程序的调用
        使用BL 指令进行调用,该指令会把返回的PC 值保存在LR,示例如下:
        …
        BL DLEAY
        …
        DELAY …
        MOV PC,LR
        当子程序执行完毕后,使用MOV、B/BX、STMFD 等指令返回,当然STMFD 要与LDMFD 配套使用,子程序返回的方法:
        MOV PC,LR
        或 B LR
        或 BX LR
        或 STMFD SP!{R0-R7,PC }
        ARM7TDMI(-S)是没有BLX 指令的,但是可以通过几条程序实现其功能,模拟BLX 指令如下:
        ADR R1,DELAY+1
        MOV LR,PC ;保存返回地址到LR
        BX R1 ;跳转并切换指令集
        …

A.5.4  数据比较跳转
        汇编程序可以使用CMP 指令进行两个数据比较,然后调高相应的ARM 条件码,实现跳转。代码例子如下:
        CMP R5,#10
        BEQ DOEQUAL ;若R5 为10,则跳转到DOEQUAL
        …
        CMP R1,R2
        ADDHI R1,R1,#10 ;若R1>R2,则R1=R1+10
        ADDLS R1,R1,#5 ;若R1<=R2,则R1=R1+5
        …
        ANDS R1,R1,#0x80 ;R1=R1&0x80,并设置相应标志位
        BNE WAIT ;若R1 的d7 位为则跳转到WAIT

A.5.5  循环
        下面的代码为循环程序的例子。例子指定循环次数,每循环一次进行减1 操作,并判断结果是否为0,若为0 则退出循环。
        MOV R1,#10
        LOOP … ;循环体
        SUBS R1,R1,#1
        BNE LOOP
        …

A.5.6  数据块复制
        程序可以使用存储器访问指令LDM/STM 指令进行读取和存储,数据块复制示例如下:
        LDR R0,=DATA_DST ;指向数据目标地址
        LDR R1,=DATA_SRC ;指向数据源地址
        MOV R10,#10 ;复制数据个数为10*N 个字
        LOOP LDMIA R1!,{R2-R9} ;N 为LDM/STM 指令操作数据个数
        STMIA R0!,{R2-R9}
        SUBS R10,R10,#1
        BNE LOOP
        …

A.5.7  栈操作
        ARM 使用存储器访问指令LDM/STM 实现栈操作,用于子程序寄存器保存。注意,使用堆栈时,要先分配好堆栈空间,设置好寄存器R13(即堆栈指针SP),否则操作失败。
        OUTDAT
        STMFD SP!{R0-R7,LR}
        …
        BL DELAY
        …
        LDMFD SP!{R0-R7,PC}

A.5.8  特殊寄存器定义及应用
        基于ARM 核的芯片一般有片内外设,它们通过其特殊寄存器访问。片内外设的使用示例如下:
        WDTC EQU 0xE000000 ;寄存器定义
        …
        LDR R0,=WDTC
        MOV R1,#0x12
        STR R1,[R0] ;WDTC=0x12
        散转功能
        散转是汇编程序常用的一种算法,其示例如下:
        CMP R0,#MAXINDEX ;判断索引号是否超出最大索引值
        ADDLO PC,PC,R0,LSL #2 ;若没有超出,则跳转到相应位置
        B ERROR ;若已经超出,则进行出错处理
                           ;散转表,对应索引号为0~N
        B FUN1
        B FUN2
        B FUN3
        …

A.5.9  查表操作
        查表操作是汇编程序常用的一种操作,其示例如下:
        …
        LDR R3,=DISP_TAB ;取得表头
        LDR R2,[R3,R5,LSL #2] ;根据R5 的值查表,取出相应的值
        …
        ;下表为0--F 的字模
        DISR_TAB DCD 0xC0,0xF9,0xA4,0x99,0x92
        DCD 0x82,0xF8,0x80,0x90,0x88,0x83
        DCD 0xC6,0xa1,0x86,0x8E,0xFF

A.5.10  长跳转
        ARM 的B 和BL 指令不能全空间跳转,但通过对PC 进行赋值,实现32 位地址的跳转/调用,示例如下:
        ADD LR,PC,#4 ;保存返回地址,即RET_FUN
        LDR PC,[PC,#-4] ;跳转到LADR_FUN
        DCD LADR_FUN
        RET_FUN …
        也可使用伪指令LDR PC,=LADR_FUN 实现长跳转。

A.5.11  对信号量的支持
        ARM 提供一条内存与寄存器交换的指令SWP 用于支持信号量的操作,实现系统任务之间的同步或互斥,其使用的例子如下:
        DISP_SEM EQU 0x40002A00
        …
        DISP_WAIT MOV R1,#0
        LDR R0,=DISP_SEM
        SWP R1,R1[R0] ;取出信号量,并设置其为0
        CMP R1,#0 ;判断是否有信号
        BEQ DISP_WAIT ;若没有信号,则等待
        …

A.5.12  伪指令使用
        LDR 伪指令和NOP 伪指令应用例子代码如下:
        LDR R1,=0x12345678 ;加载32 位立即数
        LDR R0,=LDE_TAB ;加载标号地址
        NOP ;空指令
        B ;死循环

A.5.13  一个完整的例子
        下面是汇编程序完整的例子:
        ABC EQU 0x12
        ;声明一个代码段Example
        AREA Example,CODE,READONLY
        ENIRY
        CODE32
        ADR R0,Thumb_START+1 ;装载地址,并设置d0 位为1
        BX R0 ;切换到Thumb 状态
        CODE16 ;声明16 位代码(Thumb)
        Thumb_START
        MOV R1,#ABC
        ADD R1,R1,#0x10
        B Thumb_START
        END

A.5.14  外设控制
        在32 位的ARM 核芯片中,其外设的控制寄存器中,一般会设置“置位/复位”寄存器,这样可以方便的实现位操作,而不会影响其它位,如        IOSET=0x01 只会将P0.1 的置位,而其它I/O 状态不变,另外,ARM 存储/保存指令具有前偏移功能,所以对外设的控制寄存器进行操作时可使用此功能,避免了每次都加载寄存器地址的操作示例如下:
        LDR R0,=GPIO_BASE
        MOV R1,#0x00
        STR R1,[R0,#0x04] ;IOSET=0x00
        MOV R1,#0x10
        STR R1,[R0,#0x0C] ;IOCLR=0x101

A.5.15  三级流水线介绍
        ARM7TDM(-S)使用三级流水线执行指令,第一阶段从内存中取回的指令,第二阶段开始解码,而第三阶段实际执行指令。故此,程序计数器总是超出当前执行的指令两条指令。(在为跳转指令计算偏移量时必须计算在内)。因为有这个流水线,在跳转时丢失2 个指令周期(因为要重新添满流水线)。所以最好利用条件执行指令来避免浪费周期。
        条件跳转示例:
        …
        CMP R0,#0
        BEQ LOOP1
        MOV R1,#0x10
        MOV R2,#0x88
        LOOP1
        …
        可以写为更有效的:
        …
        CMP R0,#0
        MOVNE R1,#0x10
        MOVNE R2,#0x88
        …