资源预览内容
第1页 / 共120页
第2页 / 共120页
第3页 / 共120页
第4页 / 共120页
第5页 / 共120页
第6页 / 共120页
第7页 / 共120页
第8页 / 共120页
第9页 / 共120页
第10页 / 共120页
亲,该文档总共120页,到这儿已超出免费预览范围,如果喜欢就下载吧!
资源描述
第第4章章 单片机的单片机的C51程序设计程序设计 吴政江制作吴政江制作 4.1 C51程序的结构特点程序的结构特点4.2 C51语法基础语法基础 4.3 C51的数据类型、存储类型及常量与变量的数据类型、存储类型及常量与变量 4.4 C51对单片机主要资源的定义对单片机主要资源的定义4.5 C51的基本运算的基本运算4.6 C51的构造数据类型的构造数据类型4.7 C51的流程构造语句的流程构造语句4.8 C51的函数的函数4.9 指针指针4.10 C51程序设计举例程序设计举例实训四:单片机控制流水灯实训四:单片机控制流水灯(C51程序程序)实训五:计数器的实训五:计数器的C51程序设计与制作程序设计与制作小结小结习题与思考题习题与思考题4.1 C51程序的结构特点程序的结构特点 4.1.1 C语言与汇编语言的比较语言与汇编语言的比较 单片机的C51编程与汇编语言编程相比,具有以下优点。(1)对单片机的指令系统不要求有任何的了解,就可以用C语言直接编程操作单片机。(2)寄存器分配、不同存储器的寻址以及数据类型等细节完全由编译器自动管理。(3)程序有规范的结构,可分成不同的函数,可使程序结构化。(4)库中包含许多标准函数,具有较强的数据处理能力,使用十分方便。(5)具有方便的模块化编程技术,使已编好的程序很容易移植。4.1.2 C51程序的结构特点程序的结构特点 举例:举例: 设单片机AT89C51的P1.0口接有一个发光二极管,如图4-1所示。试用C51编程使该发光二极管间隔1s亮灭闪动。LEDVccAT89C51 P1.0R1图4-1 驱动发光二极管#include /包含51系列单片机头文件#define uint unsigned int /宏定义sbit led1 = P10; /声明单片机P1口的第一位void delayms(uint); /声明延时子函数void main () /主函数 While(1) /大循环 led1= 0; /*点亮接在P1.0引脚上的LED*/ delayms(1000); /延时1秒 led1= 1; /*关闭接在P1.0引脚上的LED*/ delayms(1000); /延时1秒 void delayms(uint xms)/延时x毫秒子函数 uint i,j; for(i=xms;i0;i-)/*i=xms即延时约xms毫秒*/ for(j110;j;j-); 由以上例子可看出C51程序结构具有以下特点:(1)C 语言程序是由函数构成的。(2)一个函数由两部份组成。函数首部,即函数的第一行。函数体,即函数首部下面的大括号“”内的部分。函数体一般包括:a声明部分:在这部分中定义所用到的变量,如“sbit led1 = P10;”。b执行部分:由若干个语句组成。(3)一个C语言程序,总是从main ()函数开始执行的,而不管物理位置上这个main ()放在什么地方。(4)C语言源程序的前面几行通常是以“#”开头的预处理命令。预处理命令包括文件包含(include)命令、宏(define)命令以及条件编译命令等。(5)C 语言区分大小写。如将“include”写成“INCLUDE”就会编译出错。(6)C 语言书写的格式自由,可以在一行写多个语句,也可以把一个语句写在多行。(7)每个语句和资料定义的最后必须有一个分号“;”,分号“;”是C语句的必要组成部分,即语句结束标志。但预处理命令、函数头和花括号“”之后不能加分号“;”。(8)可以用/*/的形式为C程序的任何一部分作注释。 C51也支持C+风格的注释,即用“/”引导的后面的语句是注释。例如: P1_0 = !P1_0; /取反P1.0 这种风格的注释,只对本行有效,所以不会出现上面的问题,而且书写也比较方便。因此,程序在只需要一行注释的时候,建议采用这种格式。(9)一个C语言源程序可以由一个或多个源文件组成。4.2 C51语法基础语法基础 4. 2. 1 C语言词汇语言词汇(1)标识符 在程序中使用的变量名、函数名、标号等统称为标识符。C语言规定,标识符只能是字母(AZ,az)、数字(09)、下划线(_)组成的字符串,并且第一个字符必须是字母或下划线。标准C语言不限制标识符的长度,但它受各种版本的C语言编译系统限制,同时也受到具体机器的限制。在标识符中,大小写是有区别的。标识符虽然可由程序员随意定义,但标识符是用于标识某个量的符号。标识符不能使用系统保留的关键字。(2)关键字(也叫保留字)数据类型关键字。用于定义或说明变量、函数或其他数据结构的类型。主要有char,double,float,int,long,short,signed,struct,union,unsigned,void,enum等12个。控制语句关键字。用于表示一个语句的功能。主要有break,case,continue,default,do,else,for,goto,if,return,switch,while等12个。存储类型关键字。用于说明变量或函数的存储类型。主要有auto,extern,register,static等4个。预处理命令关键字。用于表示一个处理命令。主要有include、define等2个。其他关键字。主要有const,sizeof,typedef,volatile等4个。(3)运算符 C语言中含有相当丰富的运算符。如强制类型转换运算符、算术运算符、关系运算符、逻辑运算符以及位运算符等。运算符与变量、函数一起组成表达式,表示各种运算功能。运算符由一个或多个字符组成。(4)分隔符 逗号分隔符。 空格分隔符。4. 2. 2 编译预处理编译预处理 编译预处理命令概念:计算机将C语言编译为机器语言时进行的预处理。这些命令只在编译时有效,不是计算机运行的可执行语句。编译预处理命令以“#”开头,末尾不加分号,包括头宏命令、文件包含命令、条件编译命令等。 (1)宏命令 作用:用标识符来代表一个字符串,系统在编译之前自动将标识符替换为字符串。主要用于定义常量。宏定义的标识符(常量)一般用大写字母,以便与变量相区别。 定义形式:#define 标识符 字符串 例如:#define PI 3.14 /*后续程序中所有的PI都用3.14代替。即凡是出现PI的地方即表示3.14,这样做有利于阅读程序和修改程序。*/(2)文件包含 文件包含:指在一个文件中将另一个文件的全部内容包含进来,通常用来将程序中用到的系统函数、宏标识符、自定义函数等的文件包含进来。被包含的文件也叫头文件(以“.h”为扩展名的文件)。 格式:#include或#include“文件名”在Keil C51中常用的包含文件(头文件)有以下三种: reg51.h头文件。reg51.h是对51子系列单片机的特殊功能寄存器进行定义的头文件,使用汇编语言中的特殊功能寄存器名称,将各个特殊功能寄存器定义为该寄存器的直接地址,在C语言中可以通过寄存器名称直接对这些寄存器进行操作,特殊功能寄存器名称全部使用大写。reg51.h没有对单片机的四个输入输出端口进行位定义,如果程序中需要对并行端口进行位操作,可以使用regx51.h。对于52系列单片机,相应的头文件为reg52.h或regx52.h。stdio.h头文件。stdio.h是标准输入输出库函数头文件。该库函数文件是定义计算机键盘输入与计算机屏幕显示的库函数,单片机本身无需这些库函数,但为了方便利用计算机调试程序,需要包含该头文件。常用的标准输入、输出库函数有以下两种:aprintf(格式控制,输出列表);。该函数按指定格式在屏幕上显示对应输出项的值。bscanf(格式控制,地址列表);。该函数接收计算机键盘输入的数据并赋值给对应的变量。例如: int x=3;char y3=“abc”; printf(“x=%d,y=%3sn”,x,y); scanf(“%d,%cn”,&x,&y); printf(“x=%d,y=%3sn”,x,y);其中,格式控制必须包含在一对双引号内,包括格式说明符(%d)、普通字符(x=,y=)和转义控制符(n)三种类型。格式说明符前必须用百分号“%”。常用的格式说明符的含义如表4-1所示。表4-1 常用格式字符的含义格式字符说明附加格式字符说明d带符号的十六进制数m数据长度s字符串n截取字符的个数c一个字符f小数形式的实数nn位小数 常用的转义控制符有两个:n:换行。r:回车。他的前必须用反斜杠“”。 再如,在计算机屏幕上打印99乘法口诀表。参考程序如下所示:/*cfb.c*/#include /包含51系列单片机头文件#include /包含基本输入、输出头文件void cominit() /设置定时器、串行通信函数 SCON=0x50; /设置串行口控制寄存器 TMOD|=0x20; /设置定时器方式寄存器 TH1=0xf3; /设置定时器1(T1)高8位的初值 TR1=1; /启动定时器1 TI=1; /置位发送标志位void main(void) /主函数 unsigned int i,j; /定义无符号整型变量i与j cominit(); /调用设置定时器、串行通信函数 for(i=1;i=9;i+) /外循环 for(j=1;j=i;j+) /内循环printf(%dX%d=%d ,j,i,i*j); /计算机屏幕上打印一行乘法口诀表printf(n); /换行打印 在keil C51中,为了借助计算机键盘与屏幕调试程序,需要模拟单片机串行口与计算机键盘与计算机屏幕进行通信。因而需要设置定时器/计数器与串行通信,“void cominit()”函数就是为此目的而写的。程序调试运行结果如下所示: 1X1=1 1X2=2 2X2=4 1X3=3 2X3=6 3X3=9 1X4=4 2X4=8 3X4=12 4X4=16 1X5=5 2X5=10 3X5=15 4X5=20 5X5=25 1X6=6 2X6=12 3X6=18 4X6=24 5X6=30 6X6=36 1X7=7 2X7=14 3X7=21 4X7=28 5X7=35 6X7=42 7X7=49 1X8=8 2X8=16 3X8=24 4X8=32 5X8=40 6X8=48 7X8=56 8X8=64 1X9=9 2X9=18 3X9=27 4X9=36 5X9=45 6X9=54 7X9=63 8X9=72 9X9=81 用户自定义标题文件。用户将自己常用的宏定义、条件编译、图片代码、数据表格等组成一个文件,然后在各个源程序中用“#include”命令包含进来,无需重复定义。4.3 C51的数据类型、存储类型及常量与变量的数据类型、存储类型及常量与变量 4.3.1 C51的数据类型的数据类型 C51的数据类型大体上可以分为基本数据类型、构造数据类型和空类型等三种。其中基本数据类型又可分为位型、无符号字符型、有符号字符型、无符号整型、有符号整型、无符号长整型、有符号长整型、浮点型及双精度实型等九种类型。它们的符号、长度、值域等如表4-2所示。表4-2 C51的数据类型数据类型数据类型名称长度/bit长度/Byte表示数的范围bit或sbit位型10,1unsigned char无符号字符型810255signed char有符号字符型81-128+127unsigned int无符号整型162065535signed int有符号整型162-32768+32767unsigned long无符号长整型32404294967295(0232-1)signed long有符号长整型324-2147483648+2147483647(-231231-1)float单精度实型(浮点型)3243.4e -383.4e+38(相当于6位有效数字)double双精度实型6481.7e-381.7e+38(相当于10位有效数字)4.3.2 C51的数据存储类型的数据存储类型(1)数据存储类型 C51允许将变量或常量定义成不同的存储类型,以使单片机访问不同的存储空间。主要包括片内直接寻址型、片内位寻址型、片内间接寻址型、片外分页寻址型、片外间接寻址型以及代码型等六种类型,它们和单片机的不同存储区相对应。如表4-3所示。表4-3 C51存储类型与MCS-51单片机存储空间的对应关系存储类型存储类型名称长度/bit长度/Byte值域范围与MCS-51单片机存储空间的对应关系data片内直接寻址型810255直接寻址片内低128字节数据存储器RAMbdata片内位寻址型813247按位或字节寻址片内RAM的20H2FH地址空间idata片内间接寻址型810255间接寻址片内RAM的00HFFH地址空间pdata片外分页寻址型810255分页寻址256字节片外RAM,对应MOVX Rixdata片外间接寻址型162065535寻址64K字节片外RAM,对应MOVX DPTRcode代码型162065535寻址64K字节ROM,对应MOVC DPTR (2)变量存储类型定义举例变量的存储类型通常与数据类型一起使用。先说明数据类型,再说明存储类型。现举例如下:char data var1; /字符型变量var1被定义为data型,被分配在片内RAM中的低128字节中。bit bdata flags; /位变量flags被定义为bdata型,定位在片内RAM中的位寻址区。float idata x,y,z; /*浮点型变量x、y和z被定义为idata存储类型,定位在片内RAM中,并只能用间接寻址方式进行访问。*/unsigned int pdata dimension; /*无符号整型变量dimension被定义为pdata型,定位在片外数据存储区,相当于用MOVX Ri访问。*/unsigned char xdata vector1044; /*无符号字符型三维数组变量vector1044 被定义为xdata存储类型,定位在片外RAM中,占据1044=160个字节,相当于用MOVX DPTR访问。*/ (3)存储模式如果在变量定义时略去存储类型标志符,则由编译器(如Keil C51)自动默认存储类型。默认的存储类型进一步由SMALL、COMPACT和LARGE存储模式指令限制。存储模式的有关说明如表4-4所示。表4-4 存储模式及说明存储模式模式名称说 明SMALL小模式参数及局部变量放入可直接寻址的片内存储器(最大128字节,默认存储类型是data),因此访问十分方便。另外所有对象,包括栈,都必须嵌入片内RAM。栈长很关键,因为实际栈长依赖于不同函数的嵌套层数。COMPACT一般模式(中模式)参数及局部变量放入分页片外存储区(最大256字节,默认存储类型是pdata),通过寄存器R0和R1间接寻址,栈空间位于内部数据存储器中。LARGE大模式参数及局部变量直接放入片外数据存储区(最大64KB字节,默认存储类型是xdata),使用数据指针DPTR来进行寻址。用此数据指针访问的效率低,尤其是对二个或多个字节的变量,这种数据类型的访问机制直接影响代码的长度,另一个不便之处在于这种数据指针不能对称操作。4.3.3 常量与变量常量与变量(1)常量在程序运行过程中,其值不能被改变的量称为常量,如数字、字符等。每种数据类型的数值都有常量。字符型常量用单引号括起来,字符串常量用双引号括起来,十六进制数常量用“0x+数值”表示。直接常量(也叫字面常量)。如12、0、0xCE、A、“GUI ZHOU”等。符号常量。符号常量即是用一个标识符来代替一个常量。符号常量通常用前面介绍的宏命令define来定义。如:#define tscr_init 0x40#define tmodh_init 0#define tmodl_init 250也可通过const关键字来定义的符号常量。 定义形式:数据类型 const 标识符=数字 如:int const PI=3; 区别:用define定义的常量不占任何存储空间,在编译时直接用对应的数据代替其常量名称参与运算。而用const定义的常量则会占去单片机ROM区的内存,占去的空间大小为其定义数据类型所占空间的大小,如上面定义的PI常量占2个字节的空间。(2)变量 在程序运行过程中,其值可以被改变的量称为变量。每个变量都必须有变量名,变量名是标识符,必须满足用户标识符的要求。变量必须先定义后使用,定义变量的目的是说明变量的数据类型,以便为变量分配相应的存储单元。在程序使用变量前最好给变量赋初值,不赋初值的数值型变量其初值为0。变量在定义的同时可以赋初值。如: static int x,y=2,j=5; float sum; 以上两条语句定义了x、y、j三个静态整型变量和一个浮点型变量sum,并且给变量y赋初值2,j赋初值5,没有给变量x赋初值,其初值默认为0。其中的存储类型在需要特别声明的时候才需要,否则可以省略。同类变量可以共用一个数据类型说明符,各个变量之间用逗号隔开,最后以分号结束。这三个整型变量x、y、j在后面的程序中可以存放整型数据。浮点型变量sum可以存放一个浮点型数据的值。4.4 C51对单片机主要资源的定义对单片机主要资源的定义 4.4.1 使用关键字定义特殊功能寄存器(使用关键字定义特殊功能寄存器(SFR) SFR及其可位寻址的位是通过关键字sfr和sbit来定义的,这种方法与标准C不兼容,只适用于C51。(1)用sfr定义特殊功能寄存器(SFR)的字节地址格式:sfr 寄存器变量名称=地址值;功能:将右边的地址赋值给左边的寄存器变量名称。举例:asfr PSW = 0xD0; /定义程序状态字寄存器PSW的地址为D0H bsfr SCON = 0x98; /定义串行口控制寄存器SCON的地址为98H csfr P0 = 0x80; /定义P0口的地址为80H dsfr TMOD = 0x89; /定义定时器/计数器方式控制寄存器TMOD的地址为89H (2)用sbit定义特殊功能寄存器(SFR)的位地址格式:sbit的用法有三种格式,分别介绍如下。asbit 位变量名称=位地址值;bsbit 位变量名称=SFR名称变量位地址值;csbit 位变量名称=SFR地址值变量位地址值;功能:将右边的位地址赋值给左边的位变量名称。举例:asbit CY = 0xD7; /定义进位标志CY的地址为D7H bsbit AC = PSW6; /定义辅助进位标志AC为PSW的第6位,其中PSW 必须先用sfr 定义好 csbit RS0 = 0xD03; /定义RS0的地址为D3H(即D0H单元的第3位)注意:用sfr和sbit定义的SFR必须位于函数外,一般放在程序的开头。 4.4.2 通过头文件访问特殊功能寄存器(通过头文件访问特殊功能寄存器(SFR) 头文件:以“.h”为扩展名的文件。C51编译器给出的头文件已经给出了MCS-51单片机中的SFR及其可位寻址位的定义。并且Keil C51编译器将这些头文件按单片机的不同生产公司、不同型号分别存放在Keil C51的INC子目录下,在程序中只需引用这些头文件即可实现对SFR的访问和控制。头文件通常有reg51.h,reg52.h,math.h,ctype.h,stdio.h,stdlib.h,absacc.h,intrins.h等八个。但常用的却只有reg51.h或reg52.h,math.h等二个。reg51.h或reg52.h是定义51单片机或52单片机特殊功能寄存器和位寄存器的,这两个头文件中大部分内容是一样的,52单片机比51单片机多一个定时器T2,因此,reg52.h中也就比reg51.h多几行定义T2的内容。math.h是定义常用数学运算的,比如求绝对值、求方根、求正弦或余弦等,该头文件中包含有各种数学运算函数,当我们需要使用时可以直接调用它的内部函数。在代码中引用头文件,其实际意义就是将这个头文件中的全部内容放到引用头文件的位置,从而免去我们每次编写同类程序时都要将头文件中的语句重复编写。打开reg51.h,其全部内容见教材所示。举例如下:#include /使用的单片机为AT89C51main()TL0 = 0xb0; /访问定时器0,设置时间常数TH0 = 0x3c;TR0 = 1; /启动定时器04.4.3 扩展扩展I/O端口或片外端口或片外RAM的直接访问的直接访问 扩展的片外RAM或I/O端口需要用户自己先定义后才能在语句或函数中使用。现举例如下:#include /*包含头文件absacc.h,其内部将XBYTE定义为一个指针,指向外部数据存储器RAM的零地址单元*/#define PA XBYTE0xffec /将PA定义为外部I/O口,地址为0xffecmain () PA = 0x3a; /将数据3AH写入地址为0xffec的存储单元或I/O端口 4.4.4 定义和使用位变量定义和使用位变量 C51使用关键字“bit”来定义位变量。格式:bit 变量名称;功能:将变量名称定义为位型变量。例:bit lock; /将lock定义为位变量 bit dirention; /将dirention定义为位变量函数既可以有bit类型的参数,也可以有bit类型的返回值。例如: bit func(bit b0,bit b1) bit a; return (a); 4.5 C51的基本运算的基本运算 4.5.1 C51的算术运算的算术运算(1)基本的算术运算符+:加法运算符。-:减法运算符。*:乘法运算符。/:除法或求模运算符。%:取余运算符。(2)自增、自减运算符+:自增运算符。 -:自减运算符。例如:+ j、j +、- j、j-注意:+和-运算符只能用于变量,不能用于常量和表达式。+ j表示先加1,再取值;而j +表示先取值,再加1。自减运算也是如此。(3)算术表达式和运算符的优先级与结合性算术表达式就是用算术运算符和括号将运算对象连接起来的式子。例如:(2a+3b)* c/d。C51规定了算术运算符的优先级为先乘除和取模,后加减,括号最优先。C语言中各运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左)。算术运算符均为左结合性,即先左后右。例如表达式xyz,则y应先与“”号结合,执行xy运算,然后再执行z的运算。这种自左至右的结合方向就称为“左结合性”。而自右至左的结合方向称为“右结合性”。最典型的右结合性运算符是赋值运算符。如x=y=z,由于“=”的右结合性,应先执行y=z,再执行x=(y=z)运算。不同数据类型间的转换。其一是自动类型转换,即在程序编译时,由C编译器自动进行数据类型转换。其二是使用强制类型转换运算符。语句形式为:(类型说明符)(表达式)。功能:把表达式的运算结果强制转换成类型说明符所表示的类型。例如:int a,b; / a、b为整数(double) (a+b); /将a+b强制转换成double数据类型4.5.2 C51的关系运算的关系运算(1)关系运算符及其优先级关系运算符也叫比较运算符,C51提供了以下6种关系运算符。:小于运算符。 =:大于等于运算符。=:小于等于运算符。 = =:等于运算符。:大于运算符。 ! =:不等于运算符。在以上运算符中,、=、=这四个运算符的优先级相同,处于高优先级;= =和! =这两个运算符的优先级也相同,处于低优先级。此外,关系运算符的优先级低于算术运算符的优先级,而高于赋值运算符的优先级。(2)关系表达式关系表达式:用关系运算符将运算对象连接起来的式子。如:ab、(a=4)(b=3)等。关系表达式的值为逻辑值,其结果只能是真和假两种值之一。C51中用1表示真,用0表示假。4.5.3 C51的逻辑运算的逻辑运算(1)逻辑运算符及其优先级逻辑运算是对变量进行逻辑与运算、或运算及非运算。&:逻辑与运算符。|:逻辑或运算符。!:逻辑非运算符。其中非运算的优先级最高,而且高于算术运算符;或运算的优先级最低,低于关系运算符,但高于赋值运算符。(2)逻辑运算表达式逻辑运算表达式:用逻辑运算符将运算对象连接起来的式子。例如:(ab)|c&!d,表示a大于b的值跟c与d非的值相或。逻辑表达式的值与关系表达式的值一样,也是逻辑量,即真或假。4.5.4 C51的位运算的位运算位运算的操作对象只能是整形和字符型数据,而不能是实型数据。&:按位与运算符,两个字符或整数按位进行逻辑与运算。例如:var3 = var1 & var2。|:按位或运算符, 两个字符或整数按位进行逻辑或运算。例如:var3 = var1 | var2。:按位异或运算符,两个字符或整数按位进行逻辑异或运算。例如:var3 = var1 var2。:按位取反运算符,两个字符或整数按位进行逻辑非运算。例如:var1 = var2。:左移运算符,字符或整数按位进行逻辑左移运算。例如:var1 = var2:右移运算符,字符或整数按位进行逻辑右移运算。例如:var1 = var26。4.5.5 C51的赋值运算的赋值运算(1)赋值运算符赋值符号“=”完成的操作即为赋值运算,它是右结合性,且优先级最低。(2)赋值表达式将一个变量与表达式用赋值号连接起来就构成赋值表达式。形式如下:变量名=表达式功能:将“=”右边表达式的值赋给“=”左边的一个变量。例如:a = (b = 4) + (c = 6),该表达式的值为10,变量a的值也为10。(3)赋值的类型转换规则在赋值运算中,当赋值运算符“=”两侧的类型不一致时,系统自动将右边表达式的值转换成左侧变量的类型,再赋给该变量。转换规则如下。实型数据赋给整型变量时,舍弃小数部分。整型数据赋给实型变量时,数值不变,但以IEEE浮点数形式存储在变量中。长字节整型数据赋给短字节整形变量时,实行截断处理。如将long型数据赋给int型变量时,将long型数据的低两字节数据赋给int型变量,而将long型数据的高两字节的数据丢弃。短字节整型数据赋给长字节整型变量时,进行符号扩展。如将int型数据赋给long型变量时,将int型数据赋给long型变量的低两字节,而将long型变量的高两字节的每一位都设为int型数据的符号值。(4)复合赋值运算符赋值符号前加上其它运算符即构成复合运算符。+=,=,*=,/=,%=,&=,|=,=,=。其功能是先进行加、减、乘、除、取余、与、或、异或、左移以及右移运算后再进行赋值操作。例如:a += b; /等价于a = (a + b),赋值表达式加上分号即“a + = b;”也叫赋值语句x *= a+b; /等价于x = x * (a + b)a &= b; /等价于a = (a & b)a= 4; /等价于a = (a 联合元素。例如:对于前面定义的联合变量a、b、c,下面的引用方法都是正确的:a.fval /* 引用联合变量a中float型元素fval */b.ival /* 引用联合变量b中int型元素ival */csval /* 引用联合变量c中char型元素sval */注意:在引用联合元素时,要注意联合变量用法的一致性。因为联合类型中定义的各个不同类型的元素都可以分时地赋给变量,而所读取变量的值是最近放入的某一元素的值,因此在表达式中对它进行处理时,必须注意其类型与表达式所要求的类型保持一致,否则将导致程序运行出错。不能只引用联合变量。例如,下面的写法就是错误的:printf(“%f”,a);因为变量a可能是float、int和char三种类型,分别占用不同长度的内存区域,若在引用时仅写联合变量名a,系统将难以确定究竟应该输出哪一个联合元素的值。正确的写法应为:printf(“%f”,a. ival); 联合类型的数据可以采用同一个内存段来存放几种不同类型元素的值,但是在每一瞬时只能存放其中一种类型的元素,而不能同时存放几种。换句话说,每一瞬时只有一个元素在起作用。起作用的是联合中最后一次存放的元素,如果存入了一个新的元素,则上次存入的元素就自动失去作用。例如,对于下列语句:a.fval = 100.25;a.ival = 200;a.sval = 10;在执行以上三条赋值语句之后,只有a.sval是有效的,而a. ival和a.fval都已失去作用。因此在引用联合变量时一定要十分注意当前在联合变量中存放的究竟是哪一个元素。不能直接对联合变量进行赋值,也不能在定义联合变量时对它进行初始化。4.7 C51的流程控制语句的流程控制语句 4.7.1 选择控制语句(选择控制语句(有if语句和switch/case语句,用于设计分支结构程序)(1)if语句 if 语句是用来判定所给定的条件是否满足,再根据判定的结果(真或假)来决定执行给出的两种操作之一。C51提供了三种形式的if 语句。形式1。格式: if (表达式) 语句;功能:如果表达式的结果为真,则执行语句,否则不执行。形式2。格式: if (表达式) 语句1; else 语句2;功能:如果表达式的结果为真,则执行语句1,否则执行语句2。形式3。格式:if (表达式1) 语句1;else if (表达式2) 语句2;else if (表达式3) 语句3;else if (表达式m) 语句m;else 语句n;功能:这是if语句的嵌套,即一个if语句中又包含有一个或多个if语句。主要用于多分支选择程序。在if语句的嵌套中应注意if与else的对应关系,else总是与它前面最近的一个if语句相对应。 【例4.1】 某浮点数的范围在0.0009999之间,试编写一个函数返回浮点数的小数点位置。解:此题的基本思路是根据浮点数的4种不同取值范围给出4种不同的返回值。可以约定浮点数的大小在0.0009.999、10.0099.99、100.0999.9、10009999之间时,分别返回0、1、2和3。参考程序如下:int ftochar (float valp) int dotno = 0; if (valp10.00) dotno = 0; else if (valp= 10.0) & (valp100.0) dotno = 1; else if (valp= 100.0) & (valp1000.0) dotno = 2; else if (valp= 1000.0) dotno = 3;return dotno;根据小数点的位置,即可在实际的单片机系统中显示出浮点数或小数。(2)switch/case语句(语句(用于直接处理多分支选择)switch/case语句的一般形式如下:switch (表达式)case 常量表达式1:语句1;break;case 常量表达式2:语句2;break;case 常量表达式n:语句n;break;default:语句n+1;说明:当switch后面括号内的“表达式”的值与某一个case后面的常量表达式的值相等时,就执行此case 后面的语句(可以是复合语句),遇到break语句就退出switch语句。若所有的case中的常量表达式的值都没有与表达式的值匹配时,就执行default 后面的语句。【例4.2】 AT89C51单片机的P1. 1和P1. 0引脚接有两只按键,其4种逻辑组合分别点亮由P2.0P2.3控制的4只LED(高电平点亮),试编程实现此功能。解:参考程序如下:#include /包含AT89x51系列单片机的头文件void main () char a;do a = P1; a = a&0x03; /屏蔽掉P1口的高6位 P2 = P2&0xf0; /开始4只LED全熄 switch(a) case 0:P2 =P2|0x01;break; case 1:P2 =P2|0x02;break; case 2:P2 =P2|0x04;break; case 3:P2 =P2|0x08; while(1); /循环继续读取按键4.7.2 循环控制语句循环控制语句 循环程序可分为“当型”循环程序和“直到型”循环程序两种类型,这与其它语言的循环程序一样。在C51语言中,用来构成循环控制的语句有:while语句、do-while语句、for语句以及if和goto语句等。(1)基于while语句构成的循环采用while语句构成循环结构的一般格式如下:while (条件表达式) 内部语句; /内部语句可以是复合语句,也可以为空 意义:当条件表达式的结果为真(非0值)时,程序就重复执行后面的内部语句,一直执行到条件表达式的结果变为假(0值)时为止。也就是说,这种循环结构是先检查条件表达式所给出的条件,再根据检查的结果决定是否执行后面的内部语句。如果条件表达式的结果一开始就为假,则后面的内部语句一次也不会执行。因此属于“当型”循环。图4-4为while语句的执行过程。 假条件表达式内部语句真图4-4 while语句的执行过程【例4.3】 使用while语句计算自然数1100的累加和,并用printf ()函数通过单片机的串口显示在终端上。解:参考程序如下:#include /*包含基本输入输出头文件stdio.h*/真内部语句条件表达式假图4-5 do-while语句循环结构的流程图void main () int i, sum = 0; i = 1; while (i=100) /*复合语句循环体开始*/ sum = sum+i; i+; /*复合语句循环体结束* /printf (“1+2+100 = %dn”, sum );while (1);(2)基于do-while语句构成的循环do-while语句只能用来实现“直到型”循环,其一般格式如下:do 内部语句; /可以是复合语句 while(条件表达式);意义:先执行给定的循环体语句,然后再检查条件表达式的结果。当条件表达式的值为真(非0值)时,则重复执行循环体语句,直到条件表达式的值变为假(0值)时为止。因此,用do-while语句构成的循环结构在任何条件下,循环体语句至少会被执行一次,这就是“直到型”循环。图4-5给出了这种循环结构的流程图。真内部语句条 件 表 达式假图4-5 do-while语句循环结构的流程图【例4.4】 实型数组sample存有10个采样值,编写一个函数返回其平均值(即平均值滤波程序)。解:参考程序如下:float avg (float *sample) float sum = 0; char no = 0; do sum += sampleno; no +; while (no10); return (sum/10); 假初值设定表达式内部语句更新表达式循环条件表达式真图4-6 for语句的执行过程示意图(3)基于for语句构成的循环采用for语句构成循环结构的一般格式如下:for(初值设定表达式;循环条件表达式;更新表达式) 内部语句; /可以是复合语句 意义:先计算出初值设定表达式的值作为循环控制变量的初值,再检查循环条件表达式的结果,当满足条件时就执行循环体语句并计算更新表达式,然后再根据更新表达式的计算结果来判断循环条件是否满足,只有一直进行到循环条件表达式的结果为假(0值)时才退出循环体。各表达式之间必须用分号“;”进行分隔。for语句的执行过程如图4-6所示。【例4.5】 使用for语句计算自然数1100的累加和,并用printf ()函数通过单片机的串口显示在终端上。解:参考程序如下:#include#includeint getsum(void);void main()SCON = 0x50; /如果用Keil C进行模拟调用或使用printf (),必须初始化SCONTMOD = TMOD|0x20; /定时器T1工作方式2,用做波特率控制TH1 = 0xfd; /9600bps对应T1的时间常数为0xfdTL1 = 0xfd;TR1 = 1; /启动T1TI = 1; /启动发送,以发送第一个字符printf (“%dn”, getsum ();do while (1);int getsum (void)int sum = 0;int n;for (n =1;n=100;n +) sum = sum+n; return (sum);(4)基于if和goto构成的循环“当型”循环。采用if和goto可以构成“当型”循环程序,其格式如下:loop:if (条件表达式) 内部语句; /可以是复合语句 goto loop; 说明:loop是语句标号,实质是带冒号“:”的标识符,原则上任何一条语句都可以有标号,标号和语句之间用冒号“:”分隔。其执行过程是首先判断条件表达式的值,当条件表达式的值为真(非0值)时,则执行内部语句,遇到goto loop语句时则无条件返回loop处继续判断条件表达式的值。直到条件表达式的值为假(0值)时才退出循环。需注意的是,条件表达式中条件变量的值必须在内部语句中进行改变,否则就成为死循环程序。“直到型”循环。采用if和goto也可以构成“直到型”循环程序,其格式如下:loop: 内部语句; /可以是复合语句 if (条件表达式) goto loop; 说明:首先执行内部语句(即循环体),再判断条件表达式的值。若条件表达式的值为真(非0值)时,则执行goto loop语句,无条件返回loop处继续执行内部语句(即循环体)。当条件表达式的值为假(0值)时就退出循环。显然,条件表达式的值不能一直为真(非0值),否则就成为死循环程序。goto语句。goto语句是一个无条件转向语句,它的一般形式为:goto 语句标号;前已述及,语句标号是一个带冒号“:”的标识符,将goto语句和if语句相结合使用,就可以构成“当型”循环或“直到型”循环结构。【例4.6】 使用goto语句跳出循环结构。 本程序采用循环结构来求一个整数的等差数列,该数列满足条件:头四个数的和值为26,积值为880。该数列的公差应为正整数,否则将产生负的项,此外该数列的首项数必须小于5,且其公差也应小于5,否则头四项的和值将大于26。解:参考程序如下:#includevoid main () int a,b,c,d,i; for (a =1;a5;+ a) for (d =1;d5;+ d) b = a + (a + d) + (a + 2 * d) + (a + 3 * d); c = a * (a + d) * (a + 2 * d) * (a + 3 * d); if (b = = 26 & c = = 880) goto pt; pt: for (i =0;i=10;+ i) printf (“%d,”, a + i * d); printf (“n”); while (1);continue语句。 在循环结构中还可以使用一种中断语句continue,它的功能是结束本次循环,即跳过循环体中下面尚未执行的语句,把程序流程转移到当前循环语句的下一个循环周期,并根据循环控制条件决定是否重复执行该循环体。continue语句的一般格式为:continue; continue语句通常和条件语句一起用在由while、do-while和for语句构成的循环结构中,它也是一种具有特殊功能的无条件转移语句,但与goto、break语句不同,continue语句并不跳出循环体,而只是根据循环控制条件确定是否继续执行循环语句。【例4.7】 利用continue语句把1020之间不能被3整除的数输出。解:参考程序如下:#includevoid main () int n; for (n =10;a=20;n + ) if (n%3 = = 0) continue; printf (“%d”,n); while (1);4.8 C51的函数的函数 4.8.1 函数的分类与定义函数的分类与定义(1)函数的分类主函数main ()。关于主函数main (),我们应该很熟悉了。下面再简要介绍其格式和特点。格式:void main () /*注意:后面没有分号*/ 总程序从这里开始执行; 其他语句; 特点:无返回值,无参数。 无返回值表示该函数执行完后不返回任何值,上面main前面的void表示“空”,即不返回值的意思。无参数表示该函数不带任何参数,即main后面的括号中没有任何参数,只写“( )”就可以了,也可以在括号里写上void,表示空的意思,如void main (void)。普通函数。a标准库函数。b用户自定义函数。(2)函数的定义函数定义的一般形式为: 函数类型 函数名 (形式参数列表) 形式参数说明 局部变量定义; 函数体语句; 其中,“函数类型函数类型”说明了自定义函数返回值的类型。返回值的类型可以是基本数据类型(如int、char、float、double等),也可以是指针类型。当函数没有返回值时,则使用标识符void进行说明。若没有指定函数的返回值类型,则默认返回值为整型类型。一个函数只能有一个返回值,该返回值是通过函数中的return语句获得的。“函数名函数名”是用标识符表示的自定义函数的名字,它必须是一个合法的标识符。“形式参数列表形式参数列表”中列出的是在主调用函数与被调用函数之间传递数据的形式参数,形式参数的类型必须加以说明。形式参数可以是基本数据类型的数据、指针类型数据、数组等。在没有调用函数时,函数的形式参数和函数内部的变量未被分配内存单元,即它们是不存在的。如果定义的是无参数函数,可以没有形式参数,但圆括号不能省略。“局部变量定义局部变量定义”是对在函数内部使用的局部变量进行定义。局部变量是定义在函数内部的变量,编译器只在定义该变量的函数范围内给其分配存储空间。一旦调用结束,即刻释放所分配的内存单元。不同函数中的局部变量可以同名,但编译器会给它们分配不同的存储空间。形式参数也是一种局部变量。如果将变量定义在主函数之前,则成为全局变量。在其他函数中不加定义即可使用全局变量,各个函数对全局变量的赋值都会引起全局变量内容的变化,因为各个函数修改的是同一个存储单元的内容。对同一名称变量,如果在某一函数中有对该变量的定义(静态变量除外),则该变量在这个函数中成为局部变量,而在该函数之外是全局变量。“函数体语句函数体语句”是为完成该函数的特定功能而设置的各种语句。在函数体中可以根据用户自己的需要,设置各种不同的语句。这些语句应能完成所需要的功能。【例4.8】 定义一个计算整数的正整数次幂的函数。解:参考程序如下:int power (x,n) int x,n; int i,p; p =1; for (i = 1;i= n;+ i) p = p * x; return (p);这里定义了一个返回值为整型的函数power (),它有两个形式参数:x,n。形形式参数的作用式参数的作用是接受从主调用函数传递过来的实际参数的值。上例中形式参数x和n被说明为int类型。大括号以内的部分是自定义函数的函数体。函数体。上例中在函数体内定义了两个局部变量i和p,它们均为整型数据。需要注意的是,形式参数的说明与函数体内局部变量的定义是完全不同的两个部分,前者应写在大括号的外面,而后者是函数体的一个组成部分,必须写在大括号里面。(3)返回语句return 返回语句用于终止函数的执行,并控制程序返回到调用该函数时所处的位置。返回语句有两种格式:return (变量或表达式); 或者 return; 如果return语句后边带有变量或表达式,则要计算该变量或表达式的值,并将该变量或表达式的值作为该函数的返回值返回到主调用函数中去。函数返回值的类型就是该函数的类型,因此在定义一个函数时,函数本身的类型应与return语句中变量或表达式的值的类型一致。如果函数的类型与return语句中变量或表达式的值的类型不一致,则以函数的类型为准。对于数值数据可以自动进行转换,即函数的类型决定返回值的类型。 若使用不带表达式的第2种形式,则被调用函数返回主调用函数时,函数值不确定。一个函数的内部可以含有多个return语句,但程序仅执行其中的一个return语句而返回主调用函数。 如果不需要被调用函数返回一个确定的值,则可以不要return语句。在这种情况下,当程序执行到最后一个界限符“”处时,就自动返回主调用函数。对于不需要有返回值的函数,可以将该函数定义为void类型(空类型)。 4.8.2 函数的调用函数的调用(1)函数的调用形式 C语言程序中函数是可以互相调用的。所谓函数调用就是在一个函数体中引用另外一个已经定义了的函数,前者称为主调用函数,后者称为被调用函数。 函数调用的一般形式如下:函数名 (实际参数列表); “函数名”指出被调用的函数。 “实际参数列表”中可以包含多个实际参数,各个参数之间必须用逗号隔开。实际参数的作用是将它的值传递给被调用函数中的形式参数。因此,函数调用中的实际参数与函数定义中的形式参数必须在数量、类型以及顺序上严格保持一致,以便将实际参数的值正确地传递给形式参数。否则在函数调用中会产生意想不到的结果。实际参数可以是常数,也可以是变量或表达式,但要求它们具有确定的值。如果调用的是无参函数,则可以没有实际参数列表,但圆括号不能省略。 在C语言中可以采用三种方式完成函数的调用: 函数语句:指在主调函数中将函数调用作为一条语句使用。 例如:fun1 ( ); 这是无参函数,它不要求被调用函数返回一个确定的值,只要求它完成一定的操作。 函数表达式:指在主调用函数中将函数调用作为一个运算对象直接出现在表达式中,这种表达式就称为函数表达式。 例如:c = power(x,n) + power (y,m);实际上,这是一个赋值语句,它包含两个函数调用,每个函数调用都有一个返回值,将两个返回值相加的结果赋值给变量c。因此这种函数调用方式要求被调用函数返回一个确定的值。 函数参数:指在主调用函数中将函数调用作为另一个函数调用的实际参数。例如:y = power (power (i,j),k);其中,函数调用power (i,j)放在另一个函数调用power (power (i,j),k)的实际参数列表中,以其返回值作为另一个函数调用的实际参数。这种在调用一个函数的过程中又调用了另外一个函数的方式,称为嵌套函数调用。 (2)在主调用函数中对被调用函数的说明 与使用变量一样,在调用一个函数之前(包括标准库函数),必须对该函数的类型进行说明,即“先说明,后调用”。 函数说明的一般形式:类型标识符 被调用的函数名 (形式参数列表);其中,“类型标识符”说明了函数返回值的类型。 “形式参数列表”中说明各个形式参数的类型。 注意: 函数的说明与函数的定义是完全不同的。 如果被调用函数是在主调用函数的前面定义的,或者已经在程序文件的开始处说明了所有被调用函数的类型,在这两种情况下不必再对被用函数进行说明。 可以将所有用户自定义函数的说明另存为一个专门的头文件,需要时用#include将其包含到主程序中去。 在C语言程序中不允许在一个函数定义的内部包括另一个函数的定义,即不允许嵌套函数定义。但是允许在调用一个函数的过程中包含另一个函数调用,即嵌套函数调用在C语言程序中是允许的。【例4.9】 嵌套函数的例子。程序如下:#includeint Max (int x,int y); /对被调用函数进行说明void main () /主函数 int a, b; /主函数的局部变量定义 printf (“input a and b: n”); scanf (“%d %d”,%a,%b); /调用库输入函数,从键盘获得a、b的值 printf (“Max = %d”,Max (a,b); /调用库输出函数,输出a、b中较大者 while (1);int Max (int x,int y) /功能函数定义 int z; /功能函数的局部变量定义 if (xy) /函数体语句 z = x; else z =y; return (z); (3)实际参数的传递方式 在进行函数调用时,必须用主调用函数中的实际参数来替换被调用函数中的形式参数,这就是所谓的参数传递。有三种参数传递方式: 基本类型的实际参数传递。 数组类型的实际参数传递。 指针类型的实际参数传递。 4.9.1 内存单元、地址和指针内存单元、地址和指针(1)内存单元和地址 在计算机中,运行的程序和数据都是存放在计算机的内存中的。内存的基本单元是字节(Byte),一般把存储器中的一个字节称为一个内存单元。不同的数据类型所占用的内存单元数是不同,如整型量占2个单元,字符量占1个单元等。为了正确地访问内存单元,必须给每个内存单元一个编号,这个编号就是该内存单元的地址。(2)变量与地址 程序中每个变量在内存中都有固定的位置,有具体的地址。由于变量的数据类型不同,它所占的内存单元数也不同。若在程序中定义以下变量。 int a=2,b=3; float x=4.5,y=5.8; double m=3.141; char ch1=a,ch2=b;4.9 指针指针 让我们先看一下C51编译系统是怎样为变量分配内存的。变量a、b是整型,在内存中各占2个字节;x、y 是单精度实型,在内存中各占4个字节;m是双精度实型,在内存中占8个字节;ch1、ch2是字符型,在内存中各占1个字节。由于计算机内存是按字节编址的,设变量从内存2A00H单元开始存放,则编译系统对变量在内存中的单元分配情况如图4-7所示。 结论:变量在内存中按照数据类型的不同,占内存的大小也不同,都有具体的内存单元地址,如变量a在内存中的地址是2A00H,占据两个字节后,变量b的内存地址就为2A02H,变量m的内存地址为2A0CH等。也就是说,经过编译后所定义的各个变量与内存单元地址具有一一对应的关系。 (3)指针的概念 对内存中变量的访问方式 a“直接访问”方式。 b“间接访问”方式。 变量的指针与指针变量 a变量的指针。变量的指针就是变量在内存中存放的地址。 b指针变量。指针变量是一种特殊类型的变量(即是指向变量的变量),其内容是变量的地址。指针变量简称为指针,其值不仅可以是变量的地址,也可以是其他数据结构的地址,比如在一个指针变量中可存放一个数组或一个函数的首地址。 指针的类型。当定义一个指针变量时,若未给出它所指向的对象的存储类型,则该指针变量被认为是一般指针;反之若给出了它所指对象的存储类型,则该指针被认为是基于存储器的指针。 指针变量与变量在内存中的关系。设有一组指针变量pa,pb,px,py,pm,pch1,pch2分别指向上述的变量a,b,x,y,m,ch1,ch2。指针变量也同样存放在内存中,设从1A00H单元开始存放,则二者的关系如图4-8所示。 4.9.2 指针变量的定义、赋值与引用指针变量的定义、赋值与引用(1)指针变量的定义指针变量与C语言中其他变量一样,在使用前必须先进行定义。有两种定义指针变量的形式。基于存储器的指针变量的定义在定义一个指针时,若给出了它所指对象的存储类型,则该指针是基于存储器的指针。基于存储器的指针的定义形式如下:数据类型 存储类型1 *存储类型2 指针变量;存储类型1表示被定义为基于存储器的指针,无此选项时,被定义为后面介绍的一般指针。存储类型2指定指针本身的存储空间。若无此选项时,则由默认的存储器中(由编译器模式决定)确定指针本身的存储空间。char xdata *px; /* px指向一个片外RAM的字符变量,px本身在默认的存储器中(由编译器模式决定),占用2个字节*/char xdata *data py; /* py指向一个片外RAM的字符变量,py本身在片内data中,与编译模式无关,占用2个字节*/char data *c_ptr; /表示指向data区中的char型变量,c_ptr本身在默认的存储器中char data * data c_ptr; /表示指向data区中的char型变量,c_ptr本身在片内存储区data中int xdata *i_ptr; /表示指向xdata区中的int型变量,i_ptr本身在默认的存储器中int xdata * xdata i_ptr; /表示指向xdata区中的int型变量,i_ptr本身在片外存储区xdata中;long code *l_ptr; /表示指向code区中的long型变量,l_ptr本身在默认的存储器中long code * xdata l_ptr; /表示指向code区中的long型变量,l_ptr本身在片外存储区xdata中一般指针变量的定义在定义一个指针时,若未给出它所指向的对象的存储类型,则该指针变量被认为是一般指针,其存储类型由编译模式决定。在函数的调用中,函数的指针参数需要用一般指针。一般指针的定义形式如下:数据类型 *存储类型 指针变量;存储类型 指定指针本身的存储空间。若无此选项时,则由默认的存储器中(由编译器模式决定)确定指针本身的存储空间。例如:char *pz; / pz被定义为一般指针这里没有给出pz所指变量的存储类型,因而是一般指针,也没有指出pz本身的存储空间,pz本身处于编译模式默认的存储区,长度为3个字节。基于存储器的指针与一般指针的区别在于它们的存储字节不同。基于存储器的指针在内存中占用2个字节,而一般指针在内存中占用3个字节。其中第一个字节存放该指针存储类型的编码(在编译时由编译模式的默认值确定),第二字节和第三字节分别存放该指针的高位和低位地址偏移量,其格式如表4-5所示。存储类型由编译模式决定,不同的存储区域的编码值如表4-6所示。 表4-5 一般指针占用的3字节及其分配地址+0+1+2内容存储类型的编码高位地址偏移量低位地址偏移量表4-6 存储类型的编码值存储类型idata/data/bdataxdatapdatacode编码值0x000x010xFE0xFF(2)指针变量的赋值(有两种方法)对指针变量初始化。例如:int a,*p=&a;该语句定义了一个整型变量a和一个指向整型变量的指针p,并将整型变量a的地址赋值给指针p。其中&为取变量地址运算符,用于获取某个变量的地址。用赋值语句对指针变量赋值。例如:int a,*p;p =&a;用这种方法,被赋值的指针变量前不能再加“*”说明符,如写为“*p=&a;”是错误的。另外,由于指针变量用于存放另一同类型的变量的地址,因而不允许将任何非地址类型的数据赋给它。如“p=2000;”就属于不合法。(3)指针变量的引用 指针变量的引用是指取出指针变量所指对象的值,其引用形式为:*指针变量其中,“*”是取内容运算符(注意是取变量的内容,而不是取变量的地址)。它是单目运算符,其结合性为右结合,用来表示指针变量所指向的数据对象。在“*”运算符之后所跟的变量必须是指针变量。例如,若定义了变量及指向该变量的指针为int a,*p;p =&a;则称p指向变量a,或者说,p具有了变量a的地址。在以后的程序中,凡是可以写成&a的地方,就可以替换成指针的表示p,a也可以替换成*p。 下面再看几个问题。&*p的含义是什么? &*p与& a相同,即变量a的地址。*& a的含义是什么?先进行& a运算,得到a的地址,再进行*(取内容)运算,结果就是a。这里*& a、*p及变量a等价。(*p)+相当于a+,它与* p+不同。* p+等价于*(p+),即先进行*运算,得到a的值,然后使p的值改变(加1),这样p便不再指向a了。4.9.3 指针与数组指针与数组 1、指向数组的指针。引用数组元素可以用下标法(如a3),也可以用指针法,即通过指向数组元素的指针找到所需的数组元素。假如我们定义了一个一维数组,那么数组在内存中会有系统分配的一个存储空间,其数组名就是该数组在内存中的首地址。若再定义一个指针变量,并将数组的首地址传给指针变量,则该指针就指向了一个一维数组。也就是说,数组名是数组的首地址,也就是数组的指针;而定义的指针变量就是指向该数组的指针变量。例如: int a10,*ptr;/*定义一维数组a与指针变量ptr */ ptr=a; /*将一维数组a的首地址赋值给指针变量ptr */或 ptr=&a0;则指针变量ptr就得到了数组的首地址。其中,a是数组的首地址,&a0是数组元素a0的地址,由于a0的地址就是数组的首地址,所以两条赋值语句效果完全相同。指针变量ptr就是指向一维数组a的指针变量。2、C语言用指针对数组的操作方法。(1)ptrn与an表示数组元素an的地址,即&an。对整个a数组来说,共有10个元素,n的取值范围为09,则数组元素的地址就可以表示为ptr0ptr9或a0a9,与&a0&a9保持一致。(2)知道了数组元素的地址表示法,*(ptrn)和*(an)就表示数组的元素,即等效于an。(3)指向数组的指针变量也可用数组的下标形式表示为ptrn,其效果相当于*(ptrn)。根据以上叙述,可以用4种方法来访问数组元素。第1种为下标法,用ai形式访问数组元素;第2种为指针法,用*(ptri)形式访问数组元素(间接访问的方法);第3种为数组名法,用*(ai)形式访问数组元素;第4种为指针下标法,用ptri形式访问数组元素。【例4.10】 输入/输出一维数组各元素。方法1:下标法#includemain() int i,a10,*ptr=a; for(i=0;i=9;i) scanf(“%d”,&ai); for(i=0;i=9;i) printf(“%4d”,ai); printf(“n”);方法3:数组名法#includemain() int i,a10,*ptr=a; for(i=0;i=9;i) scanf(“%d”,ai); for(i=0;i=9;i) printf(“%4d”,*(ai); printf(“n”);方法2:指针法#includemain()int i,a10,*ptr=a; for(i=0;i=9;i) scanf(“%d”,ptri); for(i=0;i=9;i) printf(“%4d”,*(ptri); printf(“n”);方法4:指针下标法#includemain() int i,a10,*ptr=a; for(i=0;i=9;i) scanf(“%d”,&ptri); for(i=0;i=9;i) printf(“%4d”,ptri); printf(“n”);或#includemain() int i,a10,*ptr=a; for(i=0;i=9;i) scanf(“%d”,ptr); ptr=a;/*指针变量重新指向数组首地址*/ for(i=0;i=9;i) printf(“%4d”,*ptr); printf(“n”);4.9.4 指针变量作为函数的参数指针变量作为函数的参数(1)值传递参数方式如果函数的形参不是指针型变量,则称为值传递参数方式。使用值传递方式的特点是不会改变实参的值。如定义了如下从小到大排序的函数:void max(int a,int b)int t;if(ab) t=a; a=b; b=t;调用时使用语句:x=5;y=4;max(x,y);函数调用时,a和b被分别赋值为5和4(即a=5,b=4)。在函数体中,经过计算a=4,b=5(即进行了交换)。但是由于函数调用结束时a和b所占的内存空间就会被销毁,对于x和y的值无任何影响,其值仍旧为5和4。此时若要求对x和y的值也有所改变则要使用地址传递参数的方法。(2)地址传递参数方式 如果函数的形参是指针型变量,则称为地址传递参数方式。使用地址传递参数方式的特点是不仅可以改变形参的值,也可以改变实参的值。如上述从小到大排序的函数可定义如下:void max(int *a,int *b)int *t;if(*a*b) *t=*a; *a=*b; *b=*t;调用时使用语句:int x=5;int y=4;max(&x,&y);【例4.11】 用选择法对10个整数排序(从大到小排序)。#includesort(int *x,int n) int i,j,k,t; for(i=0;in1;i) k=i; for(j=i1;jn;j) if(*(xj)*(xk) k=j; if(k!=i) t=*(xi); *(xi)=*(xk); *(xk)=t; main() int *p,i,array10; p= array; for(i=0;i10;i) scanf(“%d”,p);p= array;sort(p,10);for(p=array,i=0;i10;i) printf(“%4d”,*p); p; 4.10.1 在在C51中加入汇编语言语句中加入汇编语言语句 Keil C51编译器能对C51源程序进行高效率的编译,生成高效简洁的代码,在绝大多数场合采用C语言编程即可完成预期的目的。但有时为了编程直观或某些特殊地址的处理,还须采用一定的汇编语言编程。而在另一些场合,出于某种目的,汇编语言也可调用C语言。为此Keil C51编译器提供了与汇编语言程序的接口规则,按此规则可以很方便地实现C语言程序与汇编语言程序的相互调用。实际上,C语言程序与汇编语言程序的相互调用可以视为函数的调用,只不过此时函数是采用不同语言编写的而已。因此,C51语言与汇编语言的混合编程,关键是参数的传递和返回值。它们必须有完整的约定,否则数据的交换可能出错。本小节主要介绍在C语言中加入汇编语言的两种方法。1、对汇编函数名等定义使用C语言,但是在函数的内部是通过#pragma asm和#pragma endasm关键字来写入汇编程序的;2、将所有的与函数有关的代码都用汇编语言来实现,将这些汇编代码存成单独的文件,以.A51或.ASM为文件后缀名,并将其添加到项目中,然后在要调用这些汇编程序的C语言文件中使用exter关键字来定义出函数原型,这样就可以直接对其进行调用了。由于第一种方法比较简单,所以它也就被广大程序设计人员所采用。4.10 C51程序设计举例程序设计举例【例4.12】 编程实现由键盘输入两个整数,延时一段时间后将较大者输出在显示器上。要求用C语言编写比较大小的主程序,而延时程序用汇编语言编写,在C51主程序中调用汇编语言延时子程序。解:参考程序如下:#pragma SRC /注意这个语句是加在整个程序开头的#include#includevoid main () /C语言主程序int a,b;int z;printf (“input a and b: n”);scanf (“%d %d”,%a,%b);if (ab) /比较a和b的大小 z = a; else z = b;delay (); /调用汇编语言延时子程序printf (“Max = %d”,z); /输出a、b中较大者的值while (1); /动态停机void delay (void) /定义汇编语言延时子程序#pragma asm /这条语句是加在被调用汇编语言程序的开头的LOOP2: MOV R0, #34HLOOP1: MOV R1, #7BHNOPNOPDJNZ R1, LOOP1DJNZ R0, LOOP2RET#pragma endasm /这条语句是加在被调用汇编语言程序的末尾的4.10.2 LED动态显示驱动程序设计动态显示驱动程序设计图4-9 时钟电路硬件电路原理图【例4.12】 试用AT89C51单片机设计一个时钟显示电路。要求:(1)用6位LED数码管分别显示时、分、秒。(2)可控制采用24小时制或12小时制显示。(3)时、分均可用按键来进行设定。(4)采用C51进行程序设计。 解:硬件电路如图4-9所示。其中U1为AT89C51单片机。U2为共阴极的六位LED数码管,A、B、C、D、E、F、G、Dp为其段选线与小数点,1、2、3、4、5、6为其位选线。RP1为由7个电阻组成的排阻,这里作为LED数码管的上拉电阻用。K1、K2和K3为三个功能开关,分别用于设置小时、设置分钟和切换12/24小时制及循环加。具体控制为:(1)当系统正常运行时,若按下K1键,则进入设置小时状态,此时若按K3键,则小时循环加1递增,再按一下K1键,则又转入正常运行状态;(2)当系统正常运行时,若按下K2键,则进入设置分钟状态,此时若按K3键,则分钟循环加1递增,再按一下K2键,则又转入正常运行状态;(3)当系统正常运行时,若按K3键,则进行12/24小时制切换,再按一次K3键,则又转入正常运行状态。本系统的硬件及软件已通过Proteus 6.7和Keil uVision2联合调试。参考程序:见教材所示。实训四:单片机控制流水灯实训四:单片机控制流水灯(C51程序程序)1实训目的实训目的 通过本次实训,(1)进一步掌握Keil C51 uvision4的基本操作方法;(2)进一步掌握ISIS7 professional的基本操作方法;(3)掌握单片机C51程序设计方法;(4)能熟练调试C51源程序。2知识要点知识要点(1)硬件电路及其工作原理 硬件电路仍然采用第2章实训二中的图2-15。它就是单片机最小应用系统加上P1口所接8个发光二极管。所不同的是实训二中只要求用AT89C51编程控制接在P1.0上的发光二极管亮灭,即亮一段时间然后熄灭一段时间再亮一段时间,依此规律循环。现在要求用AT89C51编程控制8只发光二极管,使各发光二极管从P1.0开始点亮并延时熄灭,然后再点亮下一个发光二极管,8只发光二极管循环点亮后再从P1.0开始重复循环。这种显示方式下的发光二极管通俗地称为流水灯。我们在第3章实训三中用汇编语言编程实现过该功能,现在改用C51编程实现同一功能。可见,同样一个硬件电路采用不同的控制程序就能实现不同的功能,采用不同的语言编程亦能实现相同的功能。这正是单片机的奇妙之处。可以说,单片机就是让内内外外都相同的一块集成电路(MCU)通过控制程序实现千千万万个不同的具体功能。只要你编得出程序,单片机就能完成你所需要的功能。(2)参考程序C51参考源程序如下所示: #include /51系列单片机的头文件unsigned char code shuju8=0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f; /定义流水灯数据void yanshi02s(void) /0.2秒延时函数 unsigned char i,j,k;/定义变量 for(i=2;i0;i-) for(j=200;j0;j-) for(k=250;k0;k-);void main(void) /主函数 unsigned char jishu; /定义变量 while(1) /无限循环 for(jishu=0;jishu8;jishu+) /完成8次循环 P1= shujujishu;/依次把数组里的数据输出到P1口 Yanshi02s(); /延时0.2秒 3实训器材实训器材(1)DICE-5208K开发型单片机综合实验仪1套。(2)PC机1台。(3)DICE-3000仿真器1台。(4)带插针的导线若干。4实训内容及步骤实训内容及步骤(1)启动ISIS7 professional软件(具体使用方法见本教材附录五),并用其绘制本实训图2-15所示电路原理图。(2)启动Keil C51 uvision4软件(具体使用方法见本教材附录四)。建立工程,输入上述C51参考源程序并编译调试生成二进制的目标文件。(3)将第(2)步生成的二进制目标文件加载到第(1)步所绘图2-15所示电路原理图的AT89C51单片机中,然后仿真运行观察P1口所接8只发光二极管的亮灭变化是否符合要求。(4)按硬件电路焊好电路板,将参考程序写入AT89C51单片机中,通电,观察P1口所接8只发光二极管的亮灭变化是否符合要求。5思考题思考题(1)如果需要加快流水灯的流动速度,需要如何修改程序?(2)如何修改程序使流水灯的流动方向相反?(3)硬件电路如图2-15所示,两个灯同时点亮,两端往中间移动,再往两端移动,不断循环。即P1.0、P1.7亮P1.1、P1.6亮P1.2、P1.5亮P1.3、P1.4P1.2、P1.5P1.1、P1.6亮P1.0、P1.7亮。试编写程序实现该功能。(4)柱状灯的设计。硬件电路如图2-15所示,开始一个灯点亮,接着二个、三个到八个灯全亮,然后全灭,再重新开始,不断循环。试编写程序实现该功能。实训五:计数器的实训五:计数器的C51程序设计与制作程序设计与制作1实训目的实训目的 通过本次实训,(1)初步认识数码管及其动态显示原理;(2)初步认识计数器的结构与原理;(3)进一步掌握ISIS7 professional的基本操作方法;(4)进一步熟悉C51程序设计方法;(5)进一步熟悉调试C51源程序的方法与技巧。2知识要点知识要点 (1)硬件电路及工作原理硬件电路原理如图4-10所示。其中AT89C52为美国ATMEL公司生产的MCS-51单片机。电阻R1与电容C1组成上电自动复位电路。JT为石英晶体振荡器,它与电容C2和C3共同组成时钟振荡电路。数码管LED1(十位)、LED2(个位)为共阴极结构。数码管LED1、LED2采用动态扫描显示方式,P0口为段码输出端,P2.6、P2.7分别为LED1、LED2位选端。将T0 作为计数器来使用,对外部输入的方波信号计数,并将所计的数值实时显示在数码管上。当计满100时清0,再从头计起。(2)参考程序。C51参考程序如下:图4-10 计数器硬件电路原理图#include /52系列单片机的头文件#define uchar unsigned char /宏定义#define uint unsigned intsbit shiwei = P26; /声明十位sbit gewei = P27; /声明个位uchar code table = /定义共阴极数码管编码表0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71;void delayms (uint); /对延时函数进行说明void display (uchar shi,uchar ge) /定义显示函数 shiwei = 0; /选择十位LED P0 = tableshi; /送十位数据显示 delayms (5); /延时5毫秒 shiwei = 1; /关闭十位LED gewei =0; /选择个位LED P0 = tablege; /送个位数据显示 gewei =1; /关闭个位LED delayms (5);void delayms (uint xms) /定义延时函数 uint i,j; for (i = xms;i0;i-) for (j = 110;j0;j-);uint read () /定义读取计数器值函数uchar t1,th1,th2;uint val;while (1) th1 = TH0; t1 = TL0; th2 =TH0; if (th1 = th2) break; val = th1*256+t1; return (val);void main () /主函数 uchar a,b; uint num; TMOD = 0x05; /设置计数器0为工作方式1 TH0 = 0; /将计数器寄存器初值设为0 TL0 = 0; TR0 =1; /启动计数器0 while (1) num = read (); /读取计数器寄存器的值 if (num=100) /判断计数器寄存器的值是否到100num = 0; TH0 = 0; /将计数器寄存器初值清0 TL0 = 0; a = num/10; b = num%10; display (a,b); 说明:“uint read ( )”函数实现读取运行中计数器寄存器中的值,由于该寄存器的值会随时变化,若只读一次,当发生进位时,很有可能会读错数据,因此TH0寄存器的值需要读两次,以确保读取的时候没有发生进位。具体操作为,先读取TH0一次,再读取TL0一次,然后再读取TH0一次,如果两次读取TH0的值相同,说明TL0没有向TH0进位,数据可用。否则,就要重新读一次。3实训器材实训器材(1)DICE-5208K开发型单片机综合实验仪1套。(2)PC机1台。(3)DICE-3000仿真器1台。(4)带插针的导线若干。4实训内容及步骤实训内容及步骤(1)启动ISIS7 professional软件(具体使用方法见本教材附录五),并用其绘制图4-10所示电路原理图。(2)启动Keil C51 uvision4软件(具体使用方法见本教材附录四)。建立工程,输入上述C51参考源程序并编译调试生成二进制的目标文件。(3)将第(2)步生成的二进制目标文件加载到第(1)步所绘图4-10所示电路原理图的AT89C52单片机中,然后仿真运行观察数码管的显示情况是否符合要求。注意:实际调试中为模拟T0(P3.4)引脚的输入方波,可在该引脚接一个开关到地,然后单击开关来实现。也可接一台信号发生器。(4)按硬件电路焊好电路板,将参考程序写入AT89C52单片机中,通电,观察数码管的显示情况是否符合要求。5思考题思考题(1)简述数码管的动态显示原理及特点。(2)若数码管LED1、LED2为共阳极结构,应怎样修改源程序?(3)本实训有哪些注意事项?(4)如何用汇编语言编程实现该功能?小小 结结本章在前面学习汇编语言的基础上介绍了C51的基本内容和程序设计方法。主要介绍了以下内容。(1)C语言与汇编语言相比所具有的优点以及C51程序结构的特点。(2)C51的基本数据有位型、无符号字符型、有符号字符型、无符号整型、有符号整型、无符号长型、有符号长型、浮点型及双精度型等九种类型,它们的符号、长度、值域范围等各不相同。(3)C51允许将变量或常量定义成不同的存储类型,以使单片机访问不同的存储空间。C51编译器允许的存储类型主要包括片内直接寻址型、片内位寻址型、片内间接寻址型、片外分页寻址型、片外间接寻址型以及代码型等六种,它们和单片机的不同存储区相对应。(4)C51编译器提供SMALL、COMPACT和LARGE等三种存储模式供用户选择。(5)MCS-51单片机通过其特殊功能寄存器(SFR)实现对其主要资源的控制。C51允许通过使用关键字sfr、sbit或直接引用编译器提供的头文件来对SFR进行访问,但对于片外RAM或扩展I/O口的直接访问只能由用户实现。(6)C51的基本运算主要包括算术运算、关系运算、逻辑运算、位运算和赋值运算及其表达式等。(7)C51的构造数据类型主要有数组、结构和联合等。在单片机系统中,数组的应用最广泛。(8)与汇编语言程序结构一样,C51的程序结构也可以分为顺序结构、选择结构或分支结构、循环结构和子程序结构等四种基本类型。其中子程序就是C51中的函数,选择结构或分支结构由选择控制语句if或switch/case语句实现,而实现循环结构的语句有while、do-while、for以及if和goto语句等。(9)函数是C语言中的一种基本模块,实际上,一个C语言程序就是由若干个模块化的函数所构成的。从C语言程序的结构上划分,C语言函数可分为主函数main ()和普通函数两种。对于普通函数,从用户的角度来看又可分为标准库函数和用户自定义函数两种。对于用户自定义函数,应重点掌握其定义、调用以及参数传递等基本知识。(10)指针是C语言的精华。本章对C51中指针的概念、定义及引用作了祥细的介绍。(11)最后,通过在C51中加入汇编语言语句以及LED动态显示驱动程序设计等二个实例进一步介绍C51程序设计方法。只有认真研读这些实例,方可做到举一反三和触类旁通。充分重视C51与汇编语言的区别和联系是学习好C51的关键,因此读者在学习本章之前,应具有一定的汇编语言及C语言基础知识。习题与思考题习题与思考题一、填空题一、填空题1C51的基本数据有_、_、_、_、_、_、_、_、_等九种类型。2C51的选择控制语句主要有_语句和_语句,主要用于设计选择或分支结构程序。3C语言中用于终止函数的执行,并控制程序返回到调用该函数时所处位置的语句是_。 4C51编译器提供_、_和_等三种存储模式供用户选择。5实现循环结构的语句有_、_、_以及_语句等。6C语言中unsigned int型数据共占_个字节_个二进制位。二、选择题二、选择题1以下标识符中错误的是( )。 ABeep_Cnt BCnt2ms Cbeep_cnt Dwhile2以下标识符中正确的是( )。 Agoto B3y Cgoto_5 Dreturn 3若定义数组int a5=2,4,6,8;则元素a4的值是( )。A0 B2 C6 D84与“x*=a+b”等价的语是( )。Ax=x*a+b Bx=x*(a+b) Cx=x* b + a Dx=x+a*b5若有定义“int b,*p=&b;”则“ *& b”的含义是( )。A变量b的地址 B指针p的地址 C变量b的值 D指针p的内容与变量b相与的值6运行“int j=2,a;a=+j+3;”a、j的结果是( )。 Aa=5、j=2 Ba=5、j=3 Ca=6、j=3 Da=6、j=2三、综合题三、综合题1与汇编语言相比,C语言有哪些优点?试简述C51程序的结构特点。2C51对单片机的主要资源是如何定义的?3C51有哪些基本运算?试比较C51各种运算的优先级。4C51的构造数据类型有哪些?如何对数组元素进行赋值?指针类型有哪些?5C51的函数有哪些?如何定义与调用一个C51自定义函数?6试归纳总结C51中星号“*”的功能?7试编程求自然数1100中能被3整除的所有数的累加和。8利用continue语句把1020之间不能被5整除的数输出。9编写一个函数,将demo0 demo3存放的4个单字符转换成一个数。10MCS-51单片机的P1口接有8个按键,分别对应P2口连接的8只LED,要求P1口的任一按键按下,则LED从该位置开始形成走马灯输出显示,再按该键则停止。试编程实现此功能需求。
收藏 下载该资源
网站客服QQ:2055934822
金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号