首先,来自百度百科是如下描述的。
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
一般是用于连接微控制器及其外围设备,由两根线组成,分别是:时钟线SDA和数据线SCL ,在CPU和被控IC或是IC与IC之间双向传递,高速的IC总线一般可以达到400kbps以上。
事实上,I2C总线通信实际上是一种通信协议,通过一种已经制定好的约束关系在两个储存器之间来回的传输需要的数据。那么这种协议或者说这种约束的具体内容是怎么样的呢,在有了数据线SCL和时钟线SCL之后是如何进行数据的传输的呢?
我们知道有时钟线也就相当于有了总指挥,而数据线是需要在时钟线处于不同的电平的时候做出不同的反应来表示几种数据传输中需要的标志,比如说,传输一个数据,什么时候开始呢,什么时候结束呢,数据来了,如何告诉对方做好准备呢?这些都是传输数据时需要明确规定的信号。下面是这几种信号的简单介绍。
(1)开始信号:SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。
(2)结束信号: SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。
(3)应答信号:接收数据的 IC 在接收到8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,
表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接
收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为
受控单元出现故障。
这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要。
除了这些信号标志外,下面对几种I2C的状态做简单的介绍:
(1) 空闲状态:当没有数据需要传输的时候,也就是说这个时候I2C总线是处于空闲的,那就让它休息就好不需要工作,这时,SDA和SCL两根信号线同时处于高电平就规定为总线的空闲状态。
(2) 起始信号:也就是前面所说的开始信号,它是一种电平跳变的时序信号,当SCL为高电平期间,SDA由高跳变到低,这就代表着数据传输的开始。
(3)停止信号:具体叙述如上中的结束信号,这里不再重复。
(4)应答信号:发送器每发送一个字节,就会在时钟脉冲的第九个脉冲期间释放数据线SDA,然后由接收器反馈一个应答信号给发送器,如果这个应答信号为低电平,就为有效应答位(ACK简称应答位),表示接收器已经成功的接受了了这个字节;如果这个应答信号为高电平,就为非应答位(NACK),一般表示接收器没有成功接受这个字节。具体来说,对于反馈回来的有效应答位(ACK)的要求是:接收器在第九个时钟脉冲之前的低电平期间将SDA拉低,并且确保在该时钟的高电平期间为稳定的低电平。 如果接收器是主控器,则在他接受的最后一个字节之后,发送一个NACK信号,来通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P。
(5)数据有效性:在I2C总线进行数据传输时,在时钟信号为高电平的期间,数据线上的数据必须保持稳定,也就是说,比如第一个时钟脉冲时要传输的数据时1,那么在第一个时钟高电平到来之前以及过去之后的这个时间内,数据线上的1(也就是高电平)都要保持为高。只有在时钟线为低电平期间,数据线上的高电平或低电平才允许变化。
(6)数据的传输:在I2C总线上传送的每一位数据都有一个时钟脉冲相对应,即在SCL串行时钟的配合下,在SDA上逐位的串行传送每一位数据,数据位的传输是边沿触发。
void IIC_Init(void) // (1)本函数的作用很简单,就是配置两个io口分别作为时钟线SCL和数据线SDA,
{ 模式是推挽输出,当主控IC是作为接受器是应该设置为浮空输入
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE ); //PB 时钟使能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 GPIO
GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7); //PB6,PB7 输出高
}
//产生 IIC 起始信号
void IIC_Start(void) // (2)函数作用已经标出来了,产生起始信号,具体步骤看函数
{
SDA_OUT(); //sda 线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0; //START:when CLK is high,DATA change form high to low
delay_us(4);
IIC_SCL=0; //钳住 I2C 总线,准备发送或接收数据
}
//产生 IIC 停止信号
void IIC_Stop(void) // (3)函数作用已经标出来了,产生停止信号,具体步骤看函数
{
SDA_OUT(); //sda 线输出
IIC_SCL=0;
IIC_SDA=0; //STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
IIC_SDA=1; //发送 I2C 总线结束信号
delay_us(4);
}
//等待应答信号到来 //(4)这是当传输一个字节之后等待应答信号ACK的到来
//返回值: 1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA 设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA) 这里的while循环中是在等待SDA的返回信号,如果是0就跳出循环,返回为0,属于有效应答
{ ucErrTime++; 如果等待中一直是1,就返回为1,位无效应答并且产生停止信号。
if(ucErrTime>250)
{ IIC_Stop();
return 1;
}
}
IIC_SCL=0; //时钟输出 0
return 0;
}
//产生 ACK 应答 // (5)产生ACK应答
void IIC_Ack(void)
{ IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生 ACK 应答 (6)不产生应答
void IIC_NAck(void)
{ IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//(7)IIC 发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{ u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{ IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2); //对 TEA5767 这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//(8)读 1 个字节, ack=1 时,发送 ACK, ack=0,发送 nACK
u8 IIC_Read_Byte(unsigned char ack)
{ unsigned char i,receive=0;
SDA_IN(); //SDA 设置为输入
for(i=0;i<8;i++ )
{ IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck(); //发送 nACK
else
IIC_Ack(); //发送 ACK
return receive;
}
以上部分是参考原子的程序,上面列出了在使用i2c通信所用到的各个函数,包括产生起始信号,停止信号,应答,不应答,发送一个字节和读取一个字节等函数的封装。使用方便!
下面开始正式介绍i2c通信的具体实践内容:
理解:首先无论我们是要读取还是写入数据,我们都需指定地址,然后在这个地址里面读取或者写入数据。
写入数据的具体过程遵从I2C的通信协议,前面已经介绍过就不再重复说明了。
这里以24c02(EEPROM)这个存储器为例: (以后学习或者需要使用到其他的存储器时再添加)
如上图,以24C02的写时序为例,(1)发送一个start的开始信号;
(2)发送写的命令给24C02(上图中的device address);
(3)等待应答信号ACK;
(4)发送需要写入的地址 (Word address);
(5)等待应答信号ACK;
(6)写入数据(date);
(7)等待应答信号ACK;
(8)发送停止信号;
以上是对上面时序图的解读,也是主控芯片(stm32f103)对被控芯片写入数据时的具体步骤(这里的被控芯片是以24C02为例)。
下面是上述操作的代码:
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{ IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}else IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址 0XA0,写数据
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop(); //产生一个停止条件
delay_ms(10);
}
这里就不对代码展开详细的解释了,代码的过程是依据上述24C02的写时序来的。如果有疑问的地方,可以评论区留言,共同交流学习。
总结:至此,I2C的初探学便结束了,总的来说,这种通信方式还是相对比较简单的,也只占用了两个IO口。实际的使用效果如何目前还不敢妄下定论。这只是一种在两个IC之间通行的手段,依据其规定好的方式,对数据进行传输。它是一个完整的数据传输过程。有开始,传输 ,应答,结束等不同的信号进行指示。以后还将学到SPI,485,CAN 等通信方式,届时再做专门的比较和学习。
本篇博客其实是本人在学习I2c中的理解和笔记。如此叙述出来,一是加深自己对I2C通信的理解,二是也希望与各位同学交流,共同进步。当然如果还能对大家有所帮助那更是意外收获了。本篇博客也会在以后深入学习中进行补充和加深。本人也只是初学,如果文中有不当或错误的地方,还请指正。谢谢!
如有疑问的地方可以加QQ:1547530968