zoukankan      html  css  js  c++  java
  • 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! "); return -1; } IIC_Send_Byte(Comcode); delay_us(90); if(IIC_Wait_Ack() == 1) { // printf("Comcode wait ack fail! "); return -1; } IIC_Start(); IIC_Send_Byte(slaveAddr|0x01);//发送地址 if(IIC_Wait_Ack() == 1) { // printf("slaveAddr+1 wait ack fail! "); 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 ",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通讯的相关总结,如果有错误的地方,请大家斧正!




  • 相关阅读:
    张照行 的第九次作业
    张照行 的第八次作业
    Learning by doing
    张照行 的第七次作业
    张照行 的第六次作业
    Java第七次作业
    java第五次作业
    Java第七次作业
    Java第六次课后作业
    第五次Java作业
  • 原文地址:https://www.cnblogs.com/young-dalong/p/13920090.html
Copyright © 2011-2022 走看看