一、I2C概述
I2C(Intel Interface Circuit,简称IIC、I2C),是应用广泛的芯片间串行扩展总线。由飞利浦公司开发用于微控制器(MCU)和外围设备(从设备)进行通信的一种总线,属于一主多从(一个主设备(Master),多个从设备(Slave))的总线结构,总线上的每个设备都有一个特定的设备地址,以区分同一I2C总线上的其它设备。
I2C支持多主机,即I2C允许两个或多个主设备在一个I2C上竞争,此时将会通过仲裁决定哪个主设备获取使用权。
1.1 基本结构
I2C是一个同步半双工硬件层次的串行通信协议、I2C总线总共只有两条信号线,串行时钟线SCL和串行数据线SDA。I2C总线上的各器件的数据线都接到SDA线上,I2C总线上的各器件的时钟线都接到SCL线上。
每个连接到I2C总线上的器件都有一个唯一的地址,扩展器件时受到器件数量的限制,I2C总线的基本结构如图所示:
当I2C总线空闲时,SDA和SCL均为高电平。由于连接到总线上的器件输出级必须是漏极开路或者集电极开路的,因此只要有一个器件任意时刻输出低电平,都将总线上的信号都将总线上的信号变低,即各器件的SDA和SCL都是“线与“的关系。
由于各器件输出端为漏极开路,则SCL和SDA线必须通过上拉电阻接正电源,以保证SDA和SCL在空闲状态被上拉到高电平。
关于是么是漏极开路,如果你已经忘记了数电技术,这里有个简单的介绍:
漏极开路输出需要接一个上拉电阻(上图中的R),可以利用改变上拉电源的电压,改变输出电平。上拉电阻是接在输出引脚和输出电压(上图中的Vcc)之间,可以获得高电平输出:
- 当内部N沟道场效应管关闭的时候,上拉电阻R会把输出拉到高电平,此时场效应管的漏电流将非常的小;
- 当内部N沟道场效应管导通的时候,它会把输出引脚拉到接近GND,此时的电流是根据欧姆定律计算的 (I = Vcc/R);
1.2 优缺点
I2C总线的优点:
-
简单性(I2C只有两条信号线)和有效性(根据SCL线上的时钟线来判断):带有I2C总线的接口的单片机都可直接与具有I2C总线接口的各种扩展器件(如存储器、I/O芯片、A/D、D/A、键盘、显示器、时钟、日历等)连接。由于I2C总线采用纯软件的寻址方法,无须片选线的连接,这样就大大的简化了总线的数量;
- 允许多主器件。任何能够进行发送和接收的设备都可以成为主总线,一个主控能够控制信号的传输和时钟频率,在任何时间点上只能有一个主控。在实际应用中,经常遇到的是以单一单片机为主器件,其他外围接口期间为从器件的情况;
I2C的缺点:
- I2C数据传输速率有标准模式100kbps,快速模式400kbps和高度模式3.4Mbps,另外一些变种实现了低速模式10kbps和快速+模式1Mbps。
-
I2C所接的外围器件的数量并不是无穷多的,总线上的扩展的期间数量是由限制的。总线上的扩展的器件数量并不是由电流负载决定的,而是由电容负载决定的。I2C总线上的每个器件的接口都有一定的等效电容,器件越多(扩展外围器件可以看作是并联在主器件的总线上的),电容值就越大,就会造成信号传输的延迟;
- 总线上允许接的器件数量以器件的电容量不能超过400pF(可以通过其他的方法增加总线的电容量),据此可以算出总线的长度和扩展外围器件的数量;
二、I2C通信协议
I2C总线在传送数据过程中一共有四种类型的信号,分别是起始信号,结束信号、应答信号以及有效数据。
2.1 起始信号
I2C通信的起始信号由主设备发起,SCL保持高电平,SDA由高电平变为低电平;只有在起始信号发送之后,其它数据才有效;
GPIO模拟代码:
void I2C_Start(void) { SDA_OUT(); I2C_SDA =1; delay_us(5); I2C_SCL =1; delay_us(5); I2C_SDA =0; delay_us(5); I2C_SCL =0; }
2.2 终止信号
I2C通信的终止信号由主设备发起,SCL保持高电平,SDA由低电平变为高低电平;随着终止信号的出现,所有外部操作就结束;
GPIO模拟代码:
void I2C_Stop(void) { SDA_OUT(); I2C_SDA =0; delay_us(5); I2C_SCL =1; delay_us(5); I2C_SDA =1; delay_us(5); I2C_SCL =0; }
2.3 应答信号
I2C总线在进行数据传送时,传送的字节数没有限制,但是每个字节长度必须为8位。
数据传送过程中,先传送最高位(MSB),接收端在收到有效数据后向对方相应的信号,发送端每发送一个字节数据(8位),在第9个始终周期释放数据线去接收对方的应答;因此一帧数据共有9位;
- 当SDA位低电平位有效应答(ACK),表示接收端已经接收到数据;
- 当SDA是高电平位无效应答(NAK),表示接收端没有接收成功;
主设备发送完数据需要等待从设备的应答,GPIO模拟:
// 0:接受失败 1:接收成功 u8 I2C_WaitAck(void) //等待来自从机的应答信号 { u16 time; SDA_IN(); I2C_SCL =0; delay_us(5); I2C_SDA =1; delay_us(5); I2C_SCL =1; delay_us(5); while(Read_SDA) { time++; if(time>=2500) { I2C_Stop(); return 0; } } I2C_SCL =0; return 1; }
主设备接收到从设备发送的数据后,需要向从设备发送方发送应答,GPIO模拟:
// ack:0不应答 1:应答 void I2C_Ack(u8 ack) { SDA_OUT(); I2C_SCL =0; delay_us(5); if(ack) I2C_SDA =0; else I2C_SDA =1; I2C_SCL =1; delay_us(5); I2C_SCL =0; delay_us(5); }
2.4 有效数据
I2C总线进行数据传送时,时钟信号SCL为高电平期间,数据线SDA上的数据必须稳定;只有在SCL上的信号为低电平时,SDA上的高电平或低电平状态才允许变化。
因为当SCL是高电平时,数据线SDA的变化被规定为控制命令(也就是前面的起始信号和终止信号)。
主设备向从设备发送发送一个字节数据,GPIO模型模拟:
void I2C_WriteData(u8 byte) { u8 i=0; SDA_OUT(); I2C_SCL =0; for(i=0;i<8;i++) { I2C_SDA =(byte&0x80)>>7; byte <<=1; delay_us(5); I2C_SCL =1; delay_us(5); I2C_SCL =0; delay_us(5); } }
主设备从从设备读取一个字节数据,GPIO模拟:
u8 I2C_ReadData(void) { u8 i=0; u8 data=0; SDA_IN(); I2C_SCL =0; for(i=0;i<8;i++) { I2C_SCL =1; data <<=1; data |=(u8)Read_SDA; delay_us(5); I2C_SCL =0; delay_us(5); } return data; }
三、主从设备通信过程
在前面我们提到过,I2C总线上的每一个设备都对应一个唯一的地址,主从设备之间的数据传输是建立在地址的基础上,也就是说,主设备在传输有效数据之前要先指定从设备的地址,地址指定的过程和数据传输的过程一样,只不过大多数从设备的地址是7位的,然后协议规定再给地址添加一个最低位用来表示接下来数据传输的方向,0表示主设备向从设备写数据,1表示主设备向从设备读数据。
3.1 主设备向从设备写入数据
数据格式如下:
流程如下:
- 主设备发送START起始信号;
- 主设备发送从设备地址信息(I2C addr(7bit)和w操作0(1bit)),等待ACK确认信号;
- 从设备发送ACK确认信号;
- 主设备发送数据到从设备,一般发送的每个字节数据后会跟着等待接收来自从设备的响应(ACK);
- 数据发送完毕,主设备发起STOP终止信号;
3.2 主设备从从设备读取数据
数据格式如下:
流程如下:
- 主设备发送START信号;
- 主设备发送从机设备地址信息(I2C addr(7bit)和r操作1(1bit)),等待ACK确认信号;
- 从设备发送ACK确认信号;
- 主设备接收来自从设备的数据,一般接收的每个字节数据后会跟着向从设备发送一个响应(ACK);
- 主设备一般接收到最后一个数据后会发送一个无效响应(NACK),然后主设备发送停止(STOP)信号终止传输;
3.3 主设备向从设备读/写数据
在数据传输过程中,可能需要改变数据传送方向时;起始信号和从机地址都需要重复产生一次;
四、I2C通信的实现
4.1 I2C控制器
就是使用芯片上的I2C外设,也就是硬件I2C,它有相应的I2C驱动电路,有专用的I2C引脚,效率更高,写代码会相对简单,只要调用I2C的控制函数即可,不需要用代码去控制SCL、SDA的各种高低电平变化来实现I2C协议,只需要将I2C协议中的可变部分(如:从设备地址、传输数据等等)通过函数传参给控制器,控制器自动按照I2C协议实现传输,但是如果出现问题,就只能通过示波器看波形找问题。
4.2 GPIO模拟时序
软件模拟I2C比较重要,因为软件模拟的整个流程比较清晰,哪里出来bug,很快能找到问题,模拟一遍会对I2C通信协议更加熟悉。
如果芯片上没有I2C控制器,或者控制接口不够用了,通过使用任意IO口去模拟实现IIC通信协议,手动写代码去控制IO口的电平变化,模拟I2C协议的时序,实现I2C的信号和数据传输。
五、AT24C01/02/04/08/16
AT24C01/02/04/08是一个1K/2K/4K/8K/16K 位串行可擦可编程只读存储器(EEPRAM),内部含有128/256/512/1024/2048个8位字节,AT24C01有一个8字节页写缓冲器,AT24C02/04/08/16有一个16字节页写缓冲区,该器件通过I2C总线接口进行操作,有一个专门的写保护功能。
5.1 引脚配置
- A0、A1、A2:器件地址选择;
- SDA:串行数据线;
- SCL:串行地址线;
- WP:写保护;当WP为高电平时进入写保护状态;
- VCC:+1.8~6.0V工作电压;
- GND:地;
5.2 设备地址
A0、A1、A2对应器件的引脚1,2,3;a8、a9、a10空间存储块BLOCK选择;;
这里我们以AT24C08型号为例介绍,其设备地址如下:
固定值 | 器件选择 | 器件BLOCK选择 | 读写操作 | ||||
1 | 0 | 1 | 0 | A2 | a9 | a8 | R/W |
A |
0:选择第一片 1:选择第二片 |
00:BLOCK0 01:BLOCK2 10:BLOCK2 11:BLOCK3 |
R:1 W:0 |
由于I2C每次传输的数据都是按8字节传输的,因此在写地址的范围为0~0xFF,AT24C08将存储区划分为4个BLOCK,每个BLOCK大小为256字节。
AT24C08每一个BLOCK设备地址分别为:
- 第一块区域:0x50;如果将读写位组合在一起,进行读操作,地址为0xA1;进行写操作,地址为0xA0;
- 第二块区域:0x51;如果将读写位组合在一起,进行读操作,地址为0xA3;进行写操作,地址为0xA2;
- 第三块区域:0x52;如果将读写位组合在一起,进行读操作,地址为0xA5;进行写操作,地址为0xA4;
- 第四块区域:0x53;如果将读写位组合在一起,进行读操作,地址为0xA7;进行写操作,地址为0xA6;
5.3 字节写
流程如下:
- 主设备发送START起始信号;
- 主设备向AT24C08发送地址信息(I2C addr(7bit)和w操作0(1bit)),等待ACK确认信号;
- AT24C08发送ACK确认信号;
- 主设备向AT24C08发送字节地址(范围0~0xFF,发送这个存储器地址就是告诉AT24C08接下来的数据改存储到哪个地方),等待ACK确认信号;
- AT24C08发送ACK;
- 主设备向AT24C08发送一个字节数据;等待ACK确认信号;
- AT24C08发送ACK;
- 主设备发起STOP终止信号;
GPIO模拟:
void Write24c08( u16 address,u8 byte) // 地址0~1023 { u8 block = address / 256; I2C_Start(); I2C_WriteData(0xA0 + block*2); I2C_WaitAck(); I2C_WriteData(address % 256); I2C_WaitAck(); I2C_WriteData(byte); I2C_WaitAck(); I2C_Stop(); delay_ms(10); //这个延时一定要足够长,否则会出错。因为24c08在从sda上取得数据后,还需要一定时间的烧录过程。 }
5.4 页写
流程如下:
- 主设备发送START起始信号;
- 主设备向AT24C08发送地址信息(I2C addr(7bit)和w操作0(1bit)),等待ACK确认信号;
- AT24C08发送ACK确认信号;
- 主设备向AT24C08发送字节地址(范围0~0xFF,发送这个存储器地址就是告诉AT24C08接下来的数据存储到哪个地方),等待ACK确认信号;
- AT24C08发送ACK;
- 主设备可以循环向AT24C08发送16字节数据;AT24C08的页缓冲区是16个字节,所有这里的循环最多也只能发送16个字节,多发送的字节会将前面的覆盖掉。需要注意的地方: 这个页缓冲区的寻址也是从0开始,比如: 0~15算第1页,16~32算第2页......依次类推。 如果现在写数据的起始地址是3,那么这一页只剩下13个字节可以写;并不是说从哪里都可以循环写16个字节。
- 主设备发起STOP终止信号;
5.5 随机读
流程如下:
- 主设备发送START起始信号;
- 主设备向AT24C08发送地址信息(I2C addr(7bit)和w操作0(1bit)),等待ACK确认信号;
- AT24C08发送ACK确认信号;
- 主设备向AT24C08发送字节地址(范围0~0xFF,发送这个存储器地址就是告诉AT24C08接下来的读取哪个地址的数据),等待ACK确认信号;
- AT24C08发送ACK;
- 主设备重新发送START起始信号;
- 主设备向AT24C08发送地址信息(I2C addr(7bit)和r操作1(1bit)),等待ACK确认信号;
- AT24C08发送ACK确认信号;
- 主设备接收来自AT24C08的数据;接收到数据后会向AT24C08发送一个无效响应(NACK);
- 主设备发起STOP终止信号;
GPIO模拟:
u8 Read24c08(u16 address) // 地址0~1023 { u8 byte; u8 block = address / 256; I2C_Start(); I2C_WriteData(0xA0 + block * 2); I2C_WaitAck(); I2C_WriteData(address % 256); I2C_WaitAck(); I2C_Start(); I2C_WriteData(0xA1 + block * 2); I2C_WaitAck(); byte=I2C_ReadData(); //I2C_Ack(0); I2C_Stop(); delay_ms(10); return byte; }
除此之外,AT24C08还支持立即读,顺序读,这里就不介绍了,有兴趣可以研究一下datasheet。
5.6 连续读/写
针对于字节写如果我们进行循环操作,就可以实现连续写入多个字节数据:
void Write_24c08Buffer(u16 address,const u8 *str) // 需要注意,不能超出EEPROM内存大小 { u16 i=0; u16 length=0; length= strlen((char *)str); for(i=0;i<length;i++) { Write24c08(address++,str[i]); } }
针对于随机读如果我们进行循环操作,就可以实现连续读取多个字节数据:
void Read_24c02Buffer(u16 address,u8 *str,u16 length) // 需要注意,不能超出EEPROM内存大小 { u16 i=0; for(i=0;i<length;i++) { str[i]=Read24c08(address++); } str[i]='