
1-6s电池组充电器
简介
此工程为一个双通道的1-6s锂电池充电器,带被动均衡功能,以stm32g474作为主控,使用PID控制环路。
简介:此工程为一个双通道的1-6s锂电池充电器,带被动均衡功能,以stm32g474作为主控,使用PID控制环路。开源协议
:CC BY-NC 3.0
(未经作者授权,禁止转载)描述
简介
一个双通道的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 期间做了很多修改,比如优化电池电压采样电路,去掉了输出的二极管,使板子具备双向的功能,截止今日,基本完成项目,但还是有些功能还没写完
硬件原理介绍
总框图
此图为原理框图,由STM32G474控制一个BUCK-BOOST电路实现恒流恒压给电池充电
电池电压采样与均衡控制
此图为电池电压采样框图
- 电池1的两端直接连接一个增益为0.5的差分放大器,使用ADC采集输出得到电池1的电压
- 电池2的两端直接连接一个增益为0.5的差分放大器,使用ADC采集输出得到电池2的电压
- 电池3-6也使用差分放大器采样,但是因为共模电压太高,必须缩小电压,因此2-6的电压都将缩小至原来的1/6(增益0.16666667),再使用两个单刀四掷模拟开关(CD4052)即可同时取到电池3-6中某一个电池两端各自的电压,送入差分放大器即可获得3-6电池中某一个电池的电压
此图为电池平衡控制图
- 图中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电压采样电路>
这样看起始看不出来什么,看下面这张图就清晰了
可能有人疑问,这个电路的增益怎么算?这个电路的电池电压衰减后送入差分放大器可不可行?输出阻抗大,输入阻抗小,这个差分放大电路应该用不了吧?然而事实是,V+与V-直接接在电池两端,运放的输出电压等于1/2(V+ - V-),也就是增益为0.5。这里直接给结果,G = R18/(R16+R19+R14||R10)/(1+R14/R10)。下面上个仿真图看看。
从图中可以发现,该电路输入电压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了,与图中的输出符合很好。
可能有人发现分压电阻与差分放大器之间还接了模拟开关,那将会引入模拟开关的内阻,导致增益计算不准,诶,确实啊,引入模拟开关的那70-100欧的内阻确实会稍稍改变增益,图中的R19与R20就是用来模拟模拟开关的内阻的,如果把内阻定位100欧,代入公式计算,会发现影响不大,因为100欧相比于千欧级别还是小了点。
辅助电源
此电路将输入直流电压降至5V左右,供后续所有芯片使用。也不知道是不是我买到假芯片了,这玩意手册标称VREF = 0.81V,可是我焊接完上电发现输出不对劲,最终发现VREF为0.6V,后修改反馈电阻使之输出5V左右,目前工程原理图中的反馈电阻适配的是VREF = 0.6V这种情况,如果VREF = 0.8则下分压电阻取13K,上分压电阻取68K。图中电感取4.7uf,D62肖特基二极管不能少,这是防反接用的。
此电路将5V电压升至12V,供扇热风扇和栅极驱动使用,D58可不用焊接,那是调试时才用到的。使用先降压再升压结构,可以降低输入电压。
这是两个ldo,分别给单片机以及运放供电,图中有个电容标注千万不能少,那是我在调试期间发现的奇怪问题,虽然解决了,但是增加他可以提高稳定性。
这是给单片机ADC用的VREF,输出电容串一个1欧电阻是为了提高ESR,这个器件对输出电容的ESR有要求,ESR高一些好,MLCC的ESR太小,因此串1欧电阻。第七引脚接地是因为layout问题,看过PCB就懂了,datasheet里NC是没有内部连接的,倒也没问题,而DNC则只能空着不能连接任何网络。
主电路
这是主功率电路,包括输入输出接口,输入输出电容,6个mos管,一个电感。可实现能量双向流动。Q38由+12V直接控制,只要+12V网络有足够电压,Q38会直接开启,Q38主要用于防反接,开始时Q38关断,如果正常输入,则电流从S极通过体二极管流向D极再流入输入负极,这是正常的回路,辅助电源陆续输出,当+12V加载到Q38之后,Q38开启,此时GND与输入负极接通,不再有二极管的压降。如果输入反接,因为则直接没有了电流回路,辅助电源不会工作,Q38也不会开启。
Q39则用于输出端防反接,当输出端的电池反接时,没有回路,也就没有电流了。剩余的其他器件为一个四开关buck-boost电路,具备升降压功能。
图为栅极驱动的自举电容部分,自举电容用10uf主要是因为处于buck或者boost模式时,其中必有一个半桥的PWM频率很低,如果电容小了充的电不够用。
此电路采样输出端电压,因为输出端口不共地,因此需要使用差分采样,图中缩小11倍端口电压。
这是电流采样电路,图中图为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和均衡电阻的散热器需要做成异形的,对于个人,打样异形散热器难度过高,于是设计一个铝垫子,垫在器件与散热器之间。
如图,加上垫子之后,再在垫子上贴装密齿散热器。在运行过程中,电感的热量也不能忽略,使电感的顶面与散热垫子的顶面共面,可以额外给电感散热,如下图,电感与散热器贴合良好。
硬件成本
此项目有许多器件的选型是基于本人的器件库进行的,因此有些器件的选型对于成本优化是无利的,但是DIY也就不讲究那么多了。主要器件成本如下图。
图中统计的是一般手头上没有的器件的价格,价格可能变化,因此不是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;
}
}
电池电压采样:
如图,CHA_GET_前缀的网络标签为电池电压,其中1、2电池所在引脚只需要读取一次,而3-6电池则需要读取4次,分别获取3-6电池的电压,读取一次就需要操作一次模拟开关切换电池。图中我标注好了使用何ADC何方法读取电压,电池电压都是使用ADC的注入组读的,注入组读取简单方便快捷,以下为一些操作细节:
-
读取电池电压的频率为50Hz,即20ms一次
-
因为打开平衡开关时读取到的电池电压不是电池的真是电压,因此读取电压时应该避开平衡开关打开的时间
-
设置一个定时器10ms中断一次,第一次进中断读取电压,第二次进中断则打开平衡开关(如果需要),平衡开关打开6ms自动关闭(相当于30%占空比),第三次进中断读取电压,如此反复,平衡与读取电压错开
-
因为3-6电池的电压读取的时候需要操作模拟开关切换电池,切换过程需要一定时间,因此3-6电池电压的读取与切换电池也得错开进行,使用1ms中断一次的定时器,使它们分别错开进行
具体实现
#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有四个界面,主界面、参数输入界面,设置界面和键盘界面。
主界面
主界面顶栏显示了输入电压和温度,底栏显示信息已改为使用MessageBox显示信息,提示异常状态以及通知。两个大框框分别为两个通道的信息,顶部为电压、电流和功率,中间部分的小框框为1-6电池的电压,下方的P:---V/---A为参数,选定参数后会改变,显示P为电源,C为充电器,点击显示电池电压的框后会转变显示信息,电池电压会变为电池内阻(代码还未写),下方的参数信息会转为mAh和Wh值。关于mAh值与Wh值的统计,这两个值仅供参考,因为这两个值受电压与电流精准度的影响。对于充电模式,mAh值仅有关闭“平衡电池”功能时才有参考价值,关闭“平衡电池”功能时该mAh值为电池组内容量最低的那一片电芯的容量。
参数输入界面
在参数输入界面可以选择输出通道、输出模式、是否平衡电池,设置电压、电流和电池串数,如果电池串数选择auto,则通过平衡口自动检测电池串数,但电池电压过低时检测可能不对,因此启动前要检查,确定参数之后在主界面会显示电池串数,核对无误再启动。如果需要运行中途更改电压电流参数或者是否平衡电池,则直接进入该界面设置即可,确认后生效。
设置界面
设置界面包含全部系统设置以及校准功能,校准电压时给输出口加载指定电压,然后选择对应通道,记录电压,最后Save就行,校准电流时需选择下拉框第六项,然后按+或者-调整,按一次调整值过大则可以通过Save按钮旁边的下拉框选择调整步进值。校准电池电压和校准电流一致。
键盘界面
这个界面用于输入参数,单单是一个数字键盘。
更多待开发功能
- 支持更多种类电池
- 双通道并联充电实现更大充电电流
- 预设功能
- 内阻简单测量
- 双向dcdc
- 电池放电
存在的问题
由于测试条件有限,目前还未测试最大充电电流,目前最大8A已实现12A电流采样电路的布线有问题,差分对的GND网络不小心被GND铺铜给覆盖了导致采样不准,目前在电路上已经更改,但还未打板测试测试无问题防倒灌电路还未验证(防止电源模式时插入电池),为了省心写代码测试,目前为了防倒灌在输出处加两个并联的10A的肖特基二极管,效率比较低目前buck-boost电路的控制方法是最简单的那种,四管同时工作,效率较低采用单一模式控制,效率更高- 电流环做的不好,低电流时有点震荡
关于复刻
硬件
本项目使用较多的qfn封装的芯片以及fpc座子,这两种器件以及单片机的焊接都建议先上锡再贴片。具体操作:先在焊盘涂上锡膏,风枪加热,然后使用烙铁除去多余的锡,再在焊盘上涂上薄薄一层助焊剂,放上元器件,引脚基本对其即可,最后加热,锡融化后使用镊子稍微碰触元件即可归位。
四开关buck-boost的输出电容中的电解电容的焊接一定要仔细对准正负极,之所以要强调这个是因为原理图中我有一个电容是画反的,而且这电容封装在PCB上是没有标注正负极的。
软件
项目代码的编写与调试付出了本人太多的时间,虽然写的比较烂,但也不想直接上传上来,显然这样做不利于本项目的复刻,违背了开源的初衷,因此,只要是有心复刻本项目的或者觉得本项目软硬件有欠缺的,都可以自行修改并且打板制作,然后加我qq1911989299获取源码,只有焊接完硬件的我才会提供源码。对于复刻需要帮助的都可以联系我,有空都会回答。
一些图片
以下图片随着改版而变化
板子正面
此图为通道A给一块5s电池充电,通道B为电源模式
板子正面
加装了不太合适散热片的侧面
板子底面
输出口
加装了散热垫片的板子
整机顶面
整机前面
设计图

BOM


评论