HC08 C语言程序设计<br /><br />1. 编程语言的使用原则<br />在目前的单片机应用系统研制过程中,主要使用汇编语言和C语言作为开发语言,两者各有特点。<br /> 汇编语言的特点:<br />l 直接操作硬件及资源。<br />l 生成代码精简高效。<br />l 起步要求较高,程序员需对硬件有深刻的了解,一个好的程序员往往能大大减少开发与调试的周期。<br />l 可移植性较差,硬件或资源改变代码需要大量修改。<br />C语言的特点:<br />l 对程序员要求较低,往往只需要程序员了解一定的硬件知识即可。<br />l 可移植性较好,可以编写跨平台的嵌入式软件。<br />l 不需要程序员管理堆栈。在调用子程序和中断子程序时,不需要用户处理繁锁的栈操作,加快了开发进度,同时减少了栈操作出错的可能性。<br />l 生成代码的效率取决于编译器和程序员的编程风格。HC08C的编译器已经对部分硬件资源进行了封装,如:堆栈、子程序的跳转、中断处理时现场的保护。C代码中的函数、控制语句在编译时都会被编译器编译成相应的汇编指令,这些语句生成的代码量也不尽相等。而且同样功能的总代码生成量比使用汇编语言略高20%<br />任何一款编译器都不会比一个有经验的汇编语言程序员编写的汇编程序效率高,但是写一个好的C程序然后转换为高效的汇编程序比直接写高效的汇编程序就容易多了。<br />综上所述,无论是采用C语言还是汇编语言都各有其利弊。我们既不推荐在学习与开发嵌入式软件过程中完全采取汇编语言(因为汇编语言对一个不懂硬件的新手来说,是需要一定的时间才能上手的),也不赞同完全使用C语言(因为有许多底层的操作,C的语句是无法做到的)。所以在开发过程中,我们往往采用C和汇编结合的一种编程风格,要充分利用这两者的优势。例如我们通常把底层的对硬件的操作留给汇编指令,把与硬件无关或相关性较少的部分用C代码实现。当然,要充分发挥两者的性能,需要程序员对编译器有一定了解,并注重平时的积累<br />2. C语言对硬件的要求<br />并不是所有的MCU都可以用C语言来开发,它们必需具备一定的硬件条件:<br />l 完整的指令系统<br />l 拥有分别为运算和指针服务的16位寄存器,在HC08中就是HX和PC<br />l 堆栈指针和堆栈结构指针,在HCS08 C 语言中它们都是SP<br />l 连续的地址空间<br />3. 嵌入式C语言与标准C语言的区别<br />纯粹的ANSI C语言并不适合单片机开发,这是因为:<br />l 嵌入式系统与硬件密切相关<br />l 大部分嵌入式系统使用中断<br />l ANSI C 语言的变量类型提升规则对存储器消耗非常大<br />l 有些MCU不支持C语言堆栈.<br />l 很多MCU具有多种存储空间,例如51单片机的程序存储器、数据存储器、特殊功能存储器等 <br />因此嵌入式C语言必需适合嵌入式的特殊环境:<br />l 有限 RAM<br />l 有限 ROM<br />l 有限堆栈空间<br />l 要求具有硬件编程向导<br />l 严格的时序(ISR, tasks, ...)<br />l 必需具有多种指针类型 (far/near/rom/uni/paged/...)<br />l 必需具有特殊关键字/符号 (@, interrupt, tiny, ...)<br />4. 嵌入式C编译器应具有的功能<br />l 适合ROM代码<br />l 优化代码<br />l 重入代码<br />l 支持CPU系列的不同成员<br />l 支持不同的存储器模式<br />对于一个编译器我们还应该注意以下细节,不同的编译器在这些方面差异较大<br />l 能内嵌汇编、混合编程<br />l 具有中断服务编程能力<br />l 可输出汇编语言源文件<br />l 带有标准库<br />l 具有初始化硬件和建立C语言运行环境的启动代码<br />5. 嵌入式C连接器应具有的功能<br />l 合并不同代码段<br />l 为CPU分配存储器 (RAM, ROM, stack, special areas)<br />l 产生调试文件 (包括符号、行号等)<br />l 产生目标文件的存储器映像<br />16.1.2 CodeWarrior for HCS08<br />Metrowerks 是MOTOROLA于1999收购并独立运作的子公司,其软件产品CodeWarrior专门是面向Motoroal(freescale)所有的MCU与DSP嵌入式应用、跨平台的软件工具,是Motorola向用户的推荐产品。<br />CodeWarrior for HCS08 是面向以HC08或S08为CPU的单片机嵌入式应用开发的软件包。其中包括集成环境IDE、处理器专家库、全芯片仿真、可视化参数显示工具、项目工程管理器、C交叉编译器、汇编器、链接器以及调试器。可以完成从源代码编辑、编译到调试的全部工作,本书将在以后的章节中介绍其使用方法,以下所提到C语言均是指CodeWarrior for HCS08的C交叉编译器所支持的C语言。<br />16.2 HCS08 C语言的扩展语法<br />HCS08C语言的语法基本与标准C相同。有关标准C语言的语法本书不再介绍,请读者自行参考相关书籍。本节主要介绍HCS08 C语言对标准C语言的扩展语法。<br />16.2.1基本数据类型<br />数据类型 大小 无符号(unsigned)数据范围 有符号(signed)数据范围<br />Char 8 bits 0 ~ 255 –128 ~ 127<br />Short int 16 bits 0 ~ 65535 –32768 ~ 32767<br />Int 16 bits 0 ~ 65535 –32768 ~ 32767<br />long int 32 bits 0 ~ 4294967295 –2147483648 ~ 2147483647<br /> ANSI C没有为int类型规定长度,不过八位的单片机的C编译器一般都把int定义为16位有符号数。但是由于8位机处理8位数据是效率最高的,所以int类型和更大的类型都应该在必需要时才使用。最好不用使用浮点数。<br />char类型可能在不同编译器有不同的定义,或者根本没有定义,为了便于程序移植要,最好不要用char这个符号应给它起个别名。<br />可以为所有用到的数据类型进行重新定义。<br />typedef unsigned char UINT8;<br />typedef signed char SINT8;<br />typedef unsigned int UINT16;<br />typedef int SINT16;<br />typedef unsigned long int UINT32;<br />typedef long int SINT32;<br />使用这样的定义程序移植性较好。<br />为了节约有限的RAM资源应该为每个变量尽量用最小的数据类型;尽量少用有符号数据;可以在表达式中用强制类型转换把数据长度缩到最短。<br />变量定位 <br />普通变量的定义和访问同标准C语言,在HCS08 C语言中我们主要要解决映像寄存器变量和某些特殊变量的定位问题,即把这些变量存放在RAM中指定的位置。<br />1映像寄存器定位<br /> 映像寄存器单片机中跟硬件有关的寄存器,它们都有各自固定RAM地址,其定位有3种方法<br />1)宏定义<br />例如:#define PortA ( * ( volatile unsigned char * ) 0x0000 )<br /> 这样 PortA 成为一个地址在0x0000的unsigned char类型变量。这个定义看起来很复杂,其实它也可以分解成几个很简单的部分来看。 ( volatile unsigned char * )是C语言中的强制类型转换,它的作用是把0x0000这个纯粹的十六进制数转换成为一个(地址)指针,其中volatile并不是必要的,它只是告诉编译器,这个值与外界环境有关,不要对它优化接下来在外面又加了一个*号,就表示0x0000内存单元中的内容了。经过这个宏定义之后,PortA就被可以做为一个普通的变量来操作,所有出现PortA的地方编译的时候都被替换成( * ( volatile unsigned char * ) 0x0000 ),外面一层括号是为了保证里面的操作不会因为运算符优先级或者其它不可预测的原因被改变而无法得到预期的结果。<br />这种定义方法适合所有的C编译器,可移植性好,但PortA并不是一个真正的变量,只是一个宏名,当你调试一个程序的时候,无法在调试窗口观察它的值。另外连接器也失去了灵活性,它得防止其它变量跟此变量冲突。<br />2)使用@关键字<br />例如: volatile unsigned char PortA @0x0000;<br />@是编译器扩展的一个特殊修饰符,其它编译器很可能并不认识。这种定义具有很好的可读性,失去了可移植性。 <br />3)使用段定义<br />这种方法分为2个步骤<br />首先把变量定义在段中,其次在连接参数文件(*.prm)中把段定位在一个合适的位置<br />例如:第1步:在源程序文件中<br />#pragma DATA_SEG PORTB_SEG<br />volatile unsigned char PortA;<br />#pragma DATA_SEG DEFAULT<br />这样变量 PortA 定义在段 PORTB_SEG 中<br />第2步:在 prm 文件中 <br />SECTIONS<br />PORTB_SEG = READ_WRITE 0x0000 SIZE 1;<br />这样段 PORTB_SEG 定位在地址0x0000上。<br />这种方法可移植性也很差,如果你要把它移植在别的编译器上,你不得不修改源程序。<br />2变量定义修饰符<br />变量定义有三个修饰符值得注意,虽然它们与标准C是相同的,但是在嵌入式C语言中又有不同的含义。<br />1) static<br />在子函数中static用声明的变量是局部变量,但是退出这个子函数后其值不消失。下一次调用这个函数时仍可以访问到原来的值。注意,在子函数中声明的static变量只对声明他的函数可见,别的函数是不可以使用的。如果static变量是在模块中声明的,那么只有本模块的函数可以使用它,别的模块中的函数是不能访问的。<br />void MyFunction (void)<br />{ <br /> static char myVar = 0; //用 static声明的局部变量<br /> myVar = myVar + 1;<br />}<br />void main (void)<br />{<br /> MyFunction(); //调用之前myVar = 0,调用之后myVar = 1<br /> MyFunction(); //调用之前myVar = 1,调用之后myVar = 2<br />}<br />2) volatile<br />如果一个变量的值可能会被程序操作之外的其它操作所改变,那么你必需用volatile 声明。在嵌入式系统中其它操作是:中断服务程序的操作、硬件动作的操作。<br />用volatile声明的变量是不会被编译器优化掉的,如:<br />volatile unsigned char PortA @0x0000;<br />PORTA做为一个输入端口,其值是由外部设备决定的,由于外部设备的变化是随机的,因此第一次读取的值和第二次读取的值很可能不同,所以我们把它声明为volatile变量。<br />a = PORTA; <br />a = PORTA;<br />由于PORTA是用volatile声明的变量,编译器不会把它优化成一句,而如果不是volatile声明的编译器就会将第二句优化掉,从而程序将会忽略输入端口的变化。<br />通常把嵌入式设备的所有外围器件寄存器都声明为volatile 的。<br />3) const<br />修饰符 const 可以用在任何变量之前, 告诉编译器把此变量存储在ROM中。ROM_VAR段是定位 const 变量的默认段<br />语法格式:#pragma CONST_SEG <段名><br />例如:<br />#pragma DATA_SEG DEFAULT<br />#pragma CONST_SEG DEFAULT<br />static int a;//变量 a 存放在默认的 RAM 段 DEFAULT_RAM 中,DEFAULT_RAM是段名<br />static const int c0 = 10;//变量 c0 存放在默认的 ROM 段 ROM_VAR 中,ROM_VAR是段名<br />此时编译器选项-Cc必需是打开的。如果编译器选项-Cc必需是关闭的,则变量a和c0都定位在DEFAULT_RAM中。<br />例如:<br />#pragma DATA_SEG MyVarSeg<br />#pragma CONST_SEG MyConstSeg<br />static int a; //变量 a 存放在段MyVarSeg中,MyVarSeg是段名<br />static const int c0 = 10; //变量 c0 存放在段 MyConstSeg 中,MyConstSeg是段名<br />此时编译器选项-Cc必需是打开的。如果编译器选项-Cc必需是关闭的,则变量a和c0都定位在MyVarSeg中。<br />3全局变量和局部变量<br /> 全局变量为整个程序而定义,在整个程序运行期间。它们占用固定的RAM资源,因此除非在必需的情况,否则不要轻易使用。局部变量为某个函数而定义,只在此函数运行的时候,占用栈空间,局部变量实质上是函数运行所需要的一段RAM空间。因此函数不运行时,它们不占用RAM资源。全局变量通常是为了给中断服务函数传递参数定义的,建议定义时把它们定义在一个相对集中的RAM空间。<br />例如:<br />#pragma DATA_SEG SCI_DATA /* 这条预处理指令之后的所有变量定义在 SCI_DATA 段 */<br />char senderBuffer[50];<br />char receiverBuffer[100];<br />...<br />#pragma DATA_SEG DEFAULT /*这条预处理指令之后的所有变量定义在默认段*/<br />在参数文件中必需指定 SCI_DATA 段。<br />4位定义和访问<br />HCS08 C 语言采用直接位访问的方法来访问位,位的定义采用联合和结构数据类型来实现。<br />例如:<br />volatile union {<br />struct {<br />unsigned char MWPR:1; /* MWPR is bit 0 in FEETST */<br />unsigned char STRE:1; /* STRE is bit 1 in FEETST */<br />unsigned char VTCK:1; /* VTCK is bit 2 in FEETST */<br />unsigned char FDISVFP:1; /*FDISVFP is bit 3 in FEETST */<br />unsigned char FENLV:1;/* FENLV is bit 4 in FEETST */<br />unsigned char HVT:1; /* HVT is bit 5 in FEETST */<br />unsigned char GADR:1; /* GADR is bit 6 in FEETST */<br />unsigned char FSTE:1; /* FSTE is bit 7 in FEETST */<br />} FEETST_BITS;<br />unsigned char FEETST_BYTE; /* Alternate definition of the<br />port as a 8-bit variables. */<br />} FEETST_struct @0x00F6;<br />/* Define Symbol to access register FEETST */<br />#define FEETST FEETST_struct.FEETST_BYTE<br />/* Define Symbols to access single bits in FEETST */<br />#define MWPR FEETST_struct.FEETST_BITS.MWPR<br />#define STRE FEETST_struct.FEETST_BITS.STRE<br />#define VTCK FEETST_struct.FEETST_BITS.VTCK<br />#define FDISVFP FEETST_struct.FEETST_BITS.FDISVFP<br />#define FENLV FEETST_struct.FEETST_BITS.FENLV<br />#define HVT FEETST_struct.FEETST_BITS.HVT<br />#define GADR FEETST_struct.FEETST_BITS.GADR<br />#define FSTE FEETST_struct.FEETST_BITS.FSTE<br />这里的“:1”表示仅需要一个位,HCS08会把它们包装在一起形成一个字节。这样我们就可以以字节方式或位方式访问整个寄存器和位。<br />例如:<br />FEETST =0X80;//字节方式<br />MWPR = 1;//位方式<br />VTCK=1; // BSET 2,FEETST1<br />FDISVFP=0; // BCLR 4,FEETST1<br />中断服务程序定义 <br /> 在HCS08 C 语言中中断服务程序是用中断函数来实现的。中断函数即没有入口参数也没有返回值,但可以全局变量来实现。中断函数的定义方法有3种<br />1)用预处理“#pragma TRAP_PROC”定义<br />这种定义方法分为两步:首先在源程序中定义中断函数,其次在参数文件中指定各中断函数在中断向量表中的地址,即在参数文件中指定一个地址,该地址的内容是中断服务程序入口地址(向量)。<br />例如:<br />unsigned int intCount = 1;<br />#pragma TRAP_PROC<br />void IntFunc(void)<br />{<br />intCount++;<br />}<br />#pragma TRAP_PROC<br />void IntFunc2(void)<br />{<br />intCount--;<br />}<br />#pragma TRAP_PROC<br />void IntFunc3(void)<br />{<br />intCount=intCount*5;<br />}<br />#pragma TRAP_PROC 仅对紧跟着它的函数有效,通知编译器位于它下面的函数是中断函数,其返回指令是RTI,而不是RET,因此每个中断函数前面都必需有这个预处理。<br />在参数文件中要加入以下内容:<br />VECTOR ADDRESS 0xFFF0 IntFunc1 /* 0xFFF0 包含 IntFunc1 的地址 */<br />VECTOR ADDRESS 0xFFF2 IntFunc2 /* 0xFFF2 包含 IntFunc2 的地址*/<br />VECTOR ADDRESS 0xFFF4 IntFunc3 /* 0xFFF4 包含 IntFunc3的地址 */<br />2)用关键字“interrupt”<br />格式为:<br />interrupt <函数名><br />/* 在参数文件中指定中断类型号 */<br />{<br />…/*代码*/<br />}<br />关键字’interrupt’通知编译器位于它后面的函数名是中断函数,同样也要在参数文件中指定各中断函数在中断向量表中的地址。<br />例如:<br />interrupt IntFunc2()<br />{<br />…..<br />}<br />在参数文件中加入:<br />VECTOR ADRESS 0xFFF2 IntFunc2<br />3)用关键字“interrupt”和中断向量号<br />格式为:<br />interrupt <中断向量号> <函数名><br />{<br />…/*code*/<br />}<br />这种定义方法关键字’interrupt’通知编译器位于它后面的函数名是中断函数,且通过中断向量号指定了各中断函数在中断向量表中的地址。这种方法就不再需要修改参数文件,移植性较好。<br />中断向量号与中断向量表地址的对应关系如下:复位向量为0号位于地址0xFFFE,1号紧跟着0号,位于地址0xFFFC,其余依此类推。<br />另外用预处理命令#pragma TRAP_PROC: SAVE_REGS可以确保在中断函数中,所有CPU寄存器或编译器使用的伪寄存器内容不会被中断函数破坏。<br />
|