目录
概述
特点
I2C总线的主要特点是:两条线可以挂多个参与通信的器件,即多机模式,而且任何一个器件都可以作为主机,当然同一时刻只能有一个主机。
类别
I2C属于同步通信,SCL时钟线负责收发双方的时钟节拍,SDA数据线负责传输数据。
I2C的发送方和接收方都以SCL这个时钟节拍为基准进行数据的发送和接收
半双工通信
用途
I2C多用于板内通信
I2C:通信协议
E²PROM:器件
I2C时序
I2C总线是由始终总线SCL和数据总线SDA两条线构成。连接到总线上的所有器件的SCL都连到一起,所有SDA都连到一起。
I2C总线是开漏引脚并联的结构,因此外部需要添加上拉电阻。添加上拉电阻后,所有线就组成了线“与”的关系,即所有接入的器件均保持高电平,这条线才是高电平;任意一个器件都可以拉低电平,也就是任何一个器件都可以作为主机。
I2C中有起始信号、数据传输和停止信号。其中数据传输部分可以一次通信过程传输很多个字节,字节数是不受限制的,而每个字节后都跟一位,答应位,通常用ACK表示。
I2C通信,必须两条线都参与工作才能完成
起始信号:
SCL为高电平期间,SDA由高电平向低电平变化产生一个下降沿,表示起始信号
数据传输:
I2C通信是高位在前、低位在后。
I2C没有固定波特率,但是有时序要求。要求当SCL在低电平的时候,SDA允许变化。当SCL是高电平的时候,SDA绝对不可以变化,因为此时接收方要读取当前SDA的电平信号是0还是1,必须保证SDA的稳定。
停止信号:
SCL为高电平期间,SDA由低电平向高电平变化产生一个上升沿,表示结束信号。
I2C寻址模式
I2C通信在字节级的传输中也有固定的时序要求。
I2C在起始信号(Start)后,首先要发送一个从机地址,这个地址一共有7位;紧跟着第8位,数据方向位(R/W),“0”表示接下来要发送数据(写),“1”表示接下来是请求数据(读)。
若是发送的地址存在,从机SDA输出“0”;如果不存在,从机SDA输出“1”,即应答信号ACK
主机向从机发送数据,数据传输方向在整个过程中不变:
S | 从机地址 | 0 | A | 数据 | A | 数据 | A/A非 | P |
主机在第一个字节后,立即从从机读取数据:
S | 从机地址 | 1 | A | 数据 | A | 数据 | A/A非 | P |
在传输过程中,当需要改变方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好相反:
S | 从机地址 | 0 | A | 数据 | A/A非 | S | 从机地址 | 1 | A | 数据 | A非 | P |
24C01(E²PROM器件)
24C02和24C01原理图完全一样
24C02一共256个字节的存储空间,地址从0x00~0xff
24C02的七位地址中,其中高4位是固定的0b1010,
低3位的地址取决于电路设计,由A0、A1、A2的实际电平决定
E²PROM写数据流程:
第一步:首先是I2C的起始信号
紧接着跟上I2C的器件地址
在读写方向上选择“写”操作
第二步:发送数据的存储地址
第三步:发送要存储的数据,每个字节都会回应一个“应答位0”,来告诉我们写E²PROM数据成功,若是没有应答位,说明写入不成功。
注:每成功写入一个字节,E²PROM存储空间的地址都会自动加1,当加到0xff,再加则会溢出,变为0x00。
E²PROM读数据流程:
第一步:首先是I2C的起始信号
紧接着跟上I2C的器件地址
在读写方向上选择“写”操作
第二步:发送要读取的数据的地址
第三步:再次发送I2C的起始信号
紧接着跟上I2C的器件地址
在读写方向上选择“读”操作
第四步:读取完一个字节,如果想要接着读取,就发送一个“应答位ACK(0)”,若是不想读了,就发送一个“非应答位NAK(1)”
注:无论是读还是写,SCL始终都是由主机控制的
写的时候,应答信号由从机发出,表示从机是否正确的接收了数据
读的时候,应答信号由主机发出,表示是否继续读下去
E²PROM的页写入
由于一个字节一个字节的写效率过低,所以厂商就对E²PROM进行了分页管理
24C01、24C02这两个型号是8个字节一个页
24C04、24C08、24C16是16个字节一个页
分配好页之后,就可以一次性的把一页的数据写入非易失区域,然后再发送一个停止位,再进行空闲检测,发送下一页。
按时序写的程序
#include <reg52.h>
#include <intrins.h>
#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;
/* 产生总线起始信号 */
void I2CStart()
{
I2C_SDA = 1; //首先确保SDA、SCL都是高电平
I2C_SCL = 1;
I2CDelay();
I2C_SDA = 0; //先拉低SDA
I2CDelay();
I2C_SCL = 0; //再拉低SCL
}
/* 产生总线停止信号 */
void I2CStop()
{
I2C_SCL = 0; //首先确保SDA、SCL都是低电平
I2C_SDA = 0;
I2CDelay();
I2C_SCL = 1; //先拉高SCL
I2CDelay();
I2C_SDA = 1; //再拉高SDA
I2CDelay();
}
/* I2C总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2CWrite(unsigned char dat)
{
bit ack; //用于暂存应答位的值
unsigned char mask; //用于探测字节内某一位值的掩码变量
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
if ((mask&dat) == 0) //该位的值输出到SDA上
I2C_SDA = 0;
else
I2C_SDA = 1;
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL,完成一个位周期
}
I2C_SDA = 1; //8位数据发送完后,主机释放SDA,以检测从机应答
I2CDelay();
I2C_SCL = 1; //拉高SCL
ack = I2C_SDA; //读取此时的SDA值,即为从机的应答值
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
//0=不存在或忙或写入失败,1=存在且空闲或写入成功
}
/* I2C总线读操作,并发送非应答信号,返回值-读到的字节 */
unsigned char I2CReadNAK()
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先确保主机释放SDA
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //读取SDA的值
dat &= ~mask; //为0时,dat中对应位清零
else
dat |= mask; //为1时,dat中对应位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使从机发送出下一位
}
I2C_SDA = 1; //8位数据发送完后,拉高SDA,发送非应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成非应答位,并保持住总线
return dat;
}
/* I2C总线读操作,并发送应答信号,返回值-读到的字节 */
unsigned char I2CReadACK()
{
unsigned char mask;
unsigned char dat;
I2C_SDA = 1; //首先确保主机释放SDA
for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行
{
I2CDelay();
I2C_SCL = 1; //拉高SCL
if(I2C_SDA == 0) //读取SDA的值
dat &= ~mask; //为0时,dat中对应位清零
else
dat |= mask; //为1时,dat中对应位置1
I2CDelay();
I2C_SCL = 0; //再拉低SCL,以使从机发送出下一位
}
I2C_SDA = 0; //8位数据发送完后,拉低SDA,发送应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线
return dat;
}
按照E²PROM读写流程写的程序
/* 读取EEPROM中的一个字节,addr-字节地址 */
unsigned char E2ReadByte(unsigned char addr)
{
unsigned char dat;
I2CStart();
I2CWrite(0x50<<1); //寻址器件,后续为写操作
I2CWrite(addr); //写入存储地址
I2CStart(); //发送重复启动信号
I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
dat = I2CReadNAK(); //读取一个字节数据
I2CStop();
return dat;
}
/* 向EEPROM中写入一个字节,addr-字节地址 */
void E2WriteByte(unsigned char addr, unsigned char dat)
{
I2CStart();
I2CWrite(0x50<<1); //寻址器件,后续为写操作
I2CWrite(addr); //写入存储地址
I2CWrite(dat); //写入一个字节数据
I2CStop();
}
多字节读写程序
/* E2读取函数,buf-数据接收指针,addr-E2中的起始地址,len-读取长度 */
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len)
{
do { //用寻址操作查询当前是否可进行读写操作
I2CStart();
if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
} while(1);
I2CWrite(addr); //写入起始地址
I2CStart(); //发送重复启动信号
I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
while (len > 1) //连续读取len-1个字节
{
*buf++ = I2CReadACK(); //最后字节之前为读取操作+应答
len--;
}
*buf = I2CReadNAK(); //最后一个字节为读取操作+非应答
I2CStop();
}
/* E2写入函数,buf-源数据指针,addr-E2中的起始地址,len-写入长度 */
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len)
{
while (len--)
{
do { //用寻址操作查询当前是否可进行读写操作
I2CStart();
if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
} while(1);
I2CWrite(addr++); //写入起始地址
I2CWrite(*buf++); //写入一个字节数据
I2CStop(); //结束写操作,以等待写入完成
}
}
按页写程序
读的程序没有改变,依旧是一个字节一个字节的读取
/* E2写入函数,buf-源数据指针,addr-E2中的起始地址,len-写入长度 */
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len)
{
while (len > 0)
{
//等待上次写入操作完成
do { //用寻址操作查询当前是否可进行读写操作
I2CStart();
if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
} while(1);
//按页写模式连续写入字节
I2CWrite(addr); //写入起始地址
while (len > 0)
{
I2CWrite(*buf++); //写入一个字节数据
len--; //待写入长度计数递减
addr++; //E2地址递增
if ((addr&0x07) == 0) //检查地址是否到达页边界,24C02每页8字节,
{ //所以检测低3位是否为零即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2CStop();
}
}