之前的两个示例程序都是完全由汇编编写的,而这次的示例程序由汇编和C语言写成并编译。
汇编自有汇编的优点,比如你可以非常清楚地知道CPU在执行每条指令时到底做了什么,怎么做的,数据到底保存在哪儿等等,但是缺点也很明显,写汇编代码太痛苦,而且代码也不易阅读,所以C语言出现了。//其实,还包括C++
从Code Warrior 的DebugRel Setting 中可以看到,Code Warrior 应该支持汇编,C ,C++ 三种语言。此外,在利用汇编和C语言编程时,要记得两个编译器都要设置成对应的ARM处理器架构。
程序方面,这次要写的是一个发送SOS信号的程序(莫尔斯码:...---... ,即三短三长三短)。输出方式有很多,比如上两篇文章提到的LED灯或者蜂鸣器。考虑到扰民问题,决定使用LED作为输出。当然,这个示例代码的关注点仅仅是如何从汇编代码跳转到C代码。
汇编代码如下:
init.s
1 AREA |DATA|,CODE,READONLY 2 ENTRY 3 4 LDR R13, =0x34000000 5 IMPORT ledMain 6 b ledMain 7 8 END
这段汇编代码非常简单:
4:LDR伪指令(有等号“=”),将 0x34000000 存入R13 寄存器,为堆栈初始化。
5:IMPORT伪指令,说明了一个跳转符号。比如,上两篇提到过的START,BEEPON 等。可以通过B 跳转指令来控制程序的执行顺序。而当这个符号并不在本文件中时,需要用IMPORT 来说明。从汇编代码跳转到C 代码,就需要这个指令。符号名称即为C 函数的函数名。
6:B 跳转指令,跳转到ledMain 处。即调用C 代码的ledMain 函数处。
有关第4行:
在ARM 中,R13寄存器常用作堆栈寄存器。之前的纯汇编代码并没有用到R13,但也能运行。说明逻辑程序是并不一定需要一个堆栈。而这里之所以又用到了堆栈寄存器,是因为C 语言所编写的程序必须要一个堆栈。就是说,是C 需要堆栈,而不是ARM 需要。
那么,如何确定这个初始化的值呢?答案不唯一,甚至可以随便一点。
一、C 程序的布局
首先来看看C 程序的布局:
如图,一般而言,C 程序具有固定的布局格式。一般而言,正在运行的程序从内存地址低到高分为如下几个“段”:
1. 文本段(TEXT),存放代码以及常量,这些数据无论何时,都不会改变。也有人会把常量单独分为一个“常量段”,这也不无道理。简单起见,因为他们不会改变的特性,我把它们一起当做文本段。
注意:C 语言中,用CONST 声明的是一个“只读变量”,它本质上还是变量而非常量,利用指针,是可以改变其值的。
而只有那些在程序中被“写死”的数字符号才是真正意义上的常量。
2. 数据段(DATA),存放在程序运行之初,已经初始化赋值过的变量。其中有全局变量以及静态变量(static)。
3. BSS段(BSS),存放程序运行之初,尚未初始化赋值的变量。其中有全局变量以及静态变量。上图中写道会被初始化为0,但实际并不一定都是。比如C 中的野指针就是一个例子,很可能存放着垃圾数据。
4. 堆(HEAP),动态储存区,只有程序执行时才会出现。常用的MALLOC 所分配的内存就在这个地方。增长方向为从内存地址低到高向上。
5. 堆栈(STACK),动态存储区,和堆一样,只有程序执行时才会出现。在不考虑编译优化的情况下,只要调用函数,就会压栈。子程序返回,则出栈。增长方向和堆相反,为从内存地址高到低向下。另外,堆就是堆(HEAP),堆栈就是栈(STACK)。两者只是名字比较容易混淆,但是作用完全不一样。
一般而言,局部变量都会被存放在堆栈里。这也说明了为什么局部变量在子程序返回时就被销毁而不可用。
而ARM 在这个地方会有个特别之处:如果函数调用的参数在4个以内,会利用寄存器R0~R3来传递。只有超过部分才会利用栈来传递。
相关文章链接: C 程序内存分布;ARM函数调用时参数传递规则
二、堆栈寄存器的初始化
知道了程序在内存中的布局后,如何确定堆栈寄存器也变得有点眉目了。规则我觉得就两条:
1.地址在内存范围里;2.堆栈不和数据段撞车。
先讲第1条:首先要明确一点,此时我们的程序并没有启动内存管理单元MMU,真正的内存地址得看S3C2440的储存空间映射图:
s3c2440有2种启动方式,从NOR FLASH 启动(左)和从NAND FLASH 启动(右)。
从图中看到,并不是所有的地址都是所谓的“内存地址”。甚至地址也不一定是连续的。ARM 使用统一编址,就是乱七八糟设备的地址都混杂在一起的编址方式(- -)。所以,仔细观察上面这张图,应该立刻发现,我们得把堆栈指针设置到内存地址范围内,而不是随便挑一个自己喜欢的地址。
具体的例子:64M的SDRAM作为内存,地址空间映射到BANK6,那么内存地址范围就是 0x30000000~0x34000000 (64*1024*1024=67108864,即 0x04000000)
理论上而言,如果希望CPU运行起来非常简单,通电就可以,内存,硬盘什么都可以不要。但是,此时CPU仅仅是出于“运行”状态,不会去执行任何操作——因为没有指令可以执行。于是,我们给它加上了内存。这样,CPU可以从内存中得到指令,从而执行。并将执行结果放到内存里去。
此时的计算机可以执行具体操作了,但是仍然不能完成任务。因为内存一掉电数据就丢失——想想一下只能一直开着的电脑。这依然不科学。于是,我们又加上了硬盘,里面存放着掉电也不会丢失的指令和数据。
这样,计算机一启动,先把硬盘里的指令和数据放到内存,然后CPU从内存里获取指令和数据并执行,把结果返回到内存,再保存到硬盘。这样,关机后,硬盘里也存放着你希望得到的数据。
s3c2440 拥有一块4K 的片内RAM ,可以看做是一个内存。但并没有任何片内ROM,也就是没有硬盘。在s3c2440 以NOR FLASH 启动时,NOR FLASH 相当于一块速度非常快的硬盘。它好像内存一样,CPU 直接从NOR FLASH 获取指令和数据,然后执行。而NAND FLASH 启动时,由于NAND FLASH 非常慢,相当于硬盘。在这种启动方式下,s3c2440 自动把NAND FLASH 最开始的4K 内容复制到片内RAM,然后再执行。
所以,按NAND FLASH 启动时,堆栈寄存器可以设为片内RAM 的最大地址:0x1000(4K)或者属于SDRAM的地址,比如本例的 0x34000000。而千万不要设置成其他地址,比如ROM 的地址或者其他奇怪的地址。
第2条堆栈不和数据段撞车:这就很好理解了。因为堆栈空间是向下增长,也就是向数据段(DATA)方向增长的。如果堆栈里的数据跑到数据段里把数据覆盖,肯定是出问题。当然,这也可以成为破解或者黑客的一种手段。
本人使用AXD+JLINK来运行程序,实际操作下来 0x1000 和 0x34000000 都可以。
剩下的是C 代码:
ledMain.c
1 #define GPBCON (*(volatile unsigned *)0x56000010) //定义IO口地址 2 #define GPBDAT (*(volatile unsigned *)0x56000014) 3 #define GPBUP (*(volatile unsigned *)0x56000018) 4 5 //延迟函数,通过让CPU空转实现 6 void Delay(int x) 7 { 8 unsigned a; 9 10 while(x) 11 { 12 for(a=0xffff;a>0x0;a--); 13 x--; 14 } 15 } 16 17 //LED灯亮灭部分,可参照上两篇文章 18 void ledup() 19 { 20 GPBDAT=~(1<<5); 21 } 22 23 24 void leddown(void) 25 { 26 GPBDAT=~0; 27 } 28 29 //控制亮起时间长短和亮灭次数 30 void ledFreq(int t,int count) 31 { 32 while(count--) 33 { 34 ledup(); 35 Delay(t); 36 leddown(); 37 Delay(7); 38 } 39 } 40 41 //主函数 42 void ledMain() 43 { 44 GPBCON=0xddd7fc; //对IO口的设置可以参考前两篇文章 45 GPBUP=0x0; 46 GPBDAT=~0x0; 47 48 while(1) 49 { 50 ledFreq(5,3); 51 Delay(30); 52 53 ledFreq(15,3); 54 Delay(30); 55 56 ledFreq(5,3); 57 Delay(30); 58 } 59 }
本文涉及内容较多,如有谬误或者疏漏,敬请提出!