1. 程式人生 > 實用技巧 >stm32與BQ4050通訊

stm32與BQ4050通訊

最近在做一個關於電池管理的專案,用到了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通訊的相關總結,如果有錯誤的地方,請大家斧正!