C51之I2C总线与EEPROM

本文介绍了常见的串行总线协议如UART,I2C,SPI,详细讲解了I2C的工作原理,包括其电路组成、传输协议和时序。同时,提供了软件模拟I2C通信的C语言代码示例,涉及启动、停止信号以及数据传输的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这东西它不简单,因为理论很多也要要记住,并且是使用程序来模拟IIC通信协议

但是请静下心学习,人能常清静,天地悉皆归,多念几遍

        · 

1.常用的串行总线协议

常用的微机与外设之间进行数据传输的串行总线主要有UART,1-wire,I2C,SPI

UART:异步通信,一条数据输入线,一条数据输出线

1-wire:单线总线

I2C:同步串行2线方式进行通信,一条时钟线和一条数据线

SPI:同步串行3线方式进行通信,一条时钟线和一条数据输入线和一条数据输出线

2.电路知识

场效应管(FET)是利用输入回路的电场效应来控制输出回路电流的一种半导体器件,又叫单极性晶体体。

结型场效应管(JEFT)给栅极一个足够的负电压场效应管截止,给0则导通(NPN型)

绝缘栅型场效应管(MOS管) 

VCC :    接入电路中的电压         双极器件的正 

                下标可以理解为NPN晶体管的集电极C

                来源于集电极电源电压,CollectorVoltage,一般用于双极型晶体管

                PNP管时为负电源电压,有时也标成-Vcc,NPN管时为正电压

               模拟电源

VDD :      器件内部的工作电压    单级器件的正

               下标可以理解为场效应管的漏极D

               来源于漏极电源电压,DrainVoltage,用于MOS晶体管电路,一般指正电源

VSS :      电路公共接地端电压     源极电源电压

               在CMOS电路中指负电源,在单电源时指零伏或接地  

VEE :     发射极电源电压

VBB :     基极电源电压

总:

一般来说VCC=模拟电源,VDD=数字电源,VSS=数字地,VEE=负电源。

有些IC既有VDD引脚又有VCC引脚,说明这种器件自身带有电压转换功能。
对于数字电路来说,VCC是电路的供电电压,VDD是芯片的工作电压(通常Vcc>Vdd),VSS是接地点。
在场效应管(或COMS器件)中,VDD为漏极,VSS为源极,VDD和VSS指的是元件引脚,而不表示供电电压

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

I2C   有两根线  数据线 - SDA(serial data I/O)                            serial:串行

                          时钟线 - SCL  (serial clock)

I2C总线上可以挂很多器件,每个器件都有一个地址,主机负责主动联系从机,而从机则被动回应数据。

 

I2C总线通过上拉电阻接正电源,当总线空闲时,两根线均为高电平,当任意一个器件输出的低电平都会使总线的信号变低,各器件的SDA和SCL都是线与的关系

 4.        I2C总线传输协议

数据位的有效性规定

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

I2C的起始和终止信号

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

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

I2C字节的传送与应答

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

数据帧的格式

 I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号

在起始信号后必须穿送一个从机的地址(7位),第八位是数据传送的方向位(R/T),用0来表示主机发送数据(T),1表示主机接收数据(R)

总线的一次数据传送过程中,有以下几种组合方式

 总线的寻址

I2C总线协议有明确的规定:采用七位的寻址字节(寻址字节是起始信号后的第一个字节)

从机地址由固定部分和可编程部分组成,在一个系统中,从机地址中的可编程部分决定了可接入总线该类器件的最大数目。

5.软件模拟I2C的通信时序

       自己去找数据手册,如果看不懂,就慢慢的看,迟早要学会看,我也不怎么看得懂,多看看就熟悉了。但是我给你列几个翻译还是可以的,要注意我上面画的图和真实的数据图有差别是为了更好的理解,如果你能看懂数据手册,以数据手册为准,其实没什么难的,不需要麻烦的计算,没让你去自己设计,先看懂再说,一口吃不成胖子

Clock Frequency, SCL                          时钟频率  

Clock Pulse Width Low                        时钟脉冲宽度低

Clock Pulse Width High                       时钟脉冲宽度高

Noise Suppression Time                      噪声抑制时间

Clock Low to Data Out Valid                时钟低电平至数据输出有效

Time the bus must be free before a new transmission can start 

在开始新传输之前,总线必须空闲的时间

Start Hold Time                                        开始保持时间

Start Setup Time                                      开始设置时间

Data In Hold Time                                    数据处于保留时间

Data In Setup Time                                  设置时间中的数据

Inputs Rise Time                                      输入上升时间

Inputs Fall Time                                        输入下降时间

Stop Setup Time                                       停止设置时间

Data Out Hold Time                                  数据输出保留时间

Write Cycle Time                                       写入周期时间

volt                                                              伏特

至于时序图自己查,去看数据手册,别偷懒,否则你不明白我代码里的延时的意义在哪,如果你自信的删掉了,恭喜你,你会收获到一个没有错误的代码但是就是做不到你要做的功能。

真的很绕的一定要去理解为什么,最好带上逻辑分析仪,不贵,否则很难找出问题。至于里面的判断你感觉没有任何意义,其实还真没啥用,但是它展示了过程,你要想让他应答和非应答有意义留给你自己去改正。

#include <reg52.h>
#include <intrins.h>
#define uint unsigned int
#define uchar unsigned char
#define At24c02ADDR 0xA0//AT24C02硬件地址
#define I2CRead 1 //I2C读方向位
#define I2CWrite 0 //I2C写方向位

sbit SCL = P2^1; //时钟
sbit SDA = P2^0; //数据
sbit DU = P2^6;//数码管段选
sbit WE = P2^7;//数码管段选
uchar num;//数码管显示的值
bit Ackflag;//应答标志位
 
//共阴数码管段选表0-9
uchar code duanxuan[]= {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F,};
//数码管位选码
uchar code weixuan[] = {0xfe, 0xfd, 0xfb};
 
void delay(uint z)
{
	uint x,y;
	for(x = z; x > 0; x--)
		for(y = 114; y > 0 ; y--); 		
} 
 
void display(uchar i)
{  static uchar wei; 		
	DU=1;
    P0=0x00;
    DU =0;
	WE = 1;//打开位选锁存器
	P0 = weixuan[wei];
	WE = 0;//锁存位选数据
	switch(wei)
	{
		case 0: DU = 1; P0 = duanxuan[i/100%10]; DU = 0; break;
		case 1: DU = 1; P0 = duanxuan[i/10%10]; DU = 0; break;	
		case 2: DU = 1; P0 = duanxuan[i%10]; DU = 0; break;		
	}
	wei++;
	if(wei == 3)
		wei = 0;
}

void timer0Init()
{
    EA = 1;	//打开总中断
	ET0 = 1;//打开定时器0中断
	TR0 = 1;	 //启动定时器0
	TMOD |= 0X01; //定时器工作模式1,16位定时模式
	TH0 = 0xED;
	TL0 = 0xFF; //定时5ms
}
/*********************************
IIC通信代码
**********************************/
void delay5us()//延时函数,大概是5.43us 
{
  _nop_();//上面要调用库函数才行
}

void I2C_Start()//启动信号函数
{
    SCL = 1;//将时钟置于高电平
	SDA = 1;//将数据也置于高电平
	delay5us();//根据时序要使SDA保持一定时间
	SDA = 0;//从高电平到低电平产生启动信号
    delay5us();//同上
}

void I2C_Stop()//函数停止函数
{
  SCL = 0;//将时钟置于低电平
  SDA = 0;//将数据也置于低电平
  SCL = 1;//让SCL置于高电平,并延时
  delay5us();
  SDA = 1;//SDA置于高电平,电平由低到高,产生停止信号,
  delay5us();
}

bit ReadACK()//I2C总线读从机应答信号
{  
   SCL = 0;//允许从机控制SDA
   SCL = 1;//拉高SCL读SDA值
   delay5us();
   if(SDA)//判断SDA状态
   {
   	 SCL = 0;
	 return(1);//返回1
   }
   else
   {
   	 SCL = 0;
	 return(0);//返回0
   }
}

void SendACK(bit i)//主机发送应答
{
  SCL = 0;///拉低时钟总线,允许主机控制SDA
  if(i)	//发非应答
     SDA = 1;
  else //发应答
     SDA = 0;
  SCL = 1;//拉高总线,让从机读SDA
  delay5us();
  SCL = 0; //拉低时钟总线,允许SDA释放
  SDA = 1; //释放数据总线
}

void I2C_SendByte(uchar DAT)//I2C发送一个字节数据
{
  uchar	i;
  for(i=0;i<8;i++)//分别写8次,每次写1位
  {
       SCL = 0;//拉低时钟总线,允许SDA变化
       if(DAT & 0x80)//先写数据最高位
           SDA = 1;
       else
            SDA = 0;
       SCL = 1;//拉高时钟,让从机读SDA
	   DAT<<=1;//为发送下一位左移1位  
  } 
  SCL = 0;//拉低时钟总线,允许SDA释放
  SDA = 1;//释放数据总线                                
}

void At24c02_Write(uchar ADDR,DAT)
{
  I2C_Start();//I2C起始信号
  I2C_SendByte(At24c02ADDR + I2CWrite);//发送器件地址加读写方向位
  if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
  else
       Ackflag = 0;//应答
   I2C_SendByte(ADDR);//发送储存单元地址字节
   if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
   else
       Ackflag = 0;//应答
   I2C_SendByte(DAT);//发送一字节数据
   if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
   else
       Ackflag = 0;//应答
   I2C_Stop();	//I2C停止信号
}

uchar I2C_Readbyte()
{
  uchar i ,DAT;
  for(i=0;i<8;i++)
  {
  	DAT<<=1;//数据左移1位,准备接收一位
	SCL = 0;//拉低时钟总线,允许从机控制SDA变化
	SCL = 1;//拉高时钟总线,读取SDA上的数据
	if(SDA)
	 DAT |= 0x01;//为1则写1 
  }
  return(DAT);//返回读出的数据
}

uchar At24c02_Read(uchar ADDR)
{
  uchar DAT;
  I2C_Start();//I2C起始信号
  I2C_SendByte(At24c02ADDR + I2CWrite);//发送器件地址加读写方向位
  if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
  else
       Ackflag = 0;//应答
   I2C_SendByte(ADDR);//发送一个字节
   ReadACK();
   I2C_Start();//I2C起始信号
   I2C_SendByte(At24c02ADDR + I2CRead);//发送器件地址加读写方向位
   if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
   else
       Ackflag = 0;//应答
   DAT = I2C_Readbyte();//读一字节
   SendACK(1);//主机发送非应答
   I2C_Stop(); //I2C停止信号
   return(DAT);//返回读出数据
}
 
void main()//main函数自身会循环
{	
    EA = 0;//屏蔽中断
   	timer0Init();
	At24c02_Write(3,188);
	delay(2);
	num = At24c02_Read(3);
	if(Ackflag)
	 P1 = 0;
	else
	 P1 = 0xff;
	EA = 1;//开中断
	while(1);	
} 
 
//定时器0中断函数
void timer0() interrupt 1
{
	TH0 = 0xED;
	TL0 = 0xFF; //定时5ms
	display(num); //数码管显示函数	
} 

算了算了我改好

#include <reg52.h>
#include <intrins.h>
#define uint unsigned int
#define uchar unsigned char
#define At24c02ADDR 0xA0//AT24C02硬件地址
#define I2CRead 1 //I2C读方向位
#define I2CWrite 0 //I2C写方向位

sbit SCL = P2^1; //时钟
sbit SDA = P2^0; //数据
sbit DU = P2^6;//数码管段选
sbit WE = P2^7;//数码管段选
uchar num;//数码管显示的值
bit Ackflag;//应答标志位
 
//共阴数码管段选表0-9
uchar code duanxuan[]= {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F,};
//数码管位选码
uchar code weixuan[] = {0xfe, 0xfd, 0xfb};
 
void delay(uint z)
{
	uint x,y;
	for(x = z; x > 0; x--)
		for(y = 114; y > 0 ; y--); 		
} 
 
void display(uchar i)
{  static uchar wei; 		
	DU=1;
    P0=0x00;
    DU =0;
	WE = 1;//打开位选锁存器
	P0 = weixuan[wei];
	WE = 0;//锁存位选数据
	switch(wei)
	{
		case 0: DU = 1; P0 = duanxuan[i/100%10]; DU = 0; break;
		case 1: DU = 1; P0 = duanxuan[i/10%10]; DU = 0; break;	
		case 2: DU = 1; P0 = duanxuan[i%10]; DU = 0; break;		
	}
	wei++;
	if(wei == 3)
		wei = 0;
}

void timer0Init()
{
    EA = 1;	//打开总中断
	ET0 = 1;//打开定时器0中断
	TR0 = 1;	 //启动定时器0
	TMOD |= 0X01; //定时器工作模式1,16位定时模式
	TH0 = 0xED;
	TL0 = 0xFF; //定时5ms
}
/*********************************
IIC通信代码
**********************************/
void delay5us()//延时函数,大概是5.43us 
{
  _nop_();//上面要调用库函数才行
}

void I2C_Start()//启动信号函数
{
    SCL = 1;//将时钟置于高电平
	SDA = 1;//将数据也置于高电平
	delay5us();//根据时序要使SDA保持一定时间
	SDA = 0;//从高电平到低电平产生启动信号
    delay5us();//同上
}

void I2C_Stop()//函数停止函数
{
  SCL = 0;//将时钟置于低电平
  SDA = 0;//将数据也置于低电平
  SCL = 1;//让SCL置于高电平,并延时
  delay5us();
  SDA = 1;//SDA置于高电平,电平由低到高,产生停止信号,
  delay5us();
}

bit ReadACK()//I2C总线读从机应答信号
{  
   SCL = 0;//允许从机控制SDA
   SCL = 1;//拉高SCL读SDA值
   delay5us();
   if(SDA)//判断SDA状态
   {
   	 SCL = 0;
	 return(1);//返回1
   }
   else
   {
   	 SCL = 0;
	 return(0);//返回0
   }
}

void SendACK(bit i)//主机发送应答
{
  SCL = 0;///拉低时钟总线,允许主机控制SDA
  if(i)	//发非应答
     SDA = 1;
  else //发应答
     SDA = 0;
  SCL = 1;//拉高总线,让从机读SDA
  delay5us();
  SCL = 0; //拉低时钟总线,允许SDA释放
  SDA = 1; //释放数据总线
}

void I2C_SendByte(uchar DAT)//I2C发送一个字节数据
{
  uchar	i;
  for(i=0;i<8;i++)//分别写8次,每次写1位
  {
       SCL = 0;//拉低时钟总线,允许SDA变化
       if(DAT & 0x80)//先写数据最高位
           SDA = 1;
       else
            SDA = 0;
       SCL = 1;//拉高时钟,让从机读SDA
	   DAT<<=1;//为发送下一位左移1位  
  } 
  SCL = 0;//拉低时钟总线,允许SDA释放
  SDA = 1;//释放数据总线                                
}

void At24c02_Write(uchar ADDR,DAT)
{
  I2C_Start();//I2C起始信号
  I2C_SendByte(At24c02ADDR + I2CWrite);//发送器件地址加读写方向位
  if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
  else
       Ackflag = 0;//应答
  if(Ackflag == 0)
	{   
      I2C_SendByte(ADDR);//发送储存单元地址字节
       if(ReadACK())//读从机应答
          Ackflag = 1;	//没应答
       else
          Ackflag = 0;//应答
	    if(Ackflag == 0)
		{
             I2C_SendByte(DAT);//发送一字节数据
             ReadACK();
             I2C_Stop();	//I2C停止信号
	    }
   }
}

uchar I2C_Readbyte()
{
  uchar i ,DAT;
  for(i=0;i<8;i++)
  {
  	DAT<<=1;//数据左移1位,准备接收一位
	SCL = 0;//拉低时钟总线,允许从机控制SDA变化
	SCL = 1;//拉高时钟总线,读取SDA上的数据
	if(SDA)
	 DAT |= 0x01;//为1则写1 
  }
  return(DAT);//返回读出的数据
}

uchar At24c02_Read(uchar ADDR)
{
  uchar DAT;
  I2C_Start();//I2C起始信号
  I2C_SendByte(At24c02ADDR + I2CWrite);//发送器件地址加读写方向位
  if(ReadACK())//读从机应答
       Ackflag = 1;	//没应答
  else
       Ackflag = 0;//应答
	 if(Ackflag == 0)
	 {
        I2C_SendByte(ADDR);//发送一个字节
        ReadACK();
        I2C_Start();//I2C起始信号
        I2C_SendByte(At24c02ADDR + I2CRead);//发送器件地址加读写方向位
        if(ReadACK())//读从机应答
             Ackflag = 1;	//没应答
        else
            Ackflag = 0;//应答
		   if(Ackflag == 0)
		   {
               DAT = I2C_Readbyte();//读一字节
               SendACK(1);//主机发送非应答
               I2C_Stop(); //I2C停止信号
		   }
	 }
	 return(DAT);//返回读出数据
}
 
void main()//main函数自身会循环
{	
    EA = 0;//屏蔽中断
   	timer0Init();
	At24c02_Write(5,18);
	delay(2);
	num = At24c02_Read(5);
	if(Ackflag)
	 P1 = 0;
	else
	 P1 = 0xff;
	EA = 1;//开中断
	while(1);	
} 
 
//定时器0中断函数
void timer0() interrupt 1
{
	TH0 = 0xED;
	TL0 = 0xFF; //定时5ms
	display(num); //数码管显示函数	
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值