文章目录
1. ATPCS 规则
2. 汇编和C程序传递参数
汇编程序向 C 程序的函数传递参数
C 程序返回结果给汇编程序
代码示例
3. C 函数使用栈
4. C 语言中读写寄存器
在嵌入式开发中,经常需要在 C 程序和 ARM 汇编程序之间进行相互调用。为了保证这些调用的正确性和兼容性,ARM 提出了 ATPCS(ARM-Thumb Procedure Call Standard)规范。该规范定义了函数调用时的基本规则和寄存器使用约定。
1. ATPCS 规则
ATPCS 是 ARM 和 THUMB 指令集过程序调用的规范,它规定了函数调用时如何传递参数、如何获取参数以及如何返回值。
寄存器使用规则:
- 在函数中,通过寄存器 R0-R3 传递参数,被调用的函数在返回前无须恢复寄存器 R0-R3 的内容。
- 在函数中,通过寄存器 R4~R11 保存局部变量。
- 寄存器 R12 用作函数间的 scratch 寄存器,即临时寄存器。
- 寄存器 R13 用作堆栈指针,即 SP(Stack Pointer),在函数中寄存器 R13 不能用于其他用途。寄存器 SP 在进入函数时的值和退出函数时的值必须相等。
- 寄存器 R14 用于存放返回地址,即 LR(Link Register),它用于存放调用函数的返回地址。函数返回时,CPU 会跳转到 LR 指向的地址继续执行调用函数。
- 寄存器 R15 是程序计数器,即 PC(Program Counter),用于指向当前指令的地址,指令执行时自动递增。
示例代码
假设一个 C 函数 add,它接收两个整数参数并返回它们的和:
// add.c
int add(int a, int b) {return a + b;
}
汇编代码示例:
.global main.extern add  // 声明外部函数 addmain:MOV R0, #5       // 第一个参数 a = 5MOV R1, #10      // 第二个参数 b = 10BL add           // 调用 C 函数 addMOV R7, #1       // syscall: exitSWI 0            // 软件中断,退出程序
- .global main:声明- main函数为全局符号,以便链接器能够识别和连接它。
- .extern add:声明外部符号- add,表明- add函数是在外部文件中定义的。
- MOV R0, #5和- MOV R1, #10:将值 5 和 10 分别加载到寄存器 R0 和 R1 中,这两个寄存器用于传递参数 a 和 b。
- BL add:调用 C 函数- add。BL 指令(Branch with Link)会将当前 PC 值存储到 LR 中,并跳转到- add函数的地址。
- MOV R7, #1和- SWI 0:用于执行软件中断,退出程序。
函数调用前准备:
- 在调用 add函数之前,需要将参数准备好。根据 ATPCS 规则,前四个参数依次存放在 R0、R1、R2 和 R3 寄存器中。
函数调用:
- 使用 BL add指令调用add函数。BL 指令会保存返回地址到 LR 寄存器,并跳转到add函数的入口地址。
函数返回:
- 当 add函数执行完毕后,返回值存放在 R0 寄存器中,CPU 会从 LR 寄存器中读取返回地址并跳转回调用函数继续执行。
2. 汇编和C程序传递参数
汇编程序向 C 程序的函数传递参数
在汇编程序中调用 C 函数时,通常需要传递参数给 C 函数。ATPCS 规范规定了如何传递参数的方法:
- 参数少于等于 4 个时: - 使用寄存器 R0 至 R3 来进行参数传递。第一个参数传递到 R0,第二个参数传递到 R1,依此类推。如果只有一个参数,它将存储在 R0 中;如果有两个参数,第一个参数在 R0 中,第二个参数在 R1 中,以此类推,直到 R3。
 
- 参数大于 4 个时: - 前四个参数按照上述方法传递。剩余的参数通过堆栈传递。调用函数时,调用者需要将这些多余参数压入堆栈。入栈的顺序是从最后一个参数开始,即最后一个参数先入栈。
 
C 程序返回结果给汇编程序
C 函数执行完毕后,需要将结果返回给调用它的汇编程序。返回值的传递方法也有一定的规范:
-  结果为一个 32 位的整数时: - 通过寄存器 R0 返回。函数的返回值会存储在 R0 寄存器中。
 
-  结果为一个 64 位整数时: - 通过寄存器 R0 和 R1 返回。64 位整数的低 32 位存储在 R0 中,高 32 位存储在 R1 中。
 
-  结果为一个浮点数时: - 通过浮点寄存器 f0、d0 或 s0 返回。浮点数结果存储在浮点寄存器中。
 
-  结果为一个复杂的浮点数时: - 通过寄存器 f0fN 或者 d0dN 返回。复杂浮点数的每一部分可能存储在不同的浮点寄存器中。
 
-  对于传递更多位数的结果: - 通过调用者的不同寄存器来传递,具体依据实际情况和编译器的规定。
 
代码示例
假设一个 C 函数 multiply,它接收两个整数参数并返回它们的乘积:
// multiply.c
int multiply(int x, int y) {return x * y;
}
在汇编程序中调用这个 C 函数并传递参数 6 和 7:
.global main
.extern multiply  // 声明外部函数 multiplymain:MOV R0, #6       // 第一个参数 x = 6MOV R1, #7       // 第二个参数 y = 7BL multiply      // 调用 C 函数 multiplyMOV R7, #1       // syscall: exitSWI 0            // 软件中断,退出程序
- MOV R0, #6和- MOV R1, #7:将值 6 和 7 分别加载到寄存器 R0 和 R1 中。这两个寄存器用于传递参数 x 和 y。
- BL multiply:调用 C 函数- multiply。BL 指令会将当前 PC 值存储到 LR 中,并跳转到- multiply函数的地址。
- MOV R7, #1和- SWI 0:用于执行软件中断,退出程序。
调用完 multiply 函数后,乘积结果会存储在 R0 中。通过这种方式,可以在汇编程序和 C 程序之间进行参数传递和结果返回。
3. C 函数使用栈
在 C 程序和 ARM 汇编程序的函数调用过程中,使用栈(stack)是非常重要的一部分。栈的主要作用有两个:保存现场/上下文,传递参数。
保存现场/上下文
-  保存现场: 在程序执行过程中,当发生函数调用时,需要暂时保存当前的执行现场,以便函数执行完毕后可以恢复到调用前的状态。这个保存的过程通常称为“保存现场”。 例如,当 CPU 运行到某些寄存器时(如 R0~R3,LR 等),这些寄存器中可能存有重要的数据。如果直接跳转到函数去执行,而不保存这些寄存器的数据,函数执行过程中对这些寄存器的操作就会破坏原有的数据。因此,需要先将这些寄存器的数据暂时存放到栈中。 
-  保存上下文: 保存上下文的过程和保存现场类似。上下文指的是当前程序执行的状态,包括寄存器内容、程序计数器等信息。当函数调用发生时,需要将这些上下文信息保存到栈中,以便函数执行完毕后能够准确地恢复到调用前的状态。 因此,在函数调用之前,应该将这些寄存器的数据临时保存到栈中,等待函数执行完毕返回后,再恢复现场。这样 CPU 就可以正确地继续执行后续的指令。 
传递参数
-  传递参数: 当参数数量大于 4 个时(不包括第 4 个参数),第 4 个参数后的参数就保存在栈中。 传递参数的过程通常如下: - 当被调用函数的参数超过 4 个时,前四个参数通过寄存器 R0~R3 传递,剩余的参数依次压入栈中。
- 在函数调用过程中,调用者会将这些参数按顺序压入栈中,函数被调用时通过从栈中读取这些参数来进行处理。
 
代码示例
假设一个 C 函数 func,它接收五个整数参数并返回它们的和:
// func.c
int func(int a, int b, int c, int d, int e) {return a + b + c + d + e;
}
在汇编程序中调用这个 C 函数并传递参数 1, 2, 3, 4, 5:
.global main
.extern func  // 声明外部函数 funcmain:MOV R0, #1       // 第一个参数 a = 1MOV R1, #2       // 第二个参数 b = 2MOV R2, #3       // 第三个参数 c = 3MOV R3, #4       // 第四个参数 d = 4PUSH {R4}        // 保存现场,R4 用于传递第 5 个参数MOV R4, #5       // 第五个参数 e = 5BL func          // 调用 C 函数 funcPOP {R4}         // 恢复现场MOV R7, #1       // syscall: exitSWI 0            // 软件中断,退出程序
- MOV R0, #1至- MOV R3, #4:将前四个参数加载到寄存器 R0~R3 中。
- PUSH {R4}:将寄存器 R4 的内容压入栈中,以便在使用 R4 传递第 5 个参数时,不破坏原有的内容。
- MOV R4, #5:将第 5 个参数加载到寄存器 R4 中。
- BL func:调用 C 函数- func。BL 指令会将当前 PC 值存储到 LR 中,并跳转到- func函数的地址。
- POP {R4}:从栈中恢复寄存器 R4 的内容,恢复现场。
- MOV R7, #1和- SWI 0:用于执行软件中断,退出程序。
通过使用栈,可以确保在函数调用过程中,参数能够正确传递,并且在函数调用完毕后,能够正确恢复程序的执行现场,从而保证程序的正常运行。
4. C 语言中读写寄存器
在嵌入式系统开发中,经常需要访问芯片上某些特定模块的寄存器。这些寄存器并不位于 CPU 内部,而是在芯片的外部模块中。我们通过指针访问这些寄存器的方式与访问普通内存一样。
寄存器地址与指针
每个寄存器都有一个固定的地址。通过定义一个指向这个地址的指针,可以对寄存器进行读写操作。
例如,假设有一个寄存器 CCM_CCGR1,它的地址为 0x20C406C,我们可以定义一个指向该地址的指针,并使用 volatile 关键字告知编译器该寄存器的值可能会被外部硬件或其他程序修改,不应进行优化。
示例代码
volatile unsigned int *CCM_CCGR1 = (volatile unsigned int *)(0x20C406C);
这里的 volatile unsigned int * 表示一个指向 unsigned int 类型的指针,并且这个指针指向的数据是 volatile 类型,即数据可能会被外部因素修改。
读写寄存器
通过这个指针,我们可以进行读写操作:
- 读寄存器
val = *CCM_CCGR1;
这行代码将读取 CCM_CCGR1 寄存器的值,并存储到变量 val 中。
- 写寄存器
*CCM_CCGR1 |= (3 << 30);
- *CCM_CCGR1通过指针访问寄存器- CCM_CCGR1。
- |= (3 << 30)是一个位操作,将- 3左移 30 位,得到一个值,这个值在二进制中表示为- 11(即第 31 位和第 30 位都为 1),然后将这个值与- CCM_CCGR1的当前值进行按位或操作,结果将这两位设置为 1。
通过上述方法可以在 C 语言中方便地对寄存器进行读写操作,确保对硬件寄存器的正确访问和修改。这种方法广泛应用于嵌入式系统开发中,特别是在处理器需要与外部设备进行通信或控制时。