2.2 本章相关的C语言知识精讲
C语言是目前单片机编程的主要语言(C语言在计算机编程中应用也非常广泛),它既具有汇编语言的操作硬件的能力,又兼有其他高级语言的优点。所以用C语言开发单片机的程序非常方便。
单片机C语言的主要特点是,①单片机C语言是由标准C语言拓展而来的,两者大部分是相同的,它们的语句、结构、顺序都是很相似的。和标准C语言的不同之处是,单片机的C语言运行于单片机平台,而标准C语言运行于计算机等桌面平台。单片机C语言和标准C语言相比,多了一些变量类型、中断等内容。②具有结构化的控制语句。程序的各个部分除了必要的信息交流外,彼此独立,层次清晰,便于使用和维护。③适用范围广,可移植性好。单片机C语言不是只适用某些特定的单片机,只要某种单片机具有相应的C编译器,就能使用该语言编程。目前主流单片机都具有C编译器。由于开发者在做不同的项目时往往需要采用不同的单片机以各尽其能,所以采用C语言编程,借助C语言的可移植性,只要熟悉了不同单片机的性能和特点,就能应用这些单片机的特点开发产品,这样就能提高开发效率。
2.2.1 函数简介
1.函数的基本类型
完成一个产品称为一个任务,一个任务又包含多个子任务,编程时,怎样完成某一个子任务或实现某一个特定功能?我们可以编写一个函数来实现。函数就是具有特定功能的代码段。
函数的基本结构如下:
这个结构的格式是固定的。根据需要,参数可以有,也可以没有。{}内是实现一定功能的语句以及调用其他子函数的语句。
函数的分类详见表2-1。
表2-1 函数的分类
2.函数的特点
(1)库函数和自定义函数
51单片机C语言(简称C51语言)支持库函数和自定义函数。编程时,只要包含了编译器内库函数相应的头文件后,就可以使用库函数了,这样可以简化代码、减轻工作量。使用自定义函数,则可以使代码结构化、模块化。
(2)主函数和子函数
在C语言中,对函数的个数没有限制。但是,有这么多函数,究竟从哪个函数开始执行呢?C语言中提供了一个特殊的函数,即main函数(主函数),主函数中可以调用其他子函数(除主函数之外的其他函数叫作子函数),子函数之间可以相互调用,但不能调用主函数。程序首先从主函数的第一个语句开始执行,然后再依次逐句执行。在执行过程中如果遇到调用子函数的语句,则跳转到相应的子函数去逐条执行子函数内部的语句,子函数执行完毕,再返回到原调用的位置继续向后执行。主函数内的语句是不断循环执行的(某些特定功能的语句除外)。
(3)函数的调用规律
1)在一个函数体的内部,不能再定义另一个函数,即不能嵌套定义。
2)函数可以自己调用自己,称为递归调用。
3)函数之间允许嵌套调用。
4)同一个函数可以被一个或多个函数任意调用。
(提示:读到这里,对函数有了大致的印象,当然可能还比较模糊。这就需要细读以下内容。)
2.2.2 数据类型
1.C语言的数据类型
数据是单片机(计算机)处理的对象,单片机(计算机)要处理的一切内容最终都将以数据的形式出现。所以,编程中涉及的数据有着很多种不同类型的含义。由于数据在单片机的存储器内都是以二进制的形式存储的,同一个数据的值,如果预先对它指定的数据类型发生了改变,则其存储时所占存储器的容量也会发生改变。C语言的数据类型如图2-4所示。
图2-4 C语言的数据类型
基本类型是C语言中的基础类型。构造类型就是使用基本类型的数据进行添加、设计而成的组合型数据类型。构造体的每一个组成部分称为构造体的成员。空类型专用于对函数返回值的限定和函数参数的限定。指针是C语言的精华,本章在其基本应用方面有详细介绍。
2.51单片机C语言的数据类型
51单片机C语言简称C51语言,它是适用于51单片机的编程语言,与标准C语言基本相同,但略有拓展(差异)。C51语言的数据类型有字符型、整型、实型、指针类型、数组类型等,详见表2-2。
表2-2 C51语言常用的数据类型
(续)
除了支持标准C语言外,C51语言还增加了以下数据类型:
1)位类型数据。使用一个二进制位来存储数据,其值只有0和1两种。其关键字是bit。
2)sfr型数据。51单片机内部有一些特殊功能的寄存器,为了定义、使用这些寄存器,C51增加了sfr、sfr16、sbit这三个关键字。sfr和sfr16是用来定义8位、16位特殊功能寄存器的。sbit用来定义特殊功能寄存器的位变量。
2.2.3 常量
在程序运行过程中,其值不能被改变的量称为常量。常量可分为数值型常量、字符型常量、符号常量三大类。
1.数值型常量
(1)整型常量
所有的整数都是整型常量,C语言的整型常量可以用十进制整数、十六进制整数、八进制整数这三种形式表示。
(2)实型常量
实型常量也称为浮点型常量,是由整数和小数部分组成的,两部分之间用十进制小数点隔开。它有两种表现方式。
1)十进制小数形式,如0.54。
2)指数形式,如4.5e3或者4.5E3都代表4.5×103。这是规范化的指数形式,即在字母e或E之前的小数部分中,小数点左边有且只有1位非零数字。
2.字符型常量
(1)字符常量
字符常量由单个字符组成,所有字符来自ASCII字符集(按照规定,各个字符都用一个对应的数值来表示,该值就是ASCII值,如字符常量a的ASCII值是十进制97,A的ASCII值是十进制65。详见附录B)。在程序中,通常用一对单引号将单个字符括起来表示一个字符常量,如'a'、'A'、'0'等。注意,每一个字符常量相当于一个整型数值(ASCII值),可以参加表达式的运算。字符常量区分大小写。如'a'和'A'是不一样的。这一对单引号是定界符,不属于字符常量的一部分。
查看ASCII字符集可以发现,有一些字符没有形状,如换行(ASCII值为10)、回车(ASCII值为13)等。有些字符虽有形状,却无法从键盘输入,如ASCII值大于127的一些字符。如果编程中要用到这些字符,则可以用C语言中的一些特殊形式进行输入,即用一个“\”开头的字符序列来表示字符。例如,用“\r”表示含义为回车的字符,用“\n”表示含义为换行的字符。这种用“\”开头的字符叫转义字符,常用的转义字符见附录B。
(2)字符串常量
字符串常量(简称字符串)是双引号括起来的若干字符组成的序列,存储时每个字符串末尾自动加一个'\0'作为字符串结束标志。例如,字符串“welcome”在内存中的存储形式如图2-5所示。
图2-5 字符串常量在内存中的存储形式示例
注意:在程序编写中,不必在字符串的结尾处加上“\0”这个结束标志。
3.符号常量
在C语言中,可以用一个符号名来代替一个固定的常量值,该符号名称为符号常量。使用符号名的好处是可以为编程和阅读带来方便。符号常量在使用之前必须先定义,定义的方法为
其中,#define是一条预处理命令(预处理命令都以"#"开头),称为宏定义命令(在后面的任务程序中有一些例程可供示范),其作用是用该符号名来表示其后的常量值。一经定义,以后在程序中所有出现该符号名的地方均用该常量的值来代替。
注意:
1)习惯上符号常量的符号名使用大写字母,下面介绍的变量的标识符使用小写字母,以示区别。
2)符号常量的值在其作用域内不能改变,也不能再被赋值。
3)使用符号常量的好处:一是可以做到含义清楚(可以用我们易记、易识别的字符);二是能做到“一改全改”,例如,如果程序中多次出现某符号常量,当我们需要修改符号常量的数值时,只需要在宏定义语句中修改符号常量的值,则程序中多次出现的该符号常量的值就全部改变了。
4)还可以通过宏定义,用符号名来表示一个代码段。这样在随后的编程中,只要写入该标识符,就等价于写入了那个代码段。这样可使程序简单、易读。
2.2.4 变量
1.变量概述
变量是在程序执行过程中其值可以发生改变的量。每一个变量都必须用一个标识符作为它的变量名。在使用一个变量之前,必须首先对该变量进行定义,指出它的数据类型和存储模式,以便编译系统为它分配相应的存储单元。C语言中变量的类型有整型变量、实型变量和字符型变量。
2.声明和定义
声明就是说明当前变量或函数的名字和类型,但不给出其中的内容,即先告诉你有一个什么类型的变量或函数,但是这个变量或函数的具体信息却是不知道的。定义就是写出变量或函数的具体内容(具体功能)。
对于变量,一般情况都是直接进行定义的,不需声明。对于函数,如果把函数的具体内容(定义)写在调用它的函数(主函数和其他子函数都可调用它)后面,则需要在调用它的函数之前进行声明。一般在程序的开头部分进行声明;如果把函数的具体内容写在调用它的函数之前,则直接进行定义,不需声明。
3.局部变量与全局变量
在程序中变量既可以定义在函数内部,也可以定义在所有函数的外部。根据定义变量的语句所处的位置,可将变量分为局部变量和全局变量。
(1)局部变量
在函数内部定义的变量叫作局部变量,它只在本函数范围内有效,只有在调用该函数时才给该变量分配内存单元,调用完毕,则将内存单元收回。
注意:
1)主函数中定义的变量只在主函数中有效,在主函数调用的子函数中无效。
2)不同的子函数中可以使用相同名字的变量,但它们代表的对象不同,作用范围也不同,互不干扰。
3)函数的形式参数也是局部变量,只能在该函数中使用。
4)在{}内的复合语句中可以定义变量,但这些变量只能在本复合语句中使用。
(2)全局变量
一个C程序文件里有若干个函数。在所有函数之外定义的变量称为全局变量。全局变量在该C程序文件内可供所有的函数使用。
注意:
1)一个函数既可以使用本函数中定义的局部变量,又可以使用函数之外定义的全局变量。
2)如果不是十分必要,应尽量少用全局变量,这是因为:第一,全局变量在程序执行的全部过程中一直占用存储单元,而不是像局部变量那样仅在需要时才占用存储单元;第二,全局变量会降低函数的通用性,而我们在编写函数时,都希望函数具有很好的可移植性,以便其他C程序文件可以方便地使用;第三,使用全局变量过多,整个程序的清晰性将变差,因为在调试程序时如果一个全局变量的值与设想的不同,则不能很快地判断是哪个函数出了问题。
3)在同一个C程序文件中,如果全局变量与局部变量同名,则在局部变量的作用范围内,全局变量会被屏蔽。
4.变量定义格式
对变量进行定义的格式如下:
其中,“存储种类”和“存储器类型”是可选项,意思是根据编程者的需要,既可以加上,也可以省略。
(1)变量的存储种类
变量的存储种类有四种:自动(auto)、静态(static)、外部(extern)、寄存器(regis-ter)。其概念解释见表2-3。
表2-3 变量的存储种类
在定义一个变量时如果省略“存储种类”选项,则该变量将为自动(auto)变量。
(2)变量的数据类型
根据需要,在定义变量时,可将变量的数据类型定义成位型、字符型、整型和浮点型等(其取值范围在表2-2已详述)。
(3)变量的存储器类型
第1章介绍了单片机的存储器。C51编译器允许说明变量存储在单片机内部的什么类型的存储器内。C51编译器完全支持8051系列单片机的硬件结构,可以访问其硬件系统的所有部分;对每个变量可以准确地赋予其存储器类型,从而可使变量在单片机系统内被准确地定位。
若使用code定义变量(以及数组等),则其存储器类型为程序存储器(ROM),数据就存储在程序存储器内。
为了充分表达单片机内部数据存储器的3个不同部分,C51编译器引入了3个新的关键字:date,idate,bdate。
用date定义变量,则存取内部数据存储器的前128字节。用idate定义变量,则存取内部数据存储器全部的256字节。
如果定义的变量与位操作有关,就要使用bdate来定义,这样数据在内部数据存储器的位寻址区进行存取。
定义变量时,如果存储器类型省略,那么编译器系统默认将变量的存储器类型定义为“date”型。
例如,对变量x、y这样定义:
该语句定义了无符号整形变量x和y,省略了存储种类和存储器类型,这两个变量被默认为自动变量、在内部数据存储器的前128字节进行存取。
定义变量的注意事项如下:
1)定义变量时,只要值域(数值范围)够用,就应尽量定义、使用位数较小的数据类型,如char型、bit型,这是因为较小的数据类型占用的内存单元较小。例如,假设x的值是1,当将x定义为unsigned int型时,就会占用2字节的存储空间(存储的是00000000 00000001);若定义为unsigned char型,则只占用1字节的存储空间(存储的是00000001);若定义为bit型,则只占用1位的存储空间。
2)51系列单片机是8位机,对于8位机,进行8位数据运算要比16位及更多位数据运算快得多,因此要尽量使用char或unsigned char型。
3)如果满足需要,应尽量使用unsigned(即无符号)的数据类型,因为单片机处理有符号的数据时,要对符号进行判断和处理,运算速度会变慢一些。由于单片机的速度比不上计算机,单片机又工作在实时状态,所以任何可以提高效率的措施都应重视。
(4)变量名
变量名可以采用任意合法的标识符。
2.2.5 标识符和关键字
1.标识符
在编程时,标识符用来表示自定义对象名称,所谓自定义对象就是常量、变量、数组、函数、语句标号等。使用标识符必须注意以下事项:
1)标识符必须以英文字母或下画线开头,后面可使用若干英文字母、下画线或数字的组合,但长度一般不超过32个,不能使用系统关键字,如area、PI、a_array、s123、abc、P101p都是合法的标识符,而456P(以数字开头)、code-y(code为关键字)、a&b(&为关键字)都是非法的。
2)标识符是区分大小写的,如A1和a1表示两个不同的标识符。
3)为了便于阅读,标识符应尽量简单,而且能清楚地看出其含义。一般可使用英文单词的简写、汉语拼音或汉语拼音的简写。
2.关键字
关键字是C51编译器保留的一些特殊标识符,具有特定的定义和用法。
C51语言继承了标准C语言定义的32个关键字,同时又结合自身的特点扩展了一些,如char、P0、P1、unsigned、bit等,详见附录A。
2.2.6 单片机C语言程序的基本结构
单片机C语言程序有清晰的结构和条理,一般包含6个部分,见表2-4。
表2-4 单片机C语言的基本结构
提醒:表2-4与2.3节具体程序代码结合起来阅读,可较容易理解单片机C语言程序的基本结构。
2.2.7 算术运算符和算术表达式
C语言的运算符范围很宽,除了控制语句和输入、输出语句外,大多数基本操作均由运算符处理。运算符较多,其中算术运算符详见表2-5。
表2-5 C语言的算术运算符和算术表达式
用算术运算符和括号将运算对象(包括常量、变量、函数等)连接起来,符合C语言语法规则的式子叫作算术表达式,如a-(b∗c)。
算术运算符的优先级是,乘除的优先级相同,加减的优先级也相同,但乘除高于加减,优先级高的先执行,因此要先乘除后加减。
算术运算的结合性是自左向右。
2.2.8 关系运算符和关系表达式
1.关系运算符
C语言一共提供了6种关系运算符,详见表2-6。
表2-6 C语言的关系运算符
2.关系表达式
用关系运算符将两个运算对象连接起来形成的式子叫作关系表达式,如a+b>b+c,a==b<c。
注意:关系表达式如果成立,则该表达式的值为1;如果不成立,则该表达式的值为0。例如,对表达式“a=c>b”的理解是,当c的值大于b的值时,关系表达式“c>b”的值为1,该值赋给a,因此a的值为1,否则若c的值小于b,则“c>b”的值为0,因此a的值为0。
2.2.9 逻辑运算符和逻辑表达式
逻辑运算符用于操作数之间的逻辑运算,操作数可以为各个数据类型的变量或者表达式。逻辑运算符有逻辑与、逻辑或、逻辑非3种,用逻辑运算符连接起来的式子就是逻辑表达式。逻辑运算符的运算功能详见表2-7。
表2-7 逻辑运算符的运算功能
逻辑运算法则说明如下:
1)逻辑与:A、B两者同时为真(即值为1),则逻辑表达式A&&B为真(值为1),否则A&&B为假(值为0)。“逻辑与”相当于“并且”的意思。
例如,对于表达式y=(a>3)&&(b<5),只有当a>3成立(表达式的值为1)并且b<5也成立(表达式的值为1),表达式(a>3)&&(b<5)的值才为1,y的值也就才为1。
2)逻辑或:A、B中只要有一个为真,则A‖B为真(值为1),否则A‖B为假(值为0)。“逻辑或”相当于“或者”的意思。
例如,对于表达式y=(a>3)‖(b<5),当a>3成立(表达式的值为1)或者b<5成立(表达式的值为1)时,表达式(a>3)‖(b<5)的值就是1,y的值也就为1。
3)逻辑非:若A为真,则!A为假;若A为假,则!A为真。“逻辑非”相当于“值取反”的意思。例如,对于位变量x,y,表达式y=!x中的x为1时,y的值就为0,反之,当x为0时,y就为1。
2.2.10 位操作运算符及其表达式
位操作运算符是两个操作数中的二进制位(bit)进行的运算。C语言的位操作运算符详见表2-8。
表2-8 C语言的位操作运算符
(续)
2.2.11 赋值运算符和复合赋值运算符
基本的赋值运算符是“=”,作用是将一个数据赋给一个变量,含有“=”的式子叫赋值表达式。例如“a=8;”就是赋值表达式,其作用是将常数8赋给变量a。
另外,二目运算符可以与“=”组成复合赋值运算符。C语言提供了十种复合运算符,即+=,-=,∗=,/=,%=,<<=,>>=,&=,|=,^=。
其作用是可以提高程序的执行效率,也可以简化书写。复合赋值运算符的含义如下:
a+=b;相当于a=a+b;
a-=b;相当于a=a-b;
……
2.2.12 单片机的周期
学习单片机,需要掌握时钟周期、机器周期和指令周期三个概念,详见表2-9。
表2-9 单片机的时钟周期、机器周期和指令周期
2.2.13 while循环语句和for循环语句
1.while循环语句
while循环语句的基本形式是
while循环语句的执行过程是,判断()内的条件表达式是否成立,若不成立(即表达式的值为0),则{}内的语句不会被执行,直接跳到执行{}后的语句;若表达式成立(即表达式的值为1),则按顺序执行{}内的各条程序语句。执行完毕后再返回,判断()内的条件表达式是否成立,若仍然成立,则继续按顺序执行{}内的语句;若不成立,则执行{}后的语句,如图2-6所示。
图2-6 while循环语句执行的流程图
【应用示例】用while循环语句写一个简单的延时语句。
while循环语句的执行过程是,首先判断i>0是否成立,只要是成立的,就执行i=i-1;直到i减小到0时,i>0不成立,则跳出循环,这样起到了延时作用(延时的时间长度是i从10000减小到0所用的时间)。
注意:给变量赋的值必须在变量类型的取值范围内,否则数值会出错。例如第02行,若写成i=70000,则会出错,因为i是unsigned int型变量,其取值范围是0~65535。给它赋值为70000,超出了取值范围。
while循环语句()中的条件表达式可以是一个常数(如1)、一个运算式或一个带返回值的函数。
2.for循环语句
for循环语句的一般结构是
其执行过程如下:
第1步,给变量赋初值。
第2步,判断条件表达式是否成立。若条件表达式不成立,则{}内的语句不被执行,直接跳出for循环,执行{}后的语句;若条件表达式成立,则按顺序执行{}内的程序语句。执行完毕后,返回到for后面的()内执行一次循环变量的增或减,然后再判断条件表达式是否成立,若不成立,则跳出for循环语句而执行{}后的语句;若成立,则执行{}内的语句。这样不断地循环,直到跳出循环为止,如图2-7所示。
图2-7 for循环语句的执行流程图
注意:for循环语句的{}内的语句可以为空,这时{}就可以不写,即for循环语句可写成:for(给循环变量赋初值;条件表达式;循环变量增或减);(分号不能去掉)
例如,用for循环语句写延时函数,如下:
执行过程是,先给i赋初值,再判断i>0是否成立,若不成立,则跳出for循环;若成立,由于后面没有{}的内容,所以省掉了执行{}内语句的过程。接着再执行i--,再判断i>0是否成立……直到i=0时(要执行i自减3000次),i>0才不会成立,才会跳出for循环,这样就起到了延时作用。
2.2.14 不带参数和带参数函数的声明、定义和调用
1.不带参数函数的声明、定义和调用
如果在编程中多次用到某些语句且语句的内容完全相同,则可以把这些语句写成一个不带参数的子函数,当在其他函数中需要用到这些语句时,直接调用这个子函数就可以了。例如,1s的延时子函数的定义示例如下:
执行过程:首先执行第03行。开始x=1000,x>0为真,因此执行第04行,即y由110逐步减1,直到减小到0,所耗时间约为1ms(对于STC89C52单片机),第04行执行完毕后,再执行x--,x的值变为999,再判断x>0是否为真,结果为真,因此又执行第05行(耗时约1ms),然后又执行一次x--……这样循环。每执行一次x减1,y就要从110逐步减1,直到减小到0,x共要自减1000次,第05行也要执行1000遍,耗时约为1s。
注意:子函数可以定义在主函数的前面或后面,但不能写在主函数里面。如果定义在主函数后面,必须在主函数的前面进行声明。
声明的格式是,返回值特性 函数名();
若函数无参数,则()内为空,如void delay();
【应用示例】用调用延时子函数的方法,写出一个程序,使图2-1中的发光二极管VL0间隔600ms亮、灭闪烁。
图2-1所示的实训板上,用单片机P0.0端口驱动VL0。
注意:如果不加上第07行、第08行和第13行这个while(1)语句,有可能出现程序跑飞现象。这是因为我们编写的程序烧入单片机后,一般没有将存储器存满。上电后单片机程序指针PC就会从程序存储器的0地址开始执行,中间会按照程序的要求跳到需要的地址执行相应的语句,如果执行完毕最后一条指令而没有相应的跳转指令,PC会继续往存储器的下一地址执行,而下一地址是没有烧写指令进去的(理论上是全1或全0,随厂家而定),这时就会出现跑飞现象,在PC将所有地址都跑一遍之后会回到0地址,如此循环。
2.带参数函数的声明、定义和调用
如果在一个程序里需要不同的延时时间,则需要写多个不同的延时函数,用上述不带参数的子函数就不方便了。这时宜采用带参数的子函数。该函数的定义如下:
同样,当带参数函数出现在调用它的函数之后时,需要在程序的起始处声明。其格式为
如语句:
声明时形参列表,也就是()内的内容也可以不写,但在定义时必须写。
【应用示例】详见2.3节。
2.2.15 良好的编程规范
良好的编程规范有利于开发人员理清思路、整理代码,同时也便于他人阅读、交流。在进行编程时,总的原则是要做到格式清晰、注释简明扼要、命名规范易懂、函数模块化、程序易读易维护、功能准确实现、代码空间效率和时间效率高、适度的可扩展性等。
初学者要遵守的基本编程规范见表2-10。
表2-10 初学者要遵守的基本编程规范
注:表中内容是为了初学者养成良好的习惯,而不是语法。更为详尽的编程规范详见本书《资料》。