1.2 PIC单片机C语言入门
PIC单片机的编程语言主要有两种:汇编语言和C语言。汇编语言的机器代码生成效率很高,但可读性并不强,复杂一点的程序就更难读懂,而C语言虽然机器代码生成效率不如汇编语言,但可读性和可移植性却远远超过汇编语言。因此,开发PIC单片机,采用的一般都是C语言。
1.2.1 为什么采用C语言编程
与汇编语言相比,C语言在功能、结构性、可读性、可维护性上都有明显优势,易学易用。用过汇编语言后再使用C语言来开发,体会更加深刻。下面简要说明单片机采用C语言编程的几点好处。
1.语言简洁,使用方便灵活
C语言是现有程序设计语言中规模最小的语言之一,C语言的关键字很少,ANSIC标准共有32个关键字,9种控制语句,压缩了一切不必要的成分。C语言的书写形式自由,表达方法简单,使用一些简单的方法就可以构造出相当复杂的数据类型和程序结构。同时,当前几乎所有单片机都有相应的C语言级别的仿真调试系统,调试十分方便。
2.代码编译效率较高
当前,较好的C语言编译系统编译出来的代码效率只比直接使用汇编语言低20%左右,如果使用优化编译选项甚至可以更低。况且,PIC系列单片机片上ROM空间可以做得很大,代码效率所差的20%已经不是一个重要问题了。
3.无须深入理解单片机内部结构
采用汇编语言进行编程时,编程者必须对单片机的内部结构及寄存器的使用方法十分清楚。在编程时,一般还要进行RAM分配,稍不小心,就会发生变量地址重复或冲突。
而采用C语言进行设计时,则不必对单片机硬件结构有深入了解,编译器可以自动完成变量存储单元的分配,编程者可以专注于应用软件部分的设计,大大加快了软件的开发速度。
4.可进行模块化开发
C语言是以函数作为程序设计基本单位的,C语言程序中的函数相当于汇编语言中的子程序。各种C语言编译器都会提供一个函数库,此外,C语言还具有自定义函数的功能,用户可以根据自己的需要编制满足某种特殊需要的自定义函数(程序模块),这些程序模块可不经修改,直接被其他项目所用。因此,采用C语言编程,可以最大程度地实现资源共享。
5.可移植性好
用过汇编语言的读者都知道,即使是功能完全相同的一种程序,对于不同的单片机,必须采用不同的汇编语言来编写,这是因为汇编语言完全依赖于单片机硬件。C语言是通过编译来得到可执行代码的,本身不依赖机器硬件系统,用C语言编写的程序基本上不用修改或者只需进行简单的修改,即可方便地移植到另一种结构类型的单片机上。
6.可以直接操作硬件
C语言具有直接访问单片机物理地址的能力,可以直接访问片内或片外存储器,还可以进行各种操作。
总之,用C语言进行单片机程序设计是单片机开发与应用的必然趋势,一旦学会使用C语言,尤其学习过51单片机C语言的读者,再学习PIC单片机C语言是十分方便的,只需要对PIC单片机的硬件结构及相关寄存器做一简单了解即可。
1.2.2 简单的C语言程序
下面以一个简单的流水灯程序为例,了解一下PIC单片机C语言。
1.硬件电路
下面先来看一个实例,这个例子的功能十分简单,就是让单片机RD口的LED灯按流水灯的形式进行闪烁,硬件电路如图1-11所示。
5V电源分别连接8个发光二极管的正极,8个发光二极管的负极分别连接8个1kΩ限流电阻,然后再接到PIC单片机RD口。这样,当单片机的I/O口输出高电平时,发光二极管两端都是高电平,发光二极管不会导通,当I/O口输出低电平时,发光二极管正向导通,同时也就发出光亮了。
图1-11 点亮P0口LED灯电路
这里解释一下LED灯上串联电阻大小的选择问题。LED灯的工作电压为1.6~2.8V(一般为2V),工作电流为2~30mA(一般控制在4~10mA),如果系统供电VCC为5V,LED上串联的电阻为1kΩ,并取LED上电压为2V,那么,此时通过LED的电流则为(5V-2V)/1000Ω=3mA。如果需要提高亮度,需要增大LED灯的工作电流,当工作电流为10mA时,此时电阻应该选择(5V-2V)/10mA=300Ω,所以,LED灯的串联电阻一般在0.3Ω~1kΩ之间选择。
2.程序实现
8位流水灯源程序如下:
#include<pic.h> #define uchar unsigned char #define uint unsigned int __CONFIG(HS&WDTDIS&LVPDIS); /********延时函数********/ void Delay_ms(uint xms) { int i,j; for(i=0;i<xms;i++) { for(j=0;j<71;j++) ; } } /********主函数********/ void main (void) { TRISD=0x00; //RD口设置为输出 while(1) { PORTD=0xFE; //点亮第1个LED灯 Delay_ms(500); //延时 PORTD=0xFD; //点亮第2个LED灯 Delay_ms(500); PORTD=0xFB; //点亮第3个LED灯 Delay_ms(500); PORTD=0xF7; //点亮第4个LED灯 Delay_ms(500); PORTD=0xEF; //点亮第5个LED灯 Delay_ms(500); PORTD=0xDF; //点亮第6个LED灯 Delay_ms(500); PORTD=0xBF; //点亮第7个LED灯 Delay_ms(500); PORTD=0x7F; //点亮第8个LED灯 Delay_ms(500); } }
下面对这个程序进行简要分析。
(1)程序的第一行是“文件包含”。“文件包含”是指一个文件将另一个文件的内容全部包含进来。所以,这里的程序虽然只有几行,但C编译器(如HI-TECH编译器,即PICC软件)在处理的时候却要处理几十行或几百行。为加深理解,可以用任何一个文本编辑器打开HT-PIC\include文件夹下面的pic.h来看一看里面有什么内容,如下所示(摘取部分):
#ifndef _PIC_H #define _PIC_H …… #if defined(_16F87) || defined(_16F88) #include <pic16f87.h> #endif #if defined(_16F873) || defined(_16F874) ||\ defined(_16F876)|| defined(_16F877) ||\ defined(_16F872)|| defined(_16F871) ||\ defined(_16F870) #include <pic1687x.h> #endif #if defined(_16F873A) || defined(_16F874A) ||\ defined(_16F876A) || defined(_16F877A) #include <pic168xa.h> ……
PIC单片机与80C51系列单片机的不同之处在于其包含了一个庞大的系列,这个系列中的很多芯片有其特定的头文件。为了编写程序的方便,PICC编译器给出了一个统一的头文件pic.h,在这个文件中,根据编译环境所定义的器件名称,调入定义这个器件的头文件。在编译这段程序时,需要先建立一个工程,在建立工程时,假设定义器件名称为PIC16F877A,相当于满足了下述条件中的defined(_16F877A)部分,因此,在编译程序时会执行:
#include <pic168xa.h>
即开始调pic168xa.h头文件,下面再来看看这个头文件,用记事本打开后,内容如下(部分):
/* * Header file for the Microchip * PIC 16F873A chip * PIC 16F874A chip * PIC 16F876A chip * PIC 16F877A chip * Midrange Microcontroller */ #if defined(_16F874A) || defined(_16F877A) #define __PINS_40 #endif static volatile unsigned char INDF @ 0x00; static volatile unsigned char TMR0 @ 0x01; static volatile unsigned char PCL @ 0x02; static volatile unsigned char STATUS @ 0x03; static unsigned char FSR @ 0x04; static volatile unsigned char PORTA @ 0x05; static volatile unsigned char PORTB @ 0x06; static volatile unsigned char PORTC @ 0x07; #ifdef __PINS_40 static volatile unsigned char PORTD @ 0x08; static volatile unsigned char PORTE @ 0x09; #endif ……
从以上定义可以看出,这些符号的定义规定了符号名与地址的对应关系。其中有
static volatile unsigned char PORTD @ 0x08;
这样的一行,即定义PORTD与地址0x08对应,PORTD的地址就是0x08。
熟悉PIC内部结构的读者不难看出,PORTD口的地址就是0x08。在这个宏定义中,volatile是C语言的关键字,加上volatile关键字后,将不进行编译优化。
(2)源程序中有一行语句:
__CONFIG(HS&WDTDIS& LVPDIS);
此行程序称为配置文件,其功能是为这款单片机进行配置。PIC单片机内部具有多种功能,可以根据需要来进行配置,以便在不同场合正确地工作。这种配置工作可以在烧写芯片时手工设置,不过更好的方法是将配置写在程序中,在生成可烧写文件后,就将配置信息也包含在内了。对于大部分编程器所配套的软件而言,它们能够识别这种信息,从而自动完成配置,避免了手工设置可能带来的错误。
下面简要说明这条配置语句的意义:
① PIC16F877A芯片可以在4种不同类型的振荡方式下工作,实验板上采用的是外接4MHz的高频晶体振荡器,因此,配置时要选择HS。
为了避免在刚开始学习时由于芯片内部看门狗复位而造成误判,一般在学习程序过程中总是关掉看门狗,因此,配置时选择WDTDIS(禁止看门狗)。
实验时,一般采用高电压编程方式来下载程序,因此,这里选择LVPDIS(低电压编程禁止)。
② Delay_ms(500)的用途是延时,由于单片机执行指令的速度很快,如果不进行延时,灯亮之后马上就灭,灭了之后马上就亮,速度太快,人眼根本无法分辨,所以,需要进行适当的延时,这里采用自定义函数Delay_ms(500)实现延时,函数前面的void表示该延时函数没有返回值。
Delay_ms(500)函数是一个自定义函数,它不是由PIC编译器提供的,也就是说不能在任何情况下写这样一行程序以实现延时,如果在编写其他程序时写上这么一行,会发现编译通不过。注意观察本程序会发现,在使用Delay_ms(500)之前,已对Delay_ms(int k)函数进行了事先定义,因此,在主程序中才能对Delay_ms(500)进行使用。
注意:在延时函数Delay_ms(uint xms)定义中,参数xms称为“形式参数”(简称形参);而在调用延时函数Delay_ms(500)中,小括号里的数据“500”称为“实际参数”(简称实参),参数的传递是单向的,即只能把实参的值传给形参,而不能把形参的值传给实参。另外,实参可以在一定范围内调整,这里的“500”表示延时时间为0.5s,若为“1000”,则延时时间是1000ms,即1s。
③ PIC单片机的I/O口是标准的I/O口,I/O口的功能是负责实现CPU通过系统总线把I/O电路和外围设备联系在一起,标准的I/O口具有输入、输出、高阻3种状态,PIC单片机通过两个寄存器来控制I/O口的状态:输入和输出方向寄存器TRISx(x表示端口,如TRISD表示端口RD的方向寄存器)、输出寄存器PORTx。
程序中,“TRISD=0x00;”就是将端口RD口设置为输出。
“PORTD=0xFE;”的含义就是将端口RD的输出寄存器设置为0xFE,即让端口RD的高7位输出高电平,低1位输出低电平,其他依次类推。
④ 在单片机程序中,让程序进入一个while(1){}死循环中,这样保证程序一直运行。程序都是一步一步向下执行的,执行到程序的结尾就会停止,这时即使外界再有什么动作,单片机也不再响应了,加上死循环,那么程序就会一直在这个循环体中运行。如果在这个循环体中进行相应操作,程序就会很快检测到并给出响应。
在本例中,while(1){}死循环的功能轮流点亮PORTD口的LED灯,使PORTD口的LED灯流动显示。