正文

MCS-51单片机的C语言编程2010-03-05 19:45:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/niao0311/50728.html

分享到:

§3-1 C语言与MCS-51
一、C语言特点 C语言是一种编译型程序设计语言,它兼顾了多种高级语言的特点,并具备汇编语言的某些特点,用C语言进行程序设计已经成为软件开发的一个主流。单片机系统的开发也适应了这个潮流。与汇编语言相比,用C语言开发单片机具有如下特点:

  1. 开发速度优于汇编语言;
  2. 软件的可读性和可维护性显著改善;
  3. 提供了库函数包含许多标准子程序,具有较强的数据处理能力;
  4. 关键字及控制转移方式更接近人的思维方式;
  5. 方便进行多人联合开发,进行模块化软件设计;
  6. C语言本身并不依赖于机器硬件系统,移植方便;
  7. 适合运行嵌入式实时操作系统;

对于MCS-51单片机的C语言:

  1. 针对8051的特点对标准的C语言进行扩展。
  2. 对单片机的指令系统不要求十分了解,只要对8051单片机的存储结构了初步了解,就可以编写出应用软件。
  3. 寄存器的分配、不同存储器的寻址及数据类型等细节由编译器管理。

用C语言编写的应用程序必须经单片机的C语言编译器(简称C51)转换生成单片机可执行代码程序。支持MCS-51系列单片机的C语言编译器有很多种。如American Automation、Auoect、Bso/Tasking、KEIL等等。其中德国KEIL公司的C51编译器在代码生成方面领先,可产生最少代码,它支持浮点和长整数、重入和递归,使用非常方便。本章针对这种被广泛应用的KEIL C51编译器,介绍MCS-51单片机C语言的程序设计。
二、C51程序的开发过程
用C语言编写单片机应用程序和编写标准的C语言程序的不同之处,在于根据单片机的存储结构及内部资源定义C语言中的数据类型和变量,其他的语法规定、程序结构及程序设计方法与标准的C语言相同,所以在后面的几节中主要介绍如何定义C51中的变量的数据类型、存储类型、特殊功能寄存器以及中断函数,与标准C相同的部分就不再 述。
C51的开发过程和用其它语言包括汇编语言开发没有什么不同,其开发流程见下图:
例1:如图所示,P1口连接8只发光二极管,要求每隔0.5秒移动一次,当P2.0为高时,发光二极管左移,否则右移。
#include <reg51.h> //标准的8051头文件,定义了所有的SFR
#include<intrins.h> // 内部函数包含到程序中,
#define uchar unsigned char
#define uint unsigned int
sbit KEY =P2^0; //定义P2.0为开关输入
void delay(void) //软件延时函数
{ uint t;
for(t=0;t<30000;t++); //空循环延时,大约0.5秒左右
}
void main(void)
{ uchar data led;
led=0xfe; //低电平点亮发光管,初始值对应最低位P1.0为低,即L0点亮
while(1)
{ P1=led; //将led送到P1口
delay(); //延时0.5秒
if(KEY) //如果KEY为1(开关断开),则变量led循环左移一位
led=_crol_(led,1);
else
led=_cror_(led,1); //否则led循环右移一位
}
}
这是一个完整的C51源程序,注意主函数中由while(1)所构成的死循环,因为单片机中没有其它软件,也就没有PC机C语言中所谓的退到DOS或WINDOWS的概念,如果程序中没有这样的死循环,程序执行完最后一条语名,随后的结果将不可预计。
§3-2数据与数据类型
无论我们学习哪一种语言,首先遇到的是数据类型, C51共有以下几种数据类型:
位型:bit
字符型:char
整型:int
基本类型 长整型:long
浮点型:float
双精度浮点型:double
数组类型: array
数据类型 构造类型 结构体类型: struct
共用体 : union
指针类型 枚举: enum
空类型
KEIL C51所支持的基本数据说型说明如下:


数据类型

长度(bit)

长度(Byte)

值域范围

bit

1

 

0,1

unsigned char

8

1

0~255

signed char

8

1

-128 ~ 127

unsigned int

16

2

0 ~65535

signed

16

2

-32768 ~ 32767

unsigned long

32

4

0~4 294 967 295

uigned long

32

4

-2147483648~2147483647

float

32

4

±1.176E-38~±3.40E+38

double

64

8

±1.176E-38~±3.40E+38

一般指针

24

3

存储类型(1字节)偏移量(2字节)

有了这些数据类型,我们用变量去描述一个现实中的数据时,就应按需选择变量类型。对于C51来讲,不管采用哪一种数据类型,虽然源程序看起来是一样的,但最终形成的目标代码却大相径庭,其效率相并非常大。
例如:当我们去表示时间量秒的时候,虽然可用unsigned int类型甚到double类型,但由于秒的取值范围是0~59,所以采用unsigned char就够了。这样不仅节省了存贮空间,而且还可以提高程序的运行速度。因此我们在编程时应按照变量可能的取值范围、精度要求去选择恰当的数据类型。
另外,如果不涉及负数运算,要尽量采用无符号类型,这样可以提高编译后目标代码的效率。我们编程时最常用到的时无符号数运算,因此为了编程时书写的方便,我们可以采用简化的缩写形式来定义变量的数据类型。其方式是在源程序的开始处加上下面两条语句:
#define uchar unsigned char
#define uint unsigned int
这样在定义变量时,就可以使用 uchar uint 来代替 unsigned char和signed char。

§3-3 C51的数据存贮类型与8051存贮器结构
8051系列单片机将程序存贮器(ROM)和数据存贮器(RAM)分开,并有各自的寻址机构和寻址方式。8051单片机在物理上有四个存贮空间:
片内程序存贮器空间:0000—0FFF
片外程序存贮器空间:1000—FFFF(/EA=1) 0000—FFFF(EA=0)
片内数据存贮器空间:

  1. 1F:通用工作寄存器区

20—2F:位寻址空间
30—7F:用户RAM区
80—FF:特殊功能寄存器区
片外数据存贮器空间:0000—FFFF
我们采用汇编编语言编程时,是按地址去读写指定的存储单元的,用不同的指令去表示不同的存储空间,例如:MOV指令访问片内数据存储器,MOVX指令访问片外数据存储器,MOVC指令访问程序存储器。而在C51中直接使用变量名去防问存储单元,而无需关心变量的存放地址,程序的可读性大大增加了。但变量放在哪一个存贮空间呢?这对最终目标代码的效率影响很大。因此在编程时除了说明变量的数据类型外,还应说明变量所在的存储空间即存储类型。
C51将变量、常量定义成不同的存贮类型,以完全支持8051单片机的存贮器结构。
C51 存储类型与8051存储空间的对应关系


存贮类型

与存贮空间的对应关系

data

直接寻址片内数据存贮区,速度快(00—7F)

bdata

可位寻址片内数据存贮区,允许位/字节混合访问(20—2F)

idata

间接寻址片内数据存贮区,可访问全部RAM空间(00—FF)由MOV @Ri访问

pdata

分页寻址片外数据存贮区(256字节),用MOVX @Ri访问

xdata

片外数据存贮区(64字节),用MOVX @DPTR访问

code

代码存贮区(64K),由MOVC @DPTR访问

C51中变量定义的格式:
数据类型 [存储类型] 变量名1 [,变量名2]……[,变量名n]
例:
char data temp; //字符变量temp,定位在片内数存贮区,用直接寻址方式访问。
bit bdata flags; //位变量flags,定位在可位寻址片内数据存贮区。
uchar bdata speed; //无符号字符变量speed ,定位在可位寻址片内数据存贮区
uchar idata len; //无符号字符变量len ,定位在片内数据存贮区,用间接寻址
uchar code seg[ ]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d};
//定义无符号字符型数组seg,共有10个元表,存放在程序存储器中。
说明: 选择变量的存储类型时,可按以下的原则:
通常将一些固定不变的参数或表格放在程序存储器中,即存储类型设为code。例如上面所定义的seg数组就是用来存放LED数码管的字段码的。
访问片内数据存储器(存储类型为data/bdata/idata)比访问片外数据存储器的速度要快,因此对一些使用频率较高的变量或者对速度要求较高的程序中的变量可选择片内数据存储器,而将一些不常使用的变量放存片外数据存储器(存储类型为pdata/ xdata)中。当然如果系统中所用的变量较少,片内数据存储器的空间足够应付时,就无需使用片外数据存储器了 。
对于52子系列的单片机,其内部RAM是256字节,其高128字节的地址从80H~FFH,正好与特殊功能寄存器地址重叠,两者通过寻址方式加以区别,对于52子系列的高128字节的RAM,必须用R0、R1寄存器间接寻址方式访问,而对特殊功能寄存器只能用直接寻址方式访问。也就是说如果希望在C51中对80H~0FFH之间的数据存储器进行读写,可以将变量的存储类型定义为idata。
通常将位变量或希望位与字节混合访问的变量的存储类型设置为bdata。但要注意,不能定义指向指向位型的指针,也不能定义位数组。例如:下面两条定义语名时错误的。
bit bdata *pb;
bit bdata status[8];
如果变量定义时省去存贮类型说明,编译时会自动选择默认的存储类型,而默认的存贮类型由存贮模式确定。在C51中有SMALL、COMPACT、LARGE三种存储模式,在KEIL环境中,可以通过目标工具选项设置选择所需的存储模式。下面分别对这三种模式进行说明:
存贮模式作为编译选项如下表所示:


存贮模式

说明

 

SMALL

参数和局部变量放入可直接寻址的内部数据存贮器(最大128字节,默认的存储类型为DATA),速度快,访问方便。所用堆栈在片内RAM。

CPMPACT

参数和局部变量放入分页外部数据存贮器(最大256字节,默认的存储类型为PDATA),通过MOVX @Ri指令间接寻址,所用堆栈在片内RAM。

 

LARGE

参数和局部变量直接放入外部数据存贮器(最大64KB,默认的存储类型为XDATA),通过MOVX @DPTR指令进行访问,所形成的目标代码效率低。

例如,有一程序如下:
#define uchar unsigned char
void main(void)
{ uchar b; //由于省去了存储类型,C51会根据当前的选择存贮模式来确定b的存储类型。
b=12;
}
如果当前的存贮模式为SMALL,编译后所形成的目标代码为:
MOV 08H,#0CH ;这里08H为变量b在片内RAM的地址,#0CH即为12。
如果当前的存贮模式为CPMPACT,编译后所形成的目标代码为:
MOV R0,#00H ;这里00H为变量b在片外RAM的页面地址,#0CH即为12。
MOV A,#0CH
MOVX @R0,A
如果当前的存贮模式为LARGE,编译后所形成的目标代码为:
MOV DPTR,#0000H ;这里0000H为变量b在片外RAM的地址,#0CH即为12
MOV A,#0CH
MOVX @DPTR,A

由此可见,在存储类型缺省的情况下,由当前的存贮模式确定变量的存储类型,初学者使用时一定要小心!例如,你的单片机系统并没有扩充外部数据存贮器,当前的存储模式为LARGE,而且一些变量又没有选择存储类型,这样在调试时会出现一些意想不到的错误。


教学目的:掌握8051特殊功能寄存器、并行接口、位变量的C51定义,C51 内部函数及常用的宏。
教学难点:可位寻址对象的定义及实际应用中的技巧,关键字sbit与bit的差别。

§3-4 8051特殊功能寄存器及其C51定义
通过对变量的存储类型的定义,我们可以通过变量访问MCS-51系列单片机的各类存储器,但又通过什么方法去访问它的特殊功能寄存器呢?

一、对特殊功能寄存器的访问
8051单片机内有21个特殊功能寄存器(SFR),分散在片内RAM的高端,地址在80H—0FF间,对它们的操作,只能用直接寻址方式。为了能够直接访问21个特殊功能寄存器(SFR),C51提供了一种自主形式的定义方法。
格式: sfr SFR =SFR地址;
例: sfr TMOD=0x89; //定时器方式寄存器的地址是89H
sfr TL0=0x8A; //定时器TL0的地址是8AH
一般程序设计时,将所有特殊功能寄存器的定义放在一个头文件中,在程序的开始处用#include <头文件名>指明一下,在随后的程序中即可引用。
例: TMOD=0X12; //将定时器0设置为方式2,定时器1设置为方式1
TL0=0X50; //将时间常数50H赋给TL0。
在C51中,对所有特殊功能寄存器的定义已放在一个头文件REG51.H中。因此只要在程序的开始处加上#include <reg51.h>语句,即可在C51中按名访问所有的特殊功能寄存器,无需用户再用sfr定义。

二、对于SFR16位数据的访问:
16位寄存器的高8位地址位于低8位地址之后,为了有效地访问这类寄存器,可使用如下格式定义:
sfr16 16SFR = 8SFR地址
例: sfr16 DPTR=0x82; //DPTR由DPH、DPL两个8位寄存器组成,其中DPL的地址为82H
……
DPTR=0X1234; //将立即数1234H传送给DPTR,相当于 MOV DPTR,#1234H

三、SFR中的某位进行访问
MCS-51单片机的特殊功能寄存器中,有11个寄存器的共83位具有位寻址能力,特点时其寄存器的地址为8的倍数。 C51通过特殊位(sbit)定义,可以实现对这些特殊功能寄存器的83个位直接进行访问:
方法1:
sbit SFR位名 = SFR^i (i=07)
例: sfr TCON=0X88; //定义TCON寄存器的地址为0X88
sbit TR0=TCON^4; //定义TR0位为TCON.4,地址为0x8c
sbit TR1=TCON^6; //定义TR1位为TCON.6,地址为0x8e
方法2:
sbit SFR位名 = SFR地址^I(i=07)
例: sbit TR0=0x88^4; //定义TR0的位地址为0x8c
sbit TR1=0x88^6; //定义TR1位位地址为0x8e
方法3:
sbit SFR位名 = 位地址; (位地址应在80HFFH之间)
例: sbit TR0=0x8C ; //定义TR0的位地址为0x8c
sbit TR1=0x8E ; //定义TR1位位地址为0x8e
有了以上定义,在随后的程序中就可以像访问位变量一样方便,例如:
TR0=1;//启动定时器0,相当于汇编中的指令:SETB TR0
TR1=0;//停止定时器1,相当于汇编中的指令:CLR TR0
同样,除了P0、P1、P2、P3口之外的所有特殊位在reg51.h已定义,因此只要在程序的开始处加上#include <reg51.h>语句,即可在C51中按名访问这些位,无需用户再用sbit定义。

四、可位寻址对象的定义
可位寻址对象指既可以字节寻址,又可位寻址的对象,位于片内RAM的20~2FH中。
一般先定义变量的数据类型,数据类型可以是字符型、整型、长整型等,其存贮器类型必须定义为bdata,然后使用sbit定义该变量中可单独寻址访问的位。
例:
char bdata state;
sbit state_7= state ^7; // ^后的最大值取决于变量的类型,char为7,int为15。
main()
{
state =5;
state_7=1; //结果 temp= 0X85;
}

例:从IC卡读取一个字节,其中,CLK为时钟线,IO为数据线。
unsigned char bdata ibase; //定义位寻址单元,用于发送与接收一个字节。
sbit mybit7 = ibase^7; //定义一个位
unsigned char Readchar(void)
{ unsigned char i;
for(i=0;i<8;i++)
{ CLK=0;
Delay5Us();
ibase=ibase>>1;
mybit7=IO;
CLK=1;
Delay5Us();;
}
return ibase;
}
§3-5 8051并行接口及其C51定义
一、片内并行口的定义
8051单片机带有4个8位并行口,即SFR中的P0、P1、P2、P3口,对它的定义在reg51.h已存在,可直接对其引用,例:
P2=0xFE; //将数据0xFE输出到P2口。
Key=P1; //从P1口输入数据到变量Key。
如果要单独对某位进行操作,可在程序的开头加上位寄存器定义,例如:
sbit P1_0= P1^0; //定义P1_0为P1口的第0位,
sbit P1_1= P1^1; //同上。
sbit P1_2= P1^2;
在随后的程序中即可对这些位进行访问。例如:
while(P1_0==1); //等待P1_0脚出现低电平。
对应的目标代码为:
JB P1.0 , $
假如P0、P1、P2、P3口的某些位是连接到外部电路的指定引脚的,可将这些引脚名作为位名,例:假如打印机的BUSY 引脚和P1.0相连,见下图所示,可以这样进行定义:
sbit BUSY= P1^0;
于是,前面的语句‘while(P1_0==1); ’就可改写为 ‘while(BUSY==1);’这样程序的可读性就增加了。

二、片外并行口的定义
对于MCS-51单片机外扩展的I/O口,例8255、8155等,则根据其硬件译码地址,将其视为片外数据存贮器的一个单元,使用#define 语名定义格式如下:
#define I/O口名称 XBYTE[I/O口地址]
其中,XBYTE表示绝对存储器访问的宏,在文件absacc.h中定义,方括号中[]中是存储器的绝对地址。在使用这种格式定义之前,应加上语句: #include <absacc.h>
例如:在系统中扩充中USB接口芯片,其数据口地址为0xBCF0,数据口地址为0xBCF0,可以这样定义:
#include <absacc.h>
#define CMDPORT XBYTE[0xBDF1] //将CMDPORT定义为外部I/O口,地址为0xffc0,长8位。
#define DATPORT XBYTE[0Xbdf0] //将DATPORT定义为外部I/O口,地址为0xffc0,长8位。
……
CMDPORT=0x21; //将0X21写入CMDPORT口
d=DATPORT; //从DATPORT口读数据到变量d

§3-6 C51的内部函数及常用的宏
C51运行库提供了100多个预定义函数和宏,用户可以在自已的C程序中使用这些函数和宏。

  1. 内部函数

c51编译器支持许多内部库函数,内部函数产生的在线嵌入代码比调用函数产生的代码相比,执行速度快,效率高。常用的内部数如下:
1、_crol_(v,n) : 将无符号字符变量v循环左移n位.
2、_cror_(v,n) : 将无符号字符变量v循环右移n位.
3、_irol_(v,n) : 将无符号整型变量v循环左移n位.
4、_iror_(v,n) : 将无符号整型变量v循环右移n位.
5、_lrol_(v,n) : 将无符号长整型变量v循环左移n位.
6、_lror_(v,n) : 将无符号长整型变量v循环右移n位.
7、_nop_() : 延时一个机器周期,相当于NOP指令。
以上内部函数的原型在INTRINS.H头文件中,为了使用这些函数,必须在程序开始时加上:#include <intrins.h>

二、绝对存储器访问宏
C51标准库包含了可以访问显式存储地址的宏,可以像使用数组一样使用这些宏:
1、CBYTE 允许用户访问程序存储器中指定地址单元。例:
ID=CBYTE[0X200]; 读取程序存储器地址为0X200单元的内容到变量ID
2、XBYTE 允许用户访问外部数据存储器中指定地址单元。例:
XBYTE[0X100]=D; 将变量D存入外部数据存储器地址为0X100的单元。
3、DBYTE 允许用户访问片内数据存储器中指定地址单元。
例:将片内RAM 30H单元开始的10个字节传送到片内数据存储器100H开始的区域。
for(n=0;n<10;n++)
XBYTE[0x100+c]=DBYTE[0x30+c];
以上宏在ABSACC.H文件中定义,为了使用这些宏,必须在程序开始时加上:
#include <absacc.h>
教学目的通过复习掌握MCS51中断系的功能及应用,掌握C51中断函数
的编写的一般方法。
教学难点:C51中断服务函数的定义及注意事项
§3-7 MCS-51单片机的中断及中断服务函数的定义
一、 MCS-51单片机的中断(复习)

  1. 断源和中断请求标志
    1. 中断源

外部中断:/INT0 、/INT1 定时器: TO、T1 串行口

    1. TCON寄存器

IT0、IT1 :外部中断的触发方式(1:负跳变 0:低电平)
IE0、IE1:/INT0、/INT1的中断请求标志。
TF0、TF1: 定时器T0、T1溢出时,置1。中断向应后,清0

    1. SCON寄存器

RI:串口收到一帧数据后,置1,必须在中断服务程序中由户清0。
T1:串口发完一帧数据后,置1,必须在中断服务程序中由户清0。

  1. IE中断允许控制寄存器

EA CPU中断允许标志 EA=1:开中断,否则关中断
EX0、EX1、ET0、ET1、ES:分别对应5个中断源是否允中断。
中断优先级控制
IP寄存器(复位后,IP=00H)
PX0、PX1、PT0、PT1、PS:分别对应5个中断源的优先级(1:高)
默认优先级 :/INT0→T0→/INT1→T1→串口

  1. 中断的入口地址

INT0 :0003H INT1:0013H T0:000BH T1:001BH 串行口:0023H

 

  1. 中断服务函数的定义

C51编译器支持在C语言源程序中直接编写8051单片机的中断服务函数程序,定义中断服务函数的一般形式如下:
函数类型 函数名([形式参数表] interrupt n [using n]
用汇编语言编写中断服务程序时,程序员必须将中断服务程序放在由中断向量所指定的位置。用C51编程时,只要指出相应的中断号,编译器会根据中断号产生中断向量,关键字 interrupt 后面的n就是中断号,取值范围为0~31,编译器从8n+3处产生中断向量,8051单片机的常用中断源和中断向量的关系如下:


n

中断源

中断向量8n+3

0

外中断0

03

1

定时器0

0B

2

外中断1

13

3

定时器1

1B

4

串行口

23

关键字using 后面n的取值范围为0~3,分别表示四组工作寄存器R0到R7,如不带该项,则由编译器选择一个寄存器组作为绝对寄存器组访问。
说明:

  1. 中断函数不能进行参数传递,如果中断函数中含有任何参数申明都将导致编译出错,同时也没有返回值,一般定义中断程序的类型为void。
  2. 函数名的选择与普通的函数一样,编译器是根据中断号而不是函数名来识别中断源的,但为了程序的可读性,可根据中断源来定义函数名,例如定时器TO的中断服程序可以这样定义:

void timer0(void) interrupt 1

三、C51中断函数编写举例
例1:每产生一中断,变量hour加1,加到24时清0,hour小于12时,点亮指示灯,否则熄灭指示灯
#include<reg51.h>
#include<absacc.h>
#define uchar unsigned char
uchar hour;
sbit LED=P1^0;
main()
{
hour=0;
LED=0;
EA=1;
EX0=1;
IT0=1;
while(1)
{ if (hour<12) LED=0;
else LED=1;
}
}
void key_int0() interrupt 0 using 1
{ hour++;
if (hour==24) hour=0;
}
对应中断函数反汇编代码如下:

举例:每隔1秒在P1.0引脚产生一方波,发光管闪烁一次

#include <reg51.h>
sbit P1_0=P1^0;
unsigned int t;
void timer0() interrupt 1 using 1
{ t++;
if(t==2000)
{ P1_0=!P1_0;
t=0;
}
}
void main(void)
{ TMOD=0x02;
TR0=1;
{ TH0=-250;
TL0=-250;
EA=1;
ET0=1;
TR0=1;
t=0;
do {} while(1);
}

 

例:用查询方式发送,中断方式接收8个字节。
#include<reg51.h>
#include<absacc.h>
#define uchar unsigned char
uchar s_buf[8]={8,9,10,11,12,13,14,15};
uchar k,j;
uchar r_buf[8];
void serial() interrupt 4 using 1
{ if (RI)
{ RI=0;
r_buf[k++]=SBUF;
}
}
main()
{
TMOD=0x20;
TL1=0xfd;
TH1=0xfd;
SCON=0xd8;
PCON=0x00;
TR1=1;
k=0;EA=1;ES=1;
for(j=0;j<8;j++)
{ SBUF=s_buf[0];
while(TI==0);
TI=0;
}
TR1=0;
while(k<8);
While(1);
}


教学目的:了解I2C总线的基本结构特点、应用,掌握I2C总线协议。
教学难点:I2C总线数器件地址的定义及I2C总线时序要求。

第四章 I2C总线及其应用
在新一代单片机中,无论总线型还是非总线型单片机,为了简化系统结构,提高系统的可靠性,都推出了芯片间的串行数据传输技术,设置了芯片间的串行传输接口或串行总线。串行总线扩展接线灵活,极易形成用户的模块化结构,同时将大大简化其系统结构。串行器件不仅占用很少的资源和I/O线,而且体积大大缩小,同时还具有工作电压宽,抗干扰能力强,功耗低,数据不宜丢失和支持在线编程等特点。目前,各式各样的串行接口器件层出不穷,如:串行EEPROM,串行ADC/DAC,串行时钟芯片,串行数字电位器等等。
§4-1 I2C总线概述
I2C总线是PHLIPS公司推出的一种高性能芯片间串行传输总线,数据传输时只需两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有连接到I2C总线上的设备,其串行数据线都连接到总线的SDA上,而各设备的时钟线均连接到总线SCL线上,。

I2C总线是一个多主总线,即一个I2C总线上可以有一个或多个主机,总线运行由主机控制。所谓主机是指启动数据传送,发出时钟信号,传输结束时发出终止信号的设备。通常主机由单片机或其它微处理器担当。在多主机系统中,可能同时有几个主机企图启动总线传送数据。为了防止混乱,保证数据的可靠传送,任一时刻总线只能由某一主机控制。被主机寻访的设备叫从机,它可以是单片机或其它微处理器,也可以是其它器件,如存储器、A/D或D/A转换器、数字电位器等。I2C总线的基本结构如下图所示。
实际应用中,多数单片机系统使用的是单主结构形式,即挂在总线上的设备只有一个主机,其它设备均是从机。在这种方式下,I2C总线数据的传输比较简单,没有总线的竞争,只存在单片机对I2C总线器件的读(单片机接收)、写(单片机发送)操作。此时我们可以使用不带I2C总线接口的单片机,如8051,AT89C2051等作为主机,利用这些单片机的普通I/O口完全可以实现主机对I2C总线器件的读写操作。采用的方法就是利用软件实现I2C总线的数据传送,即软件与硬件结合的信号模拟。

§4-2 I2C总线协议介绍

    1. 总线上数据的有效性

在I2C总线上进行数据传送时,每一位数据位都与时钟脉冲相对应,在时钟信号为高电平期间,数据线上必须保持稳定的逻辑电平状态,高电平表示数据1,低电平表示数据0。只有在时钟线为低电平时,才允许数据线的电平发生变化。
2、起始信号
当SCL为高时,SDA由高电平到低电平的跳变作为I2C总线的起始信号。起始信号表明一次数据传送的开始。在起始信号产生后,总线就处于被占用状态。
3、停止信号
当SCL为高时,SDA从低电平到高电平的跳变作为I2C总线的停止信号。在终止信号产后一定时间后,总线就处于空闭状态。
4、应答信号
I2C 总线数据传送时,每次传送一个字节数据后,接收器都必须产生一个应答信号,应答的器件在第9 个时钟周期时将SDA 线拉低,表示其已收到一个8 位数据。
利用I2C总线进行数据传送时,传送的字节数是没有限制的,但是每一个字节必须保证是8位长度,并且首先发送数据的最高位,每传送一个字节数据后都必须跟随一位应答信号,与应答信号相应的时钟由主机产生,主机必须在这一时钟位上释放数据线,使其处于高电平状态,以便从机在这一位上送出应答信号。应答信号在第9个时钟位上出现,从机输出低电平变应答信号(A),表示继续接收,若从机输出高电平则为非应答信号(/A),表示结束接收。
如果主机接收数据时,它收到最后一个数据字节后,必须向从机发送一个非应答信号(/A),使从机释放SDA线,以便主机产生终止信号,从而停止数据传送。
5、器件地址
I2C总线上的每一个从机均有一个唯一的地址,每次主机发出起始信号后,必须接着发出一个字节的地址信息,以选取挂在总线上的某一从机。地址信息的格式如下:
其中D7-D0位表示从机的地址,D0位是数据传送方向,为0时,表示主机向从机发送数据(写),为1时,表示主机由从机处读取数据。
主机发送地址时,总线上的每一个从机都将这7位地址码与自已的器件地址进行比较,如果相同则认为自已正被主机寻址,根据读写位将自已确定为发送器或接收器。
从机的地址由一个固定部分和一个可编程部分组成。固定部分为器件的编号地址,表明了器件的类型,出厂时固定的。可编程部分为器件的引脚地址,视硬件接线而定。


1

0

1

0

A2

A1

A0

R/W

例:24C02的地址格式如下:

 

其中高四位1010为器件标识类型。
A2~A0:引脚地址,对应于该芯片引脚A2~A0的取值,当A2-A0引脚均接低电平时,该器件的地址为A0H或A1H,如果为A0H表示写数据到该器件,A1H表示从该器件读数据。

说明:从机地址只表明选择挂在总线的哪一个器件及传送方向,而器件内部的地址是由编程者传送的第一数据中指定的,即第一个数据为器件内的子地址。
6I2C总线数据传送的时序要求
为了保证数据传送的可靠性,标准的I2C总线数据传送有着严格的时序要求,用普通I/O线模拟I2C总线数据传送时,必须遵从定时规范,下图单独绘出了启动、停止、应答信号、非应答信号的时序规范。


教学目的: 根据I2C总线协议,掌握典型信号模拟函数的设计方法。
掌握I2C总线器件24C02及其应用
教学难点:向有子地址器件发送或读写多个字节数据的格式及相应函数
§4-3 典型信号模拟函数
设采用80C51单片机,晶振频率为12MHZ,即机器周期为1us,使用P1.0作为数据线SDA, P1.1作为时钟线SCL,首先在源程序开始处进行如下定义:
#include <reg51.h> /*头文件的包含*/
#include <intrins.h>
#define uchar unsigned char
#define uint unsigned int
#define NOP _nop_();_nop_() ;_nop_();_nop_();_nop_()
/*定义空指令,NOP相当于延时5us*/
sbit SDA=P1^0; /*模拟I2C数据传送位*/
sbit SCL=P1^1; /*模拟I2C时钟控制位*/
bit ack_mk; /*应答标志位*/
根据I2C总线数据传送的典型信号时序要求,模拟信号启动、停止、发送应答信号、非应答信号的函数如下:
1、启动总线函数
函数原型: void Start();
功能: 启动I2C总线,即发送起始信号.
void Start() //启动I2C总线,即发送I2C起始条件.
{
SDA=1; /*先将SDA、SCL置为1*/
SCL=1;
NOP; /*因起始条件建立时间大于4.7us,故延时5us */
SDA=0; /*在SCL为高时,SDA由高变低,产生起始信号*/
NOP; /*延时5us */
SCL=0; /*SCL变低电平,准备发送或接收数据 */
}
2、结束总线函数
函数原型: void stop();
功能: 结束I2C总线,即发送I2C结束信号.
void stop()
{
SDA=0; /*将SDA清0, SCL置1*/
SCL=1;
NOP; /*结束条件建立时间大于4μs*,所以延时5us*/
SDA=1; /*当SCL为高电平时,SDA由低变高,产生I2C总线结束信号*/
NOP; /*延时5us*/
SCL=0;
}

3 发送应答位函数
原型: void Ack_I2c(void);
功能:向从器件发送应答信号
void Ack(void)
{
SDA=0; /*SDA先清0,发应答信号 */
SCL=1; /*SCL由低变高,产生一个时钟*/
NOP; /*延时5μs*/
SCL=0; /*时钟线SCL清恢复到低电平,以便继续接收*/
}
4、发送非应答位函数
原型: void NAck_I2c(void);
功能:向从器件发送非应答信号
void NAck(void)
{
SDA=1; /*SDA先置1,发非应答信号 */
SCL=1; /*SCL由低变高,产生一个时钟*/
NOP; /*延时5μs*/
SCL=0; /*时钟线SCL清恢复到低电平,以便继续接收*/
}
I2C总线数据模拟传送除了上述基本的启动、停止、发送应答位、发送非应答位之外,还需要发送一个字节数据、接收一个字节数据、发送n个字节数据、接收n个字节数据的函数。

5、向I2C总线发送一个字节
函数原型: void SendByte(uchar c);
功能: 将数据c发送出去, c可以是从器件地址或器件的子地址,也可以是数据,发完后等待应答,如果没有应答,ack_mk=0,表示发送失败,否则 ack_mk=1,发送成功。
void SendByte(uchar c)
{
uchar n ;
for(n=0;n<8;n++) /*一字节为8位,循环8次,先送高位,后送低位*/
{
if(c&0x80) SDA=1; /*根据发送位将数据线SDA置为1或清0*/
else SDA=0;
SCL=1; /*置时钟线SCL为高,通知被控从机开始接收数据位*/
NOP; /*延时5μs,保证时钟高电平周期大于4μs*/
SCL=0; /*SCL变低电平,准备发送下一位数据 */
c=c<<1; /*将下一位要发送的数据移到最高位*/
}
NOP; /*延时5μs*/
SDA=1; /*8位发送完后释放数据线,准备接收应答位*/
NOP;
SCL=1; /*SCL由低变高,产生一个时钟,读取SDA的状态*/
NOP; /*延时5μs*/
if (SDA==1)ack_mk=0; /*如果SDA=1,则发送失败,将ack_mk清0*/
else ack_mk=1; /*否则发送成功,将ack_mk置1*/
SCL=0;
}

6 I2C总线从器件接收一个字节数据的函数
函数原型: uchar RcvByte();
功能: 从I2C总线接收一个字节数据。
uchar RcvByte()
{
uchar c;
uchar n;
for(n=0;n<8;n++) /*循环8次,先读高位,后读低8位*/
{ SDA=1; /*置数据线为输入方式*/
SCL=1; /*SCL由低变高,产生一个时钟*/
if (SDA==0) c=c&0x7f; /*根据数据线SDA的状态,将变量C清0或置1*/
else c=c|0x80;
c= _crol_(c,1); /*将C循环左移一位,准备接收下一位*/
SCL=0; /*时钟线SCL清0*/
}
return(c);
}

7、向无子地址器件发送字节数据函数
函数原型: bit I2C_SendByte(uchar sla,ucahr c);
功能: 该函数与SendByte()不同,它包含了从启动总线、发送从器件地址、数据到结束总线的全过程,如果返回1表示操作成功,否则操作有错。
SDA上数据的格式见下图:


START

sla

ACK

c

ACK

STOP

其中:
START 起始信号;
Sla 器件地址(写);
c 要写入器件的数据;
ACK 器件应答信号;
STOP 停止信号;
注:图中有阴影部分表示数据由主机向从器件传送,无阴影部分表示数据由从器件向主机传送。

bit I2C_SendByte(uchar sla ,uchar c)
{
Start(); /*向总线发起始信号,启动总线*/
SendByte(sla ); /*发送器件地址*/
if(ack_mk==0)return(0);
SendByte(c); /*发送数据*/
if(ack_mk==0)return(0);
stop(); /*结束总线*/
return(1);
}
8、向无子地址器件读字节数据函数
函数原型: bit I2C_RcvByte(uchar sla,uchar *c);
功能: 该函数与RcvByte()不同,它包含了从启动总线、发送从器件地址、读数据到结束总线的全过程。
Sla: 从器件地址
c : 指向读到的数据
如果返回1表示操作成功,否则操作有错。
SDA上相应的数据格式见下图:


START

Sla+1

ACK

*c

NACK

STOP

 

其中 sla+1:从器件地址(读);
*c : 读到的数据.
NACK : 非应答信号。
bit I2C_RcvByte(uchar sla,uchar *c)
{
Start(); /*发起始信号,启动总线*/
SendByte(sla+1); /*发送器件地址*/
if(ack_mk==0)return(0);
*c=RcvByte(); /*读取数据*/
NAck(); /*发送非就答位*/
stop(); /*发结束信号,结束本次数据传送*/
return(1);
}

9、向有子地址器件发送多字节数据函数
函数原型: bit ISendStr(uchar sla,uchar suba,ucahr *s,uchar n);
功能: 该函数包含了从启动总线到发送器件地址、器件子地址、数据、结束总线的全过程。
sla:从器件地址(写)
suba:子地址。
s:指向要发送数据。
n:要发送数据的字节数。
如果返回1表示操作成功,否则操作有误。
SDA上相应的数据格式见下图:


START

sla

ACK

suba

ACK

DATA1

ACK

DATA2

ACK

……

DATAn

ACK

STOP

bit I2C_SendStr(uchar sla,uchar suba,uchar *s,uchar no)
{
uchar i;
Start(); /*发起始信号,启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack_mk==0)return(0);
SendByte(suba); /*发送器件子地址*/
if(ack_mk==0)return(0);
for(i=0;i<n;i++)
{
SendByte(*s); /*发送数据*/
if(ack_mk==0)return(0);
s++;
}
stop(); /*发结束信号,结束本次数据传送*/
return(1);
}

10、向有子地址器件读取多字节数据函数
函数原型: bit I2C_RcvStr(uchar sla,uchar suba,uchar *s,uchar no);
功能: 该函数包含了从启动总线到发送地址,子地址,读数据,结束总线的全过程。
sla:从器件地址
suba:器件子地址。
s: 读出的内容放在s指向的存储区。
n:读取的字节数。
如果返回1表示操作成功,否则操作有误。
SDA上相应的数据格式见下图:


START

sla

ACK

suba

ACK

START

Sla+1

ACK

DATA1

ACK

DATAn

NACK

STOP

说明:主机首先通过发送起始信号、从器件地址sla和它想读取的字节数据所在地址suba(器件子地址),执行一个伪写操作,在从器件应答之后,主器件重新发送起始信号和从器件地址Sla+1,此时R/W 位置1 ,从器件响应并发送应答信号后,输出所要求的一个字节数据DATA1,主器件随后发送应答信号ACK,以后从器件每输出一个字节数据,主机均回送ACK应合,当从器件输出最后字节数据DATAn后,主机回送非应答信号,接着发送停止信号结束总线传送。
bit I2C_RcvStr(uchar sla,uchar suba,uchar *s,uchar n)
{
uchar i;
Start(); /*发起始信号,启动总线*/
SendByte(sla); /*发送器件地址*/
if(ack_mk==0)return(0);
SendByte(suba); /*发送器件子地址*/
if(ack_mk==0)return(0);
Start(); /*再次发起始信号*/
SendByte(sla+1); /*sla+1表示对该器件进行读操作*/
if(ack_mk==0)return(0);
for(i=0;i<n-1;i++) /*对前no-1个字节发应答信号*/
{
*s=RcvByte(); /*接收数据*/
Ack(); /*发送就答信号*/
s++;
}
*s=RcvByte(); /*接收最后一个字节*/
NAck(); /*发送非应信号*/
stop(); /*发结束信号,结束本次数据传送*/
return(1);
}
§4-4 I2C总线器件24C02及其应用
串行E2PROM是在各种串行器件应用中使用较频繁的器件,和并行E2PROM相比,串行E2PROM的数据传送的速度较低,但是其体积较小,容量小,所含的引脚也较少。所以,它特别适合于需要存放非挥发数据,要求速度不高,引脚少的单片机的应用。这里介绍串行E2PROM芯片,以及它们和单片机的接口技术。
24CXX系列的E2PROM有10种型号,其中典型的型号有24C01/02/04/08/16等5种,它们的存储容量分别是128/256/512/1024/2048字节。24CXX系列的E2PROM 支持I2C 总线数据传送协议,通过器件地址输入端A0、A1、A2 可以将最多8 个24C01 /24/C02 器件,4 个24C04 器件,2 个24C08 器件, 1 个24C16 器件连接到总线上。这里我们就24C02的进行分析,其它型号与此类似。
1、引脚的功能
VCC 电源+5V。
GND 地线。
SCL 串行时钟输入端。用于产生器件所有数据发送或接收的时钟。
SDA 串行数据I/O端,用于输入和输出串行数据。这个引脚是漏极开路的端口,可与其它开漏输出或集电极开路输出组成“线或”结构。通常需要用外部上拉电阻将其电平拉高。
A0、A1、A2 器件地址输入端,用于多个器件级联时设置器件地址,当这些脚悬空时默认值为0, 对于24C02 时最大可级联8 个器件,如果只有一个24C02 被总线寻址,这三个地址输入脚A0 A1 A2 可悬空或连接到GND。
WP 写保护端。这个端提供了硬件数据保护。当把WP接地时,允许芯片执行一般读写操作;当把WP接VCC时,则对芯片实施写保护。
289C51单片机与24C02的连接
89C51单片机与24C02的连接,其中P1.0作为24C02的数据线SDA,P1.1作为24C02的时钟线SCL,两条线均接10K的上接电阻,24C02的器件标识地址为1010,由于系统中只有一片24C02,所以直接将器件地址输入端A2、A1、A0接地处理,这样24C02在系统中的器件地址SLAW=0xA0,SLAR=0xA1。
由于89C51需对24C02进行写操作,所以应把WP脚接地电平,即允许写操作。

  1. 89C5124C02的读写程序

读写程序是以前面所介绍的函数为基础设计的,24C02的内部有连续的子地址空间,对这些空间进行n个字节的连续读/写时,都具有地址自动加1功能。只要设定好要读/写的器件内起始子地址及字节数,就能完成整个操作。
注意:对于24C02连续写的字节数不应超过页容量16,一次连续写所形成的总线传送结束后(主机发出停止信号后),24C02执行内部擦写过程,大约需要10ms左右,24C02不再应答主器件的任何请求。
24C02内有一个8位的地址计数器,连续读操作时,24C02每次输出一个数据字节后,地址计数器自动加1,当地址计数器加到255,并输出一个字节数据后,地址计数器将翻转到0,并继续输出数据字节,这样整个存储区域可以在一个读操作内全部读完。

#define SLAW 0xA0 /*24C02的器件地址为0xA0 */
uchar delay(uchar j)
{ uchar k,l;
for(l=0;l<=j;l++)
for(k=0;k<=250;k++);
return 0;
}
void main()
{
uchar sbuf[5]={0x00,0x12,0x55,0x30,0x12};/*定义发送缓冲区*/
uchar rbuf[5]; /*定义接收缓冲区*/
I2C_SendStr(SLAW,0x10,tbuf,0x5);
/*将发送缓冲区中5个字节的数据写入24C02从0X10开始的10个单元*/
delay(100); /*延时,等待2402内部写操作的完成*/
I2C_RcvStr(SLAW,0x10,rbuf,0x5);
/*从24C02 0X10单元开始读取5个字节存入接收缓冲区*/
while(1);
}
}

阅读(14613) | 评论(6)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

loading...
您需要登录后才能评论,请 登录 或者 注册