Boost升压闭环命门:STM32双ADC同步采样与PID实现 简介一套基于STM32的DC-DC高效Boost升压电路完整工程采用C语言编写并在Keil5环境下开发面向嵌入式电源设计、电池供电设备及太阳能系统等需要电压提升的应用场景。工程实现输入输出电压同时采样结合PID/PWM控制策略实时调节占空比以维持稳定的升压输出。资源共239个文件涵盖C语言源码与头文件、编译产生的中间文件、启动文件、可烧录的HEX/AXF文件以及工程配置备份压缩包整体约8.28MB目录和模块划分清晰便于按功能检索。已有904人学习下载。源码中ADC采样、PWM生成、显示、定时器及通信等模块均独立组织既可直接参考主控配置与中断逻辑也可提取关键算法迁移到其他控制器。通过阅读工程可完整理解Boost主电路参数、STM32外设驱动与软件闭环控制的配合方式是学习高效升压控制器设计的一份实用模板。1. 同时采样才是Boost升压闭环的命门手上这份用 Keil 打开的 STM32 DC-DC 升压工程模块划分很典型adc、pid、pwm、timer、data_communication、dc_display、lq_lcd 各占一个源文件。很多人搭 Boost 升压电路的时候占空比给上了、电压也升上去了但一加负载输出电压就来回飘。写 C 语言驱动时反复排查最后发现大多数情况下问题不在拓扑在采样ADC 先采输入再采输出两次转换间隔了十几微秒PID 却拿这两路数据算同一个占空比输入输出的状态根本不是同一时刻的。STM32 双 ADC 同步触发就是解决这个问题的一个边沿同时拉起 ADC1 和 ADC2两路电压同一拍进寄存器。下面从 Boost 参数计算讲到 Keil 下的同时采样工程实现再落到 PID 调占空比的 C 语言代码和实测排雷。2. Boost升压电路原理与器件参数计算2.1 电感储能与升压的两种运行模式2.1.1 连续导通模式下的电压增益推导Boost 电路的拓扑很精简一个电感、一个开关管、一个整流二极管、一个输出电容。开关管导通时输入电源直接给电感充磁电感电流线性上升开关管关断时电感电流不能突变感应电动势与输入电压串联把能量推到输出电容。稳态且工作在 CCM连续导通模式下电压增益关系是 Vout/Vin 1/(1-D)D 是 PWM 占空比。按这个公式D0.6 理论上能升到 2.5 倍。实际电路里电感 DCR、开关管 RDS(on)、二极管正向压降和电容 ESR 都会吃掉一部分电压真实增益永远低于理想值负载越大偏差越明显。这偏差本身就是需要闭环控制的原因。还要看到CCM 的增益公式里没有负载电流项说明在连续导通时输出电压主要由占空比决定一旦电路掉进另一种模式这个结论就不成立了。2.1.2 断续导通模式下增益与负载强耦合占空比偏小或者负载偏轻时电感电流会在一个周期内提前放完电路进入 DCM断续导通模式。DCM 下的电压增益不再只由占空比决定而是与开关频率、电感感值、负载电阻耦合在一起。开环系统在这两种模式下输出电压差异很大如果负载还动态变化输出电压会跟着剧烈波动。设计时尽量让主工作区间落在 CCM轻载时允许掉进 DCM但必须靠闭环把输出电压拉回设定值。2.2 关键器件参数的计算实例2.2.1 电感感值与纹波电流的约束关系CCM 下电感电流纹波 ΔIL Vin × D / (fsw × L)。工程上习惯把纹波电流设计在满载平均电流的 20% 到 40% 之间。纹波过大输出电容 ESR 发热明显电磁干扰也随之上升纹波过小电感体积和成本增加不少。按 30% 目标计算电感感值下限为 Lmin Vin × (Vout - Vin) / (fsw × ΔIL × Vout)。举个实际例子输入 5V输出 12V开关频率 100kHz最大负载 1A纹波取 0.3A。占空比 D 1 - 5/12 0.583Lmin ≈ 5 × 0.583 / (100000 × 0.3) ≈ 97μH。留 20% 到 30% 设计裕量取 100μH 或 120μH 都可以。这段计算写成 C 代码也就是几行float vin 5.0f, vout 12.0f, fsw 100000.0f, di_l 0.3f; float duty 1.0f - vin / vout; float l_min vin * duty / (fsw * di_l); /* 约97μH */开关频率的选择直接影响电感和功耗的平衡。100kHz 在 STM32 项目里是个折中值电感不至于做得太大MOSFET 的开关损耗也不至于太明显。如果追求体积把频率提到 250kHz同样的 5V 升 12V、0.3A 纹波下Lmin 可以降到 39μH 左右电感体积明显变小但开关损耗相应增加MOSFET 的驱动功耗也要重新核算。做便携设备时这种取舍很常见。2.2.2 输入输出电容的容量下限输出电容的任务是在开关管导通期间维持负载电流。给定允许的输出纹波 Vripple输出电容下限可以估算为 Cout ≥ Iout × D / (fsw × Vripple)。按 Iout 1A、D 0.583、fsw 100kHz、Vripple 50mV算出来 Cout ≥ 117μF。实际工程里取 220μF 电解电容并联一个 10μF 陶瓷电容电解电容提供容量陶瓷电容压低高频 ESR。输入侧主要吸收电感电流的脉动47μF 电解电容就够了。2.2.3 开关管与二极管的选型依据开关管漏源耐压要高于最大输出电压通常预留 1.5 到 2 倍裕量输出 12V 时选 30V 以上耐压的 MOSFET。RDS(on) 越小导通损耗越低Qg 越小开关损耗越低两者之间往往要平衡。二极管必须用肖特基或快恢复管普通整流管反向恢复时间太长开关频率一高效率明显下降。若是同步整流上下管都换成 MOSFET同步解决的代价是必须处理死区时间防止上下管同时导通。以下是这个案例的设计参数汇总设计参数符号计算值实际选取电感感值L97μH100μH饱和电流 1.5A 以上输出电容Cout≥117μF220μF 电解 10μF 陶瓷输入电容Cin——47μF 电解开关管耐压Vds≥18V30V N-MOSFET整流二极管VRRM≥24V肖特基 30V / 2A2.3 同步整流与非同步整流的效率取舍2.3.1 同步整流的死区与驱动非同步 Boost 靠二极管续流肖特基压降约 0.4V输出电流 2A 时就是 0.8W 的损耗。同步整流把二极管换成 MOSFET导通损耗变为 I² × RDS(on)2A 电流下通常只有零点几瓦整体效率能高 3 到 5 个百分点。代价是控制逻辑变复杂两个管子不能同时导通否则电源直接短路。STM32 高级定时器可以输出互补 PWM 并配置死区时间死区典型范围是 100ns 到 500ns最终要根据栅极驱动 IC 的传播延迟来标定。死区太小会穿通烧管死区太大则续流阶段中断输出纹波变大。非同步结构在成本敏感的小功率场合仍然可用但损耗集中在二极管上温度上升明显。同步结构的效率优势在中大电流下不可替代缺点是控制代码必须考虑死区和模式切换调试工作量上了一个台阶。做 5V 升 12V/1A 这个量级的项目我的建议是直接上同步整流Keil 工程里把互补 PWM 和死区寄存器配好调试时间多半天效率收益却很直观。3. STM32 双 ADC 同时采样与 Keil 驱动实现3.1 顺序采样与同步采样的时序差异3.1.1 顺序采样引入的控制环路相位滞后最省事的采样做法是让单个 ADC 轮流转换输入和输出两个通道。STM32 的 ADC 单次转换本身也就一两个微秒但加上通道切换、采样保持时间两路数据之间会拉开十几微秒。在 100kHz 开关频率下这十几微秒跨越了十几个开关周期输入输出状态已经明显变化。PID 控制器拿到的反馈实际上存在一段时间延迟等效于在控制环路里插入了一个延迟环节环路相位裕度下降增益稍高就容易振荡这就解释了为什么开环正常、一闭环就抖。这里有个容易被忽略的细节ADC 的采样保持电容充电时间与外部信号源阻抗直接相关。阻抗大采样保持阶段电压建立慢转换结果就会偏低。所以采样电路前端加了电压跟随器后不只是阻抗匹配问题采样精度和一致性都会改善。3.1.2 双 ADC 规则同步的触发机制STM32F103 内部有两个独立的 12 位 ADC。双 ADC 规则同步模式下ADC1 收到外部触发信号时ADC2 同步启动各自通道的转换转换结束后数据分别存入 ADC1_DR 和 ADC2_DR两个数据是同一时刻的快照。外部触发源可以设置为定时器更新事件或者定时器比较输出推荐直接用 PWM 定时器的更新事件这样采样频率和开关频率天然锁定控制环路的时间基准就统一了。下面把两种方式的差别列出来采样方式两路时间差对 PID 环路的影响资源占用单 ADC 顺序采样10~20μs相位滞后明显易振荡仅 ADC1双 ADC 规则同步约 0同沿触发可忽略ADC1 ADC2外加采样保持器硬件级一致无成本高高速场合才用3.2 可复用的 ADC 双通道同步初始化代码3.2.1 标准外设库下的初始化流程Keil uVision5 里用标准外设库初始化的完整流程如下。这里用 DMA 环形模式把两路转换结果持续搬运到内存数组不占用 CPU__align(4) uint16_t adc_value[2]; /* [0]输入电压, [1]输出电压 */ void ADC_SyncInit(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_ADC2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); /* 72MHz/612MHz */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_RegSimult; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T1_CC1; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_Init(ADC2, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC2, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_value; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 2; 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_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_Cmd(ADC2, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }代码里的关键点有三个。第一是ADC_Mode_RegSimult它把两路 ADC 绑定成规则同步模式ADC2 不需要单独配置触发源跟着 ADC1 的触发信号起步。第二是ADC_ExternalTrigConv_T1_CC1用 TIM1 的通道 1 比较事件做触发让采样时刻和 PWM 开关时刻保持同一时间基准。第三是 DMA 的外设地址写成ADC1-DR同步模式下 DMA 会自动把 ADC1 和 ADC2 的结果连续搬运到内存两个值在adc_value数组里天然对齐。3.2.2 采样结果的处理时机转换结果准备好后主循环或者低优先级中断里直接读adc_value[0]和adc_value[1]。ADC 触发频率如果设为 100kHz每次触发都会刷新两路数据。12 位分辨率下单次转换噪声并不小建议对最近 8 到 16 次结果做滑动平均后再进 PID。平均窗口太大会在环路上引入额外滞后具体长度依赖纹波要求电源输出纹波要求严格的项目里取 8 次折中。3.3 前端信号调理与标定3.3.1 分压电阻与电压跟随器STM32 的 ADC 输入范围是 0 到 3.3V。以输出电压 12V 为例用 R1 30kΩ、R2 10kΩ 两只 0.1% 精度电阻分压分压比 1/412V 落到 3V留了 10% 余量。但分压网络的等效源阻抗约 7.5kΩSTM32 内部采样电容在短采样时间内充不满采样结果会系统性偏低。常见做法是在分压点和 ADC 引脚之间加一级电压跟随器运放输入阻抗高、输出阻抗低把分压网络和 ADC 隔离开来采样准确度明显改善。3.3.2 输入电压采样的前馈意义输入电压采样不是可选项。输入源是电池时端电压随放电深度持续下降如果没有输入采样负载突变瞬间 PID 要花较长时间才能找回应有的占空比。如果先用 D 1 - Vout/Vin 估算一个期望占空比作为前馈项PID 只负责修正误差动态响应会快很多。工程实现上把pid_update()返回的占空比与前馈占空比相加再写入比较寄存器即可float duty_total pid_update(pid, measured_vout) (1.0f - setpoint_voltage / input_voltage);这个写法在输入电压变化频繁的场景里非常有效电池电量从满到空输出电压都能稳住。4. 增量式PID控制与PWM占空比调节4.1 电压型PID控制结构4.1.1 为什么这里用增量式PIDBoost 电路下游负载可能动态变化位置式 PID 输出的是一整个占空比值积分项容易在偏差持续存在时饱和。负载恢复后占空比不能迅速回落输出电压会产生明显的过冲甚至振荡。增量式 PID 输出的是占空比的增量 ΔD每次只允许占空比小步调整工程上更稳健计算公式是ΔD(k) Kp × (e(k) - e(k-1)) Ki × e(k) Kd × (e(k) - 2·e(k-1) e(k-2))最终占空比 D(k) D(k-1) ΔD(k)。代码里只需要维护 e(k)、e(k-1)、e(k-2) 三个误差值和上一个占空比实现成本很低。位置式 PID 在 Boost 里还有一个麻烦启动瞬间误差很大积分项快速堆积输出电压会冲过头。增量式 PID 每次只改 5% 占空比启动就像给油门而不是踩地板油电感电流冲击小很多。4.1.2 PID更新频率与开关频率的关系PID 计算间隔要远大于 PWM 周期。比如 PWM 频率 100kHz、周期 10μsPID 每 200 个开关周期计算一次相当于 2ms 间隔、500Hz 更新率这是比较合适的中点。PID 更新太频繁比如接近开关频率占空比每个周期都在变电感电流会出现不规则振荡更新太慢负载突变后恢复时间太长。具体频率要结合负载变化的陡峭程度调整没有统一规定。4.2 PWM输出通道与保护机制4.2.1 TIM1 通道的 PWM 初始化下面是 TIM1 输出 100kHz PWM 的初始化代码。倍频器 72 分频后计数时钟为 1MHz自动重载值设为 9输出频率就是 100kHzvoid PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; /* TIM1_CH1 */ GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 10 - 1; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 5; /* 初期占空比50% */ TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInit(TIM1, TIM_OCInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }TIM_Pulse是 CCR 寄存器的值对应占空比输出。改变占空比时把 PID 返回的 0~1 之间的浮点数乘上TIM_Period 1再写入比较寄存器就是TIM_SetCompare1(TIM1, (uint16_t)(duty * 10))。4.2.2 占空比极限与短路保护占空比不能无限制增大。输出短路时占空比推高电感电流线性飙升超过开关管电流极限后直接炸管。保护思路有两种软件上给占空比设置上限比如 90%硬件上再额外采样电感电流或采样电阻电压超过阈值立即拉低 PWM 输出。软件限幅成本最低响应速度慢一些硬件过流保护动作在微秒级适合更高功率的项目。多数低成本应用先用软件限幅配合一个简单的比较器电路做辅助保护。4.3 PID 模块的 C 语言实现与参数整定4.3.1 可移植的增量式 PID 结构体工程里 pid.c 拆成初始化、更新、复位三个函数结构体把参数和中间变量封装在一起方便运行时在线改参typedef struct { float Kp, Ki, Kd; float target; /* 目标输出电压用ADC码值表示 */ float err[3]; /* err[0]e(k), err[1]e(k-1), err[2]e(k-2) */ float duty; /* 当前占空比0.0~1.0 */ } IncrementalPID; void pid_init(IncrementalPID *pid, float kp, float ki, float kd, float target) { pid-Kp kp; pid-Ki ki; pid-Kd kd; pid-target target; pid-err[0] pid-err[1] pid-err[2] 0.0f; pid-duty 0.5f; /* 从50%占空比起步防止启动冲击 */ } float pid_update(IncrementalPID *pid, float measured) { float delta; pid-err[2] pid-err[1]; pid-err[1] pid-err[0]; pid-err[0] pid-target - measured; delta pid-Kp * (pid-err[0] - pid-err[1]) pid-Ki * pid-err[0] pid-Kd * (pid-err[0] - 2.0f * pid-err[1] pid-err[2]); if (delta 0.05f) delta 0.05f; /* 单步增量限幅5% */ if (delta -0.05f) delta -0.05f; pid-duty delta; if (pid-duty 0.90f) pid-duty 0.90f; if (pid-duty 0.05f) pid-duty 0.05f; return pid-duty; }target之所以用 ADC 码值而不是电压值是为了省去每个控制周期把浮点电压换算成码值的计算。delta的 5% 限幅对动态响应影响很大限幅太紧负载阶跃后恢复慢太松启动瞬间电感电流冲击大。实际调试原则是第一次松一点观察波形后再收紧。4.3.2 PID 参数的整定起点与观察方法手头没有参考参数时我的做法是先把 Kd 设为 0形成纯 PI 控制。Kp 从 0.1 逐步往上加直到输出电压出现轻微但持续的振荡取这个临界值的 0.6 倍作为 Kp 初值。然后加 Ki从 0.001 开始逐步加大直到空载到满载切换时静差消失。最后加 Kd 抑制超调但 Kd 过大会把高频噪声放大。下面是一个参考整定表参数作用取值过大的表现取值过小的表现启动参考值Kp比例增益高频振荡、啸叫动态响应慢、静差大0.1 ~ 0.3Ki积分增益低频振荡稳态误差消不掉0.001 ~ 0.01Kd微分增益噪声被放大、占空比毛糙超调明显0.01 ~ 0.05一个 5V 升 12V 的实测案例最终整定结果是 Kp 0.35、Ki 0.015、Kd 0.02满载 1A 下输出纹波约 40mV 峰峰值。5. 实测验证与常见问题的排查思路5.1 电感电流波形与开关节点振铃把电流探头夹在电感前端正常 CCM 下电感电流是连续的三角波谷值大于零。谷值触底为零且维持一段水平线说明已经进入 DCM要么电感量偏小要么负载太轻。开关管关断瞬间漏源电压上的高频振铃也要留意振铃过大会辐射干扰到控制信号。RC 吸收电路是最直接的抑制手段在开关管漏源之间并联 R 10Ω、C 470pF用热像仪看吸收电阻温升温度过高就把 C 减小到 220pF。5.2 负载阶跃下输出电压过冲的调整用电子负载做 0 到 100% 的阶跃观察输出电压的过冲幅度和恢复时间。过冲偏大时先适当增大 Kd同时检查 ADC 前端滤波电容是否过大。滤波电容太大会让 ADC 看到的电压变化变迟钝控制环路以为输出还没追上实际已经超调反而放大过冲。如果微分作用容易引入噪声可以在 PID 的微分项前加一级 5Hz 左右的低通滤波或者把采样平均值窗口从 16 次降到 8 次。5.3 Keil 调试环境的几个检查点5.3.1 SWD 连接失败与虚拟串口驱动的处理Keil 报 “No STM32 Target Found!” 时先确认 SWD 引脚没有被代码复用为普通 GPIO再检查 Debug 设置里的下载时钟频率线缆较长时从 4MHz 降到 1MHz 往往能解决。设备管理器里 STM32 Virtual COM Port 出现黄色感叹号是虚拟串口驱动版本不匹配卸载后重装 ST-LINK 配套驱动即可。5.3.2 用预处理文件定位宏定义问题Keil 工程编译后会在输出目录生成多个.__i预处理文件比如 adc.__i、pid.__i、pwm.__i。怀疑某个宏定义或者头文件展开了异常代码时直接打开对应的.__i文件查看预处理结果比反复改代码重编快得多。这也是手头这份工程里留下 data_communication.__i、dc_display.__i 等文件的原因它们是编译中间产物平时留着做排错用正好。5.3.3 串口在线调参的实现PID 参数放进结构体里是为了能够运行时直接修改。在串口中断里解析简单文本命令比如PKP 0.35表示设置比例系数void ParseUartCommand(char *buf) { if (strncmp(buf, PKP, 3) 0) { pid.Kp atof(buf 4); } else if (strncmp(buf, PKI, 3) 0) { pid.Ki atof(buf 4); } else if (strncmp(buf, PKD, 3) 0) { pid.Kd atof(buf 4); } }写好这个命令解析后连上串口助手就能在电路运行状态下改控制参数省去反复编译下载的等待现场调 PID 的时间至少能压缩一半。本文还有配套的精品资源点击获取