
简介这是一套基于STM32的多功能数控电源完整工程资料面向嵌入式电源设计开发者与STM32进阶学习者。项目以STM32微控制器为核心结合数字信号处理实现可编程电压/电流调节覆盖Buck降压拓扑、PID控制、PWM调制等关键技术适合用于学习数字电源设计、原理图阅读和固件二次开发。压缩包共144个文件约1.39MB以C源码59个c文件、77个h文件为主辅以1份设计原理图PDF、Keil工程配置uvprojx/uvoptx及硬件调试辅助文件源码与工程结构清晰便于直接导入开发环境分析。已有3431人浏览学习。资料包含完整工程源码与原理图可深入理解STM32如何驱动电源转换器以及ADC采样、定时器PWM输出、闭环调节等具体实现对理解HAL库驱动和实际项目开发流程有直接帮助。1. 先把需求说清楚数控电源到底“数控”在哪手头缺一台能设定电压、能限流、还能实时回读输出的电源是很多人开始做“基于STM32的多功能数控电源设计”的直接动力。数控电源和普通线性电源的区别不在于“显示带数字”而在于给定值来自数字控制、闭环也在数字域完成你要的电压是主控算出来的PWM或DAC输出实际输出电压又被ADC实时采回来做闭环修正。这带来的直接好处是能稳压、能限流、能存储多组预设甚至能通过串口或蓝牙远程调节。本文围绕STM32做控制核心从拓扑选型、闭环实现、代码结构、校准与踩坑四条线往下拆适合准备做毕业设计、实验室自用电源或就是想把手头单片机玩出实物的那批人。方案不追求极致精度但求能复现、能稳定跑、遇到问题知道看哪里。2. 系统方案设计拓扑选型、控制环结构与关键器件参数2.1 主拓扑为什么降压方案我选了BUCK而不是线性稳压多功能数控电源的第一件事是决定功率级怎么搭。常见做法有两种线性稳压和开关降压。线性稳压靠调整管吃掉压差输出电压纹波低、电路简单但压差一大效率就崩输入24V输出5V时多余的19V全部变成热量实验室用久了散热器烫得没法碰。BUCK开关电源则通过MOS管高频斩波、电感储能来降压压差大时效率反而高热量可控代价是纹波和电磁干扰需要额外处理。对STM32而言控制BUCK比控制线性电源多一个关键变量开关频率和死区。同步BUCK需要互补PWM异步BUCK只需要一路PWM加一个续流二极管。新手入门我一般建议先做异步BUCK原因有三个第一不需要处理上下管死区逻辑简单第二驱动电路用半桥驱动芯片或者直接低端MOS搭配自举都行翻车概率低第三环路增益特性相对好调。后续想升级再做同步整流那是效率优化的事不是功能必需的事。电感值的选择直接影响纹波和动态响应。手算也好用TI WEBENCH这类在线工具也行常见经验是让电感纹波电流占满载电流的20%到40%。例如输出5A、开关频率100kHz时按公式 L (Vin - Vout) * Vout / (Vin * f * ΔI) 算出来一般在几十微亨量级。电感选大了纹波小但动态慢选小了动态好但纹波高这个取舍后面调PID时还会再遇到。2.2 控制环电压环、电流环怎么分层给定通道和回读通道如何配合数控电源的控制环和普通BUCK电源不太一样。普通BUCK只要稳住输出电压就行数控电源要求“恒压”和“恒流”两种模式下都能稳住而且能在两者之间平滑切换。常见做法是双环并联结构电压环负责稳压电流环负责限流哪个环的误差大就由哪个环主导输出。硬件上可以做成两个运放做环路的竞争软件上也可以用STM32直接判断当前工作在哪种模式再切换PID参数。回读通道是另一个容易忽略的点。你输出多少伏、多少安MCU必须实时知道才能做闭环和显示。电压回读用电阻分压电流回读用采样电阻加运放放大这两个信号都要进ADC。采样电阻的压降会影响实际负载电压所以电压采样点要接在采样电阻之后、输出端子之前也就是“远端采样”。这个细节很多人第一次做的时候没注意结果发现大电流时输出电压显示值比万用表低不少其实就是采样点接错了位置。给定通道方面有两种主流做法。一种是用STM32内置DAC直接输出给定电压成本低但分辨率有限另一种是用PWM加RC滤波当“软件DAC”分辨率可以做到很高但需要处理好纹波和建立时间。我倾向于用PWM加二阶RC滤波做给定因为STM32的定时器分辨率足够成本为零而且滤波后的直流电平非常干净。关键在于RC截止频率要远低于PWM频率至少低两个数量级否则给定上就带着周期性纹波后面输出纹波无论如何都压不下去。2.3 关键器件选型采样电阻、运放、DAC与PWM的配合模块常用器件方案注意点功率开关IRF540NS / IRF3205 N-MOSVgs阈值、Qg大小、散热条件续流二极管SS54 肖特基 / MBR1045反向恢复和压降采样电阻毫欧级锰铜或合金电阻功率要留裕量位置要远离热源电流放大运放LM358 / OP07 / INA240共模电压范围够不够、带宽PWM滤波2阶RC 运放跟随截止频率低于PWM频率2个数量级主控STM32F103 / STM32F407定时器通道数、ADC采样率采样电阻的功率不能只看标称电流。比如0.02Ω、5A满载时损耗是 I²R 0.5W选1W的采样电阻看起来够但温升会导致阻值漂移电流回读也跟着漂。我一般选功率裕量3倍以上并用开尔文四线接法把采样线和功率线分开走线在PCB上也尽量对称。运放方面如果采样电阻在低端接地侧LM358这类单电源运放也能工作但要注意输入失调电压对回读精度的影响如果采样电阻放高端就必须选共模电压范围覆盖电源轨的运放INA240这类专用电流检测运放最省心。2.4 整机结构与前面板的人机交互方案多功能电源的“多功能”很大程度体现在人机交互上。常见方案是编码器加OLED屏编码器负责调节电压和电流设定OLED显示当前回读值、设定值、工作模式。STM32用定时器编码器接口读取旋转方向用按键切换电压/电流调节对象长按可以进入菜单存储预设。这个交互逻辑不算复杂但非常影响使用体验。调节步进建议做成动态的旋转快时步进大旋转慢时步进小这样既能快速拉到12V也能精确调到3.30V。设定值和回读值要分开显示否则闭环工作后你根本不知道MCU认为的“目标值”在哪个档位。3. 基于STM32的控制代码从ADC回读到PID再到PWM输出3.1 工程基础定时器PWM输出与ADC多通道采样配置控制逻辑落在代码上最底层的两件事是产生PWM和读ADC。用STM32标准外设库或HAL库都可以关键是搞清楚各个定时器和ADC的时钟树关系。PWM频率我一般设置在100kHz到200kHz之间。频率高一些电感和输出电容可以选小一点但MOS管开关损耗增加驱动电路也要跟上频率低一些功耗低但纹波和可听噪声可能比较烦。ADC方面多个通道需要轮询或使用DMA。常见坑是ADC切换通道后第一次采到的值还是上一个通道的值因为通道切换需要稳定时间。最稳妥做法是用DMA循环扫描所有通道并且每轮采样的第一组数据丢弃。初始化代码里特别要注意设置采样时间为最大档位对于STM32F103来说就是采样周期设为239.5个周期这样输入等效阻抗高的时候读数才稳。下面是一段HAL库的ADC多通道DMA初始化参考。// 初始化ADC1, 通道0(电压回读), 通道1(电流回读), 通道2(给定监测) static void MX_ADC1_Init(void) { ADC_ChannelConfType_t sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode ENABLE; // 多通道扫描模式 hadc1.Init.ContinuousConvMode ENABLE; // 连续转换靠DMA取数 hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件启动不用定时器触发 hadc1.Init.NbrOfConversion 3; // 采样3个通道 HAL_ADC_Init(hadc1); sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 采样时间拉满降低内阻影响 sConfig.Channel ADC_CHANNEL_0; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_1; HAL_ADC_ConfigChannel(hadc1, sConfig); sConfig.Channel ADC_CHANNEL_2; HAL_ADC_ConfigChannel(hadc1, sConfig); // DMA缓冲长度必须和NbrOfConversion一致否则数据错位 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, 3); }初始化里有三处值得说明。第一ScanConvMode和ContinuousConvMode必须同时开启DMA才能真正循环搬运数据否则每轮转换结束就停下来要重新触发。第二采样时间拉满不是万能的如果信号源内阻极大采样保持电容充不满读数会偏低这时只能加一级运放跟随器做阻抗变换而不是继续拉采样时间。第三DMA缓冲数组要用uint32_t类型每个元素对应一个ADC通道顺序和配置顺序一致不要指望它和物理通道编号一致。3.2 PID调节器实现位置式PID和增量式PID怎么选电源的环路控制PID是主力。位置式PID直接计算输出占空比绝对值增量式PID只计算占空比的增减量。对于BUCK电源我一般用增量式PID原因是它天然带积分记忆不会因为设定值突变导致输出大幅跳变SoftStart也更好做。增量式的另一好处是限幅逻辑容易直接把每次的增量限幅输出变化速度就被控住了。下面这段是增量式PID的实现积分分离也一并加上了。积分分离的作用是当误差很大时停止积分累加防止积分饱和导致超调误差回到阈值内再恢复积分。电源控制里这个技巧非常实用。typedef struct { float kp, ki, kd; float target; // 目标电压或目标电流 float err_last; // 上次误差 float err_prev; // 上上次误差 float out_increment; // PID增量输出 float out_max; // PID输出最大值 float out_min; float integral_threshold; // 积分分离阈值 } PidHandle; float Pid_Calc(PidHandle *pid, float feedback) { float err pid-target - feedback; float p_term pid-kp * (err - pid-err_last); // 只有误差小于阈值时才允许积分累积 float i_term 0.0f; if (fabsf(err) pid-integral_threshold) { i_term pid-ki * err; } float d_term pid-kd * (pid-err_last - pid-err_prev); float inc p_term i_term d_term; // 增量限幅防突变 if (inc pid-out_max) inc pid-out_max; if (inc pid-out_min) inc pid-out_min; // 更新误差历史 pid-err_prev pid-err_last; pid-err_last err; pid-out_increment inc; return inc; }这段代码是在电压环和电流环共用的区别在于传入的target是电压还是电流、反馈是电压回读值还是电流回读值。一个常见错误是在电压环和电流环之间切换时PID内部的状态没有清零导致切换瞬间出现巨大的错误增量。解决方法是定义Pid_Reset函数在模式切换时调用把err_last、err_prev和累计量全部归零。3.3 主循环和软启动用给定斜坡替代硬上电主循环的节奏控制很关键。PID计算和ADC读取不一定要放在同一个频率但必须保证每一步的时序是可预测的。我一般用定时器产生1kHz的中断作为控制节拍在中断里读ADC、跑PID、更新PWM主循环只负责处理按键、屏幕刷新和通信。这样控制频率稳定显示逻辑再卡也不会影响电源输出。软启动这块单独拿出来说。直接上电就输出设定电压大概率会在电容充电瞬间触发过流保护或者让输出电压过冲。常见做法是设置一个软启动斜坡从上电开始目标电压在100ms内从0线性爬到设定值。这个斜坡可以放在软件里做也可以交给PID的target值递增来实现。我倾向于直接对target做斜坡因为这样不需要修改PID结构。// 1kHz控制中断内执行的软启动闭环逻辑 void Control_Task_1kHz(void) { // 读取ADC最新一轮结果已有DMA持续更新 float vfb adc_buf[0] * V_ADC_SCALE; // 电压分压系数换算 float ifb adc_buf[1] * I_ADC_SCALE; // 电流采样放大系数换算 // 软启动斜坡目标从0逐渐增加到设定值 if (soft_start_active) { float ramp_step target_set / 0.1f; // 100ms ramp target_ramp ramp_step; if (target_ramp target_set) { target_ramp target_set; soft_start_active 0; // 斜坡结束 } } else { target_ramp target_set; } // 从电压环给定目标算增量 pid_v.target target_ramp; float inc Pid_Calc(pid_v, vfb); // 累加到占空比上并限幅 duty inc; if (duty DUTY_MAX) duty DUTY_MAX; if (duty DUTY_MIN) duty DUTY_MIN; __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, duty); }参数换算系数V_ADC_SCALE和I_ADC_SCALE不是随意写的。它们必须由实际分压电阻和采样放大倍数标定出来最好在整机装配后用万用表实测几个电压点反推线性系数。后面第5章会详细说校准流程。另外DUTY_MAX不能设为100%BUCK在极高占空比下电感容易饱和续流二极管也承受不住我一般留5%到10%的余量。3.4 过压、过流保护的代码路径软件的过压和过流保护是最后一道防线不能指望它替代硬件保护但可以在几百微秒内做出反应。在1kHz控制中断里做这个检查就够了一旦电压超过过压阈值立即把PWM输出强制拉低并进入锁存故障状态。故障状态的清除只能靠按键或者上位机指令不能在故障消除后自动恢复否则反复打嗝会让负载死得更难看。// 保护逻辑放在控制中断最前面 void Protection_Check(void) { // 过压保护电压回读超过设定值1.2倍0.5V if (vfb target_set * 1.2f 0.5f) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); // 立即关断 fault_flag FAULT_OVP; } // 过流保护电流回读超过限制的1.1倍 if (ifb current_limit * 1.1f) { __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 0); fault_flag FAULT_OCP; } }过压阈值加的是绝对值偏移而不是纯比例是为了避开输出电压在特定档位下允许的合理误差范围。例如设定5V时超过6.5V才判定过压设定30V时超过36.5V才判定。如果你用纯比例小电压档位稍微波动就会误触发。电流保护的阈值设定同样要留有正常容差否则负载在正常范围内跳变时也会触发保护用起来非常恼火。4. 踩坑与排查ADC、PID、上电冲击和纹波4.1 ADC切换通道后首采值不准是通道稳定时间问题现象DMA循环采样采集到的电流值在通道切换后总有一个明显的跳变而且这个跳变方向不固定。原理是ADC内部采样保持电容在上一次采样中存的是旧通道电压切到新通道后需要几个采样周期才能完全放电或充电到新值。解决方法是把每轮采样的第一组数据丢弃或者让每个通道连续采样两次只取第二次。还有一个辅助手段是检查ADC时钟APB2总线上ADC时钟不要超过14MHzSTM32F103超过了转换结果抖动会明显变大。这个坑不深但会让PID反馈值带毛刺表现出来就是输出电压在小范围内反复抖很容易误判成PID没调好。4.2 PMW输出纹波大RC滤波参数和PID输出打架现象给定通道用PWM加RC滤波输出电压能稳但示波器上看总是叠加周期性的三角波。原因很可能是RC滤波截止频率偏高或者PWM频率本身偏低。修改原则是RC截止频率至少要低于PWM频率的100倍以上。例如PWM频率是50kHzRC截止频率就不能超过500Hz如果用的是两阶RC建议一阶设在200Hz二阶设在100Hz左右。另一点是给定通道的运放跟随器必须用低失调电压型号CMOS输入运放比双极型运放更适合这里的直流放大。这类问题说玄学也玄学但把示波器探头换成地线弹簧再测很多时候纹波是探头本身带来的。4.3 上电瞬间电压冲高闭环响应顶不住现象开机时输出电压瞬间冲到设定值的1.5倍以上再慢慢回落。这几乎是新手做数控电源最常见的翻车现场。原因是主控上电初始化过程中PWM占空比默认值可能是寄存器复位值或者软启动斜坡还没跑完ADC已经读到0电压PID疯狂输出正增量等斜坡爬上来时占空比已经被叠加到很高。解决分三段第一定时器PWM初始化时必须先设置比较值为0再启动输出不能让PWM在初始化期间带未知占空比运行。第二软启动斜坡必须在进入主循环之前就把target_ramp清0如果是外部基准芯片做软启动要等基准稳定后再使能功率级。第三PID输出上限DUTY_MAX在启动阶段可以临时限得更严等输出电压爬到接近目标值再放开额度比如启动阶段上限20%稳定后放开到90%。4.4 电流采样受开关噪声干扰大布局和滤波的顺序现象电流回读值和万用表实测值对得上但示波器看ADC引脚信号有一堆毛刺。根因在采样电阻两端的dv/dt噪声BUCK的电感节点高频开关时会通过寄生电容耦合到采样电阻上。解决分两面硬件上采样线要绞合走线远离电感节点软件上ADC采样时间点避开MOS管开通和关断瞬间用一个定时器触发ADC启动让采样窗口落在电感续流阶段干扰会小很多。如果确实改不了硬件可以在ADC采集后加软件均值滤波但要注意均值滤波会把控制环的相位裕度吃掉滤波窗口太长稳定时间会拖慢。我的经验是控制环用的采样值先做3次滑动平均显示用的采样值再做15次平均两个用途分开处理。4.5 STM32的Delay卡死在循环里通常是中断优先级出了岔子现象初始化或者主循环中调用HAL_Delay时程序卡死。原因一般是SysTick中断优先级设置低于某个持续触发的外设中断或者某个外设中断没有清标志导致CPU一直在响应中断。排查优先级时控制中断1kHz和保护逻辑应该保持最高抢占优先级UART和ADC的DMA完成中断次之HAL_Delay依赖的SysTick优先级要放在任何一个可能长时间占用CPU的优先级之上。另一个隐蔽现象是程序卡死前最后停留在某个外设中断服务函数内部此时用调试器暂停看PC指针最直接。这个坑和电源本身关系不大但几乎所有工程都会碰到。5. 验证与进阶校准流程、恒流自动切换与上位机调参5.1 校准流程先标定回读再标定给定最后才调闭环数控电源装好之后第一步不是写PID参数而是校准回读通路。用一台成色好的万用表分别设定输出电压为1V、5V、12V、20V、30V几个点记录示波器或万用表读数与STM32回读值的对应关系。电压回读是分压网络理论上是线性关系直接用两点斜率法算出V_ADC_SCALE即可。电流回读因为有运放的失调电压线性往往不太理想至少测三个点做最小二乘拟合并把零点电流对应的ADC值单独记录在代码里强制清零。校准完成后再标定给定通道让PID不接入直接固定PWM占空比测量PWM滤波后实际电压确保软件给定电压和实际开路电压一致。这个步骤别省略否则你后面调PID时会分不清误差是来自控制环还是来自给定通道本身。5.2 恒流/恒压自动切换的软件实现恒压和恒流的切换常见做法是计算两个环各自的PID输出增量哪个增量先饱和就认为工作在哪一区。另一种更工程化的做法是把电流环当成一个限制器电压环正常调节当电流回读值接近设定限流值时强制把电压环的target往下拉。这样不需要在两种PID之间切换也不会出现切换瞬间抖动。下面这段思路可以参考。// 电压环目标根据电流限制自动降落 float current_error current_limit - ifb; if (current_error 0.2f) { // 接近限流点 // 把电压目标按比例拉低电流误差越小拉得越低 target_limited target_set - (0.2f - current_error) * 10.0f; if (target_limited 0) target_limited 0; } else { target_limited target_set; }这里系数10.0f是电压降落增益实际调整要按你的电流采样量纲来定。测试方法是用电子负载从开路状态缓慢拉电流观察输出电压是否能平滑走进限流区而不是突然掉到0。5.3 串口调试PID参数别靠拍脑袋PID参数如果不通过串口可视化调起来就是黑匣子。我一般把控制状态直接打包成JSON或有固定帧头的二进制帧输出到串口内容包含目标电压、电压回读、电流回读、当前占空比、PID增量、工作模式、故障标志。波特率115200足够用每秒输出100帧对UART来说压力不大。示波器抓电压波形串口看内部变量两者对照才能准确判断超调量和稳定时间。调试PID时先把积分项清零只调比例项找到系统发生振荡的临界增益然后取三分之一到二分之一作为起始比例增益再一点点加积分项观察是否有低频振荡。这个流程比直接套网上的参数靠谱得多因为你电感的感量、输出电容的ESR都不是网上那份参数对应的硬件。5.4 进阶用定时器输入捕获做动态负载监测如果你的电源要带一些动态负载比如电机或者LED灯板可以在输出端加一个电流采样并用STM32的定时器输入捕获测量负载电流的脉冲宽度和频率。定时器捕获测频率和脉宽是STM32的经典功能配置一个输入捕获通道上升沿启动计时、下降沿捕获脉宽就能实时算出负载变化的频率。把这个数据纳入上位机显示可以帮助你判断是负载本身在跳变还是电源调节还没跟上。这也是“多功能”的一条值得加分的路。我做这个方案最深的教训是先硬件后软件先校准后闭环。跳过任何一步后面找问题都要多花三五倍的时间。数控电源这种东西看似功能简单真正做好细节还是需要反复打磨的。希望这篇笔记能帮你少走一段弯路照着上面的顺序走一遍能把一套可用的方案完整跑起来。本文还有配套的精品资源点击获取