分类: LINUX
2010-10-19 14:06:04
面对C语言可能大家不会陌生,在嵌入式开发中,C语言由于其高效、简捷、可读性强等特点,始终占据着主导地位,依据我个人的理解,C语言占主导地位的时间还将持续很长一段时间,因此,本章我们将用C语言来实现点亮LED,在这里,我给C语言还不太熟练的朋友推荐两本书,(纯属个人意见)一本是《Linux C编程一站式学习》,另一本是《C和指针》。
C语言环境前的环境准备
有些学习过Bootloader和内核的朋友肯定会知道,在调用C语言函数之前肯定会有一段汇编代码在前面铺路,进行一些必要的初始化工作;而那些只学过单片机而没有学过ARM的朋友肯定会觉得很奇怪,在单片机中写C代码,前面完成可以不用任何汇编代码。这是为什么呢?
这主要是因为我们的开发环境(这里主要是指编译环境)的不同,在开发单片机程序的时候,开发环境(如KEIL)会在编译C代码的时候,给我添加启动代码(startup-51)或者在编译时已经由编译器在后台为我们初始化好了。而在开发ARM程序时,ARM处理器支持多种模式,多种功能,而在不同的领域不同的项目里面,我们可以有选择的、适当的选择这些功能,这时,编译器就不知道我们需要什么功能,需要什么模式,编译器也就无法给我们提供默认的“初始化”代码,所以,编译器干脆就“不管”这些了,把这些工作交由我们开发者来处理。
下面,我们就通过本章的实例来讲解一下C代码的开发流程。以下包括以后的大部分代码都由韦东山老师的《嵌入式LINUX应用开发完全手册》移植而来,编译后可以直接在TQ2440上运行。我认为该书是一本参考价值很高的书,在这也感谢一下韦东山老师的分享精神。
本程序由crt0.S led_on_c.c Makefile三个文件组成。后两个文件将在下面详解。
@******************************************************************************
@ File:crt0.S
@ 功能:通过它转入C程序
@******************************************************************************
.text
.global _start
_start:
ldr r0, =0x56000010 @ WATCHDOG寄存器地址
mov r1, #0x0
str r1, [r0] @ 写入0,禁止WATCHDOG,否则CPU会不断重启
ldr sp, =0x31000000 @ 设置堆栈,注意:这时我们是将程序直接烧录到
@SDRAM中,所以堆栈要设置在SDRAM中
@ 如果将程序烧在NAND FLASH中,需将堆栈改成
@1024x4,因为nand flash中的代码
@ 在复位后会移到内部ram中,此ram只有4K
bl main @ 调用C程序中的main函数
halt_loop:
b halt_loop
这里涉及到两个新的知识:
:看门狗定时器。学习过单片机的朋友都会多多少少知道看门狗的作用,简单的说,它的作用就是当程序发生异常时,重启CPU。这里大家只知道作用就可以,后面我会单独讲看门狗的使用。这里将其赋值为0就是关闭看门狗,不让其工作,否则CPU会不断重启。
:堆栈。 堆栈的概念首先,我们来看一下,一个由于C语言编译出来的程序,运行时,内存的画分情况。
名称 |
概 念 |
栈(stack) |
由编译器自动分配、翻译。存放函数的参数值和局部变量值。操作方式类似于数据结构中的栈。 |
堆(heap) |
由程序员分配、释放。如果程序员不释放,程序结束时有可能由操作系统释放。请注意它和数据结构中的堆是两回事,操作方式类似于链表。 |
BSS |
存放未初始化的全局变量和静态变量。 |
数据段DATA |
存放初始化之后的全局变量和静态变量。 |
代码段TEST |
程序代码主体,函数主体等。注意为二进制格式。 |
为了理解上述概念,请看下面的例子:
//main.cpp 堆栈的特性 有了上面的例子,相信大家对堆和栈有了理解。下面我们来对比的分析一下堆和栈的特性:
对比内容 栈(heap) 堆(stack) 申请方式 由系统自动分配 程序员分配,并且需指明大小 申请后 系统的响应 只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 申请 大小的限制 在编译程序时,由编译器确定。是一个常数,在WINDOWS系统下一般为2M大小。因此,能在栈获得的空间比较小。 由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。 扩展方向 向低地址扩展 向高地址扩展 申请效率 栈由系统自动分配,速度较快。但程序员是无法控制的。 由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便. 存储内容 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
int a = 0; 全局初始化区 DATA
char *p1; 全局未初始化区 BSS
main()
{
int b; 栈
char s[] = "abc"; 栈
char *p2; 栈
char *p3 = "123456"; 123456\0在常量区,p3在栈上。
static int c =0; 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
C“世界”经过上面的一些概念后,我想大家肯定又有点迷糊了,不过没关系,希望大家慢慢理解,没有人一下子可以理解得了那么多概念。汇编代码倒数第二句的:bl main 说明经过前面的初始化后,接下来运行的是C代码main()函数了。下面先列出代码:
#define GPBCON (*(volatile unsigned long *)0x56000010)
#define GPBDAT (*(volatile unsigned long *)0x56000014)
int main()
{
GPBCON = 0x00000400; // 设置GPB5为输出口, 位[11:10]=0b01
GPBDAT = 0x00000000; // GPB5输出0,LED1点亮
return 0;
}
这个文件看起来很简单,但值得我们思考得却又很多,首先来看一下寄存器的定义。从单片机转向ARM开发的朋友肯定有点疑问,在单片机开发的时候,我们可以对单片机内部寄存器直接进行赋值,为什么到这里我们先定义呢?答案大家肯定都人知道,因为单片机开发时,我们都包含了一个头文件,在头文件中给我定义好了,所以我们可以直接拿来用,不过问题又来了,这里的形式很陌生,(*(volatile unsigned long *)0x56000010) 这个是什么意思呢?
好。我们来分析一下:首先看一下0x56000010,这是一个16进制的整数,而在ARM中,所有的寄存器都被映射到相应的地址,如GPBCON寄存器就被映射到S3C2440地址空间的0x56000010,所以,我们操作寄存器实际上就是操作地址为0x56000010的内存空间一样,但在C语言中,0x56000010并不是地址空间,它只是一个整数而已,要想把在C语言中操纵地址空间,只有指针才可以,那这时我们怎么办呢?C语言为我们提供了一种格式转换---“强制类型转化”。因此,在其前面加上一个(unsigned long *),这个类型就是一个无符号长整形指针,加上这个,就说明将0x56000010转换成指针类型了。那volatile是什么意思呢?这是C语言中的一个关键字,目的是告诉编译器一些信息,这个我们下面会详述。然后在整个的批针前面加一个“*”,大家就恍然大悟了吧,就是对这个指针的引用,现在大家肯定明白了,对GPBCON操作就是对0x56000010的指针操作。
重点:volatile
现在我们来看一下,我们操作的是S3C2440这个设备(CPU)的寄存器,而不加volatile时,编译器只会将0x56000010当作普通的内存地址指针,这时就有一个问题了:普通的内存地址指针如果我们(程序员)在程序中不对其改写,那么它里面的值肯定是不会变的。而对比设备寄存器就不一样了,如GPBDATA,假设我们不对它操作,当GPB端口为输入状态时,会由于外界的因素改变GPB端口的电平状态,从而改变了GPBDATA寄存器。由于以上这两点的区别,当编译器编译程序时,如果我们都不加以区分,编译器都会当作普通内存单元来处理,这时,编译器会优化掉“多余”的代码。我们来举个具体的例子:
#define GPBDAT (*( unsigned long *)0x56000014)
Void main()
{
Unsigned long val;
While(1)
{
Val = GPBDAT;
}
}
上面的代码只是我随便写的,目的不是为了运行,只是为了说明volatile的作用。上面的代码只是将volatile关键字去掉了,它“本意”要完成的任务是不断的去读GPBDAT寄存器,将其值赋给Val,但当编译程序后,编译器会将其“读的部分代码”优化掉,也就是说无论GPBDAT寄存器变化不变化,它都不会去读了,因为程序根本没有改变GPBDAT的值,编译器就会认为它没有变化,因此读它没有意义,干脆“优化”掉。
上面只是分析了读的情况,而写的情况也是一样,请读者自行分析。
用优化选项编译生成的指令明显效率更高,但使用不当会出错,为了避免编译器“自作聪明”,把不该优化的也优化了,程序员应该明确告诉编译器哪些内存单元的访问是不能优化的,在C语言中可以用volatile限定符修饰变量,就是告诉编译器,即使在编译时指定了优化选项,每次读这个变量仍然要老老实实从内存读取,每次写这个变量也仍然要老老实实写回内存,不能省略任何步骤。
现在相信大家看这段代码就没有问题了吧,但这里我还想提一个问题,在以往我们都会写一个while(1)的死循环,让程序不断的执行下去,这次我们怎么没有呢?
请大写看代码的最后一句,是一个返回语句,return 0 。说明main()函数是会返回的,那它会返回哪呢,我们回过头来再看一下汇编语言,结果大家肯定会明白了吧,汇编的最后两句是不断的调用自己本身,起到的作用和while(1)是一样的,一个死循环。(希望大家以后在分析程序时要做到善始善终)
接着,我们把代码编译好,烧到开发板中,验证一下。(以后,我都会将完整的代码打包发到网上,具体的网址请见博客,编译方法只要在目录下make就可以,其次就是将编译出来的.BIN文件COPY到TFTP目录下以方便烧写,烧写请见第一章和第二章,以后不再赘述。)
1. 韦东山 《嵌入式linux应用开发完全手册》
2.《linuxC编译一站式学习》
3.《S3C2440手册》
4. 天嵌科技相关文档
5. 堆和栈详解 http://www.cppblog.com/oosky/archive/2006/01/21/2958.html
machoe2010-12-27 09:21:19
machoe2010-12-27 09:21:17
love2008lzk2010-12-26 22:30:33
chinaunix网友2010-12-02 16:07:47
machoe,你好,我下载的你的程序中为什么sp是指向0x30000000,而 你这是在0x31000000,还有请问下你能具体的说下就是设置成这几个地址的区别么?如果设置在0x30000000,那么你go 0x30000000的时候应该是运行这个sdram里面的程序的吧!求解答下,谢谢