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总线有两根双向信号线:SDA
和SCL
,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,其原理图如下:
实现功能:
- 设置4个独立按键K1,K2,K3,K4,分别用于I2C写入、I2C读取、数据累加、数据清零;
- 设置一个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.7usSDA = 1;Delay10us();SCL=1; // SDA/SCL = HDelay10us();SDA=0; // SDA H->LDelay10us();SCL=0; // SCL=L时,数据可以改变 Delay10us();
}// 模拟终止信号
void I2CStop()
{// SCL=H,SCL=L->H,delay4.7usSDA = 0;Delay10us();SCL=1;Delay10us();SDA=1; // SDA L->HDelay10us();
}// 模拟发送数据
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 -> 0dat |= 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。
出现的问题:
- 刚开始定义数据时使用data,编译报错
I2C.H(13): error C141: syntax error near ')'
;原因是不能声明变量名为data,data是C51中的关键字。C51中的关键字data,idata,xdata,pdata,bdata。 - proteus绘制总线使用方法:proteus中的标签及总线的使用方法。
- 独立按键:51单片机 4个独立按键控制LED灯 (protues仿真)(C语言版)。