第5章 08C语言及第一个08C工程

更新时间:2024-04-09 08:03:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

第5章 08C语言及第一个08C工程

第5章 08C语言及第一个08C工程

C语言作为一种通用的高级语言,语言简洁、紧凑,运算符丰富,具有现代化语言的各种数据结构,特别是灵活的指针类型。随着MCU资源的丰富、C语言编译效率的提高,基于MCU的应用系统中采用C语言编程也成为一种趋势。本章首先简要介绍了标准C语言的基本语法,比较了08C语言和标准C的差异,然后阐述了08C的一些特殊用法,接下来通过实例讲述08C语言编程框架,最后结合我们的实际开发经验,总结了08C中的一些高级用法及编程技巧。

使用C语言进行单片机程序开发,它的编程方法和编程手段与PC机上使用C语言还是有很大差别,只有对单片机体系结构和硬件资源作详尽了解,才能写出高质量高效率的C语言程序。读者在实际学习过程中,要多加练习,从实际编程中体会单片机的C语言编程方法。本章给出了08C的第一个C语言工程,读者要仔细体会工程文件的组织,如何更有效、更直观地反映工程的内涵。

5.1 标准C语言的基本语法

C语言是在70年代初问世的。1978年美国电话电报公司(AT&T)贝尔实验室正式发表了C语言。同时由B.W.Kernighan和D.M.Ritchit合著了著名的《THE C PROGRAMMING LANGUAGE》,简称为《K&R》,也有人称之为K&R标准。但是,在《K&R》中并没有定义一个完整的标准C语言,后来由美国国家标准学会在此基础上制定了一个C语言标准,于1983年发表,通常称之为ANSI C或标准C。

本节简要介绍C语言的基本知识,特别是一些和单片机编程密切相关的基本知识,未学过标准C语言的读者可以通过本节了解C语言,对于C语言很熟悉的读者,可以跳过本节。

5.1.1 数据类型

C语言的数据类型有基本类型和构造类型两大类。基本数据类型如表5-1所示。

表5-1 C语言基本数据类型

数据类型 signed char unsigned char signed short unsigned short signed int unsigned int signed long unsigned long float 简明含义 有符号字节型 无符号字节型 有符号短整型 无符号短整型 有符号整型 无符号整型 有符号长整型 无符号长整型 浮点型 位数 8 8 16 16 16 16 32 32 32 字节数 1 1 2 2 2 2 4 4 4 值域 -128~+127 0~255 -32768~+32767 0~65535 -32768~+32767 0~65535 -2147483648~+2147483647 0~4294967295 3.4E-38~3.4E+38 1

第5章 08C语言及第一个08C工程

double 双精度型 64 8 1.7E-312~1.7E+312 注:08C语言的double类型长度为4字节。

构造类型有数组、结构、联合、枚举、指针和空类型。结构和联合是基本数据类型的组合。枚举是一个被命名为整型常量的集合。空类型字节长度为0,主要有两个用途:一是明确地表示一个函数不返回任何值;二是产生一个同一类型指针(可根据需要动态地分配给其内存)。

5.1.2 运算符

C语言的运算符与大多数计算机语言基本相同,分为算术、逻辑、关系和位运算及一些特殊的操作符。表5-2列出了C语言的运算符及使用方法举例。

表5-2 C语言的运算符

运算类型 算术运算 运算符 + - * / ^ % || && ! > < >= <= == != ~ << >> & ^ | ++ -- += -= >>= <<= *= |= &= ^= %= /= * & 0x 0o 0b 简明含义 举 例 加、减、乘、除 N=1,N=N+5等同于N+=5,N=6 幂 A=2,B=A^3,B=8 取模运算 N=5,Y=N%3,Y=2 逻辑或 A=TRUE,B=FALSE,C=A||B,C=TRUE 逻辑与 A=TRUE,B=FALSE,C=A&&B,C=FALSE 逻辑非 A=TRUE,B=!A,B=FALSE 大于 A=1,B=2,C=A>B,C=FALSE 小于 A=1,B=2,C=A=B,C=TRUE 小于等于 A=2,B=2,C=A<=B,C=TRUE 等于 A=1,B=2,C=(A==B),C=FALSE 不等于 A=1,B=2,C=(A!=B),C=TRUE 按位取反 A=0b00001111,B=~A,B=0b11110000 左移 A=0b00001111,A<<2=0b00111100 右移 A=0b11110000,A>>2=0b00111100 按位与 A=0b1010,B=0b1000,A&B=0b1000 按位异或 按位或 增量运算符 减量运算符 加法赋值 减法赋值 右移位赋值 左移赋值 乘法赋值 按位或赋值 按位与赋值 按位异或赋值 取模赋值 除法赋值 取内容 取地址 无符号十六进制无符号八进制数 无符号二进制数

A=0b1010,B=0b1000,A^B=0b0010 A=0b1010,B=0b1000,A|B=0b1010 A=3,A++,A=4 A=3,A--,A=2 A=1,A+=2,A=3 A=4,A-=4,A=0 A=0b11110000,A>>=2,A=0b00111100 A=0b00001111,A<<=2,A=0b00111100 A=2,A*=3,A=6 A=0b1010,A|=0b1000,A=0b1010 A=0b1010,A&=0b1000,A=0b1000 A=0b1010,A^=0b1000,A=0b0010 A=5,A%=2,A=1 A=4,A/=2,A=2 0xa=0d10 0o10=0d8 0b10=0d2 2

逻辑运算 关系运算 位运算 增量和减量运算 复合赋值运算 指针和地址运算 输出格式转换 第5章 08C语言及第一个08C工程 0d 0u 带符号十进制数 无符号十进制数 0b10000001=0d-127 0b10000001=0u129 注:增量运算符和减量运算符存在运算和取数先后次序,例如,A++是先取变量A的值再对A加1,而++A是先对变量A加1再取A的值。

5.1.3 流程控制

在程序设计中主要有三种基本控制结构:顺序结构、选择结构和循环结构。 (1) 顺序结构

顺序结构就是从前向后依次执行语句。从整体上看,所有程序的基本结构都是顺序结构,中间的某个过程可以是选择结构或循环结构。

(2) 选择结构

在大多数程序中都会包含选择结构。它的作用是,根据所指定的条件是否满足,决定从给定的两组操作选择其一。在C语言中选择结构可以用两种语句来实现:if语句和switch语句。下面分别介绍这两种C语句的具体用法。

① if结构:

if (表达式) 语句项; 或 if (表达式) 语句项; else 语句项;

如果表达式取值真(除0以外的任何值),则执行if的语句项;否则,如果else存在的话,就执行else的语句项。每次只会执行if或else中的某一个分支。语句项可以是单独的一条语句、也可以是多条语句组成的语句块,是语句块时要用大括号括起来:

格式: if(条件)

例: if (var = = TRUE)

{ 语句块 }

printf(\

else

else

{ 语句块 }

printf(\

if语句可以嵌套,有多个if语句时else与最近的一个配对。对于多分支语句,可以使用if ... else if ... else if ... else ...的多重判断结构,也可以使用下面讲到的switch()开关语句。

② switch结构

switch是C语言内部多分支选择语句,它根据某些整型和字符常量对一个表达式进行连续测试,当一常量值与其匹配时,它就执行与该变量有关的一个或多个语句。switch语句的一般形式如下:

3

第5章 08C语言及第一个08C工程

格式:switch(表达式) 例: { switch(number) case 常数1: { 语句项1 case 1: break; printf(“First”); case常数2: break; 语句项2 case 2: break; printf(“Second”); ???? break; default: default: 语句项 printf(“input a number”);

} }

根据case语句中所给出的常量值,按顺序对表达式的值进行测试,当常量与表达式值相等时,就执行这个常量所在的case后的语句块,直到碰到break或switch语句执行完成为止。若没有一个常量与表达式值相符,则执行default后的语句块。Default是可选的,如果它不存在,并且所有的常量与表达式值都不相符,那就不做任何处理。

switch语句与if语句的不同之处在于switch只能对等式进行测试,而if可以对关系表达式或逻辑表达式进行测试。

break语句还可用于循环语句,break语句在switch语句中是可选的,如果不用break,就继续在下一个case语句中执行,一直到碰到break或switch的末尾为止,这样的程序效率比较低。

(3) 循环结构

C语言中的循环结构常用for循环,while循环与do...while循环。 ① for循环 格式为:

for(表达式1;表达式2;表达式3){语句} 执行过程为:

Ⅰ 先求解表达式1;

Ⅱ 再判断表达式2,如果其值为真(非0),则执行for中指定的内嵌语句,然后执行下面的第Ⅲ步。若为假(0),则结束循环,转到循环下面的语句;

Ⅲ 求解表达式3;

Ⅳ 转到上面第Ⅱ步骤继续执行。 For语句的执行流程如图5-1所示。 例: a=0;

4

第5章 08C语言及第一个08C工程

for(I=0;I<100;I++) a=a+2;//循环结束,a=200 ② while循环 格式为: while(表达式) {语句} 当表达式的值为真(非0)时执行while语句中 的内嵌语句。其流程如图5-2所示。其特点是: 先判断表达式,后执行语句。

例: while(bOverFlow) { if(n<100) n++; else bOverFlow = FALSE; }

③ do....while循环 格式为:

do {循环体语句} while(表达式);

其特点是:先执行语句,后判断表达式。它的执行过程与while循环不同的是先执行循环体语句,再判断循环条件。如图5-3所示。

例: do {

if(n<100)

n++; else

bOverFlow = FALSE; }while(bOverFlow);

(4) break语句和continue语句

在循环中常常使用break语句和continue语句,这两个语句都会改变循环的执行情况。break语句用来从循环体中强行跳出循环,终止整个循环的执行;continue语句只用来结束本次循环,而不是整个循环。如以下两个循环结构:

图5-1 for循环流程图

图5-2 while循环流程图

图5-3 do while循环流程图

5

第5章 08C语言及第一个08C工程

① while (表达式1) {

??

if(表达式2) break; ?? }

② while (表达式1) {

??

if(表达式2) continue; ?? }

程序①的流程图如图5-4所示,而程序②的流程图如图5-5所示。请注意两个图中当“表达式2”条件为真时流程的转向。

图5-4 break执行示意图

图5-5 continue执行示意图

5.1.4 函数

所谓函数,即子程序,也就是“语句的集合”,就是说把经常使用的语句群定义成函数,供其他程序调用,这样就可以避免重复编写程序的麻烦,也可以缩短程序的长度。当一个程序太大时,建议将其中的一部分程序改成用函数的方式调用较好,因为大程序过于繁杂容易出错,而小程序容易调试,也易于阅读和修改。函数定义的一般形式如下所示:

类型标识符 函数名(类型 参数1,类型 参数2,类型 参数3,……) {说明部分 语句}

6

第5章 08C语言及第一个08C工程

(1) 使用函数的注意事项

①函数定义时要同时声明其类型。 ②调用函数前要先声明该函数。

③传给函数的参数值,其类型要与函数原定义一致。 ④接收函数返回值的变量,其类型也要与函数类型一致。 (2) 函数的声明

void function1(void)

此函数无返回值,也不传参数。 void function2(unsigned char i, int j)

此函数无返回值,但需要unsigned char类型的参数i和int类型的参数j。 unsigned char function3(unsigned char i)

此函数有返回值,其类型为unsigned char。 (3) 函数的返回值

return 表达式;

return语句用来立即结束函数,并返回一确定值给调用程序。如果函数的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。

5.1.5 指针

指针是C语言中广泛使用的一种数据类型,运用指针是C语言最主要的风格之一。利用指针变量可以表示各种数据结构,很方便地使用数组和字符串,并能像汇编语言一样处理内存地址,从而编出精练而高效的程序。

指针是一种特殊的数据类型,在其它语言中一般没有。指针是指向变量的地址,实质上指针就是存储单元的地址。根据所指的变量类型不同,可以是整型指针(int *)、浮点型指针(float *)、字符型指针(char *)、结构指针(struct *)和联合指针(union *)。

(1) 指针变量的定义

其一般形式为:类型说明符 * 变量名;

其中,*表示这是一个指针变量,变量名即为定义的指针变量名,类型说明符表示本指针变量所指向的变量的数据类型。

例如:

int *point_1;

表示point_1是一个指向整型的指针变量,它的值是一个整型变量的地址。

(2) 指针变量的赋值

指针变量同普通变量一样,使用之前不仅要定义说明,而且必须赋予具体的值。未

7

第5章 08C语言及第一个08C工程

经赋值的指针变量不能使用,否则将造成系统混乱,甚至死机。指针变量的赋值只能赋予地址。

① 指针变量初始化的方法 int a;

int *point =&a;

② 给指针赋值的方法 int a;

int *point; point =&a;

将数值赋给指针将导致错误,例如:int *point; point =1000;是错误的。 被赋值的指针变量前不能再加“*”说明符,如写为*point =&a也是错误的。

(3) 指针的运算

① 取地址运算符 &

取地址运算符&是单目运算符,其结合性为自右至左,其功能是取变量的地址。 ② 取内容运算符 *

取内容运算符*是单目运算符,其结合性为自右至左,用来表示指针变量所指的变量。在*运算符之后跟的变量必须是指针变量。

注意:指针运算符*和指针变量说明中的指针说明符*并非相同。在指针变量说明中,“*”是类型说明符,表示其后的变量是指针类型。而表达式中出现的“*”则是一个运算符用以表示指针变量所指的变量。

main(){

int a=5,*point=&a; printf(\}

表示指针变量point取得了整型变量a的地址。本语句表示输出变量a的值。 ③ 加减算术运算

对于指向数组的指针变量,可以加上或减去一个整数n。设pa是指向数组a的指针变量,则pa+n,pa-n,pa++,++pa,pa--,--pa运算都是合法的。指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。

注意:数组指针变量向前或向后移动一个位置和地址加1或减1在概念上是不同的。因为数组可以有不同的类型,各种类型的数组元素所占的字节长度是不同的。如指针变量加1,即向后移动1个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。

例如: int a[5],*pa;

pa=a; /*pa指向数组a,也是指向a[0]*/ pa=pa+2; /*pa指向a[2],即pa的值为&pa[2]*/

8

第5章 08C语言及第一个08C工程

指针变量的加减运算只能对数组指针变量进行,对指向其它类型变量的指针变量作加减运算是毫无意义的。

(4) void指针类型

顾名思义,void *为“无类型指针”,即用来定义一个指针变量,不指定它是指向哪一种类型数据,但可以把它强制转化成任何一种类型的指针。对于void *类型的指针变量不能进行取内容运算和加减算术运算,因为编译器不知道它指向的具体类型。

众所周知,如果指针p1和p2的类型相同,那么我们可以直接在p1和p2间互相赋值;如果p1和p2指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。

例如: float *p1; int *p2;

p1 = (float *)p2;

而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换: void *p1; int *p2; p1 = p2;

但这并不意味着,void *也可以无需强制类型转换地赋给其它类型的指针,也就是说p2 = p1这条语句编译就会出错,而必须将p1强制类型转换成“int *”类型的。因为“无类型”可以包容“有类型”,而“有类型”则不能包容“无类型”。

5.1.6 结构体

结构体是由基本数据类型构成的,并用一个标识符来命名的各种变量的组合。结构体中可以使用不同的数据类型。

(1) 结构体的说明和结构体变量的定义

定义一个结构体的一般形式为:

①struct 结构体名 ②struct 结构体名 或

{成员列表}; {成员列表}结构体变量;

在①方式中,仅定义了一个结构体。如果要使用这种类型的结构体还需要使用变量定义语句。②方式在定义结构体的同时,定义了结构体变量。例如下面左右两边的代码是等价的。

9

第5章 08C语言及第一个08C工程

struct student

/*定义一个student结构体*/ { char name[8];

int age; char sex[2]; };

struct student student1;

/*定义一个student结构变量(2) 结构体变量的使用

struct student {

char name[8]; int age; char sex[2]; }student1;

/*定义一个student结构体,同时定义一个student结构变量student1*/

结构体是一个新的数据类型,因此结构体变量也可以象其它类型的变量一样赋值运算,不同的是结构体变量以成员作为基本变量。

结构体成员的表示方式为: 结构体变量.成员名

如果将“结构体变量.成员名”看成一个整体,则这个整体的数据类型与结构体中该成员的数据类型相同,这样就像前面所讲的变量那样使用。例如:

student1.age=18;

(3) 结构体指针

结构体指针是指向结构体的指针。它由一个加在结构体变量名前的“*”操作符来定义。例如用上面已说明的结构体定义一个结构体指针如下:

struct student *Pstudent;

使用结构体指针对结构体成员的访问,与结构体变量对结构体成员的访问在表达方式上有所不同。结构体指针对结构体成员的访问表示为:

结构体指针名->结构体成员

其中\是两个符号\和\的组合,好象一个箭头指向结构体成员。例如要给上面定义的结构体中name和age赋值, 可以用下面语句:

strcpy(Pstudent->name,\Pstudent->age=18;

实际上,Pstudent->name就是(*Pstudent).name的缩写形式。 需要指出的是结构体指针是指向结构体的一个指针,即结构体中第一个成员的首地址,因此在使用之前应该对结构体指针初始化,即分配整个结构体长度的字节空间。这可用下面函数完成,例如:

Pstudent=(struct sudent*)malloc(size of (struct student));

size of (struct student)自动求取student结构体的字节长度, malloc() 函数定义了一个大小为结构体长度的内存区域,然后将其地址作为结构体指针返回。

10

第5章 08C语言及第一个08C工程

5.1.7 共用体

有时需要将几种不同类型的变量存放到同一段内存单元中。例如,可把一个整型变量、一个字符型变量放在同一个地址开始的内存单元(如图5-6所示)。以上3个变量在内存中占的字节数不同,但都从同一地址开始(图中设地址为$0080)存放。这种几个不同的变量共同占用同一段的结构,称为“共用体”类型结构。

(1) 共用体的定义

定义一个联合类型的一般形式为: union 共用名 { 成员表 }共用变量名;

成员表中含有若干成员,成员的一般形式为: 类型说明符 成员名 例如:

union abc{ int a; char b; long c; }u1; (2) 共用体的使用

在共用体变量u1被分配的内存单元数量等于长整型变量c的长度(4字节)中。如果整型变量c的赋值为:

u1.c=0x12345678;

则u1.a和u1.b的值也被修改。本例中,它们的值为: u1.a=0x1234; u1.b=0x12;

图5-6 共用体的内存“共用”

5.1.8 位域

有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空

11

第5章 08C语言及第一个08C工程

间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

(1) 位域的定义

位域定义与结构定义相仿,其形式为: struct 位域结构名{ 位域列表 }; 位域列表格式为:

类型说明符位域名:位域长度 例如:

struct bs {

int a:8; int b:2; int c:6; }b1;

说明b1为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:

①一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

struct bs {

int a:4

int :0 //空域

int b:4 //从下一单元开始存放 int c:4 };

在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

②由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

③位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k

12

第5章 08C语言及第一个08C工程

{

int a:1

int :2 //无域名,2位不能使用 int b:3 int c:2 }b1;

从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。

(2) 位域的使用

位域的使用和结构成员的使用相同,其一般形式为: 位域变量名·位域名位

例如在上面定义的位域b1可以这样调用: b1.a=1; //将b1的第0位置1

b1.b=7; //将b1的第3~5位置111 通过位域定义位变量,是实现单个位操作的重要途径和方法,采用位域定义位变量,产生的代码紧凑、高效。

5.1.9 编译预处理

C语言提供编译预处理的功能,“编译预处理”是C编译系统的一个重要组成部分。C语言允许在程序中使用几种特殊的命令(它们不是一般的C语句)。在C编译系统对程序进行通常的编译(包括语法分析,代码生成,优化等)之前,先对程序中的这些特殊的命令进行“预处理”,然后将预处理的结果和源程序一起再进行常规的编译处理,以得到目标代码。C提供的预处理功能主要有宏定义、条件编译和文件包含。

(1) 宏定义

#define 宏名 表达式 表达式可以是数字、字符、也可以是若干条语句。在编译时,所有引用该宏的地方,都将自动被替换成宏所代表的表达式。例如:

#define PI 3.1415926 #define S(r) PI*r*r

(2) 条件编译

#if 表达式 #else 表达式 #endif

如果表达式成立,则编译#if下的程序,否则编译#else下的程序,#endif为条件编译的结束标志。

13

第5章 08C语言及第一个08C工程

#ifdef 宏名 //如果宏名称被定义过,则编译以下程序。 #ifndef 宏名 //如果宏名称未被定义过,则编译以下程序。 条件编译通常用来调试、保留程序(但不编译),或者在需要对两种状况做不同处理时使用。

(3) “文件包含”处理

所谓“文件包含”是指一个源文件将另一个源文件的全部内容包含进来,其一般形式为:

#include “文件名”

5.1.10 用typedef定义类型

除了可以直接使用C提供的标准类型名(如int、char、float、double、long等)和自己定义的结构体、共用体、指针、枚举类型外,还可以用typedef定义新的类型名来代替已有的类型名。例如:

typedef unsigned char INT8U;

指定用INT8U代表unsigned char类型。这样下面的两个语句是等价的: unsigned char i; 等价于 INT8U i; 用法说明:

①用typedef可以定义各种类型名,但不能用来定义变量。

②用typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型。 ③typedef与#define有相似之处,如: typedef unsigned int INT16U; #define INT16U unsigned int;

这两句的作用都是用INT16U代表unsigned int。但事实上,它们二者是不同的,#define是在预编译时处理的,它只能作简单的字符串替代,而typedef是在编译时处理的。

④当不同源文件中用到各种类型数据(尤其是像数组、指针、结构体、共用体等较复杂数据类型)时,常用typedef定义一些数据类型,并把它们单独存放在一个文件中,而后在需要用到它们的文件中用#include命令把它们包含进来。

⑤使用typedef有利于程序的通用与移植。

5.2 08C语言的使用

目前针对嵌入式编程所采用的语言主要是汇编语言和C语言,这两种编程语言同时拥有与生俱来的优点和缺点。汇编语言可以控制硬件且代码紧凑,C语言则具有较好的可读性、可移植性和编码效率。在嵌入式应用中,最理想的选择是将这两种语言结合在一起使用,主体使用C语言,涉及底层驱动使用汇编语言。

08C语言与标准C语法基本相同,用08C编写的源程序编译后的目标代码适用于

14

第5章 08C语言及第一个08C工程

Freescale HC08和S08CPU,而标准C语言编译后的目标代码适用于Intel CPU。另外,08C的函数库和标准C的函数库有些差异,而且用08C在编程时涉及到的硬件知识比较多。本节讲述08C语言编程中需要特别关注的知识点。

5.2.1 寄存器和I/O口的使用

(1) I/O口和寄存器的定位

普通变量的定义和访问同标准C语言,在嵌入式C语言中我们主要要解决映像寄存器变量和某些特殊变量的定位问题,即把这些变量存放在RAM中指定的位置。

在08C语言中操作寄存器及I/O口时,通常预先在头文件中使用宏定义,其定义方法如下:

#define 寄存器名 (*(volatile unsigned char *)寄存器地址) #define I/O口名 (*(volatile unsigned char *)I/O口地址) #define 变量名 (*(volatile unsigned char *)变量地址) 例如:在GP32中,I/O口A的地址为0x0000,定时器1的状态寄存器地址为0x0020,那么在头文件中可以做如下宏定义:

#define PTA (*(volatile unsigned char *) 0x0000) #define T1SC (*(volatile unsigned char *)0x0020)

这个定义看起来很复杂,其实它也可以分解成几个很简单的部分来看:

①( volatile unsigned char * )是C语言中的强制类型转换,它的作用是把0x0000这个纯粹的十六进制数转换成为一个地址指针,其中volatile并不是必要的,它只是告诉编译器,这个值与外界环境有关,不要对它优化,volatile的具体用法在进一步讨论中有所讲述。

②接下来在外面又加了一个*号,就表示0x0000内存单元中的内容。经过这个宏定义之后,PTA就被可以做为一个普通的变量来操作,所有出现PTA的地方编译的时候都被替换成(*(volatile unsigned char * )0x0000)。

③外面一层括号是为了保证里面的操作不会因为运算符优先级或者其它不可预测的原因被改变而无法得到预期的结果。

这种定义方法适合所有的C编译器,可移植性好,但PTA并不是一个真正的变量,只是一个宏名,当你调试一个程序的时候,无法在调试窗口观察它的值。

(2) I/O与寄存器的操作

使用上面定义的I/O口或寄存器宏,可以方便对I/O置高低电平或读取I/O的状态,读写寄存器。

例如:

unsigned char sPortA = PTA; //将A口的状态赋给sPortA变量

PTA = 0xff; //将0xff赋给A口,A口将全为高电平

15

第5章 08C语言及第一个08C工程

5.2.2 位操作方法

在嵌入式系统编程中,位操作使用频率很高。所谓“位操作”是对一个字节中的某一位的值置1或清0,同时不改变其它位的值。例如:用某个I/O口控制小灯亮暗、LCD片选信号等,都需要对I/O口寄存器的某一位置高或置低,并且此时不能影响口上其它位的位值。对于这种操作,用位操作的汇编指令很容易实现。如:

BSET #1 , PTA //置PTA.1为高电平

C语言中没有直接的位指令,它实现位操作的思想是:读出整个寄存器或内存的字节值,改变需要置1或清0的位,然后把整个值写回到寄存器或内存中。

注意:在位操作时,一个字节的最低位是第0位,最高位是第7位。

(1) 用位运算符实现位操作

C语言中提供了6种基本的位运算符:按位与(&)、按位或(|)、按位取反(~)、按位异或(^)、左移(<<)及右移(>>),其基本用法如表5-2所示。

根据与、或和异或运算的特点,可以得出如下结论:

“按位与”运算的特点是参加运算的操作数的某位有0时,结果中的该位就是0,所以“按位与”运算可以实现位的清0操作。

“按位或”运算的特点是参加运算的操作数的某位有1时,结果中的该位就是1,所以“按位与”运算可以实现位的置1操作。

“按位异或”运算的特点是参加运算的某一个操作数的某位有1时,结果中的该位是另一个操作数的相应位取反,所以“按位异或”运算可以实现位的取反操作。

例如:A=0b11011011 B=0b11110111 C=0b00000100 D=0b00000110 则: A&B、A|C、A^D的按位运算如下: A 11011011 A 11011011 A 11011011 B 11110111 D 00000110 C 00000100 ^(按位异或) |(按位或) &(按位与) 11011101 11010011 11011111 在A&B时,A的第3位清0;A|C时,A的第2位置1;A^D时,A的第1和第2位均取反。这里的B、C、D称之为掩码,通过设定不同的B、C、D值可以改变A中不同的位。

掩码可以用16进制或二进制常数表示,也可以用移位运算符的表达式来表示,上面的B和C可用以下方式来声明:

#define B ~(1 << 3) 等价于 #define B 0b11110111 #define C 1 << 2 等价于 #define C 0b00000100

掩码以一个常量值的移位表达式来表示,更明确地指示了要测试的位所在的位置,并且编译器会单独用一个常量来替换这样的常量值表达式,这种形式不会产生任何额外的代码。

例如:改变寄存器device_register第2位值的方法如下:

#define STATUS_MASK 1<<2 //定义掩码

16

第5章 08C语言及第一个08C工程

device_register= device_register | STATUS_MASK //将第2位置1 device_register= device_register & (~STATUS_MASK) //将第2位置0 device_register= device_register ^ STATUS_MASK //将第2位取反

以上三种操作方法可以简写为:

device_register |= STATUS_MASK //将第2位置1 device_register &= (~STATUS_MASK) //将第2位置0 device_register ^= STATUS_MASK //将第2位取反

(2) 测试位

按位“与”运算最常用于测试单个位(或位域)的值,在需要关注的“位”所在位置由单独一个1组成的特征码与操作数作“与”运算,当关注的“位”是1时,结果才是非0值,即逻辑真值。在实际书写时,特征码通常用十六进制、二进制数或移位表达式。例如:要测试第4位是否为1,有以下几种写法:

if ( (bits & 0x10)!=0) if ( bits & 0x10)

if ( bits & 0b00010000) if ( bits & (1<<4))

由于任意非0值都解释为真,所以条件中可以省略对0的冗余比较。 表5-3给出了08C位操作语句编译后所对应的指令,从表中可以看出编译器在编译时,已经做了优化,将这些C语句变成了08CPU中的位指令,达到和汇编相同的执行效率。

表5-3 08C中的位操作语句及对应的编译后的指令

C语句 PTA |=(1<<7); // PTA的第7位置1 PTA &= ~(1<<7); //PTA的第7位清0 PTA ^= 0x80; // PTA的第7位取反 编译后的汇编指令 Bset 7,0x00 Bclr 7,0x00 LDA 0x00 EOR #-128 STA 0x00 LDA 0x00 BIT #-127 BNE 0x×××× if ((PTA & 0x81) == 0) // 检查第7位和第0位是否为0 08C中除了上述的位操作的方法外,还可以综合共用体和位域等多种数据类型,很直观地实现位操作,关于这部分的讲述读者参考本章的进一步讨论部分。

5.2.3 中断处理

上一章已详述过中断的处理过程,C和汇编的这个过程是一致的,这里将不再展开。现在,需要关心的问题是如何在C工程中编写ISR(中断服务例程)。

首先,ISR与C中的正常子函数是有差别的: 第一,正常子函数被编译后的返回指令为RTS,而ISR被编译后的返回指令为RTI。 第二,正常子函数是通过调用方式进入的,而ISR是通过中断机制进入的。 第三,ISR的参数和返回类型总是void。

17

第5章 08C语言及第一个08C工程

现在,就可以带着ISR的这些独有的特性编写它了: ①新建一个Vectors08.c,并加入工程中。 ②定义中断向量表。

在HC08系列嵌入式Flash地址空间中,有一段是用来存储所有的中断矢量 (通常在最后的Flash页面上,参见第2章的存储器映像图),每两个字节存储的是一个中断处理函数的地址。而中断向量表在逻辑上组织了这些地址。

例如MC908GP32的中断向量表如下所示:

#pragma abs_address:0xffdc //中断向量表起始地址 void (* const _vectab[])(void) = { isrDummy, //时基中断

isrDummy, //AD转换中断 isrDummy, //键盘中断 isrDummy, //SCI发送中断 isrDummy, //SCI接收中断 isrDummy, //SCI错误中断 isrDummy, //SPI发送中断 isrDummy, //SPI错误中断 isrDummy, //TIM2溢出中断

isrDummy, //TIM2通道1输入捕捉/输出比较中断 isrDummy, //TIM2通道0输入捕捉/输出比较中断 isrDummy, //TIM1溢出中断

isrDummy, //TIM1通道1输入捕捉/输出比较中断 isrDummy, //TIM1通道0输入捕捉/输出比较中断 isrDummy, //CGM的PLL锁相状态变化中断 isrDummy, //IRQ引脚中断 isrDummy //SWI指令中断

//RESET是特殊中断,其向量由开发环境直接设置(在本软件系统的crt08.o文件中) };

#pragma end_abs_address

中断向量表是一个指针数组,内容是中断函数的地址。 首先要定义该数组的地址,MC908GP32的中断矢量从0xffdc开始(不同的MCU中断矢量起始地址是不相同的,使用时需要查找相关的技术手册),要使用预编译指令将数组的首地址定义在0xffdc。

预编译指令格式:“#pragma abs_address:地址”(可针对不同的芯片改变地址)

中断数组格式:void (* const _vectab[])(void) = {中断处理函数名,中断处理函数名??}

中断矢量表内容,是从中断矢量起始地址开始顺序增加,均与Flash的中断矢量地址相对应,如果某个中断不需要使用,要将在数组对应的项中填入isrDummy。isrDummy()是中断向量表中不需要使用的中断填入的函数,它是一个空函数。

#pragma interrupt_handler isrDummy void isrDummy(void) { }

18

第5章 08C语言及第一个08C工程

③定义ISR并在中断向量表中填入相应ISR的名称; 例如:

若工程中需要定时器1的溢出中断,可以定义如下:

#pragma interrupt_handler isrTimer1 void isrTimer1(void) {

//存放中断处理语句------------------ … … … …

//---------------------------------- }

在定义好中断处理程序之后,还要在中断矢量表的相应位置上填入该中断处理函数名“isrTimer1”。每个中断处理函数的前面都要加一个“#pragma interrupt_handler 函数名”,这是一个预编译指令,告诉编译器,下面的函数是中断处理函数,生成目标代码后,其返回指令将不使用RTS而使用RTI。

通过上述3个步骤,就可以定义好所需要的中断了。在实际编程中,可以直接从给定的C工程框架中得到“Vectors08.c”文件,该文件中只定义了一个空中断处理函数“isrDummy”,和由这个空函数名组成的中断向量表。用户只须定义所需的中断处理函数,并用该函数名代替向量表中相应位置上的“isrDummy”即可。

5.2.4 08C的常用库函数

08C提供一系列函数库供程序员使用,其中囊括了标准C所具有的大部分库函数和一些08C特有的函数,但是08C中的有些函数和标准C中的函数的功能不一样。这些函数的头文件位于安装目录的include目录下,库文件位于安装目录的lib目录下。下面对08C中一些常用的库函数做简要说明,更多的函数库说明参见附录D。

(1) 串口操作类函数

函数的声明如下:

①int printf(char *fmt,?)格式化输出

printf函数是一个标准库函数,它的函数原型在头文件“stdio.h”中。但作为一个特例,不要求在使用 printf 函数之前必须包含stdio.h文件。printf函数调用的一般形式为:printf(“格式控制字符串”,输出列表)其中格式控制字符串用于指定输出格式。格式控制串可由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串,在%后面跟有各种格式字符,以说明输出数据的类型、形式、长度、小数位数等。如“%d”表示按十进制整型输出,“%ld”表示按十进制长整型输出,“%c”表示按字符型输出等。所有的输出都是发送至串口,而不是屏幕。

例如:

void main() {

int a = 88,b = 89;

19

第5章 08C语言及第一个08C工程

printf(\printf(\

printf(\}

②int putc(char c)

putc也是stdio.h中的函数,它只能发送一个字符。 例如:putc(‘a’); //串口发送字符a ③int puts(char *s)

puts比putc功能强一些,可以发送一个字符串,但无法像printf那样做格式化输出。 例如:puts(“hello”); //串口发送hello ④int getchar(void)

通过串行模块接收一个字符。 例如:receive=getchar(); (2) 内存操作类函数

memcpy

声明:void *memcpy(void *s1, void *s2, size_t n)

将以s2为起始地址的n个字节复制到以s1为起始地址的内存中。 例如:

//在0x50上存放着‘hello’5个字符

memcpy((void*)0x0120, (void *)0x50, 5);

//此时,hello已被复制到0x0120起始的内存单元中

5.2.5 08C语言与汇编语言的混合编程

在绝大多数场合采用C语言编程即可完成预期的目的,但是对一些特殊情况进行编程时要结合汇编语言。汇编语言具有直接和硬件打交道、执行代码的效率高等特点,可以做到C语言所不能做到的一些事情,例如:

①一个程序中的关键部分对执行速度有很高要求,实时性强。用汇编编程可以更有效地利用CPU的寄存器和指令集,因此,用其产生的代码比用编译器产生的代码运行更快。

②对特定硬件接口的访问。例如对I/O端口的访问,或者用于禁止、启用中断系统。 因此,选用C语言编程时,还需要夹杂一些汇编程序,通过这种混合编程的方法将C语言和汇编语言的优点结合起来,这已经成为目前MCU开发最流行的编程方法。

(1) 在08C中使用汇编

目前大多数MCU系统,在C语言中使用汇编语言有两种情况:一种是汇编程序部分和C程序部分为不同的模块,或不同的文件,通常由C程序调用汇编程序模块的变量和函数(也可称为子程序或过程);另一种是嵌入式汇编,即在C语言程序中嵌入一段汇编语言程序。

①调用汇编指令构成的子程序

20

第5章 08C语言及第一个08C工程

当汇编程序和C程序为不同模块时,程序一般可分为若干个C程序模块和汇编程序模块,C程序模块通常是程序的主体框架,而汇编程序模块通常由用C语言实现效率不高的函数组成,也可以是已经成熟的、没有必要再转化成C语言的汇编子程序。在这种混合编程技术中,关键是参数的传递和函数的返回值。它们必须有完整的约定,否则数据的交换就可能出错。

定义汇编子程序,定义格式如下: _子程序名 代码 … rts

这种使用方法要注意以下几点: 第一,在子程序名前加‘_’;

第二,汇编子程序只能放在*.s文件中,然后将该文件加入到工程中; 第三,在C代码中调用汇编子程序时可直接调用:子程序名();

第四,在子程序中不能使用映象寄存器的宏定义,只能用它们的直接地址。

第五,汇编子程序的编写时,对于使用过的寄存器需要进行保护。08C的编译器把寄存器封装在下层,不需要用户管理,如果汇编子程序没有保存这些寄存器,在返回时将造成不可预测的后果。

②嵌入汇编语句

对于嵌入式汇编,可以在C程序中使用一些关键字嵌入一些汇编程序,这种方法主要用于实现数学运算或中断处理,以便生成精练的代码,减少运行时间。当汇编函数不大,且内部没有复杂的跳转时,可以用嵌入式汇编实现。

使用关键字asm可以嵌入一条或多条汇编语句。例如:

asm(“SEI”); //单条指令 asm(“LDA $0000 \\n” //多条指令 “AND #1\\n”

“STA $0000\\n”);

(2) 在汇编中使用C语言

在前面已经讲述了C代码中嵌入汇编程序的方法,实际上汇编中也可以调用C代码中的变量与子程序。

使用C代码中定义的变量:在变量名前加’_’或’%’,例如:

unsigned char PortA;

asm(“LDA %PortA”)或asm(“LDA _PortA”)

调用C代码中定义的函数:在函数名前加 ‘_’,例如:

function1();

asm(“JSR _function1”);

21

第5章 08C语言及第一个08C工程

5.2.6 08C与标准C的其他一些不同之处

08C语言与标准C语言的语法基本相同,但是也存在一些不同之处,使用时应该注意。

(1) 部分数据类型不同

标准C中,double类型长度为8字节;而08C中,double类型长度为4字节。 (2) 地址分配不同

标准C语言适用于Intel CPU,08C适用于Freescale 08CPU,这两种CPU对多字节的数据分配地址时分别采用了“小端”和“大端”方式,在2.5.1节中有详细阐述。所以在标准C语言分配数据存放地址时,高字节的数据存放在高地址处,低字节的数据存放在低地址处,符合“高高低低”的分配原则。08C语言在分配数据存放地址时,高字节的数据存放在低地址处,低字节的数据存放在高地址处,符合“高低低高”的分配原则。

例:有共用体u1如下:

union abc{ char a; int b; long c; }u1;

如果整型变量c的赋值为:

u1.c=0x12345678;

则u1.a和u1.b的值也被修改。在标准C语言中,它们的值为:

u1.a=0x78; u1.b=0x5678;

在08C语言中,它们的值为:

u1.a=0x12; u1.b=0x1234;

5.3 08C工程文件组织

第一个08C工程”PrgFrame.prj”给出了Freescale MCU C编程框架,同时它也是一个很好的编程规范示例。图5-7是PrgFrame.prj的文件组织状态图。该工程的功能是通过普通I/O口控制发光二极管闪烁,它对应于上一章同名汇编工程,请读者参照阅读。

关于总头文件、主程序文件、芯片相关文件、通用文件和面向硬件对象文件这些概念已在相应的汇编工程中介绍过,这里不再详细阐述。下面将结合第一个08C工程实例“PrgFrame.prj”,详细分析08C工程的组成、C程序文件的编写规范、软硬件模块的合理划分等。希望读者通过认真分析与实践第一个实例程序,达到HC08系列C语言编程入门的目的。

22

第5章 08C语言及第一个08C工程

PrgFrame.prj

芯片相关程序文件 GP32映像寄存器名定义头文件(GP32C.h) 开放或禁止MCU模块中断头文件(EnDisInt.h) 芯片初始化文件(MCUinit.c)

中断处理子程序与中断向量表文件(Vectors08.c)

硬件对象控制文件 小灯驱动文件(LED.c)

总头文件(Includes.h) 通用程序文件

小灯驱动头文件(LED.h)

类型别名定义头文件(Type.h) 通用函数头文件(GeneralFun.h)

通用函数文件(GeneralFun.c)

主函数文件(Main.c)

图5-7 PrgFrame.prj工程文件组织图

1.芯片相关的程序文件

PrgFrame.prj工程中芯片相关的程序文件包括:GP32映像寄存器名定义头文件(GP32C.h)、开放或禁止MCU模块中断头文件(EnDisInt.h)、芯片初始化文件(MCUinit.c)和中断处理子程序与中断向量表文件(Vectors08.c)。

(1) GP32C.h

该文件与芯片相关,不要修改。具体内容参见附录C.2。 (2) EnDisInt.h

开放或禁止MCU模块中断往往是设置寄存器的标志位,用宏的方式在本头文件中给出形象地定义,在实际使用时,只需要调用宏,而不需要去设置位,减少出错的可能性。

//[EnDisInt.h]开放或禁止MCU各模块中断头文件----------------------------- //头文件

#include \映像寄存器名定义

//开放或禁止中断宏定义

#define EnableMCUint() asm(\开放总中断 #define DisableMCUint() asm(\禁止总中断

如果要在程序中对SCI模块使用中断设置,可以在这个头文件增加如下语句:

#define EnableSCIReInt() SCC2|= (1<<5) //令SCC2.5=1,开放SCI接收中断 #define DisableSCIReInt() SCC2&=~(1<<5) //令SCC2.5=0,禁止SCI接收中断

(3) MCUinit.c

该文件与具体的芯片型号有关,一般不要改动,并且只包含一个芯片初始化函数,来完成芯片初始化的相关工作。若想由编译器自动调用芯片初始化函数,其函数名必须

23

第5章 08C语言及第一个08C工程

为“_HC08Setup”,否则编译器会自动建立并调用一个空的“__HC08Setup”汇编子程序,而不理会用户创建的芯片初始化函数。为了统一,将该函数起名为“MCUInit”,并在主函数中调用该函数。这部分内容对初学者较难,请直接使用,本文将在第14章详细阐述。

(4) Vectors08.c

该文件与具体的芯片型号有关,可以根据需要向文件中添加中断处理函数和相应的中断向量。第一个C工程为了给出完整的编程框架,包含了“Vectors08.c”文件,但实际上,该工程只用到复位中断,这个中断比较特殊,由编译环境直接设置,所以不包含“Vectors08.c”也是可以的。5.2.3节已经详细讨论过“Vectors08.c”文件,这里不在祥述。

2.硬件对象控制文件

MCU C工程编程是面向硬件对象的。例如,要用MCU控制电机(Motor),在这样一个系统中,“面向硬件对象”概念体现在,工程中会创建“Motor.c”的源程序文件专门用于定义电机控制函数。相应的,也要创建一个同名头文件“Motor.h”,用于控制电机的MCU引脚定义、相关宏定义和电机控制函数声明等。这两个文件都是针对于Motor对象而建立,共同完成了对电机的控制动作。

PrgFrame.prj工程中要控制的硬件对象是小灯,所以,本工程的硬件对象控制文件包含小灯驱动头文件(LED.h)和小灯驱动文件(LED.c)。

(1) LED.h

该文件包含: ①文件名。

②小灯控制所需用到的头文件。 ③小灯控制引脚定义。 ④小灯控制函数声明。

外部函数要用到小灯控制函数时,可包含这个头文件进行函数声明。

//[LED.h]小灯驱动头文件----------------------------------------------------- //小灯控制需要用到的头文件

#include \映像寄存器名定义 #include \类型别名定义

//小灯控制引脚定义

#define Light_P PTA //小灯(Light)接在PTA口 #define Light_D DDRA //相应的方向寄存器

#define Light_Pin 1 //小灯接在相应口的1引脚

//小灯控制相关函数声明

void LEDinit(void); //定义控制小灯的MCU引脚为输出 void LED_L_A(INT8U flag); //驱动小灯\亮\暗\

24

第5章 08C语言及第一个08C工程

(2) LED.c

该文件包含:

①文件描述:文件中的函数的索引、硬件对象及其与MCU的连接描述。 ②头文件

小灯控制所需用到的头文件和小灯控制引脚定义及小灯控制函数中用到的其他常量声明可全部已放在“LED.h”中,“LED.c”只要包含该头文件即可。

③小灯驱动函数定义

每个函数前要有相应的函数描述,包括函数名,函数的功能、参数和返回,必要时还可加上函数说明。

//[LED.c]小灯驱动----------------------------------------------------------* //本文件包含: * // (1)LEDinit:定义控制小灯的MCU的I/O引脚为输出 * // (2)LED_L_A:驱动小灯\亮\暗\//硬件连接: * // (1)本处的小灯是一个发光二极管,由MCU的I/O引脚控制 * // (2)控制引脚=高电平时,小灯\暗\反之,小灯\亮\//-------------------------------------------------------------------------*

#include \该头文件中包含了小灯控制引脚定义和相关函数声明

//LEDinit:定义控制小灯的MCU引脚为输出--------------------------------------* //功能:定义控制小灯的MCU引脚为输出,并使小灯初始为暗 * //参数:无 * //返回:无 * //-------------------------------------------------------------------------* void LEDinit(void) {

Light_D |= 1<

//LED_L_A:驱动小灯\亮\暗\//功能:根据flag的值控制小灯的亮和暗 * //参数:flag(flag = 'A',小灯暗;flag = 'L',小灯亮) * //返回:无 * //-------------------------------------------------------------------------* void LED_L_A(INT8U flag) {

if (flag == 'A') {

Light_P |= 1<

else if (flag == 'L') {

Light_P &= ~(1<

25

第5章 08C语言及第一个08C工程

} 3.通用程序文件

该类文件并不专门针对于某些特定的硬件对象,而是有一定的通用性,可以说它们是为整个工程的和谐运作而建立的。

PrgFrame.prj工程中通用程序文件包括:总头文件(Includes.h)、类型别名定义头文件(Type.h)、通用函数头文件(GeneralFun.h)和通用函数文件(GeneralFun.c)。

(1) Includes.h

总头文件在一个工程中只有一个,它的名称较为固定,一般取为“Includes.h”。为了使主函数文件尽可能不被改动,结构清晰,“Includes.h”是主函数文件中唯一包含的头文件。主函数文件中用到的头文件(通用的和面向硬件对象的头文件)、宏定义、函数声明都放在这个总头文件中。主函数文件的这些部分如要修改,只要在总头文件中修改即可,不用改动主函数文件。注意要避免总头文件中间接的头文件重复包含。

//[Includes.h]总头文件-----------------------------------------------------* //本文件包含: * // 主函数(main)文件中用到的头文件,外部函数声明及有关常量命名 * //-------------------------------------------------------------------------* //1.包含通用头文件

#include \类型别名定义

#include \开放或禁止MCU各模块中断

#include \该头文件中包含了通用函数声明 //2.包含面向硬件对象头文件

#include \该头文件中包含了LED驱动函数声明

//函数声明

void MCUinit(void); //芯片初始化

(2) Type.h

顾名思义,“Type.h”文件就是在给C语言中的类型名起别名,这样使程序中的类型名简短明了,同时,也便于程序移植。用户还可以根据自己的需要,随时在该文件中添加条目。在工程的任一文件中,需要用到这些别名时,都要包含“Type.h”。

在多个程序文件中都有可能用到类型别名定义,为了防止在一个文件中多次包含“Type.h”。因此,在“Type.h”中需要加入条件编译语句。

//[Type.h]类型别名定义-----------------------------------------------------*

//说明:为了方便使用与程序移植定义类型别名,用户可以根据需要自行添加 * //-------------------------------------------------------------------------* #ifndef TYPE_H #define TYPE_H

typedef unsigned char INT8U; //无符号8位数 typedef signed char INT8S; //有符号8位数 typedef unsigned int INT16U; //无符号16位数 typedef signed int INT16S; //有符号16位数

26

第5章 08C语言及第一个08C工程

typedef unsigned long INT32U; //无符号32位数 typedef signed long INT32S; //有符号32位数 typedef float FP32; //单精度浮点数 typedef double FP64; //双精度浮点数 #endif (3) GeneralFun.h

该文件包含文件名、通用函数所需用到的头文件、通用函数用到的宏定义和通用函数声明。

外部函数要用到通用函数时,可包含这个头文件进行函数声明。

//[GeneralFun.h]通用函数头文件---------------------------------------------- #include \类型别名定义 void Delay(INT16U); //延时函数声明 (4) GeneralFun.c

该文件包含:

①文件描述:文件中的函数的索引。 ②头文件

通用函数所需头文件和宏定义可都放在“GeneralFun.h”中,“GeneralFun.c”中只要包含该头文件即可。

③通用函数定义

每个函数前要有相应的函数描述,包括函数名,函数的功能、参数和返回,必要时还可加上函数说明。工程可以根据需要向“GeneralFun.c”文件中添加通用函数,如内存数据移动函数、延时函数这样的通用函数都可以放入其中。

//[GeneralFun.c]通用函数---------------------------------------------------* //本文件包含: * // (1)Delay:延时函数 * //-------------------------------------------------------------------------* #include \该头文件中包含了类型别名定义

//Delay:延时函数-----------------------------------------------------------* //功 能:用程序的方法延时一段时间 * //参 数:k = 延时长度(0-65535) * //返 回:无 * //-------------------------------------------------------------------------* void Delay(INT16U k) {

INT16U u;

for(u=0; u

27

第5章 08C语言及第一个08C工程

4.主函数文件(Main.c)

工程中有且仅有一个主函数文件“Main.c”,它定义了工程的主处理流程。 该文件中包含:

①工程概要:包含工程名、硬件连接索引、工程描述、目的、注意要点和日期等。 ②总头文件 ③主函数

//-------------------------------------------------------------------------* //工 程 名:PrgFrame.prj * //硬件连接: * // (1)MCU的I/O口引脚接小灯(见\文件说明) * //程序描述:用I/O口控制小灯闪烁 * //目 的:第1个freescale HC08系列MCU C语言程序框架 * //说 明:提供Motorola MCU的编程框架,供教学入门使用 * //注 意:如果延时不够长的话,会发觉灯不会闪烁,而是一直亮,这是由于人的 * // 视觉引起的. * //------------------清华2007版《嵌入式技术基础与实践》实例-----------------*

//总头文件

#include \//主函数 void main() {

DisableMCUint(); //禁止总中断 //1. 芯片初始化 MCUinit(); //2. 模块初始化

LEDinit(); //(1) 小灯控制引脚初始化 //总循环 while (1) {

LED_L_A('L'); //小灯亮 Delay(15000); //延时 LED_L_A('A'); //小灯暗 Delay(15000); //延时 } }

上述GP32 第一个C工程实例的源程序请见教学资料“MC08Ex2007\\GP32\\GP32C\\C01_简单IO及程序框架”。

通过上述C工程实例分析,除了要理解08C工程的组成、C程序文件的编写规范和软硬件模块的合理划分等内容外,还要注意文件的命名规则,现简述如下:

①工程中较为固定的程序文件,其命名也较为固定,如“Includes.h”、“Main.c”、“EnDisInt.h”、“MCUinit.c”、“Vectors08.c”等。

②有些文件要同名,如“GeneralFun.c”,它是工程中的通用函数定义文件(像内存数据移动函数和延时函数都属于通用函数),其他文件在用到这些函数之前,必须进行

28

第5章 08C语言及第一个08C工程

函数原型声明,或者这个“.c”文件自身也需要包含一些头文件和宏定义,这些内容都可以放到一个与之同名的“GeneralFun.h”文件中。 这两个文件是相互对应的。同样,面向硬件对象头文件的名称一定要与相应的硬件对象控制文件同名。

③有些文件的命名没有特殊要求,只要表清含义就可以,如:“Type.h”。 总之,给工程中的文件命名时,要尽量做到见名知意,简洁明了。

*5.4 进一步讨论 5.4.1 变量的定义

1.全局变量和局部变量

全局变量为整个程序而定义,在整个程序运行期间,它们占用固定的RAM资源。在C语言中,在所有函数外部声明的变量都认为具有全局作用域,这些声明通常置于源文件的顶部。

“全局”实际上仅仅意味着标识符从声明点到文件末尾的范围内是可访问的,当程序包含多个源文件时,则在一个文件中定义的全局变量在其他文件引用时,需要使用extern关键字声明。在引用文件内部,标识符的作用域是由extern声明的位置确定的。如果该声明是全局的,那么该标识符对于文件是全局的;如果该声明是放在块内的,则它对于那个块就是局部的。

局部变量为某个函数或子程序而定义,只在函数运行时,从堆栈空间中分配存储空间;函数运行结束,所占用堆栈空间释放。

2.变量修饰符

变量定义有三个修饰符值得注意,虽然它们与标准C是相同的,但是在嵌入式C语言中又有不同的含义。

(1) volatile

大多数编译器对源程序编译时做优化操作,其中一种优化方法是基于这种假设:除非明确地把某值写到内存,否则内存中的值不会改变。所以如果源程序中频繁使用某个内存,编译器会把这个内存放到CPU寄存器或高速缓存中,提高代码运行速度。

在嵌入式系统中,这种优化会影响程序的正确执行,典型的情况是: ①硬件外设寄存器的值随时都在变化,并且这种变化是不需要在写寄存器程序来改变。

②内存变量在主程序中没有显示改变,但在中断服务程序被改变,如果编译器在主程序中将内存以寄存器来取代,中断服务程序对变量的改变就不能传递到主程序中。

对于这两种情况做变量声明时,需要加前缀volatile,告诉编译器不要对这些变量做优化操作。

例如:volatile char device_status

29

第5章 08C语言及第一个08C工程

(2) static

在子函数中用static声明的变量是局部变量,局部的范围可能是一个文件、函数、过程中,在局部的范围内,变量可以调用,变量值可以共享。下面给出了一个子函数中使用的局部变量的用法。

void MyFunction (void) {

static char myVar = 0; //用 static声明的局部变量 myVar = myVar + 1; }

void main (void) {

MyFunction(); //调用之前myVar = 0,调用之后myVar = 1 MyFunction(); //调用之前myVar = 1,调用之后myVar = 2 }

(3) const

修饰符const可以用在任何变量之前,用于声明变量值不会被改变,即“只读的”。这提供了一种保护性编程,编译器会将任何想修改这种变量的行为看成是违犯语法的行为。const声明的变量必须包含一个初值,不允许在以后的使用中修改它的值。

宏定义常量和const有一些相似之处,但const还声明了数据类型,编译器对它们的处理也有所不同,如:

#define TYPEA 10 /*字符”TYPEA”在编译时用10来代替*/ const unsigned int typeA=10; /*typeA 是一个无符号整型数值为10*/

当在一个指针声明中使用const关键字时,其意义有所不同,如:

const int *p; /*P是一个可修改的指针,指向一个只读的int值*/ int *const p=&i; /*P是一个只读的指针,指向一个可修改的int值*/ const int * const p=&i; /*P是一个只读的指针,指向一个只读的int值*/

在嵌入式系统编程时,const修饰的变量应该把它看成一种常量,常量值存储在ROM中。

5.4.2 变量存储空间分配

嵌入式内部数据存储器RAM只有几百字节,如果通过扩展外部存储器RAM来提高数据存储量必将会增加了硬件成本,使系统更加的复杂,访问外部存储器比访问内部存储器所需的代码也要长得多。有效地使用片内存储器、提高存储器空间的利用率对开发者来说十分关键。

内部处理器、内部堆栈、压缩栈、所有程序变量和所有包含进来的库函数都将使用数量有限的内部数据存储器RAM。因为C语言采用了存储器的覆盖技术,可以在程序进行连接时,它将那些已经被其它程序段释放了的存储器空间重新定义给另一个程序段的变量使用,当这个程序运行结束时再将这些存储器释放以供其它程序段使用。全局变量的作用范围是整个程序,因此不能被释放;静态变量由于在函数的调用中也不能被释放;只有局部变量中的动态变量可以被释放。

30

第5章 08C语言及第一个08C工程

因此在进行程序设计时应该尽量的使用局部变量,提高内部数据存储器的使用率。在C语言中程序中间结果及参数传递是通过内部的寄存器来完成的,要是内部的存储器不够,将会给你的程序带来许多莫名其妙的错误。例如在进行程序设计时语句不应太长,一条长语句可以分成多条语句,这样可以减少中间变量。若语句太长可能造成临时寄存器不够用,导致计算出错。

下面的示例给出了08C中对于变量的一些详细的使用信息。

例:08C中对于变量的存储空间的使用,在MT_IDE For Freescale HC08环境中输入如下程序:

unsigned char pubVar0; //全局变量 /*[主函数]*/ void main() {

unsigned int tmpVar1; //局部变量 static unsigned int staticVar2; //静态变量 const static unsigned int constVar3=0x11; //静态常量 tmpVar1=0x22; staticVar2=0x33; pubVar0=0x44; }

编译后的列表文件:

_main: tmpVar1 --> X+0 8037 A7 FE aiS #-2 8039 95 tSX FILE: main.c

(0001) unsigned char pubVar0; //全局变量声明 (0002) /*[主函数]*/ (0003) void main() (0004) {

803A 4F clrA 803B F7 stA 803C A6 22 ldA 803E E7 01 stA 0,X #34 1,X (1) (0005) unsigned int tmpVar1; (0006) static unsigned int staticVar2; (0008) tmpVar1=0x22; (0009) staticVar2=0x33; 8040 4F clrA 8041 C7 0041 stA 8044 A6 33 ldA 8046 C7 0042 stA __r0+1 #51 __r0+2

31

(3) (2) (0007) const static unsigned int constVar3=0x11; 第5章 08C语言及第一个08C工程

(0010) pubVar0=0x44; FILE: 8049 A6 44 ldA 804B C7 0043 stA 804E A7 02 aiS 8050 81 rts

#68 _pubVar0 #2

(4) 说明:

①tmpVar1是一个无符号整型的局部变量,编译后的列表文件中,在main函数的开始处,将堆栈指针减2,预留了2个字节的空间用于存放tmpVar1的值,如果main函数有更多的局部变量,会开辟更多的堆栈空间。执行语句“tmpVar1=0x22”时,将十六进数22(十进制数34)放到堆栈空间,高字节0存放在堆栈指针处,低字节34存放在堆栈指针加1处。

②语句“const static unsigned int constVar3=0x11;”在执行时并不生成具体的执行代码,只是将常量0x11放到S19代码中,并且通常放在S19代码的最前面。本程序生成的S19代码的第一行是:

S1238000001145024094CD805145004165004427066F00AF0120F54500429445800265803B

其中加阴影的“0011”就是上述语句所产生的。

③staticVar2是一个静态局部变量,虽然是局部变量,但子程序结束后,其值仍然保留。所以也需要分配固定的存储空间。在08C中,这些变量存放在“Vreg”段变量空间的后面,所以语句“staticVar2=0x33”在执行时,高字节放在_r0+1的位置(r0位于Vreg段),低字节放在_r0+2处。

④staticVar2是一个字节型的局部变量,编译后分配了固定的存储空间。

对于这些加了修饰符的变量的使用方法在不同编译器中存储空间的分配有所差别,使用者只需要像上面一样编写小的程序段进行编译,然后查看列表文件(.lst)、映象文件(.mp)及代码文件(.s19)等,就可以弄清楚使用方法,理解编译器这些问题对于软硬件编程是一件很重要的事情。

基于嵌入式的08C语言和标准C语言虽然在语法上区别不大,但要结合嵌入式的系统资源,用C语言开发符合实际工程需要的嵌入式软件系统,对编程者来说是一件很难的事情。本节根据我们的开发经验讲述一些08C的编程技巧。

5.4.3 数据类型的选用

嵌入式C语言编程不同于一般C语言编程的一个显著特点,就是要和程序存储器资源结合起来,虽然其提供的数据类型十分丰富,但是只有bit和char等数据类型是机器语言直接支持的数据类型,用此类数据类型的语句所生成的代码较短;而其它的数据类型如整型、浮点型等数据要有一定的内部程序或内部函数的支持,相对来说用该类数据类型的语句生成的代码要长。有些C语言程序表面上看起来十分的简单,但在实际编译时,生成的代码却相当长。因此我们要按照实际需要,尽量选用占用存储空间少的

32

第5章 08C语言及第一个08C工程

数据类型,可以大大的减少所生成的代码长度。例如在08C中用不同的数据类型定义i时,语句:for(i=0;i<10;i++);

经编译后生成的代码长度如表5-4所示。

表5-4 不同数据类型占用存储器字节数和代码长度对比

数据类型 字节数 代码长度 unsigned char 1 12 char 1 19 unsinged int 2 43 int 2 43 unsigned long 4 134 long 4 134 float 4 508 通过表5-3我们知道,不同的数据类型所生成的机器代码长度相差很大,相同类型的数据类型有无符号对机器代码长度也有影响。在程序编译时生成机器代码长的数据类型的优先级越高,不同的数据类型在进行程序运算时要转化为高优先级的数据类型,相应的代码长度也会增长。因此我们应尽可能地使用bit, char等机器语言直接支持的数据类型,无符号数的变量应声明为无符号数,尽可能地减少程序中使用的数据类型的种类。

5.4.4 位操作的其他实现方法

1.用共用体和位域实现位操作

综合共用体和位域等多种数据类型,可以实现很直观的位操作方法。下面以A口的数据寄存器位操作方法来说明。

(1) 定义

//A口数据寄存器及位定义 typedef union {

unsigned char Byte; struct {

int PTA_0 :1; /* Port A数据Bit 0 */ int PTA_1 :1; /* Port A数据Bit 1 */ int PTA_2 :1; /* Port A数据Bit 2 */ int PTA_3 :1; /* Port A数据Bit 3 */ int PTA_4 :1; /* Port A数据Bit 4 */ int PTA_5 :1; /* Port A数据Bit 5 */ int PTA_6 :1; /* Port A数据Bit 6 */ int PTA_7 :1; /* Port A数据Bit 7 */ } Bits; }PTASTR;

#define _PTA (*(volatile PTASTR *)0x00) #define PTA _PTA.Byte

#define PTA0 _PTA.Bits.PTA_0 #define PTA1 _PTA.Bits.PTA_1 #define PTA2 _PTA.Bits.PTA_2 #define PTA3 _PTA.Bits.PTA_3 #define PTA4 _PTA.Bits.PTA_4 #define PTA5 _PTA.Bits.PTA_5 #define PTA6 _PTA.Bits.PTA_6

33

第5章 08C语言及第一个08C工程

#define PTA7 _PTA.Bits.PTA_7

(2) 使用

对A口整个口的使用,则直接对PTA赋值。如:

PTA = 0xFF;

对A口的某一位的使用,则直接对PTAx赋值。如:

PTA4 = 1;

很显然这样定义了以后,操作很方便,程序的可读性也很好。但这种方法08C编译器编译后的效率比第1种方法要差。

如上面用到的PTA4 = 1编译后的代码为:

8089 B6 00 ldA 0x00 808B AA 10 orA #16 808D B7 00 stA 0x00

本来用1条位操作指令来实现的操作,现在用了3条指令,在执行效率和存储空间上都比较差,对于接口编程时对时序要求很高的时候要特别注意这个问题。

2.用宏定义+“按位与、按位或运算”的方法实现位操作

在5.2.2节所讲述的位操作方法效率高,但程序的易读性不强。在有些情况下,将5.2.2节的位操作语句再来一次宏定义,就能兼顾程序的效率和可读性。

例如:串行模块(SCI)的控制寄存器2(SCC2)的第5位控制接收中断的开放(=1时)和禁止(=0时),此时可以定义两个宏:

#define EnableSCIReInt() SCC2|= (1<<5) //令SCC2.5=1,开放SCI接收中断 #define DisableSCIReInt() SCC2&=~(1<<5) //令SCC2.5=0,禁止SCI接收中断

这样在程序中开放串行接收中断就用如下语句:

EnableSCIReInt();

5.4.5 算法设计问题

嵌入式C语言和标准C语言存在着很大差别,在计算机上进行C语言程序设计时可以不必考虑程序代码的长短,只需考虑程序功能实现,但是在嵌入式上进行C语言程序设计就必须考虑系统的硬件资源。有时并不是程序的算法越简单、长度越短越好,因为有一些算法要调用一些内部的子程序和函数,生成的机器代码可能非常长。不同的算法对程序代码长度影响十分大,因此在进行程序设计时,就尽量采用程序生成代码短的算法,在不影响程序功能实现的情况下可以采用一些优化算法。

在嵌入式C语言编译成机器代码时,不同的运算生成的机器代码的长度相差很大,尽可能地减少程序中对某种数据类型的运算种类,越复杂的数据类型效果越明显。在进行数据计算时,在一定的精度范围内,可以用一些近似的计算来完成一些运算,既不损失精度又能减少大量的代码。比如:用逻辑AND/&取模比MOD/%操作更有效。

在用热敏电阻测量温度时,可根据热敏电阻—温度特性公式来求值。数学表达式表示为:

RT=RT0e(B(1/T-1/T0))

34

第5章 08C语言及第一个08C工程

其中:

RT----T℃时的阻值

RT0----T0℃(基准温度)时的阻值 B----热敏电阻特性参数

如果直接按照公式计算RT时程序结构简单,算法复杂度不高,但是程序将调用文件中的对数函数,在编译成机器码时函数有1K多字节,对于一般只有几K字节的嵌入式系统来说,这是十分不合适的。考虑到系统资源问题可以用一种替代方法—查表法来实现算法。只要给出一定温度范围内不同温度值对应热敏电阻的电阻值,然后建立表格,只要按照系统求出的阻值,进行查表、插值,就可以求出相应的温度值。这种算法相比前面的公式法的算法复杂,C语言程序代码也长,但在编译成机器码时,代码长度却很短,只有一、二百字节。

练习题

【基础题】

1.C的哪些特征使得它能够成为嵌入式系统中使用率最高的高级语言? 2.为什么要在嵌入式系统中使用无限循环? 3.给出下列各个表达式的整型值: (1) 6 && -2 (2) 6 & -2 (3) 3 || 6 (4) 3 | 6 (5) !(-5) (6) ~(-5) 4.用C语言编写一个表达式,该表达式当且仅当整型变量i的第5位为1时为真。 5.假定n和x都声明为整型。编写一行C代码(不考虑x的当前值,并且不修改x的其他任何位)用于执行以下操作:

(1) 设置x的第n位为1。 (2) 将x的第n位清0。 (3) 反转x的第n位。 6.在执行指示的代码行后,给出保存在unsigned char 类型的x中的8位二进制值: x的初值 代码行 (1) 11100101 x|=(1<<4) (2) 11011001 x &=~(1<<6) (3) 01111010 x^=(1<<5) (4) 10101010 x=(x>>3) & 0x0f (5) 00001111 x=~x (6) 00001111 x = ! x (7) 00000000 x || =0x20 (8) 11111111 x && =0xF0 【综合题】

35

第5章 08C语言及第一个08C工程

7.给定一个定义如下的宏,说明下列各种用法如何展开: #define REM(a,b) a%b REM(5,2) REM(5+2,X) REM(5,X-2)

8.定义一个名为BIT(x,n)的宏,将其展开成一个表达式,该表达式的值对应于x的第n位的值,严格等于0或1。

9.用查表的方法实现数学函数y=sin(x)。

36

第5章 08C语言及第一个08C工程

7.给定一个定义如下的宏,说明下列各种用法如何展开: #define REM(a,b) a%b REM(5,2) REM(5+2,X) REM(5,X-2)

8.定义一个名为BIT(x,n)的宏,将其展开成一个表达式,该表达式的值对应于x的第n位的值,严格等于0或1。

9.用查表的方法实现数学函数y=sin(x)。

36

本文来源:https://www.bwwdw.com/article/fq7r.html

Top