所谓模拟I2C是指使用普通GPIO口的输入输出功能来模拟I2C总线的时序,用来通过I2C总线进行通信。

I2C的基本知识:

1、I2C总线有两条线:SCL是时钟线,SDA是数据线;

2、I2C总线通信方式是主从模式,即由主设备发起通信,从设备响应通信;

3、I2C从设备具有I2C地址,从设备只有收到自己的地址信息后才会被唤醒;

4、具有不同地址的从设备可以挂载到同一个I2C总线上;

5、从设备地址的最后一个Bit表示读写,0表示写操作,1表示读操作;

6、I2C总线地址有7Bit表示方法和8Bit表示方法,7Bit表示方法是地址中不包含表示读写的最后一个Bit;

7、当SCL=1时,SDA产生下降沿来启动I2C;

8、当SCL=1时,SDA产生上升沿来停止I2C;

9、I2C启动后,当SCL=1时,SDA的电平不允许有变化;

10、I2C启动后,只有当SCL=0时,数据发送方才能在SDA上改变发送电平;

11、I2C总线上数据接收方在接收完一个字节数据(8Bit)后,要在下一个SCL的上升沿,通过SDA响应ACK(SDA=0)或NACK(SDA=1)信号;

12、I2C外部需根据传输速率匹配上拉电阻,速率越高,上拉电阻越小,否则会影响时序;

13、I2C引脚作为输出时需是开漏输出,作为输入时需是浮空输入,不能匹配内部上拉或下拉电阻;

话不多说,直接上代码:

一、基本接口定义

为了提高代码的可移植性和使用方便性,我定义了一些宏和结构体,下面介绍一下这些宏和结构体。

1、结构体

I2C总线有SDA和SCL两个引脚,所以我构造了一个结构体来定义表示这两个引脚的基本信息,我是在STM32平台做的例程,所以是这样定义的:

typedef struct {uint32_t SDA_RCC_APB2Periph;// SDA脚时钟GPIO_TypeDef* SDA_Port;//SDA脚Portuint16_t SDA_Pin;//SDA脚Pinuint32_t SCL_RCC_APB2Periph;//SCL脚时钟GPIO_TypeDef* SCL_Port;//SCL脚Portuint16_t SCL_Pin;//SCL脚Pin
} sw_i2c_gpio_t;

当然如果你使用的是其他平台,可以对结构体进行重新构造。结构体的成员要能拿来直接控制两个引脚的输入和输出即可。

2、宏定义

#define I2C_USE_7BIT_ADDR //如果使用的从机地址是7Bit模式,则打开这个宏,否则注释掉这个宏
#define I2C_DELAY                50 // I2C每个Bit之间的延时时间,延时越小I2C的速率越高

下面这些宏要根据具体的平台进行调整。

#define SW_I2C_SCL_LOW          GPIO_ResetBits(gpio->SCL_Port,gpio->SCL_Pin) // I2C SCL脚输出0
#define SW_I2C_SCL_HIGH         GPIO_SetBits(gpio->SCL_Port,gpio->SCL_Pin) // I2C SCL脚输出1
#define SW_I2C_SDA_LOW          GPIO_ResetBits(gpio->SDA_Port,gpio->SDA_Pin) // I2C SDA脚输出0
#define SW_I2C_SDA_HIGH         GPIO_SetBits(gpio->SDA_Port,gpio->SDA_Pin) // I2C SDA脚输出1
#define SW_I2C_SDA_INPUT        sw_i2c_set_sda_input(gpio) // 将SDA脚方向设置为输入
#define SW_I2C_SDA_OUTPUT        sw_i2c_set_sda_output(gpio) // 将SDA脚方向设置为输出
#define SW_I2C_SDA_STATUS        sw_i2c_sda_status(gpio) // 获取SDA脚输入电平状态  #define i2c_delay_us(a)            SystemDelayUs(a) // 获取SDA脚输入电平状态

一、I2C基本操作实现

1、SDA脚输入输出切换及输入状态读取

/**************************************************************************
***                          读取SDA脚的状态                             ***
***************************************************************************/
static uint8_t sw_i2c_sda_status(sw_i2c_gpio_t *gpio)
{uint8_t sda_status;sda_status = GPIO_ReadInputDataBit(gpio->SDA_Port,gpio->SDA_Pin);    return sda_status?1:0;
}
/**************************************************************************
***                          设置SDA脚为输入                             ***
***************************************************************************/
static void sw_i2c_set_sda_input(sw_i2c_gpio_t *gpio)
{GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = gpio->SDA_Pin;    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入模式GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init (gpio->SDA_Port, & GPIO_InitStructure );
}
/**************************************************************************
***                          设置SDA脚为输出                             ***
***************************************************************************/
static void sw_i2c_set_sda_output(sw_i2c_gpio_t *gpio)
{GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Pin = gpio->SDA_Pin;    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;   //开漏输出模式GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init (gpio->SDA_Port, & GPIO_InitStructure );
}

2、I2C启动

static void sw_i2c_start(sw_i2c_gpio_t *gpio)
{// I2C 开始时序:SCL=1时,SDA由1变成0.SW_I2C_SDA_HIGH;         i2c_delay_us(I2C_DELAY);SW_I2C_SCL_HIGH;           i2c_delay_us(I2C_DELAY);SW_I2C_SDA_LOW;i2c_delay_us(I2C_DELAY);SW_I2C_SCL_LOW;i2c_delay_us(I2C_DELAY);
}

3、I2C停止

static void sw_i2c_stop(sw_i2c_gpio_t *gpio)
{// I2C 开始时序:SCL=1时,SDA由0变成1.SW_I2C_SDA_LOW;i2c_delay_us(I2C_DELAY);SW_I2C_SCL_HIGH;i2c_delay_us(I2C_DELAY);SW_I2C_SDA_HIGH;
}

4、等待数据接收方反馈ACK

static uint8_t sw_i2c_wait_ack(sw_i2c_gpio_t *gpio)
{uint8_t sda_status;uint8_t wait_time=0;uint8_t ack_nack = 1;//先设置SDA脚为输入SW_I2C_SDA_INPUT;//等待SDA脚被从机拉低while(SW_I2C_SDA_STATUS){wait_time++;//如果等待时间过长,则退出等待if (wait_time>=200){ack_nack = 0;break;}}// SCL由0变为1,读入ACK状态// 如果此时SDA=0,则是ACK// 如果此时SDA=1,则是NACKi2c_delay_us(I2C_DELAY);SW_I2C_SCL_HIGH;i2c_delay_us(I2C_DELAY);//再次将SCL=0,并且将SDA脚设置为输出SW_I2C_SCL_LOW;i2c_delay_us(I2C_DELAY);SW_I2C_SDA_OUTPUT;i2c_delay_us(I2C_DELAY);return ack_nack;
}

5、发送ACK给数据发送方

static void sw_i2c_send_ack(sw_i2c_gpio_t *gpio)
{// 发送ACK就是在SDA=0时,SCL由0变成1SW_I2C_SDA_LOW;i2c_delay_us(I2C_DELAY);SW_I2C_SCL_HIGH;i2c_delay_us(I2C_DELAY);SW_I2C_SCL_LOW;i2c_delay_us(I2C_DELAY);
}

6、发送NACK给数据发送方

static void sw_i2c_send_nack(sw_i2c_gpio_t *gpio)
{// 发送NACK就是在SDA=1时,SCL由0变成1SW_I2C_SDA_HIGH;i2c_delay_us(I2C_DELAY);SW_I2C_SCL_HIGH;i2c_delay_us(I2C_DELAY);SW_I2C_SCL_LOW;i2c_delay_us(I2C_DELAY);
}

7、主设备向从设备写一个字节

static void sw_i2c_write_byte(sw_i2c_gpio_t *gpio,uint8_t aByte)
{uint8_t i;for (i=0;i<8;i++){//先将SCL拉低;SW_I2C_SCL_LOW;i2c_delay_us(I2C_DELAY);//然后在SDA输出数据if(aByte&0x80){SW_I2C_SDA_HIGH;}else{SW_I2C_SDA_LOW;}i2c_delay_us(I2C_DELAY);//最后将SCL拉高,在SCL上升沿写入数据SW_I2C_SCL_HIGH;i2c_delay_us(I2C_DELAY);aByte = aByte<<1;//数据位移}//写完一个字节只后要将SCL拉低SW_I2C_SCL_LOW;i2c_delay_us(I2C_DELAY);
}

8、主设备从从设备读一个字节

static uint8_t sw_i2c_read_byte(sw_i2c_gpio_t *gpio)
{uint8_t i,aByte;//先将SDA脚设置为输入SW_I2C_SDA_INPUT;for (i=0;i<8;i++){//数据位移aByte = aByte << 1;//延时等待SDA数据稳定i2c_delay_us(I2C_DELAY);//SCL=1,锁定SDA数据SW_I2C_SCL_HIGH;i2c_delay_us(I2C_DELAY);//读取SDA状态if(SW_I2C_SDA_STATUS){aByte |= 0x01;}//SCL=0,解除锁定SW_I2C_SCL_LOW;}//读完一个字节,将SDA重新设置为输出SW_I2C_SDA_OUTPUT;return aByte;
}

二、I2C传输数据函数实现

1、模拟I2C初始化

void sw_i2c_init(sw_i2c_gpio_t *gpio)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd ( gpio->SCL_RCC_APB2Periph, ENABLE );                                                                GPIO_InitStructure.GPIO_Pin = gpio->SCL_Pin;    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;   //开漏输出模式   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init (gpio->SCL_Port, & GPIO_InitStructure );RCC_APB2PeriphClockCmd ( gpio->SDA_RCC_APB2Periph, ENABLE );                                                                GPIO_InitStructure.GPIO_Pin = gpio->SDA_Pin;    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;   //开漏输出模式  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init (gpio->SDA_Port, & GPIO_InitStructure );SW_I2C_SCL_HIGH;SW_I2C_SDA_HIGH;
}

2、主设备向从设备写N个字节数据

void sw_i2c_write_nBytes(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint8_t *data,uint8_t len)
{uint8_t j;//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDRi2c_addr = i2c_addr<<1;
#endif//启动I2Csw_i2c_start(gpio);//写I2C从机地址,写操作sw_i2c_write_byte(gpio,i2c_addr);//如果从机响应ACC则继续,如果从机未响应ACK则停止if (!sw_i2c_wait_ack(gpio))goto err;//开始写n个字节数据for (j=0;j<len;j++){sw_i2c_write_byte(gpio,data[j]);// 每写一个字节数据后,都要等待从机回应ACKif (!sw_i2c_wait_ack(gpio))goto err;}//停止I2Cerr:sw_i2c_stop(gpio);
}

3、主设备从从设备读取N个字节数据

void sw_i2c_read_nBytes(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint8_t *buf,uint8_t len)
{uint8_t j;//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDRi2c_addr = i2c_addr<<1;
#endif//启动I2Csw_i2c_start(gpio);//写I2C从机地址,读操作sw_i2c_write_byte(gpio,i2c_addr|0x01);//如果从机响应ACC则继续,如果从机未响应ACK则停止if (!sw_i2c_wait_ack(gpio))goto err;//开始读n个字节数据for (j=0;j<len;j++){buf[j]=sw_i2c_read_byte(gpio);// 每读一个字节数据后,都要发送ACK给从机sw_i2c_send_ack(gpio);}//停止I2Cerr:sw_i2c_stop(gpio);
}

4、主设备向从设备16Bit长度的寄存器地址读取N个字节

void sw_i2c_send2read_16bit(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint16_t reg,uint8_t *buf,uint8_t len)
{uint8_t j;//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDRi2c_addr = i2c_addr<<1;
#endif//启动I2Csw_i2c_start(gpio);//写I2C从机地址,写操作sw_i2c_write_byte(gpio,i2c_addr);//如果从机响应ACC则继续,如果从机未响应ACK则停止if (!sw_i2c_wait_ack(gpio))goto err;//写寄存器地址高8位sw_i2c_write_byte(gpio,(reg>>8)&0xff);if (!sw_i2c_wait_ack(gpio))goto err;//写寄存器地址低8位sw_i2c_write_byte(gpio,reg&0xff);if (!sw_i2c_wait_ack(gpio))goto err;//重新启动I2Csw_i2c_start(gpio);//写I2C从机地址,读操作sw_i2c_write_byte(gpio,i2c_addr|0x01);if (!sw_i2c_wait_ack(gpio))goto err;//开始读n个字节数据for (j=0;j<len;j++){buf[j]=sw_i2c_read_byte(gpio);// 每读一个字节数据后,都要发送ACK给从机sw_i2c_send_ack(gpio);}//停止I2Cerr:sw_i2c_stop(gpio);
}

5、主设备向从设备8Bit长度的寄存器地址读取N个字节

void sw_i2c_send2read_8bit(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint8_t reg,uint8_t *buf,uint8_t len)
{uint8_t j;//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDRi2c_addr = i2c_addr<<1;
#endif//启动I2Csw_i2c_start(gpio);//写I2C从机地址,写操作sw_i2c_write_byte(gpio,i2c_addr);//如果从机响应ACC则继续,如果从机未响应ACK则停止if (!sw_i2c_wait_ack(gpio))goto err;//写寄存器地址sw_i2c_write_byte(gpio,reg);if (!sw_i2c_wait_ack(gpio))goto err;//重新启动I2Csw_i2c_start(gpio);//写I2C从机地址,读操作sw_i2c_write_byte(gpio,i2c_addr|0x01);if (!sw_i2c_wait_ack(gpio))goto err;//开始读n个字节数据for (j=0;j<len;j++){buf[j]=sw_i2c_read_byte(gpio);// 每读一个字节数据后,都要发送ACK给从机sw_i2c_send_ack(gpio);}//停止I2Cerr:sw_i2c_stop(gpio);
}

三、应用举例

我们使用这一份驱动代码,定义两组I2C,来读写MCP4725的寄存器

#define MCP4725_DAC1_I2C_ADDR       (0xC0>>1)//DAC1 的I2C地址(7Bit模式,要把驱动中宏I2C_USE_7BIT_ADDR打开)
#define MCP4725_DAC2_I2C_ADDR       (0xC0>>1)//DAC2 的I2C地址(7Bit模式,要把驱动中宏I2C_USE_7BIT_ADDR打开)
sw_i2c_gpio_t dac1_i2c_port; //定义DAC1的I2C引脚
sw_i2c_gpio_t dac2_i2c_port; //定义DAC2的I2C引脚
/**************************************************************************
***                           对DAC芯片进行初始化                         ***
***************************************************************************/
void dac_init(void)
{      //对DAC1的I2C引脚进行负值
​dac1_i2c_port.SCL_RCC_APB2Periph = RCC_APB2Periph_GPIOD;dac1_i2c_port.SCL_Port=GPIOD;dac1_i2c_port.SCL_Pin=GPIO_Pin_2;dac1_i2c_port.SDA_RCC_APB2Periph = RCC_APB2Periph_GPIOC;dac1_i2c_port.SDA_Port=GPIOC;dac1_i2c_port.SDA_Pin=GPIO_Pin_12;//对DAC2的I2C引脚进行负值dac2_i2c_port.SCL_RCC_APB2Periph = RCC_APB2Periph_GPIOC;dac2_i2c_port.SCL_Port=GPIOC;dac2_i2c_port.SCL_Pin=GPIO_Pin_11;dac2_i2c_port.SDA_RCC_APB2Periph = RCC_APB2Periph_GPIOC;dac2_i2c_port.SDA_Port=GPIOC;dac2_i2c_port.SDA_Pin=GPIO_Pin_10;
​sw_i2c_init(&dac1_i2c_port);//对DAC1的I2C进行初始化sw_i2c_init(&dac2_i2c_port);//对DAC2的I2C进行初始化
}
/**************************************************************************
***                       读取DAC芯片寄存器的值                           ***
***************************************************************************/
void mcp4725_read_reg(uint8_t ch,uint8_t *reg_data)
{if(ch == 1)//读取DAC1寄存器的值   sw_i2c_read_nBytes(&dac1_i2c_port,MCP4725_DAC1_I2C_ADDR,reg_data,5);else if(ch == 2)//读取DAC2寄存器的值  sw_i2c_read_nBytes(&dac2_i2c_port,MCP4725_DAC2_I2C_ADDR,reg_data,5);
}
/**************************************************************************
***                 写DAC芯片寄存器,实现DAC输出模拟电压                    ***
***************************************************************************/
void mcp4725_write_data_voltage(uint8_t ch,uint8_t mode,uint16_t voltage)   //电压单位mV
{uint8_t data_buf[3];uint16_t Dn;Dn = ( 4096 * voltage) / MCP4725_VREF;if(mode == 0) // 快速模式写dac 寄存器{data_buf[0] = ((Dn&0x0F00)>>8);// | 0x6F;data_buf[1] = Dn & 0x00FF;
​if(ch == 1)sw_i2c_write_nBytes(&dac1_i2c_port,MCP4725_DAC1_I2C_ADDR,data_buf,2);else if(ch == 2)sw_i2c_write_nBytes(&dac2_i2c_port,MCP4725_DAC2_I2C_ADDR,data_buf,2);}else if(mode == 1) // 写DAC寄存器{data_buf[0] = 0x40;data_buf[1] = (Dn&0x0FFF)>>4;// | 0x6F;data_buf[2] = ((Dn&0x0FFF)<<4) & 0x00F0;if(ch == 1)sw_i2c_write_nBytes(&dac1_i2c_port,MCP4725_DAC1_I2C_ADDR,data_buf,3);else if(ch == 2)sw_i2c_write_nBytes(&dac2_i2c_port,MCP4725_DAC2_I2C_ADDR,data_buf,3);}else if(mode == 2) // 写DAC寄存器和EEPROM{data_buf[0] = 0x60;data_buf[1] = (Dn&0x0FFF)>>4;// | 0x6F;data_buf[2] = ((Dn&0x0FFF)<<4) & 0x00F0;if(ch == 1)sw_i2c_write_nBytes(&dac1_i2c_port,MCP4725_DAC1_I2C_ADDR,data_buf,3);else if(ch == 2)sw_i2c_write_nBytes(&dac2_i2c_port,MCP4725_DAC2_I2C_ADDR,data_buf,3);}}

模拟I2C怎么用--教你使用GPIO口模拟I2C总线协议相关推荐

  1. GPIO口模拟I2C操作

    /*         作者:天空         日期:2014.5.12         功能:利用GPIO口模拟I2C总线,对传感器寄存器读取数据         注意:如果需要移植些文件到其他设 ...

  2. 3399使用GPIO口模拟i2c升级NT68411

    RK3399使用GPIO口模拟i2c升级NT68411固件 sda = gpio_to_desc(129); //GPIO4_A1 scl = gpio_to_desc(130); //GPIO4_A ...

  3. 【msm8953】带clk的gpio口模拟pwm

    1.选择带有clk功能的gpio33作为pwm模拟口: 2.配置设备树 ① 在msm8953-pinctrl.dtsi添加: 位置:kernel/msm-3.18/arch/arm64/boot/dt ...

  4. linux gpio 模拟串口,STM32的GPIO口模拟串口通信.rar

    [实例简介] 利用GPIO.EXTI外部中断.TIM定时器实现URAT串口,该例子来自21IC网,未做改动,明天自己调试,看看效果 完全是根据UART协议编写 [实例截图] [核心代码] STM32的 ...

  5. 用GPIO口模拟串口通信,它真的来了

    你是否遇到过某个MCU串口不够的情况? 这时我们可以考虑用GPIO去模拟,如何具体实现呢? 首选我们需要了解串口的传输协议, UART使用异步模式工作,不需要时钟信号,其一般格式为:起始位+数据位+校 ...

  6. GPIO口模拟IIC--适用于任何ARM系列单片机

    这个IIC协议呢,硬件IIC复杂且不好用,看资料说是怕侵权原子哥就做的很复杂,功能不好.所以在看的都是模拟IIC,配置的话就是IO口的配置 如果对您的问题有帮助的话可以帮忙点个赞,谢谢~QQ群:540 ...

  7. c语言 单片机模拟,【51单片机】普通I/O口模拟SPI口C语言程序

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 89C51系列单片机都不带SPI口,所在在这种情况下,我们可以模拟SPI口来现实我们要的功能,程序如下: //---------------------- ...

  8. GPIO口模拟串口发送接收(基于H861)

    以前常听说码农需要有严密的逻辑思维,以前不明白.没有思维框架,真的很难写代码,不能瞎蒙,等我的只是效率低下 思路 首先我们要模拟串口通信,就要了解通信的必须条件,包括数据位及其他标志位,以及他的时序, ...

  9. MTK 驱动开发(3)---GPIO口的使用方法汇总

    1简介 GPIO=General Purpose Input Output,通用输入输出.有时候简称为"IO口".通用,就是说它是万金油,干什么都行.输入输出,就是说既能当输入口使 ...

  10. NVIDIA Jetson Nano GPIO口和通信协议简单介绍及点亮第一个程序LED灯闪烁

    在前面的文章中,我已经想大家介绍了NVIDIA Jetson Nano这个板子.今天我将给大家介绍NVIDIA Jetson Nano最重要的一个接口–GPIO.Jetson Nano 和树莓派一样作 ...

最新文章

  1. python【蓝桥杯vip练习题库】BASIC-5查找整数
  2. 用python处理excel表格_python用win32com处理excel表格
  3. asp.net中将枚举绑定到下拉列表
  4. mysql in 子查询优化_mysql in 子查询 容易优化
  5. Android7.0 emui主题,全新EMUI5.0基于Android7.0 天生快,一生快!
  6. always中的敏感变量
  7. matplotlib 28原则
  8. vue3中套用echarts官网例子
  9. 华为计算机复制怎么删,华为电脑复制粘贴快捷键
  10. 华为牛人的十年工作感悟
  11. 软件文档的概念和细分
  12. # 汉洛塔问题的解决思路及其代码
  13. 启动tomcat卡在“信息: Destroying ProtocolHandler”
  14. android studio出现,Android studio 出现缺少sdk的情况,如何解决?
  15. 无法打开虚拟磁盘服务器,win2008R2 修改了带有快照的父虚拟磁盘;导致启动不了...
  16. 没有稿酬,混了这么多年-文艺it工程师自白(写于2014年)
  17. 2021年茶艺师(中级)复审考试及茶艺师(中级)理论考试
  18. Idea新建项目名后出现中括号别名
  19. `LINK : fatal error LNK1104: 无法打开文件“***.dll”`的问题解决
  20. 分支定界法 python_分支定界(Branchbound)算法

热门文章

  1. -1岁的产品经理日记(20年秋招产品经理经历分享,含简历、笔经、面经)
  2. win10下怎么运行java,如何在Windows 10中运行Java程序
  3. 《数字图像处理 第三版》(冈萨雷斯)——第八章 图像压缩
  4. python 爬虫基础——淘宝评论
  5. 易语言 安装目录没有VC98linker 编译不成功 VC98linker静态连接器(迷你版),易语言VC98linker破解工具,修复静态编译。
  6. Tuxedo中间件学习
  7. Android 支付宝小程序跳转
  8. u-blox gps 串口驱动安装恢复解决方案
  9. Android:实现应用版本更新
  10. 学生选课管理信息系统