天天看點

模拟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);
    }
           
}
           

繼續閱讀