跳至内容
RM24-25超级电容控制板Lite & Plus「软件篇」

RM24-25超级电容控制板Lite & Plus「软件篇」

碎碎念

虽然传统弱队没有什么技术积累,本项目也只是作为个人学习电源的一部分。我平时也很少写代码,这个项目大概凝结了我所学习的所有C语言的知识与技巧,如果有什么错误还请多多包涵。

超级电容控制板,本质上就是一个DCDC电源。那为什么不直接用成品芯片,而是选择自己写代码制作呢?

RM这种基于功率限制的应用场景,很难说是一种正常的应用场景,所以市面上基本没有相关的芯片可以使用(有一种古老的方案就是使用成品芯片来对超级电容进行充电,我没见过),而如果想要通过外部搭建一个纯硬件电路来对功率进行相关的反馈运算,需要非常扎实的基础以及更深的造诣,无论是作为爱好还是对于竞赛,这样的人才大概不是本科阶段就能培养出来的吧。

把复杂的反馈控制转移到软件层面,通过代码算法来实现对电源的控制,这是一种降低学习门槛的途径,即使是最传统的PID控制算法,也能够完成对电源的反馈控制;同时,纯软件的控制方式,能够更加方便进行修改和调试。完全由软件算法对电源环路进行数字化控制的电源,我称为数字电源。

还有一种是使用成品的电源芯片,其控制环路完全是硬件实现的,但是芯片集成了通信功能(其实没有通信功能也可以),能够通过与单片机进行通信从而修改其输出电压或其他参数,但是不能介入反馈环路,这种我称为数控电源。

本文不过多探讨其他方案的设计,只对我所使用的方案进行详细的介绍。

RM这种非正常的应用场景,没必要往上面死磕,如果你真的喜欢玩电源的话,可以去研究各种不同的功率拓扑,环路控制,其中有非常深奥的学问。

代码初始化

代码初始化部分由CubeMX完成,相关说明请查看 CubeMX篇。

控制原理

本项目延续2023年开源版本的功率拓扑,其原理一致。电容充电的时候,直接看作同步Buck电路来分析;放电的时候,直接看作同步Boost分析。这两种状态不会且不可能同时运行,所以完全可以独立进行分析,但是其切换的过渡可能会产生一些小问题。

充电(Buck)

Buck 电路框图

图中的电池端口,应接入电源管理模块的Chassis接口。

Buck电路的伏秒积定理什么的可以不用去理会,这个你感兴趣的时候再去深入学习,现阶段直接根据公式套参数进去选电感电容,或者找一个现成的原理图抄一个板子出来(千万要把板子画好,画板不规范,炸板两行泪)。

学习它的原理,以及各个节点的电压电流波形,能更好地帮助你调试与维护,更快找出问题所在。

对于Buck的控制,我们只需要记住一个公式:

Vout=Vin×DutyV_{out} = V_{in} \times Duty

对于本图:

Vcap=Vbat×DutyV_{cap} = V_{bat} \times Duty

这个公式可能会因为一些器件参数产生些许误差,不要紧,只要最终他呈线性关系就好,否则需要好好检查电路。

  • Vcap:电容组电压。
  • Vbat:电池电压。
  • Duty:Buck型开关占空比。栅极控制器的输入是PWM,当PWM处于高电平时,定义为上管导通,下管关断;当PWM处于低电平时,定义为上管关断,下管导通。故,上管导通的时间占开关周期时间的比值即为开关占空比。占空比的值为%的形式,从0%-100%。

在超级电容控制板运行在充电状态下,输入电压是电池电压,输出电压是电容电压。电池电压可以认为是一个恒定不变的值(24V),而超级电容充电的过程,电容电压会随着充电而提高,所以我们需要对开关占空比进行控制,使其能够让电容组进行充电,并且能够进一步控制充电的功率或者电流。

电池消耗电量,电压也会降低,为什么能看做电压恒定? 电池的充放电曲线存在一个平台电压,其80%以上的电量都在这个平台电压附近,一般认为单颗锂电池的平台电压为3.7V,而TB47/TB48系列电池为6串的锂电池,其平台电压为22.2V,满电电压26V,所以认为平均其处于24V不改变。但是超级电容的充放电过程,其电压不存在平台电压,整个过程电压变化范围非常大,所以超级电容的电压变化不可忽略。

通过这个公式可以知道,输入电压不变的情况下,改变开关占空比的值就可以直接控制输出电压。电容组在充电的过程中,单纯控制输出电压是不可行的。因为电容的内阻很小,如果电压不合适会产生一个很大的电流,然后爆炸💥。超级电容应该像电池充电一样,先恒流再恒压进行充电。所以就需要有输出电流检测电路进行电流反馈。

开关占空比对应的输出电流如何计算?

一般的电源都是恒压源,就是他正常工作的时候电压是不会改变的,此时,电源输出的电流是根据负载情况,然后通过欧姆定律 I=U(set)/R(load)I = U_{(set)} / R_{(load)} 来决定的,电源无法控制这个电流的大小。绝大多数的设备都只能使用恒定的电压进行供电才能正常工作。

当这个电流超过电源的供电能力时,电源会进行保护,转变为恒流源(有的电源是直接关闭输出),恒流源的时候,他能对电流进行【限制】,而非【控制】。在这种状态下,输出电压会随着限制的电流大小与负载电阻变化而变化,即 U=I(limit)×R(load)U = I_{(limit)} \times R_{(load)}。一般需要使用恒流源驱动的电路,其往往都有相对固定的直流电阻。例如LED,电池充电,电容充电。

应该如何给超级电容充电呢?

  • 电压是产生电流的原因,有电压差才会产生电流;电流会从电压高的地方流向电压低的地方,且在同一时间仅会有一个明确的方向。

给电容组充电,就是希望让电流流向电容组。那电容组没电的时候,电压不是非常低吗,他肯定会流向电容组啊。虽然电容组电压很低,但是他不是直接连接到电池上的,而是接了一个降压电路,所以电容组的电压不能直接和电池电压进行比较,应该使用降压电路的输出电压与电容组电压项比较。当降压电路的输出电压比电容组当前电压要大的时候,电容组才能进行充电。

我队2024赛季所使用的超级电容组,内阻大约为350mΩ。在其0V的时候,如果使用3.5V对其充电,会产生大概10A的电流,随着电容能量的提高,其电压的升高会降低电容组与电源电压的压差,故充电电流会随着减小,当电容充电电流小于一定值的时候,就认为电容充到了3.5V。但是电容组的额定电压为21V,如果直接使用21V电源对0V的电容组进行充电呢?

I=U/R=21/0.35=60AI = U / R = 21 / 0.35 = 60A

💥

开始瞬间的电流将达到60A,这是不可被接受的。

以上只是针对初始状态的情况下进行分析的,电容组的电压会因为充电而提高,降压电路的输出电压与电容组之间的压差会降低,进而使充电电流减小。

那,在电容组电压为0V的时候,我给一个比较小的电压,比如1V,然后随着电容充电,电容组电压的提高,逐步提高提高输出电压的大小,使输出电压与电容组电压的差值维持一个合适的大小,这样不就可以进行小电流充电了吗?

可以,但是实际上,当电容开始充电的时候,因为你的电源输出和电容组是直接接在一起的,基本可以认为电容组的电压和输出电压的大小一样,无法准确判断充电的电压差。所以,加入电流的检测,使用电流作为PID的反馈值,设定一个目标电流,PID的输出去控制开关占空比,PID算法自动去调整充电的电压差,就可以实现恒定电流充电的效果。

恒功率也是一样的,使用功率作为反馈,设定一个目标功率值,PID的输出去控制开关占空比,就可以起到恒功率的效果。

PID运算的调节逻辑:

  • 充电目标功率50W,实际反馈功率30W → 想要提高↗充电功率 → 需要提高↗充电电压 → 提高↗开关占空比(Duty)。
  • 充电目标功率50W,实际反馈功率70W → 想要降低↘充电功率 → 需要降低↘充电电压 → 降低↘开关占空比(Duty)。

请注意,电压、电流、功率三者同一时间只能实现一个量的闭环控制,比如进行电流闭环控制的时候,电压、功率都是无法被控制的,只能通过测量得出其大小。

PID控制算法,与控制系统本身没有任何关系,在系统可以被控制的前提下,它只需要一个反馈量、一个控制量(PID输出)和一个设定值即可完成控制。你无需关注反馈量与控制量之间的转换关系,只要参数合适,PID就会自己算出一个合适的控制量,使反馈值达到设定值。

关于PID控制算法的相关内容,本文无法进行过多的展开讨论。使用PID控制算法,是一个很简单的过程,而他的公式或者控制模型,本人并没有过多的了解。

放电(Boost)

Boost 电路框图

图中的电池端口,应接入电源管理模块的Chassis接口。

对于Boost的控制,我们只需要记住一个公式:

Vout=Vin/(1−D)V_{out} = V_{in} / ( 1- D )

对于本图:

Vbat=Vcap/(1−D)V_{bat} = V_{cap} / ( 1 - D )
  • Vcap:电容组电压。
  • Vbat:电池/底盘电压。
  • D:Boost型开关占空比。栅极控制器的输入是PWM,当PWM处于高电平时,定义为下管导通,上管关断;当PWM处于低电平时,定义为下管关断,上管导通。故,下管导通的时间占开关周期时间的比值即为开关占空比。占空比的值为%的形式,从0%-100%。

诶嘿,看出什么区别了吗?本公式,是书本上总结出来的计算公式,因为推导公式的时候,是使用非同步的Buck和Boost电路,所以,其所对应的开关占空比的定义不相同。

本项目中,我使用的是【Duty】的这个定义,故,我们在此转换一下:

Vbat=Vcap/DutyV_{bat} = V_{cap} / Duty

Duty:Buck型开关占空比。栅极控制器的输入是PWM,当PWM处于高电平时,定义为上管导通,下管关断;当PWM处于低电平时,定义为上管关断,下管导通。故,上管导通的时间占开关周期时间的比值即为开关占空比。占空比的值为%的形式,从0%-100%。

在Boost这一小节剩余的内容中,公式均以:Vbat=Vcap/DutyV_{bat} = V_{cap} / Duty 为准;开关占空比均为【Duty】定义的占空比。

所以,其实Boost公式只是Buck的公式移了项,没有区别吗?

不是!由于电流存在方向,电流流入的方向定义为输入,电流流出的方向定义为输出。Boost的输出与Buck的输入,Boost的输入与Buck的输出交换了位置。所以他们输入输出与开关占空比(Duty)的关系也改变了。Buck的输出是随着开关占空比(Duty)的增大而增大,最大能够达到输入的电压。Boost则是随着开关占空比(Duty)的增大而减小,最小能够达到输入电压。

可以看到,电流检测的位置发生了改变,这里检测的是电池/裁判系统的电流。这里我们要有一个"常识":底盘与电池是直接相连的,所以底盘的电压,正常情况下应该在24V左右,且变化范围非常小。所以此时,电源本身无法对底盘使用的电流以及功率进行控制。电机消耗多少电流功率完全取决于电控给电机发送的数据。

裁判系统的功率限制,只是一种机制上的限制,他限制的不是实际使用的功率。在2025赛季的规则中(2024年12月25日1.1.0版本),假设裁判系统功率限制50W,在不装备超级电容模块的情况下,你仍然可以以60甚至更高的功率去使底盘移动(但是电管超过300W会过载保护,关闭输出)。超出裁判系统的部分,先去消耗缓冲能量,缓冲能量消耗完之后,裁判系统会将你的底盘断电。

若是装备了本超级电容模块,在移动的时候,可以通过CAN对超级电容模块发送信息控制是否开启超级电容,若不开启超级电容,则与不装备超级电容模块一致。若开启超级电容,则代表底盘将会以高于功率限制的功率去移动,这个时候超级电容模块会自动分配电池流向底盘的电流大小与电容组流向底盘电流的大小,优先保证从电池流向底盘的功率大小在功率限制以内,超出功率限制的部分由电容组进行补充,从而实现功率分配。

例如,功率限制50W的情况下,底盘运行功率100W,电池消耗50W,超级电容模块补充50W;底盘功率动态变化,电池消耗的功率基本上不变,超级电容模块会进行动态补充,直至电容组能量耗尽,才会自动关闭输出,并且通过CAN向外反馈相应的状态消息。

请注意,电流在同一时间仅会有一个明确的方向,本拓扑连接的超级电容模块不存在一边充电一边放电的现象。

超级电容模块如何实现功率分配呢?

其实很多时候,不需要特地说功率,因为功率 P=U×IP = U \times I,他是一个可以比较"泛"的统一指标,他是由电压和电流共同确定的,在不同的电压平台下,相同的电流会有不同的功率表现。但是现在是一个明确的电压,直接说电流更能体现负载的消耗,以及电源的供电能力。

  • 底盘在正常移动的时候,电压基本不变,电机消耗的电流是由CAN向电机发送消息来控制的,也就是功率仅由CAN来控制。
  • 电压是产生电流的原因,有电压差才会产生电流;电流会从电压高的地方流向电压低的地方,且在同一时间仅会有一个明确的方向。

关于第二点,由于电容组经过升压电路升压之后才对底盘进行输出,不能认为电容组电压比底盘低,电流就流向电容组。而是要把电容组和升压模块当成一个整体,使用升压电路的输出电压与底盘电压进行对比,判断电流方向。

底盘仅由电池进行供电的时候,假设此时电池电压24V,电池内阻0.2Ω,负载等效电阻4.6Ω。则回路中流过的电流大小为5A,电池对负载提供了 4.6×5=23V4.6 \times 5 = 23V,即为此时电路中测量到的电压,电池内阻会自己消耗掉1V,这会导致电池发热。

假若此时加入超级电容模块,超级电容模块的内阻也为0.2Ω,让其升压到24V进行输出,接入上述回路中,由于其电压、内阻均与电池一致,超级电容模块与电池会以相同的2.5A电流对负载进行供电。如果此时改变超级电容模块的输出电压,电压变大,就会使得超级电容模块输出的电流变高;电压变小,就会使得超级电容模块输出的电流变低。

这个电压变化的高低的具体公式我无法推导出,且此模型为我本人用于理解此电流分配的现象而假设的,不一定为实际模型。

由此可知,调整输出电压来实现功率分配的方式是可行的,但是,要获取电池内阻、负载等效电阻和超级电容模块内阻才能计算出实际的分配电压未免过于困难,而且机器人在移动的时候,其功率不同而形成的等效内阻大小也不同,所以我们再次引入PID控制算法,让其根据电池端口的输出功率,自动调整超级电容模块的输出电压。

使用电池输出功率作为PID的反馈值,设定一个目标功率,PID的输出去控制开关占空比,PID算法自动去调整超级电容模块输出的电压,就可以实现功率分配的效果。这个目标功率,其代表超级电容模块与电池共同给底盘供电时,底盘能够从电池获取到的功率大小,若有裁判系统,则为电管管理模块检测到的Chassis接口输出的功率大小。所以目标功率应等于或略小于裁判系统功率限制,因为此时底盘运行的实际功率已经超出了裁判系统功率限制,超出部分的功率,由超级电容模块通过PID的调节进行动态补充。

PID运算的调节逻辑:

  • 电池目标功率50W,实际反馈功率30W → 想要提高↗电池分配的输出功率 → 需要降低↘超级电容模块分配的输出功率 → 需要降低↘超级电容模块的输出电压 → 提高↗开关占空比(Duty)。
  • 电池目标功率50W,实际反馈功率70W → 想要降低↘电池分配的输出功率 → 需要提高↗超级电容模块分配的输出功率 → 需要提高↗超级电容模块的输出电压 → 降低↘开关占空比(Duty)。

但是请注意,此时超级电容模块没有进行且无法进行对底盘运行功率的限制,底盘运行功率永远由CAN向其发送的相关消息进行控制。

在电源应用中,PID控制算法,我们只需要使用P和I两个参数即可完成对电源环路的控制。为什么?我也不知道。

具体的代码请查看工程文件,其有相应的代码注释。

断电(PMOS关断)

本项目所使用的拓扑结构,在硬件上存在一个缺陷:当机器人死亡,电源管理模块Chassis口的供电被切断时,超级电容模块会通过NMOS的体二极管向底盘继续供电,这是不符合制作规范的。为了解决这个问题,本项目在控制板的同步半桥与底盘之间添加一个PMOS用于完全关闭超级电容模块的输出。

PMOS的开关由电池端口的电流进行判断(Power_Down() 函数),使用的是滞回比较器(Hysteresis_Comparator()),比较阈值分别是 PMOS_ON_CURRENT 和 PMOS_OFF_CURRENT,防止临界状态反复开关:

  • 机器人存活的情况下,底盘只要移动,电池端口都会有电流,此时打开PMOS。
  • 死亡的时候,电源管理模块切断了Chassis接口的输出,此时当然不会有电流,所以就把PMOS关断即可。

但在代码实现中,直接关断是不够的,还做了以下处理:

  • 关断延迟 500ms:电池电流低于关断阈值后,持续 500ms 才真正关断 PMOS,避免底盘瞬时掉电误判。
  • 重新打开机制:PMOS 关断 2s 后,会检查母线电压(Vbat)是否仍高于电池欠压阈值。如果底盘其实没断电(比如只是电流暂时为零),会重新打开 PMOS 给底盘供电,延迟 100ms × 10 后恢复。
  • 开关时序(Lite):失能时先关 PWM 再关 PMOS(100ms 延迟),避免关掉 PMOS 之后电感续流无处可去产生电压尖峰击穿 MOS;使能时先开 PMOS 再开 PWM(10ms 延迟),避免 PMOS 开关速度过慢导致烧毁。Plus 版本则是直接通过 ENABLE_CAP 引脚控制功率电路使能。

另外,若 PWM 处于刹车状态,Power_Down() 会清除所有 PID 的积分(PID_Clear_Integral()),避免刹车期间 PID 仍按错误状态累积。

运行逻辑

其实我感觉,这玩意看看我代码也能看得懂,也就几个if,else。本代码为裸机环境运行的,不存在函数乱序执行的现象,只会按顺序执行,因为所有重要的函数全部都在最高优先级的中断当中,不会且不能被打断。

上电初始化

上电流程图

上电的各种初始化,均在main.c中完成,在开启ADC中断之后,其会每100Khz进入一次stm32g4xx_it.c中的DMA1_Channel1_IRQHandler()函数,此条函数为ADC转换完成并且DMA搬运之后产生的中断函数,在此函数中调用A_Timing_Ranking_Idea()函数,此条函数是进行整个电源状态运行与环路计算的函数,必须为最高优先级运行,不能被打断。

在上电初始化执行到Power_Loop_Parameter_Init()函数的时候,会通过把TIM1_BKIN引脚拉高,以避免从启动PWM到PID运算的这段时间中,半桥以错误的占空比进行运行,也就是上电的时候你可能会听到电感"嗒"地响一下,这会带来比较大的浪涌电流。

TIM1_BKIN只会把PWM的输出关闭并拉低,不会对定时器的计数和ADC的触发采样造成影响,其环路仍在持续运行。

ADC采样与半桥开关

ADC采样与半桥开关时序

定时器计数器、半桥开关占空比与ADC触发采样时刻如图所示,ADC的采样时间配置为24.5时钟周期,转换时间为固定的12.5时钟周期,ADC时钟为56.7MHz,单次采样转换时间:1/(56.7×106)×(24.5+12.5)=652ns1 / ( 56.7 \times 10^6 ) \times ( 24.5 + 12.5 ) = 652ns。半桥开关频率为100KHz,单个开关周期为10us,假若在电容组电压很低,例如5V的时候,电池电压25V,则此时大概需要运行在20%占空比中。上半桥开通的时间仅为2us,希望他能够在上半桥开通的中间前后都能够进行采样,由于我需要进行两次采样,电容电压/电流(同步)和电池电压/电流(同步),所以我将触发时间设置为占空比的3/8,即在上半桥开通的3/8处进行采样,这样子就可以实现一个略早于1/2处,一个略晚于1/2处。

以上 3/8 处采样是 Lite 的实现(代码中 SET_ADC_TRIGGER_COMPARE(b) = (b >> 3) + (b >> 2))。Plus 由于采用了注入通道进行电压电流采样,且占空比使用下管主导(PWM2 模式),触发位置是占空比的 1/2 处(SET_ADC_TRIGGER_COMPARE(b) = b >> 1)。

为什么要在1/2附近采样?

因为开关电源输入输出因为电容充放电,其纹波大概为三角波的形式,在上半桥开通的1/2处采样,就相当于在三角形的"半山腰"进行采样,约为多个三角波周期的平均值,能够获得较高的精度。

我这是ADC采样频率与开关周期相同的情况,你也可以,把ADC的采样频率提高到2倍开关频率以上,对整个三角波进行完整的采样,然后计算平均值能够获得更高的精度。

需要注意的是,应尽量避免半桥开关的瞬间进行采样,即使是INA240这种带有PWM抑制器的检流放大器,其在开关瞬间也会产生一定的误差,他只是应对这种跳变能够有更快的恢复速度,以及更小的误差。另外,开关瞬间可能会对整个板子的电源轨产生一个巨大的毛刺噪声,如果处理不好,其同样也会影响到ADC的采样精度。所以应该尽量避免ADC在开关瞬间进行采样。

INA240对PWM抑制的效果是有限度的,在其最新型号INA241的描述中,其PWM抑制器最大为125KHz有效,可能INA240大概就100KHz左右,若在更高的开关频率中使用,其性能也无法得到保障。

当所有ADC采样转换完之后,DMA搬运数据完成产生DMA中断,在ADC的DMA中断中对电源环路进行计算更新,确保能够及时处理最新的数据。

逻辑处理与环路运算 A_Timing_Ranking_Idea()

本函数为整个电源状态逻辑处理与环路运算的函数,其每次运行都必须要执行以下几个函数:

  • ADC_Convert_To_Reality():ADC数据还原为真实值。
  • State_Change():状态判断与改变。
  • Power_Loop():状态相应的环路的执行。

而剩下的几条内容,则会定时分频进行运行,分时处理减少CPU占用,保证电源环路的优先运行。

  • ADC_Value_AVG_Compute(DIV512):ADC均值滤波,此滤波用于发给电控,而非环路运算的滤波。运行频率 100K/512 = 200Hz。
  • Power_Calculations():功率计算,发给电控的。运行频率 200Hz。
  • Can_SendMess(&sCAN_TX_data):CAN发送函数。运行频率 200Hz,基本就能满足电控对于底盘的功率实时控制了。
  • CAN_ReceiveDataRefresh_Flag:限制CAN接收数据的刷新速率,运行频率 100K/1024 ≈ 98Hz,避免输入数据频繁跳变引起不必要的问题。
  • HAL_IWDG_Refresh(&hiwdg):喂狗。运行频率 100K/4096 ≈ 24Hz。
  • LED_Refresh():LED状态刷新函数。运行频率 100K/8192 ≈ 12Hz。

另外还有一个 Free_Loop() 函数,放在 main 的 while(1) 中空闲运行,不需要实时性的东西都丢到这里:

  • Temperature_Calculations():温度计算函数(Plus 还有 ADC 温度通道的规则组 DMA 采样)。
  • Plus 的 DebugOut_UART():串口日志回传,使用 VOFA+ 的 JustFloat 格式,115200 波特率。

其实际的运行情况与运行时间大概如下图所示:

运算时间

可以看到,其每周期环路运算时间均在10us开关周期以内,且接近5us,这带来了一个好处,假设PI参数合理的情况下,我每个周期都能根据本开关周期内的电压值,对下一个周期的占空比进行调整,以获得最快的响应速度(因为你在这个周期更新开关占空比,其下一个周期才会生效)。并且,我的数据并未经过软件滤波,仅仅通过一个硬件RC低通滤波,我可以保证我每次处理的数据都是最新,且实时的(RC滤波会导致产生一点相位滞后,但是相对于软件计算造成的滞后,几乎可以忽略不记)。

不过这会带来一个影响,就是ADC采集到的数据是短时间的瞬时值,其对噪声的抑制能力非常差,但是他可以捕捉到短时间内的过冲或者跳变,获得最高的响应速度。如果经过几个周期的平均滤波或者其他滤波,其采样精度可以得到比较好的保证,但是也会把过冲,瞬时跳变等电压滤除,不能及时对其做出响应。在我看来,获得更高的响应速度远比获得精准的输出电压更好,请稍微考虑以下应用场景啊。

不过实际上,我对输出平均电压的长时间观测绘图来看,其跳变幅度小于0.1V(实际上输出纹波都有0.2Vpp了),我也很难说这到底准不准,且对于本应用场景,0.2Vpp对于电机运行所产生的噪声来讲,也不算太糟糕。

ADC_Convert_To_Reality()

这个函数是将ADC的值根据拟合参数转化出的瞬时值的函数,仅用于控制环路的运算。

状态判断与改变 State_Change()

这个函数对超级电容控制板的运行状态进行判断与改变。使能/失能(Enable_bit)的控制直接从CAN获取,但充电/放电不再由CAN直接指定,而是由功率环路根据母线功率自动判断(详见下文 Automatic_Power_Compensation_Loop())。其他全都是保护相关的判断,使用滞回比较器(Anomaly_Detection())进行检测,只要存在一个保护因素,超级电容控制板就不会执行CAN对应的命令,并反馈当前的状态到CAN。本函数,只能使超级电容控制板进入保护,而无法使其退出保护,保护状态的退出根据所执行的保护状态的函数内容进行判断。

在DEBUG模式下(宏定义 DEBUG_MODE),会无视CAN消息,强制使能并运行在设定的功率限制下,方便不带电控调试。

功率环路 Power_Loop()

进入本函数后,会根据当前状态进入以下的某一个函数中,请注意,每次循环仅会,且仅能执行一条对应状态的函数,只要处于任意一种保护状态,其都无法进行充放电。下列函数执行是拥有优先度的,第一个优先度最高,依次往下递减。

  • Boom_Protection_Loop():保险丝熔断保护(Plus)。一旦进入就必须更换保险丝,重新上电。
  • Bat_Undervoltage_Protection_Loop():电池欠压保护。一旦进入就必须换电池,重新上电。
  • Over_Current_Protection_Loop():过流保护,也是短路保护。过流不会恒流,只会直接关断。
  • Bat_Over_Voltage_Protection_Loop():母线过压保护。
  • Over_Temperature_Protect_Loop():过温保护(MOS 或电容组)。
  • CAN_Offline_Loop():CAN 离线保护。
  • Soft_Start_Loop():软起动保护,每次上电必须执行。
  • CAP_Undervoltage_Protection_Loop():电容组欠压保护,设定值为电容组额定电压的 30%。
  • Automatic_Power_Compensation_Loop():自动功率补偿环路(使能状态)。
  • Wait_Loop():待机状态(失能状态)。

其中大部分保护函数的内容基本都是,关闭半桥,关闭PMOS,等待异常状态恢复,重新运行。重点仍然在自动功率补偿环路中。下文有自动功率补偿环路的原理,请结合代码重新阅读理解。

自动功率补偿环路 Automatic_Power_Compensation_Loop()

这个函数是 V1.1 中 Charge() 与 Discharge() 两个函数合并后的版本,超功率自动补偿(放电),低功率自动充电。其主要逻辑如下:

  • 使用母线功率(Pbat = Vbat × Ibat)作为反馈,PowerLimint(裁判系统功率限制,由电控通过 CAN 下发)作为设定值,进行 PI 运算,直接输出 PWM 占空比(SET_PWM_COMPARE)。
  • 充电状态下,使用滞回比较器判断充电电流是否超过安全充电电流,超过则启动限流 PI,通过 Loop_Competition_Buck() 与功率环竞争取小值,防止充电电流过大。
  • 放电状态下,用滞回比较器监控母线电压与电容组电压,超过软件过压阈值时启动对应的 PI 环路(Loop_Competition_Boost),避免升压过高或过充。
  • 使用滞回比较器监控电容组电压,若电压低于欠压阈值并持续一段时间(500ms),则进入欠压保护;若欠压的同时母线超功率 100W 持续 200ms,则提前进入欠压保护(防止超电没电时硬扛功率)。
  • 每次环路运算还会同步更新 ADC 采样触发比较值(SET_ADC_TRIGGER_COMPARE),保证采样时刻与占空比同步。

发送给电控的状态码

代码通过 CAN 发送 CAN_TransmitDataTypeDef 结构体给电控,其中 SuperCapState 字段对应以下状态码(SuperCapStateTypeDef 枚举),电控可以根据状态码判断超级电容模块当前处于什么状态:

状态码枚举含义
0DISCHARGE放电中
1CHARGE充电中
2WAIT待机(失能)
3SOFRSTART_PROTECTION软起动保护
4OVER_LOAD_PROTECTTION过载(过流)保护
5OVP_BAT_PROTECTION母线过压保护
6UVP_BAT_PROTECTION电池欠压保护
7UVP_CAP_PROTECTION电容组欠压保护
8OTP_PROTECTION过温保护
9BOOM_PROTECTION保险丝熔断保护
10CAN_OFFLINECAN 离线

此外,SuperCapReady 字段为 1 表示超级电容可用,0 表示不可用;SuperCapEnergy 为超级电容可用能量(0-100%);ChassisPower 为底盘功率,传输时右移了一位扩大量程,接收时需要左移还原(丢精度)。

本代码的主要运行逻辑到此就结束了,我已经尽力写得比较简单了,大多都是if,且都为单独的判断条件,各个函数之间关联性基本没有,希望不会对各位的学习造成太大的困难(我自己本身的代码水平也一般)。

代码优化

ATRI算法

A_Timing_Ranking_Idea()

这个就是随便写写的一个小优化,因为不是所有函数都需要在每个开关周期执行。

使用二进制计数的好处在于,可以直接使用位操作来进行比较,最大化运行效率,坏处就是其分频无法非常精确,但是本应用也不需要精确的分频系数,所以无所谓了。CAN发送的频率为200Hz(100K / 512),基本就能满足电控对于底盘的功率实时控制了。

本优化方法,对于少数的函数分配可以起到一定的作用,且较简单,如果需要大量的任务进行分时执行,会比较烧脑,可能会产生分配冲突之类的。

常规优化

  • 编译器优化。最初使用的是-Ofast,后来改成了-Og,运行效率几乎没影响,避免优化过猛导致程序跑飞。遇事不决,编译器优化,神!
  • 硬件浮点运算加速,可以使浮点运算成为单周期指令,但是其真正速度会受到SRAM速度的影响。
  • 所有函数进行内联编译,这使得在编译阶段就把函数拆散,优化程序跳转函数的过程。

为什么不用CCMRAM?

因为G4系列有一个叫作Adaptive real-time accelerator的Flash加速器,其可以做到Flash的零等待执行。使用CCMRAM进行优化,提高不超过5%。

  • 定时器更新使用寄存器地址直接赋值。

使用以上优化策略之后,两个PI运算的速度从4us优化到了2us左右,基本就是极限速度了。

下面以放电环路的主要计算为例:

这张图是-O0编辑器优化生成的汇编指令:

O0优化汇编

可以看到汇编命令中有两行branch (with link)命令,应该是要链接到PID.c里对应的函数里:

bl  0 <PID_Preload_Integral>
bl  0 <PID_Compute>

下图就是PID_Compute()函数的部分汇编指令(太多了,截不完):

PID_Compute汇编

接下来是开启-Ofast编译的汇编指令:

Ofast优化汇编1

Ofast优化汇编2

可以看到,Ofast编译出来的汇编与O0编译出来的汇编精简不少,并且由于STM32G4拥有FPU,其单精度浮点乘法vmul.f32是可以做到1个时钟周期的。

其运行瓶颈并不在运算中,而是大量的Load指令ldr与vldr,Store指令str与vstr,还有一些函数跳转。

虽然我不懂代码优化,目前以我的水平来说,开启编译器优化确实能够显著提高代码运行效率。

相关汇编指令可以查阅《Arm® Cortex®-M4 Processor Technical Reference Manual》

PID积分预加载

这是我随便起的名字,因为他确实是如此操作。

见PID_Preload_Integral()函数,因为每次上电,电池端口的电压基本都是固定值,而电容组电压为非固定值,如果PID的首次运算,都从0开始累计积分,其PWM输出也会从0开始,PWM占空比与半桥两边电压比以及当前所处的状态不对应,就会爆炸💥。所以,PID首次运行的时候,让其通过半桥两边的电压比值计算出一个相近的占空比,再以此为始进行调节,可以降低爆炸的风险。

PWM刹车(TIM_BREAK)

加上这个功能,可以任意控制半桥的开关,例如,上电尚未进行PID运算的时候,其PWM为初始值,可能会导致爆炸,所以通过关闭PWM输出,可以保证在PID运算出正确值的时候再关闭半桥。

现在看看这个项目的代码部分,实现起来真的挺简单的,可能就是对各种状态所处的环境有所评估,如何进行保护比较有挑战性,整体的逻辑应该都算是比较简单的,非常流水化,相信有一定编程能力的各位一定都能看得懂。

参考资料

  • Arm Cortex-M4 Processor Technical Reference Manual Revision r0p1
  • 【合集·手把手教你学数字电源系列-哔哩哔哩】
  • 【合集·全數字電源零基礎教學,入門到精通-哔哩哔哩】
  • 【合集·STM32数字电源教学视频-哔哩哔哩】

下载地址

RM-SuperCapCtrlLib 预览版 | 稳定版 | 历史版本

附:代码库说明

本文对应的是作者开源的 RM-SuperCapCtrlLib 超级电容控制库,当前版本为 V1.2.2608-beta(代码版本以 GitHub 仓库为准),编码格式 UTF-8,CubeMX 6.12.0,STM32G4 固件包 1.6.1,兼容 Lite 和 Plus 控制板(通过 LITE / PLUS 宏定义区分,宏定义在对应版本的 CubeMX 生成的 main.h 中,请勿自行定义)。

本控制库只是运行逻辑部分的代码,具体使用 Lite 或 Plus 版本控制板还需要搭配其相应的 CubeMX 初始化工程。

主要文件:

  • Inc/SuperCapCtrl.h:状态枚举、状态标志位结构体、CAN 发送数据结构体、ADC 拟合参数结构体、寄存器操作宏(含 Lite/Plus 两种实现)。
  • Src/SuperCapCtrl.c:核心控制逻辑,包括 A_Timing_Ranking_Idea()、State_Change()、Power_Loop()、Automatic_Power_Compensation_Loop()、Power_Down() 及各保护函数。
  • Inc/PID_v1.h、Src/PID_v1.c:PID 库,修改自 Arduino PID Library,只使用了 P 和 I 两个参数,包含积分预加载、积分限幅、环路竞争(Loop_Competition_Buck/Boost)等功能。