最近在做一個關於電池管理的項目,用到了TI公司的BQ4050,這個IC是專門對電池進行管理、保護和數據采集的,在TI配套的上位機中可以對這個芯片進行配置,具體的配置方法還有各種寄存器的意義可以參照手冊,實際上我對怎么配置這個IC也不怎么明白,基本上是按照默認配置來的。不過因為項目中我們用到四串的電池,所以必須配置為4串,不然第四個電池就不能獲取到電壓。
具體的寄存器描述如圖:
接下來,我們來說說BQ4050的通訊,BQ4050與單片機的通訊是通過SMBus完成的,剛開始我對這個通訊一無所知,查找了一些資料發現這個通訊跟I2C沒有根本上的區別,只是在速率上有些許的區別罷了。I2C的通迅速率:標准:100kHz,快速:400kHz。但是SMBus的速率只在10kHz~100kHz之間。
I2C是飛利浦公司在1980年發明的。stm32為了避開它的專利,把硬件I2C設計得很復雜,通常我們都用模擬I2C來進行通訊,據悉模擬I2C的穩定性要比硬件I2C更高。更重要的一點是模擬I2C可以由普通的IO口進行模擬,所以就可以很自由的選擇通訊的時鍾線SCL和數據線SDA了。而且網上能找到的例程更多的也是模擬I2C。我這個項目的SMBus通訊也是模擬的,下面是我的SMBus代碼片段:
#define I2C_GPIO_Port GPIOB #define I2C_SCL_Pin (uint16_t)GPIO_PIN_6 #define I2C_SDA_Pin (uint16_t)GPIO_PIN_7 #define SCL_H HAL_GPIO_WritePin(I2C_GPIO_Port,I2C_SCL_Pin,GPIO_PIN_SET) #define SCL_L HAL_GPIO_WritePin(I2C_GPIO_Port,I2C_SCL_Pin,GPIO_PIN_RESET) #define SDA_H HAL_GPIO_WritePin(I2C_GPIO_Port,I2C_SDA_Pin,GPIO_PIN_SET) #define SDA_L HAL_GPIO_WritePin(I2C_GPIO_Port,I2C_SDA_Pin,GPIO_PIN_RESET) #define READ_SDA HAL_GPIO_ReadPin(I2C_GPIO_Port,I2C_SDA_Pin) /******************************** *函數名稱:void I2C_Pin_Init(void) *函數功能:I2C管腳初始化 *函數形參:無 *函數返回值:無 *備注:PB6————SCL,PB7————SDA ********************************/ void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); //打開PB組時鍾 /*配置SCL==PB6*/ GPIO_InitStruct.Pin = I2C_SCL_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(I2C_GPIO_Port, &GPIO_InitStruct); /*配置SDA==PB7*/ GPIO_InitStruct.Pin = I2C_SDA_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(I2C_GPIO_Port, &GPIO_InitStruct); SCL_H; SDA_H; } //產生IIC起始信號 void IIC_Start(void) { SDA_OUT(); SCL_L; delay_us(2); SDA_H; delay_us(1); SCL_H; delay_us(9); SDA_L;//START:when CLK is high,DATA change form high to low delay_us(9); SCL_L;//鉗住I2C總線,准備發送或接收數據 } //產生IIC停止信號 void IIC_Stop(void) { SDA_OUT(); SCL_L; delay_us(1); SDA_L; delay_us(9); SCL_H; delay_us(9); SDA_H;//發送I2C總線結束信號 delay_us(9); } //等待應答信號到來 //返回值:1,接收應答失敗 // 0,接收應答成功 uint8_t IIC_Wait_Ack(void) { uint8_t ucErrTime=0; SDA_IN(); //SDA設置為輸入 SDA_H;delay_us(9); SCL_H;delay_us(9); while(READ_SDA) { ucErrTime++; if(ucErrTime>250) { IIC_Stop(); return 1; } } SCL_L;//時鍾輸出0 delay_us(2); return 0; } //產生ACK應答 void IIC_Ack(void) { SCL_L; SDA_OUT(); SDA_L; delay_us(9); SCL_H; delay_us(9); SCL_L; } //不產生ACK應答 void IIC_NAck(void) { SCL_L; SDA_OUT(); SDA_H; delay_us(9); SCL_H; delay_us(9); SCL_L; } //IIC發送一個字節 //返回從機有無應答 //1,有應答 //0,無應答 void IIC_Send_Byte(uint8_t txd) { uint8_t t; SDA_OUT(); SCL_L;//拉低時鍾開始數據傳輸 for(t=0;t<8;t++){ if((txd&0x80)>>7) { SDA_H; } else { SDA_L; } txd<<=1; delay_us(8); SCL_H; delay_us(8); SCL_L; delay_us(8); } } //讀1個字節,ack=1時,發送ACK,ack=0,發送nACK uint8_t IIC_Read_Byte(void) { unsigned char i,receive=0; SDA_IN();//SDA設置為輸入 for(i=0;i<8;i++ ) { SCL_L; delay_us(12); SCL_H; receive<<=1; if(READ_SDA)receive++; delay_us(9); } return receive; } /* 函數:I2C_Write() 功能:向I2C總線寫1個字節的數據 參數: dat:要寫到總線上的數據 */ void I2C_Write(unsigned char dat) { /*發送1,在SCL為高電平時使SDA信號為高*/ /*發送0,在SCL為高電平時使SDA信號為低*/ unsigned char t ; for(t=0;t<8;t++) { if(dat & 0x80) { SDA_H; } else { SDA_L; } delay_us(10); SCL_H; //置時鍾線為高,通知被控器開始接收數據位 delay_us(10); SCL_L; delay_us(10); dat <<= 1; } } /******************************** *函數名稱:void SDA_OUT(void) *函數功能:SDA線配置為輸出 *函數形參:無 *函數返回值:無 ********************************/ void SDA_OUT(void) { GPIOB->MODER &= ~(3<<(2*7)); GPIOB->MODER |= 1<<(2*7); } /******************************** *函數名稱:void SDA_IN(void) *函數功能:SDA線配置為輸入 *函數形參:無 *函數返回值:無 ********************************/ void SDA_IN(void) { GPIOB->MODER &= ~(3<<(2*7)); GPIOB->MODER |= 0<<(2*7); }
這是實現SMBus通訊的最基礎幾個功能函數,其中與I2C最大的區別應該就是每個時鍾周期都比較長,每次時鍾線電平變化后的延時基本都達到10us左右。另外,把SDA配置為輸入或者輸出模式最好直接用寄存器配置。接下來是stm32與BQ4050的通訊函數:
#define BQ4050_REG_TEMP 0x08 //Temperature U2
#define BQ4050_REG_VOLT 0x09 //Voltage U2#define BQ4050_REG_CURRENT 0x0A //CURRENT I2
#define BQ4050_REG_RSOC 0x0D //RelativeStateOfCharge U1
#define BQ4050_REG_FCC 0x10 //FullChargeCapacity U2
#define BQ4050_REG_TTE 0x12 //TimeToEmpty U2
#define BQ4050_REG_TTF 0x13 //TimeToFull U2
#define BQ4050_REG_RMC 0x0F ///* Remaining Capacity */
#define BQ4050_REG_CURR 0x0A
#define BQ4050_REG_DSG 0x16
#define BQ4050_ADD 0x16
int16_t Get_Battery_Info(uint8_t slaveAddr, uint8_t Comcode) { int16_t Value; uint8_t data[2] = {0}; IIC_Start(); IIC_Send_Byte(slaveAddr);//發送地址 if(IIC_Wait_Ack() == 1) { // printf("SlaveAddr wait ack fail!\r\n"); return -1; } IIC_Send_Byte(Comcode); delay_us(90); if(IIC_Wait_Ack() == 1) { // printf("Comcode wait ack fail!\r\n"); return -1; } IIC_Start(); IIC_Send_Byte(slaveAddr|0x01);//發送地址 if(IIC_Wait_Ack() == 1) { // printf("slaveAddr+1 wait ack fail!\r\n"); return -1; } delay_us(50); data[0] = IIC_Read_Byte(); IIC_Ack(); delay_us(125); data[1] = IIC_Read_Byte(); IIC_NAck(); delay_us(58); IIC_Stop(); printf("data[0]:%x,data[1]:%x\r\n",data[0],data[1]); Value = (data[0] |(data[1]<<8)); delay_us(100); return Value; }
過程是:起始信號代表通訊開始——>發送BQ4050的器件地址,默認是0x16——>等待應答——>發送命令——>等待應答——>發送器件地址+1(1表示讀,0表示寫)——>等待應答信號——>讀取數據——>發送應答信號——>再次讀取數據——>發送非應答信號。BQ4050發送的數據是16bit的,第一次發送數據的低8位,第二次發送數據的高8位。我們將這兩個數據拼接起來就是一個16bit的數據,注意電流有可能是負數,正數代表充電,負數代表放電。
但是調試的過程中,發現接收到的數據有時並不是正確的,很明顯地超出了正常的范圍,比如獲取電壓數據時第一個數據是一個正常的數據,第二個數據是第8位數據是1(電壓沒有負數,所以這一位為1將會是很大的數據,對比TI的上位機采集到的數據,似乎除了這一位,其他位都是正確的),判斷是BQ4050在發送第二次數據的時候沒有將SDA拉低,那么為什么拉不低呢?我猜是延時時間太短,來不及拉低,之前stm32接收第一個數據和接受第二個數據之間的間隔只有25us,經過測試,至少得50us才能避免這種情況,安全起見,我延時了125us。
即使這樣,接收到的數據有時也不正確,比如發來兩個連續的0xff,很明顯就是錯誤的,幸好這種概率很低,在沒有其他更好的解決辦法之前,應該制定一種過濾算法,把錯誤的數據過濾掉。
PS:上面的延時函數是我在網上找到的,在不使用額外定時器的情況下,利用SysTick(滴答定時器)產生的微妙級延時:
#define CPU_FREQUENCY_MHZ 48 /******************************** *函數名稱:void Delay_us(uint32_t delay) *函數功能:微秒級別的延時 *函數形參:uint32_t delay ———— 延時時間 *函數返回值:無 ********************************/ void delay_us(uint32_t delay) { int temp,last,curr,val; while(delay != 0) { temp = delay > 900 ? 900 : delay; last = SysTick->VAL; curr = last - temp * CPU_FREQUENCY_MHZ; if(curr > 0) { do { val = SysTick->VAL; } while(( val < last )&&( val >= curr)); } else { curr += CPU_FREQUENCY_MHZ * 1000; do { val = SysTick->VAL; } while(( val <= last )||( val > curr)); } delay -= temp; } }
CPU_FREQUENCY_MHZ這個宏定義是當前單片機的主頻率,我這里是48MHz。解釋一下這個函數的實現過程:
1、如果形參傳進來delay的數值小於900,那么temp的值就等於delay,這樣的話只需要循環一次就可以實現延時delay us。
2、SysTick->VAL是滴答定時器的當前值寄存器,它是一個遞減的寄存器,每過1us就會減1。last = SysTick->VAL求出現在last的數值大小。
3、curr = last - temp * CPU_FREQUENCY_MHZ。CPU_FREQUENCY_MHZ(48)這個值實際上主頻(48000000)除以1000000,48000000代表1秒鍾48000000次,那么48就是1us 48次,那么temp*48得出來的當然就是temp微妙的次數了。那么當SysTick->VAL減小到curr =last - temp * CPU_FREQUENCY_MHZ,就代表延時時間到了。
為了方便理解,我畫了一個圖:
這是第一種情況,curr>0的情況,如果是curr得出來小於0呢?那就是第二種情況了,如圖:
這樣得出來的curr是一個負數,但是SysTick->VAL遞減到0之后又從1000開始(這里Max是1000,即1000us,1ms),所以curr += CPU_FREQUENCY_MHZ * 1000得到curr②,即SysTick->VAL遞減到0之后,從1000又開始遞減到curr②這個位置,就是延時時間到了。
當然這是延時時間小於900的情況下,如果是延時時間大於900的,那就得重復多幾次了,所以有了下面delay -= temp這個函數,如果delay大於900,就會再循環。
以上就是我對SMBus通訊的相關總結,如果有錯誤的地方,請大家斧正!