FastBond4挑战部分-基于STM32的可调电源
该项目使用了KiCad软件、C语言,实现了-4V到4V电源可调的设计,它的主要功能为:STM32F103C8T6通过I2c控制MCP4725和反馈网路实现-4V到4V电源可调。
标签
嵌入式系统
测试
曦曦曦曦
更新2026-04-09
24
KiCad文件
全屏

一、项目主题与项目介绍

项目名称

基于 STM32F103C8T6 的数控可调直流电源(含正负电源)和PWM波输出设计

项目背景

在嵌入式开发、电子测量(如信号发生器、示波器)等场景中,需要低纹波、高精度、可调的直流电源,同时需要正负电压为运放等模拟电路供电。本项目设计了一款以 STM32F103C8T6 为控制核心,结合线性稳压LM1117S、电荷泵MCP4725、数模转换(DAC)MCP4725、运算放大LM358的数控可调电源,可实现 -4V~4V连续可调输出,同时提供 + 5V、+3.3V、-5V 等辅助电源,满足嵌入式系统与模拟电路的供电需求。

核心功能

  1. 输入:DC 12V 直流电源输入
  2. 固定电源输出:+5V(最大 1A)、+3.3V(最大 800mA)、-5V(最大 100mA)
  3. 可调电源输出:数控-4V~4V 连续可调,8 位 DAC 实现约 19.5mV 步进精度
  4. PWM 波输出:PA8 (TIM1_CH1) 引脚输出可编程 PWM 波,频率 1Hz~100kHz 可调,占空比 0~100% 可调
  5. 人机交互:按键控制电压调节+ PWM 参数调节
  6. 状态指示:电源 LED 指示灯,实时反馈供电状态,TFT屏幕,显示输出具体数据
  7. 扩展能力:预留 STM32 全 IO 口、电源接口,支持电压采样、上位机控制等功能扩展

二、硬件器件全介绍

表格

器件类型

器件型号

功能说明

主控芯片

STM32F103C8T6

ARM Cortex-M3 内核,72MHz 主频,作为系统控制核心,负责 DAC 控制、按键扫描、电压调节逻辑

线性稳压器

LM1117-5.0

输入 7~20V,输出 + 5V/1A,为 DAC、运放、电荷泵等模拟电路提供主电源

线性稳压器

LM1117-3.3

输入 4.5~15V,输出 + 3.3V/800mA,为 STM32 及数字电路供电

电荷泵

ICL7660

将 + 5V 转换为 - 5V,为运放提供负电源,实现单 12V 输入下的正负双电源

数模转换器

MC14725

8 位乘法型 DAC,将 STM32 数字信号转换为模拟电压,作为可调电源的基准

运算放大器

LM358

双运放,一路用于 DAC 输出电压的放大 / 跟随,提高带载能力;另一路用于辅助信号处理

输入与保护

DC 电源座 + 船型开关

12V 电源输入与总开关,控制系统上电

无源器件

电解电容 / 陶瓷电容

全链路电源滤波,抑制纹波,稳定电压(如 C1、C2、C13 等)

无源器件

电阻

运放反馈 / 分压、限流、保护(如 R1、R7、R25 等)

指示器件

LED + 限流电阻

电源指示灯,上电常亮,指示系统供电正常

显示器件

2.4英寸TFT屏幕

将数据可视化

扩展接口

STM32 IO 排针

预留全 IO 口,支持 OLED/TFT 显示、ADC 采样、串口调试等扩展


三、方案框图与项目设计思路

1. 系统方案框图

2. 核心设计思路

  1. 电源架构设计
    • 采用线性稳压 + 电荷泵方案:LM1117 系列线性稳压器产生低纹波 + 5V、+3.3V,满足数字 / 模拟电路供电需求;ICL7660 电荷泵将 + 5V 转换为 - 5V,实现单 12V 输入下的正负双电源,避免运放单电源供电的失真问题。
    • 线性稳压优势:低纹波、低噪声,适合对电源质量要求高的 DAC、运放等模拟器件。
  2. 数控可调设计
    • 采用STM32+DAC + 运放架构:STM32 控制 MC14725 DAC 输出 0~5V 模拟电压,再通过 LM358 运放放大 / 跟随,实现可调电压输出,同时提高带载能力。
    • 8 位 DAC 分辨率 1/256,实现约 19.5mV 步进精度,满足通用可调电源需求。
  3. PWM 输出设计
  • 采用 STM32 高级定时器 TIM1 生成硬件 PWM,PA8 为 TIM1_CH1 复用功能引脚,硬件输出无软件占用 CPU;
  • 支持频率、占空比独立编程,适配电机驱动、LED 调光、模拟信号输出等场景;
  • PWM 输出引脚增加测试点与限流保护,避免外接负载损坏 IO 口。
  1. 四、原理图与 PCB 设计介绍

    1. 原理图设计

    使用KiCad软件进行原理图绘画

    打版与迭代

    五、软件设计与调试

    1. 开发与调试工具

    • 1.0 版本:首次打版,验证电源架构、DAC / 运放电路、主控电路的可行性;
    • 后续重打原因和优化方向
      • 负电源带载不足:优化 ICL7660 泵电容、布线,增加滤波;
      • 运放失真:优化运放电源滤波、反馈网络,更换高精度运放;
      • 干扰问题:优化地平面、增加磁珠隔离,进一步抑制数字噪声;
    • 开发环境:Keil MDK v5用于代码编写、编译、调试;
    • 调试工具:ST-Link/V2 仿真器,用于程序下载、在线调试;
    • 辅助工具:串口调试助手、示波器(纹波 / 电压测试)、万用表(电压 / 电流测量)。

步进和按键控制逻辑

// 全局参数初始化(删掉无用的增益相关)
DeviceParams dev_params = {
.curr_mode = MODE_POWER,
.curr_adj = ADJ_POWER,
.step_gear = 0,

.power_val = 0.0f,
.freq_val = 1000,
.duty_val = 50.0f,

.power_steps = {0.1f, 0.5f, 1.0f},
.freq_steps = {1,10, 50, 100, 500, 1000, 10000},
.duty_steps = {0.1f, 0.5f, 1.0f, 5.0f, 10.0f}
};

// 只保留:电源、PWM
const char* mode_names[] = {
"",
"Power",
"PWM"
};

const char* adj_names[] = {
"None",
"Voltage",
"Frequency",
"Duty Cycle"
};

// 只保留两种模式的调节项过滤
static AdjTarget Filter_AdjTarget(WorkMode mode, AdjTarget curr_adj) {
switch(mode) {
case MODE_POWER:
return ADJ_POWER;
case MODE_PWM:
if(curr_adj == ADJ_POWER) return ADJ_FREQ;
return curr_adj;
default:
return ADJ_NONE;
}
}

// 删掉增益限制,其余完全不变
static void Limit_Params(void) {
if(dev_params.power_val < -4.4f) dev_params.power_val = -4.4f;
if(dev_params.power_val > 4.4f) dev_params.power_val = 4.4f;

if(dev_params.freq_val > 150000) dev_params.freq_val = 150000;
if(dev_params.freq_val < 1) dev_params.freq_val = 1;

if(dev_params.duty_val < 0.0f) dev_params.duty_val = 0.0f;
if(dev_params.duty_val > 100.0f) dev_params.duty_val = 100.0f;
}

float Get_Curr_Step(void) {
switch(dev_params.curr_adj) {
case ADJ_POWER:
return dev_params.power_steps[dev_params.step_gear % 3];
case ADJ_FREQ:
return (float)dev_params.freq_steps[dev_params.step_gear % 7];
case ADJ_DUTY:
return dev_params.duty_steps[dev_params.step_gear % 5];
case ADJ_NONE:
default:
return 0.0f;
}
}

void App_Init(void) {
KEY_Init();
LCD_Init();
LCD_Fill(0, 0, LCD_W, LCD_H, 0xFFFF);
Power_Init();
PWM_Init(dev_params.freq_val, dev_params.duty_val);

dev_params.curr_adj = Filter_AdjTarget(dev_params.curr_mode, dev_params.curr_adj);
LCD_Display_All();
}

// 按键在 Power / PWM 两个模式之间切换
void Key_Process(void) {
float step = Get_Curr_Step();

if(KEY_Scan(1) == KEY_PRESSED) {
// 只有 1 和 2 两个模式
if(dev_params.curr_mode == MODE_POWER)
dev_params.curr_mode = MODE_PWM;
else
dev_params.curr_mode = MODE_POWER;

dev_params.curr_adj = Filter_AdjTarget(dev_params.curr_mode, dev_params.curr_adj);
dev_params.step_gear = 0;
LCD_Display_All();
delay_ms(100);
}

if(KEY_Scan(2) == KEY_PRESSED) {
AdjTarget next_adj;
switch(dev_params.curr_mode) {
case MODE_POWER:
next_adj = ADJ_POWER;
break;
case MODE_PWM:
next_adj = (dev_params.curr_adj == ADJ_FREQ) ? ADJ_DUTY : ADJ_FREQ;
break;
default:
next_adj = ADJ_NONE;
break;
}
dev_params.curr_adj = next_adj;
dev_params.step_gear = 0;
LCD_Display_All();
delay_ms(100);
}

if(KEY_Scan(3) == KEY_PRESSED) {
dev_params.step_gear++;
LCD_Display_All();
delay_ms(100);
}

if(KEY_Scan(4) == KEY_PRESSED) {
switch(dev_params.curr_adj) {
case ADJ_POWER:
dev_params.power_val += step;
break;
case ADJ_FREQ:
dev_params.freq_val += (u32)step;
if(dev_params.curr_mode == MODE_PWM) {
PWM_Update(dev_params.freq_val, dev_params.duty_val);
}
break;
case ADJ_DUTY:
dev_params.duty_val += step;
if(dev_params.curr_mode == MODE_PWM) {
PWM_Update(dev_params.freq_val, dev_params.duty_val);
}
break;
case ADJ_NONE:
break;
}
Limit_Params();
if(dev_params.curr_mode == MODE_POWER) {
Power_Update();
}
LCD_Display_All();
delay_ms(100);
}

if(KEY_Scan(5) == KEY_PRESSED) {
switch(dev_params.curr_adj) {
case ADJ_POWER:
dev_params.power_val -= step;
break;
case ADJ_FREQ:
if(dev_params.freq_val >= (u32)step) {
dev_params.freq_val -= (u32)step;
if(dev_params.curr_mode == MODE_PWM) {
PWM_Update(dev_params.freq_val, dev_params.duty_val);
}
}
break;
case ADJ_DUTY:
dev_params.duty_val -= step;
if(dev_params.curr_mode == MODE_PWM) {
PWM_Update(dev_params.freq_val, dev_params.duty_val);
}
break;
case ADJ_NONE:
break;
}
Limit_Params();
if(dev_params.curr_mode == MODE_POWER) {
Power_Update();
}
LCD_Display_All();
delay_ms(100);
}
}

PWM初始化


#include "pwm.h"
#include "stm32f10x_tim.h"

// PA8 -> TIM1_CH1(C8T6适配,占空比输出准确+方向正确)
void PWM_Init(u32 freq, float duty) {
GPIO_InitTypeDef gpio;
TIM_TimeBaseInitTypeDef tim;
TIM_OCInitTypeDef oc;

// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1, ENABLE);

// PA8 复用推挽输出
gpio.GPIO_Pin = GPIO_Pin_8;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);

// 动态计算预分频和ARR(解决16位定时器下限+数值准确问题)
u32 psc = 0;
u32 arr = 0;
if(freq <= 1098) {
psc = 7200 - 1; // 预分频7200倍,时钟10KHz(保证低频精度)
arr = 10000 / freq - 1;
} else {
psc = 0;
arr = 72000000 / freq - 1;
}

// 定时器基本配置(保证计数精度)
tim.TIM_Period = arr;
tim.TIM_Prescaler = psc;
tim.TIM_ClockDivision = 0;
tim.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &tim);

// PWM输出配置(核心:极性+占空比计算统一)
// ========== 修复:宏名改为标准的 TIM_OCMode_PWM1 ==========
oc.TIM_OCMode = TIM_OCMode_PWM1; // 标准PWM1模式(修正笔误)
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_OCPolarity = TIM_OCPolarity_High; // 高电平有效(统一逻辑)
// 精准计算占空比:避免浮点精度丢失
oc.TIM_Pulse = (u32)((arr + 1) * duty / 100.0f);
TIM_OC1Init(TIM1, &oc);

// 使能预装载(保证参数更新立即生效)
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1, ENABLE);
TIM_Cmd(TIM1, ENABLE);
TIM_CtrlPWMOutputs(TIM1, ENABLE);
}

void PWM_Update (u32 freq, float duty) {
// 边界保护(严格限制范围)
if (freq < 1) freq = 1;
if (freq > 60000) freq = 60000;
if (duty < 0.0f) duty = 0.0f;
if (duty > 100.0f) duty = 100.0f;

// 重新计算预分频和ARR(保证频率准确)
u32 psc = 0;
u32 arr = 0;
if(freq <= 1098) {
psc = 7200 - 1;
arr = 10000 / freq - 1;
} else {
psc = 0;
arr = 72000000 / freq - 1;
}

// 精准更新参数(避免数值误差)
TIM1->PSC = psc;
TIM_SetAutoreload(TIM1, arr);
// 核心:精准计算占空比,(arr+1) 弥补计数从0开始的误差
TIM_SetCompare1(TIM1, (u32)((arr + 1) * - duty / 100.0f));
}

MCP4725初始化

#include "mcp4725.h"
#include "delay.h"

// I2C位操作
static void I2C_Start(void) {
MCP4725_GPIO_PORT->BSRR = MCP4725_SDA_PIN;
MCP4725_GPIO_PORT->BSRR = MCP4725_SCL_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BRR = MCP4725_SDA_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BRR = MCP4725_SCL_PIN;
}

static void I2C_Stop(void) {
MCP4725_GPIO_PORT->BRR = MCP4725_SDA_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BSRR = MCP4725_SCL_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BSRR = MCP4725_SDA_PIN;
}

static void I2C_WriteByte(uint8_t data) {
for(uint8_t i=0; i<8; i++) {
if(data & 0x80) MCP4725_GPIO_PORT->BSRR = MCP4725_SDA_PIN;
else MCP4725_GPIO_PORT->BRR = MCP4725_SDA_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BSRR = MCP4725_SCL_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BRR = MCP4725_SCL_PIN;
data <<= 1;
}
// 等待ACK
MCP4725_GPIO_PORT->BSRR = MCP4725_SDA_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BSRR = MCP4725_SCL_PIN;
delay_us(1);
MCP4725_GPIO_PORT->BRR = MCP4725_SCL_PIN;
}

void MCP4725_Init(void) {
GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

gpio.GPIO_Pin = MCP4725_SCL_PIN | MCP4725_SDA_PIN;
gpio.GPIO_Mode = GPIO_Mode_Out_OD;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(MCP4725_GPIO_PORT, &gpio);

I2C_Stop(); // 初始化I2C总线
}
void MCP4725_SetVoltage(float display_val)
{
uint16_t dac_val;

if(display_val < -8.8f) display_val = -8.8f;
if(display_val > 8.8f) display_val = 8.8f;

// 精准线性映射:-3.3V~3.3V → 0~4095
dac_val = (uint16_t)((display_val + 4.4f) * 4095.0f /13.2f);

if(dac_val > 4095) dac_val = 4095;
// if(dac_val < 0) dac_val = 0;
// 发送数据
I2C_Start();
I2C_WriteByte(0xC0);
I2C_WriteByte(0x40);
I2C_WriteByte((dac_val >> 4) & 0xFF);
I2C_WriteByte((dac_val << 4) & 0xF0);
I2C_Stop();
delay_us(50);
}

电源调节函数

#include "power.h"
#include "mcp4725.h"
#include <math.h>
#include "app_func.h"
// 运放增益和偏移计算(-5V~+5V 对应 DAC 0~3.3V)
#define POWER_GAIN 0.75f // 运放增益
#define POWER_OFFSET 2.2f // 中点电压(3.3V/2)

void Power_Init(void) {
MCP4725_Init();
Power_Update(); // 初始输出0V
}

void Power_Update(void) {
// 目标输出电压:dev_params.power_val(范围-5V~+5V)
float v_out = dev_params.power_val;

// 反推DAC输出电压:V_dac = (V_out / Gain) + Offset
float v_dac = (v_out / POWER_GAIN) + POWER_OFFSET;

// 边界保护
if(v_dac < -10.0f) v_dac = -10.0f;
if(v_dac > 10.0f) v_dac =10.0f;

// 设置DAC输出
MCP4725_SetVoltage(v_dac);
}

通过5个按键分别控制不同的数据调整

按键1切换模式

按键2切换调节频率还是占空比

按键3在步进切换

按键4和5加减








六、硬件功能展示与说明

1. 功能测试与展示

上电测试 LED3 常亮,万用表测量 +5V/3.3V/-5V 稳定在标称值 系统供电正常,固定电源输出稳定

可调电压测试 按键调节时,万用表测量输出 0~5V 连续可调,步进约 19.5mV DAC 与运放工作正常,可调功能实现

纹波测试 示波器测量 +5V / 可调输出纹波峰峰值≤10mV 线性稳压 + 滤波设计有效,纹波满足要求

带载测试 5V 输出带载 1A 时,电压跌落≤0.05V-5V 带载 100mA 时稳定 电源带载能力符合设计指标

PWM 波形输出测试 示波器接 PA8 引脚,可观测到标准方波,默认频率 1kHz、占空比 50% TIM1 定时器初始化正常,硬件 PWM 输出功能正常

PWM 参数调节测试 按键调节时,PA8 输出 PWM 占空比平滑变化,频率可按需修改 PWM 占空比控制程序运行正常,参数更新实时生效  

2. 功能说明

固定电源:+5V/3.3V/-5V 为系统与外设提供稳定供电,满足数字 / 模拟电路需求;

可调电源:数控 0~5V 连续可调,按键操作简单,可扩展显示、闭环控制;

PWM 波输出:PA8 引脚可输出可编程硬件 PWM 波形,频率与占空比均可独立调节,可用于信号测试、LED 调光、小型驱动等场景;

状态指示:LED 实时反馈供电状态,异常时可设置闪烁报警;

扩展能力:预留 IO 口支持显示、上位机控制、过流保护等功能升级。

 七、设计中遇到的难题与解决方法

1. 负电源(-5V)带载不足、纹波大

问题:ICL7660 输出 -5V 带载后电压跌落,纹波大导致运放异常;

原因:泵电容容量不足、布线不合理,电荷泵效率低;

解决:更换 22μF ESR 泵电容,输出端添加 100μF+0.1μF 滤波电容,优化布线缩短走线。

 2. 运放输出失真、振荡

问题:DAC 输出正常,但运放输出失真、无法满幅输出;

原因:单电源供电、反馈电阻不合理、电源滤波不足;

解决:为运放提供 ±5V 双电源,重新计算反馈电阻设置合理放大倍数,电源引脚添加去耦电容。

3. 数字干扰模拟电路,DAC 输出噪声大

问题:STM32 运行时,DAC 输出有明显噪声,可调电源纹波大;

原因:数字 / 模拟地共地不合理、布线交叉干扰;

解决:4 层板单独划分地层,数字 / 模拟地分区单点共地,数字线远离模拟线,添加磁珠隔离。

4. 线性稳压器发热严重

问题:LM1117-5.0 发热严重,甚至过热保护;

原因:输入输出压差大(12V→5V,压差 7V)、负载电流大;

解决:增加开关电源预降压(12V→7V),减少线性稳压器功耗,添加散热片。

 八、心得体会与建议

通过本次项目设计,我完整完成了从硬件到软件的整个开发过程,收获很多。
在硬件上,我学会了稳压电源、负电源、运放和数模混合电路的设计,也懂得了合理布线、接地和滤波对系统稳定性的重要性。在软件上,掌握了 STM32 的 GPIO、DAC 驱动以及 PA8 的 PWM 波形输出编程,实现了电压调节与 PWM 控制的结合。

调试过程中遇到的纹波、干扰、发热、PWM 无输出等问题,让我学会了耐心排查和实际解决问题,提升了动手和工程实践能力。

同时也发现本项目还有很多可以优化的地方,比如增加电压显示、加入过流保护、优化电源效率等。这次设计让我明白理论必须结合实践,也为今后的电子设计积累了宝贵经验。


 

附件下载
可调电源.zip
代码
ProPrj_可调电源.zip
KiCad软件PCB原理图
团队介绍
个人项目
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号