普通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;
}