简介:这份资源面向嵌入式开发学习者与电源设计爱好者,提供一套基于STM32的Boost数控电源完整工程,帮助理解升压变换器与数字闭环控制的实际落地方式。压缩包共510个文件,约32.82MB,以96个h头文件、70个c源文件为核心,配合Keil工程文件(uvprojx、uvoptx、sln)、编译产物(o、axf、hex、map)以及上位机相关的cs、resx、config等文件,覆盖从底层驱动到上位机交互的完整链路。内容涉及STM32外设配置、PWM调制、ADC采样、恒流与恒压双环切换策略,并附带芯片包、固件库与通信协议说明,便于对照硬件电路调试。目前已有1609人学习下载,适合希望系统掌握数控电源软硬件协同设计、积累嵌入式电源项目经验的读者参考借鉴。
1. 从一块能“说话”的电源说起:为什么我盯上了这个 STM32 Boost 数控电源项目
去年帮一个做电池测试的朋友调设备,他需要一台能按任意曲线输出 0~36V、电流上限 5A 的电源,还要能实时回传电压电流数据。买成品?预算不够。用普通可调电源?没法编程控制。后来翻到这个基于 STM32 的 Boost 数控电源项目,才意识到它正好卡在一个很实用的位置上:不是实验室里那种只跑仿真的玩具,也不是工业级机架电源,而是一套能自己改参数、自己写上位机协议、自己加保护逻辑的完整工程。它用 STM32 做数字闭环,Boost 升压拓扑负责把输入电压抬到目标值,配合 ADC 采样和 PWM 调节,实现恒压恒流输出。适合谁?电子类学生做毕设、嵌入式新手练外设、以及需要小功率可编程电源的硬件工程师。资料丰富这点很关键——不是扔几个原理图就完事,而是从主电路到控制代码都有东西可看。
2. 硬件架构拆解:Boost 功率级与 STM32 控制环是怎么搭上的
2.1 主电路拓扑:为什么选 Boost 而不是 Buck
先明确一点,这个项目做的是升压电源,输入电压低于输出电压。常见做法是输入 12V 左右,输出可调范围大概 12V 到 36V,具体上限看电感和 MOS 管的耐压。Boost 拓扑的核心就三样:电感、开关管、二极管。STM32 输出 PWM 驱动 MOS 管,电感储能后通过二极管给输出电容充电。相比 Buck,Boost 的难点在于右半平面零点,环路补偿更麻烦,但好处是输入电流连续,对前级电源友好。
我一般会先看功率级参数:电感值决定纹波电流,输出电容决定纹波电压,开关频率决定电感和电容的尺寸。这个项目里开关频率通常设在 50kHz 到 100kHz 之间,STM32 的高级定时器正好能输出互补 PWM 带死区,虽然 Boost 不需要同步整流,但留出死区可以防止直通。
// 以 STM32F1 为例,TIM1 通道1 输出 PWM 驱动 MOS // 假设系统时钟 72MHz,PWM 频率 100kHz #define PWM_FREQ 100000 #define TIM_CLK 72000000 #define PWM_PERIOD (TIM_CLK / PWM_FREQ) // 720 void Boost_PWM_Init(void) { // 时基配置:向上计数,周期 720-1 TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler = 0; tim.TIM_Period = PWM_PERIOD - 1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, &tim); // 通道1 PWM 模式,初始占空比 0 TIM_OCInitTypeDef oc; oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; TIM_OC1Init(TIM1, &oc); // 高级定时器需要使能主输出 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }这段代码里PWM_PERIOD决定了开关频率,改频率就改这个值。TIM_Pulse就是占空比,后面 PID 算出来的结果直接写进去。注意高级定时器必须调用TIM_CtrlPWMOutputs,否则引脚没波形,这个坑我踩过。
2.2 采样与反馈:ADC 怎么读电压电流
数字电源的“数控”体现在反馈环由软件算。项目里一般用 STM32 的 ADC 扫描模式,两个通道分别采输出电压和输出电流。电压采样用电阻分压,电流采样用采样电阻加运放放大。关键点是采样时刻要避开开关噪声,常见做法是在 PWM 周期中点触发 ADC,或者用定时器触发注入通道。
// ADC 注入通道,由 TIM1 触发,在 PWM 中点采样 void Boost_ADC_Init(void) { ADC_InitTypeDef adc; adc.ADC_Mode = ADC_Mode_Independent; adc.ADC_ScanConvMode = ENABLE; adc.ADC_ContinuousConvMode = DISABLE; adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfChannel = 2; ADC_Init(ADC1, &adc); // 规则通道:电压、电流 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这里用外部触发而不是软件启动,是为了让采样点和 PWM 同步。ADC_SampleTime_55Cycles5是采样保持时间,信号源内阻大就加长。采回来的原始值要换算:电压 = ADC值 × 参考电压 / 4096 × 分压比。电流同理,还要除以放大倍数和采样电阻。
2.3 控制环:PID 在中断里怎么跑
整个控制环跑在定时器中断里,频率一般和 PWM 同频或者分频。中断里做三件事:读 ADC 结果、算 PID、更新 PWM 占空比。恒压模式下目标值是电压,反馈是电压采样;恒流模式下目标值是电流。模式切换靠外部按键或者上位机命令。
// 定时器中断服务函数,控制周期 100us void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 1. 读取 ADC 注入通道结果 float v_out = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1) * V_SCALE; float i_out = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2) * I_SCALE; // 2. 根据模式选择反馈量 float feedback = (mode == CV_MODE) ? v_out : i_out; float target = (mode == CV_MODE) ? v_set : i_set; // 3. 位置式 PID float err = target - feedback; integral += err * KI; if (integral > INTEGRAL_MAX) integral = INTEGRAL_MAX; if (integral < 0) integral = 0; // Boost 占空比不能为负 float duty = KP * err + integral + KD * (err - last_err); last_err = err; // 4. 限幅并写入 PWM if (duty > DUTY_MAX) duty = DUTY_MAX; if (duty < DUTY_MIN) duty = DUTY_MIN; TIM_SetCompare1(TIM1, (uint16_t)(duty * PWM_PERIOD)); } }参数说明:KP、KI、KD需要根据实际功率级调,一般先调 KP 让响应快但不振荡,再加 KI 消静差,KD 抑制超调。DUTY_MAX通常设 0.9 以下,防止电感饱和。integral做了抗饱和处理,因为 Boost 启动瞬间误差大,积分容易冲顶。
3. 软件框架与工程目录:拿到源码先看哪几个文件
3.1 目录结构:从启动文件到应用层
一个典型的 STM32 数控电源工程目录大概长这样:
BoostPower/ ├── CMSIS/ # 内核相关 ├── STM32F10x_StdPeriph_Driver/ # 标准外设库 ├── User/ │ ├── main.c # 主循环,初始化 + 状态机 │ ├── boost_ctrl.c # PID 与 PWM 控制 │ ├── adc_sample.c # 采样与换算 │ ├── oled.c # 本地显示 │ └── uart_protocol.c # 上位机通信 ├── Hardware/ │ ├── boost_sch.pdf # 原理图 │ └── bom.xlsx # 物料清单 └── Project/ └── MDK-ARM/ # Keil 工程拿到源码先看main.c里的初始化顺序,再看boost_ctrl.c的中断服务函数,最后看uart_protocol.c的帧格式。硬件部分先对原理图确认采样电阻和分压电阻的值,不然换算系数全错。
3.2 主循环状态机:待机、恒压、恒流、保护
主循环不干重活,只做状态切换和显示刷新。控制环在中断里跑,主循环通过标志位和中断交换数据。
typedef enum { STATE_IDLE, STATE_CV, STATE_CC, STATE_FAULT } PowerState; int main(void) { SystemInit(); Boost_PWM_Init(); Boost_ADC_Init(); OLED_Init(); UART_Init(115200); Timer_Init(); // 控制周期定时器 PowerState state = STATE_IDLE; while (1) { // 读取按键或上位机命令 Command cmd = UART_PollCommand(); switch (state) { case STATE_IDLE: if (cmd == CMD_START_CV) { v_set = cmd.value; state = STATE_CV; } if (cmd == CMD_START_CC) { i_set = cmd.value; state = STATE_CC; } break; case STATE_CV: case STATE_CC: // 检查保护:过压、过流、过温 if (v_out > V_MAX || i_out > I_MAX || temp > TEMP_MAX) { state = STATE_FAULT; TIM_SetCompare1(TIM1, 0); // 关 PWM } if (cmd == CMD_STOP) state = STATE_IDLE; break; case STATE_FAULT: // 等待复位命令 if (cmd == CMD_RESET) state = STATE_IDLE; break; } OLED_Refresh(v_out, i_out, state); } }这个状态机的好处是保护逻辑集中,任何模式下过流都直接切到 FAULT 并关 PWM。注意v_out和i_out是中断里更新的全局变量,主循环读的时候最好关一下中断或者用原子读,不然可能读到半更新的值。
3.3 上位机通信:帧格式与命令解析
资料丰富的项目一般会带一个简单的串口协议,方便用电脑发指令。常见帧格式是帧头 + 命令 + 数据 + 校验。
// 帧格式:0xAA 0x55 | CMD | LEN | DATA... | CRC8 typedef struct { uint8_t header[2]; uint8_t cmd; uint8_t len; uint8_t data[16]; uint8_t crc; } UART_Frame; void UART_ParseByte(uint8_t byte) { static uint8_t buf[32]; static uint8_t idx = 0; buf[idx++] = byte; if (idx < 4) return; // 检查帧头 if (buf[0] != 0xAA || buf[1] != 0x55) { idx = 0; return; } uint8_t len = buf[3]; if (idx < 4 + len + 1) return; // 没收完 // 校验 if (CRC8(buf, 4 + len) != buf[4 + len]) { idx = 0; return; } // 处理命令 switch (buf[2]) { case 0x01: v_set = *(float*)&buf[4]; break; // 设电压 case 0x02: i_set = *(float*)&buf[4]; break; // 设电流 case 0x03: /* 启动 */ break; case 0x04: /* 停止 */ break; } idx = 0; }这里用 CRC8 而不是简单累加和,抗干扰好一些。数据段用 float 直接传要注意大小端,STM32 是小端,上位机如果是 C# 也要按小端解析。如果只做本地控制,这部分可以裁掉,但留着方便做数据记录。
4. 上手跑通:从零搭建到输出第一度电
4.1 硬件准备:最小系统与功率板
先别急着焊功率级。第一步用 STM32 最小系统板加杜邦线,只连 PWM 输出和 ADC 输入,用信号发生器模拟反馈电压,验证代码逻辑。确认 PID 能调、串口能通、OLED 能显示,再上功率板。功率板上的电感选铁硅铝磁环,饱和电流要大于最大输入电流的 1.5 倍。MOS 管选低导通电阻的 N 沟道,驱动用专用芯片或者图腾柱。二极管必须用快恢复或者肖特基,普通整流管反向恢复会炸管。
提示:第一次上电一定要串限流电阻或者用可调电源限流,防止占空比异常导致电感饱和。
4.2 软件编译与烧录:Keil 工程配置要点
打开Project/MDK-ARM下的工程,先确认器件型号选对。然后在Options for Target里检查:
- Target 页:晶振频率填实际值,比如 8MHz 外部晶振。
- C/C++ 页:预定义宏
USE_STDPERIPH_DRIVER,头文件路径包含User、Hardware、STM32F10x_StdPeriph_Driver/inc。 - Debug 页:选 ST-Link 或 J-Link,勾选 Reset and Run。
编译如果报undefined symbol,多半是库文件没加全。标准外设库需要把stm32f10x_gpio.c、stm32f10x_tim.c、stm32f10x_adc.c、stm32f10x_usart.c都加进工程。烧录后先看 OLED 有没有显示,串口有没有输出,再逐步加功率。
4.3 上电调试:先开环再闭环
开环测试:固定占空比 10%,输入 12V,测输出应该比输入高。如果输出等于输入,检查 MOS 管有没有开关、电感有没有接错。开环正常后,切闭环,目标电压设 15V,看输出能不能稳住。如果振荡,先把 KP 调小,KI 置零,等稳定后再加积分。
// 调试时可以先写死占空比,验证功率级 void Boost_OpenLoop_Test(void) { TIM_SetCompare1(TIM1, PWM_PERIOD * 0.1); // 10% 占空比 while (1) { // 只读 ADC 显示,不跑 PID float v = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1) * V_SCALE; OLED_ShowFloat(0, 0, v); Delay_ms(200); } }这个函数用来确认采样换算对不对。如果显示值和万用表差很多,先查分压电阻值,再查参考电压是不是 3.3V。
5. 避坑与常见问题:烧管、振荡、采样跳变怎么排查
5.1 上电就烧 MOS 管
现象:一上电 MOS 管发烫甚至冒烟。原因通常是占空比初始值太大或者驱动不足。解决:初始化时TIM_SetCompare1写 0,主循环里等状态机切到 CV/CC 再开 PWM。驱动电阻选 10Ω 左右,太小会振铃,太大会开关损耗大。
5.2 输出电压振荡
现象:输出在目标值附近来回摆。原因:PID 参数太激进,或者采样噪声大。解决:先降 KP,加输出电容,ADC 采样加均值滤波。如果还是振,检查电感值是不是太小,纹波电流太大。
5.3 ADC 采样值跳变严重
现象:显示电压跳动超过 0.5V。原因:采样点和开关沿重叠,或者地线没处理好。解决:把 ADC 触发点移到 PWM 中点,模拟地和功率地单点连接,采样电阻用开尔文接法。
5.4 串口命令没反应
现象:上位机发了指令,电源不动。原因:帧头不对、波特率不匹配、或者中断优先级冲突。解决:先用示波器看 STM32 的 TX 有没有波形,再确认串口中断优先级低于 PWM 中断,否则控制环会被打断。
5.5 带载后电压掉
现象:空载 15V,接负载掉到 12V。原因:电流采样没参与反馈,或者 PID 积分限幅太小。解决:切恒流模式看电流能不能稳住,如果恒流正常,说明电压环增益不够,适当加 KI。
6. 二次开发:把固定输出改成可编程曲线输出
这个项目最值钱的地方不是它现在能输出多少伏,而是它的控制环是开放的。你可以在TIM2_IRQHandler里把v_set从固定值改成查表值,就能输出任意波形。我一般会加一个数组存曲线点,用定时器每 1ms 更新一次目标值。
// 可编程曲线输出:数组存目标电压,索引递增 #define CURVE_POINTS 100 float curve[CURVE_POINTS] = {0}; // 由上位机下发 uint16_t curve_idx = 0; void TIM3_IRQHandler(void) // 1ms 定时器 { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if (mode == CV_MODE && curve_idx < CURVE_POINTS) { v_set = curve[curve_idx++]; } } }改造时注意两点:一是曲线更新频率不能高于控制环频率,否则 PID 跟不上;二是要在曲线结束后自动切回待机,防止一直输出。验证方法很简单,用示波器接输出,看波形跟数组是不是一致。如果滞后明显,把控制周期缩短到 50us。
还有一个实用技巧:把 PID 参数做成上位机可调,不用每次改代码重新烧录。在串口协议里加两个命令,一个写 KP,一个写 KI,存到 Flash 里。这样调电源就像调示波器一样方便。从那以后我每次做数字电源,都强制把参数和曲线做成可在线修改的,不然调试阶段烧录次数太多,芯片座都松了。希望帮到你。
本文还有配套的精品资源,点击获取