
简介一套基于STM32微控制器与Keil开发环境的DC-DC高效Boost升压电路工程覆盖原理理解、代码编写到调试烧录的完整链路适合嵌入式电源方向开发者、电子竞赛参赛者以及希望掌握升压拓扑与闭环控制技术的工程师。工程重点实现了输入输出电压的同步采样结合PID调节与PWM驱动使升压输出稳定可调实时监测输入输出状态以实现高精度控制适用于电池供电设备、太阳能系统等需要电压提升的应用场景。压缩包共包含239个文件整体大小8.28MB内有43个头文件与41个C语言源程序以及Keil工程配置、编译生成的hex与axf烧录文件还包含LCD显示、数据通信、定时器、ADC、PID、PWM等模块化代码结构清晰便于直接运行或二次移植。已有904人学习下载。通过该工程可系统掌握STM32外设初始化、ADC多通道采样、PID算法实现、PWM占空比控制及Boost电路调试方法也能参考其模块划分与代码风格缩短相关项目开发周期是一份兼具实用性与学习价值的完整参考范例。1. 把Boost升压电路的采样和控制同时放进STM32Boost电路不难难的是在STM32上用C语言写出一个闭环控制的“同时采样”方案。绝大多数人做DC-DC时采样输出电压看纹波采样输入电流做保护两个通道串行读一遍就结束了。但当你把负载动态调快、把效率往95%以上推时会发现串行采样导致电压环和电流环看到的数据不是同一时刻的系统状态控制量永远滞后半拍。这篇文解决的就是这个事用STM32的ADC硬件同步模式把Boost电路的输入电流和输出电压绑在同一个时间点采样然后用C语言写控制闭环在Keil里把整个工程跑起来。适合刚把Boost板子焊好、正在为软件发愁的嵌入式工程师也适合想在数字电源这条线往下走的人。2. 先把升压电感和采样电阻算清楚再打开Keil建工程2.1 临界电感计算公式和连续导通模式的判断写采样代码之前得先确定硬件工作在什么状态。STM32控制的Boost如果电感选小了负载从轻载往重载切换时会从断续模式DCM跳进连续模式CCM电感电流从零变成有底座的斜波采样电阻上的波形形态完全不同你写的C语言ADC换算公式就得做两套分支。与其这样不如直接把电感做成无论在什么负载下都工作在CCM。按临界电感公式来算临界电感Lc Vin_min² × (Vout - Vin_min) / (f_sw × I_load_max × Vout)假设输入12V输出24V开关频率100kHz最大负载电流2A临界电感是Lc 12² × (24 - 12) / (100000 × 2 × 24) 144 × 12 / 4800000 0.36 mH这个值偏大因为公式是按满载连续算的。我一般会按输出满载电流的20%作为纹波电流目标来选这样电感可以小一些。纹波电流设计值ΔI取0.4A反推电感L Vin × (Vout - Vin) / (f_sw × ΔI × Vout) 12 × 12 / (100000 × 0.4 × 24) 1.5 mH这里取1.5mH还是1mH起步取决于你板上有没有空间。1.5mH的好处是纹波电流小采样电阻上的信噪比好ADC读出来的数值抖动也小。缺点是动态响应慢负载突变时需要十几个周期才能把输出电压拉回目标值。如果你的控制目标是手机快充那种快充快放的场景可以把电感降到1mH把PID调节器调激进一点。2.2 采样电阻阻值选择ADC精度、功耗和运放增益怎么平衡采样电阻直接串在输入回路上电阻越小功耗越低但采样电压也越小STM32的ADC在3.3V参考电压下1个LSB对应约3.2mV12位如果你采样电阻两端的电压只有几毫伏读出来全是量化噪声。这个矛盾有两条路走换一个16位的ADC或者在硬件上装一个差分运放把信号放大后给STM32。正经做法是后者。先定采样电阻按功耗不超过1%的输出功率来算。假如输入电流最大5A采样电阻功耗控制在0.25W以内阻值就是0.25/2510mΩ。10毫欧在5A时产生50mV压降这个量级用仪表放大器放大10倍后变成500mVADC采出来就是约155个LSB每安培精度够用了。表里给几个常用参考值最大输入电流采样电阻满量程压降ADC前增益ADC输入电压2A25mΩ50mV20倍1.0V5A10mΩ50mV20倍1.0V10A5mΩ50mV20倍1.0V输出电压分压电阻的思路也一样要让满载时的输出电压经过分压后落在2V左右留出15%的裕量防止过冲把ADC打满。这里还有一个很多人忽略的细节Boost电路在MOS管导通和关断两个阶段采样电阻上的电压波形是完全不一样的单片机必须避开开关切换瞬间去采样这个在后面ADC触发器配置那里解决。3. 双ADC同步采样一个触发时刻两个通道同时保持3.1 为什么不能用DMA快速多次采样然后取平均很多人在STM32上做电流采样就是开一个ADCDMA把所有通道轮流扫一遍连续取平均值。这个办法在电机控制里能用因为电机的机电时间常数比电周期长得多。但Boost电路不一样功率管的开关频率是100kHz量级也就是开关周期只有10微秒ADC虽然能跑几MHz采样率但两个通道如果分时采样在时间上是一个在MOS开通过程中采、一个在MOS关断过程中采采到的电流根本不是同一状态下的电流。“同时采样”在这里的工程含义是两个ADC通道的采样保持开关要在同一个时钟沿闭合。STM32F103之类的中低端MCU虽然有两个ADC但官方资料里用同步规则模式可以做到ADC1和ADC2同时采集各自的通道。规则模式的问题在于它只能由同一个触发源启动转换是同步的但按官方设计常规模式下同一时刻能最多转换规则序列中的一个通道同步注入模式才是真正的多通道同时转换。所以正确配置是使用ADC的注入组同步模式用定时器更新事件作为触发源。3.2 用定时器触发ADC注入组把采样点钉在PWM周期的中间PWM周期中间是Boost电感电流的正中心电流波形在这个点最平稳不受开关切换毛刺影响。用STM32的定时器1输出PWM控制开关管同时把定时器更新事件或某个比较通道事件配置为ADC触发源这两件事在同一个定时器上完成。代码里先把ADC配置为双ADC同步注入模式让 ADC1 采电压、ADC2 采电流或者反之再用内部连接把TIM1的TRGO连到ADC的注入组触发输入上。代码片段如下使用STM32标准外设库// 双ADC同步注入模式配置截至——仅做核心部分扩展 void ADC_SyncInjected_Init(void) { ADC_InitTypeDef ADC_InitStructure; ADC_InjectedConfTypeDef ADC_InjectedStructure; // 先初始化ADC1和ADC2时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_ADC2, ENABLE); // 双ADC模式同步注入模式 ADC_InitStructure.ADC_Mode ADC_Mode_InjecSimult; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 注入组一次只转一个通道 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 必须由外部触发启动 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T1_CC1; // TIM1比较通道1触发 ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); // 配置注入组通道ADC1采输出电压ADC2采采样电阻电压 ADC_InjectedChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_71Cycles5); ADC_InjectedChannelConfig(ADC2, ADC_Channel_2, 1, ADC_SampleTime_71Cycles5); // 注入组转换结果保存在ADC1_InjectedDR和ADC2_InjectedDR中 ADC_InjectInjectedConvModeCmd(ADC1, ENABLE); ADC_InjectedSequencerLengthConfig(ADC2, 1); }定时器输出PWM这一端也要同步配置重点是把TIM1的CC1事件既用来产生PWM波形又用来给ADC做触发源。实际做的时候还要注意触发时刻要避开PWM的高频开关边沿。MCU的PWM是中心对齐模式时计数器的计数值到达峰值那一刻正好是PWM周期中点把比较通道配置成在峰值点产生触发事件采样点的信噪比最好。示波器上验证时你会看到ADC注入组的转换标志总是出现在电感电流纹波的波峰或波谷位置——这个标志位可以用来做软件时序调试。4. C语言控制代码ADC中断里读数据、算功率、跑PID4.1 ADC注入组转换完成中断里读出两个通道数据ADC转换完成后会进入注入转换完成中断在这个中断里读取两个ADC的数据寄存器。值得注意的一个点是ADC1和ADC2的注入组转换结果分别存在各自的数据寄存器里不要像规则组那样依赖DMA搬运直接读寄存器最快中断里读完就退出整个中断服务函数耗时控制在2微秒以内。volatile uint16_t adc_vout_raw; // 输出电压原始值 volatile uint16_t adc_iin_raw; // 输入电流原始值 void ADC_IRQHandler(void) { if (ADC_GetITStatus(ADC1, ADC_IT_JEOC) ! RESET) { // 先读ADC2再读ADC1保证两组数据对应同一时刻 adc_iin_raw ADC_GetInjectedConversionValue(ADC2, ADC_InjectedChannel_1); adc_vout_raw ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1); ADC_ClearITPendingBit(ADC1, ADC_IT_JEOC); // 在这里调用闭环控制函数 Boost_ControlLoop(); } }注意先读ADC2再读ADC1原因在于两个寄存器的值在转换完成时刻已经锁存读取顺序不会影响时间一致性但Keil编译器优化后读顺序可能影响缓存一致性。这样写能保证取到的电压和电流是同一个PWM周期内的瞬时值。4.2 从ADC原始值换算到真实的电压和电流分压电阻网络和运放放大倍数要预先算好换算系数最好在代码里用宏定义写清楚后面写上位机标定脚本时直接改宏。输出电压我用的分压比是1/11也就是满量程33V除以3V的ADC参考电压。采样电阻上的电压经过20倍放大反算电流的公式里要先除以放大倍数再除以电阻值。最终结果用定点数表示避免浮点运算拖慢中断速度。#define VOUT_DIVIDER 11.0f // 输出电压分压比 #define ADC_REF_VOLT 3.3f // ADC参考电压 #define ADC_FULL_SCALE 4095.0f // 12位ADC满量程 #define CURRENT_GAIN 20.0f // 电流运放增益 #define SENSE_RESISTOR 0.01f // 采样电阻阻值10mR #define VOLT_CONVERT(adc) ((adc) * ADC_REF_VOLT / ADC_FULL_SCALE * VOUT_DIVIDER) #define CURR_CONVERT(adc) ((adc) * ADC_REF_VOLT / ADC_FULL_SCALE / CURRENT_GAIN / SENSE_RESISTOR)这套宏换算下来输出电压的范围是0到36.3V电流范围是0到16.5A满足常见Boost板的量程。如果输出电压偏高导致分压后超过3.3VADC会直接读到4095闭环控制要在这里加一个满量程保护防止PI积分项饱和后输出失控。4.3 电压环PID和输出限幅PWM寄存器更新的时机PID控制器用增量式写法输出直接加到当前的比较寄存器值上。Boost电路是单极性的PWM占空比有明确上下限。我在代码里把上下限和积分分离写得比较细防止负载断开时输出电压超出设定值导致积分饱和。#define PWM_ARR_VALUE 7200 // 定时器自动重载值对应100kHz #define PWM_MAX_DUTY 6600 // 最大占空比限制约92% #define PWM_MIN_DUTY 600 // 最小占空比限制约8% static float err_sum 0.0f; // 积分项 void Boost_ControlLoop(void) { float vout VOLT_CONVERT(adc_vout_raw); float iin CURR_CONVERT(adc_iin_raw); float err BOOST_TARGET_VOLTAGE - vout; // 积分分离误差大于2V时不累积积分防止超调 if (err 2.0f err -2.0f) { err_sum err; } // 抗积分饱和钳位 if (err_sum 100.0f) err_sum 100.0f; if (err_sum -100.0f) err_sum -100.0f; float duty KP_VOLT * err KI_VOLT * err_sum; // 占空比限幅 if (duty PWM_MAX_DUTY) duty PWM_MAX_DUTY; if (duty PWM_MIN_DUTY) duty PWM_MIN_DUTY; // 更新比较寄存器PWM周期边界加载避免中间跳变 TIM_SetCompare1(TIM1, (uint16_t)duty); }更新比较寄存器这一行的位置有讲究。TCNT计数中改变比较值不会立即生效要到下一个周期溢出时才会加载这个机制在硬件上就保证了占空比在PWM周期边界切换不会出现半个周期宽半个周期窄的毛刺波。如果你用的MCU支持影子寄存器可以显式开启预装载功能保证这一点。STM32的TIM1比较通道默认就带预装载前提是你在初始化时没有关掉它。4.4 Keil工程里编译链接时的中斷优先级和浮点性能调整这个闭环控制跑在ADC中断里面中断里带浮点运算在Cortex-M3上会比较吃力。实测F103在72MHz下PI运算加两次宏换算大约耗时3微秒和ADC采样时间71.5个ADC时钟周期约合10微秒重叠来得及。但如果你用的是不带FPU的F1系列可以把换算系数改成整数运算把电压的0.01V作为一个量化单位电流的0.001A作为一个量化单位全部中间变量用int32_t算。这对Keil的默认优化设置更友好速度能提升40%以上。中断优先级这里把ADC中断设置成最高优先级优先级数字设为0PWM定时器更新中断设为1。需要确保控制环路不能被其他中断打断否则采样值和PWM更新之间的延迟会随机抖动表现在输出上是本来平滑的电压波形上多出周期性的小台阶。5. 采样时序验证用示波器、串口和一个小技巧保证电流波形可靠5.1 用示波器同时看PWM输出和ADC采样完成标志验证相位关系代码写完后先别急着看控制效果。用一个IO口在ADC中断里翻转电平接到示波器的第二通道第一通道接电感电流的采样电阻输出波形。如果配置正确你会看到翻转的方波边沿正好落在电感电流三角波的中间位置用CCM模式或者电流为零后的谐振点用DCM模式。如果边沿位置不稳定说明触发源没有从TIM1正确连到ADC回去检查ADC外部触发参数是否配置为TIM1的TRGO。如果边沿的抖动范围超过一个PWM周期多半是中断响应延时导致了相位偏移需要开启定时器更新事件中断并在里面做一次标志位同步。这里有个在Keil调试时容易踩的坑如果下载程序后连接不上报error: no stm32 target found! if your product embeds debug authentication, please disable it这个报错基本都是SWD引脚被复用或者RCC时钟树初始化不完全导致内核调试口被杀掉了拿镊子把BOOT0拉高再上电可救回等程序恢复后再改回BOOT0低电平。5.2 双通道数据通过串口打印出来验证ADC读数确实没有串扰Boost电路在MOS开关时di/dt非常大采样信号线如果紧挨着功率走线ADC输入引脚上会耦合出几十mV的尖峰这个尖峰一旦落在采样时刻附近电流值就会偏大。为验证串扰在空载时让Boost不工作用万用表短接采样电阻两端串口以1kHz频率打印电流原始值正常情况打印出来是在±1个LSB附近。如果打印出来的数值里有明显的周期脉冲就是采样时刻正好落在了开关沿附近解决办法是把ADC的采样时间调长从71.5周期改到239.5周期牺牲一点吞吐量换取样保持窗口内的电压稳定。void Debug_UART_Send(void) { char buf[64]; sprintf(buf, vout%d mV, iin%d mA\r\n, (int)(VOLT_CONVERT(adc_vout_raw) * 1000), (int)(CURR_CONVERT(adc_iin_raw) * 1000)); // 发送字符串注意这里如果用printf会阻塞太久手写UART发送 for (char *p buf; *p; p) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)(*p)); } }串口打印代码放在主循环里不要放进ADC中断。中断里做SPRINTF这类重活会把控制周期拖垮主循环里打印只是偶尔丢几包不影响控制。把打印时间戳也打出来和示波器对比检查打印机本身的时序问题。5.3 电流纹波和开关毛刺的波形判断什么时候该调节采样时间最后留在板上观察空载和轻载时的纹波。空载时Boost工作在打嗝模式burst mode输出电压纹波会有一个缓慢的三角波周期这是因为轻载时电感电流在每周期的尾部降到零控制器还在按固定频率发PWM。你的代码里如果加了最小占空比限制此时输出会略高于目标值误差积分器会逐渐把占空比往下拉。这时候采样到的电流值非常小如果ADC读数还是显示有几百毫安的电流检查是不是采样电阻两端的地线形成了额外的地环路用示波器探头夹在采样电阻两端观察波形是否存在开关频率的高频振荡。如果高振频率幅度超过采样电阻满量程的5%就得在采样电阻两端并联一个1nF的陶瓷电容做差模滤波电容值不能太大太大影响电流环带宽。在这个前提下再调PID参数你会发现之前怎么调都抖的系统突然就稳了因为给到环路里的信号是干净的真实值而不是混着开关噪声的假数据。本文还有配套的精品资源点击获取