发作品签到
专业版

1-6s电池组充电器

工程标签

4.2k
0
0
17

简介

此工程为一个双通道的1-6s锂电池充电器,带被动均衡功能,以stm32g474作为主控,使用PID控制环路。

简介:此工程为一个双通道的1-6s锂电池充电器,带被动均衡功能,以stm32g474作为主控,使用PID控制环路。
星火计划2024

开源协议

CC BY-NC 3.0

(未经作者授权,禁止转载)
创建时间:2024-02-26 07:31:07更新时间:2024-08-18 09:35:12

描述

简介

一个双通道的1-6s锂电池充电器,带被动均衡功能,以stm32g474作为主控,使用PID控制环路。移植了lvgl,使用2.8英寸320*240电容触摸屏作为输入设备,ui使用gui-guider设计。

指标

  • 最大充电电流12A
  • 单路最大输入电流12A
  • 输出电压范围0.2-26V
  • 被动均衡功能

项目进度

  • 2024/2/2 已实现两路的充电功能,目前只支持锂离子电池
  • 2024/2/27 移植lvgl完毕,完成一部分ui的代码使能通过屏幕控制该充电器
  • 2024/3/4 修改硬件,移除LCR电路,电池内阻的测量通过其他方法完成,优化了电压采样电路
  • 2024/3/15 焊接最新板子完成并且验证,所有修改均无误
  • 2024/7/27 期间做了很多修改,比如优化电池电压采样电路,去掉了输出的二极管,使板子具备双向的功能,截止今日,基本完成项目,但还是有些功能还没写完

硬件原理介绍

总框图

原理框图.png
此图为原理框图,由STM32G474控制一个BUCK-BOOST电路实现恒流恒压给电池充电

电池电压采样与均衡控制

电池电压采样.png

此图为电池电压采样框图

  • 电池1的两端直接连接一个增益为0.5的差分放大器,使用ADC采集输出得到电池1的电压
  • 电池2的两端直接连接一个增益为0.5的差分放大器,使用ADC采集输出得到电池2的电压
  • 电池3-6也使用差分放大器采样,但是因为共模电压太高,必须缩小电压,因此2-6的电压都将缩小至原来的1/6(增益0.16666667),再使用两个单刀四掷模拟开关(CD4052)即可同时取到电池3-6中某一个电池两端各自的电压,送入差分放大器即可获得3-6电池中某一个电池的电压

平衡控制图.png

此图为电池平衡控制图

  • 图中R42-R51即为上面说的电池2-6电压缩放的电阻
  • R84与D37的目的是不让电池1的负极直接接地,如果直接接地,那么输出控制开关mos将失去作用并且带来一系列问题
  • 被动平衡靠电阻放电实现,R22-R27为放电电阻,Q10-Q15为放电开关,网络标签CHA_BAT1_IO-CHA_BAT6_IO为放电开关的控制信号,0关1开
  • 以电池5的放电电路为例(假设此时插入了6块电池且每块都是4.2V),此时Q11的S极为21V-0.3V = 20.7V,这个0.3V是D37的压降,当CHA_BAT5_IO为0时,数字三极管关断,因此R37相当于悬空,Vs = Vg,Q11是关断状态。当CHA_BAT5_IO为1时,数字三极管开通,R38与R20使得1/2Vs = Vg,即Vs = 20.7,Vg = 10.35,此时Vgs = Vg - Vs = 10.35 - 20.7 = -10.35,此时Q11开通,开始放电。R37的存在是为了不让Vgs超过绝对最大值而损坏

原理图-电池3-6电压采样电路.png

此图为电池3-6电压采样电路>

​ 这样看起始看不出来什么,看下面这张图就清晰了

仿真-电压采样.png

​ 可能有人疑问,这个电路的增益怎么算?这个电路的电池电压衰减后送入差分放大器可不可行?输出阻抗大,输入阻抗小,这个差分放大电路应该用不了吧?然而事实是,V+与V-直接接在电池两端,运放的输出电压等于1/2(V+ - V-),也就是增益为0.5。这里直接给结果,G = R18/(R16+R19+R14||R10)/(1+R14/R10)。下面上个仿真图看看。

电池采样运放.png

​ 从图中可以发现,该电路输入电压V+ = 15V,V- = 10V,输出为2.5V,符合上述计算。为了排出不是巧合的问题,下面把R19与R20的值改为18350欧,通过计算G = 110000/(20000+18350+100000||20000)/(1+100000/20000) = 0.333333333,那么输出就是5*0.333333333 = 1.666666665了,与图中的输出符合很好。

电池采样举例.png

​ 可能有人发现分压电阻与差分放大器之间还接了模拟开关,那将会引入模拟开关的内阻,导致增益计算不准,诶,确实啊,引入模拟开关的那70-100欧的内阻确实会稍稍改变增益,图中的R19与R20就是用来模拟模拟开关的内阻的,如果把内阻定位100欧,代入公式计算,会发现影响不大,因为100欧相比于千欧级别还是小了点。

辅助电源

原理图-辅源-降压.png

​ 此电路将输入直流电压降至5V左右,供后续所有芯片使用。也不知道是不是我买到假芯片了,这玩意手册标称VREF = 0.81V,可是我焊接完上电发现输出不对劲,最终发现VREF为0.6V,后修改反馈电阻使之输出5V左右,目前工程原理图中的反馈电阻适配的是VREF = 0.6V这种情况,如果VREF = 0.8则下分压电阻取13K,上分压电阻取68K。图中电感取4.7uf,D62肖特基二极管不能少,这是防反接用的。

原理图-辅源-升压.png

​ 此电路将5V电压升至12V,供扇热风扇和栅极驱动使用,D58可不用焊接,那是调试时才用到的。使用先降压再升压结构,可以降低输入电压。

原理图-辅源-ldo.png

​ 这是两个ldo,分别给单片机以及运放供电,图中有个电容标注千万不能少,那是我在调试期间发现的奇怪问题,虽然解决了,但是增加他可以提高稳定性。

REF.png

​ 这是给单片机ADC用的VREF,输出电容串一个1欧电阻是为了提高ESR,这个器件对输出电容的ESR有要求,ESR高一些好,MLCC的ESR太小,因此串1欧电阻。第七引脚接地是因为layout问题,看过PCB就懂了,datasheet里NC是没有内部连接的,倒也没问题,而DNC则只能空着不能连接任何网络。

主电路

主电路.png

​ 这是主功率电路,包括输入输出接口,输入输出电容,6个mos管,一个电感。可实现能量双向流动。Q38由+12V直接控制,只要+12V网络有足够电压,Q38会直接开启,Q38主要用于防反接,开始时Q38关断,如果正常输入,则电流从S极通过体二极管流向D极再流入输入负极,这是正常的回路,辅助电源陆续输出,当+12V加载到Q38之后,Q38开启,此时GND与输入负极接通,不再有二极管的压降。如果输入反接,因为则直接没有了电流回路,辅助电源不会工作,Q38也不会开启。

​ Q39则用于输出端防反接,当输出端的电池反接时,没有回路,也就没有电流了。剩余的其他器件为一个四开关buck-boost电路,具备升降压功能。

自举电容.png

​ 图为栅极驱动的自举电容部分,自举电容用10uf主要是因为处于buck或者boost模式时,其中必有一个半桥的PWM频率很低,如果电容小了充的电不够用。

输出端采样.png

​ 此电路采样输出端电压,因为输出端口不共地,因此需要使用差分采样,图中缩小11倍端口电压。

电流采样.png

​ 这是电流采样电路,图中图为ina240的内部框图,为了监测双向电流,必须给一个REF,因为INA240内部集成了分压电阻,因此REF1接地,REF2接VS,此时REF为VS/2 = 1.65V,REF为1.65V将加在INA240运放的V+与V-上,这个1.65V不仅会加在运放上,还会通过IN+傍边的电阻加载到输出端口的高侧,这会造成什么问题呢?其实也没有啥大问题,主要会给输入电容充电,然后上升一点电压,这会导致电路静态时输出端口也有一定的电压,在此工程,我加了一个10k的电阻在输出端口,但即便是这样,输出端口可能也有10-30mV的电压,如果不加,电压为140mV左右。如果是低测采样电流就没有这个问题,因为低测采样IN-直接就接地了。如果在目前的情况下想让静态下的输出端口为0,只能加大输出电阻或者让boost桥臂上管打开,但是boost桥臂上管是绝对不能随便打开的,因为这是一个充电器,输出端口随时插入电池,如果boost上管开着,会导致电压倒灌,因此这个问题没啥好的解决办法。

散热设计

​ 为了压缩高度,把具有一定高度的器件均放置于板子背面,这样就导致一个问题,mos和均衡电阻的散热器需要做成异形的,对于个人,打样异形散热器难度过高,于是设计一个铝垫子,垫在器件与散热器之间。

1722089512288.jpg

​ 如图,加上垫子之后,再在垫子上贴装密齿散热器。在运行过程中,电感的热量也不能忽略,使电感的顶面与散热垫子的顶面共面,可以额外给电感散热,如下图,电感与散热器贴合良好。

散热设计1.png

硬件成本

​ 此项目有许多器件的选型是基于本人的器件库进行的,因此有些器件的选型对于成本优化是无利的,但是DIY也就不讲究那么多了。主要器件成本如下图。

主要器件成本.png

​ 图中统计的是一般手头上没有的器件的价格,价格可能变化,因此不是100%准确,同时,也可能有统计遗漏的地方,但是不会很多,总价也不会相差太多。外壳成本没有统计是因为本项目的外壳水平太低了,有能力的制作者还请自行设计外壳。

一些软件实现介绍

buck-boost控制方式:

  • 总体来说,输出小于输入时,BOOST上管常开,输出大于输入时,BUCK上管常开,输出穿越输入电压时变频
  • 半桥驱动没有集成电荷泵,无法实现100%占空比,于是最终结果直接是近似100%占空比
  • 经过实测FD6288q的最短脉冲为三百多纳秒,于是当需要实现近似100%占空比时,保持下管的脉冲宽度为最短脉冲(我这里使用500ns),不断降低频率,此时上管占空比会不断逼近100%,频率的最低值也会有上限,太低会因为自举电容掉电太多导致上管打不开,我这里最低频率定为400Hz,实测可行
  • 当处于BUCK模式,需要上升电压时,控制buck占空比提升
  • 当输出电压接近输入电压时,降低buck的频率使占空比尽可能更高,直至buck的频率达到最低
  • buck占空比最高时,输出电压如果不能满足,还需要继续升高时,则开始控制boost的占空比
  • 保持boost上管的低电平为500ns,不断提升boost的频率,直至达到最高频率后,如果再需要提升电压,则提高占空比
  • 从高电压降低电压时则是相反过程

具体实现

#define MIN_PERIOD 3400
#define MAX_PERIOD 25000
#define MIN_COMP 340

#define MIN_PERIOD_f 3400.0f
#define MAX_PERIOD_f 25000.0f
#define MIN_COMP_f 340.0f

#define MIN_DUTY_MAX_PERIOD (MIN_COMP_f/MAX_PERIOD_f)
#define MAX_DUTY_MAX_PERIOD ((MAX_PERIOD_f - MIN_COMP_f)/MAX_PERIOD_f)
#define MIN_DUTY_MIN_PERIOD (MIN_COMP_f/MIN_PERIOD_f)
#define MAX_DUTY_MIN_PERIOD ((MIN_PERIOD_f - MIN_COMP_f)/MIN_PERIOD_f)

#define min_duty_MAX_PERIOD MIN_DUTY_MAX_PERIOD
#define max_duty_MAX_PERIOD MAX_DUTY_MAX_PERIOD
#define min_duty_MIN_PERIOD MIN_DUTY_MIN_PERIOD
#define max_duty_MIN_PERIOD MAX_DUTY_MIN_PERIOD

#define CHA_BOOST_PERIOD(value)			(hhrtim1.Instance->sTimerxRegs[4].PERxR = (value))
#define CHA_BUCK_PERIOD(value)			(hhrtim1.Instance->sTimerxRegs[5].PERxR = (value))
#define CHB_BOOST_PERIOD(value)			(hhrtim1.Instance->sTimerxRegs[1].PERxR = (value))
#define CHB_BUCK_PERIOD(value)			(hhrtim1.Instance->sTimerxRegs[0].PERxR = (value))
#define CHA_BUCK_CCR(value)  			(hhrtim1.Instance->sTimerxRegs[5].CMP1xR = (value))								
#define CHA_BOOST_CCR(value)			(hhrtim1.Instance->sTimerxRegs[4].CMP1xR = (value))	
#define CHB_BUCK_CCR(value)  			(hhrtim1.Instance->sTimerxRegs[0].CMP1xR = (value))								
#define CHB_BOOST_CCR(value)			(hhrtim1.Instance->sTimerxRegs[1].CMP1xR = (value))	

uint8_t CHA_PWM_DUTY_SET_BUCK(float duty_cycle)
{
	static uint16_t tick = 0;
	uint16_t buck_period = 0;
	uint16_t buck_comp = 0;
	
	tick++;
	if(tick >= 50)
		tick = 0;
    if(duty_cycle <= min_duty_MAX_PERIOD)
    {
		if(tick == 10)
		{
			HAL_GPIO_TogglePin(TEST_IO_GPIO_Port,TEST_IO_Pin);
			buck_period = 25000;
			buck_comp = 340;
			CHA_BUCK_PERIOD(buck_period);
			CHA_BUCK_CCR(buck_comp);
		}
		else
		{
			buck_period = 25000;
			buck_comp = 0;
			CHA_BUCK_PERIOD(buck_period);
			CHA_BUCK_CCR(buck_comp);
		}
      return 0;
    }
    if(duty_cycle < min_duty_MIN_PERIOD && duty_cycle > min_duty_MAX_PERIOD) //变频区,小占空比
    {
        buck_comp = MIN_COMP;
        buck_period = round(MIN_COMP_f/duty_cycle);
				CHA_BUCK_PERIOD(buck_period);
				CHA_BUCK_CCR(buck_comp);
				last_period = buck_period;
				last_comp = buck_comp;
        return 0;
    }
    if(duty_cycle <= max_duty_MIN_PERIOD && duty_cycle >= min_duty_MIN_PERIOD) //定频区,中占空比
    {
        buck_period = (MIN_PERIOD);
        buck_comp = round(duty_cycle*MIN_PERIOD_f);
				CHA_BUCK_PERIOD(buck_period);
				CHA_BUCK_CCR(buck_comp);
				last_period = buck_period;
				last_comp = buck_comp;
        return 0;
    }
    if(duty_cycle > max_duty_MIN_PERIOD && duty_cycle < max_duty_MAX_PERIOD) //变频区,大占空比
    {
        buck_period = round(MIN_COMP_f/(1 - duty_cycle));
        buck_comp = round(buck_period - MIN_COMP);
			CHA_BUCK_CCR(buck_comp);
			CHA_BUCK_PERIOD(buck_period);
        return 0;
    }
    if(duty_cycle >= max_duty_MAX_PERIOD)    
    {
			buck_period = 25000;  	//最大周期
			if(tick == 20)
			{
				buck_comp = 25000 - 340;
			}
			else
			{
				buck_comp = 25000 + 1;  //如果不加1没法输出100%占空比
			}
			CHA_BUCK_PERIOD(buck_period);
			CHA_BUCK_CCR(buck_comp);				
        return 0;
    }
}

电池电压采样:

mcu adc部分.png

如图,CHA_GET_前缀的网络标签为电池电压,其中1、2电池所在引脚只需要读取一次,而3-6电池则需要读取4次,分别获取3-6电池的电压,读取一次就需要操作一次模拟开关切换电池。图中我标注好了使用何ADC何方法读取电压,电池电压都是使用ADC的注入组读的,注入组读取简单方便快捷,以下为一些操作细节:

  • 读取电池电压的频率为50Hz,即20ms一次

  • 因为打开平衡开关时读取到的电池电压不是电池的真是电压,因此读取电压时应该避开平衡开关打开的时间

  • 设置一个定时器10ms中断一次,第一次进中断读取电压,第二次进中断则打开平衡开关(如果需要),平衡开关打开6ms自动关闭(相当于30%占空比),第三次进中断读取电压,如此反复,平衡与读取电压错开

  • 因为3-6电池的电压读取的时候需要操作模拟开关切换电池,切换过程需要一定时间,因此3-6电池电压的读取与切换电池也得错开进行,使用1ms中断一次的定时器,使它们分别错开进行

    采样与均衡.png

具体实现

#define SWITCH_SERIAL_DATA_HIGH	  HAL_GPIO_WritePin(SW_SERIAL_DATA_GPIO_Port,SW_SERIAL_DATA_Pin,GPIO_PIN_SET)
#define SWITCH_STORAGE_CLOCK_HIGH  HAL_GPIO_WritePin(SW_OUT_CLK_GPIO_Port,SW_OUT_CLK_Pin,GPIO_PIN_SET)
#define SWITCH_SHIFT_CLOCK_HIGH	  HAL_GPIO_WritePin(SW_SHIFT_CLK_GPIO_Port,SW_SHIFT_CLK_Pin,GPIO_PIN_SET)

#define SWITCH_SERIAL_DATA_LOW	  HAL_GPIO_WritePin(SW_SERIAL_DATA_GPIO_Port,SW_SERIAL_DATA_Pin,GPIO_PIN_RESET)
#define SWITCH_STORAGE_CLOCK_LOW	  HAL_GPIO_WritePin(SW_OUT_CLK_GPIO_Port,SW_OUT_CLK_Pin,GPIO_PIN_RESET)
#define SWITCH_SHIFT_CLOCK_LOW	  HAL_GPIO_WritePin(SW_SHIFT_CLK_GPIO_Port,SW_SHIFT_CLK_Pin,GPIO_PIN_RESET)

//595的驱动
uint8_t SW_595_drv(uint8_t reg_data)
{
	uint8_t i;
	for(i = 0;i< 8;i++)
	{
		if(0x80 & reg_data)
			SWITCH_SERIAL_DATA_HIGH;
		else 
			SWITCH_SERIAL_DATA_LOW;
	SWITCH_SHIFT_CLOCK_HIGH;
	reg_data = reg_data<<1;
	SWITCH_SHIFT_CLOCK_LOW;
	}
	SWITCH_STORAGE_CLOCK_HIGH;
	SWITCH_STORAGE_CLOCK_LOW;
	return 1;
}

//电池切换
uint8_t Switch_Bat(uint8_t Bat_number) 
{
	uint8_t shift_reg_data;
	switch(Bat_number)
	{
		case 4:shift_reg_data = 0x82;//电池4 
		break;
		case 6:shift_reg_data = 0xc3;//电池6 
		break;		
		case 3:shift_reg_data = 0x41;//电池3 
		break;
		case 5:shift_reg_data = 0x00;//电池5 
		break;
		default:break;
	}
	SW_595_drv(shift_reg_data);
	return shift_reg_data;
}

void ChargerLoop()
{
	static uint8_t stage = 0;
	switch(stage)				
	{
		//采样
		case 0:
		HAL_TIM_Base_Start(&htim6);
		stage = 1;
		break;
		//充电环计算、电压处理以及均衡
		case 1:
		v_bat_data_process();
		HAL_TIM_Base_Start_IT(&htim5);
		CHAChargeControl(&CHA_MyChargePara,&CHA_BatBalancePid,&CHA_BatMsg,CHA_power_msg.RunningMode);
		CHBChargeControl(&CHB_MyChargePara,&CHB_BatBalancePid,&CHB_BatMsg,CHB_power_msg.RunningMode);		
		stage = 0;
		break;
	}
}

void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc) 
{
		static uint8_t stage = 0;
		if(hadc->Instance == ADC1)
		{
			CHB_BatMsg.Bat1RawData = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_4);
			CHB_BatMsg.Bat2RawData = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_3);
			CHA_BatMsg.Bat1RawData = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);
			CHA_BatMsg.Bat2RawData = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);		
		}
		if(hadc->Instance == ADC2)
		{		
			switch(stage)
			{
				case 0:	
				CHA_BatMsg.Bat3RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1);
				CHB_BatMsg.Bat3RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_2);
				Access_Bat(4);	
				stage = 1;
				;break;
				case 1:
				CHA_BatMsg.Bat4RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1);
				CHB_BatMsg.Bat4RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_2);		
				Access_Bat(5);				
				stage = 2;				
				;break;
				case 2:

				CHA_BatMsg.Bat5RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1);
				CHB_BatMsg.Bat5RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_2);		
				Access_Bat(6);					
				stage = 3;				
				;break;
				case 3:
									
				CHA_BatMsg.Bat6RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1);
				CHB_BatMsg.Bat6RawData = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_2);		
				Access_Bat(3);		
				stage = 0;
				HAL_TIM_Base_Stop(&htim6);
				__HAL_TIM_SET_COUNTER(&htim6, 0);		
				stage = 0;
				break;				
			}
		}		
}


输出关断:

​ 输出关断可能很多人认为把PWM关掉就行了,确实是关掉就行了,但是对关断有一定要求,两个桥臂同时关闭应该是最好的,只要不是同时关闭,都有一定概率冲坏电路,因此关闭时不能只是把定时器的CCR置零就行了,要确保两个桥臂同时关断可以使用刹车,也可以直接disable掉pwm。

void PowerStop(uint8_t CHANNEL)
{
	if(CHANNEL == CHANNEL_CHA)
	{
		CHA_OUTPUT_CLOSE();
		HAL_HRTIM_WaveformOutputStop(&hhrtim1, HRTIM_OUTPUT_TA1 | HRTIM_OUTPUT_TA2 | HRTIM_OUTPUT_TB1 | HRTIM_OUTPUT_TB2);		
		__CHA_BUCK_SET_CCR(0);
		__CHA_BOOST_SET_CCR(0);
		CHA_power_msg.RunningMode = IDLE_MODE;
		CHA_MyPowerPara.TargetV = 0;
		CHA_MyPowerPara.TargetI = 0;
	}
	if(CHANNEL == CHANNEL_CHB)
	{
		CHB_OUTPUT_CLOSE();
		HAL_HRTIM_WaveformOutputStop(&hhrtim1, HRTIM_OUTPUT_TE1 | HRTIM_OUTPUT_TE2 | HRTIM_OUTPUT_TF1 | HRTIM_OUTPUT_TF2);
	__CHB_BUCK_SET_CCR(0);
	__CHB_BOOST_SET_CCR(0);
		CHB_power_msg.RunningMode = IDLE_MODE;
		CHB_MyPowerPara.TargetV = 0;
		CHB_MyPowerPara.TargetI = 0;	
	}
}

输出启动

​ 如果电路硬件上加了防反接二极管,那么输出启动没啥讲究的,不怕倒灌,直接干就完了,然而输出没有二极管的时候,输出启动就不是那么随意的了,整不好的板子冒烟,整坏的前级电源炸鸡。如果你的pid很慢,输出电压从0慢悠悠升到电池电压,那么电池电压就倒灌进来了,所以需要输出瞬间就达到电池电压,因此需要前馈,先接入电池,启动前读取一次输出端口电压,把这个电压当做前馈值,反算出所需的pwm占空比,填入pid的积分环节,这样启动瞬间就能输出电池电压了,倒灌问题似乎就解决了。单单这样处理,确实可以用于内阻很大的电池,但内阻小的电池就不好使了,因此需要改进方法。通过观察波形,可以发现启动瞬间两个桥臂的PWM存在很大的相位问题,这是导致这个方法不好使的原因,只要解决了启动相位问题,这个方法还是很好使的。这个问题具体上是两个PWM启动瞬间之间存在相位差,比如给一块电压大于输入电压的电池充电,启动时,boost pwm都跑了几个周期了,buck pwm才启动,这个问题会导致电感充反向电流,或者buck pwm启动了很久了boost pwm才启动,这个问题会导致给电感充电过久导致电感直接饱和然后炸机。

​ 相对正确的方法是:输入小于输出时,buck pwm先启动给电感充电(充适量,几个us就行了),然后boost pwm再启动,这样就不会倒灌了,当输出小于输入时,只需要同步启动两个pwm就可以了。另外也有其他的启动方法,比如设置一个输出开关,当输出端口电压大于电池电压再闭合开关,这样也不会倒灌,还有一个方法,不过貌似效果不太好我就没怎么研究,就是先让boost桥臂当做非同步boost,靠上管体二极管输出,当有输出电流时再把非同步boost切换为同步boost。

void output_set_chb()
{
		//CHB
	uint32_t primask;
	extern float CHB_pid_output_to_controller;
			if(CHB_INF.user_wander_mode == POWER_MODE)			//用户->电源模式
			{
				pid_clear(&CHB_voltage_pid);
				pid_clear(&CHB_current_pid);
				if(CHBPowerParaChange(CHB_INF.output_voltage,CHB_INF.output_current))
				{
					CHB_OUTPUT_OPEN();
					HAL_HRTIM_WaveformOutputStop(&hhrtim1, HRTIM_OUTPUT_TE1 | HRTIM_OUTPUT_TE2 | HRTIM_OUTPUT_TF1 | HRTIM_OUTPUT_TF2);						
					HAL_HRTIM_WaveformCounterStop(&hhrtim1, HRTIM_TIMERID_TIMER_E | HRTIM_TIMERID_TIMER_F);
					HAL_Delay(10);
					primask = __get_PRIMASK();
					__disable_irq();			
					preload(CHANNEL_CHB,CHB_GetOutputVol());
					CHB_power_msg.RunningMode = POWER_MODE;					
					CHB_BUCK_CNT(0);
					CHB_BOOST_CNT(0);
					HAL_HRTIM_WaveformCounterStart(&hhrtim1, HRTIM_TIMERID_TIMER_E | HRTIM_TIMERID_TIMER_F);
					__set_PRIMASK(primask);
					__enable_irq();
					HAL_HRTIM_WaveformOutputStart(&hhrtim1, HRTIM_OUTPUT_TE1 | HRTIM_OUTPUT_TE2 | HRTIM_OUTPUT_TF1 | HRTIM_OUTPUT_TF2);
				}
			}
			if(CHB_INF.user_wander_mode == CHARGER_MODE)  //用户->充电模式
			{
				CHB_BatMsg.BatPackVolt = CHB_GetOutputVol();
				if(CHBSetChargerPara(CHB_INF.end_voltage,CHB_INF.user_ensured_batnum,CHB_INF.charging_current) == CHARGER_PARA_VALID) 
				{
					if(CHB_INF.use_balance_func == 1)			
						CHB_BatMsg.start_balance = 1;
					else
						CHB_BatMsg.start_balance = 0;
					pid_clear(&CHB_voltage_pid);
					pid_clear(&CHB_current_pid);
					pid_clear(&CHB_BatBalancePid);					
					CHB_pid_output_to_controller = 0;
					CHB_power_msg.RunningMode = CHARGER_MODE;
					HAL_HRTIM_WaveformCounterStop(&hhrtim1, HRTIM_TIMERID_TIMER_E | HRTIM_TIMERID_TIMER_F);
					CHB_BUCK_CNT(0);
					CHB_BOOST_CNT(0);							
					HAL_HRTIM_WaveformOutputStart(&hhrtim1, HRTIM_OUTPUT_TE1 | HRTIM_OUTPUT_TE2 | HRTIM_OUTPUT_TF1 | HRTIM_OUTPUT_TF2);	
					HAL_Delay(100);
					primask = __get_PRIMASK();
					__disable_irq();
					preload(CHANNEL_CHB,CHB_BatMsg.BatPackVolt);  //这里就是前馈,根据输出口电压计算出所需的pwm占空比,再填入PID的I环
					CHB_PWM_SET(CHB_pid_output_to_controller);	//这里是根据PID的输出计算相应桥臂的period和ccr,然后填入相应寄存器							//这里解决启动输出pwm的相位问题的
					if(CHB_power_msg.BOOST_PERIOD >= CHB_power_msg.BUCK_PERIOD) 
					{
						CHB_BOOST_CNT( CHB_power_msg.BOOST_PERIOD - CHB_power_msg.BOOST_PERIOD % CHB_power_msg.BUCK_PERIOD);
						HAL_HRTIM_WaveformCounterStart(&hhrtim1,   CHB_TIM_BUCK | CHB_TIM_BOOST);				
					}
					else if(CHB_power_msg.BOOST_PERIOD < CHB_power_msg.BUCK_PERIOD)
					{
						CHB_BUCK_CNT( CHB_power_msg.BUCK_PERIOD - CHB_power_msg.BUCK_PERIOD%CHB_power_msg.BOOST_PERIOD);
						HAL_HRTIM_WaveformCounterStart(&hhrtim1, HRTIM_TIMERID_TIMER_E | HRTIM_TIMERID_TIMER_F);	
					}
					CHB_OUTPUT_OPEN();
					__set_PRIMASK(primask);
					__enable_irq();
			
				}		
			} 
}

均衡电池的PWM生成器

​ 使用一个1K频率的定时器产生PWM,周期为20ms。这个20ms前10ms用于采样电池电压,只有后10ms是用来产生pwm驱动均衡电阻的,因此均衡电阻的开启占空比最高为50%,也因此平均均衡电流最大为 电池电压/3欧*50%,其实也跑不到这么高的占空比,因为平衡电流大了电阻非常烫,很容易烧电阻,本工程最大跑30%占空比。

uint8_t CHA_Bat_control(int8_t bat1_level,int8_t bat2_level,int8_t bat3_level,int8_t bat4_level,int8_t bat5_level,int8_t bat6_level) 
{
	uint8_t shift_reg_data = CHA_BatMsg.reg_595;
	
	if(bat1_level >= 1)
		shift_reg_data |= CHA_BAT1_HIGH;
	else
		shift_reg_data &= CHA_BAT1_LOW;
	if(bat2_level >= 1)
		shift_reg_data |= CHA_BAT2_HIGH;
	else
		shift_reg_data &= CHA_BAT2_LOW;	
	if(bat3_level >= 1)
		shift_reg_data |= CHA_BAT3_HIGH;
	else
		shift_reg_data &= CHA_BAT3_LOW;
	if(bat4_level >= 1)
		shift_reg_data |= CHA_BAT4_HIGH;
	else
		shift_reg_data &= CHA_BAT4_LOW;
	if(bat5_level >= 1)
		shift_reg_data |= CHA_BAT5_HIGH;
	else
		shift_reg_data &= CHA_BAT5_LOW;
	if(bat6_level >= 1)
		shift_reg_data |= CHA_BAT6_HIGH;
	else
		shift_reg_data &= CHA_BAT6_LOW;	
	CHA_BatMsg.reg_595 = shift_reg_data;
	CHA_595_drv(shift_reg_data);
	return shift_reg_data;
}
//此函数放在一个1k的定时器中断中运行,当开启均衡标志,就会自动运行
void Battery_Balance()
{
	static uint8_t tick = 0;
	static uint8_t A_tick_duty = 7;
	static uint8_t B_tick_duty = 7;
	tick++;
	uint8_t Bat1Level;
	uint8_t Bat2Level;
	uint8_t Bat3Level;
	uint8_t Bat4Level;
	uint8_t Bat5Level;
	uint8_t Bat6Level;
	
	uint8_t A_Bat1Level;
	uint8_t A_Bat2Level;
	uint8_t A_Bat3Level;
	uint8_t A_Bat4Level;
	uint8_t A_Bat5Level;
	uint8_t A_Bat6Level;
	
	if(tick < 2 )
	{
		if(CHB_BatMsg.data_figured_status == 1 && CHB_BatMsg.start_balance == 1 && CHB_power_msg.RunningMode == CHARGER_MODE)
		{
			 CHB_BatMsg.BalancingVol_status = 1;
			if(CHB_BatMsg.bat1_duty > 0)
				Bat1Level = 1;
			else
				Bat1Level = 0;
			if(CHB_BatMsg.bat2_duty > 0)
				Bat2Level = 1;
			else
				Bat2Level = 0;
			if(CHB_BatMsg.bat3_duty > 0)
				Bat3Level = 1;
			else
				Bat3Level = 0;
			if(CHB_BatMsg.bat4_duty > 0)
				Bat4Level = 1;
			else
				Bat4Level = 0;
			if(CHB_BatMsg.bat5_duty > 0)
				Bat5Level = 1;
			else
				Bat5Level = 0;
			if(CHB_BatMsg.bat6_duty > 0)
				Bat6Level = 1;
			else
				Bat6Level = 0;
			CHB_Bat_control(Bat1Level,Bat2Level,Bat3Level,Bat4Level,Bat5Level,Bat6Level);	
		}
		if(CHA_BatMsg.data_figured_status == 1 && CHA_BatMsg.start_balance == 1 && CHA_power_msg.RunningMode == CHARGER_MODE)
		{
			CHA_BatMsg.BalancingVol_status = 1;
			if(CHA_BatMsg.bat1_duty > 0)
				A_Bat1Level = 1;
			else
				A_Bat1Level = 0;
			if(CHA_BatMsg.bat2_duty > 0)
				A_Bat2Level = 1;
			else
				A_Bat2Level = 0;
			if(CHA_BatMsg.bat3_duty > 0)
				A_Bat3Level = 1;
			else
				A_Bat3Level = 0;
			if(CHA_BatMsg.bat4_duty > 0)
				A_Bat4Level = 1;
			else
				A_Bat4Level = 0;
			if(CHA_BatMsg.bat5_duty > 0)
				A_Bat5Level = 1;
			else
				A_Bat5Level = 0;
			if(CHA_BatMsg.bat6_duty > 0)
				A_Bat6Level = 1;
			else
				A_Bat6Level = 0;
			CHA_Bat_control(A_Bat1Level,A_Bat2Level,A_Bat3Level,A_Bat4Level,A_Bat5Level,A_Bat6Level);	
		}
	}
	//tick = 7大概30%占空比  
	if(CHB_BatMsg.max_voltage - CHB_BatMsg.minimum_voltage < 0.004)
		B_tick_duty = 2;
	else
		B_tick_duty = 7;
	if(CHA_BatMsg.max_voltage - CHA_BatMsg.minimum_voltage < 0.004)
		A_tick_duty = 2;
	else
		A_tick_duty = 7;
	
	if(tick >= A_tick_duty) 
	{
		CHA_Bat_control(0,0,0,0,0,0); 
		CHA_BatMsg.data_figured_status = 0;
		CHA_BatMsg.BalancingVol_status = 0;

	}
	if(tick >= B_tick_duty)   
	{
		CHB_Bat_control(0,0,0,0,0,0); 
		CHB_BatMsg.data_figured_status = 0;
		CHB_BatMsg.BalancingVol_status = 0;
	}
	if(tick >= A_tick_duty && tick >= B_tick_duty)
	{
		tick = 0;
		HAL_TIM_Base_Stop_IT(&htim5);
		__HAL_TIM_SET_COUNTER(&htim5, 0);
	}
}

充电环

​ 要确保不过压充电,仅仅是恒流再恒压就不行了,因此需要一个充电环路,这个环路是计算应该输出多少电压或者多少电流的,以确保所有电池不过充。具体原理是以电压最大的那一个电池电压值为pid的目标,输出一个电压或者电流,再把这个电压或者电流给电源的电压pid或者电流pid,实测输出电压效果会好一点,如果电流环做的好,那么输出电流也没问题,我电流环做的不太行,电流太低时有点震荡,因此我的充电环是输出电压的。

充电环的pid
float pid_calc_balance(SPID *tspid,BAT_MESSAGE bat_inf,CHARGE_PARA* myChargePara, POWER_MGS power_msg)
{
	float sum_P,sum_I,sum_D;
	tspid->error = myChargePara->BatEndVoltage - bat_inf.max_voltage;
	if(tspid->output < myChargePara->BatEndVoltage*myChargePara->BatNum || tspid->error < 0)
	{
		tspid->i_error += tspid->error;
	}	
	sum_P = tspid->kp*tspid->error;
	sum_I = tspid->ki*tspid->i_error;
	sum_D = tspid->kd*(tspid->error - tspid->last_error);
	tspid->output = sum_P + sum_I + sum_D;	
	
	if(tspid->output > myChargePara->BatEndVoltage*myChargePara->BatNum)
	{
		tspid->output = myChargePara->BatEndVoltage*myChargePara->BatNum;
		tspid->i_error = (tspid->output - sum_P)/tspid->ki;
	}
	if(tspid->output < bat_inf.BatPackVolt)
	{
		tspid->output = bat_inf.BatPackVolt;
		tspid->output = myChargePara->BatEndVoltage*myChargePara->BatNum;
		tspid->i_error = (tspid->output - sum_P)/tspid->ki;		
	}
	return tspid->output;
}
//充电控制,20ms计算一次
void CHBChargeControl(CHARGE_PARA* charge_parameter,SPID* balancePID,BAT_MESSAGE *pBatVolt,uint8_t mode)
{
	static uint32_t output_control_tick = 0;	//每个tick 20ms
	float TotalVoltage = charge_parameter->BatNum*charge_parameter->BatEndVoltage;
	float	OutputVolt;
	float ChargeCurrFromPid;
	
	if(mode == CHARGER_MODE)
	{
		output_control_tick++;

			ChargeCurrFromPid = pid_calc_balance(balancePID,*pBatVolt,charge_parameter,CHB_power_msg);	
//			OutputCurr = (charge_parameter->ChargeCurrent > ChargeCurrFromPid) ? ChargeCurrFromPid:charge_parameter->ChargeCurrent;
			OutputVolt = ChargeCurrFromPid;
			CHBPowerParaChange(OutputVolt,charge_parameter->ChargeCurrent);
		
		if(CHB_GetOutputCurr() < 0.1 && output_control_tick > 40 && CHB_power_msg.RunningMode == CHARGER_MODE) //结束充电标志
		{
			turn_off_chb();
			output_control_tick = 0;
			pBatVolt->start_balance = 0;
			MessageBox("","CHB Charge complete");
		}
	}
	else
	{
		pBatVolt->start_balance = 0;
		output_control_tick = 0;
	}
}

​ 从代码中也能看到了,我的充电结束条件是电流小于0.1A,目前是固定的,不可以自己设置,以后更新再改为可更改的。

校准方面

​ 为了让充电的截止电压更精确,因此设置了多段校准,需要校准4.2*n(n为1-6的整数)的电压值,本人不会什么效果好的校准方法,只能先这样了。而且我发现ADC采样到的值非线性有点大了,确实不好校准,但是我也见过一些产品同样的ADC确能做的很准,只需要校准一个值,确实不懂人家怎么做到的。下面是输出电压电流计算的代码,如果在校准状态则先输出一个大概值,校准完了则通过其他方法计算电压值,而电流的计算使用这个模型还是比较准的,只校准一个值即可。同时检测是否过流过压电流倒灌,检测到则立马关断。

	//CHB
	if(sys_inf.is_in_calib)
	{
		CHB_power_msg.output_voltage = (CHB_power_msg.Vout_raw_data/RES)*26.6700001;  
		CHB_power_msg.output_voltage += 0.00179999997*CHB_power_msg.output_voltage*CHB_power_msg.output_voltage - 0.00179999997/10*CHB_power_msg.output_voltage;	
		CHB_power_msg.Vout_raw_data = 0.05*CHB_Vout_raw_data + 0.95*CHB_Vout_raw_data_last;		
	}
	else
	{
		if(CHB_power_msg.Vout_raw_data <= MyCalibData.CHB_volt_raw1)
		{
			CHB_power_msg.output_voltage = CHB_power_msg.Vout_raw_data*(4.2/MyCalibData.CHB_volt_raw1);
		}
		else if(CHB_power_msg.Vout_raw_data <= MyCalibData.CHB_volt_raw2)
		{
			CHB_power_msg.output_voltage = (CHB_power_msg.Vout_raw_data - MyCalibData.CHB_volt_raw1)*(4.2/(MyCalibData.CHB_volt_raw2 - MyCalibData.CHB_volt_raw1)) + 4.2;
		}
		else if(CHB_power_msg.Vout_raw_data <= MyCalibData.CHB_volt_raw3)
		{
			CHB_power_msg.output_voltage = (CHB_power_msg.Vout_raw_data - MyCalibData.CHB_volt_raw2)*(4.2/(MyCalibData.CHB_volt_raw3 - MyCalibData.CHB_volt_raw2)) + 8.4;
		}
		else if(CHB_power_msg.Vout_raw_data <= MyCalibData.CHB_volt_raw4)
		{
			CHB_power_msg.output_voltage = (CHB_power_msg.Vout_raw_data - MyCalibData.CHB_volt_raw3)*(4.2/(MyCalibData.CHB_volt_raw4 - MyCalibData.CHB_volt_raw3)) + 12.6;
		}
		else if(CHB_power_msg.Vout_raw_data <= MyCalibData.CHB_volt_raw5)
		{
			CHB_power_msg.output_voltage = (CHB_power_msg.Vout_raw_data - MyCalibData.CHB_volt_raw4)*(4.2/(MyCalibData.CHB_volt_raw5 - MyCalibData.CHB_volt_raw4)) + 16.8;
		}
		else if(CHB_power_msg.Vout_raw_data <= 65535)
		{
			CHB_power_msg.output_voltage = (CHB_power_msg.Vout_raw_data - MyCalibData.CHB_volt_raw5)*(4.2/(MyCalibData.CHB_volt_raw6 - MyCalibData.CHB_volt_raw5)) + 21;
		}		
		CHB_power_msg.Vout_raw_data = 0.1*CHB_Vout_raw_data + 0.9*CHB_Vout_raw_data_last;	
	}
         
	
	if(CHB_power_msg.output_voltage > CHB_MyPowerPara.TargetV + 1 && CHB_power_msg.RunningMode != IDLE_MODE)							//如果输出电流大于10,过流
	{
		CHB_power_msg.ERROR_STATUS |= OVER_VOLTAGE;
	}		

	CHB_power_msg.output_current = (CHB_Iout_raw_data/RES*VREF)*POWER_IOUT_GAIN - MyCalibData.CHB_current_zero_offset;
	CHB_Vout_raw_data_last = CHB_power_msg.Vout_raw_data;
	

	if((CHB_power_msg.output_current > 15 || CHB_power_msg.output_current < -0.4 ) && CHB_power_msg.RunningMode != IDLE_MODE)	   
	{
		CHB_power_msg.ERROR_STATUS |= OVER_CURRENT;
	}	
	CHB_power_msg.output_current = add_value_and_get_average(&CHB_current_filter, CHB_power_msg.output_current);
	if(sys_inf.init_end)
	{
		CHB_power_msg.output_current += MyCalibData.CHB_CURRENT_BINOMIAL_MODEL*CHB_power_msg.output_current + MyCalibData.CHB_CURRENT_BINOMIAL_MODEL/10.0*powf(CHB_power_msg.output_current,2);
	}
}

UI方面

UI的设计

ui设计使用lvgl,设计软件使用gui-guider,刚开始本项目是使用squareline的,但是squareline的工程文件莫名丢失了,导致一半代码软件生成一半代码自己手搓,后来放弃squareline转为gui-guider,重新设计ui。没啥美感,搞出来的东西确实不好看,只能将就用了。目前的ui有四个界面,主界面、参数输入界面,设置界面和键盘界面。

主界面

主界面.png

​ 主界面顶栏显示了输入电压和温度,底栏显示信息已改为使用MessageBox显示信息,提示异常状态以及通知。两个大框框分别为两个通道的信息,顶部为电压、电流和功率,中间部分的小框框为1-6电池的电压,下方的P:---V/---A为参数,选定参数后会改变,显示P为电源,C为充电器,点击显示电池电压的框后会转变显示信息,电池电压会变为电池内阻(代码还未写),下方的参数信息会转为mAh和Wh值。关于mAh值与Wh值的统计,这两个值仅供参考,因为这两个值受电压与电流精准度的影响。对于充电模式,mAh值仅有关闭“平衡电池”功能时才有参考价值,关闭“平衡电池”功能时该mAh值为电池组内容量最低的那一片电芯的容量。

参数输入界面

参数界面.png

​ 在参数输入界面可以选择输出通道、输出模式、是否平衡电池,设置电压、电流和电池串数,如果电池串数选择auto,则通过平衡口自动检测电池串数,但电池电压过低时检测可能不对,因此启动前要检查,确定参数之后在主界面会显示电池串数,核对无误再启动。如果需要运行中途更改电压电流参数或者是否平衡电池,则直接进入该界面设置即可,确认后生效。

设置界面

设置界面.png

​ 设置界面包含全部系统设置以及校准功能,校准电压时给输出口加载指定电压,然后选择对应通道,记录电压,最后Save就行,校准电流时需选择下拉框第六项,然后按+或者-调整,按一次调整值过大则可以通过Save按钮旁边的下拉框选择调整步进值。校准电池电压和校准电流一致。

键盘界面

键盘界面.png

​ 这个界面用于输入参数,单单是一个数字键盘。

更多待开发功能

  • 支持更多种类电池
  • 双通道并联充电实现更大充电电流
  • 预设功能
  • 内阻简单测量
  • 双向dcdc
  • 电池放电

存在的问题

  • 由于测试条件有限,目前还未测试最大充电电流,目前最大8A 已实现12A
  • 电流采样电路的布线有问题,差分对的GND网络不小心被GND铺铜给覆盖了导致采样不准,目前在电路上已经更改,但还未打板测试 测试无问题
  • 防倒灌电路还未验证(防止电源模式时插入电池),为了省心写代码测试,目前为了防倒灌在输出处加两个并联的10A的肖特基二极管,效率比较低
  • 目前buck-boost电路的控制方法是最简单的那种,四管同时工作,效率较低 采用单一模式控制,效率更高
  • 电流环做的不好,低电流时有点震荡

关于复刻

硬件

​ 本项目使用较多的qfn封装的芯片以及fpc座子,这两种器件以及单片机的焊接都建议先上锡再贴片。具体操作:先在焊盘涂上锡膏,风枪加热,然后使用烙铁除去多余的锡,再在焊盘上涂上薄薄一层助焊剂,放上元器件,引脚基本对其即可,最后加热,锡融化后使用镊子稍微碰触元件即可归位。

​ 四开关buck-boost的输出电容中的电解电容的焊接一定要仔细对准正负极,之所以要强调这个是因为原理图中我有一个电容是画反的,而且这电容封装在PCB上是没有标注正负极的。

软件

​ 项目代码的编写与调试付出了本人太多的时间,虽然写的比较烂,但也不想直接上传上来,显然这样做不利于本项目的复刻,违背了开源的初衷,因此,只要是有心复刻本项目的或者觉得本项目软硬件有欠缺的,都可以自行修改并且打板制作,然后加我qq1911989299获取源码,只有焊接完硬件的我才会提供源码。对于复刻需要帮助的都可以联系我,有空都会回答。

一些图片

以下图片随着改版而变化

板子正面.jpg

板子正面

测试图1

此图为通道A给一块5s电池充电,通道B为电源模式

1722089511867.jpg

板子正面

1722089512063.jpg

加装了不太合适散热片的侧面

1722089512129.jpg

板子底面

1722089512209.jpg

输出口

1722089512288.jpg

加装了散热垫片的板子

整机顶面.jpg

整机顶面

整机前面.jpg

整机前面

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

附件

序号文件名称下载次数
1
BOM_Power_Board_Schematic1_2024-07-28.xlsx
40
2
BOM_Screen_Board_Schematic2_2024-07-28.xlsx
33
3
主要器件成本.xlsx
39
4
输出防反接测试.mp4
14
5
电源模式下误插电池测试.mp4
14
6
输入防反接测试.mp4
13
7
4s电池9A电流充电测试.mp4
40
8
双电池充电测试-播放速度X4.mp4
36
9
新-散热器垫子.step
21
10
新-外壳上盖.step
24
11
新-外壳下盖.step
20
12
Charger_new.hex
29
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

评论

全部评论(1)
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航