上一个程序我们使用汇编来编写了第一个点亮LED的程序,总的来说程序并不复杂,使用纯汇编来编写程序我们只需了解相应的汇编指令,然后去查阅原理图和数据手册来完成整个程序的编写,但是汇编程序的可读性不是那么好,所以,我们还是需要使用更加高级的程序语言来进行编程,今天,就来写一个简单的C语言控制LED的程序
在写程序之前,我们需要知道几个事情
1、使用C语言之前需要具备什么环境?
2、如何调用C语言?
3、汇编程序与C语言之间如何进行参数的传递?
有以上几个简单的问题摆在我们面前,这些个问题如果不是很清晰的话就会在编写程序的过程中产生疑虑,导致我们不能顺利的完成本次程序的编写,现在来对上面的问题进行说明
**1、我们都知道,C语言的变量会使得编程变得更加灵活,那么在C语言中我们所申请的临时变量是存在哪里的呢?是保存在栈里边的,之前的汇编程序我们并没有去设置栈,使用C语言之前就需要就行栈的设置,初学者可能一听这个名词“栈的设置”就会觉得有点害怕。该怎么去设置呢?其实很简单,在ARM处理器的通用寄存器中编号13的寄存器R13,有一个别名SP就是常说的栈指针寄存器,设置栈得时候只需要给该寄存器一个我们不会去涉及到的地址就行了,程序中的入栈和出栈都是CPU自动来完成的。
R12 | IP | 中断优先寄存器 |
---|---|---|
R13 | SP | 栈指针寄存器 |
R14 | LR | 连接寄存器,保存程序跳转的返回地址 |
R15 | PC | 程序计数器 |
2、汇编程序如何去调用C语言呢?我们可以使用汇编指令BL或者B跳转指令直接跳转到C语言函数中,例如BL main
3、既然可以实现跳转,那么在跳转时我们的参数该如何传递呢,使用的是APCS规则来进行参数的传递,要求在参数不多的时候依次将参数存入R0-R3寄存器中,当参数的个数大于这些寄存器所能保存的话,就将参数依次入栈,出栈即可,所以来看,之前的栈设置是那么有必要的了。**
这个问题搞清楚之后我们就可以进行编写C语言来控制LED灯的程序了,话不多说来实际写程序吧
.text
.global _start
_start:
ldr sp,=4096
/* 调用C函数 */
bl main
halt:
b halt
我们的汇编引导程序就已经写好了,对于上面栈的设置需要说明一下
1、所谓栈的设置其实只需要将栈指针SP进行赋值即可,在这里选择了4096这个地址作为栈指针的起始地址,因为我们写的程序都还很小,程序不会使用到这个地址。
2、我们打算将程序烧录进jz2440的nand flash中启动,我们知道nand flash启动的时候系统会自动把前4K代码复制到片内的SRAM中执行,所以我们将栈指针SP设置为4K的最高地址,也就是4096
设置好栈之后就可以简单满足C语言的运行环境,所以使用BL指令跳转到main函数中
#include
int main()
{
unsigned int *gpfcon = (unsigned int *)0x56000050;
unsigned int *gpfdat = (unsigned int *)0x56000054;
*gpfcon = 0x100;
*gpfdat = 0;
return 0;
}
这是一个简单的实例C函数,在C语言里直接使用寄存器地址进行设置,这里就不再解释,然后将编译好的二进制文件烧入开发板,设置为nand flash启动,LED被正常点亮,我们的实验也就到此结束。
接下来对上面的过程进行一个总结,在使用C语言之前进行了栈的设置,我们来通过二进制的反汇编文件来分析一个C语言的内部机制
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
4: eb000000 bl c
00000008 :
8: eafffffe b 8
0000000c
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd008 sub sp, sp, #8 ; 0x8
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
24: e50b3010 str r3, [fp, #-16]
28: e3a03456 mov r3, #1442840576 ; 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
30: e50b3014 str r3, [fp, #-20]
34: e51b2010 ldr r2, [fp, #-16]
38: e3a03c01 mov r3, #256 ; 0x100
3c: e5823000 str r3, [r2]
40: e51b2014 ldr r2, [fp, #-20]
44: e3a03000 mov r3, #0 ; 0x0
48: e5823000 str r3, [r2]
4c: e3a03000 mov r3, #0 ; 0x0
50: e1a00003 mov r0, r3
54: e24bd00c sub sp, fp, #12 ; 0xc
58: e89da800 ldmia sp, {fp, sp, pc}
从反汇编程序中可以看到,当程序刚一进入main函数执行时,先把当前栈指针寄存器sp保存在Ip中,然后使用指令stmdb sp!, {fp, ip, lr, pc}将寄存器的值进行入栈,
stmdb:(地址先减而后完成操作),
stmia :(完成操作而后地址递增)
SP!:SP的值使用上一次变化之后的结果
stmdb sp!, {fp, ip, lr, pc}
sp=sp-4,sp=pc;先压PC pc = 4092
sp=sp-4,sp=lr;再压lr lr = 4088
sp=sp-4,sp=ip;再压ip ip = 4084
sp=sp-4,sp=fp;再压fp fp = 4080
接着计算fp = ip -4 = 4092, sp = sp - 8 = 4072,中间的一些代码是对LED灯寄存器的写值操作,这里就不做分析
看到最后 sub sp, fp, #12 ; 0xc 得到 sp = fp - 12 = 4092 - 12 = 4080
ldmia sp, {fp, sp, pc}
fp = [4080] 原来保存的fp的值 sp += 4
sp = [4084 : ip] = 4096
pc = [4088]
总结:在汇编调用C函数之后,先把原来一些寄存器的值入栈,然后再将C语言里的临时变量入栈、做计算,调用完成之后将原来保存的值出栈,还原汇编的执行环境,在还原的过程中将lr的值保存到了pc中,lr中的值是调用C语言函数的返回地址,将这个地址赋给了pc寄存器,实现了指令的跳转,也就是返回到了调用C函数之后的地方,实现了程序的跳转,来自一名小白的学习感想,若存在不足之处,欢迎批评指正