【单片机】C51中的I2C操作-Proteus+Keil4+C语言实现

51单片机中I2C读写操作

开发板:普中51-单核-A2;

I2C器件-EEPROM:AT24C02、FM24C02(仿真EEPROM);

仿真软件:proteus;

开发环境:Keil4;

参考资料:开发板所附视频;

如有错误,感谢指正。如有侵权请联系博主。


首先需要了解I2C是什么。

1. I2C是什么

I2C Inter-Integrated circuit的简称,飞利浦公司于1980年代提出,为了让主板、嵌入式系统或手机用以连接低速周边外部设备而发展。

I2C具备多主机系统需要的总线裁决、高低速器件同步等功能,是一种高性能的串行总线。

串行总线用于串行通信,串行通信速率低,在数据通信吞吐量不是很大的微处理器中比较简易、方便、灵活;

并行总线用于并行通信,并行通信速度快,实时性好,但是占用的口线多,不适于小型化产品;

更多关于总线的内容可以参考:总线的定义, 并行总线和串行总线

I2C允许相当大的工作电压,典型的电压基准为+3.3V或+5V。

I2C总线有两根双向信号线:SDASCL,SDA是数据线,用来传送数据;SCL是时钟线,用来传输CLK时钟信号,主设备向从设备发送。

如下所示,在I2C总线上可以连接多个器件,通常这些器件中有一个是主器件,其它的是从器件。

每个接到I2C总线上的器件都有唯一的地址,可以通过该地址识别主机想要通信的是哪个器件。

在这里插入图片描述

在多主机系统中,可能同时有几个主机想要主动传送数据,为了避免混乱,I2C总线通过总线仲裁,决定由哪一台主机控制总线。

80C51单片机应用系统中,通常是以80C51为主机,其它接口器件为从机的单主机情况。

I2C的模式

常用到的I2C总线以传输速率不同分为:

  • 标准模式:100Kbit/s
  • 低速模式:10Kbit/s
  • 快速模式:400Kbit/s
  • 高速模式:3.4Mbit/s

I2C的特征

I2C有如下特征:

  • 串行通信:所有的数据以bit为单位在SDA线上串行传输;
  • 同步通信:双方在用一个时钟下通信。同步通信的特征是通信线中有时钟信号CLK
  • 非差分:I2C速率不高,并且通信距离近,使用电平信号通信;
  • 低速率:I2C通常是同一个板子上的两个IC芯片间通信,数量不大,速率低。

I2C的总线状态

I2C总线有两种状态:空闲态和忙态。

  • 空闲态:没有设备发生通信,此时SDA和SCL均处于高电平状态;
  • 忙态:其中一个从设备和主设备通信,I2C总线被占用,其它从设备处于等待状态;

I2C总线通过上拉电阻接正电源,这样可以保证当总线空闲时,两根线均为高电平。连接到总线上的任一器件输出低电平,都将使总线信号变低,各器件的SDA和SCL是线的关系。

I2C数据传送

数据有效位规定
  • 进行数据传送时,时钟线SCL为高电平期间,数据线SDA保持稳定;
  • 只有时钟线SCL为低电平时,SDA上的高低电平才允许变化;
    在这里插入图片描述
起始信号和终止信号

起始信号和终止信号(由主机发出):

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

接收端将SCL拉低,可以使主机处于等待状态;接收端将SCL拉高,可以使主机工作。

在这里插入图片描述

起始信号产生后,总线就处于忙态(被占用);终止信号产生后,总线处于空闲态。

字节传送与应答
  • 每个字节必须保证是8位,先传送最高位MSB,每一个被传送的字节后面都必须跟随一个应答位,即一帧有9位。

在这里插入图片描述

  • 如果从机在做其他操作等原因不能对主机寻址信号应答时,从机必须将SDA置为高电平,由主机产生终止信号结束数据传送;
  • 如果从机应答了主机,但数据传送一段时间后无法再接收更多数据,从机可以通过对无法接收的第一个数据字节的非应答通知主机,主机则应发出终止信号结束数据的继续传送;
  • 当主机接收数据,收到最后一个数据字节后,必须向从机发送结束传送信号,这个信号由对从机的非应答来实现的。然后从机释放SDA线,允许主机产生终止信号;
数据帧格式

I2C总线上传送的数据信号既包括地址信号,又包括数据信号。

注:如下图中数据传送方向,阴影表示数据由主机向从机传送,无阴影部分表示数据由从机向主机发送,A表示应答(低电平), A ‾ \overline{A} A表示非应答(高电平),S表示起始信号,P表示终止信号。

在这里插入图片描述

起始信号后必须传送一个7位的从机地址+1位数据传送方向,0表示主机发送数据,1表示主机接收数据。

然后可以传送一个字节的数据,每字节数据后会收到1位应答信号,数据传送结束会收到非应答信号。

每次数据传送都是由主机产生终止信号结束。但是如果主机希望继续占用总线进行新的数据传送,可以不产生终止信号,马上再次发出起始信号对另一个从机寻址。

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

  • 主机向从机发送数据,数据传送方向在整个传送过程中不变;

在这里插入图片描述

  • 主机接收从机的数据,主机发送完从机的地址后,立即从从机中读数据;

在这里插入图片描述

  • 传送过程中需要改变方向时,其实信号和从机地址都被重复产生一次,但两次读/写方向位相反;

在这里插入图片描述

总线寻址

I2C协议规定,采用7位的寻址字节,寻址字节的位定义:

在这里插入图片描述

D7~D1是从机地址,D0表示数据传送方向,0表示主机向从机写数据,1表示主机由从机读数据。

从机的地址由固定部分和可编程部分组成。

总线信号模拟

主机可以采用不带I2C总线接口的单片机,使用软件实现I2C总线的数据传送,即软件和硬件结合的信号模拟。

为保证数据传送的可靠性,标准的I2C总线的数据传送有严格的时序要求。如下。

  • 起始信号需要SCL&SDA保持高电平>4.7us后,SDA由高电平到低电平>4us;

  • 终止信号需要TSCL=H&SCL=L >4us,TSCL=H&SDA=L->H>4/7us;

  • 应答信号需要TSCL=H&SDA=0>4us;

  • 非应答信号需要TSCL=H&SDA=H > 4us;

在这里插入图片描述

数据写入

单片机操作时,首先发送该器件的7位地址码和1位写方向位0(共1个字节,8位),发送完后释放SDA线(拉高SDA),并在SCL线上产生第9个时钟信号,从器件产生应答信号作为确认是自己的地址,单片机收到应答后就可以传送数据了。

传送数据时,单片机首先发送一个字节的被写入器件的存储区的首地址,收到应答后,主机逐个发送各数据字节,每发送一个字节都要等待应答。

当写入的数据传送完后,主机发出终止信号以结束写入操作.

写入n个字节的数据格式如下:

在这里插入图片描述

数据读出

单片机操作时,首先发送该器件的7位地址码和1位写方向位0(共1个字节,8位),发送完后释放SDA线(拉高SDA),并在SCL线上产生第9个时钟信号,从器件产生应答信号作为确认是自己的地址。

然后再发送要读出器件的存储区的首地址,收到应答后,主机要重复一次起始信号并发出器件地址和读方向位,收到器件应答后就可以读出数据字节。

没读出一个字节,主机都要回复应答信号,当最后一个字节数据读完,主机返回非应答信号,并发出终止信号以结束读数据操作。

在这里插入图片描述

2. 模拟实现I2C读写数据

板子上的EEPROM是 AT24C02,其原理图如下:

在这里插入图片描述

实现功能:

  1. 设置4个独立按键K1,K2,K3,K4,分别用于I2C写入、I2C读取、数据累加、数据清零;
  2. 设置一个LCD数码管显示器,用于显示读取的数据;

原理图如下:

在这里插入图片描述

main.c

#include "reg52.h"
#include "i2c.h"

typedef unsigned char u8;
typedef unsigned int u16;

sbit K1 = P3^1;  // K1 保存显示的数据
sbit K2 = P3^0;  // K2 读取保存的数据					   
sbit K3 = P3^2;  // K3 累加读取的数据 
sbit K4 = P3^3;  // K4 清零

// 38 译码器
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4;

// 数码管段选数据
u8 code smgduan[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};  // 显示0~9的值
char num=0,disp[4];

// 延时函数
void delay(u16 i)
{
	while(i--);
}

// 按键处理函数
void Keypros()
{
	if(K1==0) // 按键按下
	{
		delay(1000);
		if(K1==0)  // 按键按下
		{
			AT24C02Write(1,num);  // 写入数据
		}
		while(!K1);   // 独立按键是否松开	
	}

	if(K2==0) // 按键按下
	{
		delay(1000);
		if(K2==0)  // 按键按下
		{
			num = (char)AT24C02Read(1);	  // 读取数据
		}
		while(!K2);   // 独立按键是否松开	
	}
		
	if(K3==0) // 按键按下
	{
		delay(1000);
		if(K3==0)  // 按键按下
		{
			num++;  // 累加
			if(num > 255)
				num=0;
		}
		while(!K3);   // 独立按键是否松开	
	}

	if(K4==0) // 按键按下
	{
		delay(1000);
		if(K4==0)  // 按键按下
		{
			num=0;  // 清零
		}
		while(!K4);   // 独立按键是否松开	
	}
}

void datapros()
{
    // 4~0分别存储个、十、百、千位
	disp[0] = smgduan[num/1000];
	disp[1] = smgduan[num%1000/100];
	disp[2] = smgduan[num%1000%100/10];
	disp[3] = smgduan[num%1000%100%10];
}

// 数码管显示
void DigDisplay()
{
 	u8 i = 0;
	for(i=0; i<4; i++){
	 	switch(i){
		 case 0:
		 	LSA = 1;LSB = 1; LSC = 1; break;  //显示第0位
		 case 1:
		 	LSA = 0;LSB = 1; LSC = 1; break;  //显示第1位
		 case 2:
		 	LSA = 1;LSB = 0; LSC = 1; break;  //显示第2位
		 case 3:
		 	LSA = 0;LSB = 0; LSC = 1; break;  //显示第3位
		 
		 }

		P0 = disp[i];  // 发送段码
		delay(100);  // 间隔一段时间扫描
		P0 = 0x00;  //消隐
	}
}

void main()
{
	while(1)
	{
		Keypros();
		datapros();
		DigDisplay();
	}  
}

i2c.h

#ifndef _I2C_H
#define _I2C_H

#include <reg52.h>

// 定义时钟线和数据线
sbit SCL = P2^1;
sbit SDA = P2^0;

void I2CStart();
void I2CStop();
unsigned char I2CWriteByte(unsigned char dat);
unsigned char I2CReadByte();
void AT24C02Write(unsigned char addr,unsigned char dat);
unsigned char AT24C02Read(unsigned char addr);

#endif

i2c.c

#include "i2c.h"

// 单片机管脚模拟时许

// 延时10us
void Delay10us(void)
{
	unsigned char a,b;
	for(b=1;b>0;b--)
		for(a=2;a>0;a--);
}

// 模拟起始信号
void I2CStart()
{
	// SCL=H,SCL=H->L,delay4.7us
	SDA = 1;
	Delay10us();
	SCL=1;	// SDA/SCL = H
	Delay10us();
	SDA=0;	// SDA H->L
	Delay10us();
	SCL=0;  // SCL=L时,数据可以改变   
	Delay10us();
}

// 模拟终止信号
void I2CStop()
{
	// SCL=H,SCL=L->H,delay4.7us
	SDA = 0;
	Delay10us();
	SCL=1;
	Delay10us();
	SDA=1;	// SDA L->H
	Delay10us();
}

// 模拟发送数据
unsigned char I2CWriteByte(unsigned char dat)
{
	unsigned char a=0, b=0;
	for(a=0;a<8;a++)
	{
		SDA = (dat>>7);  // 传送最高位
		dat = (dat<<1);  // 替换最高位
		Delay10us();
		SCL = 1;  // SCL=H, 数据保持稳定
		Delay10us();
		SCL = 0;  // SCL=L, 数据可以改变
		Delay10us();
	}
	
	SDA=1;  // 释放时钟线和数据线
	Delay10us();
	SCL=1;
	// Delay10us();
	
	while(SDA)   // 应答信号SDA=0 ,非应答SDA=1
	{	
		 b++;
		 if(b>200)  // 非应答超过200次,至少200us
		 {
		 	SCL=0;   // 拉低时钟线
			Delay10us();
			return 0;   // 表示发送信号失败
		 }
	}
	
	SCL=0;
	Delay10us();
	return 1;	
}

// 模拟接收数据
unsigned char I2CReadByte()
{
	unsigned char a=0, dat=0;
	SDA=1;  // SDA=1拉高使其处于空闲状态
	Delay10us();
	for(a=0;a<8;a++)
	{
		SCL=1;  // 保持数据稳定
		Delay10us();
		dat <<= 1;  // data=0 <<1  ->  0
		dat |= SDA;
		Delay10us();
		SCL=0;  // SDA数据可以改变
		Delay10us();
	}

	return dat;
}

// AT24C02 EEPROM 读写函数
void AT24C02Write(unsigned char addr, unsigned char dat)
{
	// 1. 起始信号
	I2CStart();

	// 2. 器件地址
	I2CWriteByte(0xa0);

	// 3. 写入的首地址
	I2CWriteByte(addr);

	// 4. 写入数据
	I2CWriteByte(dat);

	// 5. 停止信号
	I2CStop();			
}

unsigned char AT24C02Read(unsigned char addr)
{
	unsigned char dat=0;
	// 1. 起始信号
	I2CStart();

	// 2. 器件地址
	I2CWriteByte(0xa0);

	// 3. 写入的首地址
	I2CWriteByte(addr);

	// 再次发送器件地址时,需要重新发送起始信号
	// 4. 起始信号  
	I2CStart();

	// 5. 器件地址
	I2CWriteByte(0xa1);

	// 6. 读取数据
	dat = I2CReadByte();

	// 5. 停止信号
	I2CStop();
	
	return dat;			
}

结果:初始读取为7是因为之前有进行过I2C写入,默认没有对I2C操作前读取到的数据是0。

在这里插入图片描述

出现的问题:

  1. 刚开始定义数据时使用data,编译报错I2C.H(13): error C141: syntax error near ')';原因是不能声明变量名为data,data是C51中的关键字。C51中的关键字data,idata,xdata,pdata,bdata
  2. proteus绘制总线使用方法:proteus中的标签及总线的使用方法
  3. 独立按键:51单片机 4个独立按键控制LED灯 (protues仿真)(C语言版)
  • 2
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值