目前常用的微机与外设之间进行数据传输的串行总线主要有UART、1-wire、I2C和SPI总线。UART是以异步方式进行通信(一条数据输入线,一条数据输出线)。1-wire即单线总线,又叫单总线(只有一条线)。I2C同步串行2线方式进行通信(一条时钟线,一条数据线)。SPI同步串行3线方式进行通信(一条时钟线,一条数据输入线,一条数据输出线)。

I2C串行总线的组成及工作原理

I2C总线是PHLIPS公司推出的一种串行总线,它只有两根双向信号线。一根是数据线SDA(serial data I/O),另一根是时钟线SCL(serial clock)。如下图所示,IIC总线上可以挂多个器件,而每个器件都有唯一的地址,这样可以标识通信目标。数据的通信的方式采用主从方式,主机负责主动联系从机,而从机则被动回应数据。

I2C总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任一器件输出的低电平,都将使总线的信号变低,即各器件的SDA及SCL都是“线与”关系。

I2C总线传输协议

SCL为高电平期间,数据线上的数据必须保持稳定,只有SCL信号为低电平期间,SDA状态才允许变化。

SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号。

起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。

I2C字节的传送与应答

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

主机在发送数据时,每次发送一字节数据,都需要读取从机应答位,当从机空闲可以接收该字节数据时,从机会发出应答(一帧数据的第9位为“0”),当从机正忙于其他工作的处理来不及接收主机发送的数据时,从机会发出非应答(一帧数据的第9位为“1”)主机则应发出终止信号以结束数据的继续传送,主机通过从机发出的应答位来判断从机是否成功接收数据。

当主机接收数据时,它收到最后一个数据字节后,必须向从机发出一个结束传送的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放SDA线,以允许主机产生终止信号。

数据帧格式

I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/T),用0表示主机发送数据(T),1表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

当要写入的数据传送完后,单片机应发出终止信号以结束写入操作。写入n个字节的数据格式:

读出过程:单片机先发送该器件的7位地址码和写方向位0(“伪写”),发送完后释放SDA线并在SCL线上产生第9个时钟信号。被选中的存储器器件在确认是自己的地址后,在SDA线上产生一个应答信号作为回应。 然后,再发一个字节的要读出器件的存储区的首地址,收到应答后,单片机要重复一次起始信号并发出器件地址和读方向位1,收到器件应答后就可以读出数据字节,每读出一个字节,单片机都要回复应答信号。当最后一个字节数据读完后,单片机应返回以“非应答”(高电平),并发出终止信号以结束读出操作。

C语言编程

点亮数码管

将通讯数据在数码管中显示出来,首先要驱动数码管点亮,用3位数码管即可程序如下:

#include <reg52.h>
#include <intrins.h>
#define uint unsigned int
#define uchar unsigned char
sbit WE=P2^7; //位选
sbit DU=P2^6; //段选
uchar x,y,c,h,m,s;
uchar code table[]={
//0 1 2 3 4 5 6 7 8
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F,
//9 A B C D E F - . 关显示
0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x40, 0x80, 0x00
}; //数码管显示数字
uchar code table1[]={0xfe,0xfd,0xfb}; //数码管的位
void delay(uint z) //延时
{
for( c=z;c>0;c--)
for( y=110;y>0;y--);
}
void display(uchar i) //数码管显示
{
P0=0xff;
WE=1;
P0= table1[x];
WE=0;
switch(x)
{
case 0: DU=1; P0=table[i/100];DU=0; break;
case 1: DU=1; P0=table[i%100/10];DU=0; break;
case 2: DU=1; P0=table[i%10];DU=0; break;
}
x++;
if(x>2)
x=0;
delay(10);
}

void main()
{

while(1)
{
display(121); //数码管显示121
}

}

在数数码管上显示121:

通讯信号

通讯信号包括起始信号、终止信号、应答信号、非应答信号,程序编写是根据器件手册中的时序图编写的:

void START()    // 开始信号
{
SCL=1; //拉高时钟总线
SDA=0; //拉低数据总线
SDA=1; //拉高数据总线
delay5us(); //延时
SDA=0; //拉低数据总线
delay5us(); //延时
SCL=0; //拉低时钟总线
}
void STOP() //终止信号
{
SCL=0;
SDA=0;
SCL=1;
delay5us();
SDA=1;
delay5us();
SDA=0;
}
void ACK() //应答信号
{
SCL=0;
SDA=1;
SDA=0;
SCL=1;
delay5us();
SCL=0;
SDA=1;
}
void NACK() //非应答信号
{
SCL=0;
SDA=0;
SDA=1;
SCL=1;
delay5us();
SCL=0;
SDA=0;

}

读、写数据

接下来还要编写读、写的程序,程序一定要严格按照器件手册中的时序图编写,否则不会通讯成功,详见如下的总程序:

#include <reg52.h>
#include <intrins.h>
#define uint unsigned int
#define uchar unsigned char
sbit WE=P2^7; //位选
sbit DU=P2^6; //段选
sbit SCL=P2^1; //时钟总线
sbit SDA=P2^0; //数据总线
uchar x,y,c,h,m,s;
uchar code table[]={
//0 1 2 3 4 5 6 7 8
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F,
//9 A B C D E F - . 关显示
0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x40, 0x80, 0x00
}; //数码管显示数字
uchar code table1[]={0xfe,0xfd,0xfb}; //数码管的位
void delay(uint z) //延时
{
for( c=z;c>0;c--)
for( y=110;y>0;y--);
}
void delay5us() //5us延时
{
_nop_();
}
void display(uchar i) //数码管显示
{
P0=0xff;
WE=1;
P0= table1[x];
WE=0;
switch(x)
{
case 0: DU=1; P0=table[i/100];DU=0; break;
case 1: DU=1; P0=table[i%100/10];DU=0; break;
case 2: DU=1; P0=table[i%10];DU=0; break;
}
x++;
if(x>2)
x=0;
delay(10);
}
void START() // 开始信号
{
SCL=1; //拉高时钟总线
SDA=0; //拉低数据总线
SDA=1; //拉高数据总线
delay5us(); //延时
SDA=0; //拉低数据总线
delay5us(); //延时
SCL=0; //拉低时钟总线
}
void STOP() //终止信号
{
SCL=0;
SDA=0;
SCL=1;
delay5us();
SDA=1;
delay5us();
SDA=0;
}
void ACK() //应答信号
{
SCL=0;
SDA=1;
SDA=0;
SCL=1;
delay5us();
SCL=0;
SDA=1;
}
void NACK() //非应答信号
{
SCL=0;
SDA=0;
SDA=1;
SCL=1;
delay5us();
SCL=0;
SDA=0;

}
void Send(uint dat) //发送地址
{
uint i;
for(i=0;i<8;i++) //8位
{
SCL=0;
SDA=dat & 0x80; //先发送最高位地址
dat <<=1; //左移一位
SCL=1;
}
SCL=0;
SDA=1;
}
void Write(uint dat,uint cmd) //写入数据
{
START(); //开始信号
Send(0xA0); //发送地址
ACK(); //应答信号
Send(dat); //发送字节
ACK();
Send(cmd); //发送数据
ACK();
STOP(); //停止信号
}
uchar Read(uint dat) //读数据
{
uint i,cmd;
START(); //开始信号
Send(0xA0); //发送地址
ACK(); //应答信号
Send(dat); //发送字节
ACK();
START();
Send(0xA1);
ACK();
for(i=0;i<8;i++) //读数据
{ cmd <<=1; //左移
SCL=0;
if(SDA)
cmd |=0x01; //将数据写入cmd中
SCL=1;
}
SCL=0;
SDA=1;
NACK();
STOP();
return cmd; //返回cmd的值
}
void main()
{
Write(2,131); //在2通道中写下131
delay(5);
while(1)
{
display(Read(2)); //读出2号通道中的数据在数码管上显示131
}

}

I2C通讯的调试需要借助工具,可以使用示波器来抓取波形,从而检查出程序哪里有问题,下面是程序烧录单片机后的果,将131写入2通道中,然后再读出来,显示在数码管上: