2.1 单片机C语言入门
51单片机的编程语言主要有两种,一种是汇编语言,另一种是C语言。汇编语言的机器代码执行效率很高,但可读性却不强,复杂一点的程序就更难读懂,而C语言虽然在机器代码生成效率上不如汇编语言,但可读性和可移植性却远远超过汇编语言,而且C语言还可以嵌入汇编来解决高时效性的代码编写问题。因此,在掌握一定汇编语言的基础上,就需要进一步学习C语言编程了。
2.1.1 C语言的特点
C语言是一种结构化语言。它层次清晰,便于按模块化方式组织程序,易于调试和维护。C语言的表现能力和处理能力极强,它不仅具有丰富的运算符和数据类型,便于实现各类复杂的数据结构。它还可以直接访问内存的物理地址,进行位(bit)一级的操作。由于C语言实现了对硬件的编程操作,因此C语言集高级语言和低级语言的功能于一体,效率高、可移植性强,特别适合单片机系统的编程与开发。
2.1.2 单片机采用C语言编程的好处
与汇编语言相比,C语言在功能性、结构性、可读性、可维护性上有明显的优势,因而易学易用。用过汇编语言后再使用C语言来开发,体会会更加深刻。下面简要说明单片机采用C语言编程的几点好处。
1. 语言简洁,使用方便、灵活
C语言是现有程序设计语言中规模最小的语言之一,C语言的关键字很少,ANSI C标准一共只有32个关键字,9种控制语句,压缩了一切不必要的成分。C语言的书写形式比较自由,表达方法简洁,使用一些简单的方法就可以构造出相当复杂的数据类型和程序结构。同时,当前几乎所有单片机都有相应的C语言级别的仿真调试系统,调试十分方便。
2. 代码编译效率较高
当前,较好的C语言编译系统编译出来的代码效率只比直接使用汇编语言低20%左右,如果使用优化编译选项甚至可以更低。况且,随着单片机技术的发展,ROM空间不断增加,51系列单片机中,片上ROM空间做到32KB、64KB的比比皆是,代码效率所差的20%已经不是一个重要问题。
3. 无须深入理解单片机内部结构
采用汇编语言进行编程时,编程者必须对单片机的内部结构及寄存器的使用方法十分清楚;在编程时,一般还要进行RAM分配,稍不小心,就会发生变量地址重复或冲突。
采用C语言进行设计时,则不必对单片机硬件结构有很深入的了解,编译器可以自动完成变量存储单元的分配,编程者可以专注于应用软件部分的设计,大大加快了软件的开发速度。
4. 可进行模块化开发
C语言是以函数作为程序设计的基本单位的,C语言程序中的函数相当于汇编语言中的子程序。各种C语言编译器都会提供一个函数库,此外,C语言还具有自定义函数的功能,用户可以根据自己的需要编写满足某种特殊需要的自定义函数(程序模块),这些程序模块可不经修改,直接被其他项目所用。因此采用C语言编程,可以最大限度地实现资源共享。
5. 可移植性好
用过汇编语言的读者都知道,即使是功能完全相同的一种程序,对于不同的单片机,必须采用不同的汇编语言来编写。这是因为汇编语言完全依赖于单片机硬件。C语言是通过编译来得到可执行代码的,本身不依赖机器硬件系统,用C语言编写的程序基本上不用修改或者进行简单的修改,即可方便地移植到另一种结构类型的单片机上。
6. 可以直接操作硬件
C语言具有直接访问单片机物理地址的能力,可以直接访问片内或片外存储器,还可以进行各种位操作。
介绍到这里,我想说一下我学习单片机编程的一个小插曲。在20世纪90年代中期,我最初接触单片机的时候,在我心中觉得51就是单片机,单片机就是51,根本不知道还有其他单片机的存在。那时,我学习的是汇编语言,根本不知道用C语言也可以进行单片机开发。幸运的是,我有一个同事,比较精通C语言,我们一起做一个项目的时候,我才真正发现C语言的威力,于是,在同事的影响下,我开始使用C语言进行单片机编程。其实我也很庆幸,学习和使用了两年多的汇编语言。由于这些锻炼,我对单片机底层结构和接口时序弄得很清楚。在使用C语言开发的时候,优化代码和处理中断就不会太费劲。
总之,用C语言进行单片机程序设计是单片机开发与应用的必然趋势,我们一旦学会使用C语言之后,就会对它爱不释手,尤其是进行大型单片机应用程序开发,C语言几乎是唯一的选择。
2.1.3 如何学习单片机C语言
C语言常用语法不多,尤其是单片机的C语言常用语法更少,初学者没有必要将C语言的所有内容都学习一遍,只要跟着本书学下去,当遇到难点时,停下来适当地查阅C语言基础教材里的相关部分,便会很容易掌握。有关C语言的基础教材较多,在这里,笔者向大家推荐谭浩强的《C程序设计》一书,该书语言通俗,实例丰富,十分适合初学者学习和查阅。
C语言仅仅是一个编程语言,其本身并不难,难的是如何灵活运用C语言编写出结构完善的单片机程序。要达到这一点,就必须花费大量的时间进行实践、实验,光看书不动手,等于是纸上谈兵,很难成功!因此,本书主要是通过不断地实践、实战,使读者在玩中学,在学中玩,步步为营,步步深入,使自己在不知不觉中,成为单片机的编程高手。
2.1.4 一个简单的流水灯程序
下面我们先来看一个实例,这个例子的功能十分简单,就是让单片机的P2口的LED灯按流水灯的形式进行闪烁,每个LED灯的闪烁时间为0.5s,硬件电路如图2-1所示。
图2-1 点亮P2口LED灯电路
图中采用STC89C52单片机,这种单片机属于80C51系列,其内部有8KB的Flash ROM和512B(字节)的RAM,并且可以通过串口进行ISP程序下载,不需要反复插拔芯片,非常适于做实验。STC89C52的P2引脚上接8个发光二极管,RP7、RP9为限流电阻排,以免LED被烧坏;P2口是准双向口,可以外接上拉电阻,也可以不接,图中外接了RP13上拉电阻排。
根据要求,用C语言编写的程序如下。
#include<reg52.h> #define uint unsigned int sbit P20=P2^0; //定义位变量 sbit P21=P2^1; sbit P22=P2^2; sbit P23=P2^3; sbit P24=P2^4; sbit P25=P2^5; sbit P26=P2^6; sbit P27=P2^7; void Delay_ms(uint xms) //延时程序,xms是形式参数 { uint i, j; for(i=xms;i>0;i--) //i=xms,即延时x毫秒,xms由实际参数传入一个值 { for(j=115;j>0;j--) {;} //此处分号不可少,表示是一个空语句 } } void main() { while(1) //循环显示 { P20=0; //P20脚灯亮 Delay_ms (500); //将实际参数500传递给形式参数xms,延时0.5s P20=1; //P20脚灯灭 P21=0; //P21脚灯亮 Delay_ms (500); P21=1; //P21脚灯灭 P22=0; //P22脚灯亮 Delay_ms (500); P22=1; //P22脚灯灭 P23=0; //P23脚灯亮 Delay_ms (500); P23=1; //P23脚灯灭 P24=0; //P24脚灯亮 Delay_ms (500); P24=1; //P24脚灯灭 P25=0; //P25脚灯亮 Delay_ms (500); P25=1; //P25脚灯灭 P26=0; //P26脚灯亮 Delay_ms (500); P26=1; //P26脚灯灭 P27=0; //P27脚灯亮 Delay_ms (500); P27=1; //P27脚灯灭 } }
这里,采用单片机C语言编译器Keil软件作为开发环境,关于Keil软件的详细内容,将在后面进行介绍。
下面我们对这个程序进行简要的分析。
程序的第一行是“文件包含”,所谓“文件包含”是指一个文件将另外一个文件的内容全部包含进来。所以,这里的程序虽然只有几行,但C编译器(Keil软件)在处理的时候却要处理几十行或几百行。为加深理解,可以用任何一个文本编辑器打开Keil\c51\inc文件夹下面的reg52.h来看一看里面有什么内容,在C编译器处理这个程序时,这些内容也会被处理。这个程序包含reg.h的目的就是为了使用P2这个符号,即通知C编译器程序中所写的P2是指80C51单片机的P2端口,而不是其他变量,这是如何做到的呢?用写字板程序打开reg52.h显示如下。
#ifndef __REG52_H__ #define __REG52_H__ / * BYTE Register * / sfr P0 = 0x80; sfr P1 = 0x90; sfr P2 = 0xa0; sfr P3 = 0xb0; …… #endif
可以看到:“sfr P2= 0xa0;”,即定义符号P2与地址0xa0对应,熟悉80C51内部结构的读者不难看出,P2口的地址就是0xa0。
程序的第2行是一个宏定义语句,注意后面没有分号。#define命令用它后面的第一个字母组合代替该字母组合后面的所有内容,也就是相当于我们给“原内容”重新起一个比较简单的“新名称”,方便以后在程序中使用简短的新名称,而不必每次都写烦琐的原内容。该例中,我们使用宏定义的目的就是将unsigned int用uint代替,在上面的程序中可以看到,在我们需要定义unsigned int类型变量时,并没有写unsigned int,取而代之的是uint。
程序的第3~10行用符号P20~P27来表示P2口的P2.0~P2.7八只引脚。在C语言里,如果直接写P2.0、P2.1……P2.7,C编译器并不能识别,而且它们不是一个合法的C语言变量名,所以得给它另起一个名字,这里起的名为P20~P27,可是P20~P27是否就是P2.0~P2.7呢?你这么认为,C编译器可不这么认为,所以必须给它们建立联系,这里使用了Keil的保留字sbit来定义。
main称为“主函数”,每一个C语言程序有且只有一个主函数,函数后面一定有一对大括号“{}”、在大括号里面书写其他程序代码。
Delay_ms(500)的用途是延时。由于单片机执行指令的速度很快,如果不进行延时,灯亮之后马上就灭,灭了之后马上就亮,速度太快,人眼根本无法分辨,所以需要进行适当的延时,这里采用自定义函数Delay_ms(500),以延时0.5s的时间,函数前面的void表示该延时函数没有返回值。
Delay_ms(500)函数是一个自定义函数,它不是由Keil编译器提供的,即你不能在任何情况下写这样一行程序以实现延时,如果在编写其他程序时写上这么一行,会发现编译通不过。注意观察本程序会发现,在使用Delay_ms(500)之前,第11~16行已对Delay_ms(uint xms)函数进行了事先定义,因此,在主程序中才能采用Delay_ms(500)进行调用。
注意,在延时函数Delay_ms(uint xms)定义中,参数xms被称作“形式参数”(简称形参);而在调用延时函数Delay_ms(500)中,小括号里的数据“500”,这个“500”被称作“实际参数”(简称实参)。参数的传递是单向的,即只能把实参的值传给形参,而不能把形参的值传给实参。另外,实参可以在一定范围内调整,这里用“500”来要求延时时间为0.5s,如果是“1000”,则延时时间是1000ms,即1s。
在延时函数Delay_ms(uint xms)内部,采用了两层嵌套for语句,如下所示。
void Delay_ms(uint xms) //延时程序,xms是形式参数 { uint i, j; for(i=xms;i>0;i--) //i=x毫秒,即延时xms,xms由实际参数传入一个值 { for(j=115;j>0;j--) {;} //此处分号不可少,表示是一个空语句 } }
在这个延时函数中,采用的是一种比较正规的形式,C语言规定,当循环语句后面的大括号只有一条语句或为空时,可省略大括号,因此,上面两个for循环语句中的大括号都可以省略,也就是说,可以采用以下简化的形式。
void Delay_ms(uint xms) //延时程序,xms是形式参数 { uint i, j; for(i=xms;i>0;i--) //i=xms,即延时x毫秒,xms由实际参数传入一个值 for(j=115;j>0;j--); //此处分号不可少 }
第一个for后面没有分号,那么编译器就会认为第二个for语句就是第一个for语句的内部语句,而第二个for语句后面有分号,编译器就会认为第二个for语句内部语句为空。程序在执行时,第一个for语句中的i每减一次,第二个for语句便执行115次,因此上面这个例子便相当于共执行了xms×115次for语句。通过改变xms变量的值,可以改变延时时间。
2.1.5 利用C51库函数实现流水灯
上面介绍的程序虽然可以实现流水灯的功能,程序比较烦琐,下面采用C51自带的库函数_crol()_()来实现,具体源程序如下所示。
#include<reg52.h> #include<intrins.h> #define uint unsigned int #define uchar unsigned char void Delay_ms(uint xms) //延时程序,xms是形式参数 { uint i, j; for(i=xms;i>0;i--) //i=xms,即延时x毫秒,xms由实际参数传入一个值 for(j=115;j>0;j--); //此处分号不可少 } void main() { uchar led_data=0xfe; //给led_data赋初值0xfe,点亮第一个LED灯 while(1) //大循环 { P2= led_data; Delay_ms(500); led_data=_crol_(led_data,1);//将led_data循环左移1位再赋值给led_data } }
该源程序在ch2/my_8LED文件夹中。
显然,这个流水灯程序比上面的流水灯程序要简捷许多,下面简要进行说明。
程序中,_crol_是一个库函数,其函数原形为:
unsigned char _crol_(unsigned char c, unsigned char b);
这个函数是C51自带的库函数,包含在intrins.h头文件中,也就是说,如果在程序中要用到这个函数,那么必须在程序的开头处包含intrins.h这个头文件。函数实现的功能是,将字符c循环左移b位。
函数中,_crol_是函数名,不用多讲,函数前面没有void,取而代之的是unsigned char,表示这个函数返回值是一个无符号字符型数据;有返回值的意思是说,程序执行完这个函数后,通过函数内部的某些运算而得出一个新值,该函数最终将这个新值返回给调用它的语句。小括号里有两个形参,unsigned char c和unsigned char b,它们都是无符号字符型数据。
现在我们应该清楚led_data=_crol_(led_data,1)这条语句的含义了,其作用就是,将led_data中的数据向左循环移1位,再赋给变量led_data。
有左移位库函数,当然也有右移库函数,函数原形为:
unsigned char _cror_(unsigned char c, unsigned char b);
右移位函数与左移位函数使用方法相同,这里不再重复。
2.1.6 小结
通过以上的几个简单的C语言程序,我们可以总结出以下几点。
(1)C程序是由函数构成的。一个C源程序至少包括一个函数,一个C源程序有且只有一个名为main()的函数,也可能包含其他函数,因此,函数是C程序的基本单位。主程序通过直接书写语句和调用其他函数来实现有关功能,这些其他函数可以是由C语言本身提供给我们的,这样的函数称之为库函数(流水灯程序中的_crol_(led_data,1)函数就是一个库函数),也可以是用户自己编写的,这样的函数称之为用户自定义函数(流水灯程序中的Delay_ms(uint xms)函数就是一个自定义函数)。那么,库函数和用户自定义函数有什么区别呢?简单地说,任何使用C语言的人,都可以直接调用C的库函数而不需要为这个函数写任何代码,只需要包含具有该函数说明的相应的头文件即可;而自定义函数则是完全个性化的,是用户根据自己的需要编写的。
(2)一个函数由两部分组成。
① 函数的首部,即函数的第一行。包括函数名、函数参数(形式参数)等,函数名后面必须跟一对圆括号,即便没有任何参数也是如此。
② 函数体,即函数首部下面的大括号“{}”内的部分。如果一个函数内有多个大括号,则最外层的一对“{}”为函数体的范围。
(3)一个C语言程序,总是从main函数开始执行的,而不管物理位置上这个main()放在什么地方。
(4)主程序中的Delay_ms(uint xms)如果写成delay_ms(uint xms)就会编译出错,即C语言区分大小写,这是很多初学者在编写程序时常犯的错误,书写时一定要注意。
(5)C语言书写的格式自由,可以在一行写多个语句,也可以把一个语句写在多行。没有行号(但可以有标号),书写的缩进没有要求。但是建议读者自己按一定的规范来写,可以给自己带来方便。
(6)每个语句定义的最后必须有一个分号,分号是C语句的必要组成部分。
(7)可以用/ *…..* /的形式为C程序的任何一部分作注释,在“/ *”开始后,一直到“* /”为止的中间的任何内容都被认为是注释;如果使用的是Keil开发软件,那么,该软件也支持C++风格的注释,就是用“//”引导的后面的语句是注释。这种风格的注释,书写比较方便,只对本行有效,在只需要一行注释的时候,我们往往采用这种格式。但要注意,只有Keil支持这种格式,其他的编译器不一定支持这种格式的注释。