ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32双向DC-DC变换器C语言控制实现与调参实战

STM32双向DC-DC变换器C语言控制实现与调参实战 简介本资源是一套基于STM32平台实现双向DC-DC变换器的完整嵌入式C语言工程代码面向电力电子、新能源与嵌入式系统方向的本科高年级学生、研究生及初级工程师解决双向能量流动控制、PWM驱动、电压电流闭环调节及硬件保护等核心开发难题适用于储能系统、车载充放电、微网能量管理等实际场景。压缩包共353个文件含89个头文件.h定义外设寄存器与功能接口、87个源文件.c涵盖GPIO/TIM/ADC/UART初始化、PID控制算法、过压过流中断保护及LCD显示逻辑辅以编译中间文件.o/.d/.crf和Keil工程配置.uvproj/.uvopt整体大小为8.59MB。已有2345人学习下载代码结构清晰、模块划分合理包含可直接烧录运行的.axf与.hex镜像配套keilkill.bat批处理脚本及多版本GUI备份便于工程复现、调试分析与二次开发。 最近在整理之前做的一个储能双向电源项目顺手把底层那套双向DC-DC变换器的C语言控制代码翻了出来。其实这块内容挺有意思的你在电池充放电测试、UPS、新能源储能、电机回馈制动这些场景里都会遇到它。这个项目用的是STM32平台核心是同步Buck/Boost双向拓扑代码全部用C语言写成包括PWM生成、ADC采样、双环PID控制、模式切换和保护逻辑。因为整套代码我在两个项目里实际跑过稳定性和可移植性都验证过所以干脆把设计思路、实现细节、调参过程和踩过的坑都写出来给正准备做双向电源的工程师省点时间。这篇内容比较适合几类人一是刚接触电源控制、想搞懂双向DC-DC怎么用单片机实现的新手二是已经有基础、想找一个可参考的代码架构和调参思路的嵌入式工程师三是做电池管理系统、储能设备需要快速搭建双向变换器控制原型的朋友。看完之后你至少能把下面这几件事搞清楚为什么选择同步Buck/Boost拓扑、STM32的资源怎么分配、C语言代码怎么组织、双环控制怎么调、以及调试中容易炸管子和烧驱动的那几个坑到底怎么避开。1. 整体方案设计与拓扑选型1.1 双向DC-DC到底要解决什么问题先花点时间把需求说清楚。单向DC-DC大家都熟输入一个电压输出一个固定的、或者可调的电压能量只能从输入端流向输出端。双向DC-DC的区别在于它允许能量在两个方向上传导。比如电池充放电测试设备放电的时候电池的能量要通过双向变换器回馈到直流母线充电的时候直流母线的能量又要传给电池。再比如电动汽车的再生制动电机发电的能量要能灌回电池。如果不用双向变换器就得单独做两套单向变换器或者用一个大功率电阻把回馈能量烧掉成本高、效率低、还发热严重。这个项目要做的双向DC-DC输入输出电压范围是18V到36V额定功率200W最大电流10A。控制目标很简单降压方向恒压输出升压方向恒流或恒压输出两个方向之间能自动切换切换过程电流不能有大的冲击。这个参数其实覆盖了常见的24V电池系统和伺服电机回馈制动场景做完之后直接改改参数就能用。1.2 为什么选同步Buck/Boost而不是其他拓扑双向DC-DC的拓扑方案其实有好几种Cuk、Sepic、反激、正激都能做双向但我最终选择的是同步Buck/Boost双向拓扑也就是两个MOS管、一个电感的经典结构。原因很直接这个拓扑元件最少功率密度高控制方式成熟而且Buck和Boost模式天然可以通过两个管的互补驱动切换不用增加额外器件。具体工作原理看图描述两个MOS管Q1和Q2串联在输入和输出之间两个管的连接点接电感。降压模式时Q1做主开关管Q2做同步整流管能量从输入流向输出升压模式时Q2变为主开关管Q1变成同步整流管能量从输出侧流向输入侧。关键是这两个模式的控制本质是一样的都是占空比控制只是要切换PWM的通道映射和控制目标。那为什么不选半桥或全桥呢半桥和全桥的隔离和高功率能力确实更强但对于18V到36V这种低压非隔离场景同步Buck/Boost的效率和成本都更优。还有一个好处是同步Buck/Boost在降压模式天然支持同步整流整流管的导通损耗比二极管小很多200W功率等级下效率能到93%以上。如果用的是异步方案效率基本会掉两个点。1.3 STM32的资源分配与主回路设计主控用的STM32F103C8T672MHz主频资源对这个应用完全够了。我的分配方案是这样的TIM1输出两路互补PWM频率80kHz带硬件死区分别驱动Q1和Q2ADC1用三通道DMA循环采样分别采集输入电压、输出电压和电感电流一个普通GPIO接按键做模式切换一个串口做上位机调试。这里有个细节值得说PWM频率选择80kHz不是随便拍的是综合考虑了电感体积、开关损耗和控制带宽的结果。频率高了电感和输出电容可以小一点但MOS管的开关损耗和驱动损耗会上升频率低了电感体积大电流纹波大控制环路带宽也受限。80kHz在这个功率等级算是比较均衡的选择。主回路的核心器件参数也share一下电感选47uH/15A的铁硅铝磁环电感饱和电流留足余量Q1和Q2都用IRF3205D极耐压55VRds(on)只有8mΩ导通损耗很小驱动用的是IR2104半桥驱动芯片自举供电外围电路简单电流采样用INA240A3高边电流放大器放大倍数100倍实测带宽高、共模抑制比好比直接用采样电阻加运放省心很多。2. C语言工程结构与关键代码设计2.1 代码分层不搞花架子但分层真能救命这个项目的代码量不大但如果你把所有逻辑都堆在中断里后面加功能会想哭。我采用的是经典的三层结构BSP层板级驱动、Control层控制算法、App层应用逻辑。BSP层负责跟硬件打交道的所有事情包括PWM的初始化、ADC的启动、GPIO配置Control层负责PID计算、模式切换判断、保护逻辑App层负责任务调度、串口通信、参数设置。这样分完调试的时候非常舒服你在App层改了业务逻辑完全不用碰底层寄存器操作出问题也好定位。工程目录大概是这样的Project/ ├── BSP/ │ ├── bsp_pwm.c // PWM初始化、死区配置、通道切换 │ ├── bsp_adc.c // ADCDMA采样、均值滤波 │ ├── bsp_gpio.c // 按键、状态灯、保护信号输入 │ └── bsp_uart.c // 调试串口、MODBUS从站 ├── Control/ │ ├── pid.c // 增量式PID、积分限幅、输出限幅 │ ├── dcdc_mode.c // 模式切换状态机 │ └── protection.c // 过压、过流、过温保护 └── App/ ├── main.c // 主循环、任务调度 └── app_cmd.c // 上位机指令解析2.2 关键数据结构PID结构体和模式状态机C语言写控制程序数据结构设计是第一步。我定义了一个PID结构体把比例、积分、微分系数和中间状态量都放在一起每个控制环一个实例typedef struct { float Kp; // 比例系数 float Ki; // 积分系数 float Kd; // 微分系数 float ref; // 目标值 float measured; // 反馈值 float error; // 当前误差 float last_error; // 上次误差 float integral; // 积分累计 float out; // 输出值 float out_max; // 输出限幅 float int_max; // 积分限幅 } PID_TypeDef;这个结构体里的int_max积分限幅是整个设计的重点。如果没有积分限幅在输出饱和的时候积分会一直累积等你把目标值降下来系统要很久才能反应过来表现就是输出过冲甚至震荡。我一般把积分限幅设为输出限幅的一半实测下来效果不错。模式状态机也用一个枚举typedef enum { MODE_OFF 0, // 关闭 MODE_BUCK_CV, // 降压恒压 MODE_BOOST_CC, // 升压恒流 MODE_BOOST_CV, // 升压恒压 MODE_FAULT // 故障保护 } DCDC_Mode_t;为什么把降压和升压模式分开因为控制目标不同降压模式更多是恒压输出升压模式在电池充电场景下需要恒流限压两个方向的PID参数和输出限幅都不一样统一成一个状态会让代码很别扭。2.3 中断和主循环的任务划分200W电源的控制环路要做到稳定电流环的执行频率不能太低。我的划分方式是ADC完成一个周期的DMA传输后触发中断在中断里读取最新采样值做滤波然后执行电流环和电压环的PID计算更新PWM占空比。这个中断频率是80kHz吗不是是20kHz。虽然PWM是80kHz但ADC每四个PWM周期采样一次控制环路的算力压力小很多20kHz的控制频率对200W电源的电流环足够用了。主循环只负责慢速任务串口命令解析、状态灯刷新、按键检测、定时报告运行参数。这样划分的好处是快速控制环路的实时性有保障不会被串口打印这种慢操作阻塞慢速任务的代码逻辑也不用考虑中断嵌套的复杂性好写好维护。3. 核心控制逻辑与关键参数配置3.1 双环控制电流内环和电压外环怎么配合双向DC-DC的控制环设计我是按照“电压外环电流内环”的标准双环结构来的。电压外环的输入是目标电压和实际输出电压的误差输出是电流环的目标值电流内环的输入是电压环给的电流目标和实际电感电流的误差输出是PWM占空比。两个环串起来好处很明显电流内环响应快能抑制输入电压波动和负载突变带来的电流冲击电压外环响应慢一些保证稳态输出电压不偏。在代码里两个环都用了增量式PID算法这一步是实现的核心float PID_Process(PID_TypeDef *pid, float ref, float measured) { pid-ref ref; pid-measured measured; pid-error ref - measured; float p_out pid-Kp * pid-error; pid-integral pid-error; if (pid-integral pid-int_max) pid-integral pid-int_max; if (pid-integral -pid-int_max) pid-integral -pid-int_max; float i_out pid-Ki * pid-integral; float d_out pid-Kd * (pid-error - pid-last_error); pid-last_error pid-error; pid-out p_out i_out d_out; if (pid-out pid-out_max) pid-out pid-out_max; if (pid-out -pid-out_max) pid-out -pid-out_max; return pid-out; }增量式PID有几个天然的好处输出是增量累加的结果不会出现位置式PID那种全量输出突变的问题切换控制模式的时候系统扰动小积分量因为有了int_max限幅抗饱和能力强。这里注意一个细节在稳态的时候微分量会引入噪声所以我的Kd一般很小甚至在电流环里直接设为零只靠PI控制就足够了。3.2 PWM死区配置这一步错了会炸管同步Buck/Boost有一个绕不开的风险两个MOS管如果同时导通输入电源直接短路瞬间大电流把管子和驱动全部烧掉。所以两路PWM之间必须有死区时间也就是在上下管切换之间插入一段两个管子都关闭的时间。这个时间不能太长也不能太短太长会导致输出波形畸变和效率降低太短则可能导致直通。我的选择是根据IRF3205的关断延迟时间和IR2104的传输延迟算出来的一般在150ns到400ns之间。STM32的TIM1支持硬件死区这是最可靠的方式因为硬件生成死区不依赖软件时序比你在软件里手动延时可靠太多。配置代码大概是这样的TIM_TimeBaseInitTypeDef TIM_BaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; TIM_BaseInitStructure.TIM_Period 899; // 80kHz TIM_BaseInitStructure.TIM_Prescaler 0; TIM_BaseInitStructure.TIM_CounterMode TIM_CounterMode_CenterAligned1; TIM_BaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse 450; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Set; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime 20; // 死区约200ns TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Enable;这里TIM_DeadTime等于20在72MHz时基下对应约278ns实测没问题。调试的时候我强烈建议先用示波器看两路PWM的死区波形再上主电这一步能避免一半以上的炸管风险。如果没有示波器至少用一个几十欧的电阻串在电源线上限流等确认波形没问题再换回粗线。3.3 ADC采样和滤波电源控制里最容易被忽视的环节快速控制环路里反馈值的质量直接决定控制质量。如果ADC采样带噪声进去PID控制器会把这些噪声放大表现在输出上就是占空比抖动电感电流纹波增大甚至莫名其妙触发过流保护。硬件上我用差分采样电路和INA240放大器把电感电流转换成0到3.3V的电压信号。软件上的关键是用DMA做多通道循环采样避免CPU逐次查询ADC状态浪费算力也保证采集到的几路信号在时间上尽量同步。采样频率20kHz每个控制周期采集一次。ADC配置的核心代码void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 3; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 3; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }然后再叠加一个软件均值滤波。我用的是滑动窗口均值窗口长度16。为什么不用一阶低通因为一阶低通在20kHz采样下会引入相位滞后对电流环的稳定性有影响滑动均值滤波没有这个问题而且实现起来简单占用的RAM也不大。实测下来滤波后的电流反馈噪声峰峰值从原来的150mA降到了30mA左右控制稳定性提升非常明显。3.4 模式切换降压和升压之间的无缝转换模式切换是整个项目里最容易出问题的地方。如果降压模式切到升压模式时处理不好输出电压或者电流会出现几毫秒甚至几十毫秒的冲击对负载和变换器本身都不好。我的方案是引入一个带滞回的状态判断机制而不是简单比较电压大小就往另一个模式跳。DCDC_Mode_t DCDC_ModeProcess(float u_in, float u_out, float i_out) { static DCDC_Mode_t current_mode MODE_OFF; switch (current_mode) { case MODE_OFF: if (u_in 12.0f u_out 10.0f) { current_mode MODE_BUCK_CV; } break; case MODE_BUCK_CV: if (u_in (u_out 2.0f)) { // 输入无法维持输出切换升压 current_mode MODE_BOOST_CV; } break; case MODE_BOOST_CV: if (u_in (u_out 5.0f)) { // 输入有足够余量切回降压 current_mode MODE_BUCK_CV; } break; case MODE_FAULT: break; default: current_mode MODE_OFF; break; } return current_mode; }这里用到了滞回区间降压切升压的阈值是u_out 2.0V升压切降压的阈值是u_out 5.0V中间的3V范围是滞回区。如果不加滞回当输入电压在切换点附近波动时模式会在Buck和Boost之间快速来回切换输出会很不稳定还可能因为频繁切换导致MOS管应力增大。模式切换时PID也需要做处理。我用的方法是切换前把PID的积分清零输出保持当前值然后让新的控制环从当前占空比开始缓慢调整。这样能最大限度避免切换瞬间的占空比跳变。可以理解为换手开车先握稳方向盘再交接不能让车在交接瞬间失控。4. 调试过程、参数整定与问题排查4.1 第一步永远是开环调试别急着闭环调试开关电源我的顺序是铁律先开环再闭环先低压再高压先轻载再满载。每次遇到同事直接上闭环然后炸管的基本都是跳过了开环验证。开环调试怎么做在代码里临时固定一个占空比比如50%然后用直流电源在输入端加一个低压比如10V用电子负载在输出端加一个小负载观察输出电压和电流波形是否符合预期。这个阶段重点看三样东西PWM波形是否正确、死区是否存在且不直通、电感电流是否连续且没有异常饱和。确认这些都没问题再把输入电压慢慢升高到额定值反复加卸载观察波形。我在这个阶段吃过的亏是一开始把占空比给到70%输出空载结果输出电容充电电压冲到接近理论值但因为负载太小电容上的纹波电流导致电容啸叫听起来像主板漏电一样滋滋响。后来才知道这是空载临界连续模式下的正常现象加一个2W的假负载电阻就能解决但当时排查了很久。4.2 PID参数整定的实用套路PID调参是最让人头大的环节尤其对于刚接触数字电源控制的人。我分享一套自己用着很顺的方法不用看太多理论书也能调出能用的参数。第一步把所有积分项和微分项设为零只保留比例系数。不断加大Kp同时观察输出波形直到系统出现等幅振荡记下这个临界Kp值和振荡周期Tu。第二步根据齐格勒-尼科尔斯经验公式算出初始P、I、D参数公式是Kp 0.6 * KruKi 2 * Kp / TuKd Kp * Tu / 8。第三步在这个基础上手工微调。微调的直觉是这样的如果输出超调大、调节时间长就优先减小Ki因为积分会让超调变大如果输出纹波大、噪声明显就减小Kd甚至直接给零如果负载突变后恢复慢就适当增加Kp。这里分享一组我在200W/24V项目里实际用过的参数电压环Kp 0.12Ki 0.05Kd 0电流环Kp 0.6Ki 0.2Kd 0。当前电流环带宽约2kHz电压环带宽约200Hz现场实测负载突变时电压跌落控制在100mV以内恢复时间小于2ms。有个容易踩的坑是数字量和物理量之间没有做单位换算。ADC采样回来的是原始码值经过换算后才是电流值如果你直接拿原始码值送给PID算出来的占空比肯定不对。我的做法是在BSP层就完成所有换算应用层只处理物理量这样CI代码可读性好也不容易出现比例差几千倍的离谱故障。4.3 常见问题与排查技巧下面整理了一下我在调试这个项目过程中遇到的高频问题每一条都是真金白银换来的。现象可能原因排查方法上电瞬间MOS管烧毁死区时间不够直通短路示波器测PWM死区波形加大死区到300ns输出有周期性低频纹波PID参数不合适环路振荡减小Kp或Ki用示波器看占空比波形负载突加时输出电压跌落过大电流环带宽不足或电感饱和提高控制频率检查电感饱和余量轻载时输出不稳定电感进入断续模式环路增益下降增加假负载或者改用CCM/DCM混合控制ADC采样噪声大占空比抖动采样电路共模干扰或滤波不足增加均值滤波窗口检查地线布局模式切换瞬间电流突变模式切换未处理好PID积分状态模式切换前清零积分保持输出占空比输入电压升高时输出突然失控输出反馈分压电阻阻值变化或ADC参考漂移检查反正馈电阻温度系数改用精密基准源这里重点说两个自己踩得比较深的坑。第一个是ADC参考电压。STM32F103的内部参考电压精度一般温漂也比较大我用它做电源反馈采样时发现输出电压从24V稳定到23.6V看起来像环路没调好各种调参都试了最后查出来是ADC参考电压漂了。后来改成外部基准芯片REF3030问题一下解决了。在电源控制这种需要稳定反馈精度的应用里外部基准是必须的不要省这个钱。第二个是电感饱和的问题。第一次打样布板时电感离MOS管的开关节点太近电磁干扰串进电流采样回路导致控制环路在小负载时还能工作大负载时电流环直接震荡。后来把电感挪远了采样线改成差分走线问题解决。这个问题排查起来非常耗时因为从软件看一切正常参数也没问题实际是硬件布局引入的干扰。4.4 保护逻辑平时用不上出事能救命保护逻辑是这个项目里必须做扎实的部分虽然没有保护功能的产品也能跑但一个失误就可能烧板子。我的保护设计包括过压保护、过流保护和过温保护三种全部在控制中断里实现响应时间在微秒级别。过压保护最简单输出电压超过设定值上限立即把PWM输出禁能同时打开一个泄放回路把输出电容上的电放掉。注意这里不能用软件延时必须直接在中断里操作否则电压可能在几个毫秒内冲到电容耐压。过流保护用比较器硬件做过流信号输入到STM32的BKIN引脚触发定时器的刹车功能硬件自动封波比软件判断快很多。这个设计非常关键因为在极端过流情况下软件中断要排队可能来不及执行。过温保护用NTC热敏电阻贴在电感和MOS管附近主循环每秒读一次温度超过85度就降低输出功率超过95度直接关机。温度保护不需要微秒级响应放在主循环里完全够用但要注意NTC得用胶水贴牢否则热耦合不好温度读数偏晚管子都过热了软件还不知道。5. 项目落地后的几点感悟做这个项目最大的感受就是双向DC-DC控制的难度不在算法本身而在细节。PID算法可能十行代码就能写完但要让它在不同的负载、不同的温度、不同的输入电压下都稳如磐石需要你对硬件特性、软件实时性、采样噪声这些细节都心里有数。最后还剩下一个值得优化的方向这个设计目前用的是固定开关频率的PWM控制如果能改造成临界导通模式控制轻载效率还能再提升几个点。而且如果后续功率需要提升到500W以上建议换用STM32G474这类带高分辨率定时器的MCUPWM分辨率更高对环路性能会有明显帮助。手上这套代码我在GitHub上整理过一版脱敏的感兴趣的话可以拿来当起点结合自己的硬件把PID参数重新过一遍比自己从零开始写会快很多。本文还有配套的精品资源点击获取
返回列表