Chinaunix首页 | 论坛 | 博客
  • 博客访问: 4470968
  • 博文数量: 1148
  • 博客积分: 25453
  • 博客等级: 上将
  • 技术积分: 11949
  • 用 户 组: 普通用户
  • 注册时间: 2010-05-06 21:14
文章分类

全部博文(1148)

文章存档

2012年(15)

2011年(1078)

2010年(58)

分类:

2010-12-01 20:40:31

原文地址:(三)、  又见LED 作者:machoe

      在上一章中,我们通过简单的汇编语言,实现了点亮LED灯,其主要目的是让大家熟悉LINUX环境下的裸奔实验。在嵌入式开发中,汇编语言也有着不可替代的作用,希望大家要重视,在以后的学习中,我们还会在BootloaderLinux内核的学习中,再次学习汇编语言。

面对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三个文件组成。后两个文件将在下面详解。

@******************************************************************************

@ Filecrt0.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 
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); 
分配得来得1020字节的区域就在堆区。 
strcpy(p1, "123456"); 
123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 

 

 

 

 

 

 

 

 

 

  堆栈的特性

有了上面的例子,相信大家对堆和栈有了理解。下面我们来对比的分析一下堆和栈的特性:

对比内容

(heap)

(stack)

申请方式

由系统自动分配

程序员分配,并且需指明大小

申请后

系统的响应

只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出

操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

申请

大小的限制

在编译程序时,由编译器确定。是一个常数,在WINDOWS系统下一般为2M大小。因此,能在栈获得的空间比较小。

由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。

扩展方向

向低地址扩展

向高地址扩展

申请效率

  栈由系统自动分配,速度较快。但程序员是无法控制的。

由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.

存储内容

在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

小结

堆和栈的区别可以用如下的比喻来看出:

使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

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输出0LED1点亮

     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文件COPYTFTP目录下以方便烧写,烧写请见第一章和第二章,以后不再赘述。)

1. 韦东山 《嵌入式linux应用开发完全手册》

2.linuxC编译一站式学习》

3.S3C2440手册》

4. 天嵌科技相关文档

5. 堆和栈详解 http://www.cppblog.com/oosky/archive/2006/01/21/2958.html

 

 

阅读(3011) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~