STC89C52的I2C与串口通信
I2C总线
单片机电路中的I2C总线(Inter Integrated Circuit Bus)是一种Philips公司推行的串行总线标准。连接在总线上的外设通过总线寻址,是具有总线仲裁和高低速设备同步等功能的高性能多主机总线。

I2C总线的工作原理
1、总线由串行数据线SDA和串行时钟线SCL构成,可发送和接受数据。
2、所有连接在I2C总线上的器件和接口电路都必须具有I2C总线接口,且所有的SDL/SCL同名端相互连接。
3、在总线上各个I2C设备之间依靠SDA发送的地址信息进行寻址。

I2C总线的特点
组成I2C系统的电路结构简单(两根线)、占用空间小、可以降低芯片的引脚数量、允许若干兼容器件共享总线。总线长度可达7.6m,传送速度可达400Kbps,标准速率100Kbps。支持多主控器件(某一时刻只能有一个主控器件)。需要注意的是:I2C线上的所有设备SDA、SCL引脚必须要外接上拉电阻(电阻值的大小会影响通信)。

I2C总线结构示意图
挂载在I2C总线上的器件

I2C总线器件寻址方式
I2C总线上的主器件在进行数据传输前需要通过寻址,选择需要进行通信的从器件。总线上所有外围器件都必须要有唯一的7位地址,由器件地址和引脚地址两部分组成。
器件地址是I2C器件固有的编码地址,器件出厂时已经给定不可更改。
引脚地址由I2C总线外设的地址引脚决定(A2、A1、A0),根据其在电路中的正负极或悬空等状态,形成不同的地址代码。(内部电路)

51单片机模拟I2C总线协议
在实际应用中,往往遇到所使用的单片机没有I2C总线接口,例如典型的51系列单片机。为了让此类单片机用于操作 I2C总线器件的能力,需要在程序模拟I2C总线数据传输协议。

I2C总线通信时序图
时序图1

起始信号用于开启I2C总线的通信。其中,起始信号是在时钟线SCL为高电平期间,数据SDA上高电平向低电平变化的下降沿信号。起始信号出现以后,才可以进行后续的I2C总线寻址或数据传输;
终止信号用于终止I2C总线的通信。其中,终止信号是在时钟线SCL为高电平期间,数据线SDA上低电平到高电平变化的上升沿信号。终止信号一出现,所有I2C总线操作都结束,并释放总线控制权;
应答信号用于表明I2C总线数据传输的结束。 I2C总线数据传送时, 一个字节数据传送完毕后都必须由主器件产生应答信号。主器件在第9个时钟位上释放数据总线SDA,使其处于高电平状态,此时从器件输出低电平拉低数据总线SDA为应答信号。
时序图2

I2C总线传送格式
I2C总线的传送格式为主从式,对系统中的某一器件来说有四种工作方式: 主发送方式、从发送方式、主接收方式、从接收方式。
单片机发送 24C02(存储芯片)接收:主器件产生开始信号以后,发送的第一个字节为控制字节。前七位为从器件的地址片选信号。最低位为数据传送方向位(高电平表示读从器件,低电平表示写从器件),然后发送一个选择从器件片内地址的字节,来决定开始读写数据的起始地址。接着再发送数据字节,可以是单字节数据,也可以是一组数据,由主器件来决定。从器件每接收到一个字节以后,都要返回一个应答信号(ASK=0)。主器件在应答时钟周期高电平期间释放SDA线,转由从器件控制,从器件在这个时钟周期的高电平期间必须拉低SDA线,并使之为稳定的低电平,作为有效的应答信号。

I2C总线应用实例
1、24C02存储芯片的使用

#include <reg52.h>
#include <intrins.h>
#define uchar unsigned char
#define uint unsigned int
#define OP_READ 0xa1 //24c02地址和读取操作
#define OP_WRITE 0xa0//写入操作
sbit SDA=P2^0;//器件数据总线
sbit SCL=P2^1; //器件时钟总线
sbit DS1302=P2^4;//1302片选端
sbit LCDEN=P2^5; 
uchar code display[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe,0xff};
void delay1ms()
{
	uchar i,j;
	for(i=0;i<10;i++)
		for(j=0;j<33;j++);
}
void delaynms(uchar n)
{
	uchar i;
	for(i=0;i<n;i++)
		delay1ms();
}
void delay_ns(uint i)
{
	uint y;
	for(y=0;y<i;y++)
		_nop_();//1us
}
void start() //I2C数据传输起始位
{
	SCL=1;
	SDA=1;
	_nop_();
	_nop_();
	_nop_();
	_nop_();
	SDA=0;
	_nop_();
	_nop_();
	_nop_();
	_nop_();
	SCL=0;
}	   
void stop()//停止位
{
	 SCL=1;
	 SDA=0;
	 _nop_();
	 _nop_();
	 _nop_();
	 _nop_();
	 SDA=1;
	 _nop_();
	 _nop_();
	 _nop_();
	 _nop_();
	 SDA=0;
	 SCL=0;
}
uchar ReadDate()//从器件中读数据
{
	uchar i,x;
	for(i=0;i<8;i++)
	{
		SCL=1;
		x<<=1;
		x|=(uchar)SDA;
		SCL=0;
	}
	return x;
}	 
bit WriteCurrent(uchar y)//写入函数
{
	uchar i;
	bit ack_bit;
	for(i=0;i<8;i++)
	{
		SDA=(bit)(y&0x80);
		_nop_();
		SCL=1;
		_nop_();
		_nop_();
		SCL=0;
		y<<=1;
	}
	SDA=1;	   //发送设备释放SDA线
	_nop_();
	_nop_();
	SCL=1;
	_nop_();
	_nop_();
	_nop_();
	_nop_();
	ack_bit=SDA;
	SCL=0;
	return ack_bit;
}
void WriteSet(uchar add,uchar date)//将数据写到add地址
{
	start();
	WriteCurrent(OP_WRITE);	 //选择芯片告知操作
	WriteCurrent(add);		 //写入地址
	WriteCurrent(date);   //向上述地址传输数据
	stop();
	delaynms(4);
}
uchar ReadCurrent()	  //读数据后发送回单片机
{
	uchar x;
	start();
	WriteCurrent(OP_READ);
	x=ReadDate();
	stop();
	return x;
}
uchar ReadSet(uchar set_addr)	//在指定地址读取数据
{
	start();
	WriteCurrent(OP_WRITE);
	WriteCurrent(set_addr);
	return(ReadCurrent());
}

main(void)
{
	uchar i;
	LCDEN=0;
	DS1302=0;
	P1=0xfe;
	SDA=1;
	SCL=1;	 //空闲状态

	for(i=0;i<16;i++)
	{
		WriteSet(i,display[i]);
	}
	for(i=0;i<16;i++)
	{
		P1=ReadSet(i);
		delaynms(200);
		delaynms(200);
	}
}

2、AD数模转换的使用

#include <reg52.h>
#include <intrins.h>
#define uchar unsigned char
#define uint unsigned int
#define delaynop(); {_nop_();_nop_();_nop_();_nop_();};//延时4us
#define PCF8591_WRITE 0x90
#define PCF8591_READ 0x91//IIC器件PCF5891的写地址和读地址
#define disdata P0
//液晶LCD1602定义
sbit lcd_RS=P1^0;
sbit lcd_RW=P1^1;
sbit lcd_EN=P2^5;

//数码管定义
sbit dula=P2^6;
sbit wela=P2^7;
sbit DS1302=P2^4;

//IIC总线定义
sbit SDA=P2^0;
sbit SCL=P2^1;

uint data dis[4]={0x00,0x00,0x00,0x00};
uchar code dis4[]={"1- .  V  2- .  V"};
uchar code dis5[]={"3- .  V  4- .  V"};
bit bdata SystemError;//丛机错误标志位
#define NUM 4			 //缓存区深度
uchar idata receivebuf[NUM];//数据接收缓冲区
void stopSMG()//关闭数码管
{
	dula=1;
	P0=0x00;
	dula=0;
}
void delay(int ms)//延时n毫秒
{
	while(ms--)
	{
		uchar i;
		for(i=0;i<250;i++)
		{
			_nop_();
			_nop_();
			_nop_();
			_nop_();
		}
	}
}
//lcd1602器件设置

bit lcd_busy()	//LCD检忙函数
{
	bit result;
	lcd_RS=0;
	lcd_RW=1;
	lcd_EN=1;
	delaynop();
	result=(bit)(P0&0x80);
	lcd_EN=0;
	return result;
}
void lcd_writezhiling(uchar cmd)	//lcd写指令函数
{
	while(lcd_busy());
	lcd_RS=0;
	lcd_RW=0;
	lcd_EN=0;
	_nop_();
	_nop_();
	P0=cmd;
	delaynop();
	lcd_EN=1;
	delaynop();
	lcd_EN=0;
}
void lcd_writedate(uchar date)		//lcd写数据
{
	while(lcd_busy());
	lcd_RS=1;
	lcd_RW=0;
	lcd_EN=0;
	P0=date;
	delaynop();
	lcd_EN=1;
	delaynop();
	lcd_EN=0;
}
void lcd_init()		//lcd初始化
{
	lcd_writezhiling(0x38);
	lcd_writezhiling(0x0c);
	lcd_writezhiling(0x06);
	lcd_writezhiling(0x01);
	lcd_writezhiling(0x80);		
}
//AD数据处理与显示

show_value(uchar ad_date)
{
	dis[2]=ad_date/51;
	dis[2]=dis[2]+0x30;

	dis[3]=ad_date%51;
	dis[3]*=10;	 
	dis[1]=dis[3]/51;
	dis[1]+=0x30;

	dis[3]%=51;
	dis[3]*=10;
	dis[0]=dis[3]/51;
	dis[0]+=0x30;	
}

//IIC设置

void iic_start()  //起始信号
{
	SCL=1;
	SDA=1;
	delaynop();
	SDA=0;
	delaynop();
	SCL=0;
}
void iic_stop()   //终止信号
{
	SCL=1;
	SDA=0;
	delaynop();
	SDA=1;
	delaynop();
	SDA=0;
}
void iic_init()   //iic 初始化
{
	SCL=0;
	iic_stop();
}

//从机发送应答信号

void slave_ACK(void)
{
	SDA=0;
	SCL=1;
	delaynop();
	SCL=0;
	SDA=1;
}

//发送非应答信号
void slave_NOACK(void)
{
	SDA=1;
	SCL=1;
	delaynop();
	SCL=0;
	SDA=0;	
}

//主机应答检查函数
void check_ACK(void)
{
	SDA=1;
	SCL=1;
	F0=0;
	delaynop();
	if(SDA==1)	  //非应答,标志位置1
		F0=1;	   //F0全局标志位
	SCL=0;
}

//IIC发送单个字节
void IICSendByte(uchar ch)
{
	uchar idata n=8;	 //向SDA上发送数据字节,8位
	while(n--)
	{
		if((ch&0x80)==0x80)	//发送数据的最高位为1则发送位1
		{
			SDA=1;
			SCL=1;
			delaynop();
			SCL=0;
		}
		else//否则传送位0
		{
			SDA=0;
			SCL=1;
			delaynop();
			SCL=0;
		}
		ch=ch<<1;
	}
}

//接收单个字节函数
uchar IICreceiveByte(void)
{
	uchar idata n=8;
	uchar tdata=0;
	while(n--)
	{
		SDA=1;
		SCL=1;
		tdata=tdata<<1;
		if(SDA==1)		//如接收到的是1,把数据的最后一位置1
			tdata=tdata|0x01;
		else	 //最后一位置0
			tdata=tdata&0xfe;
		SCL=0;
	}
	return tdata;
}

//发送n位数据函数
void DAC_PCF8591(uchar controlbyte,uchar w_data)
{
	iic_start();
	delaynop();
	IICSendByte(PCF8591_WRITE);
	check_ACK();
	if(F0==1)
	{
		SystemError=1;
		return;
	}
	IICSendByte(controlbyte&0x77);
	check_ACK();
	if(F0==1)
	{
		SystemError=1;
		return;
	}
	IICSendByte(w_data);
	check_ACK();
	if(F0==1)
	{
		SystemError=1;
		return;
	}
	iic_stop();
	delaynop();
}
			
//连续读入4路通道A/D转化结果到receivebuf(缓冲区)
void ADC_PCF8591(uchar controlbyte)
{
	uchar idata receive_da,i=0;
	iic_start();
	IICSendByte(PCF8591_WRITE);
	check_ACK();
	if(F0==1)
	{
		SystemError=1;
		return;
	}
	IICSendByte(controlbyte);
	check_ACK();
	if(F0==1)
	{
		SystemError=1;
		return;
	}
	iic_start();
	IICSendByte(PCF8591_READ);
	check_ACK();
	if(F0==1)
	{
		SystemError=1;
		return;
	}

	IICreceiveByte(); //空读一次
	slave_ACK();	  //收到一字解后发送一个应答位

	while(i<4)
	{
		receive_da=IICreceiveByte();
		receivebuf[i++]=receive_da;
		slave_ACK();
	}
	slave_NOACK();
	iic_stop();
}
main()
{
	uchar i,l;
	delay(10);
	stopSMG();
	DS1302=0;
	lcd_init();
	lcd_writezhiling(0);
	i=0;
	while(dis4[i]!='\0')
	{
		lcd_writedate(dis4[i]);
		i++;
	}
	lcd_writezhiling(0x80+0x40);
	i=0;
	while(dis5[i]!='\0')
	{
		lcd_writedate(dis5[i]);
		i++;
	}
	while(1)
	{
		iic_init();
		ADC_PCF8591(0x04);
		if(SystemError==1)
		{
			iic_init();
			ADC_PCF8591(0x04);
		}
		for(l=0;l<4;l++)
		{
		   //显示通道0
			show_value(receivebuf[0]);
			
			lcd_writezhiling(0x80+2);
			lcd_writedate(dis[2]);

			lcd_writezhiling(0x80+4);
			lcd_writedate(dis[1]);

			lcd_writezhiling(0x80+5);
			lcd_writedate(dis[0]);

			//显示通道1
			show_value(receivebuf[1]);
			
			lcd_writezhiling(0x80+11);
			lcd_writedate(dis[2]);

			lcd_writezhiling(0x80+13);
			lcd_writedate(dis[1]);

			lcd_writezhiling(0x80+14);
			lcd_writedate(dis[0]);

			//显示通道2
			show_value(receivebuf[2]);
			
			lcd_writezhiling(0x80+0x40+2);
			lcd_writedate(dis[2]);

			lcd_writezhiling(0x80+0x40+4);
			lcd_writedate(dis[1]);

			lcd_writezhiling(0x80+0x40+5);
			lcd_writedate(dis[0]);

			//显示通道3
			show_value(receivebuf[3]);
			
			lcd_writezhiling(0x80+0x40+11);
			lcd_writedate(dis[2]);

			lcd_writezhiling(0x80+0x40+13);
			lcd_writedate(dis[1]);

			lcd_writezhiling(0x80+0x40+14);
			lcd_writedate(dis[0]);

			iic_init();
			DAC_PCF8591(0x40,receivebuf[0]);
			if(SystemError==1)
			{
				iic_init();
				DAC_PCF8591(0x40,receivebuf[0]);
			}
		}
	} 
}

3、DA模数转换的使用

#include <reg52.h>
#include <intrins.h>
#define uchar unsigned char
#define uint unsigned int
#define WRITE 0x90
#define READ 0x91
sbit fm=P2^3;
sbit RST=P2^4;
sbit dula=P2^6;
sbit wela=P2^7;
sbit SDA=P2^0;
sbit SCL=P2^1;
bit ADflag;
void delayms(uchar i)
{
	uint n;
	for(;i>0;i--)
		for(n=0;n<125;n++);
}
void timer0_init()
{
	TMOD|=0x10;
	TH1=0xff;
	TL1=0x00;
	EA=1;
	ET1=1;
	TR1=1; 
}
void start(void)   //IIC起始信号
{
	SDA=1;
	_nop_();
	SCL=1;
	_nop_();
	SDA=0;
	_nop_();
	SCL=0;
}		
void stop(void)	   //IIC停止信号
{
	SDA=0;
	_nop_();
	SCL=1;
	_nop_();
	SDA=1;
	_nop_();
	SCL=0;
}
void ack(void)	   //应答信号
{
	SDA=0;
	_nop_();
	SCL=1;
	_nop_();
	SCL=0;
	_nop_();
}
void noack(void)	  //非应答信号
{
	SDA=1;
	_nop_();
	SCL=1;
	_nop_();
	SCL=0;
	_nop_();
}
void send(uchar Data)//发送一个字节
{
	uchar bitcounter=8;
	uchar temp;
	do{
		temp=Data;
		SCL=0;
		_nop_();
		if((temp&0x80)==0x80)
			SDA=1;
		else
			SDA=0;
		SCL=1;
		temp=Data<<1;
		Data=temp;
		bitcounter--;		
	}while(bitcounter);
	SCL=0;
}
uchar read(void)//读入一个字节并返回
{
	uchar temp=0;
	uchar temp1=0;
	uchar bitcounter=8;
	SDA=1;
	do{
		SCL=0;
		_nop_();
		SCL=1;
		_nop_();
		if(SDA)
			temp=temp|0x01;
		else
			temp=temp&0xfe;
		if(bitcounter-1)
		{
			temp1=temp<<1;
			temp=temp1;
		}
		bitcounter--;
	}while(bitcounter);
	return(temp);
}
void DAC(uchar Data)//写入DA数模转换值
{
	start();
	send(WRITE);
	ack();
	send(0x40); //控制位,使能DAC输出
	ack();
	send(Data);
	ack();
	stop();
}

uchar readADC(uchar ch1)//读取模数转换值
{
	uchar Data;
	start();
	send(WRITE);
	ack();
	send(0x40|ch1);//写入选择通道1,2,3,4
	ack();
	start();
	send(READ);
	ack();
	Data=read();
	SCL=0;
	noack();
	stop();
	return Data;
}
void cmg()
{
	dula=1;
	P0=0x00;
	dula=0;
	wela=1;
	P0=0x00;
	wela=0;
	RST=0;
}
void main()
{
	uchar num=0;
	uchar adtemp;
	timer0_init();
	cmg();
//	DS1302=0;
	fm=1;
	while(1)
	{
		for(num=0;num<255;num++)
		{
			DAC(num);
			delayms(10);
		}
		for(num=255;num>0;num--)
		{
			DAC(num);
			delayms(10);
		}
		if(ADflag)
		{
			ADflag=0;
		}
	}
}

串口通信传输方向
1、单工:单工是指数据传输仅能沿一个方向进行。
2、半双工:半双工是指数据传输可以沿两个方向,但需要分时进行。
3、全双工:全双工是指数据可以同时进行双向传输

串口通信奇偶校验
如约定采用奇校验:发送数据时,数据位尾随1位奇校验位,奇校验 位可以取0或1,但要保证,数据中“1”的个数与校验位“1”的个数之和 应为奇数;接收数据时,检查数据中“1”的个数与校验位“1”的个数之 和是否仍应为奇数,如不是,则说明数据在传输过程中出现了差错。
如约定采用偶校验:发送数据时,数据位尾随1位偶校验位,偶校验 位可以取0或1,但要保证,数据中“1”的个数与校验位“1”的个数之和 应为偶数;接收数据时,检查数据中“1”的个数与校验位“1”的个数之 和是否仍应为偶数,如不是,则说明数据在传输过程中出现了差错。

串口通信数据传输率
简称数传率,指单位时间内传输的信息量,可用比特率和波特率来表示。
⑴、比特率:是数字信号的传输速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数bit/s(bps) 。
⑵、波特率:波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示,其单位为波特(Baud)。
对于串行通信来说,或者说是对于普通的数字电路来说,都是两相调制,也就是单个调制状态对应1个二进制位,因此: 比特率= 波特率 1bps=1 Baud 计算机中常用的波特率是:110、300、600、1200、2400、4800、9600、 19200、28800、33600,目前最高可达56Kbps. CH340转出的虚拟串口的波特率突破了传统串口的56Kbps限制,最高可达 2Mbps。

串行通信结构框图

串行通信

51串口模块结构图

51串口

单片机串口初始化步骤
1、TX设置为输出、RX设置为输入,也可不设置,采用默认的准双向口模式。
2、设置串口1 的工作模式,SCON 寄存器中的SM0 和SM1 两位决定了串口1 的4 种工作模式。
3、如要串口1 接收,将SCON 寄存器中的REN 位置1 即可。
4、计算BRT的值,并置数;
5、设置独立波特率发生器相关位:BRTx12:=0, S1BRS: =1, SMOD
6、启动独立波特率发生器( BRTR =1);
7、串行口工作在中断方式时,还要设置串口1 的中断优先级 ( PS,PSH ),如果不设置的话,默认是低优先级;打开中断相应的 控制位(ES,EA)。接收完成标志RI,发送完成标志TI,要由软件清0。

单片机串口通信应用实例
1、接收数据

#include <reg51.h>
#define uchar unsigned char
#define uint unsigned int

uchar Receive(void) //接收一个字节数据
{
	uchar dat;
	while(RI==0);//RI没有被置1,就一直等待单片机数据接收完毕(RI=1)
	RI=0;
	dat=SBUF;
	return dat;
}
void mian()
{
	TMOD=0x20;//定时器T1工作方式2;
	SCON=0x50; //串口工作方式1,允许接收(REN=1)
	PCON=0x00; //波特率9600
	TH1=0xfd;
	TL1=0xfd;
	TR1=1;
	REN=1;//允许接收
	while(1)
	{
		P1=Receive();
	}
}

2、发送数据

 #include <reg51.h>
 #define uchar unsigned char
 uchar code  Tab[]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};

 void Send(uchar dat)
 {
 	SBUF=dat;
	while(TI==0);
	TI=0;
 }
 void delay()
 {
 	uchar m,n;
	for(m=0;m<200;m++)
		for(n=0;n<250;n++);
 }

 void main()
 {
 	uchar i;
	TMOD=0x20;
	SCON=0x40;	//串口工作方式1
	PCON=0x00;	 //设置波特率9600
	TH1=0xfd;
	TL1=0xfd;
	TR1=1;
	while(1)
	{
		for(i=0;i<8;i++)
		{
			Send(Tab[i]);
			delay();
		}
	}
 }