IIC(Inter-Integrated Circuit, 内部集成电路)总线是飞利浦公司开发的两线式串行总线,用于短距离传输,常用语微控制器及其外围设备。它是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据。
I2C总线通过上拉电阻接正电源。即当总线空闲时,两根线均为高电平。如此,连在总线上的任一器件输出的低电平,都可以使得总线的信号变低,也就是说各器件的SDA和SCL都是线"与"关系。
I2C是同步串行总线,由SCL为所有设备提供统一的时钟信号。 即使多个节点发送时钟信号时,由于SCL"线与"的原因,SCL上为统一的时钟信号。
数据位(1\0)有效性规定:I2C总线进行数据传送时,时钟信号为高电平期间,SDA线上的数据必须保持稳定;只有在SCL线的信号为低电平器件,SDA线的才可进行高低电平状态变化。
起始信号、终止信号、应答信号
起始信号: SCL线为高电平期间,SDA线由高电平向低电平跳变(下降沿)----是一种电平跳变时序信号
终止信号:SCL线为高电平期间,SDA线由低电平向高电平跳变(上升沿)-----是一种电平跳变的时序信号
应答信号:在接收数据的IC(接收器)在接收到8bit数据后,向发送数据的IC(发送器)发出特定的低电平脉冲,表示已收到数据。即发送器在时钟脉冲9期间释放数据线,这样接收器就可以反馈一个应答信号。ACK(低电平)---规定为有效应答位,NACK(高电平),规定为非应答位,表示接收器接收该字节咩有成功。
为了时钟信号的统一,我们假定只有一个设备发出时钟信号,该设备称为主机。其他IIC设备作为从机存在。
假设使用的 IIC_SCL IIC_SDA 输出 以及READ_SDA输入
#define SDA_IN() 宏定义设置管脚为输入模式
#define SDA_OUT() 宏定义设置管脚为输出模式
采用4us
void IIC_Starts(void)
{
SDA_OUT() ;
IIC_SCL = 1;
IIC_SDA = 1; //空闲态
dealy_us(4);
IIC_SCL = 1;
IIC_SDA = 0; //当SCL线为高电平是, SDA线上由高到低电平跳变
delay_us(4);
IIC_SCL = 0; //钳住,等待发送或接受
}
void IIC_Stop(void)
{
SDA_OUT() ;
IIC_SCL = 0;
IIC_SDA = 0; //
dealy_us(4);
IIC_SCL = 1;
IIC_SDA = 1; //当SCL线为高电平是, SDA线上由低到高电平跳变
delay_us(4);
}
uint8_t IIC_Wait_ACK(void)
{
uint8_t ucTimeCnt= 0;
SDA_IN() ; //SDA管脚设为输入模式
IIC_SDA = 1;
delay_us(1);
IIC_CLK = 1;
delay_us(1);
while(READ_SDA)
{
ucTimeCnt ++;
if(ucTimeCnt>250)
{
IIC_Stop();
return 1;
}
}
//等到应道地电平
IIC_CLK = 0; //钳住,等待发送或接受
}
void IIC_Send_Byte(uint8 byte)
{
uint8_t i = 0;
SDA_OUT() ;
IIC_SCL = 0; // 只有在SCL线为低电平时,SDA线才可以改变
for(i=0;i<8;i++)
{
IIC_SDA = (byte&0x80)>>7; //每次发最高位
byte<<1; //更新最高位
delay_us(2);
IIC_CLK = 1;
delay_us(2);
IIC_CLK = 0;
delay_us(2); //第三个2us是为什么???
}
}
uint8IIC_Read_Byte(uint8_t ack)
{
uint8_t i, recvVal=0;
SDA_IN();
for(i=0; i<8; i++)
{
IIC_CLK = 0;
delay_us(2); //等待输出
IIC_CLK = 1; //可读取
recvVal<<1; //将最低位空出
if(IIC_SDA()) recvVal++; //高电平,则最低位为1
delay_us(1);
}
if(ack)
{
IIC_ACK();
}
else
{
IIC_NACK();
}
retrun recvVal;
}
void IIC_ACK(void)
{
SDA_OUT() ;
IIC_CLK = 0;
IIC_SDA = 0;
delay_us(2);
IIC_CLK = 1;
delay_us(2);
IIC_CLK = 0;
}
void IIC_NACK(void)
{
SDA_OUT() ;
IIC_CLK = 0;
IIC_SDA = 1;
delay_us(2);
IIC_CLK = 1;
delay_us(2);
IIC_CLK = 0;
}
上图所说的就是I2C向从机特定地址写数据。
注意 从写完地址后,要重新发起地址+接收。
DECVICEADDR地址的8位地址信息组成如下所示,以24C02 EEPROM为例说明
对于24C02而言,最高位的4bit为1010(0xA), A2 A1 A0为管脚地址位,必须与硬件输入引脚一致,假设接地,LSB为读/写操作选择位,高为读操作,低位写操作。 0xA0---写 0xA1-----读
对于EEPROM而言,比较器件地址一致后,输出应答为"0"。如果不一致,则返回到待机状态。
章节2 完成了GPIO口模拟IIC的驱动,现在就是落实到实际应用。AT24C02 EEPROM和MPU6050,这两个器件都是作为IIC从机。
这里对从机和主机 以及发送器和接收器再次说明
主机: 发出时钟脉冲的IIC器件,这里是软件模拟的IIC 处于主动控制地位
从机: 目标的IIC器件 挂在IIC总线上的器件
发送器:在数据传输中处于发送地位
接收器:在数据传输中处于接收地址
若接收器是主机时,他在接收到最后一个自节后,发送一个NACK信号,以通知被控(从机)发送器结束数据发送,并释放SDA线,以便主控接收器发送一个终止信号P。
四 以AT24C02 EEPROM操作为例说明
1、字节写操作
写操作要求AT24C02在接收器件地址后和ACK应答后,在接收8位的字地址,AT24C02接收到地址后应答"0",然后接收一个8位数据,在接收8位数据后,AT24C02应答"0”。最后必须由主器件发送终止信号P在终止写序列。 这之后AT24C02进入内部写周期twr,数据写入非易失性存储器中。在这期间所有输入都是无效的,直到写周期完成,AT24C02才有应答。
//WriteAddr :写入数据的AT24C02目的存储地址 DataToWrite 待写入的值
void AT24CXX_WriteOneByte(u16 WriteAddr, u8 DataToWrite)
{
IIC_Starts(); //发出起始信号
IIC_Send_Byte(0xA0); //写器件地址+写操作
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8); //发送高地址
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite);
IIC_Wait_Ack();
IIC_Stop();
delay_ms(10);// 等AT24C02写数据
}
2、片写操作
即在发送一个8位数据后,在发7个数据(对于AT24C02而言)。每个数据都要有ACK,最后再由主器件发出终止信号P。
AT24C02器件按照8字节/页执行页写;AT24C04/08/16器件按16字节/页执行页写。AT24C02在接收到每个数据后,字地址的低3位内部自动加1,高位地址不变,维持在当前页内,注意当内部产生的自地址达到该页边界地址时,随后的数据将写入该页的页首。 如写入的字节数超过8个,字地址将回转到该页的首字节,先前的字节将会被覆盖。
接口函数待定
void AT24CXX_WritePage(u16 PageAdd, u8 * pBuffer,u16 NumToWrite)
{
}
3、连续写入任意字节数目---不超过全地址
//WriteAddr:开始写入的地址 对于24C02 为0~255 pBuffer:数据数组首地址 NumToWrite:要写入数据的个数
void AT24CXX_Write(u16 WriteAddr, u8 *pBuffer, u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr, *pBuffer);
WriteAddr ++; //地址加1
pBuffer++; //数组地址+1
}
}
4、当前地址读
读操作和写操作初始化相同,只是器件地址中的读/写选择位应为"1"。
AT24C02内部地址计数器只要芯片有电,就会保存着上次访问时最后一个地址加1的值。此时直接读,可读到该地址的值。当读到最后页的最后字节,地址会回转到0。
5、随机读
随机读需要先写一个目标字地址,一旦AT24C02 接收了器件地址和字地址 并应答了ACK,主器件就产生一个重复的起始信号。接着主器件发送器件地址|读操作,EEPROM应答ACK,并随时钟送出数据,主器件NACK,但是发送终止信号。
//ReadAddr 读目标地址
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 tempVal = 0;
IIC_Starts(); //发出起始信号
IIC_WriteOneByte(0xA0); //写器件地址+写操作
IIC_Wait_Ack(); //等待应答
IIC_WriteOneByte(ReadAddr>>8);//高字节地址
IIC_Wait_Ack();
IIC_WriteOneByte(ReadAddr%256); //低字节地址
IIC_Wait_Ack();
IIC_Starts(); //重新发出起始信息
IIC_WriteOneByte(0xA1); //读操作
IIC_Wait_Ack();
tempVal = IIC_ReadOneByte(0); //读取一个字节,发出NACK
IIC_Stop(); //发出终止信号
return tempVal;
}
6、顺序读
顺序读可以通过"当前地址读‘’或“随机读”启动,主器件接收到一个数据后,应答ACK。只要AT24C02接收到ACK后,将自动增加字地址并继续随时钟发送后面的数据。若达到存储器地址末尾,地址自动回转到0,仍可继续顺序读取数据。
主器件NACK+终止条件,即可结束顺序读操作。
void AT24CXX_Read(u16 ReadAddr, u8 *pBuffer, u16 NumToRead)
{
while(NumToRead--)
{
*pBuffer++ = AT24CXX_ReadOneByte(ReadAddr++);
}
}
7、设备检查是否存在,首次在末尾字节写入0x55, 其他读末尾字节看是否是0x55
void AT24CXX_Check(void)
{
u8 tmpVal;
tmpVal = AT24CXX_ReadOneByte(255);
if(tmpVal == 0x55) retrun 0;
else{
AT24CXX_WriteOneByte(0x55);
tmpVal = AT24CXX_ReadOneByte(255);
if(tmpVal == 0x55) retrun 0;
}
return 1;
}