4.1 C51的程序结构
4.2 C51的数据结构
4.3 C51与汇编的混合编程
4.4 C51仿真开发方法
4.5 通用I/O口的简单应用
4.6 通用I/O口的进阶应用
4.1.1 C51语言概述
C51语言是51单片机的一种高级编程语言,与低级语言的汇编语言相比,一方面具有结构化语言特点和机器级控制能力,代码紧凑执行效率可与汇编语言媲美。另一方面,由于接近自然语言,程序的可读性强,易于调试维护,编程工作量小,产品开发周期短。可见 C51具有很大的单片机程序开发优势,现已成为51单片机的主流编程语言。
标准C语言自问世以来,以诸多优点而获得了广泛应用,已成功移植到了大中小型各类计算机中,其中C51语言就是标准C语言用于51单片机的子集。
除了少数扩展功能外,大多数C语言的功能,如分支选择、循环控制、运算及表达式等执行语句,数组、结构体、预处理命令等基本语句在C51中都能通行延用,这对于具有C语言编程基础的读者掌握单片机程序设计,无疑是极为有利的。
由于C51毕竟是针对8051系列CPU扩展而成的,因而与标准C语言针对的机型在不少方面是有差异的,主要表现为以下几个方面。
①数据类型方面
51单片机中的特殊功能寄存器SFR、可位寻址存储空间、工作寄存器Rn等都是51单片机的特有资源,C51具有对其进行访问和操作的特殊规则;
②存储类型方面
51单片机采用哈佛结构存储器空间,程序和数据分别存储在不同的存储空间中,因而C51有表征不同存储空间变量类型的能力,这也是C51区别于标准C语言的最大之处;
③函数属性方面
由于51系统是8位机资源有限,不允许太多的复杂运算,因而标准C的库函数中只有少量可用于C51。而且由于中断函数对机器的硬件系统相关性很高,因而C51的中断函数也是有其特殊规则的;
④输入输出方面
C51的输入输出是通过访问机器端口的映射地址实现的,而标准C语言则是通过如printf和scanf等输入输出库函数实现的,两者做法相差较大。
4.1.2 C51的程序结构
C51程序的基本单位是函数。一个C51源程序至少包含一个主函数main(),也可以是一个主函数和若干个其他函数。主函数是程序的入口;主函数中的所有语句执行完毕后程序就结束。
实例:实现P1.0引脚处LED灯闪烁控制功能
C51的程序结构
预处理命令——在编译之前进行的处理。 预处理包含三方面的内容: 宏定义、文件包含、条件编译,以符号“#”开头;
函数声明——把函数的名字、函数类型以及形参类型、个数和顺序通知编译系统;
变量定义——遵循“先定义后使用”的原则,以分号结尾。
4.2 C51的数据结构
1.C51的变量
在C语言编程中,数值可以改变的量称为变量。
例如:
在51单片机多存储空间中如何确定变量与地址的关系?
C51变量定义的四要素:
【存储种类】 数据类型 【存储类型】 变量名
存储种类用于说明变量的作用范围:
1、auto(自动型)——变量的作用范围在定义它的函数体或语句块内。执行结束后,变量所占内存即被释放。
2、extern(外部型) ——在一个源文件中被定义为外部型的变量,在其它源文件中需要通过extern说明方可使用。
3、static(静态型) ——利用static可使变量定义所在的函数或语句块执行结束后,其分配的内存单元继续保留。
4、register(寄存器型) ——目前已不推荐使用。
缺省存储种类为auto (自动)型变量
【存储种类】 数据类型 【存储类型】 变量名
数据类型用于表示数据存放格式
除上述常规格式外,51单片机还有三种新的存储格式:
C51扩充的3种数据类型:bit、sfr或sfr16、sbit
bit 型
关键词bit用于定义一个位变量,语法规则:
bit bit_name [= 0或1];
例如:bit door = 0 ;
//定义一个叫door的位变量且初值为0
与标准C的数据类型声明的语法规则是一致的,
如: int int_name [ = 常数];
sfr或sfr16型
关键词sfr或sfr16用于定义SFR字节地址变量,语法规则:
sfr sfr_name = 字节地址常数;
sfr16 sfr_name = 字节地址常数;
例如, sfr P0 = 0x80; //定义P0口地址80H
sfr PCON = 0x87; //定义PCON地址87H
sfr16 DPTR=0x82; //定义DPTR的低地址82H
注意:C语言中十六进制整数是数值前加0x或0X前缀
sbit型
关键词sbit用于定义SFR位地址变量
位地址表达形式:绝对位地址、相对位地址
sbit型可用三种定义形式:
1)将SFR的绝对位地址定义为位变量名
sbit bit_name = 位地址常数;
例如, sbit CY = 0xD7;
2)将SFR的相对位地址定义为位变量名
sbit bit_name = sfr字节地址 ^ 位位置;
例如, sbit CY = 0xD0^7;
3)将SFR的相对位位置定义位变量名
sbit bit_name = sfr_name ^ 位位置;
例如, sbit CY = PSW^7;
C51编译器在头文件“REG51.H”中定义了全部sfr/sfr16和sbit变量。
用一条预处理命令#include <REG51.H>把这个头文件包含到C51程序中,无需重新定义即可直接使用它们的名称。
编程举例:
#include<REG51.h> //51单片机头文件
void delay(); //延时函数
sbit p1_0 = P1^0; //输出端口定义
main() //主函数
{
while (1) //无限循环体
{
p1_0 = 0; //P1.0 = 0,led亮
delay(); //延时
p1_0 = 1;
delay();
}
}
void delay() //延时函数
{
unsigned char i;//字符型变量i定义
for ( i = 200; i > 0; i--); //循环延时
}
【存储种类】 数据类型 【存储类型】 变量名
存储类型体现了变量的存放区域。51系列单片机共有6个存储类型(分布在3个逻辑存储空间中)。
不同存储类型的特点
三种编译模式分别对应于三种缺省存储类型:
约定:若无特殊声明,一般均为“SMALL编译模式”
【存储种类】 数据类型 【存储类型】 变量名
变量名可以由字母、数字和下划线三种字符组成,且第一个字符必须为字母或下划线,变量名长度随编译系统而定。
变量名具有字母大小写的敏感性,如SUM和sum代表不同的变量。
强调:头文件中定义的变量都是大写的,若程序采取小写变量则需要重新定义。
变量名不得使用标准C语言和C51语言的关键字。
数据结构定义举例
//定义system_status为无符号字符型自动变量,该变量位于data区中且初值为0。
unsigned char bdata status_byte;
//定义status_byte为无符号字符型自动变量,该变量位于bdata区
unsigned int code unit_id[2]={0x1234, 0x89ab};
//定义unit_id[2]为无符号整型自动变量,该变量位于code区中,是长度为2的数组,且初值为0x1234和0x89ab。
static char m, n;
//定义m和n为2个位于data区中的有符号字符型静态变量。
2. C51的指针
C语言指针的一般定义形式为:
数据类型 *指针变量名 [= &被指向变量名];
其中,指针变量指向一个由“数据类型”说明的变量。被指向变量和指针变量都位于C编译器默认的内存区中。
例如: int a =’A’;
int *p1= &a;
这表示p1是一个指向int型变量的指针变量,此时p1的值为int型变量a的地址,而a和p1两个变量都位于C编译器默认的内存区中。
对于C51,除了数据类型外,指针定义中还应能说明:
1)指针变量自身位于哪个存储区中?
2)被指向变量位于哪个存储区中?
C51指针的一般定义形式:
数据类型 [存储类型1] * [存储类型2] 变量名 [=&被指向变量名];
数据类型——被指向变量的类型,如int型或char型
存储类型1——被指向变量所在的存储区,缺省时由地址赋值关系决定
存储类型2——指针变量所在的存储区,缺省时为编译器默认的存储区
例1 若采用SMALL编译模式,试解释下述定义的含义。
char xdata a = ‘A’;
char *ptr = &a;
数据类型 [存储类型1] * [存储类型2] 变量名 [=&被指向变量名];
解:ptr是一个指向char型变量的指针,它本身位于SMALL编译模式默认的data存储区里,此时它指向位于xdata存储区里的char型变量a的地址。
解:以char *ptr形式定义的指针变量,既可指向位于xdata存储区的char型变量a的地址,也可指向位于idata存储区的char型变量b的地址(由赋值操作关系决定)。
例3:试解释以下指针定义的含义
char xdata a = ‘A’;
char xdata *ptr = &a;
【解】ptr是位于data存储区且固定指向xdata存储区的char型变量的指针变量,此时ptr的值为变量a的地址(不能像例2那样再将idata存储区的char型变量b的地址赋予ptr)。
例4:试解释以下指针定义的含义
char xdata a = ‘A’;
char xdata *idata ptr = &a;
【解】ptr是固定指向xdata存储区的char型变量的指针变量, 它自身存放在idata存储区中,此时ptr指向位于xdata存储区中的char型变量a的地址。
4.3 C51与汇编的混合编程
汇编语言特点:
优点:执行速度快、效率高、实时性强、与硬件结合紧密。
缺点:编程难度大、可读性差,不便于移植、开发时间长
C语言特点
优点:编程容易、可移植性强、支持多种数据类型,能直接对硬件进行操作,效率高。
缺点:实时处理弱于汇编语言,无法准确定时。
混合编程特点:
程序框架或主体部分用C语言编写,对那些使用频率高、要求执行效率高、延时精确的部分用汇编语言编写,这样既可保证整个程序的可读性,又可保证单片机应用系统的性能。
4.4 C51仿真开发方法
4.4.1 C51程序编译
与汇编语言程序相似的是,C51语言编写的源程序也不能直接被单片机识别,必须转换成固件程序(firmware),又称为目标代码程序后才能被执行。
C51程序的编辑、编译和仿真运行也需要借助Proteus中的Source Code标签页才能。
实例1 循环流水灯
在第3章实例13电路基础上改用C51编程,实现其流水灯循环功能
【解】本实例的C51编程思路与汇编语言基本相同,只要编写循环右移和循环左移的自定义函数即可,但为简化编程工作量也可以直接使用C51系统的库函数。
可以看出,_cror_函数具有将低位移出值补到高位的功能。该函数有两个无符号字符型的形参,前者用来存放被移位的数据,后者用来存放移位次数,函数返回值是无符号字符型。由此可知,利用P2 =_cror_(P2,1)语句便可得到循环右移一位的结果。同理,利用P2 =_crol_(P2,1)语句可得到循环左移一位的结果。
还要指出的是,调用_cror_库函数需要在源程序开头处添加一条预处理命令“#include <intrins.h>”。
实例1源程序
//实例1 循环流水灯
#include<reg51.h> //包含reg51的头文件
#include<intrins.h> //包含移位库函数的头文件
void delay(void) //定义延时函数
{
unsigned char i,j;
for(i = 1;i <= 50;i++)
for(j = 1;j<=150;j++);
}
void main()
{
unsigned char i;
P2 = 0xfe; //P2初值,对应于D1亮其余灭
delay(); //延时
while(1) //无限循环
{
for(i = 1;i<=7;i++) //由上而下流动
{
P2 = _crol_(P2,1);//调用左循环移位库函数将P2左循环1位
delay();
}
for(i = 1;i<=7;i++)
{
P2 = _cror_(P2,1);//调用右循环移位库函数将P2右循环1位
delay();
}
}
}
C51源程序的规范写法:
缩进是通过键盘的Tab键实现的,缩进可以使程序更有层次感。缩进原则是:如果地位相等,则不需要缩进;如果属于某一个代码的内部代码就需要缩进。
对齐主要是针对大括号{}而言的,{和}分别都要独占一行。互为一对的{和}要位于同一列,并且与引用它们的语句左对齐。另外,{}之内的代码要向内缩进一个Tab,且同一地位的要左对齐,地位不同的继续缩进。
重视注释语言的作用,根据软件工程的思想,注释要占整个文档的20%以上。所以注释要写得很详细,而且格式要写得很规范。格式虽然不会影响程序的功能,但会影响可读性,出错了查错也会很方便。
4.5 通用I/O口的简单应用
并行I/O口是51单片机基本结构中的重要组成部分,共有P0、P1、P2和P3四个8位端口。
单片机与外设的连接有两种方式,一是采用通用I/O口方式(以下简称为I/O口方式),二是采用片外总线方式。
前者的接口原理比较简单,应用容易实现,但受I/O口线数量限制只能用于少量外设的场合。
后者的接口原理相对复杂,通常需要外围器件配合才能连接外设,但可以节省I/O口线,便于外设扩展。
本节和下节介绍通用I/O口方式及C51编程应用,总线方式应用将在第8章中介绍。
4.1.1 基本输入/输出设备与应用
基本输出设备:发光二极管(Light Emitting Diode)
基本输入设备:按钮(Button)或开关(Switch)
P0口的两种输入接口电路有差别
有上拉电阻时,可以检测到1和0两种状态,其中按键未按下为1,按下为0;
无上拉电阻时,只能检测到是否为0状态,其中按键未按下为不确定状态,按下为0。
P1~P3口:
实例2 独立按键识别
【要求】采用独立按键方式实现下述功能:开机时LED全熄,然后根据按键动作使相应灯亮,并将亮灯状态保持到按压其它键时为止。
【解题分析】
由于P0口高4位引脚空置,电平为不确定值。为在读取P0口时能得到一个仅与按键状态有关的读入值,需要将高4位强制为0,为此可对读取的P0口值进行与操作,即key = P0 & 0x0f,使P0高4位始终为0。
为避免将按键释放后读到的P0值写入P2口,可以利用语句if (key!= 0x0f ) P2=key,仅在低4位读入值不为0x0f时才向P2输出P0状态值,这样就能保持先前的亮灯状态,直至有新的按键压下时才刷新显示。
实例2源程序
//实例2 独立按键识别
#include<REG51.H>
void main()
{
char key = 0;
while(1)
{
key = P0 & 0x0f;
if(key!= 0x0f) P2 = key;
}
}
实例3 键控流水灯
【要求】在实例2电路图的基础上,实现以下功能:
K1为“启动键”,首次按压K1可产生“自下向上” 的流水灯运动;
K2 为“停止键”,按压K2可终止流水灯的运动(全部灭灯);
K3和K4为“方向键”,分别产生 “自上向下”和 “自下向上” 运动。
思路分析:
①通过读取键值引导程序进行分支控制。需要设立两个可根据键值修改的标志变量,然后再根据标志变量的组合关系控制流水灯的流向与启停。
②流水灯的控制:提前将4种亮灯花样数据作为数组元素存入数组led中,然后再利用下标法依次调用。
花样数据: {0xfe,0xfd,0xfb,0xf7}
获取按键状态
修改方向和启停标志值
D1~D4循环方向控制
实例3源程序
4.1.2 数码管原理与静态显示应用
LED显示元件——人机交互输出设备,其作用是指示中间运行结果与运行状态,具有显示亮度高,响应速度快的特点。
七段式LED数码管(Proteus:7-Segment Display)
字符的显示码或字模与数码管的类型有关
实例5 LED数码管显示
图示为一个8位数码管显示电路,其中80C51单片机P0口的引脚与共阴极数码管的段码引脚相连。要求编程实现循环显示0~9字符,时间间隔为500循环步的功能。
分析:
数码管的显示字符与显示字模之间没有特别的规律可循。通常的做法是:将显示字模按显示字符代表的数值大小顺序存入一字符数组中,例如字符0~9的共阴极显示字模的数组
led_mod[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}
使用时,只需用待显示值作为下标变量调用该数组,即可取得相应的字模。本例中只要提取出0~9的显示字模并送P0口输出,便可实现题意要求的功能。
实例5源程序
实例6 计数显示器
下图为2位计数显示器的电路原理图
要求:数码管的显示初值为0,单击按键后,按增量1进行累加,累加值实时显示在数码管上。当累加值达到99后清零重新开始计数,如此无限循环。
解题分析:
只要设置一个按键闭合次数变量count,并将其值送到P0和P2口即可实现题意要求。
两个关键问题需要关注:
①按键的处理问题
按键通常为机械式弹性开关。当机械触点断开、闭合时,由于触点的弹性作用,按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,抖动造成电压的波动。
显然按键抖动会造成难以判断按键闭合状态的问题
按键消抖最简单的方法是软件消抖法,即当检测到有键按下时,先用软件延时10ms,然后再次检测按键的状态。若仍是闭合状态电平,则可认为是真正有键按下。反之则应作为误判处理。同理,按键释放时的检测也需做类似的处理。
虽然电路仿真时不可能有按键抖动问题,但在程序设计时还是应该按实际电路的消抖考虑。
为避免按键在压下期间被连续统计,确保一次点击仅能被统计一次,计数值应该在按键先被压下然后又被释放之后才能更新。
②计数值的拆分显示原理
为使计数器变量count中的两位十进制数能分别显示在两只数码管上,需要将计数值先进行拆分再送交显示。
拆分原理:
将count用取模运算(count%10)拆出个位值,用整除运算(count/10)拆出十位值。
P2 = table[count%10];
P0 = table[count/10];
实例6源程序
4.6 通用I/O口的进阶应用
4.6.1 数码管动态显示原理与应用
两种显示接口:静态显示接口和动态显示接口
动态显示编程原理:
快速(如10ms)切换段码值和位码值,使每一时刻只有一只数码管被驱动。利用视力暂留特性,可获得连续显示效果。
优点:占用IO口资源较少(节省空间)
缺点:需要CPU不断进行干预(占用机时)
实例7 数码管动态显示
图为采用共阴极LED数码管的电路原理图,要求采用动态显示原理显示字符“L2”。
图中双联LED数码管是Proteus提供的控件模型,相当于段码位在内部做了并联,而位码位独立接出的两只数码管。
分析:
Proteus中的双联LED数码管相当于两个并联的数码管。
L2的显示方法
将位码0x02和0x01先后送入P3口,可依次使能左、右两个数码管。此时若将0x38和0x5b两个段码(显示字模)依次送到P2口,便可产生“L2”的动态显示效果。
实例7 源程序
4.2.2 行列式键盘原理与应用
独立式键盘的电路简单,易于编程,但占用的I/O口线较多,当需要较多按键时可能产生IO口资源紧张问题。
行列式键盘——将I/O口分为行线和列线,按键跨接在行线和列线上,列线通过上拉电阻接正电源。
行列式键盘编程原理(以P2口接4×4键盘为例)
第二步 按键闭合状态判断
键值——按键闭合时从引脚读出的数值。
按键闭合前后,所在行线端口电平反转;
读P2后,若发现其低4位为f,说明无键压下;反之则相反。
如果 (P2 & 0x0f) = 0x0f →无键压下
如果 (P2 & 0x0f) ≠ 0x0f →有键压下
第三步 查找闭合键键号
实例8行列式键盘
4×4行列式键盘如下图所示
功能要求:开机黑屏→按下任意按键后,数码管上显示该键的键号(0~F)→若没有新键按下,维持前次按键结果。
实例8源程序
实例9 1位密码锁
【功能要求】:在4X4行列式键盘基础上实现1位密码锁的如下功能:
解题分析:
根据任务要求,硬件系统中可以用一位共阴极LED数码管作为显示器件,采用静态连接方式;16个按键采用4×4矩阵键盘连接方式;一位共阴极发光二极管作为密码锁开锁开关。
源程序设计思路:
小结
1. C51变量的一般定义形式为:
〔存储种类〕 数据类型 〔存储类型〕 变量名;
l 存储种类包括auto、extern、static和register 4个说明符,缺省时为auto型。
l 常用数据类型为char和int,C51扩充类型为bit、sfr、sfr16和sbit。
l 存储类型包括data、bdata、idata、pdata、xdata和code 6个具体类型,缺省类型由编译模式指定。
l 变量名可由字母、数字和下画线3种字符组成,首字符应为字母或下画线。
2.C51指针的一般定义形式为:
数据类型 〔存储类型1〕 * 〔存储类型2〕 指针变量名;
l 数据类型是被指向变量的数据类型。
l 存储类型1是被指向变量的存储类型,缺省时需根据该变量的定义确定。
l 存储类型2是指针变量的存储类型,缺省时根据C51编译模式确定。
l 变量名可由字母、数字和下画线3种字符组成,首字符应为字母或下画线。