第6章 C语言和Keil开发环境

通过上一章中的汇编例子可以看到:使用汇编语言编写程序需要对计算机硬件非常熟悉,并且一种计算机汇编语言的程序很难在另一种计算机中运行,再有汇编语言程序结构不是特别清晰,可阅读性比较差,因此人类又开发了高级语言与计算机打交道,比如C语言、BASIC语言等。

高级语言程序通常具有很好的可阅读性和可移植性,通过编译器把高级语言翻译成某一种汇编语言程序,汇编语言程序再由汇编器翻译成机器语言程序,计算机就可以执行这些机器语言程序了。

6.1 8051单片机仿真原理图

在讲解C语言之前,我们先用Proteus仿真一台8051计算机作为程序的运行平台,后面的C语言例子经过Keil开发环境生成可以在这台计算机上执行的程序,程序运行后可以在终端上直观的看到结果,仿真硬件如图6-1所示。

 

6-1 单片机仿真原理图

6.2 Keil开发环境设置

Keil集成开发环境可以实现C源代码、汇编源代码的编写,以及源代码的编译和汇编,最终生成8051的机器语言程序。

下面以在终端上输出“hello world”为例进行说明。

打开Keil软件,并新建工程,命名为test.Uv2,如图6-2所示。

 

 

6-2 新建Keil工程

选择目标单片机,如图6-3所示,这里选择常用的Ateml 公司的AT89C51,兼容8051指令集。

 

6-3 选择单片机型号

file->New创建源代码文件,并输入:

/* 这是第一个例子程序 */

#include <reg51.h>

#include <stdio.h>

void main(void)

{

SCON = 0x50;

TMOD = 0x20;

PCON = 0x00;

TH1 = TL1 = 0xFD;//波特率设置为9600

TI =  1;  //printf函数检测到TI=1才会输出

TR1 = 1; //启动定时器

 

printf ("Hello World!\r\n"); //显示Hello World

while(1);//死循环

}

保存文件到test工程目录,文件名可以用main.c,添加此文件到工程中,如图6-4所示。

 

6-4 添加源文件

设置输出可执行程序的格式为Hex文件格式,如图6-5所示。

 

6-5 设置输出HEX格式程序

编译程序,生成test.hex,如图6-6所示。

 

6-6 编译生成可执行程序

双击Proteus仿真中的8051单片机,设置8051的可执行程序为test.hex,如图6-7所示。

 

6-7 设置8051单片机程序路径

仿真执行,结果输出,如图6-8所示。

 

6-8 程序执行结果

C程序要有并且仅有一个main函数,main函数中的C代码用大括号括起来。

上面例子程序中main函数可以分成三部分。第一部分完成串口初始化,这一部分与汇编程序里实现的串口初始化方法相似,等号相当于MOV指令的作用;第二部分调用printf库函数向终端输出“hello world”,终端是通过串口连接单片机的,printf函数在Keil开发环境提供的库函数里实现好了,这里只管调用就可以了,编译器会自动把printf的具体代码加进来;最后一个while死循环,为了不让程序马上退出,因为没有死循环,程序输出完“hello world”就立即退出了,我们就看不到结果了。

#include里的内容也是事先写好的,reg51.h主要是做了一些8051寄存器的定义,把特殊功能寄存器的名字和实际地址对应起来,stdio.h里有printf等函数的声明,C语言要求别的C文件里实现的函数在本文件声明一下才能使用。

程序注释说明可以使用“//”和“ /* */”,C语言使用分号作为一句话的结束标志。

6.3 基本数据类型

程序对数据进行加工处理,数据通常存储在内存单元中,但内存地址不方便记忆,所以C语言中就给内存地址起个名字,这个名字就是变量名。特殊功能寄存器也是个地址编号,所以在C语言里也是以变量的方式存在的。C语言变量支持的基本数据类型如表6-1所示。

6-1 8051 C语言基本数据类型

数据类型

长度

范围

unsigned char

8位

0~255

char

8位

-128~127

unsigned int

16位

0~65535

int

16位

-32768~32767

unsigned long

32位

0~4294967295

long

32位

-2147483648~2147483647

float

32位

±1.175494E-38~±3.402823E+38

sfr

8位

0~255

sbit

1位

0或1

变量要先定义后使用,变量名可以用字母、数字和下划线排列组合,但第一个字符不能是数字,而且要区分大小写字母。变量的值在任何时候都不能超出变量类型表示的范围,否则可能会出现溢出错误,如果变量带有小数可以用float型。

例子1 变量的使用

#include <reg51.h>

#include <stdio.h>

 

void COM_INIT()

{

SCON = 0x50;

TMOD = 0x20;

PCON = 0x00;

TH1 = TL1 = 0xFD;

TI =  1;  

TR1 = 1;

}

void main(void)

{

  char var1, VAR1, sum1;

  int var2, VAR2, sum2;

  float var_3, VAR_3, sum_3;

 

  COM_INIT();

  var1 = 60;

  VAR1 = 37;

  var2 = 1000;

  VAR2 = 10000;

  var_3 = 3.14159;

  VAR_3 = 1.414;

  sum1 = var1 + VAR1;

  sum2 = var2 + VAR2;

  sum_3 = var_3 + VAR_3;

  printf ("%c %d %f\r\n", sum1, sum2 ,sum_3);

  while(1);

 

}

输出结果是:

a  11000  4.555590

例子1首先定义了6个变量,然后调用串口初始化代码,紧接着给变量赋值,再求和,最后通过printf函数打印输出sum1、sum2、sum_3,“%c”表示按字符类型输出sum1,“%d”表示按照int型输出sum2,“%f”表示按照float类型输出sum_3,输出类型和变量类型要保持一致,否则可能出错。

为什么sum1会输出字符a呢?其实sum1的值是97,内存里存储的也是97的二进制数,但printf里要求按照字符输出sum1,那么就是输出97在ASCII码表里对应的字符,也就是字符a,需要注意ASCII码表里的有些字符是不可见的,还要注意0字符和0不是一回事。计算机里存储的只是数据,至于这些数据代表什么意思,全在于观察者,比如float型的sum_3是一个32位的数,你可以认为它代表4个字符,也可以认为它代表2个int型的数,你还可以认为它代表2个汉字,你甚至还可以认为它代表4个灰度像素,当然这里只有代表float型的数才能得到想要的结果。

reg51.h文件里,已经把8051单片机所有的特殊功能寄存器定义为变量了,所以在C语言程序中可以像使用普通变量一样使用特殊功能寄存器变量,想要P1口输出高电平,就可以用“P1=0xff”,等同于“MOV P1 #0FFH”汇编指令。C语言能读写特殊功能寄存器,特殊功能寄存器连接着计算机的各种硬件,因此C语言能控制计算机的硬件。

以后的例子,串口初始化直接用COM_INIT()代替,调试程序时请读者自行添加COM_INIT()的实现代码。

6.4 运算符

C语言支持主要运算符:

算数运算符:+、-、*、/、%,分别代表加、减、乘、除、求余;

位运算符:<<、>>、~、|、^、&,分别代表左移、右移、按位取反、按位或、按位异或、按位与。

“%”是求余数运算,比如5%3结果是2。注意区分逻辑运算的“与或非”和位运算的“与或非”。按位运算是两个数对应的位依次运算,比如(1010B)&(0101B),结果是(0000B),也就是结果是0。

例子2 已知三角形边长是a、b、c,那么三角形面积S就是

,其中

#include <reg51.h>

#include <stdio.h>

#include <math.h>

void main(void)

{

  float a,b,c,s,S;

  COM_INIT();

  a = 3.5;

  b = 2.3;

  c = 4.1;//边长

  s = (a + b + c)/2;

  S = sqrt(s * (s - a) * (s - b) * (s - c));//面积

  printf("%f\r\n", S);

  while(1);

}

程序输出结果:

4.020862

代码中的sqrt()函数实现了求平方根的功能,是Keil C实现好的数学库函数中的一个,printf()函数需要stdio.h头文件,sqrt()函数需要math.h头文件,数学库函数中还包括三角函数、对数函数、指数函数等。

例子3 按键控制对应的LED灯亮灭

通过硬件图可知P1口的低4位连接4个LED灯,高4位连接4个按键,我们只需要读取P1口高4位并按位取反,把取反后的4位数写入P1口的低4位就行了。

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  char a;

  P1 = 0xf0;//高4位置1,低4位置0,LED灯灭

  while(1)//反复执行大括号内的代码

  {

    a = P1 & 0xf0;//读取P1口信号并保留高4位

a = ~a;//按键按下时,输入的是0,反之,输入的是1,所以按位取反

    a = a >> 4;//高4位移位到低4位   

    P1 = 0xf0 | (a & 0x0f);// a的低4位输出到P1口的低4位,P1口高4位置1

  }     

}

运行结果,按下按键时,对应的LED灯亮起,抬起按键时,对应的LED灯灭。需要注意,8051的P1口需要先写入高电平,然后才能读出有效的输入信号。

6.5 if条件语句

C语言支持的条件运算符:

关系运算符:<、<=、>、>=、==、!=,分别代表小于、小于等于、大于、大于等于、等于、不等于;

逻辑运算符:!、&&、||,分别代表逻辑与、逻辑或、逻辑非。

“==”用来判断两个值是否相等,相等结果就是1,不相等结果就是0,注意和赋值的“=”区分开来。比如“a=3”是把3写入变量a所在的存储单元;而“a==3”是判断变量a是否等于3,如果等于3,则表达式结果是1,否则结果是0。

逻辑运算的结果只有0和1,参与运算的数非0即认为是1,比如(1010B)&&(0101B),相当于1&&1,结果是1,注意和按位运算的“&”区分开来。

if语句的语法:

if(条件1) 语句1

else if(条件2 ) 语句2

else if (条件3)  语句3

… …

else 语句n

else if的意思是进一步判断,else是所有其它情况,if语句可以没有else if和else,需要进一步判断时才需要。多个条件可以使用逻辑运算符连接。

例子4 把成绩转换成ABCD等级

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int a=76; //成绩

  COM_INIT();

  if(a >= 90) printf("A\r\n");

  else if(a >= 75) printf("B\r\n");

  else if(a >= 60) printf("C\r\n");

  else printf("D\r\n");

  while(1);      

}

运行结果:输出B,可以改变变量a的值,看输出的变化。

例子5  K1、K2按键控制D1、D2、D3、D4 LED灯。

#include <reg51.h>

#include <stdio.h>

 

sbit D1=P1^0;//一位变量D1代表P1口的0端口,硬件连接的是第一个LED灯

sbit D2=P1^1;

sbit D3=P1^2;

sbit D4=P1^3;

sbit K1=P1^4;// 一位变量K1代表P1口的4端口,硬件连接的是第一个按键

sbit K2=P1^5;

  

void main(void)

{

  P1 = 0xf0;//高4位置1,低4位置0,LED灯灭

  while(1)//反复执行while大括号内的代码

  {

    //如果K1、K2按键都抬起,那么4个灯全灭

    if((K1 == 1) && (K2 == 1))

{

  D1 = 0; D2 = 0; D3 = 0; D4 = 0;

}

    //如果K1按下,K2抬起,D1、D2亮,D3、D4灭

if((K1 == 0) && (K2 == 1))

{

  D1 = 1; D2 = 1; D3 = 0; D4 = 0;

}

    //如果K2按下,K1抬起,D1、D2灭,D3、D4亮

if((K1 == 1) && (K2 == 0))

{

  D1 = 0; D2 = 0; D3 = 1; D4 = 1;

}

    //如果K1、K2按键都按下,那么4个灯全亮

if(K1 == 0)

{

  if(K2 == 0)

  {

    D1 = 1; D2 = 1; D3 = 1; D4 = 1;

  }

}  

  }     

}

sbit可以用来定义一位变量,这个一位变量就代表8051某个可位寻址的地址,对变量的读写等同于对位寻址地址的读写。if语句的判断条件可以是多个关系表达式,表达式之间通过逻辑运算符连接。条件满足情况下,可以包含多条执行语句,多条语句用大括号括起来。执行语句还可以是另外的if语句,也就是说if语句可以多层嵌套,多层嵌套时,注意大括号的配对。

6.6 switch分支语句

switch分支语句语法:

switch(变量A)

{

case 1:语句1;break;

case 2:语句2;break;

… …

case n:语句n;break;

default:语句m;

}

swtich语句判断“变量A”等于哪一个“值”,就跳转到“值”对应的分支语句执行,执行完break则跳出swtich语句。如果没有相等的“值”,则执行default对应的语句。

例子6 通过终端输入命令控制LED灯亮灭,输入“1 1”代表打开D1,输入“2 0”代表关闭D2。

#include <reg51.h>

#include <stdio.h>

 

sbit D1=P1^0;

sbit D2=P1^1;

sbit D3=P1^2;

sbit D4=P1^3;

void main(void)

{

  int num, value;

  bit state;

 

  COM_INIT();

   while(1)//反复执行while大括号内的代码

  {

    printf("please input cmd:");

scanf("%d %d", &num, &value); //通过终端输入2个数给变量num和value

if(value == 0) state = 0;

else state = 1;

switch(num)

{

  case 1: D1 = state; break;

  case 2: D2 = state; break;

  case 3: D3 = state; break;

  case 4: D4 = state; break;

  default: printf("LED num error\r\n");

}

  }     

}

scanf函数用于通过终端接收用户的输入,与printf正好相反,只是注意传入的参数是变量地址,“&num”代表变量num的地址。实际输入数据时,两个数据中间有空格,通过回车完成输入,如果没有回车,计算机会一直等待。

6.7 循环语句

1. while循环

while(条件)

{

  语句1;

语句2;

}

只要条件满足,while循环会一直执行大括号内的语句。

例子7 计算1到100的和

#include <reg51.h>

#include <stdio.h>  

void main(void)

{

  int i, sum;

  COM_INIT();

  sum = 0;  i = 1;

  while(i <= 100)

  {  

    sum = sum + i;

i = i + 1;

  }

  printf("sum: %d", sum);  

  while(1);

}

执行结果:

sum: 5050

2. for循环

for(变量初值;变量结束值;变量改变)

{

  语句1;

  语句2;

}

for循环先给变量赋初始值,下一步执行循环中的语句,下一步改变变量的值,下一步判断变量的值是否达到变量结束值,如果达到就跳出循环,如果没达到就继续下一次循环。

例子8 计算1到100的和

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int i, sum;

  COM_INIT();

  sum = 0;

  for(i=1; i<=100; i++)

    sum = sum + i;

  printf("sum: %d", sum);  

  while(1);

}

执行结果:

sum: 5050

例子9  LED灯闪烁

#include <reg51.h>

#include <stdio.h>  

void main(void)

{

  long i;  

  while(1)

  {

    for(i=0; i<10000; i++);   

P1 = ~P1;

  }     

}

执行程序可以看到4个LED灯闪烁。

3. 循环嵌套

例子10 鸡兔同笼,上数共有35个头,下数共有94只脚,问鸡、兔各有几只?

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int j, t;

  COM_INIT();

  for(j=0; j<100; j++)

    for(t=0; t<100; t++)     

     if(((j + t) == 35) && ((2 * j + 4 * t) == 94))

    printf("ji:%d tu:%d", j, t);  

  while(1);

}

执行结果:

ji:23 tu:12

4. break语句

例子11一筐鸡蛋,1个1个拿能拿完,2个2个拿剩1个,3个3个拿剩1个,4个4个拿剩1个,5个5个拿剩1个,6个6个拿剩1个,7个7个拿能拿完,这筐鸡蛋最少有多少个?

#include <reg51.h>

#include <stdio.h>  

void main(void)

{

  int i;

  COM_INIT();  

  for(i=1; i<=10000; i++)

  {

    if((i%2==1)&&(i%3==1)&&(i%4==1)&&(i%5==1)&&(i%6)==1&&(i%7==0))

  break;

  }

  printf("num: %d", i);  

  while(1);

}

结果输出:

num: 301

遇到break就会跳出循环,执行循环后面的语句。

从前面两个例子可以看出,计算机其实并不聪明,它只是机械的一个数一个数的比对,看哪一个数符合条件,但它却比最聪明的人算的还要快,原因就在于计算机的计算速度快,这也可以理解为“勤能补拙”吧。

5. continue语句

例子12 输出10以内的奇数

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int i;

  COM_INIT();  

  for(i=1; i<=10; i++)

  {

    if(i%2 == 0)

  continue;//如果是偶数,则进行下一次循环,忽略后面的printf

printf("%d ", i);

  }    

  while(1);

}

运行结果:

1 3 5 7 9

continue语句会结束本次循环,忽略continue后面的语句,直接进行下一次循环;而break语句则是直接退出循环。

6.8 数组

1. 一维数组

变量通常只是一个数,如果要存储一组数,可以使用数组。

一维数组的定义:类型 数组名[个数];

例如:int a[10];代表一组数,或一行数,共有10个元素,这10个元素分别是a[0]、a[1]... …a[9],每个元素的类型都是int型,数组的名字是a,a也代表数组中第一个元素的地址。

例子13 打印输出5个数中的最大值和最小值

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int i, max, min;

  int a[5] = {12, 30, 69, 199, 488};//数组赋初值

  COM_INIT();  

  max = a[0];

  min = a[0];

  for(i=0; i<5; i++)

  {

    if(a[i] > max)//只要a[i]大于max,那么a[i]就是当前最大

  max = a[i];

if(a[i] < min)

  min = a[i];

  }

  printf("Max value:%d\r\nMin value:%d", max, min);         

  while(1);

}

程序运行结果:

Max value:488

Min value:12

数组可以在定义时赋初始值,也可以不赋初始值,不赋初始值时,数组每个元素的初始值就是不确定的。另外,注意数组的最后一个元素,a[10]的最后一个元素是a[9]。

2. 二维数组

如果说一维数组是一行数,那么二维数组就是有行也有列,就像EXCEL表格里的数据一样。

二维数组的定义:类型 数组名[行数][列数];

例如:int a[3][4];代表有3行4列数,每个元素的类型都是int型,数组的名字是a,每个元素都通过行号和列号引用,如表6-2所示。

6-2 二维数组元素存储示意

a[0][0]

a[0][1]

a[0][2]

a[0][3]

a[1][0]

a[1][1]

a[1][2]

a[1][3]

a[2][0]

a[2][1]

a[2][2]

a[2][3]

例子14  LED流水灯显示

#include <reg51.h>

#include <stdio.h>  

sbit D1=P1^0;

sbit D2=P1^1;

sbit D3=P1^2;

sbit D4=P1^3;  

void main(void)

{

  int i;

  long j;

  char a[4][4] = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}};    

  while(1)

  {

   for(i=0; i<4; i++)

{

  D1 = a[i][0]; D2 = a[i][1]; D3 = a[i][2]; D4 = a[i][3];

  for(j=0; j<10000; j++); //延时

}      

  }

}

运行结果:D1、D2、D3、D4轮流点亮

3. 字符串

字符串就是一串字符,结尾为0,比如“hello”字符串的存储方式如图6-9所示。

h

e

l

l

o

\0

6-9 字符串存储示意图

“hello”字符串占用6个字节的存储单元,分别存储每个字符对应的ASCII码值和结尾0(不是字符0)。由于内存地址不好记忆和使用,所以字符串可以使用字符类型的数组存放和引用。

例子15 打印输出字符串

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  char a[6] = "hello";//字符串是双引号

  char b[6] = {'h','e','l','l','o', 0};//字符是单引号

  char c[] = "hello";

  COM_INIT();

  printf("a: %s\nb: %s\nc: %s", a, b, c);//数组名代表第一个元素的存储地址

  while(1);  

}

运行结果:

a: hello

b: hello

c: hello

    字符串赋值给字符数组有多种方式,程序中的第三种方式比较常用。printf函数中“%s”是指按照字符串方式输出a、b、c,printf会从给的地址(数组名代表首字符地址)开始一个一个的输出字符,直到遇到0结束。

    注意区分字符串“A”和字符‘A’。“A”代表A字符加上0结尾,占用2个字节;‘A’仅代表字符A,占用一个字节。

6.9 指针

前面讲8051汇编语言时讲过“间接寻址”,比如“MOV A, @R1”,假设R1寄存器里的数是50H,那么这条汇编指令就是把50H存储单元里存储的数赋值给累加器A,我们可以认为R1就是一个指向50H存储单元的指针,在C语言里可以*R1表示间接寻址,作用类似于@R1。

一个指针就是一个地址编号,它可以指向计算机的任何地址,当然也就可以间接访问任何地址里的任何类型的数据。如果让指针指向一个变量的地址,那么就可以通过指针访问这个变量;如果让指针指向一个数组的首地址,那么就可以通过指针访问这个数组的任一个元素;如果让指针指向一个字符串的首地址,那么就可以通过指针访问这个字符串。

例子16 交换2个变量里的值

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int a, b, temp;

  int *p;

  COM_INIT();

  a = 3; b = 4;

  temp = a;

  //指针p指向变量a的地址,把b的值赋值给p指向的地址

  p = &a; *p = b;

  //指针p指向变量b的地址,把a的原值赋值给p指向的地址

  p = &b; *p = temp;  

  printf("a:%d b:%d", a, b);

  while(1);  

}

例子17 打印输出数组里数

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  int a[5] = {1, 2, 3, 4, 5};

  int *p, i;

  COM_INIT();

  p = a;

  for(i=0; i<5; i++)   

    printf("%d ", *(p + i));  

  while(1);  

}

例子18 打印输出字符串

#include <reg51.h>

#include <stdio.h>

void main(void)

{

  char *p = "hello world";

  COM_INIT();    

  printf("%s\n", p);  

  while(*p != 0)

  {

    printf("%c", *p);

p++;

  }  

  while(1);  

}

从前面的例子可以看出,指针的使用是很灵活的,一定要抓住“地址”这一条线去认识指针,不管指针指向了什么,它终究就是个地址。

指针数组就是这个数组里有多个指针,比如int *p[3]表示有3个指针,分别是p[0]、p[1]和p[2]。

6.10 结构体

前面介绍的数据类型都是简单的基本数据类型,现在介绍一种复合的数据类型,这种数据类型可以包含多个基本数据类型的成员,用来存储复杂的数据,比如存储一个学生的信息,包括姓名、年龄、性别、学号等数据。

struct student

{

  char name[20];

int age;

char sex;

int num;

}

上面的struct student就是一种新的数据类型,这种数据类型就像基本的数据类型一样可以用来定义变量、数组、指针等,这种数据类型我们称之为结构体类型,结构体类型中包含若干成员。结构体类型用来描述复杂的数据,用户可以自己设计结构体包含哪些、哪种类型的成员。

例子19 打印输出学生的信息

#include <reg51.h>

#include <stdio.h>

void main(void)

{

struct student

{

char name[20];

int age;

char sex;

int num;  

}; //声明struct student结构体类型

struct student lihu = {"lihu",12,'M',15};//定义结构体变量

struct student *p; //定义结构体指针,此时指针没有指向

p = &lihu;//指针指向结构体变量lihu

COM_INIT();

printf ("Name:%s Age:%d Sex:%c Num:%d\r\n", lihu.name, lihu.age, lihu.sex, lihu.num);

printf ("Name:%s Age:%d Sex:%c Num:%d\r\n", p->name, p->age, p->sex, p->num);

while(1);

}

运行结果:

Name:lihu  Age:12  Sex:M  Num:15

Name:lihu  Age:12  Sex:M  Num:15

先声明一种结构体类型,然后这种类型就可以用来定义变量、指针。结构体变量成员的引用方式是“.”,比如“lihu.name”;结构体指针成员的引用方式可以用“->”,比如“p->name”,也可以用(*p).name。结构体指针需要设置具体的指向,否则是不能使用的。

结构体类型也可以用来定义数组,比如struct student stu[10]代表有10个struct student类型的元素,每一个元素就是一个结构体变量,引用成员方式是stu[0].name。

6.11 函数

把复杂问题分解成若干个容易的小问题是解决问题的有效途径,同样的道理,把大程序分解成若干个小程序,每个小程序实现起来比较容易,那么整个程序也就容易实现了,大程序可以用主函数来表示,小程序就可以用子函数来表示。

另外一方面,有些代码被重复使用,可以把重复使用的代码编写成一个一个的函数,使用的时候直接调用就可以了,比如printf等库函数。

1. 无参数函数

最简单的无参数函数可以使用如下形式:

void 函数名()

{  

变量定义;

  程序语句;

}

前面我们多次用过的COM_INIT()函数就是这样一个例子,函数只需要实现一次,就可以多次调用。

2. 有参数函数

有参数函数调用时,可以给子函数传入信息,子函数也可以传出信息,函数形式如下:

void 函数名(参数1,参数2,… …,参数n)

{

变量定义;

  程序语句;

}

例子20  D1灯1秒钟闪烁

#include <reg51.h>

#include <stdio.h>  

void delay_ms(int x)

{

  unsigned char t;

  while(x--)

  for(t = 0; t<120; t++);

}

void main(void)

{   

  while(1)

  {

     P1 = ~P1;

 delay_ms(1000); //延时1000ms

  }     

}

运行结果:4个LED灯每隔1秒钟亮灭一次,亮灭的时间可以通过传入的参数改变。

例子21 交换两个字符串的内容

#include <reg51.h>

#include <stdio.h>

void swap(char *p1, char *p2)

{

  char temp;

  while(*p1 != 0)

  {

    temp = *p1;

*p1 = *p2;

*p2 = temp;

p1 = p1 + 1;

p2 = p2 + 1;

  }   

}

void main(void)

{

  char str1[] = "hello";

  char str2[] = "world";    

  COM_INIT();

  printf("str1:%s str2:%s\r\n", str1, str2);

  swap(str1, str2);

  printf("str1:%s str2:%s\r\n", str1, str2);

  while(1);

}

运行结果:

str1:hello  str2:world

str1:world  str2:hello

调用swap(str1, str2)后,swap函数里执行:

char *p1=str1;

char *p2=str2;

等同于:

char *p1, *p2;

p1=str1;p2=str2

p1和p2分别指向了str1数组和str2数组的首地址,既然指向了数组存储的地址单元了,当然就可以改变数组存储的内容了。

例子22 输入两个数,输出两数中较大的数

#include <reg51.h>

#include <stdio.h>

int max(int num1, int num2)//有返回值

{   

  if(num1 > num2)

    return num1;

  else return num2;

}

void main(void)

{

  int a, b;   

  COM_INIT();

  while(1)

  {

    printf("please input two num:");

    scanf("%d %d", &a, &b);  

    printf("max num:%d\r\n", max(a, b));

  }

}

运行结果:输入两个数,中间空格隔开,程序输出两个数中的大值。

这个max函数是带返回值的,所以调用max函数执行完,函数的值就是返回的值。

读者需要注意,函数调用传过去的参数只是数值,这个数值既可以是普通的数,也可以是地址。要想在子函数里改变主函数变量的值,只能传变量的地址,子函数根据传过来的地址改变地址里存储的值,进而改变主函数变量的值。如果主函数只是把变量的值传给子函数,子函数无法改变主函数变量的值。

3. 函数的声明和定义

如果函数的实现(定义)在调用前面,那么就不用声明;如果函数的实现在调用后面,那么需要在前面声明一下。

例子23 比较两个字符串是否相等

#include <reg51.h>

#include <stdio.h>

int str_cmp(char *p1, char *p2);//函数声明

void main(void)

{

  char *str1 = "hello";

  char *str2 = "hello world";      

  COM_INIT();    

  if(str_cmp(str1, str2) == 0)

printf("=");

  else printf("!=");

  while(1);  

}

//函数定义、实现

int str_cmp(char *p1, char *p2)

{ //挨个字符比较,一直到字符不相等,或者任一字符串到结尾,退出

  while(*p1 && *p2 && (*p1 == *p2))

  {

    p1++; p2++;

  }

  return (*p1 - *p2); // 如果字符串相等返回0,否则非0

}

运行结果:

!=

注意函数声明结尾需要分号,函数实现结尾不需要分号。还要注意程序中的两个字符串都是存在程序存储区的,可以通过指针读取,但不可以通过指针修改,因为程序存储区是只读的,如果想要修改字符串内容,可以定义为字符数组。

如果一个函数在另外一个.c文件中实现,本文件中只需要用extern把函数声明一下就可以调用,实际#include的头文件里就有很多库函数的extern声明,只有extern声明后,才能调用库函数。

4. 局部变量和全局变量

函数中定义的变量为局部变量,只在本函数中有效,并且函数调用结束后,局部变量占用的存储单元一般(除非static修饰)会释放。全局变量是在函数外定义的变量,全局变量一般在所有函数中都有效,但如果全局变量和局部变量重名,则局部变量有效。因为全局变量是多个函数之间共享的,所以可用于多个函数之间传递数据。

例子23 输入5个数,输出最大值

#include <reg51.h>

#include <stdio.h>

int a[5], maxdata;//全局变量

void max()

{

  char i;//局部变量,调用结束则释放

  maxdata = a[0]; //全局变量引用

  for(i=0; i<5; i++)

  {

    if(a[i] > maxdata)

  maxdata = a[i];

  }

}

void main(void)

{  

  COM_INIT();

  while(1)

  {

    scanf("%d %d %d %d %d", &a[0],&a[1],&a[2],&a[3],&a[4]);

    max();

    printf("max data:%d\n", maxdata); //全局变量引用

  }  

}

5. 常用库函数

Keil C开发环境提供了有很多常用的库函数,用户只需要把相应的头文件#include进来就可以调用,像常用的数学函数、字符串处理函数等,详细信息请读者查看Keil C的用户手册。

6.12 中断处理程序

上一章中介绍了8051单片机支持的中断源,并提供了一个通过定时器控制LED闪烁的汇编小例子,如果用C语言实现又该怎样做呢?

例子24 定时中断实现LED灯闪烁

#include <reg51.h>

unsigned char count = 0;

void main()

{

  TMOD = 0x00;//方式0,13位计数器,计满8192溢出

  TH0  = (8192-5000)/32;

  TL0  = (8192-5000)%32;//设置计数器初始值

  IE   = 0x82; //允许定时器0中断

  TR0  = 1; //开始计数

  while(1);

}

//定时器溢出中断处理函数,interrupt后面的数字指出中断源

void timer0() interrupt 1

{

  TH0 = (8192-5000)/32;

  TL0 = (8192-5000)%32;//重设初始值

  count++;

  //12M时钟频率,机器周期是1us,计一个数1us,中断一次5000us

  //中断100次是0.5s,LED灯每0.5秒反转一次

  if(count == 100)

  {

    P1 = ~P1;//LED灯反转

count = 0;

  }

}

运行结果:LED灯每0.5秒亮灭一次。

从例子中可以看出,Keil C语言使用中断只需要在中断处理子程序后指出中断源序号,编译器会自动生成相应的跳转指令并写入中断入口,编译器还会自动生成中断返回指令。此外,如果中断子程序中使用了R0~Rn中的寄存器,则编译器会先保存使用的寄存器的值到栈,中断返回前再恢复寄存器的值,当然还可以使用using来指定使用哪一个工作寄存器组。

6.13 预处理命令

一个C语言源文件经过预处理->编译->汇编->链接等一系列流程后才能生成可执行程序。预处理本身不是C语言的一部分,只是编译之前由开发环境识别并处理的特殊命令,主要包括宏定义、文件包含和条件编译。

1. 宏定义

“#define NUM 100”代表编译前把源代码中所有的NUM替换成100。

“#define MAX(a,b) (a>b)?a:b”则不是直接替换,还包括参数展开。

例子25 输入5个数,输出5个数中最大值、最小值和平均值

#include <reg51.h>

#include <stdio.h>

#define MAX(a,b) (a>b)?a:b

#define MIN(a,b) (a<b)?a:b

#define NUM 5

void main(void)

{

  int a[NUM], max, min;

  char i;

 

  COM_INIT();

  scanf("%d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4]);

  max = min = a[0];

  for(i=0; i<NUM; i++)

  {

    max = MAX(max, a[i]);

    min = MIN(min, a[i]);

  }

  printf ("max:%d min:%d\r\n", max, min);

  while(1);

}

2. 文件包含

#include <stdio.h>就是把stdio.h文件里的内容加载到当前文件,“<stdio.h>”表示到编译器默认的系统目录里查找stdio.h文件,“stdio.h”就表示先在当前C文件所在的目录里查找stdio.h文件,如果没找到,再到系统目录里查找。

3. 条件编译

例子26 条件编译实现输出最大值和最小值之间的切换

#include <reg51.h>

#include <stdio.h>

#define MAX //宏定义

void main(void)

{

  int a[5] = {34, 54, 67, 78, 123};

  int max, min;

  char i;

 

  COM_INIT();

  max = min = a[0];   

  for(i=0; i<5; i++)

  {

#ifdef MAX//如果前面有MAX的宏定义,就编译下面代码

    if(a[i] > max)

  max = a[i];

#else//如果前面没有MAX的宏定义,就编译这一段代码

if(a[i] < min)

  min = a[i];

#endif

  }  

#ifdef MAX

  printf ("max:%d\r\n", max);

#else

  printf ("min:%d\r\n", min);

#endif  

  while(1);

}

程序运行结果:max:123,请读者注释掉MAX的宏定义,再重新编译运行。

条件编译可以实现按照给定的条件选择编译代码片段,可以灵活的选择哪些代码段参与编译,哪些代码段不参与编译,不参与编译的代码对最终生成的程序没有任何影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值