《C语言的嵌入式编程.ppt》由会员分享,可在线阅读,更多相关《C语言的嵌入式编程.ppt(55页珍藏版)》请在三一办公上搜索。
1、MC9S12XS单片机原理及嵌入式系统开发,合肥工业大学张 阳,吴 晔,滕 勤 Email:,TEL:,,第4章 C语言的嵌入式编程,编程语言的选择 C语言编程元素 C程序编译器与交叉编译器 CODEWARRIOR软件简介,前言,本章首先通过编程语言的选择问题介绍C语言编程的优点,然后讨论C语言进行程序设计时涉及的一些问题,并简要介绍了Freescale公司的单片机开发工具 CodeWarrior的使用方法。,4.1 编程语言的选择,为了确定嵌入式系统合适的编程语言,需要了解以下问题:计算机(如微控制器、微处理器或DSP芯片等)只接受“机器码”(即目标代码)指令。如果严格定义,机器码才是计算机
2、的语言,而不是程序员使用的其他语言。但如果由程序员去解释机器码,则工作量是非常巨大的,而且也容易出错,是不可行的。所有的软件,例如汇编语言、C语言、C+语言、Java语言等,为了能够被计算机执行,最终都必须翻译成机器码。嵌入式处理器的功能有限且内存有限,所以编程语言必须具有高效率。为嵌入式系统编程,经常需要对硬件进行底层访问操作,这意味着至少要能够读写特定的存储器地址。,4.1 编程语言的选择,当然,语言的选择问题还有一些并非技术方面的考虑:如果每个项目开发都从头编写代码,显然软件程序员是不乐意的。编程语言必须能够支持创建灵活方便的库,这样同类的项目可以重用那些经过充分测试的代码模块。当使用新
3、的处理器或升级处理器时,整个代码系统移植到新系统应该是可行的,并且工作量尽可能少。语言的选择应该具有通用性。这样才能保证比较容易产生更多的有经验的开发人员,而且开发人员也容易获得相关设计实例以及编程实践信息。随着系统和处理器的不断升级,程序代码往往需要经常进行维护。好的程序代码应该是容易被理解的,而且并不仅仅容易被开发者理解,同时程序代码的维护、升级也应该非常便利。,4.1 编程语言的选择,C语言的特性如下:它属于“中级语言”,不仅具有“高级语言”的特征(如支持函数和模块),还有“低级语言”的特性(可以通过指针访问硬件);编程效率很高;十分流行且容易理解;即使是PC程序员,以前只使用过Java
4、或C+语言,也能够很快理解C语言的语法和编程方法;每一个嵌入式处理器(从8位到32位或以上)都有良好且得到充分验证的C编译器;容易找到C语言编程经验的开发人员;容易找到有关资料、培训课程及相关网站等技术支持。,4.2 C语言编程元素,4.2.1 全局变量和局部变量,变量是程序运行时在内存中存放数据的一个存储空间。对嵌入式系统来说,它是RAM或ROM(甚至是处理器的寄存器)上的存储单元。全局变量是为整个程序定义的,在程序运行中始终有效。用全局变量传递参数,是参数传递的常用方法。局部变量是在某个函数内部声明的变量,它只能被该函数访问。在嵌入式系统中,局部变量通常位于堆栈中。全局变量和局部变量的区别
5、取决于在程序中的什么位置声明它。全局变量必须在函数外部声明,而局部变量则必须在一个函数内部声明。由于程序是固化在ROM中的,而不是下载到RAM中的。除非在应用程序运行开始后向RAM中下载什么,RAM中的内容在开机时是随机的。这就要求在用C语言开发嵌入式应用程序时不要使用初始化变量。当希望在多个源文件中共享变量时,需要确保定义和声明的一致性。最好的安排是在某个相关的.c文件中定义,然后在.h头文件中进行外部声明,在需要使用的时候,只要包含对应的头文件即可。定义变量的.c文件也应该包含该头文件,以便编译器检查定义和声明的一致性。,4.2 C语言编程元素,4.2.2 头文件,通常在一个程序的开始部分
6、进行头文件包含操作。头文件通常包括常量定义、变量定义、宏定义和函数声明等,程序员可以在自己的程序中嵌入它们。内嵌库中最常见的头文件是标准输入/输出文件(stdio.h),该头文件包含用于输出信息和接收用户键盘输入的函数声明。在很多情况下,出于特定系统要求,程序员通常需要创建自己的头文件,并将它们包含在程序中。要包含一个头文件,必须在程序的开始部分使用编译预处理指令#include。,4.2 C语言编程元素,4.2.3 编译预处理,1用于包含文件的#include指令 任何C程序首先都要包含那些准备使用的头文件和源文件,include是一个用于包含某个文件内容的预处理指令。以下给出可以被包含的文
7、件:包含代码文件:这些文件是已经存在的代码文件。包含常量数据文件:这些文件是代码文件,可以有扩展名.const。包含字符串数据文件:这些文件是包含字符串的文件,可以带扩展名.string、.str或者.txt。包含初始数据文件:这些文件用于嵌入式系统掩模只读存储器的初始或默认数据,启动程序运行后会被复制到RAM当中,可以具有扩展名.init。包含基本变量文件:这些文件是存储在RAM中的全局或者局部静态变量文件,因为它们不具有初始(默认的)值,所谓静态的意思是变量只有一个普通的变量地址实例,这些基本变量都被存储在以.bss为扩展名的文件中。包含头文件:这是一个预处理指令,目的是要包含一组源文件的
8、内容(代码或者数据)。它们都是某个特定模块的文件。头文件的扩展名为.h。,4.2 C语言编程元素,4.2.3 编译预处理,2宏定义#define指令 语言中允许用一个标识符来表示一个字符串,称为宏。被定义为宏的标识符称为宏名。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏代换或宏展开。宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。,4.2 C语言编程元素,4.2.3 编译预处理,2宏定义#define指令 对于宏定义再做以下几点说明:宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的代换,字符串中可以含任何字符
9、,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也会一起置换的。宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束,如果要终止其作用域可使用#undef命令。,4.2 C语言编程元素,4.2.3 编译预处理,3条件编译指令 条件编译指令包括#if、#ifdef、#ifndef、#else、#elif和#endif。这些指令用于根据某个表达式有条件的编译一部分代码。可以仅在程序开发过程中利用这些指令来编译部分调试代码。指令#if和#endif用于选择性地编译某段代码。#if
10、后的表达式值为TURE或FALSE。如果是TRUE,#if和#endif之间的所有代码将被编译;否则,这些代码将被忽略。#else和#elif指令可以用于更灵活选择编译功能,它们也必须同#if和#endif一起使用。,4.2 C语言编程元素,4.2.4 数据类型,4.2 C语言编程元素,4.2.4 数据类型,在声明变量的时候,可以规定变量的访问/存储类型,C语言有6个访问/存储关键字:extern、auto、static、register、const和volatile。extern说明该变量在另一个目标代码文件中声明和定义过。这些变量可以被所有函数访问。auto是默认的存储类型,在一个代码块内
11、(或在一个函数头部作为参量)声明的变量,无论有没有访问/存储关键字auto,都属于自动存储类。该类具有自动存储时期、代码块的作用域和空链接,如果未初始化,它的值是不确定的。在S12单片机中,这种类型的变量存放在栈中。一旦某个函数(一段程序)结束任务,这些用auto声明的变量将从栈中清除,不再有效。另外,只有声明这种变量的函数才有权访问该变量。,4.2 C语言编程元素,4.2.4 数据类型,static存储类型与auto类型类似,但它存储在RAM中而不是栈中,因此它在程序运行的整个过程中都有效。在C语言中,关键字static有三个明显的作用:在函数体,一个被声明为静态的变量在这一函数被调用过程中
12、维持其值不变;在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外的函数访问,它是一个本地的全局变量;在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用,也就是说,这个函数被限制在声明它的模块的本地范围内使用。register声明的变量表明要求编译器使用(如果可能)微处理器中的一个寄存器来存放该变量。使用微处理器的寄存器存储一个变量可以减少总线访问存储单元的时间、加速程序的运行。因此,若某个变量在程序中需要经常访问,可以考虑这种存储类型。如果某个变量的值在程序运行中保持不变,则可以用const类型来声明它,该变量通常存放在ROM中。一个const
13、变量必须由程序员初始化。有的程序员认为“const意味着常数”,这种说法其实有一些问题,有一种理解认为const意味着“只读”,这种理解更准确。,4.2 C语言编程元素,4.2.4 数据类型,const有以下作用:关键字const的作用是为给读代码的人员传达非常有用的信息,实际上,声明一个参数为常量是为了说明这个参数的应用目的;为优化器提供一些附加的信息,使用关键字const也许能产生更紧凑的代码;合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,简而言之,这样可以减少bug的出现。有关const的所有用法,建议读者参考Dan Saks的文章“c
14、onst T vs.T const”。volatile访问类型表示它所声明的变量值在程序运行中可能不经过相关指令就发生变化。在S12单片机中,当某个变量的值被硬件输入端口改变时,这些变量应该用volatile声明。,4.2 C语言编程元素,4.2.5 运算符,4.2 C语言编程元素,4.2.5 运算符,4.2 C语言编程元素,4.2.6 指针,指针是存放其他变量地址的变量。例如,一个字符型变量指针存放的是该字符变量的地址。声明一个指针变量的格式与声明一个变量的格式相同,只是在变量名前加一个*运算符。指针在C语言中的另一个重要应用是动态内存分配。动态内存分配与我们见到的其他内存分配方式不同,区别
15、在于动态内存分配的存储单元在程序的运行过程中才确定。这些分配的内存通常来自RAM中未被使用的部分,我们称这一部分为堆。动态内存分配常常用在不知道RAM的大小又想充分利用RAM的资源的情况下。动态内存分配的两个主要函数是malloc()和free()。malloc()函数用于分配内存空间,而free()用于释放被分配的内存空间。在嵌入式系统中程序设计中,程序员经常面临者要求去访问某特定的内存位置的情况。此时可以利用指针方便的实现这个要求。,4.2 C语言编程元素,4.2.7 条件语句、循环语句及无限循环语句,1条件语句 条件语句在程序中会经常多次使用。如果某个定义的条件能够被满足,那么执行紧跟在
16、条件语句之后的大括号内的语句(或者不带大括号的语句),否则程序会转到下一条语句或者转到另一组语句中执行。条件语句可以分为if语句和switch语句两大类。,4.2 C语言编程元素,4.2.7 条件语句、循环语句及无限循环语句,2循环语句 C语言有三种不同的循环结构:for循环、while循环和do-while循环。for循环的开头包含一条初始化语句、一个循环条件判断和一条更新语句。在更新语句后是一组指令组成的循环体,这组指令在循环条件满足之前重复执行。while循环与for循环类似,都是重复执行循环体内的指令,但它在while后只有一个循环终止条件。do-while循环基本上和while循环完
17、全一样,唯一的区别是do-while循环先执行循环体语句,再判断终止条件。,4.2 C语言编程元素,4.2.7 条件语句、循环语句及无限循环语句,3无限循环语句 使用C语言总是会被提醒无限循环是不希望发生的,因为这意味着程序永远都不会结束,但是无限循环是嵌入式系统编程的一个特征。无限循环的硬件等价于一个系统时钟(实时时钟)或者一个正在运行的空闲计时器。程序中的main()函数就是一个大循环,其间有对函数进行的调用以及对中断的处理等程序,而最终它必须回到起始处,系统主程序永远都不能出现停止状态。,4.2 C语言编程元素,4.2.8 函数,1函数定义 函数是完成某个特定任务的一段独立代码,它必须具
18、备三个特征:独立性、灵活性、可移植性。一个函数必须独立于程序的其他代码,因为函数可以被不同的用户调用。例如要求编写一个函数滤除输入信号中的噪声(过滤)。你的程序将会被许多不同的程序调用,需要从不同的信号中去除某种噪声。该函数必须独立于使用该函数的程序,并且必须足够灵活。如果编写的去除噪声的函数只能去除某个预先设定的频率段内的噪声,并且这个频率段范围无法改变,那么这个函数的价值就非常有限,尤其是与同频率范围用户指定的函数相比。最后,函数还必须是可移植的。C语言给工业界带来的革命就是提供可以在不同的硬件平台上使用同一段代码的能力,一旦某段代码编写完毕,只要有软件(编译器/汇编器)将其转换为相应的机
19、器代码,它就应该能在不同的硬件上运行。要保证一个函数是可移植的,必须使这段代码与程序的其他部分无关,也就是第一条独立性的要求。,4.2 C语言编程元素,4.2.8 函数,2主程序 主程序也是一种函数,区别在于当程序名被调用时,这个函数首先被执行。主程序是程序执行的管理者,它包含程序的总体结构,通过调用其他不同的函数来处理、完成具体的任务,并因此避免亲自处理这些任务。可以把主函数想象成一个在其他函数的帮助下控制各种命令执行的管理者。,4.2 C语言编程元素,4.2.8 函数,2主程序,4.2 C语言编程元素,4.2.8 函数,2主程序,4.2 C语言编程元素,4.2.8 函数,3函数原型 函数在
20、被调用前必须在程序的头部声明,这些声明称为函数原型。函数声明的格式为:,4.2 C语言编程元素,4.2.8 函数,4函数定义 当某个函数在一个源文件的开始部分声明后,程序员必须在该源文件或其他伴随的源文件中定义这个函数。函数还可以在伴随的库文件中定义。函数定义可以在程序的任何地方进行,但通常都位于主程序后。除了末尾没有分号外,函数定义与函数声明的格式差不多。,4.2 C语言编程元素,4.2.8 函数,4函数定义 例如,假设前面声明的compute函数接受两个输入参数作为一个向量,计算该向量的长度并返回该长度值。该函数可以作如下定义:,4.2 C语言编程元素,4.2.8 函数,5函数调用 在程序
21、地任何地方,都可以用函数名和位于一对圆括号中的参数来调用某个函数。仍然以计算向量的长度为例,可以使用如下方式调用该函数。,4.2 C语言编程元素,4.2.8 函数,6函数间参数的传递 在函数的调用过程中,调用者,即函数的触发者可以向该函数传递多个参数。以上面一个例子作为参考,调用compute函数时,需要传递两个整型参数,因为该函数在定义和声明时就已经确定了两个参数。如果单纯使用C语言编写程序,可以不必关心函数间参数是如何传递的。但是如果希望汇编语言编写的子程序和C语言编写的函数实现相互调用,则汇编语言编写的子程序与C语言编写的函数应该具有相同的格式,这样汇编语言程序就可以在C程序中被调用,同
22、样汇编语言程序也可以调用C的函数。必须彻底弄清楚函数的结构和参数传递方法,才能使汇编语言编写的子程序符合C语言的函数格式,这里提供一种分析方法。,4.2 C语言编程元素,4.2.8 函数,6函数间参数的传递 在C程序中,参数都是通过堆栈传递的,使用的C编译器不同,参数进入堆栈的顺序以及最后一个参数或第一个参数保存在什么地方也会有所不同,故与汇编语言程序的接口方式也会不同。在开发嵌入式应用程序中,因不可避免地会使用到汇编语言,故使用一个新的C交叉编译工具软件时,首先要搞清楚汇编语言程序和C程序之间是如何传递参数的。下面的例子是分析说明不同编译器是如何处理参数传递的,这里以Freescale公司的
23、CodeWarrior编译器为例分析说明。在C中定义以下函数编译器生成如下汇编代码,4.2 C语言编程元素,4.2.8 函数,6函数间参数的传递 在C中调用上面定义的函数编译器生成如下汇编代码,4.2 C语言编程元素,4.2.8 函数,6函数间参数的传递(1)返回参数 对于函数返回的参数,相当于return(n)中的n值,如果n是一个单字节数据(char),则在B寄存器中,即D寄存器的低字节;如果n是一个双字节数据(int),在D寄存器中,低字节在B寄存器中,高字节在A寄存器中,对于返回值n是其他数据类型,则返回一个指向n的指针,也在D寄存器中。(2)定义函数 定义函数时,如果只有一个形式参数
24、,C程序会默认该参数已经放在D寄存器中,参数类型定义同上述返回参数;如果有两个以上的形式参数,最后一个参数(最右端的)在D中,可以以堆栈指针为基地址,加上偏移量寻址其他参数,计算偏移量时要多加2,目的是避开调用该函数时堆栈中保存的程序返回地址。左边第一个参数偏移量值最大,上例中程序调用时堆栈数据结构如图4.2所示。,4.2 C语言编程元素,4.2.8 函数,6函数间参数的传递(3)调用函数(形式参数数目是固定的)如果函数有固定数目的两个以上的形式参数,调用前,从第一个参数(最左端的)开始,从左至右逐个压入堆栈,留最后一个参数在D中,然后调用该函数。故在用汇编语言编写该函数时,存取这些参数,应该
25、到堆栈区以SP为指针,以偏移量+2,+4,进行存取。(4)调用函数(形式参数数目是不固定的)如果函数的形式参数数目是不固定的,如printf()函数,括号内为形式参数表,此时调用函数参数的入栈顺序和(3)相反,编译器会从右向左将参数全部推入堆栈。故定义函数时,到堆栈中访问这些形式参数时,偏移量的顺序也和固定数目的参数情况相反。注意:计算偏移量时要多加3,目的是避开调用过程中本身压入堆栈的子程序返回地址。可以模仿C交叉编译器,用SP寄存器间址;也可以用指令TSX将堆栈指针传给IX,用IX寄存器间址替代SP寄存器间址。,4.3 C程序编译器与交叉编译器,C语言程序设计需要两个编译器。一个编译器在主
26、机上运行,编译器生成目标文件,编译器可以是Turbo C、Borland C、C+、VC等高级语言,用于开发、设计、测试以及调试目标系统;另一个编译器是交叉编译器,交叉编译器也是在主机上运行的,但是它为目标系统生成机器代码,对于大多数的嵌入式系统微处理器和微控制器来说,目标系统多选择指定的或者商用的交叉编译器使用。主机往往同时运行一个提供完整开发环境的编译器和交叉编译器,这意味着可以在主机上仿真、调试、模拟目标系统。,4.3 C程序编译器与交叉编译器,图4.4给出了将一个C程序转换为ROM映像文件的过程。编译器(Compiler)产生目标代码。编译器根据处理器指令和其他说明对代码进行汇编。作为
27、编译的最后一个步骤,嵌入式系统的C编译器必须使用代码优化器(Code-Optimizer)。优化器在链接之前,对代码进行优化。在编译之后,链接器(Linker)将目标代码与其他必要的代码链接在一起。例如,链接器将某些函数代码包含进来,如printf和sqrt代码。设备管理和驱动程序代码(设备控制代码)也是在这个阶段链接的,如打印机设备管理和驱动程序代码。链接之后,创建ROM映像文件的其他步骤与图4.3中所示的步骤相同。,4.4 CodeWarrior软件简介,CodeWarrior集成开发环境(IDE)可以用于MC9S12(X)单片机的程序开发,有效地提高软件开发效率。本节以CodeWarri
28、or 5.1 for S12(X)版本为例简要介绍CodeWarrior的安装及使用方法,使读者初步掌握如何在CodeWarrior下用C语言进行程序设计。,4.4 CodeWarrior软件简介,4.4.1 CodeWarrior的安装,CodeWarrior 5.1 for S12(X)软件可以在Freescale公司网站下载,运行安装文件Setup.exe后,开始解压安装文件,解压完毕后,可以根据图4.5图4.13的界面,完成软件的安装。,4.4 CodeWarrior软件简介,4.4.1 CodeWarrior的安装,4.4 CodeWarrior软件简介,4.4.1 CodeWarr
29、ior的安装,4.4 CodeWarrior软件简介,4.4.1 CodeWarrior的安装,4.4 CodeWarrior软件简介,4.4.1 CodeWarrior的安装,4.4 CodeWarrior软件简介,4.4.1 CodeWarrior的安装,4.4 CodeWarrior软件简介,4.4.1 CodeWarrior的安装,软件安装完成后,可以通过选择“开始程序Freescale CodeWarriorCodeWarrior Development Studio for S12(X)V5.1CodeWarrior IDE”运行软件,可以看到如图4.14所示的软件界面。,4.4
30、CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,选择“File”菜单下的“New Project”选项,会出现如图4.15所示的新建工程向导第1步的界面。,4.4 CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,在界面中部选择“HCS12X”下“HCS12XS Family”下的“MC9S12XS128”,在界面右侧上方可以选择默认的下载调试器,可以根据实际情况自行选择,本书程序均使用TBDML下载调试器完成程序调试,所以选择界面中的“TBDML”。单击“下一步”按钮,出现如图4.16所示的新建工程向导第2步的界面。,4.4 CodeWa
31、rrior软件简介,4.4.2 CodeWarrior使用简介,在界面中部可以选择仅适用汇编语言或者仅适用C语言,也可以同时选择使用汇编语言和高级语言混合编程,这里选择C语言进行程序设计。界面右侧的“Project Name”和“Location”分别定义工程的名称和工程存放位置,也可以单击“Set”按钮后制定工程存放位置。单击“下一步”按钮,出现如图4.17所示的新建工程向导第3步的界面。,4.4 CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,在该步骤中,可以向工程中添加已经存在的文件。本例不添加任何文件,直接单击“下一步”按钮,出现如图4.18所示的新建工程向
32、导第4步的界面。这里可以选择是否使用处理器专家模式,处理器专家自动代码生成器将会极大地帮助开发者降低系统开发时间,提高代码质量,同时还方便开发者将应用代码移植到Freescale其他的微控制器上。本例使用默认选项“None”。单击“下一步”按钮,出现如图4.19所示的新建工程向导第5步的界面。,4.4 CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,这里可以选择希望使用的启动代码、存储器模型和是否支持浮点数格式。本例各项均使用默认选项。单击“下一步”按钮,出现如图4.20所示的新建工程向导第6步的界面。,4.4 CodeWarrior软件简介,4.4.2 CodeW
33、arrior使用简介,这里可以进一步选择存储器模型的一些细节选项。本例各项均使用默认选项。单击“下一步”按钮,出现如图4.21所示的新建工程向导第7步的界面。,4.4 CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,这里可以选择是否使用PC-lint。,4.4 CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,打开项目管理窗下“Sources”文件夹下的“main.c”文件,就可以进行程序编写工作了。项目管文件的组织如图4.23所示。,4.4 CodeWarrior软件简介,4.4.2 CodeWarrior使用简介,当程序编写完成后,可以单击项目管理窗中工具栏中的“调试(Debug)”按钮,进入在线调试状态,如果此时有TBDML下载调试器,就可以完成程序的下载和在线调试工作了。程序调试界面如图4.24所示。此时,可以对系统程序进行单步、断点、全速执行等调试工作,直至系统程序设计达到目标要求。,The End,