资讯动态

STM32双向DC-DC变换器C语言控制程序设计

发布时间:2026/9/1 5:54:41 来源:尧图企业网站定制
简介本资源是一套基于STM32平台实现双向DC-DC变换器的完整嵌入式C语言工程代码面向电力电子、新能源与嵌入式系统方向的中级开发者及高校高年级学生解决双向能量流动控制、PWM驱动、电压电流闭环调节及多重保护机制等核心开发难题适用于储能系统、电动汽车车载充电机、微网能量管理等实际场景。压缩包共353个文件含89个头文件.h定义外设接口与控制参数87个源文件.c实现GPIO初始化、TIM定时器PWM生成、ADC采样、PID调节算法、过压/过流保护中断响应及UART通信协议另有编译中间文件.o/.d、Keil工程配置.uvproj/.uvopt、可执行镜像.hex/.axf等整体大小为8.59MB。已有2345人学习下载提供开箱即用的Keil uVision5工程框架包含完整目录结构、多级模块化代码组织、关键注释及调试支持文件便于快速理解双向DC-DC控制逻辑并开展二次开发与硬件联调。 做双向DC-DC变换器的嵌入式控制最容易被低估的不是主电路设计而是控制程序怎么写。很多人以为将单向Buck的代码加个判断就能变成双向结果一调试就发现输出振荡、模式切换瞬间电流尖峰爆表、甚至上下管直通炸机。这篇文章从工程实践角度分享我基于STM32平台用C语言实现双向DC-DC变换器完整源代码的思路从控制架构到关键代码段逐一拆解希望给正在调双向电源的同仁一些可复用的参考。1. 双向DC-DC的控制难点与STM32选型理由1.1 双向DC-DC的本质从单向Buck/Boost到工作模式切换先明确一个概念双向DC-DC变换器核心功能是能量双向流动。以最常见的非隔离同步Buck/Boost拓扑为例当能量从高压侧流向低压侧时是降压Buck模式从低压侧流向高压侧时是升压Boost模式。这与普通单向电源的区别在于主电路开关管需要同时具备整流和续流能力——通常用两个MOSFET反并联体二极管构成同步整流臂控制电路则要根据功率传递方向动态切换两套PWM策略。实际工程中我遇到的最大困难不是拓扑选择而是状态切换的连续性。单向变换器只需要在一个固定方向上调占空比双向变换器却必须在两种模式间来回切如果切换逻辑设计得不好电感电流会瞬间失控。比如在给电池组放电时突然改为充电电感电流反向占空比必须平滑过渡否则输出电压会跳变甚至触发过压保护。控制程序本质上是在做三件事判断当前应该工作在哪个模式根据反馈量计算合适的PWM占空比以及确保极端工况下系统安全。后面说的所有代码逻辑都在围绕这三件事展开。1.2 为什么用STM32定时器联动是双向控制的关键用STM32做双向DC-DC控制并非唯一选择但性价比和开发便利性确实明显。真正打动我的是STM32定时器与ADC的硬件联动能力。双向变换器最怕的是PWM边沿采样点落在开关噪声上STM32的定时器触发ADC功能能保证采样点精确打在PWM稳定区间内让采集到的电压电流信号干净很多。在选具体型号时我推荐带高级定时器TIM1或TIM8的型号。因为双向DC-DC需要互补PWM输出必须设置死区时间而高级定时器的TIM_BDTR寄存器可以很方便地配置死区还能利用刹车功能做硬件级故障关断。普通定时器虽然也能输出互补波形但死区需要额外逻辑实现实时性和安全性都不如硬件方案。例如我用的STM32F334系列内置高分辨率定时器HRTIM不过实验阶段用F103或F302也完全够用关键是TIM_BDTR死区配置寄存器。另一个必须考虑的点是ADC采样与PWM的同步问题。如果ADC自由运行采样点可能在开关管开通瞬间此时di/dt很大地弹噪声严重测得的电流值抖动很大。STM32的TIM_TRGO事件可以触发ADC注入组或规则组转换把采样时刻锁定在PWM的低噪区间比如占空比中段或峰值电流检测点。这个硬件特性比任何软件滤波都有效软件滤波只能削减噪声幅值解决不了采样时刻本身错误的问题。2. 控制架构设计从硬件拓扑到软件状态机2.1 拓扑选择同步Buck/Boost还是半桥变换器双向DC-DC常见拓扑有两种非隔离的同步Buck/Boost和隔离的双有源桥DAB或CLLC谐振。选择哪种取决于应用场景比如电池储能一般用非隔离拓扑充电桩模块则多用隔离拓扑。我们讨论的C语言源程序聚焦非隔离同步Buck/Boost因为做样机验证和程序开发最直接控制逻辑也容易理解后续移植到DAB只需修改PWM驱动部分和算法结构。同步Buck/Boost主电路很简单两个开关管Q1高压侧和Q2低压侧一个电感L两个电容。当Q1作为主开关管、Q2作为同步整流管时是Buck模式反过来Q2作为主开关管、Q1作为同步整流管时是Boost模式。这里的核心是互补PWM带死区死区时间内两个管都关断靠体二极管续流。死区时间不宜过大太大导致体二极管压降损耗也不宜过小太小存在直通风险。通常根据门极驱动芯片传播延迟和MOSFET关断时间设置我的经验值是100ns到300ns之间。软件上我把主电路抽象成一个双向功率单元模式选择通过状态机完成而不是用if-else简单判断充放电方向。因为实际工况不只是充电和放电两种状态还包括待机、软启动、故障锁定等。一个好的状态机可以避免临界情况下抖动和误切换也是后面介绍的源代码中比较重要的一层。2.2 模式切换状态机充电/放电/待机/故障我将双向变换器的工作模式定义如下IDLE待机模式不输出PWM主继电器断开BUCK_CC降压恒流模式常用于给电池恒流充电BUCK_CV降压恒压模式电池电压达到设定后转为恒压充电BOOST_CC升压恒流模式电池放电升压带载BOOST_CV升压恒压模式输出稳定在设定电压值FAULT故障锁定状态需要人工复位或命令复位状态机转移条件需要同时考虑方向指令和当前反馈量。比如收到“放电”指令但输出电压还高于目标值此时不能直接进入BOOST模式否则电感电流会反向突变。正确做法是先进入IDLE等待母线电压降到合理范围再逐级开启PWM。模式切换逻辑要防止从BUCK直接跳到BOOST的反向穿越除非是有意做无缝切换研究否则硬件容易损坏。使用C语言实现时状态机一般用switch-case加函数指针。我采用结构清晰的switch(action)方式每个状态对应一个处理函数函数内部根据事件执行具体动作。事件包括CMD_START_CHARGE、CMD_START_DISCHARGE、CMD_STOP、FAULT_CLEAR等事件来源可以是串口报文、按键、上位机命令或者内部保护信号。这样设计的好处是代码可读性强后续增加新状态也很方便。2.3 环路控制电压环、电流环、双环串级双向DC-DC电源最常见的控制策略是双环控制内环是电流环外环是电压环或功率环。Buck模式充电时常用CC-CV逻辑电池电压低时用恒流环电压达到设定值后切换为恒压环。Boost模式放电时输出电压稳定由电压环负责电流环作为内环抑制负载扰动。软件上我使用两个PI控制器共享同一个电流反馈值。一个PI控制器负责电压外环一个负责电流内环。电压环输出作为电流环的参考值电流环输出直接对应PWM占空比。模式切换时电压环和电流环的输出必须清零或保持否则积分项会突然释放导致占空比跳变。这个双环结构同样适用双向控制但是要消除一个常见误区Buck模式和Boost模式下的PWM占空比计算公式不一样但PID输出都可以直接用于调整占空比吗严格来说Buck模式占空比近似等于输出电压除以输入电压Boost模式占空比与输入输出比的关系是非线性的直接让PID输出作为占空比会导致不同工作点动态特性差异很大。工程上更实用的做法是PID输出看作调制量叠加到前馈占空比上。比如Buck模式前馈量duty_feedforward Vout / VinBoost模式前馈量duty_feedforward 1 - Vin / Vout。用前馈加反馈能显著提高动态响应解决大范围输入电压变化时的响应迟钝问题。我在代码里把这个计算放到了calc_duty_ff()函数中运行时根据当前模式选择不同的前馈公式。3. 关键代码实现解析基于C语言3.1 PWM生成与死区配置TIM1高级定时器STM32高级定时器TIM1的互补PWM输出是双向DC-DC控制的基础。以下是我在项目中的初始化代码注意死区时间寄存器BDTR的配置void TIM1_PWM_Init(uint16_t arr, uint16_t psc, float deadtime_us) { GPIO_InitTypeDef GPIO_InitStruct {0}; TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct {0}; TIM_OCInitTypeDef TIM_OCInitStruct {0}; TIM_BDTRInitTypeDef TIM_BDTRInitStruct {0}; // 时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); // PA8: TIM1_CH1, PA7: TIM1_CH1N GPIO_InitStruct.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_7; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_OType GPIO_OType_PP; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_TIM1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_TIM1); TIM_TimeBaseStruct.TIM_Prescaler psc; TIM_TimeBaseStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStruct.TIM_Period arr; TIM_TimeBaseStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStruct); TIM_OCInitStruct.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStruct.TIM_OutputNState TIM_OutputNState_Enable; TIM_OCInitStruct.TIM_Pulse 0; // 初始占空比0 TIM_OCInitStruct.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStruct.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStruct.TIM_OCIdleState TIM_OCIdleState_Reset; TIM_OCInitStruct.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, TIM_OCInitStruct); // 死区时间计算死区寄存器单位随时钟变化需要查阅参考手册 // 假设定时器时钟 72MHz死区 200ns公式为 deadtime_us * 72 uint16_t dt (uint16_t)(deadtime_us * 72); TIM_BDTRInitStruct.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStruct.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStruct.TIM_LOCKLevel TIM_LOCKLevel_1; TIM_BDTRInitStruct.TIM_DeadTime dt; TIM_BDTRInitStruct.TIM_Break TIM_Break_Enable; TIM_BDTRInitStruct.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStruct.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRInit(TIM1, TIM_BDTRInitStruct); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); }代码里需要注意两点第一高级定时器必须有TIM_CtrlPWMOutputs(TIM1, ENABLE)才能输出主输出和互补输出这个是许多人容易漏的。第二死区单位值与定时器时钟频率相关不能用固定的寄存器值需要根据实际时钟算。实际项目里我习惯把死区时间做成宏定义或外部可调参数方便调试时微调。死区时间太小上下管存在直通风险死区太大波形畸变率增加、效率降低。我的默认值是200ns对于通常使用的MOSFET门极驱动电路是比较兼顾的。3.2 ADC多通道采样与DMA数据搬运双向DC-DC需要采集的模拟量有高压侧电压、低压侧电压、电感电流、温度等。ADC用定时器触发配合DMA将数据搬到内存避免中断频繁打扰控制流程。下面给出核心配置#define ADC1_CH_NUM 4 volatile uint16_t adc_values[ADC1_CH_NUM]; // 存储DMA搬运结果 void ADC_Trig_Init(void) { ADC_InitTypeDef ADC_InitStruct {0}; DMA_InitTypeDef DMA_InitStruct {0}; TIM_SelectOutputTrigger(TIM1, TIM_TRGOSource_Update); // 注意如果TIM1频繁更新TRGO会比较频繁适合高频控制循环 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); DMA_InitStruct.DMA_Channel DMA_Channel_0; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStruct.DMA_Memory0BaseAddr (uint32_t)adc_values; DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStruct.DMA_BufferSize ADC1_CH_NUM; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode DMA_Mode_Circular; DMA_InitStruct.DMA_Priority DMA_Priority_High; DMA_InitStruct.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, DMA_InitStruct); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_InitStruct.ADC_Resolution ADC_Resolution_12b; ADC_InitStruct.ADC_ScanConvMode ENABLE; ADC_InitStruct.ADC_ContinuousConvMode ENABLE; ADC_InitStruct.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_Rising; ADC_InitStruct.ADC_ExternalTrigConv ADC_ExternalTrigConv_T1_TRGO; // 定时器1触发 ADC_InitStruct.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStruct.ADC_NbrOfConversion ADC1_CH_NUM; ADC_Init(ADC1, ADC_InitStruct); ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 1, ADC_SampleTime_84Cycles); // 电感电流 ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_84Cycles); // 高压侧电压 ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 3, ADC_SampleTime_84Cycles); // 低压侧电压 ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 4, ADC_SampleTime_84Cycles); // 温度 ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); }这里有一个经验采样时间不能太短尤其电流采样需要避开开关噪声尖峰但采样时间太长又会影响控制周期。84周期在72MHz主频下约1.16us对于大多数100kHz左右的开关频率是合理的。如果你用更高开关频率可以考虑多通道交替采样或使用比较器的快速通道。3.3 数字PID控制器与防积分饱和PID控制器是双向DC-DC的核心算法。我用的增量式PID结构代码如下typedef struct { float Kp; float Ki; float Kd; float dT; float integral; float prevError; float output; float outMax; float outMin; } PID_Handle_t; void PID_Init(PID_Handle_t *pid, float kp, float ki, float kd, float dt, float max, float min) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-dT dt; pid-integral 0.0f; pid-prevError 0.0f; pid-outMax max; pid-outMin min; } float PID_Update(PID_Handle_t *pid, float ref, float fdb) { float error ref - fdb; // 积分分离误差过大时禁止积分避免过冲 if (fabsf(error) 0.2f * (pid-outMax - pid-outMin)) { pid-integral pid-Ki * error * pid-dT; } // 积分抗饱和输出限幅时冻结积分 if (pid-output pid-outMax error 0) { // 输出达到上限且误差为正停止积分 } else if (pid-output pid-outMin error 0) { // 输出达到下限且误差为负停止积分 } else { pid-integral pid-Ki * error * pid-dT; } pid-output pid-Kp * error pid-integral pid-Kd * (error - pid-prevError) / pid-dT; if (pid-output pid-outMax) pid-output pid-outMax; if (pid-output pid-outMin) pid-output pid-outMin; pid-prevError error; return pid-output; }防积分饱和这点非常关键。双向DC-DC在模式切换或者负载突变时误差会突然变得很大如果积分项疯狂累积等到误差反向后积分项需要很长时间退掉导致系统超调甚至震荡。我上面用了两种积分处理误差过大时积分分离输出限幅时冻结积分。二者结合能明显提升切换过程平稳性。我在实测中还发现电流环的PID参数与电压环差异很大。电流环动态快Kp可以稍大Ki适中电压环因为电压反馈信号经过电容滤波相位滞后明显Kp不宜过大Ki也不能太小否则会有低频振荡。这些参数没有通解必须结合具体电路调整。我在调试时一般先调电流环把电流环带宽调高了再调电压环。3.4 模式切换与保护逻辑的时序安全双向DC-DC源代码里比控制算法更重要的是保护逻辑。至少要有过压、欠压、过流、过热、硬件故障BRAKE五类保护。软件保护我放在控制循环中每周期检查一次一旦异常立即将占空比清零并置FAULT状态。一个讲究的细节是顺序先关PWM再隔离ADC再断开主继电器。如果顺序反了PWM已经关闭但继电器还带着大电流触点容易拉弧。我的代码逻辑void PowerStage_Shutdown(void) { // 1. 立即关断PWM TIM_CtrlPWMOutputs(TIM1, DISABLE); // 2. 延时几个微秒等待输出电容放电 Delay_us(10); // 3. 继电器断开 RELAY_OFF(); // 4. 软件状态置为FAULT state FAULT; // 5. 保存故障码 fault_code HARDWARE_FAULT; }模式切换时还要考虑软启动。双向变换器从一个方向切到另一个方向如果直接按照目标占空比输出电感电流会剧烈变化。因此我增加了一个斜坡启动逻辑在切换过程中占空比按步进从当前值逐步过渡到目标值。步进大小取决于开关频率和控制周期比如100kHz下每个周期增加0.1%的占空比几百微秒内完成切换。这样虽然牺牲了一点动态速度但可以换得电流平稳。4. 实测调参过程与常见坑4.1 上电时序和软启动第一次没炸是因为这个说起来有些不好意思我第一次做双向DC-DC样机时上电瞬间炸了保险丝。原因很简单主电容初始电压为零控制程序还在初始化阶段PWM却已经输出了大占空比电容充电瞬间相当于短路。后来我强制规定上电时序为先初始化ADC和PWM占空比置0然后延时100ms等待电源稳定再开启硬件PWM输出最后通过软启动斜率逐步增加占空比。用代码表达就是void System_InitAndStart(void) { SystemClock_Config(); GPIO_Init(); TIM1_PWM_Init(ARR_VALUE, PSC_VALUE, 0.2); // 死区200ns ADC_Trig_Init(); PID_Init(vpid, 0.5f, 0.01f, 0.0f, 1.0f/100000.0f, 0.95f, 0.05f); PID_Init(ipid, 0.8f, 0.02f, 0.0f, 1.0f/100000.0f, 0.95f, 0.05f); // 全部初始化为安全状态再使能PWM TIM_SetCompare1(TIM1, 0); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 软启动以1%步进增加到目标 float soft_start_target 50.0f; // 初始目标占空比为50% for (float duty 0.0f; duty soft_start_target; duty 1.0f) { TIM_SetCompare1(TIM1, (uint16_t)(duty / 100.0f * ARR_VALUE)); Delay_ms(1); } }这个方法在日常调试中非常管用。不要一开始就100%目标输出用斜坡让它慢慢爬升观察电流波形是否正常再继续提目标。4.2 模式切换瞬间的电流尖峰处理双向变换器最刺激的时刻就是模式切换。我从放电切换到充电的瞬间电感电流方向要反转如果控制逻辑没有保持电流方向一致性会出现几十安培的尖峰。我示波器上真实见过一次虽然没炸机但MOSFET应力很大。根因在于电流内环参考方向发生了变化但控制代码没有同步处理。所以我在电流反馈采样前加了一个方向标志位volatile uint8_t dir_flag DIR_BUCK; float read_current(void) { float raw adc_to_current(adc_values[CURR_CH]); if (dir_flag DIR_BUCK) { return raw; // Buck模式电流流向定义高压到低压 } else { return -raw; // Boost模式电流反向后取反 } }同时模式切换时不要让PID输出从当前值直接跳到新方向的目标值。我的做法是先暂停电流环强制占空比按照斜坡过渡为零水平再切换方向标志位然后重新启动电流环环路。这个“先归零再反向”的策略牺牲了一点切换速度但大幅降低了电流冲击对MOSFET保护很有价值。4.3 调试中遇到的“测不准”采样延迟、滤波参数、地干扰ADC采样看似简单实际坑很多。我调试时发现电压环频率响应经常与Matlab仿真对不上后来发现是采样延迟导致的相位滞后。电流采样如果经过低通滤波器截止频率太低会让电流环带宽受限截止频率太高又滤不掉开关噪声。这个需要根据不同开关频率做折中。还有就是地干扰。功率地与信号地没有单点连接时地弹噪声会耦合进ADC的采样值。我在底层采样电路上加了一阶RC滤波同时在ADC初始化时选用较长的采样时间配合定时器触发避开开关瞬态基本解决了大部分干扰问题。真正遇到无法滤除的高频干扰我会在软件上再做一次滑动平均滤波但注意滑动窗口太大也会引入相位延迟所以窗口长度一般不超过4个采样点。以上都是我在实际调试过程中踩过的坑每一项都对应代码层面的调整和验证。做双向DC-DC主电路设计决定了理论效率上限但真正决定系统稳定性和可靠性的还是控制程序。上面这套C语言源码框架已经在我几个项目中反复使用从48V/12V双向变换器到电池模拟器都顺利跑通。如果你正在开发类似应用希望这篇文章能帮你少走一些弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价