嵌入式系统高级C语言编程.凌明(带详细书签)
本书主要介绍针对嵌入式系统基于C语言的软件项目开发流程、较为复杂的C语言编程知识与技巧、编程风格及调试习惯,并通过对一个具体的软件模块(ASIX Window GUI)的分析,介绍分析代码的方法以及设计软件系统需要考虑的各要素。本书以实际项目中的代码为例来进行介绍,详细分析在嵌入式系统开发中程序员应该注意的方法、技巧和存在的陷阱。本书适合用作学习嵌入式系统的高年级本科生或硕士研究生的教学用书,也可作为从事嵌入式系统编程的软、硬件工程师的技术参考用书。嵌入式系统是将先进的计算机技术、半导体技术、电子技术和各行各业的具体应用相结合的产物,这就决定了它必然是一个技术密集、资金密集、高度分散、不第↓章概述1.1C语言的历史和特点在C语言诞生以前,系统软件主要是用汇编语言编写的。由于汇编语言程序依赖于计算机硬件,其可读性和可移植性都很差,但一般的高级语言又难以实现对计算机硬件的直接操作(这正是汇编语言的优势),于是人们盼望有一种兼有汇编语言和高级语言特性的新语言出现具有讽刺意味的是,C的诞生是从失败开始的。1969年由通用电气、麻省理工、贝尔实验室联合研制的 Multics操作系统几乎彻底失败,该操作系统实在是太庞大、太复杂了,以至于超出了开发团队的控制程度。从 Multics项目撒出后,贝尔实验室的工程师 Ken Thompson和 Dennis Ritchie开始利用业余时间将 Thompson写的一个小游戏“太空旅行”移植到PDP7小型机上,这个小游戏模拟了太阳系的行星系统,游戏者可以驾驶飞船降落在某个行星上;与此同时, Thompson还为PDP-7小型机设计了一个比 Multics更简单也更轻量级的操作系统,1970年 Brian Kernighan模仿 Multics的名字将这个新操作系统戏称为“UNIx”( Multi换成了Uni,以示这个新操作系统较之原来要简单、单纯得多)。与早期的操作系统一样,最早的UNIX采用的PDP-7是用汇编语言编写的,但是汇编语言在处理复杂数据结构时难以编码,同时也难以调试和理解。 Thompson希望能够采用高级语言来编写,在尝试 FORTAN失败后,他将一种研究性的高级语言BCPL( Basic Combined Programming Language,是由伦敦大学和剑桥大学合作研发的早期高级语言)简化为一种他称之为“B”的高级语言,以使得B语言的解释器能够运行在PDP-78K的存储器中。然而由于硬件资源的限制而采用的解释执行使B的效率不高,因此B语言并不适合作为UNIX系统的编程语言,以至于 Thompson在1970年将UNX移植到PDP-11小型机的时候依然采用了汇编语言。 Dennis Ritchie利用PDP-11更强大的硬件功能创立了“NewB”语言。这种新的语言支持多种数据类型,同时因为采用编译的运行方式而提高了性能,很快人们将“NewB”称为“C”语言。经过几年的演变和完善,到了20世纪70年代中期C语言已经和今天我们使用的C语言相差无几了,虽然后续的完善一直持续不断(比如增加了新的关键字 unsigned和long等)。第1章概述1978年, Steve Johnson编写了PCC编译器( Portable C Compiler,可移植的C编译器)。由于嵌这个编译器的源码可以在贝尔实验室之外公开故该编译器被广泛地移植到不同的处理器上,入成为当时C编译器的共同基石。同年,C语言的经典著作《C编程语言》(“ The C Program-式系mng1 anguage出版,为了表示对该书两位作者 Brain Kernighan和 Dennis ritchie的敬意,统|书中的C版本被称为“K8RC”(出版社当时估计能卖掉100本就不错了,然而截至1994高年这本书一共卖了150万册)级C到20世纪80年代早期,C语言已经被业界广泛采用了,但是随之而来的是多种不同的语实现和版本。比如为了适应80X6的特殊地址架构微软公司的C语言版本增加了一些新的关键字(如far,near等)。随着越来越多的非pce基础的C语言版本出现,C语言逐渐形成了编类似于 BASIC语言一样的松散语言家族。1983年,美国国家标准化协会(ANSI)根据C语言程问世以来各种版本对C语言的发展和扩充制定了 ANSI C标准,1989年12月再次做了修订,并最终确认了该标准。国际标准化组织(ISO)随后接受了该标准作为国际标准。 ANSI C标准有4个主要部分,分别是第4部分“简介”、第5部分“环境”第6部分“C语言”第7部分“C运行库”。该标准还有几个有用的附件,比如附件F“一般警告信息”、附件G“可移植性问题”等。需要说明的是,虽然 ANSI C标准规范了C语言的实现,但是在实际情况中,各家C语言提供商都会根据各平台的不同情况对 ANSI C进行一定的扩展,比如我们上面提到的微软的C语言实现中增加了关键字far、near;又比如在嵌入式领域ARM的C编译器增加了关键字long long以支持64位整数,增加了关键字irq以支持C语言编写的中断处理程序(注意:在有些编译器中有类似的关键字# Interupt)。如图1-1所示,我们可以将现实中的C语言实现看作是 ANSI C的一个超集,这些厂商对 ANSI C的扩展部分有可能彼此不兼容,从而使得C程序的移植需要对这些非标准的部分特别小心。在这个问题上比较有代表性的例子是 Linux的gcc编译器。由于该编译器对ANSC进行了非常多的扩展, Linux的内核源码基本上只能在gcc上进行编译,希望通过其他C编译器编译 Linux内核几乎是不可能的。另外一个需要注意的问题是,虽然 ANSI C对C语言的规范进行了非常详细的约定,但是由于C语言的实现平台纵跨了从8位单片机CPU到32位甚至64位CPU的硬件环境,因此在数据类型的约定上标准C必须有足够的灵活性。比如 ANSI C只规定了char数据类型是一个8位的数据,但是并没有规定int、 short、long类型应该是多少位。这就造成了不同C编译器对于这些数据类型的不同约定,比如 Borland公司的 Turbo C规定int类型是16位整数;但是ARM的编译器规定int类型是32位整数, Freescale的68000编译器关于int、 short、long类型的数据宽度是可以配置的。因此,嵌入式软件程序员在编写C代码时或者从其他处理器平台移植C代码时必须非常谨慎地处理这些与编译器相关的内容。C语言的特点主要有以下几点:第1章概述①语言简洁、紧凑,使用方便、灵活。C只有32个关键字,9种控制语句。较之其他高级语言,C语言的关键字非常少,一方面是语言本身的设计使然,另一个重要的原因是因为C语嵌言将所有与外围硬件设备相关的输入/输出操作统统放在C运行库中实现比如从键盘输入、入式向屏幕输出、文件的操作等都没有作为C语言关键字出现,而是以库函数的方法加以实现。系这样做的好处一方面使得语言的实现变得比较简洁(编译器的实现也会比较简单),另一方面统由于与硬件设备相关的功能以函数的方法实现使得C语言本身尽可能与硬件平台无关,这高也是C语言能够在如此众多的硬件平台上实现的重要原因。级某商用C编译器1ANSIC标准C语言编程一论某商用编译器2某商用编译器3图1-1 ANSI C与商用C编译器的关系②运算符很丰富,C语言一共有34种运算符,但关键字只有32个。C语言中包含了一些特有的运算符。比如:自增自减运算符++和一一;针对指针运算的取内容运算符*和取地址运算符&;针对位运算的移位运算符<<和>>;按位与&、按位或|、按位异或按位取反;等。这些运算符大大方便了程序员在进行底层代码编写的过程中对存储器、控制寄存器等硬件资源进行操作。③数据结构丰富,C语言的数据类型支持整型、实型、字型符、数组、指针、结构( struct)、共用体( union)、枚举类型(enum)。与其他高级语言不太一样的是,C语言没有字符串类型。这也是我个人认为C语言在处理字符串问题时比较不方便的原因。事实上,在很多需要对字符串进行处理的应用中(比如脚本的解释程序,像早期Web应用中的CGI脚本)往往更多地采用非常适合字符串处理的Perl语言进行编写,而不经常采用C语言。④具有结构化的控制语句,在C语言中支持if…else、 while、do… while、 switch…case、for这些结构化的控制语句,我们后面会专门讨论这些控制语句。虽然C语言和绝大多数高级语言一样保留了goto关键字,而且C的语言结构也没有 PASCAL那样严格和规范,但是总的来说C语言依然是非常好的结构化编程语言。⑤C语言的语法限制不太严格,程序设计自由度大。这是一个双刃剑。C语法非常宽容。比如C语言里面不检查数组越界,它是C语言里面很重要的一个特点,虽然这看起来是一个不好的特点,但是在一些优秀的程序员手中,这个特点也可以变成一个非常灵活的、并且第1章概述富有技巧性的方法。所以虽然说C很危险、很灵活,但是在高手手里面这些都是可以利用的,嵌也就是留给程序员的空间非常大。C语言就像是一种非常厉害的兵器,比如流星锤,他要求玩入这个兵器的人要很厉害但如果一个新手玩就很可能被流星锤打中自己。式系⑥C语言允许直接访问物理地址,能进行位(bit)操作,可以直接对硬件操作。这是C语统|言非常重要的一个特点。C对物理地址的访问主要是通过指针,而指针又是C里面最灵活的痛部分也是初学者最难掌握的内容,但是如果没有指针的话,实际上也就意昧着C不能够访级C间硬件或者说访问硬件会变得很困难那么对内存的操作也就变得很困难。语⑦生成目标代码质量高,程序执行效率高。相对于其他高级语言,C的编译器效率可能宣是最高的。这一方面是因为对C编译器优化的研究已经达到了非常成熟的程度,这一点从程ARM公司的C编译器在性能上每年仅仅提升小于5个百分点可以得到印证另一方面是因为C语言本身的语言特性,使得在将C程序转化成为汇编代码时,需要额外增加的检查代码要少得多。比如C语言不检查数组越界和内存缓冲区越界。也正因为C目标代码的高效性,使得C语言非常适合诸如操作系统、编译器等系统软件的开发,同时也使C语言成为嵌入式软件开发的首选高级语言(基于对成本和功耗的考虑,嵌入式系统的硬件性能往往受到严格的限制)。⑧可移植性好。理论上讲,任何一个高级语言都应该具有很好的可移植性,但是实际的情况却不尽如人意,这是因为各个厂商推出的编译器往往会扩展一些自己的特性。C语言的可移植性是比较好的,从巨型机到单片机都可以使用C语言。这主要有两个原因:第一,C语言在20世纪80年代就制定了相关的标准( ANSI C标准),因此虽然各家编译器厂商推出的C语言各不相同,但是都保证与 ANSI C兼容;第二,也是往往被大家忽略的原因,就是C语言本身的标准中并没有设计输入/输出的操作,C的关键字中没有与计算机系统相关的输入/输出功能,所有的这些功能都是由C运行库中的库函数完成的。从这个意义上来说C语言本身是和硬件无关的。当然,C的可移植性是相对的,实际的工程项目中移植依然是一个不容小视的问题。C语言在1997~2009年之间都是嵌入式软件开发使用最多的语言。近五年来C与C++语言更瓜分了大部分原属于汇编语言的版图。其中较高阶的C十+发展速度虽不如预期,但仍在嵌入式软件设计领域维持27%左右的占有率。整体看来,C+十语言使用率在20世纪90年代晚期加速上升,在2001年达到高峰,然后稍微下滑,之后维持稳定。无论如何,嵌入式软件设计师不会在短时间内放弃使用C语言原因有很多个:首先,C语言编译器支持大多数的8位、16位与32位CPU;其次C语言在处理器与驱动程序层级,兼具高低级语言的特色。请看图1-2所示的关于嵌入式软件工程师所使用编程语言的调查数据(来自 TechInsights2009年嵌入式系统市场研究)。第Ⅰ章概述60%当前项目2009(N=1532)50%下一个项目2009(N=15323)27%30%20910%画%2%1%B1%|%3%4CC++汇编 JAVA BASIC UML. Labview其他嵌入式系统高级C语言编程图1-2C语言在嵌入式软件开发中的比例1.2一个小测验C语言的复杂和灵活主要体现在C语言语法的灵活以及允许程序员对底层存储器的直接操作上(这两点又恰恰是C语言最优美、最强大的地方)。下面我们来做一个小小的测验,5请阅读以下的代码并找出其中的错误和潜在的危险因素。注意:这段代码本身并没有什么实际意义,只是将人口参数ptr所指向的内容通过两个内部缓冲区p和q复制到局部数组bufL]中,并将该数组的首地址作为返回值传递到函数外。我在这里只是希望通过这个例子说明C程序语法的正确性与功能或者逻辑的正确性之间有着本质的区别。1 # include stdlib. h3 char test(char ptr)unsigned char 1;56789char buf[8*1024]:char“p,“q;/将数组初始化为0*for(1=0;i<=8*1024;i++)0f[幻1112p=ma11(1024);13if(pa=MULL) return NULL;/*p申请失败,返回空指针*14q=ma1lo(2048)15if (g NULL)return NULL;q申请失败,返回空指针*1617memcpy(p, ptr, 1024)第1章概述18memcpy (q, ptr, 2048)将ptr所指向的内容复制到q19memcpy (buf, p, 1024)/“现在我们将p和q中的内容合并到buf数组中“buf s buf 1024;嵌入式系统高级C语言编21memcpy(buf, q, 2048);2223free(p);24free(8):2526return but/*将数组buf的首地址返回出去*/27怎么样?你能在上面的代码中发现几处错误或者隐患呢?还是让我们一起来分析程下吧①代码的第1行即有问题。在C语言中包含文件的有两个符号“”或者<>。双引号“的意思是告诉编译器首先在当前目录下搜索需要包含的文件,如果当前目录下没有该文件,则在编译选项指定的系统头文件目录中搜索该文件;尖括号<>的意思则是通知编译器首先在6系统头文件目录中搜索需要包含的文件。在这个例子中,stdb.h是ANIC标准库函数的头文件,般而言这个文件是存放在系统头文件目录中的,因此准确的用法应该是采用尖括号,即# include< stdlib.h>。虽然在大多数情况下采用双引号的包含方式不会产生错误,但如果系统里(比如 Linux下的/usr/ include/)有一个叫作math.h的头文件,而你的源代码目录里也有一个自己写的 math. h头文件,那么此时系统就会默认使用你自己定义的头文件,而这可能并非你的本意。②第5行的定义 unsigned char i是定义一个无符号的8位数,但是请注意一个无符号8位数的范围是0~255,而第9行的for循环中却将i与8×1024进行比较,如果i是一个无符号8位数的话,那么这个数将永远小于8×1024,因为当i的值增长到255时再加1后i将重新变为0,这将使得这个for循环成为一个死循环而永远不会结束。③第6行定义了一个8K字节的数组bu[],从语法上来说这个定义没有任何问题,但是如果我们知道一个局部变量是如何在内存中表示的就会对这样的定义倒吸一口凉气了。编译器对局部变量有两种存储方法,对于简单数据类型的变量(比如int、char、 short或者指针变量等)编译器会首先尽可能地采用CPU内部的通用寄存器来表示,因为寄存器的访问速度远远高于外部存储器的访问速度;第二种方式是对于那些没有办法用寄存器表示的变量或者数组、结构体等变量采用当前的堆栈空间来存储。对于这段代码数组buf]显然是需要存放在堆栈中的,然而8K字节的空间对于大多数系统而言是很容易将堆栈空间耗尽的,因此在局部数组中开设大数组是需要仔细评估的,程序员必须非常清楚自己的堆栈空间是否够用。如果算法必须采用大数组,可以采用“ static char buf[8*1024”的方法来定义,虽然这同时会带来程序不可重入的问题。关于 static关键字我们会在第2章中仔细讲解的。第1章概述④第9行的for循环中,“i<=8*1024"的表达式是错误的,因为在C语言中数组的下标是从0开始的,因此对于bu[]数组,合法的下标取值是从0到(8×1024-1),所以上述的表达式的正确写法应该是“i<8*1024”。令人遗憾的是C语言对于数组越界是不作任何检查入式的。如果按照原来错误的写法在最后一次循环中程序执行了“buf[8*1024]=0x0”的操作,通系常情况下程序在当时不会有任何异常,但是其实紧邻最后一个合法元素“buf81024-1”存统放的另外一个变量已经被错误地修改了,程序只有在访问了该变量时才可能出现不正常,而这高时可能已经离你修改它的第9行很远了⑥第12行中的问题虽然不是错误但却是一个不好的编程风格, malloc(库函数的返回C值是一个指向vod类型的指针,因此好的编程风格应该是在将这个返回值赋给其他类型的指言针变量前进行显式的强制类型转换。所以比较合适的写法应该是“P=(char*)mllc编(1024)”。第14行的 malloc(函数调用也存在同样的问题。程⑥第15行代码中有两个非常隐蔽的错误。我们先来分析第一个:“f(q=NULL)”这个判断式的逻辑是错的,正确的写法应该是“if(q==NULL)”。这是几乎所有C语言使用者都会犯的错误。令人真正害怕的是前面的表达式在语法上是完全正确的,意思是将NULL赋值给变量q然后判断q的取值是否不为0,这个判断的取值永远是“否”,也就是说不管原来的q7是否真的为空,后面的“ return NULL;”语句永远不会执行。更讨厌的是,如果q原来为非空指针,则在经过第15行代码后也会将值改为空。⑦第15行的第二个错误在这个语句的后半部分。如果q指针为空,说明第14行的maloc()函数申请动态内存失败,调用“ return NULL;”语句似乎没有任何问题。但是当程序运行到第15行时实际上有一个潜在的条件,那就是p指针的动态内存申请一定是成功的(否则早在第13行程序就会“ return NULL”),因此如果我们在q申请不到时直接返回就会将p指针申请的动态内存永远地丢失,这块内存空间永远也不会被释放回系统堆(Heap),这就是所谓的“内存泄漏”( Memory Leak)。内存泄漏是一个慢性错误,由于并不影响正常的程序运行,所以通常情况下在内存泄漏的早期,程序的运行没有任何异常,直到系统堆中的内存空间已经“漏”光了,其他程序调用 malloc()申请内存时总是失败,这时解决问题的唯一办法就是重新复位系统。这一行的正确写法应该是:if q == NULLfree( p )ireturn NULL;⑧第17行中包含了一个隐蔽的错误。调用 memcpy()函数将人口参数ptr所指向的内第1章概述存复制1024个字节到p指针所指向的内存空间。但程序在将pr入口参数作为 memcpy()嵌函数的参数时没有对ptr是否为空进行检查,这是因为在通常情况下标准C库函数为了效率入往往不对人口参数进行合法性检查如果pr为空那么“ memcpy(p,ptr,1024);”这个语句式运行系统就崩溃了⑨第18行应该是“ memcpy(q,ptr+1024,2048);”。因为前1024个字节已经被第17行统高级C执行完毕,后面的数据应该从ptr指针向后偏移1024个字节开始。⑩第20行是一个语法错误,但是在写代码时往往被程序员忽略。理解这个语法错误首C|先要理解C编译器是如何处理数组的。在C语言程序编译的过程中,编译器要为数组所占用语的内存分配空间,因此在C中没有动态数组的概念,数组在存储器中的位置(也就是地址)和编容量在编译时就已经确定了,并且在程序运行过程中不再发生改变。编译器将数组的名字作程为一个符号并将该符号与数组实际存放在内存中的地址对应起来。因此在C语言中数组名就是数组的首地址,这个首地址已经在编译的时候确定,不能再改变了。所以“buf=buf+1024;”这个语句是有语法错误的,编译器会报错①最后一个错是第26行的返回语句“ return buf;”。正如前面所述,buf[数组是通过堆栈存放的,因此将堆栈中的地址作为指针传递到函数外部是非常危险的。因为大家都知道在我们出函数后,该函数的栈帧就已经无效了,原来的这个堆栈空间随时都可能被用作其他用途,返回这段内存的地址毫无意义。如果通过这个指针去修改其所指向的内容,将很容易地将系统堆栈写“脏”,将保留在新的栈帧中的数据覆盖。在第1章安排这样的小测验的目的是想说明仅仅掌握C语言的语法正确对于编写正确无误的C程序是远远不够的,要想写好C程序还必须掌握更多的知识和技巧。在下一节将向读者介绍除了掌握语法之外,还有哪些知识是需要了解和掌握的。1.3如何学好嵌入式系统中的C语言编程1.3.1真正深刻地认识存储器冯·诺伊曼说过“程序等于算法加数据结构”。首先,算法是什么?算法是通过存储在存储器中的程序代码实现的。其次,数据结构又是什么?数据结构是存放在存储器中的各种类型的数据。程序本质上就是处理器通过执行存放在存储器中的程序代码对存放在存储器中的数据进行操作和变换的过程。在这个过程中除了处理器本身外,最核心的环节就是存储器。因为不管是程序的可执行代码还是数据都是存放在存储器中的。撇开代码、变量、数组、指针结构、堆栈等这些软件中的各个元素的表象,剩下的本质就是存储器!因此,理解C语言的关键是真正理解存储器。每一个存储单元都有两个属性:一是存储器里面存放的内容;二是存储器的地址。这个内容可以是代码,也可以是数据,甚至是另一个存储单元的地址(这个时候往往我们称这个存储
用户评论