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


所谓模拟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脚Port
uint16_t SDA_Pin;//SDA脚Pin

uint32_t SCL_RCC_APB2Periph;//SCL脚时钟
GPIO_TypeDef* SCL_Port;//SCL脚Port
uint16_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,则是NACK
i2c_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变成1
SW_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变成1
SW_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_ADDR
i2c_addr = i2c_addr<<1;
#endif

//启动I2C
sw_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]);
// 每写一个字节数据后,都要等待从机回应ACK
if (!sw_i2c_wait_ack(gpio))
goto err;
}

//停止I2C
err:
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_ADDR
i2c_addr = i2c_addr<<1;
#endif

//启动I2C
sw_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);
}

//停止I2C
err:
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_ADDR
i2c_addr = i2c_addr<<1;
#endif
//启动I2C
sw_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;

//重新启动I2C
sw_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);
}
//停止I2C
err:
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_ADDR
i2c_addr = i2c_addr<<1;
#endif
//启动I2C
sw_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;

//重新启动I2C
sw_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);
}
//停止I2C
err:
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);
}

}
————————————————
版权声明:本文为CSDN博主「Mr.How」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaocaohuyang/article/details/90313636


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM