首页 > 技术文章 > 普通IO模拟SMBus (STM32F405)

jiuliblog-2016 2022-02-14 21:08 原文

普通I/O 模拟SMBUS

一、简介

最近项目要用到SMBus,用于电池和主板之间的通信。在网上了解了一下SMBus跟I2C的工作原理非常相似,主要差别是在通信速率上。本来想着用原来的I2C程序,降低一下速率应该就可以了,但实际测试中却是磕磕绊绊,现在把这个过程记录下来,希望对后来者有所帮助。

二、硬件平台

主控芯片:STM32F405 (ST)
电池管理芯片:BQ40Z80 (TI)
上拉电阻:4.7K

三、软件配置

/**
  * @brief  init i2c gpio
  * @param
  * @retval
  */
void i2c_sw_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    GPIO_StructInit(&GPIO_InitStructure);
    
    RCC_AHB1PeriphClockCmd(I2C_GPIO_CLK, ENABLE);           //使能GPIOB时钟
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;           //普通输出模式
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;          //开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;      //100MHz
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;        //无上下拉
    
    /* i2c io init */
    GPIO_InitStructure.GPIO_Pin = I2C_SCL_GPIO_PIN | I2C_SDA_GPIO_PIN;
    GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
    
    /* init io state */
    SDA_UP;
    SCL_UP;
}

四、踩坑

1、硬件线接反

这个实在是没脸说了,硬件封好了个电池盒扔给我测试,费了老大精力了,各种测时序,然后发现他电池盒里面的线接反了......

这个确实还是需要注意一下的,软件程序本来就没测试好,硬件接反了,软件怎么测都白搭。

2、软件延时

这是比较早期的SMBus协议版本规定通信频率为10kHz~100kHz,现在可以达到1MHz。不过现在很多电池都是使用比较老版本的协议,如果通信速率要求不高的情况下,使用低速率会保险一些。

我使用的简单的循环延时,这需要根据你自己芯片和主频去修改的,当然如果有精准的延时肯定是更棒的。
/**
  * @brief  a simple delay function
  * @param
  * @retval
  */
static void delay_us(uint32_t time)
{
    uint32_t delay = 450;
    while(time--)
    {
        for( ; delay>0; delay--);
    }
}

3、时序

SMBus V3.1


SMBus V1.1


V1.1的时序要求和V3.1的时序要求有一点区别,数据保持时间上,V3.1没有要求,V1.1有要求。为保证程序正常运行,最好在时钟线的变化和数据线的变化加一定的延时。

/**
  * @brief  write 1 byte
  * @param  data to be send
  * @retval
  */
static void i2c_sw_write_byte(uint8_t data)
{
    int8_t i =7;
    uint8_t tmp = 0;
    
    delay_us(10);
    SDA_OUT;
    for(; i>=0; i--)
    {
        tmp = (data>>i)&0x01;
        if(tmp)
        {
            SDA_UP;
        }
        else
        {
            SDA_DOWN;
        }
        delay_us(10);
        SCL_UP;
        delay_us(10);
        SCL_DOWN;
        delay_us(10);
    }
}

4、设备地址

这个问题折腾了蛮久的,程序是用MPU6050的程序改过来的,在MPU6050上测试完全没问题,但是读电池就是不行。最后测试发现,是设备地址的问题。
MPU6050的地址是0x68或者0x69,最后在发送的时候要左移一位(I2C地址字节最低位为读写位),所以原先的读寄存器的程序如下:
/**
  * @brief  read register data of slave
  * @param  slave_addr  -----  i2c slave address
            register_addr  -----  slave register address 
            buff  -----  a pointer of data buffer
            len  ----  the length of data to read
  * @retval  0 if success
  */
uint8_t i2c_sw_read_registers(uint8_t slave_addr, uint8_t register_addr, uint8_t *buff, uint8_t len)
{
    ......
        
    /* send slave address (write) */
    i2c_sw_write_byte(slave_addr<<1);
    
    ......
    
}
BQ40Z80的默认地址是0x16,这地址不需要左移,直接根据读写状况更改最后一位即可。(PS: 又被TI上了一课 QAQ)
/**
  * @brief  read register data of slave
  * @param  slave_addr  -----  i2c slave address
            register_addr  -----  slave register address 
            buff  -----  a pointer of data buffer
            len  ----  the length of data to read
  * @retval  0 if success
  */
uint8_t i2c_sw_read_registers(uint8_t slave_addr, uint8_t register_addr, uint8_t *buff, uint8_t len)
{
    ......
    
    /* send slave address (write) */
    i2c_sw_write_byte(slave_addr);
    
    ......
    
}

5、ACK响应

在使用I2C的时候都是稍微延时一会儿就读取数据线来看是ACK还是NACK。但是使用SMBus的时候需要注意一点,从机有可能反应较慢,这个就需要延长等待时间,而且在这个等待时间之内,时钟线必须拉低。

因为从机响应时间不确定,所以最好的方式是读取到数据线被拉低之后,再拉高时钟线,完成第九个时钟。

6、读数据

其实解决完上面这些问题之后,程序已经可以通信了,但是不稳定,有时候通信成功,有时候不成功,这估计也是为什么很少有人用普通IO去模拟SMBus的原因。网上大部分人的解决办法就是增加两个字节之间的发送间隔,这种方式虽然能降低失败率,但是没办法避免。而且增加延时是一个不好的选择,如果你是跑的裸机系统,几十甚至几百毫秒的延时会让你很酸爽。

其实造成这种问题原因是在SMBus本身通信速率较低,它用到了一个我们在I2C中很少用到的特性----clock stretch, 这个功能允许从机在未准备好数据传输的时候,将时钟线拉低。看下图:

可以看到红框里的时钟线被拉低了,所以实际上我们并没有完成完整的通信时序。
解决办法也很简单,我们在拉高时钟线后我们可以检查一下时钟线到底有没有拉高,没有的话则等待一段时间再检查,直到时钟线被拉高,再进行下一步。
经过测试这种方式能够保证通信的成功率,也能减少长时间延时对程序的影响。

五、最后

不得不说做这种东西确实需要细心和耐心,最后代码奉上,祝好运!_

smbus_sw.h

#ifndef I2C_SW_H
#define I2C_SW_H

#include "common.h"


#define     i2c_sw_delay                    sys_delay_ms


/* 引脚定义 */ 

#define     I2C_GPIO_PORT                   GPIOB
#define     I2C_GPIO_CLK                    RCC_AHB1Periph_GPIOB

#define     I2C_SCL_GPIO_PIN                GPIO_Pin_10
#define     I2C_SDA_GPIO_PIN                GPIO_Pin_11

#define     SCL_DOWN                        GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_GPIO_PIN)
#define     SCL_UP                          {GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_GPIO_PIN);\
                                             uint32_t timeout = 10000;   \
                                             while(timeout && (((I2C_GPIO_PORT->IDR)>>10)&0x01)==0)timeout--;}

#define     SDA_DOWN                        GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_GPIO_PIN)
#define     SDA_UP                          GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_GPIO_PIN)

#define     SDA_OUT                         (GPIOB->MODER  |= ((uint32_t)0x01 << (11 * 2)))//LL_GPIO_SetPinMode(I2C_GPIO_PORT, I2C_SDA_GPIO_PIN, LL_GPIO_MODE_OUTPUT)
#define     SDA_IN                          (GPIOB->MODER  &= ~((uint32_t)0x03 << (11 * 2)))//LL_GPIO_SetPinMode(I2C_GPIO_PORT, I2C_SDA_GPIO_PIN, LL_GPIO_MODE_INPUT)
#define     READ_SDA                        (((I2C_GPIO_PORT->IDR)>>11)&0x01)

/* global function define */
void i2c_sw_init(void);
uint8_t i2c_sw_write_registers(uint8_t slave_addr, uint8_t register_addr, uint8_t *buff, uint8_t len);
uint8_t i2c_sw_read_registers(uint8_t slave_addr, uint8_t register_addr, uint8_t *buff, uint8_t len);


#endif /* I2C_SW_H */

smbus_sw.c


#include "smbus_sw.h"
#include "rtthread.h"
#include "timer.h"


/**
  * @brief  init i2c gpio
  * @param
  * @retval
  */
void i2c_sw_init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    GPIO_StructInit(&GPIO_InitStructure);
    
    RCC_AHB1PeriphClockCmd(I2C_GPIO_CLK, ENABLE);           //使能GPIOB时钟
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;           //普通输出模式
    GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;          //开漏输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;       //100MHz
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;        //无上下拉
    
    /* i2c io init */
    GPIO_InitStructure.GPIO_Pin = I2C_SCL_GPIO_PIN | I2C_SDA_GPIO_PIN;
    GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
    
    /* init io state */
    SDA_UP;
    SCL_UP;
}
#if USE_HW_I2C == 0
INIT_BOARD_EXPORT(i2c_sw_init);
#endif

/**
  * @brief  a simple delay function
  * @param
  * @retval
  */
static void delay_us(uint32_t time)
{
    uint32_t delay = 450;
    while(time--)
    {
        for( ; delay>0; delay--);
    }
//    timer_delay_us(time-1);
}



/**
  * @brief  send i2c start condition
  * @param
  * @retval
  */
static void i2c_sw_start(void)
{
//    SCL_DOWN;
    delay_us(20);
    SDA_UP;
    SDA_OUT;
    delay_us(20);
    SCL_UP;
    delay_us(20);
    SDA_DOWN;
    delay_us(20);
    SCL_DOWN;
}


/**
  * @brief  send i2c stop condition
  * @param
  * @retval
  */
static void i2c_sw_stop(void)
{
    
    SCL_DOWN;
    delay_us(20);
    SDA_OUT;
    SDA_DOWN;
    delay_us(20);
    SCL_UP;
    delay_us(20);
    SDA_UP;
    delay_us(20);
}


/**
  * @brief  send i2c ack condition
  * @param
  * @retval
  */
static void i2c_sw_ack(void)
{
    delay_us(1);
    SDA_OUT;
    SDA_DOWN;
    delay_us(19);
    SCL_UP;
    delay_us(20);
    SCL_DOWN;
}


/**
  * @brief  send i2c nack condition
  * @param
  * @retval
  */
static void i2c_sw_nack(void)
{
    delay_us(1);
    SDA_OUT;
    SDA_UP;
    delay_us(19);
    SCL_UP;
    delay_us(20);
    SCL_DOWN;
}


/**
  * @brief  wait for slave response
  * @param
  * @retval 0 if ack, else nack
  */
static uint8_t i2c_sw_wait_ack(void)
{
    uint16_t timeout = 1500;
    
    delay_us(1);
//    SDA_UP;
    SDA_IN;
    delay_us(19);
    while(READ_SDA)
    {
        if(timeout--)
        {
            delay_us(1);
        }
        else
        {
            //SCL_DOWN;
            return 1;
        }
    }
    SCL_UP;
    delay_us(20);
    SCL_DOWN;
//    delay_us(20);
    
    return 0;
}


/**
  * @brief  write 1 byte via i2c bus
  * @param  data to write
  * @retval
  */
static void i2c_sw_write_byte(uint8_t data)
{
    int8_t i =7;
    uint8_t tmp = 0;
    
//    delay_us(20);
    SDA_OUT;
    for(; i>=0; i--)
    {
        tmp = (data>>i)&0x01;
        delay_us(1);
        if(tmp)
        {
            SDA_UP;
        }
        else
        {
            SDA_DOWN;
        }
//        delay_10us();
        delay_us(19);
        SCL_UP;
        delay_us(20);
        SCL_DOWN;
    }
}


/**
  * @brief  read 1 byte via i2c bus
  * @param
  * @retval  received data
  */
static uint8_t i2c_sw_read_byte(void)
{
    int8_t i =7;
    uint32_t tmp = 0;
    
    SDA_IN;
    
    for(; i>=0; i--)
    {
        delay_us(20);
        SCL_UP;
        delay_us(1);
        tmp |= (READ_SDA<<i);
        delay_us(19);
        SCL_DOWN;
    }
    
    return tmp;
}


/**
  * @brief  write  slave register
  * @param  slave_addr  -----  i2c slave address
            register_addr  -----  slave register address 
            buff  -----  a pointer of data buffer
            len  ----  the length of data to write
  * @retval  0 if success
  */
uint8_t i2c_sw_write_registers(uint8_t slave_addr, uint8_t register_addr, uint8_t *buff, uint8_t len)
{
    int err  = 0;
    /* start condition */
    i2c_sw_start();
    
    /* send slave address */
    i2c_sw_write_byte(slave_addr<<0);
    if(i2c_sw_wait_ack()) 
    {
        err = 1;
        goto ret;
    }
    
    /* send register address */
    i2c_sw_write_byte(register_addr);
    if(i2c_sw_wait_ack())
    {
        err = 2;
        goto ret;
    }

    uint8_t i=0;
    for( ; i<len; i++)
    {
        /* send data */
        i2c_sw_write_byte(*(buff+i));
        if(i2c_sw_wait_ack())
        {
            err = 3;
            goto ret;
        }
    }

    ret:
    /* stop condition */
    i2c_sw_stop();
    
    return 0;
}


/**
  * @brief  read register data of slave
  * @param  slave_addr  -----  i2c slave address
            register_addr  -----  slave register address 
            buff  -----  a pointer of data buffer
            len  ----  the length of data to read
  * @retval  0 if success
  */
uint8_t i2c_sw_read_registers(uint8_t slave_addr, uint8_t register_addr, uint8_t *buff, uint8_t len)
{
    int err = 0;
    /* start condition */
    i2c_sw_start();
    
    /* send slave address (write) */
    i2c_sw_write_byte(slave_addr<<0);
    if(i2c_sw_wait_ack())
    {
        err = 4;
        goto ret;
    }
    
    /* send register address */
    i2c_sw_write_byte(register_addr);
    if(i2c_sw_wait_ack())
    {
        err = 5;
        goto ret;
    };

    /* restart condition */
    i2c_sw_start();
    
    /* send slave address (read) */
    i2c_sw_write_byte(((slave_addr<<0)+1));
    
    if(i2c_sw_wait_ack())
    {
        err = 6;
        goto ret;
    }
    
//    delay_us(200);
    int8_t i=0;
    for( ; i<len-1; i++)
    {
        /* read data */
        *(buff+i) = i2c_sw_read_byte();
        i2c_sw_ack();
//        delay_us(200);
    }
    
    /* read data */
    *(buff+i) = i2c_sw_read_byte();
    i2c_sw_nack();
//    delay_us(200);
    
    ret:
    /* stop condition */
    i2c_sw_stop();
    
    return 0;
}

推荐阅读