迷人的8051单片机
上QQ阅读APP看书,第一时间看更新

第3章 入门C语言

单片机是一种可编程的器件,我们需要将程序预先编写好,并保存到单片机的存储器中,单片机才能按照预先的设定执行程序。在给单片机开发应用程序时,使用C语言编写代码已经是一种趋势,C语言博大精深,学精不易,但入门却十分简单,本章将带领你用最便捷的方式快速学习C语言,并且在短时间内学会编写C应用程序。

3.1 数据和运算

3.1.1 C语言的由来

语言是编写程序时人与单片机之间的交流方式,最初人们使用机器码(0与1组合)来给单片机编写程序,后来开始使用汇编语言来编写程序,汇编语言和单片机的硬件结合性好,代码简洁高效,使用汇编语言开发的程序在单片机存储空间有限的环境里能大显身手。随着近年来FLASH存储器技术被大量地应用,单片机的存储空间已经不再是瓶颈,使用汇编语言开发程序的优势已经不复存在,而C语言因其具有描述能力强、可移植性好、逻辑缜密、模块化结构等诸多优点,非常适合大型程序的开发,近年来在嵌入式系统的程序开发中被越来越广泛地应用。

C语言有着悠久的历史和众多用户群。1970年,美国贝尔实验室的Ken Thompson,以BCPL语言为基础,设计出既简单又接近硬件的B语言,并且用B语言编写了第一个UNIX操作系统。1972年,美国贝尔实验室的D.M.Ritchie在B语言的基础上设计出一种新的语言,并且以BCPL语言的第二个字母作为这种语言的名字,即C语言。1977年,D.M.Ritchie发表了不依赖于具体机器系统的C语言编译文本——《可移植的C语言编译程序》,使C语言程序可以使用在任意架构的处理器上,只要该处理器具有对应的C语言编译器和库,然后将C源代码编译、链接成目标二进制文件之后即可在目标处理器上运行。

1982年,很多有识之士和美国国家标准学会(ANSI)为了使C语言健康地发展下去,成立了C标准委员会,建立了C语言的标准。1989年,ANSI发布了第一个完整的C语言标准ANSI X3.159#1989,简称“C89”,也就是我们经常说的“ANSI C”, C语言从此步入了规范化的道路。

3.1.2 数的进制

我们在日常生活中大多使用十进制,即逢十进一,这主要是由人们的使用习惯决定的。其实在生活中还有许多不同的进位制度,如时间的表示方法是六十进制,即一小时等于六十分钟,一分钟等于六十秒等,还有常用的表示数量的单位“一打”是十二进制等。在计算机中,常用的进位制度有二进制、十进制、八进制和十六进制。

1.二进制(Binary)

二进制数由0和1两个符号来表示,基数为2,按逢2进1、借1算2的规则计数。例如:

1011001110101111000010101101

2.十进制(Decimal)

十进制数由0、1、2、3、4、5、6、7、8、9十个数字符号表示,基数为10,按逢10进1、借1算10的规则计数。例如:

128 23 47

3.八进制(Octal)

八进制数由0、1、2、3、4、5、6、7八个数字符号表示,基数为8,按逢8进1、借1算8的规则计数。例如:

70360 777

4.十六进制(Hexadecimal)

十六进制数由0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F十六个数字符号表示,基数为16,按逢16进1、借1算16的规则计数。在C语言中表示十六进制数时,大小写字母的含义相同。例如:

EF08 5ac 7BF

对于不同进制的数字间的转换在这里不做太多的叙述,对于初学者来讲,使用PC中的计算器来进行不同进制数字间的转换是一个方便快捷的办法,计算器的使用如图3-1所示。打开计算器软件,如要将十进制数“254”转换成二进制数,可以首先在计算器中选择十进制,输入数字“254”,再用鼠标单击选择二进制,这时计算器中即可显示经转换后的二进制数“11111110”。

图3-1 使用计算器进行进制转换

3.1.3 码制

在计算机内部,所有的信息都要使用二进制的方法来表示,因为二进制的0和1两个数字恰好与存储单元的“有”和“无”相对应。不仅如此,数的符号“+”或“-”也需要用二进制数来表示,在通常情况下,用0表示正数的符号“+”,用1表示负数的符号“-”。当数的符号和数值表示方法使用二进制时,这样的数被称为“机器码”。机器码有不同的码制,对应不同的表示方法,常用的码制有原码、反码和补码三种。

1)原码:原码用最高位表示数的符号位,数值部分用二进制的绝对值表示。

2)反码:正数的反码与其原码相同,负数的反码是将符号位除外,其他各位按位取反。

3)补码:正数的补码与其原码相同,负数的补码是其反码加1。

数的原码、反码和补码的表示方法详见表3-1。

表3-1 数的原码、反码和补码

3.1.4 数据类型

程序运行的目的是对数据进行处理,在C语言中数据是有类型区分的,具体分类如下:

在以上的数据类型分类中,基本类型是不可以再次拆分为其他数据类型的;构造类型则是在基本类型的基础上,按照一定方式组合而成的数据类型;指针类型是一种特殊的数据类型,其值通常用来表示某一个量在内存中的存放地址;空类型是指在对函数进行定义时,若函数没有返回值,我们会在函数的名称前面加上“void”,以此表明该函数是“空类型”。

3.1.5 常量

常量是指在程序的运行过程中,其值不能被改变的量。常量的种类有整型、实型、字符型和字符串常量四种。

1)整型常量:十进制的整数表示方法非常简单,如29、-18、156等。十六进制的整数通常以0x(或0X)开头,如0xFE、0xD7A9、0x7D等。八进制的整数则以0开头,如057,其值相当于十进制的47。

2)实型常量:实数有两种表示方法,一种是十进制的小数形式,如0.625、-16.5等;另一种是采用指数形式,即用e(或E)后面跟一个整数,表示以10为底的幂指数,如256.5的表示方法是2.565e2。

3)字符型常量:字符型常量的表示方法是用单引号引出,如‘a'、‘B’等。

4)字符串常量:字符串常量用双引号引出,如“GOOD”“thank you”等。

3.1.6 变量

变量是指在程序运行过程中其值可以改变的量。在C语言中使用变量时,要先给变量命名,还要给变量定义数据类型,有时还须指定变量的存储地点。

为什么要给变量定义数据类型呢?在数学上,一个数可以是+∞也可以是-∞,但是在计算机中,存储单元是有限的,因此必须根据数据的大小为其分配合适的存储空间。定义数据类型实际上就是为变量在内存中分配特定的存储空间,以便于用这个空间来存储相关的数据。如果将变量比喻成用于存储数据的盒子,指定数据类型就是指定盒子的大小,既要装下要装的东西,又不会造成空间的浪费。C语言中变量的数据类型详见表3-2。

表3-2 C语言中的基本数据类型

变量在程序中需要先定义后使用。定义变量的方法是先给变量指定名称和数据类型,这样编译器才能为变量分配相应的存储空间。定义变量的方式如下:

        数据类型 变量名表;(多个变量名称之间要用逗号分隔)

在C程序中定义变量的方法可以参考如下语句:

        unsigned char a, b, c;       //定义a, b, c三个无符号字符型变量
        unsigned int num;            //定义num无符号整型变量

3.1.7 运算符

C语言的运算符非常丰富,在程序中使用这些运算符来处理各种基础操作,从而完成特定的功能。C语言的运算符主要有以下几种:

1.算术运算符

❑ + :加法运算符,或为取正值运算符。例如,3+5、A+B、+23。

❑ - :减法运算符,或为取负值运算符。例如,18-17、TIME1-TIME2、-78。

❑ * :乘法运算符。例如,5*8、AD*AF。

❑ / :除法运算符。在这里除法运算符和一般的算术运算规则有所不同,如果是两个浮点数相除,结果也是浮点数。如果两个整数相除,结果也是整数。例如,10.0/20.0结果为0.5,7/2结果为3,而不是3.5。

❑ % :求余运算符。%两侧均应是整数。例如,10%3结果为1。

在上述的运算符中,我们同样可以用“()”来改变运算的优先级,这同我们在小学时学的是一样的,如(A+B)* C就需要先计算A与B的和,再计算与C的积。

2.赋值运算符

=:赋值运算符。在C语言中用于给变量赋值,其方法可以参考以下语句:

        num=25;     //给变量num赋值25
        D=C;        //将变量C的值赋给变量D

3.自增、自减运算符

❑ ++ :自增运算符。作用是使变量的值自增1。例如,I++,表示让变量I的值自增1。

❑ - - :自减运算符。作用是使变量的值自减1。例如,A--,表示让变量A的值自减1。

4.关系运算符

关系运算符通常是用来判别两个变量是否符合某个条件的,所以使用关系运算符的运算结果只有“真”或“假”,即“1”或“0”两种。

❑ > :大于。例如,A>B。

❑ < :小于。例如,NUM1<NUM2。

❑ >=:大于等于。例如,U>=5。

❑ <=:小于等于。例如,P<=7。

❑==:等于。例如,TEAM1==TEAM2,在这里要区别于赋值运算符“=”,它表示的意思不是将TEAM2的值赋给TEAM1,而是用来判定TEAM1是不是同TEAM2的值相等。

❑!=:不等于。例如,A!=B。

5.位运算符

位运算是C语言的一大特色。所谓位运算形象地说就是指将数值以二进制位的方式进行相关的运算,参与位运算的数必须是整型或字符型的数据,实型(浮点型)的数不能参与位运算。

❑ & :按位“与”运算符。它是实现“必须都有,否则就没有”的运算。它的规则如下。

0 & 0=0, 0 & 1=0, 1 & 0=0, 1 & 1=1

在实际应用中,按位“与”运算常用来对某些位清零或保留某些位。

例如,A的值为: A=10010010

只想保留A的高四位,则用: A & 11110000

“与”运算后A的值为: 10010000

❑|:按位“或”运算符。它是实现“只要其中之一有,就有”的运算。它的规则如下。

0|0=0, 1|0=1, 0|1=1, 1|1=1

在实际应用中,“或”运算常用来将一个数值的某些位定值为“1”。

例如,A的值为:

A=10010010

想将A的低四位定值为1,则用: A|00001111

“或”运算后A的值为: 10011111

❑^:按位“异或”运算符。它是实现“两个不同就有,相同就没有”的运算。它的规则如下。

0^0=0, 1^0=1, 0^1=1, 1^1=0

在实际应用中,“异或”运算常用来使数值的特定位翻转。

例如,A的值为: A=10011010

想将A的低四位翻转,即0变1,1变0,则用: A^00001111

“异或”运算后A的值为: 10010101

❑ ~ :按位“取反”运算符。它是实现“是非颠倒”的运算。它的运算规则如下。

~0=1, ~1=0

例如,A的值为: 10011010

按位“取反”运算后,其值为: 01100101

❑ << :“左移”运算符。它是实现将一个二进制数的每一位都左移若干位的运算。“左移”运算的方法如图3-2所示。

图3-2 左移运算

❑ >> :“右移”运算符。它是实现将一个二进制数的每一位都右移若干位的运算。“右移”运算的方法如图3-3所示。

图3-3 右移运算

3.1.8 复合赋值运算符

在赋值运算符“=”之前加上其他双目运算符,就可以构成复合赋值运算符。复合赋值运算符有+=、-=、*=、/=、%=、<<=、>>=、&=、^=和 |=。

构成复合赋值表达式的方式为:

        变量 双目运算符=表达式

它相当于:

        变量=变量 运算符 表达式

例如:

num+=15相当于:num=num+15

a*=b+23相当于:a=a*(b+23)

对于初学者来说,复合赋值运算符的这种书写方法也许不太习惯,但它有利于编译器的编译和处理,可以产生高质量的目标代码。

3.2 语句

C语言用语句来向计算机发出操作指令。一个C语句经编译后,可以生成若干条机器指令,它是构成函数的基础。C语言的语句可以分为控制语句、函数调用语句、复合语句、表达式语句以及空语句等多种。以下我们主要介绍的是C语言的控制语句,这种语句具有相对固定的格式,用来实现某种特定的功能。

3.2.1 控制语句

C语言有9种控制语句,可分成以下3类:

1)循环执行语句: while语句、do…while语句、for语句。

2)条件判断语句:if语句、switch语句。

3)转向语句:break语句、continue语句、return语句、goto语句。

1.while语句

while语句是一个循环语句,用来控制程序段(即循环体)的重复执行,构成“当型”循环结构。while语句的常用形式为:

        while(表达式)
        {
            语句1;
            语句2;
            …
            语句n;
        }

while语句的执行过程是:首先判断“表达式”的值,当“表达式”的值为非0时,即开始顺序执行一次while语句内循环体中的语句,之后再次判断“表达式”的值,再进行下一次的循环,直至“表达式”的值为0时,结束while循环。

while语句的特点是先判断表达式,后执行语句。在循环体中应该有使循环趋于结束的语句,否则循环会永不停止,形成死循环。while语句的用法如以下代码所示:

        while(i--)
        {
            num=num+i;     //循环体语句1
            …             //循环体语句n
        }

2.do…while语句

do…while语句的特点是先执行循环体一次,再判断表达式的值,当“表达式”的值为非0时,则执行一次循环体中的语句,之后再判断“表达式”的值,并进行下一次的循环,直至“表达式”的值为“0”时结束。do…while语句的常用形式为:

        do
        {
            语句1;
            语句2;
            …
            语句n;
        }
        while(表达式);
        do…while语句的具体用法如以下代码所示:
        do
        {
            num=num+i;      //循环体语句1
                …          //循环体语句n
        }
        While(i--);

3.for语句

for语句是C语言中功能强大的循环语句,它的优点在于不仅适用于循环次数已经确定的情况,而且也可以用于未给出循环结束条件的情况。for语句的典型形式为:

        for(表达式1;表达式2;表达式3)
        {
            语句序列;
        }

for语句的运行过程如下:

1)首先求解“表达式1”。“表达式1”一般用作于给循环初始变量赋值。

2)求解“表达式2”。如果其值为非0,就执行一次for语句中指定的循环语句;如果其值为0,则退出for循环。

3)求解“表达式3”。“ 表达式3”一般用于改变控制循环次数的量,使循环趋于结束。

4)返回第2步,执行下一次循环。

for语句的执行过程如图3-4所示。

图3-4 for语句的执行过程

for语句的用法可以参考以下代码:

        void delay(unsigned int t)         //定义名为delay的子函数
        {
            unsigned int x;                  //定义变量x
            for(x=t; x>0; x--)            //for循环
            {
            … 语句序列
            }
        }

上面的代码是由for循环语句构成的延时函数,当for循环执行后,先执行的是“x=t”,将参数t的值赋予变量x,然后判断“x>0”是否为真,为真,就执行花括号内的语句序列。执行完语句序列后,转而执行一次“x--”,让x的值自减1。完成上述任务后,重新判断“x>0”是否为真,从而开始下一次循环。

4.if语句

if语句是一个条件语句,表达的意思是“如果……就……否则……”。if语句的典型写法是:

        if(表达式)
        {
            语句序列1;
        }
        else
        {
            语句序列2;
        }

if语句在执行时,首先对条件表达式进行求解,当条件表达式的结果为真时,就执行“语句序列1”的内容,否则就执行“语句序列2”的内容。需要注意的是,不要误认为if语句和else语句是两个部分,其实它们同属于一个if语句。else子句不能单独使用,它必须同if语句一起使用,但实际使用时可以省略else及后面的语句,这时if语句就可以简单地写成:

        if(表达式)
        {
            语句序列;
        }

这种形式的if语句执行过程是先求解表达式,当其为真时,就执行“语句序列”,当其为假时,就跳过该if语句,执行后面的其他语句。在if语句中,表达式通常都是用来判断两者关系的,表达式中使用的都是关系运算符,如>、< 、>=、==、<=和!=,其运算结果只有“真”和“假”两种状态。

5.switch语句

C语言中提供了一个专门用于处理多分支结构的条件选择语句,称为switch语句,又称为开关语句。其语句的一般形式为:

        switch(表达式)
        {
            case常量表达式1: 语句1; break;
            case常量表达式2: 语句2; break;
            case常量表达式3: 语句3; break;
            …
            case常量表达式n: 语句n; break;
            default : 语句n+1;
        }

switch语句的执行过程是:首先计算switch后面圆括号中表达式的值,然后用此值依次与各个case后面的常量表达式相比较,若与某个常量表达式的值相等,就执行该case后面的语句,执行语句时遇到break后就退出switch语句;若圆括号中表达式的值与所有case后面的常量表达式都不相等,则执行default后面的语句n+1,然后退出switch语句。

使用switch语句,还应注意以下几点:

1)default语句总是放在最后,当要求在没有符合的条件下不做任何处理时,则可以去掉default语句。这时,若圆括号中表达式的值与所有case后面的常量表达式的值都不相等,则直接退出switch语句。

2)如果在每一个case后面包含多条执行语句时,语句之间用“; ”号隔开;进入某个case后,会自动顺序执行本case后面的所有执行语句,直到遇到break语句,才停止执行。

3)每一个case后面的break语句是可以省略的,如果break语句被省略,程序会自动进入下一个case中继续执行语句,而不判断是否匹配,直到遇到break语句,才停止执行。这是因为case后面的常量表达式实际上只是一个开始执行处的入口标号,而不起条件判断作用。

switch语句的实际用法可以参考以下代码:

        switch(keyv2)
        {
            case 0xee:WELA=1; P0=0x00; WELA=0;
                      DULA=1; P0=table[0]; DULA=0;
                      break;
            case 0xde:WELA=1; P0=0x00; WELA=0;
                      DULA=1; P0=table[1]; DULA=0;
                      break;
            case 0xbe:WELA=1; P0=0x00; WELA=0;
                      DULA=1; P0=table[2]; DULA=0;
                      break;
            case 0x7e:WELA=1; P0=0x00; WELA=0;
                      DULA=1; P0=table[3]; DULA=0;
                      break;
        }

6.break语句

break语句可以用在循环语句和switch语句中,在循环语句中用来结束内部循环,在switch语句中用来跳出switch语句,break语句不能用在循环语句和switch语句之外的其他语句中。break语句的一般形式为:

        break;

break语句的用法可参考以下代码:

        for(x=10; x>0; x--)        //定义10次的for循环
        {
            …
        if(num<0)break;            //如果num的值小于0,终止for循环
        …
        }

7.continue语句

continue语句的作用是结束本次循环,忽略continue语句后面的语句,重新开始下一次的循环判定。其一般形式为:

        continue;

continue语句的用法可参考以下代码:

        *for(x=10; x>0; x--)
        {
        …
        if(num<0)continue;    //如果num的值小于0,重新开始下一次for循环
        …
        }

这里需要注意的是,break语句是不再判断循环的条件是否成立,结束整个循环结构,跳出循环体,开始执行循环语句后面的语句;continue语句只结束本次循环,转向下一次循环条件的判断,如果判断结果为真,则继续下一次循环,判断结果为假,则结束循环。

8.return语句

return语句用于将函数的值返回给主调函数。所谓函数的值,是指函数被调用后,执行函数体中的程序段所取得的并需要返回给主调函数的值。return语句的一般形式为:

        return表达式;

或者为:

        return(表达式);

该语句的功能是计算表达式的值,并返回给主调函数。return语句的实际用法可以参考以下代码:

        unsigned char ReadOneChar(void)
        {
            unsigned char i=0;
            unsigned char dat=0;
            for(i=8; i>0; i--)
            {
                …
            }
            return(dat); //将读出的数据返回
        }

9.goto语句

goto语句是一个无条件分支语句,用于将程序转移到指定的位置继续执行。goto语句的一般形式为:

        goto  语句标号;

goto语句的具体用法可以参考以下代码:

        while(1)
        {
        …
        restart:                                              //词句标号,指定程序跳转地点
        …
        if((temp<200)||(temp>800)) goto restart;      //条件成立,返回restart处
        …
        }

这里需要注意的是,过多地使用goto语句会造成程序结构上的混乱。

3.2.2 其他语句

1.函数调用语句

函数调用语句由一个函数调用加一个分号构成,具体可参考以下代码:

        delay(5);   //调用延时函数

2.复合语句

将多个语句组合起来,用 “{}”括起来,即可构成复合语句,其用法可参考以下代码:

        {
            D1=0;
            D2=1;
            delay_ms(500);
            D1=1;
            D2=0;
            delay_ms(500);
        }

3.表达式语句

将一个表达式和一个分号组合在一起即构成表达式语句,具体可参考以下代码:

        X+5;

4.空语句

空语句是只有一个分号的语句,它不执行任何操作,一般作为循环语句中的循环体。空语句的使用可参考以下代码:

        While(1);

以上代码中的分号即表示该循环中循环体为空语句。

3.3 函数

将解决某一问题的算法汇集起来,组成一个相对独立的函数,在需要时就可以调用这个函数来处理相应的问题,可以说,C程序的全部工作都是由多个不同的函数来完成的。函数可以根据需要自行定义,这一类函数我们称其为自定义函数。另外,为了简化代码编写的难度,通常C编译器还会将一些相对固定的功能事先编写成函数,以库的形式存储起来,这一类函数称为库函数。

3.3.1 自定义函数

这一类函数是用户根据需要自行定义的函数,须先定义后使用。自定义函数的形式如下:

        类型标识符 函数名(形式参数列表)
        {
            声明部分;
            语句部分;
        }

类型标识符用于指定函数的类型。函数的类型就是函数返回值的类型,即函数被调用后,执行函数体中的程序段所取得的并需要返回给主调函数的值。在很多情况下函数没有返回值,此时类型标识符由“void”取代;函数名通常由1~8个字符组成,给函数起名建议与函数的功能相联系,以便于阅读和记忆;形式参数列表用于指定函数的输入参数及类型,各参数间用“, ”分隔。函数被调用时,主调函数将赋予这些形式参数以实际的值;花括号“{}”括起的部分是函数体,由声明部分和语句部分构成。声明部分是函数体内部所用到变量的类型说明或要调用的函数声明。以下我们用一个函数的实例来说明自定义函数的方法:

        int max(int a, int b)
        {
            int temp;
            if(a>b)temp=a;
            else temp=b;
            return temp;
        }

上面的函数是一个比较a、b两数大小的函数,函数的类型是整型,即函数执行后,会反馈一个整型的数据给主调函数。函数的输入参数(形参)有两个,一个是a,另一个是b,它们也是整型数据。函数调用时,主调函数会给a和b两个形式参数赋予具体的值(如3和8),以比较这两个数的大小,这一过程也称为将实际参数赋予形式参数;在函数体部分,第一行是声明部分,声明了一个整型的变量temp,之后是语句部分,使用了if语句来比较两个数的大小,并将比较结果用return语句返回。

这里需要说明的是,对于无返回值的函数,可以将函数的类型定义为“空类型”,类型说明符为“void”。同样如果函数没有入口参数,形参列表可以空白或用“void”表示,如某一既无返回值又无入口参数的函数书写方法如下:

        void t0_init(void)
        {
            …
        }

除了自定义函数以外,每一个C编译器都会提供一些库函数,这些库函数无需用户定义,也不必在程序中进行类型说明,只需在程序前包含有该函数原型的头文件即可在程序中直接调用。比如在本书后面章节里使用的延时函数,就是由GCC编译器提供的库函数,随着学习的不断深入,相信你对库函数会有更深入的理解。

3.3.2 函数的声明和调用

在一个C程序中,当自定义函数位于主调函数后面时就需要在程序的开始位置对自定义函数进行声明,以便将函数的名称、函数参数的个数和类型等信息通知编译器,从而在调用此函数时,编译器能正确识别该函数并检查调用是否合法。

1.函数的声明

        类型标识符 函数名(参数类型1 形参名1,参数类型2 形参名2, …,参数类型n形参名n);

如前面介绍比较a、b两个数大小的函数声明方法为:

        int max(int a, int b);

2.函数的调用

定义函数的目的就是为了使用它来完成某些功能,函数调用的一般形式为:

        函数名(实际参数列表);

调用前面介绍的比较a、b两个数大小的函数方法可以参考以下代码:

        z=max(3,8);  //比较3、8两个数的大小,并将比较的结果赋值给z

此函数在调用时,用于比较3和8两个数的大小,并将比较的结果赋值给z。调用无返回值的函数的方法可以参考以下代码:

        delay(5);  //延时5ms

3.4 程序

3.4.1 程序的构成

C程序的构成是由一个main函数和若干个其他函数构成的,以下我们用一个实际的例子来说明C程序的构成。

        #include<reg52.h>         //8051单片机的头文件
        void main()               //定义主函数,返回值为空
        {
            unsigned char num;      //定义变量num
            P0=0xFE;                //让P0.0口输出低电平,P0.1~P0.7保持高电平
            while(1)              //让程序在此循环
            {
            num++;                  //变量num的值自加
            }
        }

在上面的程序中,第一行“#”是预处理命令行起始符号,“include”是预处理命令,表示程序在这里引用了来自另外一个地点的文件,“include”用于将该文件中的程序行放到本程序中使用。“reg52.h”是C51编译器提供的增强型8051单片机的头文件,用于对8051单片机的寄存器进行规范化定义。

程序的第二行是一个函数。我们知道,C语言是一个模块化的语言,它的主要部分就是由多个具有特定功能的函数构成的。“main”函数和C语言中其他函数在结构上是一致的,但它的名称是固定的“main”,即“主函数”的意思。在一个C源程序中,有且仅有一个主函数,无论主函数位于源程序的什么位置,程序执行时都从主函数开始。

主函数的返回值为“空”,而且没有输入参数。在“main”函数的函数体中,首先定义了一个变量num,之后是一个赋值语句,意思是给寄存器P0赋值为十六进制的“FEH”,函数中的每一个程序行都以分号“; ”结束。接下来的程序行是一个while语句,它是一个循环语句,用来控制程序段(即循环体)的重复执行,这里程序的目的是让变量num的值不断自加。单片机的程序都是一个趋于无限的死循环,程序中使用while(1)这样的写法的目的是使程序在此进入持续的循环状态。

分析程序的运行过程,主函数是程序运行的开始。程序从主函数的函数体第一行开始执行,直至while循环之前,这一部分在每次系统复位后会顺序执行一次,程序中变量的声明、系统的初始化等可以放在这一部分运行。之后,程序进入由while语句构成的主循环中。这部分语句在程序运行时会无限地循环执行,适用于软件查询标志位、扫描按键和数码管等需要不间断访问的部分。主函数的运行过程如图3-5所示。

图3-5 主函数的运行过程

3.4.2 程序的注释

为了便于理解程序,可以在程序行的适当位置加入注释。注释有两种,一种是单行注释,即在需要注释的文字前面加入两个斜杠,其格式为:

        // 注释的文字

另一种是多行注释,即在要注释的段落开始位置加入一个斜杠和一个星号,在段落的结束位置再加入一个星号和斜杠,具体格式为:

        /* 注释的文字  注释的文字  注释的文字  注释的文字  注释的文字  注释的文字
            注释的文字  注释的文字  注释的文字  注释的文字  注释的文字  注释的文字  */

被注释的文字在编辑器中是以绿色显示的,在对程序进行编译时,被注释的文字不参加编译,也不会干扰程序的运行。经常对程序代码进行注释是一个好习惯,它不但可以帮助别人理解你的代码,也给日后自己的阅读带来方便。

3.4.3 局部变量和全局变量

变量的有效性范围称为变量的作用域,C语言中所有的量都有自己的作用域,变量说明的方式不同,决定了其作用域也不同。按作用域范围不同,C语言中的变量可分为两种,即局部变量和全局变量。

1.局部变量

局部变量也称为内部变量。局部变量是在函数内部进行定义和说明的,其作用域仅限于函数内部,离开该函数后再使用该变量是非法的。例如:

        void delay(unsigned int t)
        {
            unsigned int x, y;
            for(x=t; x>0; x--)
            {
                for(y=2650; y>0; y--)
                {
                }
            }
        }

在上面的delay函数内部,定义了两个变量x和y,这两个变量在delay函数内部使用是合法的,或者说变量x和y的作用域仅限于delay函数内部。C程序中允许在不同的函数中使用相同的局部变量名,但它们代表不同的对象,调用时会分配不同的内存单元,互不干扰。另外,在主函数中定义的变量也是局部变量,只能在主函数中使用,主函数中也不能使用其他函数中定义的变量。

2.全局变量

全局变量也称为外部变量,它是在函数的外部定义的变量。全局变量不属于某一个函数,而是属于某一个源程序文件。全局变量的作用域是整个源程序,在函数中使用全局变量,同样需要先定义后使用。例如:

        #include<reg52.h>                                //8051单片机的头文件
        unsigned int  NUM;                                 //定义全局变量NUM用于显示
        void  display(unsigned int K);                   //数码管显示函数声明
        …
        int main(void)
        {
              …
            while(1)
            {
                …
                display(NUM);                            //扫描数码管
            }
        }
        void  display(unsigned int K)
        {
            unsigned char NUM4, NUM3, NUM2, NUM1;          //定义四个局部变量
            …
        }

在以上的代码中,变量NUM是一个全局变量,它的定义位置是在函数的外面,因此它的作用域是整个程序,NUM这个变量在程序的任何地方调用都是合法的。全局变量经常用来作为函数间数据的传递。在display函数的内部定义的变量NUM1~NUM4则是局部变量,它在display函数内部,也只能在该函数内部使用。

3.5 预处理命令

在编写程序时,经常会使用以“#”开头的预处理命令。在对程序进行编译时,会有专门的预处理程序来对这些命令进行处理。预处理命令不属于C语句,因此在行末不必加分号,而且预处理命令通常要放在程序的最前面。在C程序中加入预处理命令可以改善程序结构,提高编译效率。C语言提供的预处理命令主要有宏定义、文件包含和条件编译3种,以下我们要重点介绍前面两种。

3.5.1 宏定义

宏定义的作用是用一个标识符(宏名)来表示一个字符串,其格式为:

        #define  标识符(宏名) 字符串

在宏定义中,“#”表示这是一条预处理命令,“define”为宏定义命令。标识符是我们自行定义的宏名,字符串可以是常数或表达式等。宏定义的方法可以参考以下代码:

        #define  PI  3.141592               //用PI来表示3.1415926 这个常量
        #define  M  (X*Y+8Y)             //用M来表示(X*Y+8Y)这个表达式
        #define  uint  unsigned  int        //用uint表示unsigned int

使用宏定义的方法可以增强代码的可读性,并且能使语句变得简洁明了。

3.5.2 文件包含

文件包含的作用是将另外一个文件的内容复制到包含命令所在的位置,从而将指定的文件和当前的源程序文件连接成一个源文件。文件包含的格式为:

        #include <文件名>

文件包含也可以使用这样的格式:

        #include “文件名”

在以上两种格式中,使用尖括号与引号的意义是不同的。使用尖括号时,程序首先在编译器头文件所在目录下搜索头文件;而使用引号时,程序首先搜索项目文件所在目录,然后再搜索编译器头文件所在目录,两者的搜索顺序刚好相反。文件包含的方法可以参考以下代码:

        #include<reg52.h>         //包含增强型8051单片机的头文件

在上述代码中,include <regs2.h>的作用是将“reg52.h”这个头文件连接到本程序中,用于对8051单片机的各个寄存器进行规范化定义。

3.6 构造类型数据

我们前面介绍的数据类型有字符型、整型、实型等,它们都属于基本的数据类型。C语言中还支持构造类型的数据,它是由基本数据类型按照一定的规则组合而成的,构造类型数据主要包括数组、结构体和共用体等。

3.6.1 数组

简单地说,数组就是同一类变量的有序集合。数组同普通变量一样,要先定义后使用,定义数组的方法如下:

        数据类型  数组名  [常量表达式] ;

定义数组时,“数据类型”是指数组中各个单元的类型,数组只能是同一类型的数据单元的集合;“数组名”是整个数组的标识,命名方法同变量命名方法相同;“常量表达式”表示数组中单元的个数,它必须用括号括起,括号里的数不能是变量,只能是常量。定义数组的方法可以参考以下代码:

        unsigned  int  count [10] ;

以上代码定义了无符号整型数组count,它有10个数据单元。在使用数组时,用数组名加下标的方法加以引用,具体方法可以参考以下代码:

        count [3]=X;

意思是将变量“X”的值赋给数组count的第四个元素。这里需要注意的是,数组的下标是从0开始的,“count[0]”就代表数组中的第一个数据单元,“count[9]”代表的是其最后的一个数据单元。我们也可以在定义数组的时候为其赋初值,定义这种数组的格式如下:

        数据类型  数组名 [常量表达式]={ 常量表达式1,常量表达式2, …,常量表达式n };

在赋初值的数组中,方括号内的常量表达式是可以省略的,这时数组中数据单元的个数就由实际初值的个数决定。“{}”括号内是数组各单元的初值,两个初值间用逗号分隔。定义赋初值的数组可以参考以下代码:

        unsigned char  seg_table[ ]={0x3f,0x06,0x5b,0x4f,0x66,
        0x6d,0x7d,0x07,0x7f,0x6f};

上面介绍的数组是一维的,数组也可以是多维的,关于多维数组在这里不做详细介绍,需要时可以参考C语言的相关书籍。

3.6.2 结构体

结构体是在一个统一的名称下,组合在一起的变量的集合。结构体中的每个变量都称为结构体的成员,每个成员之间的数据类型可以不同,结构体变量的总长度等于结构体中每个成员长度的总和。定义结构体的格式如下:

        struct结构体类型名
        {
            成员类型名 成员名1;
            成员类型名 成员名2;
            …
            成员类型名 成员名n;
        }变量名列表;

例如,我们要构建一个学生情况登记表,可以用下面的方法定义结构体:

        struct  student                   //结构体关键词及结构体名称
        {
        unsigned int  num;                //学号,结构体的成员1
        unsigned char name;               //名字,结构体的成员2
        unsigned char age;                //年龄,结构体的成员3
        } student1, student2;             //结构体变量名

在上面的结构体定义中,struct是结构体类型标识,是C语言的关键字。结构体的名称是student,这个结构体由3个成员构成,即num、name和age。“{}”结束后的student1、student2定义的是两个结构体变量,最后的“; ”是结构体的类型定义结束符。结构体不能与其他基础型的变量间相互赋值,对结构体变量的引用,包括赋值、运算等都是通过结构体变量的成员来实现的,引用上面定义的结构体变量成员的方法如下:

        student1.num=57;
        student2.age=16;

3.6.3 共用体

共用体与结构体有相似之处,但共用体的成员全部共用相同的存储空间,一个共用体变量的长度等于各成员中最长的成员长度。实际上,共用体是一个在不同时间保存不同类型数据的变量,这些不同类型的数据共用一个存储空间,赋入新值则会冲掉旧值。定义共用体的格式如下:

        union结构体类型名
        {
            类型说明符1  成员名1;
            类型说明符2  成员名2;
        …
            类型说明符n  成员名n;
        }变量名列表;

例如,我们要定义一个名称为number的共用体可以参考以下代码。

        union  number                       //共用体关键词和共用体名称
        {
        unsigned int    a ;                 //共用体成员1
        unsigned char   b;                  //共用体成员2
        float   c;                          //共用体成员3
        } u1, u2, u3;                       //共用体变量名

在这个共用体的类型定义中,union是共用体类型的标识,也是C语言的关键字。共用体的名称是number,这个共用体由3个成员构成,即a、b和c。“{}”结束后的u1、u2、u3定义的是3个共用体变量。共用体变量的成员在内存中占用同一首地址的空间,其引用方法如下:

        u1.a=3457;
        u1.b=215;
        u3.c=12.634;

本章回顾

本章至此已经将C语言的基础知识部分进行了概括说明,阅读完本章内容,相信你对C语言及C程序的开发会有一个基本的认识。在使用Keil C51编译器为8051单片机开发C程序时,C51编译器会针对8051单片机的特点对编译器进行一些优化,本书将在后面的章节中对Keil C51编译器的特色部分进行介绍。