普通IO模拟SMBus (STM32F405)
普通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;
}