之前一篇文章里用了模拟i2c,我这边给大家介绍一下

模拟i2c本质上就是用gpio开关来模拟i2c协议,当然,i2c协议本质上也就是电平的高低

在理解如何使用软件模拟i2c前,你需要理解i2c协议,网上又很多相关文章,我也就不在多说

模拟软件i2c需要实现以下步骤

1.gpio引脚需要是开漏输出,因为只有开漏输出,从机设备才能应答,开漏输出具有“线与”的性质

2.实现i2c协议,i2c协议是由起始型号,终止信号,应答信号,数据信号来构成的。

第一步没什么好说的,我使用的cubemx进行配置,无论是固件库还是hal库,配置起来都不难

第二步要分成很多步,我们慢慢来看

首先看一个宏定义和一个结构体

​
typedef struct i2c_bit_ops
{
    void (*set_sda)(int32_t state);
    void (*set_scl)(int32_t state);
    int32_t (*get_sda)(void);
  
    void (*udelay)(uint32_t us);

    uint32_t delay_us;  /* scl and sda line delay */
    uint8_t  writeAddr;
}soft_i2c_driver;

​

 先看这个结构体,这个结构体是软件i2c描述,里面包含了对sda线的读写,对scl线的写,以及每个操作的延时时间,延时函数指针,以及从机地址。

#define soft_i2c_Delay()    i2c_Delay(i2c_driver)
#define soft_i2c_SDA_H()    i2c_SDA_H(i2c_driver)
#define soft_i2c_SDA_L()    i2c_SDA_L(i2c_driver)
#define soft_i2c_SCL_H()    i2c_SCL_H(i2c_driver)
#define soft_i2c_SCL_L()    i2c_SCL_L(i2c_driver)
#define soft_i2c_SDA_Read() i2c_SDA_Read(i2c_driver)
#define soft_i2c_Start()    soft_i2c_start(i2c_driver)
#define soft_i2c_Stop()     soft_i2c_stop(i2c_driver)

static void i2c_DelayUs(soft_i2c_driver *i2c_driver,uint16_t time)
{
  i2c_driver->udelay(time);
}

static void i2c_Delay(soft_i2c_driver *i2c_driver)
{
  i2c_DelayUs(i2c_driver,i2c_driver->delay_us);
}

static void i2c_SCL_H(soft_i2c_driver *i2c_driver)
{
  i2c_driver->set_scl(soft_i2c_high);
}

static void i2c_SCL_L(soft_i2c_driver *i2c_driver)
{
  i2c_driver->set_scl(soft_i2c_low);
}

static void i2c_SDA_H(soft_i2c_driver *i2c_driver)
{
  i2c_driver->set_sda(soft_i2c_high);
}

static void i2c_SDA_L(soft_i2c_driver *i2c_driver)
{
  i2c_driver->set_sda(soft_i2c_low);
}

static int32_t i2c_SDA_Read(soft_i2c_driver *i2c_driver)
{
  uint8_t data;
  data = i2c_driver->get_sda();
  return data;
}

以上内容通过宏定义调用了结构,简化函数,为后面做铺垫

实现起始信号,起始信号应该是在SCL为高电平时,SDA拉低,表示起始信号

void soft_i2c_start(soft_i2c_driver *i2c_driver)
{
  soft_i2c_SDA_H();
  soft_i2c_SCL_H();
  soft_i2c_SDA_L();
  soft_i2c_Delay();
  soft_i2c_SCL_L();
  
}

实现终止信号,终止信号实在SCL为高电平时,SDA拉高

void soft_i2c_stop(soft_i2c_driver *i2c_driver)
{
  soft_i2c_SDA_L();
  soft_i2c_SCL_H();
  soft_i2c_SDA_H();
  soft_i2c_Delay();
  soft_i2c_SCL_L();
  
} 

实现应答信号,应答信号是在一个SCL周期内,拉低SDA

static void i2c_ACK(soft_i2c_driver *i2c_driver)
{
  soft_i2c_SDA_L();
  soft_i2c_SCL_H();
  soft_i2c_Delay();
  soft_i2c_SCL_L();
}

除了应答信号,应答从机的回复,还要有等待应答的代码,用于等待从机的应答

uint8_t soft_i2c_waitAck(soft_i2c_driver *i2c_driver)
{
  uint8_t ack;
  
  soft_i2c_SDA_H();
  soft_i2c_SCL_H();
  soft_i2c_Delay();
  ack = soft_i2c_SDA_Read();
  soft_i2c_SCL_L();
  
  return ack;
}

上面就是模拟i2c的基础,后续需要实现i2c的读写时序

先说i2c写时序,这部分内容相对简单,下图来自于网络

该时序图的流程为

1.主机发送START信号

2.主机发送七位从机地址和一位读写位(写位为0)

3.等待从机返回ACK指令

4.发送寄存器地址

5.等待从机返回ACK指令

6.发送写入数据

7.等待从机返回ACK指令

8.发送STOP信号

void soft_i2c_Send(soft_i2c_driver *i2c_driver,uint8_t data)
{
  int i=0;
  for(i=0;i<8;i++)
  {
    soft_i2c_SCL_L();
    if(data & 0x80)
    {
      soft_i2c_SDA_H();
    }
    else
    {
      soft_i2c_SDA_L();
    }
    soft_i2c_Delay();
    soft_i2c_SCL_H();
    data = data << 1;
  }
  soft_i2c_SCL_L();
  
}


uint8_t soft_i2c_WriteBytes(soft_i2c_driver *i2c_driver,uint8_t data[],uint8_t size)
{
  int i=0;
  soft_i2c_start(i2c_driver);
  
  for(i=0;i<size;i++)
  {
    soft_i2c_Send(i2c_driver,data[i]);
    if(soft_i2c_waitAck(i2c_driver))
    {
      return 0;
    }
  }
  
  soft_i2c_stop(i2c_driver);
  
  return 1;
} 

uint8_t soft_i2c_WriteReg(soft_i2c_driver *i2c_driver, uint8_t reg, uint8_t reg_msb, uint8_t reg_lsb)
{
  uint8_t data[4];
  data[0] = i2c_driver->writeAddr;
  data[1] = reg;
  data[2] = reg_msb;
  data[3] = reg_lsb;
  return soft_i2c_WriteBytes(i2c_driver,data,4);
}

代码如上,soft_i2c_Send 函数实现了发送的逻辑,soft_i2c_WriteBytes实现了发送的完整逻辑,

soft_i2c_WriteReg通过调用soft_i2c_WriteBytes,实现写入寄存器的功能,soft_i2c_WriteBytes发送的第一个内容是从机地址,第二个寄存器地址,第三四个内容是数据内容;

接着我们来讲读数据的时序

读数据比写数据麻烦些

1.主机发送START信号

2.主机发送从机地址和写位(0)

3.等待从机返回ACK指令

4.主机发送寄存器地址

5.等待从机返回ACK指令

6.主机发送STOP信号,在发送START信号

7.主机发送从机地址

8.等待从机返回ACK指令

9.主机读取从机发送的数据

10.发送STOP信号

  
uint8_t soft_i2c_Receive(soft_i2c_driver *i2c_driver)
{
  int i=0;
  uint8_t data;
  
  soft_i2c_SDA_H();
  for(i=0;i<8;i++)
  {
    data = data << 1;
    soft_i2c_SCL_H();
    soft_i2c_Delay();
    data = data + soft_i2c_SDA_Read();
    soft_i2c_SCL_L();

  }
  i2c_ACK(i2c_driver);

  return data;
}


uint8_t soft_i2c_ReadBytes(soft_i2c_driver *i2c_driver,uint8_t reg, uint8_t data[],uint8_t size)
{

  soft_i2c_start(i2c_driver);
  soft_i2c_Send(i2c_driver,i2c_driver->writeAddr);
  while (soft_i2c_waitAck(i2c_driver));
  soft_i2c_Send(i2c_driver,reg);
  while (soft_i2c_waitAck(i2c_driver));

  soft_i2c_start(i2c_driver);
  soft_i2c_Send(i2c_driver,i2c_driver->writeAddr + 1);
  while (soft_i2c_waitAck(i2c_driver));
  for(uint8_t i=0;i<size;i++)
  {
    data[i] = soft_i2c_Receive(i2c_driver);
  }
  soft_i2c_stop(i2c_driver);
    
  return 1;
  
}

uint16_t soft_i2c_ReadReg(soft_i2c_driver *i2c_driver,uint8_t reg)
{
  uint8_t data[2];
  soft_i2c_ReadBytes(i2c_driver,reg,data,2);
  return data[0]<<8 | data[1];
}

soft_i2c_Receive实现你读数据的部分的逻辑,soft_i2c_ReadBytes实现了完整的读时序,soft_i2c_ReadReg读取寄存器中两个字节。

以上就是软件i2c的基础代码,后续只要实现对应接口的读写,填充从机地址和等待时序时间即可

这部分代码是参考了rt-thread的软件i2c代码,原来还觉得的自己写的挺完美的,但是回过头看来还是又不少问题,希望读者注意

1.这边需要填写的从机地址是7位地址左移一位的地址,使用的时候需要注意

2.这边的soft_i2c_WriteReg 和 soft_i2c_ReadReg 写的过于局限,或许拓展一下,可以多个字节,或者写多个字节会更加合理

3.这个问题最严重soft_i2c_ReadBytes中的读数据使用的while来进行等待ACK,十分的不合理,合理的做法应该时等待一段时间后查看ack数据,之前这么写时十分不严谨的,望各位读者注意。

最后我再给大家一个完善的soft_i2c 初始化的样例,供大家参考

#include "gpio.h"
#include "main.h"
#include "soft_i2c.h"

//define soft I2c PORT PIN
#define _I2c1_SDA_PORT SDA_1_GPIO_Port
#define _I2c1_SDA_PIN  SDA_1_Pin
#define _I2c1_SCL_PORT SCL_1_GPIO_Port
#define _I2c1_SCL_PIN  SCL_1_Pin



//soft i2c define
soft_i2c_driver soft_i2c1;
soft_i2c_driver soft_i2c2;



static void delay_us(uint32_t time)
{
  uint32_t delay_count = time * 168;
  while(delay_count--)
  {
  }
}

//define i2c1 function
void set_sda_i2c1(int32_t state)
{
  HAL_GPIO_WritePin(_I2c1_SDA_PORT, _I2c1_SDA_PIN, (GPIO_PinState)state);
}

void set_scl_i2c1(int32_t state)
{
  HAL_GPIO_WritePin(_I2c1_SCL_PORT, _I2c1_SCL_PIN, (GPIO_PinState)state);

}

int32_t get_sda_i2c1(void)
{
  return HAL_GPIO_ReadPin(_I2c1_SDA_PORT, _I2c1_SDA_PIN);
}



//init soft i2c
void driver_i2c_init(void)
{
  soft_i2c_init(&soft_i2c1,(0x90),set_sda_i2c1,set_scl_i2c1,get_sda_i2c1,delay_us,2);
  soft_i2c_init(&soft_i2c2,(0x92),set_sda_i2c1,set_scl_i2c1,get_sda_i2c1,delay_us,2);

}





以上代码时通过通过同一个软件i2c,通过设置不同的的从机地址,初始化了两个软件i2c,delay_us函数通过空转特定的指令数量来实现

以上就是soft_i2c的基本内容,请十分注意的上面说的缺陷,尤其是第三点,后续我也会再进行一版更新,解决这个问题

-------------------------------------------------------------------------------------------------------------------------------

以下是正对于Soft_i2c的更新

此次更新针对于第二点和第三点进行了更新,并且修复了一个bug,优化了i2c时序

1.提供了多字节读出的函数

2.添加ACK超时判断

3.修复之间读取多个字节时没有反馈Nack导致的时序错误问题

4.优化i2c波形,使scl间隔事件固定

第一点更新代码如下,如果需要读取多个字节,直接调用soft_i2c_ReadBytes即可,但如果需要写入多个字节,目前还不支持直接调用soft_i2c_WriteBytes,但提供了soft_i2c_WriteReg_16Bits,方便一次性写入两个字节,主要目前没有遇到需要写入多个字节的情况,后续会将该接口统一,修改为和soft_i2c_ReadBytes一样可以直接读取多个字节,而不需要填充从机地址

uint8_t soft_i2c_WriteBytes(soft_i2c_driver *i2c_driver,uint8_t data[],uint8_t size)
{
  int i=0;
  soft_i2c_start(i2c_driver);
  
  for(i=0;i<size;i++)
  {
    soft_i2c_Send(i2c_driver,data[i]);
    if(soft_i2c_waitAck(i2c_driver))
    {
      return 0;
    }
  }
  
  soft_i2c_stop(i2c_driver);
  
  return 1;
} 

uint8_t soft_i2c_WriteReg_8Bits(soft_i2c_driver *i2c_driver, uint8_t reg, uint8_t reg_data)
{
  uint8_t data[4];
  data[0] = i2c_driver->writeAddr;
  data[1] = reg;
  data[2] = reg_data;
  return soft_i2c_WriteBytes(i2c_driver,data,3);
}

uint8_t soft_i2c_WriteReg_16Bits(soft_i2c_driver *i2c_driver, uint8_t reg, uint16_t reg_data)
{
  uint8_t data[4];
  data[0] = i2c_driver->writeAddr;
  data[1] = reg;
  data[2] = (reg_data >> 8) & 0xFF;
  data[3] = reg_data & 0xFF;
  return soft_i2c_WriteBytes(i2c_driver,data,4);
}


uint8_t soft_i2c_ReadBytes(soft_i2c_driver *i2c_driver,uint8_t reg, uint8_t data[],uint8_t size)
{
  uint32_t tick;
  soft_i2c_start(i2c_driver);
  soft_i2c_Send(i2c_driver,i2c_driver->writeAddr);
	
  if(soft_i2c_waitAckTimeOut(i2c_driver))
  {
	  return 0;
  }
  soft_i2c_Send(i2c_driver,reg);
  
  if(soft_i2c_waitAckTimeOut(i2c_driver))
  {
	  return 0;
  }
  soft_i2c_start(i2c_driver);
  soft_i2c_Send(i2c_driver,i2c_driver->writeAddr + 1);
  if(soft_i2c_waitAckTimeOut(i2c_driver))
  {
	  return 0;
  }

  for(uint8_t i=0;i<size;i++)
  {
    data[i] = soft_i2c_Receive(i2c_driver);
    if(i+1< size)
    {
      i2c_ACK(i2c_driver);
    }
    else
    {
      i2c_NACK(i2c_driver);
    }
  }
  soft_i2c_stop(i2c_driver);
    
  return 1;
  
}

uint8_t soft_i2c_ReadReg_8Bits(soft_i2c_driver *i2c_driver,uint8_t reg)
{
  uint8_t data[1];
  if(soft_i2c_ReadBytes(i2c_driver,reg,data,1) == 0)
  {
	return 0xFF;
  }
  return data[0];
}


uint16_t soft_i2c_ReadReg_16Bits(soft_i2c_driver *i2c_driver,uint8_t reg)
{
  uint8_t data[2];
  if(soft_i2c_ReadBytes(i2c_driver,reg,data,2) == 0)
  {
	return 0xFFFF;
  }
  return data[0]<<8 | data[1];
}

第二点更新如下,添加soft_i2c_waitAckTimeOut,通过滴答定时器提供的时钟判断是否等待超时,注意写函数没有等待超时的判断,如果当前没有反馈ack则直接判断写入失败,等待超时目前只有度函数具有这个功能。

uint8_t soft_i2c_waitAckTimeOut(soft_i2c_driver *i2c_driver)
{
	uint32_t startTick;
	
	startTick = HAL_GetTick();
	
	do
	{
		if(soft_i2c_waitAck(i2c_driver) == 0)
		{
			return 0;
		}
		
	}while(HAL_GetTick() - startTick < SOFT_I2C_TimeOut);
	
	return 1;
}

第三点在soft_i2c_ReadBytes代码中添加了判断是否是读取的最后一个字节,如果不是最后一个字节则返回ack,否则返回Nack


uint8_t soft_i2c_ReadBytes(soft_i2c_driver *i2c_driver,uint8_t reg, uint8_t data[],uint8_t size)
{
  uint32_t tick;
  soft_i2c_start(i2c_driver);
  soft_i2c_Send(i2c_driver,i2c_driver->writeAddr);
	
  if(soft_i2c_waitAckTimeOut(i2c_driver))
  {
	  return 0;
  }
  soft_i2c_Send(i2c_driver,reg);
  
  if(soft_i2c_waitAckTimeOut(i2c_driver))
  {
	  return 0;
  }
  soft_i2c_start(i2c_driver);
  soft_i2c_Send(i2c_driver,i2c_driver->writeAddr + 1);
  if(soft_i2c_waitAckTimeOut(i2c_driver))
  {
	  return 0;
  }

  for(uint8_t i=0;i<size;i++)
  {
    data[i] = soft_i2c_Receive(i2c_driver);
    if(i+1< size)
    {
      i2c_ACK(i2c_driver);
    }
    else
    {
      i2c_NACK(i2c_driver);
    }
  }
  soft_i2c_stop(i2c_driver);
    
  return 1;
  
}

代码已经更新在git仓库

如果想要借鉴代码,可以访问

zxy/soft_i2c

感谢观看

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐