IIC总线协议基本原理以及GPIO虚拟IIC接口

        IIC 一般应用于芯片之间的通信,是半双工串行同步通信总线,它的传输距离短,但其好处是 IIC 支持一主多从的挂载方式,因此主机和多个从机之间的通信线只要两条就够了,

IIC硬件结构:

 IIC 串行总线一般有两根信号线,一根是双向的数据线 SDA,另一根是时钟线 SCL。所有接到 IIC 总线设 备上的串行数据 SDA 都接到总线的 SDA 上,各设备的时钟线 SCL 接到总线的 SCL 上。因为IIC通信过程中,同一时间内,IIC的总线上只能传输一对设备的通信信息,所以同一时间只能有一个从设备和主设备通信。由于此时其它空闲设备可能对总线造成干扰,或者干扰总线,所以为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是漏极开路(OD)输出或集电极开 路(OC)输出。

        漏极开路即高阻状态,适用于输入/输出,其可独立输入/输出低电平和高阻状态,若需要产生高电平,则需使用外部上拉电阻。总线空闲时,因各设备都是开漏输出,上拉电阻 Rp(1-10k之间,一般可用5.1k) 使 SDA 和 SCL 线都保持高电平。IIC的空闲状态只能有外部上拉, 而此时空闲设备被拉到了高阻态,也就是相当于断路, 整个IIC总线只有开启了的设备才会正常进行通信,而不会干扰到其他设备,设置输出时也可以是推挽输出,只不过此时 IIC 总线上的上拉电阻就起不到作用了,这 种在多从机的情况下可能会产生问题,所以建议设置开漏输出。任一设备输出的低电平都将使相应的总线信号线变低,也就是说:各设备的 SDA 是“与”关系,SCL 也是“与”关系。

        总线的运行(数据传输)由主机控制。所谓主机是指启动数据的传送(发出启动信号)、发出时钟信号 以及传送结束时发出停止信号的设备,通常主机都是微处理器。被主机寻访的设备称为从机。为了进行通 讯,每个接到 IIC 总线的设备都有一个唯一的地址,以便于主机寻访。主机和从机的数据传送,可以由主机 发送数据到从机,也可以由从机发到主机。凡是发送数据到总线的设备称为发送器,从总线上接收数据的 设备被称为接受器。IIC 总线上允许连接多个微处理器以及各种外围设备,如存储器、LED 及 LCD 驱动器、 A/D 及 D/A 转换器等。为了保证数据可靠地传送,任一时刻总线只能由某一台主机控制,各微处理器应该在 总线空闲时发送启动数据,为了妥善解决多台微处理器同时发送启动数据的传送(总线控制权)冲突,以 及决定由哪一台微处理器控制总线的问题,I2C 总线允许连接不同传送速率的设备。多台设备之间时钟信号 的同步过程称为同步化。

IIC 主从机之间通讯步骤如下:

1. 主机发送一个起始信号通知各从机就位

2. 主机发送一个字节的数据(从机地址和读写标志位)从机地址和读写标志位一共占用 8 位,地址占用高 7 位,读写标志位占用最低位,0表示主机向从机发送数据,1表示从机向主机发送数据。

 3. 从机给主机回复响应 (包括ACK和NACK)

4. 如果是写模式,主机发送一字节数据等待从机响应,主机收到响应之后如果还有数据要发就继续发送第二段数据等待响应…直到发送完成; 如果是读模式,此时主机读取从机发来的数据,并给从机响应,如果从机还有数据要发送(接着汇报第二段),主机接着读取然后发送响应给从机…

5. 主机给从机一个停止信号 

写时序:

        主控器在检测到总线为“空闲状态”(即 SDA、SCL 线均为高电平)时,发送一个启 动信号“S”,开始一次通信的开始。主控器接着发送一个命令字节。该字节由 7 位的外围器件地址 和 1 位读写控制位 R/W 组成(此时 R/W=0)。相对应的被控器收到命令字节后向主控器回馈应答信号 ACK(ACK=0)。主控器收到被控器的应答信号后开始发送第一个字节的数据。被控器收到数据 后返回一个应答信号 ACK。主控器收到应答信号后再发送下一个数据字节当主控器发送最后一个数据字节并收到被控器的 ACK 后,通过向被控器发送一个停止信号 P 结束本次通信并释放总线。被控器收到 P信号后也退出与主控器之间的通信 。

读时序:

        主机发送启动信号后,接着发送命令字节(其中 R/W=1),对应的被控器收到地址字节 后,返回一个应答信号并向主控器发送数据,主控器收到数据后向被控器反馈一个应答信号,被控器收到 应答信号后再向主控器发送下一个数据,当主机完成接收数据后,向被控器发送一个“非应答信号(ACK=1)”, 被控器收到 ASK=1 的非应答信号后便停止发送,主机发送非应答信号后,再发送一个停止信号,释放总线 结束通信。主控器所接收数据的数量是由主控器自身决定,当发送“非应答信号NA”时被控器便结束传送 并释放总线(非应答信号的两个作用:前一个数据接收成功,停止从机的再次发送)。

         起始信号和终止信号都是由主机发送的。在起始信号产生之后,总线就处于被占用的状态,在终止信号产生之后,总线就处于空闲状态。

        IIC总线的工作速度分为 3 种版本:S(标准模式),速率为 100kb/s。主要用于简单的检 测与控制场合;F(快速模式),速率为 400kb/s;Hs(高速模式),速率为 3.4Mb/s。

具体时序图:

 ⚫ 起始信号 在时钟线 SCL 高电平期间数据线 SDA 发生下降沿跳变产生起始信号

⚫ 结束信号 在时钟线 SCL 高电平期间数据线 SDA 发生上升沿跳变产生停止信号 

(1)发起IIC开始信号

void startIIC(void)
{
    IIC_SDA_OUT();          //设置SDA引脚为开漏输出

    IIC_SCL_H();            //拉高SCL
    IIC_SDA_H();            //拉高SDA
    Delay_us(IIC_SPEED);    //延时一段时间,延时>4.7us
    IIC_SDA_L();            //拉低SDA,在SCK高电平器件产生下降沿
    Delay_us(IIC_SPEED);    //延时一段时间
    IIC_SCL_L();            //拉低时钟,完成一个时钟周期
}

(2)停止IIC停止信号

void stopIIC(void)
{
    IIC_SDA_OUT();

    IIC_SDA_L();
    IIC_SCL_H();
    Delay_us(IIC_SPEED);
    IIC_SDA_H();            //SCL高电平期间,产生SDA下降沿
    Delay_us(IIC_SPEED);
    IIC_SCL_L();
}

⚫ 应答信号 在时钟线 SCL 为高电平期间数据线 SDA 保持低电平为应答信号

⚫ 非应答信号 在时钟线 SCL 为高电平期间数据线 SDA 保持高电平为非应答信号 

(3)发起响应信号

void sendIICAck(void)
{
    IIC_SDA_OUT();

    IIC_SDA_L();
    IIC_SCL_H();
    Delay_us(IIC_SPEED);
    IIC_SDA_H();
    IIC_SCL_L();        //为下一个数据发送做准备
}

(4)发起非响应信号

void sendIICNAck(void)
{
    IIC_SDA_OUT();

    IIC_SDA_H();
    IIC_SCL_H();
    Delay_us(IIC_SPEED);
    IIC_SCL_L();
}

(5)等待从机应答信号

IIC_ACK waitAck(void)
{
    u8 i = 0;

    IIC_SDA_IN();
		IIC_SDA_H();
    IIC_SCL_H();
    while(GPIO_ReadInputDataBit(IIC_SDA_PORT,IIC_SDA_PIN))
    {//等待SDA变低,表示从机给了应答
        if(++i>50)
        {
            IIC_SCL_L();
            return  NACK;//等待时间过长,应答失败
        }
        Delay_us(1);
    }
		//应答成功,scl变低
    IIC_SCL_L();
    return ACK;
}

⚫ 数据信号 在数据传输期间,时钟线 SCL 为高电平期间,如果数据线 SDA 为高电平则代表二进制 1,同理,时钟线 SCL 为高电平期间,如果数据线 SDA 为低电平则代表二进制 0。传输时序如下图:

         SDA数据线在 SCL的每个时钟周期传输一位数据。传输时,SCL为高电平的时候 SDA 表示的数据有效,且数据必须保持稳定。当 SCL为低电平时,SDA的数据无效,允许数据变化,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。

(6)发送一个字节数据

void sendIICByte(u8 byte)
{
    u8 i;

    IIC_SDA_OUT();
		IIC_SCL_L();//拉低时钟准备数据传输
    for(i=0;i<8;++i)
    {
        if(byte & 0x80)//从高位开始一位一位发送
        {
            IIC_SDA_H();//拉高数据线,有效
        }else 
        {
            IIC_SDA_L();
        }
        byte <<= 1;

        IIC_SCL_H();//拉高时钟表示发送或者接收数据
        Delay_us(IIC_SPEED);
        IIC_SCL_L();//为下一个数据发送做准备
        Delay_us(IIC_SPEED);
    }
}

(7)接收一个字节的数据

u8 receiveIICByte(void)
{
    s8 i;
    u8 byte = 0;

    IIC_SDA_IN();
    Delay_us(IIC_SPEED);
    for(i=7;i>=0;--i)
    {
				IIC_SCL_L();//接收数据准备
        Delay_us(IIC_SPEED);
			IIC_SCL_H();//主机开始读数据,从机不能改变数据,即不能改变SDA的值
        Delay_us(IIC_SPEED);
        if(GPIO_ReadInputDataBit(IIC_SDA_PORT,IIC_SDA_PIN))
        {
            byte |= 0x01<<i;
        }else 
        {
            byte |= 0x00<<i;
        }

        IIC_SCL_L();
        Delay_us(IIC_SPEED);
    }
		if(ACK){//主机需要给从机应答
			sendIICAck();
		}else sendIICNAck();

    return byte;
}

以上就是通过软件模拟IIC时序的大概流程:

         IIC 开始信号,IIC 停止信号,IIC 等待应答信号,IIC 发送应答信号,IIC 不发送应答信号, IIC 发送一个字节,IIC 读取一个字节。我们需要掌握的就是读懂IIC的时序,然后根据时序图编写相关代码。

下面介绍5个相关的读写程序(可作为参考):

 I2C_WriteOneByte(uint8_t DevAddr, uint8_t DataAddr, uint8_t Data);//向IIC从设备写入一个字节
 I2C_WriteBurst(uint8_t DevAddr, uint8_t DataAddr, uint8_t* pData, uint32_t Num);//向IIC从设备连续写入Num个字节
I2C_ReadOneByte(uint8_t DevAddr, uint8_t DataAddr, uint8_t* Data);//从IIC从设备读取一个字节
 I2C_ReadBurst(uint8_t DevAddr, uint8_t DataAddr, uint8_t* pData, uint32_t Num);//从IIC从设备连续读取Num个字节
 I2C_WriteBit(uint8_t DevAddr, uint8_t DataAddr, uint8_t Bitx, uint8_t BitSet);//向IIC从设备设置某位数据的值

对于以上5个函数,以下封装好的IIC读写函数,在绝大多数的IIC器件中可以作为参考,当具体到某个器件时,可以根据器件规格书关于IIC部分的说明进行修改具体参数。

1.向设备指定地址写入一个字节的数据

I2C_StatusTypeDef I2C_WriteOneByte(uint8_t DevAddr, uint8_t DataAddr, uint8_t Data)
{
	I2C_Start();													//Master发送起始信号
	I2C_Send_Byte(DevAddr);								//Master发送从设备地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	I2C_Send_Byte(DataAddr);							//发送数据地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	I2C_Send_Byte(Data);									//发送数据
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	I2C_Stop();														//发送停止信号
	return I2C_SUCCESS;
}

其中,I2C_StatusTypeDef只是一个枚举的名I2C_TIMEOUT字,其包含了I2C_SUCCES,I2C_EEROR。 DevAddr : I2C从设备地址;DataAddr: 需要访问的设备内地址(如寄存器地址,EEPROM地址等);Data  : 写入的数据

2、向设备指定地址连续写入数据(burst写模式)

I2C_StatusTypeDef I2C_WriteBurst(uint8_t DevAddr, uint8_t DataAddr, uint8_t* pData, uint32_t Num)
{
	uint32_t i = 0;
	
	I2C_Start();													//Master发送起始信号
	I2C_Send_Byte(DevAddr);								//Master发送从设备地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	I2C_Send_Byte(DataAddr);							//发送数据地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	for(i = 0; i < Num; i++)
	{
		I2C_Send_Byte(*(pData+i));						//发送数据
		if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	}
	
	I2C_Stop();	//发送停止信号
	return I2C_SUCCESS;
}

其中,*pData  : 写入的数据首地址;Num  : 连续写入的数据个数

3、从指定设备读取一个字节数据

I2C_StatusTypeDef I2C_ReadOneByte(uint8_t DevAddr, uint8_t DataAddr, uint8_t* Data)
{
	I2C_Start();													//Master发送起始信号
	I2C_Send_Byte(DevAddr);								//Master发送从设备地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	I2C_Send_Byte(DataAddr);							//发送数据地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	
	I2C_Start();													//Master发送起始信号
	I2C_Send_Byte(DevAddr+1);							//Master发送从设备读地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	*Data = I2C_Read_Byte(0);							//读数据,NACK
	I2C_Stop();														//发送停止信号
	return I2C_SUCCESS;
}

4、向设备指定地址连续读出数据

I2C_StatusTypeDef I2C_ReadBurst(uint8_t DevAddr, uint8_t DataAddr, uint8_t* pData, uint32_t Num)
{
	uint32_t i = 0;
	
	I2C_Start();													//Master发送起始信号
	I2C_Send_Byte(DevAddr);								//Master发送从设备地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	I2C_Send_Byte(DataAddr);							//发送数据地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	
	I2C_Start();													//Master发送起始信号
	I2C_Send_Byte(DevAddr+1);							//Master发送从设备读地址
	if(I2C_Wait_ACK()) return I2C_TIMEOUT;//等待ACK超时错误
	
	for(i = 0; i < (Num-1); i++)
	{
		*(pData+i) = I2C_Read_Byte(1);			//读数据,ACK
	}
	*(pData+i) = I2C_Read_Byte(0);				//读数据,NACK
	
	I2C_Stop();														//发送停止信号
	return I2C_SUCCESS;
}

其中,I2C访问的结果: I2C_SUCCESS / I2C_TIMEOUT / I2C_ERROR

5、设置数据的某一位

I2C_StatusTypeDef I2C_WriteBit(uint8_t DevAddr, uint8_t DataAddr, uint8_t Bitx, uint8_t BitSet)
{
	I2C_StatusTypeDef status = I2C_ERROR;
	uint8_t tempdata = 0;
	
	status = I2C_ReadOneByte(DevAddr, DataAddr, &tempdata);	//获取原有数据
	if(status != I2C_SUCCESS) return status;								//I2C错误,则返回
	
	tempdata &= ~(1<<Bitx);																	//将要设定的位清零
	tempdata |= (BitSet<<Bitx);															//设置指定的bit
	status = I2C_WriteOneByte(DevAddr, DataAddr, tempdata);	//写入数据
	
	return status;	//返回状态
}

下面通过温度传感器芯片LM75A为例简单分析:

         由芯片手册地址表1中可以看到,前七位就是器件地址,其中前四位地址是固定的,最后一位是R/W位,一般默认为0。那么此时从设备的器件地址就是取决于A2,A1,A0这三个地址位,(实际上三个地址位就是决定传感器最多可以在IIC总线上挂载8个)这三个地址位又由工程原理图的连接情况来决定,假设A2,A1,A0都连接到Vcc,则A2,A1,A0都为1,此时器件的地址可以这样定义:#define LM75A_ADD 0x9E。

        器件中的寄存器地址表也就是内地址,一般来说,IIC总线功能都是以寄存器的功能来呈现的,因此选好了寄存器地址也就确定了该器件在IIC总线上的功能,例如利用该芯片配置器件工作为关断模式,内地址就可以选择配置寄存器地址,即0x01

I2C_WriteOneByte(LM75A_ADD,0x01,1);其中1即表示0x01。

若配置器件获取环境温度,内地址就选择为温度寄存器地址,即0x00。

 I2C_ReadBurst(LM75A_ADD,0x00,buf,2);其中buf为数据地址,2位数据数量。

参考此读写程序,可以尝试利用IIC总线读写EEPROM存储器或者驱动OLED。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值