嵌入式程序设计

这一章开始嵌入式程序设计,首先了解伪指令,然后进一步了解汇编的格式,最后应用汇编程序以及汇编语言与C/C++的混合编程


伪指令

伪指令是汇编程序的 “辅助工具”,无对应机器码,仅在汇编阶段生效,核心作用是帮编译器完成程序准备工作。

并且最后由汇编器转为几条具体的 CPU 能识别的二进制码,在运行阶段不可见

伪操作:即伪指令完成的操作

两大分类逻辑,本质是 “按功能” 和 “按适用指令集” 的区别:

分类方式 具体类别 对应典型伪指令
按 “功能” 分类(通用逻辑) 1. 符号定义伪指令(定义常量 / 变量)2. 数据定义伪指令(分配内存 / 初始化)3. 汇编控制伪指令(控制汇编流程)4. 宏指令(代码复用)5. 其他伪指令(段定义、符号导入导出等) 1. EQU/GBLA2. DCB/DCD3. IF/WHILE4. MACRO/MEND5. AREA/IMPORT
按 “适用指令集” 分类(ARM/Thumb) 1. 通用伪指令(所有指令集都能用)2. 与 ARM 指令相关的伪指令(仅 ARM 指令集用)3. 与 Thumb 指令相关的伪指令(仅 Thumb 指令集用) 1. AREA/END/EQU2. ADR(ARM 版)/LDR(ARM 版)3. ADR(Thumb 版)/NOP(Thumb 版)

通用伪指令

通用伪指令是 ARM 汇编中所有指令集(ARM/Thumb)都能通用的核心伪指令,也是嵌入式汇编编程的基础。

又分为几类指令

  1. 符号定义伪指令:给 “变量 / 寄存器组” 起名字
  2. 数据定义伪指令:给数据 “分配内存 + 初始化”
  3. 汇编控制伪指令:控制 “汇编过程” 的流程
  4. 其他常用伪指令:汇编程序的 “基础配置”

符号定义伪指令

用于定义ARM汇编程序中的变量、对变量赋值以及定义寄存器的别名等,即给汇编程序里的 “数据 / 寄存器组” 起好记的名字,并且给数据赋值

  1. 定义全局变量:GBLA/GBLL/GBLS

    “全局”= 整个汇编程序(不管多少个文件)都能访问,所以在整个程序范围内变量名必须惟一

    GBLA/GBLL/GBLS 变量名(A = 数字(默认为0)、L = 逻辑(默认为False)、S = 字符串(默认为空串))

    GBLA num1  ; 定义全局数字变量num1,初始值0
    num1 SETA 0xabcd  ; 给num1赋值为十六进制0xabcd(也就是十进制43981)
    GBLL l2    ; 定义全局逻辑变量l2,初始值F
    l2 SETL {FALSE}  ; 赋值为假(逻辑变量只能存TRUE/TRUE,加{}是格式要求)
    GBLS str3  ; 定义全局字符串变量str3,初始值空串
    str3 SETS "Hello!"  ; 赋值为字符串“Hello!”
    
  2. 定义局部变量:LCLA/LCLL/LCLS

    “局部”= 只在特定范围(比如宏、函数)内有效,出了这个范围就 “消失”,局部变量名只需要在自己的作用范围内唯一

    LCLA/LCLL/LCLS 局部变量名(和全局变量的 A/L/S 规则完全一样)

    MACRO  ; 宏开始(相当于定义一个“代码模板”,名字叫TEST)
    TEST   ; 宏名
      LCLA num1  ; 宏内定义局部数字变量num1,仅在TEST宏里有效
      LCLL l2    ; 宏内定义局部逻辑变量l2
      LCLS str3  ; 宏内定义局部字符串变量str3
      num1 SETA 0xabcd  ; 给局部变量赋值
      l2 SETL {FALSE}
      str3 SETS "Hello!"
      ...  ; 宏内其他代码
    MEND  ; 宏结束
    
  3. 变量赋值:SETA/SETL/SETS

    给已经定义好的变量(全局 / 局部都可以)“赋值”

    变量名 SETA/SETL/SETS 表达式(必须先定义变量,再赋值,不能反过来)

    LCLA num1  ; 第一步:先定义局部数字变量num1
    num1 SETA 0x1234  ; 第二步:给num1赋值为0x1234(数字)
    LCLS str3  ; 第一步:定义局部字符串变量str3
    str3 SETS "Hello!"  ; 第二步:赋值为字符串
    
  4. 寄存器列表定义:RLIST

    给一组寄存器起一个统一的名字,方便批量操作

    名称 RLIST {寄存器列表},寄存器列表里可以写单个寄存器(R0)、连续寄存器(R0-R3),用逗号分隔

    不管列表里的顺序如何,都会按寄存器编号从低到高访问

    pblock RLIST {R0-R3,R7,R5,R9}  ; 给这组寄存器起名为pblock
    ; 后续用LDM指令批量读取:不用写{R0-R3,R5,R7,R9},直接写pblock
    LDMIA SP!, pblock  ; 从栈(SP)里批量加载数据到pblock对应的寄存器组
    

数据定义伪指令

为数据分配存储单元,同时初始化

  • DCB 字节分配
  • DCW/DCWU 半字(2字节)分配
  • DCD/DCDU 字(4字节)分配
  • DCQ/DCQU 8个字节分配
  • DCFS/DCFSU 单精度浮点数分配
  • DCFD/DCFDU 双精度浮点数分配
  • SPACE 分配一块连续的存储单元
  • FIELD 定义一个结构化的内存表的数据域
  • MAP 定义一个结构化的内存表首地址

“U 后缀”(如 DCWU、DCDU):“不严格对齐”

基本语法格式:标号 <commond> 表达式

  1. DCB:1 字节 “小文件夹”(存字符 / 0-255 的数字)

    分配 1 个字节(8 位)空间,只能存「0~255 的数字」或「字符串(每个字符占 1 字节)」;

    标号 DCB 表达式(DCB 可简写为=);

    Array1 DCB 1,2,3,4,5  ; 划5个1字节空间,依次存1、2、3、4、5(相当于字节数组)
    str1 = "Your are welcome!"  ; 划一串1字节空间,每个字符占1字节(Y占1字节、o占1字节…)
    
  2. DCW/DCWU:2 字节 “中文件夹”(存半字整数)

    分配 2 个字节(16 位)空间,存整数(可正可负);

    标号 DCW/DCWU 表达式

    Arrayw1 DCW 0xa, -0xb  ; 划2个2字节空间,存10(0xa)、-11(-0xb),地址必为2的倍数
    Arrayw2 DCWU 0xc       ; 划1个2字节空间,存12,地址可以是任意数(比如0x101)
    
  3. DCD/DCDU:4 字节 “大文件夹”(存字整数 / 地址,最常用)

    分配 4 个字节(32 位)空间,ARM 处理器默认 “字对齐”,所以这是嵌入式里最常用的伪指令;

    标号 DCD/DCDU 表达式

    Arrayd1 DCD 1334,234,345435  ; 划3个4字节空间,存这三个整数
    Label DCD str1  ; 划1个4字节空间,存str1的内存地址(比如0x8000100),不是存字符串本身!
    
  4. DCFS/DCFSU:4 字节 “小数文件夹”(单精度浮点数)

    分配 4 个字节空间,存单精度浮点数(比如 1.23、600.0);

    标号 DCFS/DCFSU 表达式

    Arrayf1 DCFS 6E2, -9E-2  ; 划2个4字节空间,存600.0、-0.09
    Arrayf2 DCFSU 1.23,6.8E9 ; 划2个4字节空间,存1.23、6800000000.0(不对齐)
    
  5. DCFD/DCFDU:8 字节 “超大小数文件夹”(双精度浮点数)

    分配 8 个字节空间,存双精度浮点数(更高精度的小数);

    标号 DCFD/DCFDU 表达式

    Arrayf1 DCFD 6E2        ; 划8字节空间,存600.0(双精度)
    Arrayf2 DCFD 1.23,1.45  ; 划2个8字节空间,存这两个双精度数
    
  6. DCQ/DCQU:8 字节 “超大整数文件夹”(8 字节整数)

    分配 8 个字节空间,存超大整数(超出 4 字节范围的数);

    注意:DCQ不能给字符串分配空间

    Arrayd1 DCQ 234234,98765541  ; 划2个8字节空间,存这两个超大整数
    
  7. MAP+FIELD:画 “文件夹布局图”(定义结构体,不分配空间

    • MAP 可简写为^,FIELD 可简写为#
    • MAP:MAP 表达式 [, 基址寄存器](首地址 = 表达式 + 基址寄存器,基址寄存器可选);
    • FIELD:标号 FIELD 字节数
    MAP 0xF10000  ; 布局图起点=0xF10000(基址寄存器省略)
        count FIELD 4 ; count占4字节,位置=0xF10000(起点+0)
        x FIELD 4     ; x占4字节,位置=0xF10004(起点+4)
        y FIELD 4     ; y占4字节,位置=0xF10008(起点+8)
    
    MAP 0x130, R2 ; 布局图起点=0x130 + R2寄存器的值(比如R2=0x10,起点就是0x140)
    
  8. SPACE:建 “空文件夹”(分配连续空空间,初始值 0)

    分配一段连续字节空间,所有字节初始值都是 0

    SPACE 可简写为%

    标号 SPACE 表达式(表达式 = 要分配的字节数);

    freespace SPACE 1000  ; 划1000个连续字节空间,全初始化为0(用来预留内存,比如栈空间)
    

汇编控制伪指令

指引汇编程序的执行流程。

  1. MACRO、MEND、MEXIT:宏定义的开始与结束、从宏中退出。
  2. IF、ELSE、ENDIF:根据逻辑表达式的成立与否决定是否在编译时加入某个指令序列。
  3. WHILE、WEND:根据逻辑表达式的成立与否决定是否循环执行这个代码段。

宏是提前定义好的可复用代码片段;调用时汇编器会把模板里的代码原封不动 “复制粘贴” 到调用位置,不用重复写相同代码。类比于子程序,宏就是内联函数,而子程序就是普通的函数

对比维度 宏(MACRO/MEND) 子程序(BL 调用)
执行阶段 汇编阶段:直接展开代码(复制粘贴) 运行阶段:CPU 跳转到子程序地址执行
开销 无额外开销(不用保护现场、不用传参寄存器) 有开销(BL 指令要保存 LR、子程序要保护 R4-R11)
存储空间 调用几次就复制几次代码,占用空间多 只存一份代码,调用时跳转,占用空间少
适用场景 代码短、参数多(比如 3-5 行代码,传 4 个以上参数) 代码长、复用次数多(比如几十行代码,传参少)
  1. 宏(MACRO/MEND/MEXIT):汇编版 “代码模板”

    把一段常用代码 “打包命名”,调用时汇编器会把这段代码原封不动复制粘贴到调用位置

    $ label:可选。在宏指令被展开时,label会被替换成相应的符号,通常是一个标 号。在一个符号前使用$表示程序被汇编时将使用相应的值来替代$后的符号。

    MACRO #宏开始 
        [$ label] macroname(宏名) { $ parameter1, $ parameter,…… } #参数列表
        指令序列
    MEND #宏结束
    
    MACRO                ; 宏开始
        $DATA1 MAX $N1, $N2  ; 宏名MAX,动态标号$DATA1,参数$N1、$N2
        ; 以下是宏的核心语句段(示例里的“语句段”)
        CMP $N1, $N2         ; 比较$N1和$N2(汇编时替换成实际值)
        MOVGT R0, $N1        ; 若$N1>$N2,R0=$N1
        MOVLE R0, $N2        ; 若$N1≤$N2,R0=$N2
    MEND                 ; 宏结束
    
    ; 调用宏:
    RESULT MAX 10, 20    ; $DATA1=RESULT,$N1=10,$N2=20
    
  2. 条件编译(IF/ELSE/ENDIF):汇编版 “按需编译”

    汇编器根据 “逻辑表达式” 的真假,只编译其中一段代码汇编阶段完成),不是运行时的条件判断!

    当逻辑表达式=真时,执行语句段1,否则执行语句段2。ELSE及语句段2可以没有,没有则直接跳过

    IF 逻辑表达式
        语句段1
    ELSE
        语句段2
    ENDIF
    
    IF R0=0x10          ; 汇编器判断:R0是否等于0x10(注意:这里的R0是汇编时的常量,不是运行时寄存器!)
        ADD R0, R1, R2      ; 真→编译这句,假→跳过
    ELSE
        ADD R0, R1, R3      ; 假→编译这句
    ENDIF
    
  3. 循环编译(WHILE/WEND):汇编版 “循环复制代码”

    汇编器根据 “逻辑表达式” 的真假,循环复制 “语句段” 到代码里(汇编阶段完成),直到表达式为假,替代重复写多行相同代码。

    WHILE 逻辑表达式
        语句段
    WEND
    
    GBLA Cou1           ; 定义全局数字伪变量Cou1(汇编阶段用)
    Cou1 SETA 1         ; 赋值1(汇编阶段的赋值)
    WHILE Cou1<10       ; 汇编器判断:Cou1<10?
        ADD R1, R2, R3      ; 真→复制这句代码到当前位置
        Cou1 SETA Cou1+1    ; 汇编阶段:Cou1加1(更新循环变量)
    WEND                ; 循环结束→回到WHILE重新判断
    

汇编控制伪指令:

  1. 都是汇编阶段生效:只影响 “汇编器编译代码” 的过程,不生成运行时的机器码;
  2. 都是简化代码编写:宏 = 复用代码、IF = 按需编译、WHILE = 循环复制,本质都是减少重复代码;
  3. 都和 “运行时无关”:不要混淆成 C 的函数 /if/while——C 的是 “运行时执行逻辑”,汇编的是 “编译时处理代码”。

其他伪指令

  1. ALIGN:内存 “对齐器”(避免 CPU 访问内存出错 / 变慢)

    通过填充空字节,让当前内存地址满足 “指定的对齐规则”

    ALIGN [表达式[,偏移量]]

    B START
    ADD R0, R1, R2
    DATA1 DCB "abcde"  ; 分配5个字节,地址到0x105(假设起始地址0x100)
    ALIGN 4            ; 填充3个空字节,让下一个地址到0x108(4的倍数)
    START
        LDR R5, [R6] ; START的地址=0x108,满足4字节对齐
    
  2. AREA:程序 “分段器”(把代码 / 数据归类)

    定义汇编程序的 “段”(代码段 / 数据段)

    AREA 段名 属性,……

    一个程序至少包含一个段,可以有多个数据段,多个代码段。

    AREA test, CODE, READONLY  ; 定义名为test的代码段,属性只读
    AREA data_buf, DATA, READWRITE, ALIGN=8  ; 定义数据段,可读写,8字节对齐
    
  3. CODE16/CODE32:指令集 “标记器”(区分 ARM/Thumb 指令)

    告诉汇编器:“后面的代码是 16 位 Thumb 指令(CODE16)/32 位 ARM 指令(CODE32)”,仅标记指令类型,不切换处理器状态

    CODE32  ; 标记后面是32位ARM指令
    AREA ||.text||, CODE, READONLY
    LDR R0, =0x8500+1  ; ARM指令:加载地址到R0
    BX R0              ; ARM指令:跳转并切换到Thumb状态(地址最后1位=1触发切换)
    
    CODE16  ; 标记后面是16位Thumb指令
    ADD R3, R3, 1      ; Thumb指令:R3=R3+1(Thumb指令更精简)
    END
    
  4. ENTRY:程序 “入口标记”(告诉编译器从哪开始执行)

    指定汇编程序的执行入口

    • 整个程序(所有源文件)至少有 1 个 ENTRY;
    • 一个源文件最多 1 个 ENTRY,也可以没有;
    • 多个 ENTRY 时,最终入口由链接器指定。
    AREA subrout, CODE, READONLY
        ENTRY  ; 标记程序入口
    start  ; 入口标号
        MOV r0, #10  ; 从这行开始执行
        MOV r1, #3
        BL doadd    ; 调用子程序
    stop
        MOV r0, #0x18 ;正常返回控制状态的入口参数
        LDR r1, = 0x20026 ;
        SWI 0x123456 ;ARM中断指令SWI
    doadd
        ADD r0, r0, r1 ;子程序代码
        MOV pc, lr ;子程序返回
    END ;程序结束
    
  5. END:源文件 “结束标记”(告诉编译器 “代码到这完了”)

    • 每个源文件最后一行必须写 END;
    • 仅标记文件结束,和程序运行结束无关
    AREA constdata, DATA, READONLY
        DCB "hello"  ; 数据定义
    END  ; 源文件结束,后面写任何代码都无效
    
  6. EQU:常量 “别名器”(汇编版 #define)

    给数字、地址、寄存器值等起 “别名”

    关键字,可用*代替

    名称 EQU 表达式 [,类型]
    
    num1 EQU 1234        ; 定义num1=1234(等价#define num1 1234)
    addr5 * str1+0x50  ; addr5=str1的地址+0x50
    d1 EQU 0x2400, CODE32  ; d1=0x2400,且该地址是32位ARM指令
    
  7. GET/INCLUDE:文件 “包含器”(汇编版 #include)

    将其他汇编源文件(.s) 包含到当前文件,汇编时把被包含文件的代码 “复制粘贴” 到当前位置,等价于 C 的#include

    • 只能包含.s汇编源文件,包含二进制 / 数据文件用 INCBIN;
    GET 文件名
    AREA mycode, DATA, READONLY
        GET E:\code\prog1.s  ; 包含绝对路径的prog1.s
        GET prog2.s          ; 包含当前目录的prog2.s
    END
    
    INCBIN 文件名
    AREA constdata,DATA,READONLY
        INCBIN data1.dat ;源文件包含文件data1.dat
        INCBIN E:\DATA\data2.bin ;源文件包含文件E:\DATA\data2.bin
    END
    
  8. INCBIN:二进制文件 “嵌入器”

    二进制文件(.bin)、数据文件(.dat) 等原封不动嵌入当前文件,汇编时不修改文件内容,直接存到内存中。

    INCBIN 文件名
    AREA constdata, DATA, READONLY
        INCBIN data1.dat    ; 嵌入data1.dat(文本/数据文件)
        INCBIN E:\DATA\data2.bin  ; 嵌入二进制文件data2.bin
    END
    
  9. IMPORT/EXPORT:符号 “跨文件引用器”(汇编版 extern)

    IMPORT:导入外部符号(用别人定义的); EXPORT:导出全局符号(给别人用);其中可用 GLOBAL 代替 EXPORT;

    IMPORT 标号 [, WEAK]
    AREA mycode, CODE, READONLY
        IMPORT _printf  ; 引用其他文件的_printf函数(比如C的printf)
    ...
    END
    
    EXPORT 标号 [, WEAK]
    AREA ||.text||, CODE, READONLY
    main PROC  ; 定义main函数
    ...
    ENDP
    EXPORT main  ; 导出main,其他文件可IMPORT main并调用
    END
    

与ARM指令相关的伪指令

  • 通用伪指令:不生成任何机器码,仅告诉汇编器 “怎么处理代码”;
  • 这 4 个伪指令:汇编时被编译器替换成 1~2 条真正的 ARM 机器指令(如 ADD/SUB/MOV/LDR),但它们本身不是 CPU 能直接执行的指令,所以仍属于 “伪指令”。

  • ADR:小范围地址读取(单指令替换)

    把「基于 PC / 寄存器的小范围偏移地址」加载到目标寄存器,编译器只会用1 条 ADD/SUB 指令实现,超出范围直接编译报错。

    范围是「相对当前指令的偏移」,且和地址是否 “字对齐” 有关(字对齐 = 地址是 4 的倍数):

    • 非字对齐:-255 ~ 255 字节;
    • 字对齐:-1020 ~ 1020 字节
    ADR{<cond>} <Rd>,< expr>;
    LOOP 
        MOV R1,#0xF0   ; 假设这条指令地址=0x100
    ADR R2,LOOP         ; 要把LOOP(0x100)加载到R2
    ; 此时PC=0x100+8=0x108(ADR指令地址是0x104,+8=0x108)
    ; 编译器计算:R2 = PC - 0xC → 0x108 - 12 = 0x100(刚好是LOOP地址)
    ; 最终ADR被替换成:SUB R2, PC, #0xC
    
  • ADRL:中等范围地址读取(双指令替换)

    和 ADR 逻辑一致,但支持更大范围的地址偏移,编译器会用2 条 ADD/SUB 指令实现,超出范围编译报错。

    ADRL{<cond>} <Rd>,< expr>;
    
    start 
        MOV R0, #10        ; 假设start地址=0x200
        ADRL R4,start+60000     ; 要加载的地址=0x200 + 60000 = 0xEA60
    ; 编译器拆分偏移量,用2条ADD实现:
        ADD R4,PC,#84           ; 第一步:PC+84(先加小偏移)
        ADD R4,R4,#59904        ; 第二步:再加59904(84+59904=59988≈60000-12,修正PC偏移)
    ; 最终两条指令的总偏移=60000,把目标地址加载到R4
    
  • LDR:大范围地址读取(加载 32 位立即数 / 地址)

    把「32 位立即数」或「任意范围的地址」加载到目标寄存器,编译器会根据数值大小选择两种替换方式:

    • 方式 1:数值在 MOV/MVN 范围内 → 替换成 MOV/MVN(无内存开销);
    • 方式 2:数值超出范围 → 把数值存到「文字池」,用 LDR 指令从文字池读取(需注意 PC 到文字池的偏移≤4KB)。
    LDR{<cond>} <Rd>,< =expr/label-expr >;
    
    ;将0xFF 读取到R1中
    LDR R1,=0xFF
    汇编后会得到:
    MOV R1,0xFF
    
    ;将0xFFF读取到R1中。
    LDR R1, =0xFFF
    ;汇编后将得到:
    LDR R1,[PC,OFFSET_TO_LPOOL]
    
    LTORG ;声明数据缓冲池
    LPOOL DCD 0xFFF ;0xFFF放在数据缓冲池中
    
    func1
    LDR r1, =0x55555555  ; 替换成LDR R1, [PC, #offset到Literal Pool 1]
    MOV pc, lr           ; 子程序返回(LTORG放在返回指令后,避免数据被当指令执行)
    LTORG                ; Literal Pool 1:存放0x55555555(DCD 0x55555555)
    
    #LTORG功能:声明一个数据缓冲池(文字池)的开始
    
  • NOP:空操作伪指令

    不执行任何有效操作,仅占用 1 个机器周期,编译器会替换成无副作用的 ARM 指令(如MOV R0,R0)。

与Thumb指令相关的伪指令

Thumb 指令集是 ARM 的 16 位精简指令集,仅能操作 R0~R7(低端寄存器),且寻址 / 偏移范围更严格 —— 这 3 个伪指令必须出现在 Thumb 程序段(用 CODE16 标记)

  1. ADR 伪指令:小范围地址加载(仅 R0~R7,偏移≤1KB)

    把 “基于 PC 向前偏移的字对齐地址” 加载到 R0~R7 的专用工具

    ADR{cond} Rd, 语句标号+数值表达式
    
    CODE16  ; 标记进入Thumb程序段(必须!否则ADR伪指令报错)
    AREA ThumbADR, CODE, READONLY
    ENTRY
    START
        ADR R0, LOOP        ; 核心:把LOOP的地址加载到R0(R0属于R0~R7,符合要求)
        ADR R1, LOOP+0x40*2 ; LOOP+0x80(128字节),≤1KB,且字对齐(0x80是4的倍数)
        ALIGN 4             ; 确保LOOP地址字对齐(满足ADR的地址要求)
    LOOP
        ADD R2, R0, R1      ; Thumb指令:R2=R0+R1(R2也在R0~R7范围内)
    
  2. LDR 伪指令:加载常量 / 地址到低端寄存器

    把 32 位常量或任意地址加载到 R0~R7 的工具

    LDR{cond} Rd, = 数值表达式 ;加载数字常量 
    LDR{cond} Rd, = 语句标号+数值表达式 ;加载地址
    
    CODE16  ; 必须在Thumb程序段
    AREA ThumbLDR, CODE, READONLY
    ENTRY
    START
        ; 示例1:加载0xFF(8位立即数,Thumb的MOV能表达)
        LDR R1, =0xFF       ; 汇编器自动替换成Thumb指令:MOV R1, #0xFF
        ; 示例2:加载START地址(必用LDR+数据缓冲区)
        LDR R1, =START      ; 汇编器处理步骤:
                            ; 1. 把START的地址(如0x8000)存到数据缓冲区;
                            ; 2. 替换成Thumb指令:LDR R1, [PC, #4](PC+4指向缓冲区);
        ALIGN 4             ; 数据缓冲区(文字池):存放START的地址0x8000
        DCD 0x8000          ; 缓冲区里的地址数据
    
  3. NOP 伪指令:空操作

    NOP 是无任何有效操作的伪指令,汇编时会被替换成 Thumb 的无效指令(如MOV R0, R0

汇编语言的语句格式

  • ARM汇编语言程序一般由几个段组成,每个段均由AREA伪操作定义。
  • 段可以分为多种,如代码段、数据段、通用段。
  • 每个段有不同的属性,如代码段的默认属性为READONLY,数据段的默认属性为READWRITE。

书写格式

[标号] 指令/伪指令 [;语句的注释]

一行汇编代码 = 「可选标号」 + 「核心指令 / 伪指令」 + 「可选注释」,三部分按顺序排列

  • 标号必须顶格写,前面不能留空格,后面不能有:
  • ARM汇编器对标识符的大小写敏感。
组成部分 核心规则(文档重点)
标号(可选) ① 必须顶格写(前面无任何空格);② 后面不能加冒号(:);③ 大小写敏感(Loop≠loop)
指令 / 伪指令 ① 必须在标号后(无标号则顶格);② 大小写敏感(MOV≠mov);③ 是 CPU 执行的指令(如 SUBS)或给汇编器的伪指令(如 AREA)
注释(可选) ① 以分号(;)开头;② 分号后内容汇编器完全忽略,仅给人看;③ 可跟在指令后,也可单独占一行

标号

标号是给 “地址 / 变量 / 常量” 起的符号名

  1. 命名规则:默认以字母开头,由字母、数字、下划线组成(如main123data_buf);
  2. 特殊情况:当标号代表「地址」时,可以数字开头(如123addr),作用范围是 “当前段” 或 “下一个 ROUT 伪指令前”(ROUT 伪指令用于限定标号作用域,类似函数内的局部变量);
  3. 标号本质是 “地址 / 数值的别名”—— 比如loop代表某条指令的地址,num1代表某个常量的数值。

  4. 地址标号

    地址标号专门代表「内存地址」(指令地址 / 数据地址),是汇编跳转、寻址的核心

    程序段内:标号 = 段首地址 + 偏移量 → 用 PC(程序计数器)+ 偏移量寻址(程序相对寻址);

    映像中(整个程序):标号 = 映像首地址 + 偏移量 → 用寄存器 + 偏移量寻址(寄存器相对寻址);

    | 地址标号类型 | 地址值确定时机 | 典型用法 | 例子 | | ------------ | -------------------------------------- | -------------------- | ------------------------------------------------------------ | | 段内标号 | 汇编时确定(编译当前文件就知道地址) | 段内跳转、短距离寻址 | loop标号在当前代码段,BNE loop直接跳转到该地址 | | 段外标号 | 链接时确定(多个文件合并时才确定地址) | 跨段调用、全局函数 | EXPORT main导出的 main 标号,其他文件通过IMPORT main引用,链接时才确定 main 的最终地址 |

    loop          ; 地址标号:顶格写,无冒号,代表下面SUBS指令的地址(段内标号)
    SUBS r0, r0, #1 ;每次循环使r0=r0-1(指令不在标号行,不用顶格)
    BNE loop     ;跳转到loop标号对应的地址(程序相对寻址,汇编时确定偏移)
    
  5. 局部标号(宏内专用,可重复定义)

    局部标号是「0~99 的十进制数」(如 01、99),专门用于宏 / 短代码块内的临时跳转,可重复定义(不同宏里都能用 01),解决 “段内标号重复命名” 的问题。

    % {F|B} {A|T} N{routname}
    
    01 SUBS r0, r0, #1 ;局部标号01:代表这行SUBS指令的地址(可重复定义)
    BNE %B01 ;引用局部标号:%B01=只向后搜01标号
             ;执行逻辑:如果r0≠0,跳回01标号继续执行(r0自减到0为止)
    

汇编语言中表达式和运算符

类型 核心定义 关键特征 举例
变量 汇编阶段可修改的符号(运行时是常量) 分数字 / 逻辑 / 字符串 3 类,有全局 / 局部之分 num1 SETA 0x4f(数字变量)
常量 汇编 / 运行时都不可修改的符号 分数字 / 逻辑 / 字符串 3 类,用 EQU 定义 PI EQU 3.14(数字常量)

变量

变量形式有3种:数字变量、逻辑变量、字符串变量。

  • 串变量需要使用双引号包含。

  • $+字符串变量:在汇编时编译器将用该字符串变量的内容代替该串变量

  • $+数字变量:编译器会将该数字变量的值转换为十六进制的字符串,并用该十六进制的字符串代换$后的数字变量。
  • $+$:此时编译器将不再进行变量代换,而是把$ $看作一个 $
  • 两个|之间的$不进行变量的代换,但如果|在双引号内, 则将进行变量代换。
  • 使用.表示字符串中变量名的结束。
; 定义变量
GBLS str1; str1="bbb"
GBLL l1; l1={TRUE}
GBLA num1; num1=0x4f
; 代换字符串
str2 SETS "aaa str1:$str1. l1:$l1,a1:$num1.ccc"
; 汇编后代换结果:
str2 = "aaa str1:bbb l1:T,a1:0000004Fccc"

No1 SETA 10 
Str1 SETS "The number is $No1."

Str SETS "The character is $$"

常量

  • 常量:其值在程序运行过程中不能被改变。

数字常量;逻辑常量;字符串常量

数字表达式及运算符

用来对32 位整数做加减乘除、移位、按位运算,汇编阶段计算出结果

形式 写法示例 核心说明 实战场景
十进制 1234、56789 默认格式,直接写数字 简单数值(如计数、延时)
十六进制 0x12A、&FF00 两种写法等价,0x 更通用 外设地址、寄存器值(ARM 常用)
N 进制 2_01101111、8_54231067 N∈[2,9],2= 二进制,8= 八进制 二进制位操作、八进制权限设置
ASCII 'A'、'0' 等价于对应 ASCII 码('A'=0x41) 字符相关操作(如打印、判断)

3 类数字运算符(优先级:移位 > 算术 > 逻辑)

运算符类型 符号 / 语法 功能(大白话) 实战例子 计算结果
算术运算符 +、-、×、/、:MOD: 加减乘除 + 取余(:MOD: 是取余) 0xFF00 :MOD: 0xF(0xFF00÷0xF) 0
移位运算符 :ROL:、:ROR:、:SHL:、:SHR: 循环左移 / 右移、逻辑左移 / 右移 0x10 :ROL: 2(0x10=00010000,循环左移 2 位) 0x40(01000000)
逻辑运算符 :AND:、:OR:、:NOT:、:EOR: 按位与 / 或 / 非 / 异或 0x0F :AND: 0xF0(00001111 & 11110000) 0x00
MOV R5, #0xFF00 :MOD: 0xF :ROL: 2
; 计算步骤:
1. 0xFF00 :MOD: 0xF = 0(0xFF00 ÷ 0xF = 4096,余数0)
2. 0 :ROL: 2 = 0(0循环左移任意位还是0)
; 最终等价于:MOV R5, #0

逻辑表达式及运算符

用来判断 “条件是否成立”,结果只有 {TRUE}(真)/{FALSE}(假),仅在汇编阶段生效(不是运行时的条件判断)。

2 类逻辑运算符(优先级:关系运算符 > 逻辑运算符)

运算符类型 符号 / 语法 功能(大白话) 实战例子 结果
关系运算符 =、>、<、>=、<=、/=、<> 判断两个值的关系(/=、<> 都是不等) R6 <= R7(R6=5,R7=10) {TRUE}
逻辑运算符 :LAND:、:LOR:、:LNOT:、:LEOR: 逻辑与 / 或 / 非 / 异或(注意带 L) R5 :LAND: R6 <= R7(R5={TRUE},R6<=R7={TRUE}) {TRUE}
  • :AND: 是 “按位与”(数字运算),:LAND: 是 “逻辑与”(真假判断),千万别搞混!
IF R5 :LAND: R6 <= R7  ; 汇编器判断:R5真 且 R6<=R7真 → 整体为真
    MOV R0, #0x00      ; 编译这句
ELSE
    MOV R0, #0xFF      ; 不编译
ENDIF

字符串表达式及运算符

用来对 ≤512 字节的字符串 做长度计算、截取、拼接等操作,仅汇编阶段生效。

2 类字符串运算符(优先级:单目 > 双目)

运算符类型 符号 / 语法 功能(大白话) 实战例子 结果
单目运算符 :LEN: X 算字符串 X 的长度 :LEN: "book" 4
:CHR: X 数字 X(0~255)转 ASCII 字符串 :CHR: 65(A 的 ASCII 码) "A"
:STR: X 数字转 8 位 16 进制 / 逻辑转 T/F :STR: 10 → "0000000A";:STR: {TRUE} → "T" -
:DEF: X 判断符号 X 是否定义 :DEF: num1(num1 已定义) {TRUE}
双目运算符 X :LEFT: Y 从 X 左侧取 Y 个字符 "abc123" :LEFT: 3 "abc"
X :RIGHT: Y 从 X 右侧取 Y 个字符 "abc123" :RIGHT: 3 "123"
X :CC: Y 拼接字符串(Y 接在 X 后面) "hello" :CC: "world" "helloworld"
LCLS str
str SETS :CHR: 65 :CC: :STR: 10  ; 先转65→"A",再转10→"0000000A",拼接→"A0000000A"

寄存器 / PC 基址表达式及运算符

用来从 “寄存器 + 偏移” 的地址表达式中,拆解出寄存器编号偏移量,主要用于宏 / 条件编译。

运算符 语法 功能(大白话) 实战例子 结果
BASE :BASE: A 取地址表达式 A 中的寄存器编号 :BASE: [R1, #0x10](基址寄存器是 R1) 1(R1 的编号是 1)
INDEX :INDEX: A 取地址表达式 A 中的偏移量 :INDEX: [R1, #0x10] 0x10

宏内判断寄存器:

MACRO
$label CHECK_REG $reg_expr
    IF :BASE: $reg_expr = 5  ; 判断基址寄存器是否是R5(编号5)
        ADD $reg_expr, #0x20 ; 是R5则偏移+0x20
    ENDIF
MEND

; 调用宏:
CHECK_REG [R5, #0x10]  ; :BASE: [R5,#0x10]=5 → 执行ADD [R5,#0x10], #0x20

运算符的优先级

  • 先计算括号内,后计算括号外;
  • 在有多个操作符时,顺序和运算符有关,即按优先级运算;
  • 先单目运算,后双目运算;
  • 在相同优先权情况下,从左到右运算

image-20251216204709425

完整汇编程序框架

ARM 汇编程序的通用框架是基于段定义、入口标记、核心逻辑、退出处理的标准化结构,ENTRY/start/stop是框架的核心锚点

; ===================== 1. 段定义(必选) =====================
; 代码段:存执行指令,默认READONLY(只读,防止篡改)
AREA MainCode, CODE, READONLY  
; 可选:数据段(存变量/字符串),READWRITE(可读写)
AREA MainData, DATA, READWRITE  

; ===================== 2. 全局声明(可选) =====================
; 导出标号(让链接器/外部文件可见,如main)
EXPORT start  
; 导入外部符号(如C库函数、其他汇编文件的标号)
IMPORT _printf  

; ===================== 3. 常量/数据定义(可选) =====================
; 常量定义(汇编时替换,无内存开销)
NUM EQU 10          
; 字符串/变量定义(数据段)
src_str DCB "Hello ARM", 0  ; 字节数据(字符串)
dst_buf SPACE 100           ; 预留100字节空间

; ===================== 4. 程序入口(必选) =====================
ENTRY           ; 标记程序唯一执行起点(汇编器/链接器识别)
start          ; 入口标号(习惯用start/main,可自定义)
    ; ========== 核心业务逻辑(自定义) ==========
    MOV r0, #0  ; 示例:参数初始化
    MOV r1, #10 
    BL func     ; 调用自定义子程序

; ===================== 5. 退出处理(必选) =====================
stop           ; 退出标号(习惯用stop,可自定义)
    ; 方式1:半主机模式退出(调试/仿真环境,如Keil/DS-5)
    MOV r0, #0x18       ; 半主机退出功能码(固定)
    LDR r1, =0x20026    ; 半主机参数地址(固定)
    SWI 0x123456        ; 触发软中断,调用主机退出功能

    ; 方式2:裸机死循环(实际硬件,无操作系统)
    ; B stop            ; 无限跳转到自身,防止程序跑飞

; ===================== 6. 自定义子程序(可选) =====================
func           ; 子程序标号
    ADD r0, r0, r1      ; 示例:业务逻辑
    MOV pc, lr          ; 子程序返回(LR=返回地址)

; ===================== 7. 程序结束(必选) =====================
END             ; 标记源文件结束(汇编器停止处理)

汇编语言与C/C++的混合编程

APCS 规定了汇编和 C/C++ 之间 “怎么传参、怎么用寄存器、怎么用栈”,保证混合编程时数据能正确传递

汇编语言与C/C++的混合编程通常有3种方式:

  • 嵌入汇编:在C/C++代码中嵌入汇编指令。
  • 变量互访:在汇编程序和C/C++的程序之间进行变量的互访。
  • 相互调用:汇编程序、C/C++程序间的相互调用

  • 寄存器使用(最核心)

寄存器 APCS 命名 用途 保存规则
R0~R3 A0~A3 传递前 4 个参数、返回值 子程序无需保存,可随意修改
R4~R11 V1~V8 保存局部变量 子程序必须保存(入栈),返回前恢复
R12 ip 编译器临时寄存器 禁止手动修改
R13(SP) SP 栈指针 禁止修改,进出子程序值必须相等
R14(LR) LR 保存返回地址 调用子程序时自动赋值,返回时用MOV PC, LR
R15(PC) PC 程序计数器 禁止用作普通寄存器
  1. 数据栈使用

  2. 栈类型:满减栈(FD) ——SP 指向最后一个入栈的数据,入栈时 SP 先减 4(ARM 字对齐),再存数据;

  3. 对齐要求:8 字节对齐(保证浮点数据 / 64 位数据正确存储);
  4. 栈帧:子程序在栈中分配的区域,用于保存 R4~R11 和局部变量,返回前必须销毁(SP 恢复原值)。

  5. 参数传递

子程序类型 传参规则
参数≤4 个 依次存在 R0、R1、R2、R3
参数>4 个 前 4 个用 R0~R3,剩余参数按 “反序” 入栈(最后一个参数先入栈)
浮点参数 浮点参数按顺序处理,整数参数优先用 R0~R3,剩余入栈
  1. 返回值
返回值类型 返回方式
32 位整数 R0
64 位整数 R0+R1
>64 位 内存地址(存在 R0)

在C/C++程序中内嵌汇编指令的语法格式

在ARM的C语言程序中可以使用关键字__asm来加入一段汇编语言的程序。

// 标准格式
__asm  // 双下划线__asm是ARM编译器专用关键字(不可省略)
{
    汇编指令1 /* C风格块注释 */
    汇编指令2 // C风格行注释
    …
}
  • 可以使用表达式

  • 一条指令占用多行,使用符号\续行

  • 一行有多个汇编指令,使用;将多个指令隔开。

  • 使用C语言中的/* */或者//进行注释

  • 单行语法:

    // 标准格式
    asm ("汇编指令1 [; 汇编指令2]");  // 单下划线asm,括号内必须是字符串,多条指令用;分隔
    
  • C语言变量不要与ARM寄存器重名

C/C++与汇编语言的混合编程应用

在 C 函数中直接嵌入 ARM 汇编指令,用__asm关键字包裹,适合实现短逻辑(如字符串拷贝、寄存器操作),无需单独编写汇编文件。

限制规则 大白话解释
(1)不能直接给 PC 赋值,跳转用 B/BL 禁止写MOV PC, #0x1234,跳转只能用B LOOP/BL 子程序
(2)避免复杂 C 表达式 不要写MOV R0, a+b*c-d/2这类复杂表达式
(3)避免直接用 R12/R13/R0~R3/R14 不要手动操作这些寄存器
(4)不指定物理寄存器,让编译器分配 不要写MOV R0, #10,优先用 C 变量(如MOV ch, #10
#include <stdio.h>

// 字符串拷贝函数
void my_strcpy(const char *src, char *dest) {
    char ch;
    __asm {
        LOOP:  // 错误1修正:汇编标号必须加冒号(教材漏了)
            // 错误2修正:指令格式错误,逗号后加空格,注释修正
            LDRB ch, [src], #1  // ch = *src; src++; (教材解释写反了)
            STRB ch, [dest], #1 // *dest = ch; dest++; (教材解释写反了)
            CMP ch, #0          // 判断是否到字符串结束符'\0'
            BNE LOOP            // 非0则跳回LOOP继续拷贝
    }
}

//主函数
int main() {
    char *a = "forget it and move on!";
    char b[64] = {0};  // 初始化数组,避免乱码
    my_strcpy(a, b);
    printf("original: %s\n", a);
    printf("copyed: %s\n", b);
    return 0;
}

汇编通过IMPORT导入 C 的全局变量,通过 “先加载变量地址→再读取变量值” 的方式访问,适合汇编需要使用 C 定义的常量 / 字符串。

(1)C 文件:str.c(定义全局字符串)

// 注意:字符串末尾的'\0'可省略,C会自动补全
char *strhello = "Hello world!\n";

(2)汇编文件:hello.s(访问 strhello 并调用 printf)

; 定义代码段(||.text||是编译器兼容写法,等价于普通CODE段)
AREA ||.text||, CODE, READONLY  

; 定义main子程序(PROC/ENDP表示子程序开始/结束)
main PROC  

    ; 步骤1:保存LR到栈(调用printf后要返回,避免LR丢失)
    STMFD sp!, {lr}  

    ; 步骤2:加载strtemp的地址到R0(strtemp存储了strhello的地址)
    LDR r0, =strtemp  

    ; 步骤3:读取strtemp的值(即strhello的地址)到R0
    LDR r0, [r0]  

    ; 步骤4:调用C库函数printf(R0传参:字符串地址)
    BL _printf  

    ; 步骤5:恢复LR到PC,返回主程序
    LDMFD sp!, {pc}  

; 数据区:存储strhello的地址(DCD=定义4字节常量)
strtemp  
    DCD strhello  

; 子程序结束
ENDP  

; 导出main,让链接器识别为入口
EXPORT main  

; 导入外部符号(C定义的变量/函数)
IMPORT strhello       ; 导入C的全局变量strhello
IMPORT _main          ; 导入C的main函数(冗余,可删)
IMPORT _printf        ; 导入C库的printf函数
; 弱引用:找不到该符号也不报错(兼容编译器)
IMPORT ||Lib$$Request$$armlib||, WEAK  

; 程序结束
END

C 程序中调用汇编编写的函数

  • 汇编函数用EXPORT导出标号,供 C 调用;
  • 参数传递:前 4 个参数依次存在 R0~R3(文档示例中 strcopy 的参数 d→R0、s→R1);
  • 返回:汇编函数用MOV pc, lr返回(把 LR 的返回地址赋值给 PC)。

(1)C 文件:strtest.c

#include <stdio.h>

// 声明汇编函数(参数:d=目的串,s=源串)
extern void strcopy(char *d, const char *s);  

int main() {
    const char *srcstr = "First string – source";  // 自动补'\0'
    char dststr[64] = "Second string – destination";  // 初始化目的串

    printf("Before copying:\n");
    printf(" '%s'\n '%s'\n", srcstr, dststr);

    strcopy(dststr, srcstr);  // 调用汇编函数:dststr→R0,srcstr→R1

    printf("After copying:\n");
    printf(" '%s'\n '%s'\n", srcstr, dststr);
    return 0;
}

(2)汇编文件:scopy.s

; 定义代码段,只读
AREA SCopy, CODE, READONLY  

; 导出strcopy,供C文件调用
EXPORT strcopy  

; strcopy子程序:R0=目的串指针,R1=源串指针
strcopy  
    LDRB r2, [r1], #1  ; 读源串1字节到R2,R1自增1
    STRB r2, [r0], #1  ; 写R2到目的串,R0自增1
    CMP r2, #0         ; 判断是否到结束符'\0'
    BNE strcopy        ; 非0则跳回strcopy继续
    MOV pc, lr         ; 是0则返回(LR=调用时的返回地址)

; 程序结束
END

©OZY all right reserved该文件修订时间: 2026-05-27 09:36:00

评论区 - 04_嵌入式程序设计

results matching ""

    No results matching ""