
ESP32-S3数字电源
简介
一款基于SC8701 Buck-Boost拓扑、ESP32-S3控制的小型数字电源。
简介:一款基于SC8701 Buck-Boost拓扑、ESP32-S3控制的小型数字电源。开源协议
:CC BY-NC-SA 4.0
(未经作者授权,禁止转载)描述
零、写在前面
作为一名机械大类学生,在项目过程中调试机电设备往往需要使用到电源。但线性电源、开关电源等在不同方面具有限制,难以适应多样的要求。因此,设计一款小巧的数字电源是计划已久,但从大二到大四,因为种种原因迟迟没能抽出时间实现。本次训练营属实是push了自己一下,把挖了好久的史前大坑给填了。
本项目是第一次独立完成所有相关工作,之前在团队和项目当中主要负责机械与硬件部分工作。可以预见的,项目仍存在很多可改进的地方,尤其是在软件部分。欢迎各位提出相关建议,本项目会作为长期项目持续修改更新(若有大迭代会新建项目)。
0.1 实物图片



摘要
对不起发病了,没有摘要。
一、基本特性
1.1 电气特性
- 输入电压: 10V ≤ Uin ≤ 24V (10V - SC8701 PG要求)
- 输出电压: 3V ≤ Uout ≤ 25V (软件限制范围,25V - INA219 VBUS要求)
- 输出电流: Iout ≤ 5A (工程PCB设计限制)
- 采样LSB:1LSB - 10mV(对于电压采样)
1.2 使用特性
- TFT信息显示与按键编码器交互
- JTAG-串口上位机交互
预留蓝牙与WIFI等功能 待开发
1.3 硬件特性
- DC-DC拓扑结构:Buck-Boost
- DC-DC控制器:SC8701QDER
- MCU:ESP32-S3-WROOM-1U-N16R8(前期调试调得高血压,脑子一热直接上满配S3)
- 采样&输出放大器:LM358、LMV321、LM321(稍微有点拉但问题不大)
- 电流采样:INA219(有点问题,下文详说)
二、系统框架
2.1 系统主要框架

2.2 电源网络框架

2.3 面板交互逻辑(对于恒压输出)

2.4 串口交互逻辑(对于恒压输出)

三、设计思路
- 满足常用电压数值的可控输出(3.3V、5V、9V、12V、19V、24V - 因此需满足 3.3V ≤ Uout ≤ 24V )
- 较为可观的输出能力(设计100W)
- 能够使用USB Type-C供电(考虑便携性)
……(考虑挺多的暂时想到这么多)
3.1 DC-DC拓扑结构设计思路
市场上常见的便携数字电源常用Buck拓扑结构,这限制了输出电压不能大于输入电压(比如某原子)。因此,需要使用具有升降压功能的拓扑结构。
我想到的有两种拓扑结构:SEPIC、Buck-Boost。
对比两种拓扑结构优劣我就不写了,网上能找到很多。选择Buck-Boost而不是SEPIC的主要原因是对SEPIC有心理阴影,之前验证过但没有验证成功。
在本次项目设计开始前,我参考了一些开源工程设计了一款基于SC8701的Buck-Boost方案验证版(后续开源)。在验证通过后给我带来一定的信心,开始投入本项目的硬件设计当中。
关于控制器选型,主要考虑的是成本以及资料丰富程度。
成本:额。。。穷
资料:可参考的资料、开源方案、验证版等能够提供参考借鉴,从而降低设计开发遇到的阻力。
在比较考虑后选择了SC8701。
3.2 框架及MCU选型思路
本项目基于Arduino框架进行ESP32-S3的开发。
为什么选择Arduino和ESP32-S3呢?
主要是菜。之前主要做硬件,软件经验相对薄弱,因此挑软柿子上手了。
3.3 器件选型
由于数年的硬件设计经历(白嫖)攒了不少器件,因此选型主要根据我手里有的器件进行设计。
如果有复刻的朋友可以根据情况酌情替换。
3.4 硬件设计思路
我主要将硬件设计划分为控制、电源、接口、交互四个部分。
- 控制:以ESP32-S3-WROOM-1U-N16R8为核心的相关硬件设计,分为MCU模块、MCU外围电路、电压&电流采样、输出控制等。
- 电源:主要分为SC8701为核心的Buck-Boost拓扑结构设计,实现主输入输出的DC-DC变换;供电保障 - 10V、5V、3.3V,采用Buck DC-DC与LDO共同实现。
- 接口:电源输入(DC、Type-C);调试(JTAG Type-C、Uart 排针);主板(main)与交互板(interaction)的连接(采用PCIE x1,以锡手指连接)。
- 交互:显示(TFT ST7735 0.85);按键(轻触开关);编码器(EC11)
并根据以上划分进行模块化设计。
相对比较简单,在这里着重提一下电压采样电路和输出控制电路的设计。
3.4.1 电压采样电路

这里我采用的是片上ADC与运放电路的组合,其中采样运放电路着重做量程标度变换的处理。
ESP32-S3的片上共有2个12bit ADC合计10各通道,参考电压为3.3V,采样值为0~4095。
为了使采样值更为直观且在采样值与实际电压建立更为明确关系,这里需要做标度变换。如下式:
在考虑DC-DC电源输出、控制精度及可行性等,我将标度变换为 04095 - 040.95V,即每LSB对应10mV。
为降低成本,我采用E24系列电阻配置运放放大系数,并考虑运放输入两端阻抗匹配因素进行求解。
我采用遗传算法进行多目标优化求解,以放大系数误差绝对值为主要优化目标、阻抗偏差绝对值为次要优化目标构造适应度函数。得到结果如下:
图9 电压采样电路配置电阻优化求解
- R1 = 18K; R2 = 1.3K; R3 = 16K; R4 = 1.3K
- 放大系数偏差:7.0199e-05
- 输入匹配偏差:0.0195
至此完成电压采样电路的参数设计。(程序见附件)
3.4.2 输出控制电路

我采用电阻网络控制输出电压,控制端输入电压将会与输出电压成一阶线性反比。
图11 电阻网络控制(@BV1oC4y1V7k1)
本来是想使用片上DAC进行输出控制。但是!ESP32-S3并没有DAC(ESP32 → ESP32-S3 反向升级)。
因此改用PWM输出配合二阶低通滤波器模拟DAC进行控制。
反馈电阻网络各组成电阻的阻值共同确定输出电压范围。
如3供电范围分析及器件限制推得:2V ≤ Uout ≤ 26V
同样,以输出电压偏差量绝对值构造适应度函数,并采用遗传算法进行优化求解。得到结果如下:
图12 输出电阻网络配置优化求解
- R1 = 13K; R2 = 1K; R3 = 1.8K
- Uout_max = 25.3911V
- Uout_min = 2.1667V
至此完成输出控制电路的参数设计。(程序见附件)
四、ESP32-S3程序编写
ESP32-S3主要需要实现以下功能:
- 电压电流采样
- 输出电压控制
- 输出控制
- 本机交互
- 上位机交互
4.0 开发环境
本项目程序基于VSCode PlatformIO,Arduino框架开发。
图13 开发环境
4.1 电压电流采样程序
4.1.1 电压采样
对于电压采样较为简单,只需要使用analogread()
函数读取对应ADC引脚的值。
4.1.2 电流采样
电流采样方面需要使用I2C总线与INA219进行通信,在Arduino当中需要调用Wire.h库或其他第三方库(封装好)。
我共尝试了Adafruit_INA219.h库、INA226.h库、Wire.h库直接读取三种方法。但很可惜,读取的数值不太正常,疑似INA219损坏暂时没有实现电流采样功能。
4.2 输出电压控制
输出电压控制也相对简单,只需要使用analogwrite()
函数操控指定IO发出PWM波,范围为0 - 255(够用了,实际操控分度值约为10mV,与检测LSB对应电压相似)。
将设定的目标输出电压UoutGoal按照 图11 中关系换算为PWM控制值,并控制PWM波占空比,即可实现输出电压控制。
4.3 本机交互
本机交互包含了按键交互、编码器交互与屏幕显示。
4.3.1 按键交互
我使用了Onebutton.h库对按键功能进行调用。该库可以设置按键单击、双击、长按等事件。
4.3.2 编码器交互
我使用了Encoder.h库读取编码器数值,通过比较当前数值与上一时刻数值可以判断编码器的左旋与右旋,并对相应数值做出调整。
注意,我尝试将Encoder读取编码器位置的函数放置于定时器事件中执行,但读取信息非常诡异,几乎只增不减。当我将函数放置于void loop()
主循环内执行时,功能与读取信息正常(原因未知)。
4.3.3 屏幕显示
使用TFT_eSPI.h库驱动ST7735屏幕进行显示。
4.4 上位机交互
目前完成了串口通信上位机交互。分为上传数据和接受控制两个部分。
通过Serial.println()
将输出电压信息上传给上位机;通过Serial.read()
读取上位机发出的指令,并作出对应的控制或调整。
4.5 定时器事件
其中,大部分的任务需要定时完成。例如:数据获取、输出控制(PID控制)、串口上传、TFT刷新等。
这里我使用了两个定时器(1,2),其中定时器1用于数据获取、输出控制(PID控制)、串口上传;定时器2用于TFT刷新。
4.6 其他
在编程过程中,不可避免会使用到小数进行运算,这时候需要使用float
等对变量进行定义。
但当我使用float
定义变量时,程序一运行到使用float
变量环境就会重启。改用double
对变量进行定义时则功能正常(原因未知)。
五、调试
来了来了,最高血压的环节他来了。
调试到极其烦躁,本次项目差点让我对ESP系列粉转黑。
5.1 Flash/PSRAM
说实话,我对这玩意挺无感的,到底能带来多少优化和提升我也不知道。
但是!它额外占用IO!
图14 PSRAM管脚对应表[4]
如图13,八线SPI驱动的Flash/PSRAM会额外占用GPIO33-37。
在设计时我使用了其中几根引脚用于驱动ST7735,并且启用了PSRAM功能。
这时候高血压来了,每次启动时就是ST7735 SPI和PSRAM在抢GPIO,能不能正常启动完全看脸,复位一次可能就寄。
后面重读了一遍datasheet发现了该问题,关闭PSRAM功能后解决。(所以说xdm,datasheet全文背诵!)
以上问题主要还是我对于ESP32-S3相关认识还不够清晰,所以希望各位引以为戒,注意datasheet中提到的重要细节。
5.2 ADC校准
当完成主要功能程序的编写后,开始对输出进行测试。这时,我发现输出/输入的电压与万用表、电流表的值不一致。
因此我对测量值与输出值进行比较,得到结果如下:
图15 输出值-测量值比较(校准前)
如上图所示,可以看出实际电压测量值与输出值间两条线斜率不同、高度不同,存在斜率与截距的偏差。
可以很容易看出,误差值主要呈一阶线性分布。
因此,我定义CorrectUout(斜率校准)、UoutOffest(截距校准)对ADC进行校准。
我将测量得到的数值导入到Matlab当中,并将实际控制量、实际测量值做一阶线性拟合。得到:
图16 线性拟合结果
- CorrectUout = 1.0384
- UoutOffest = 50.8339
并将以上结果带入程序中对ADC进行校准。
图17 输出值-测量值比较(校准后)
如上图,可以看出校准后实际控制量与实际测量值一致性良好,偏差量较小,可能由测量误差或控制误差带来。
推测斜率差值来自采样运放电路的放大误差,可能由电阻误差等带来;
截距误差来自片上ADC自身基准漂移(猜测)。
以上完成ADC校准,校准后效果良好。(程序及文档见附件)
5.3 PID参数整定
输出电压准确性、电源纹波等在很大程度上会收到MCU控制的影响,因此PID的参数是否合适会显著影响电源输出质量。
图18 PID参数整定调试
目前PID参数设置还存在一定问题,例如:存在静态误差、超调量较大、振荡等。
因此仍需进一步整定参数。
六、性能测试
6.1 转换率测试
为测试电源性能,我使用电子负载进行测试。
图19 性能测试环境
- 村 · 好 · 仪 - 刷脸从老师那借来的负载 *
输入条件:努比亚GaN氮化镓单口65W充电器 @20V 3.25A max
输入信息:炬为安全充电多功能检测仪(USB电流电压表)
输出信息:电子负载
测试结果如下所示:
表1 测试结果
如上表所示,电源在大电流负载条件下转换率较低,推测MOS/电感内阻较大。
6.2 纹波测试
使用示波器交流耦合档测试,测得以下结果:
图20 纹波测试结果
emmm还行?但总感觉怪怪的。
*以上结果不完全准确,仅供参考
七、电源装配
图21 电源装配图1(使用3mm导热垫将MOS、芯片热量导到外壳)
图22 电源装配图2(增加14x14x5散热片辅助电感、MCU模块散热)
图23 电源装配图3(使用M2.5x6螺丝固定前后面板)
图24 电源装配图4
八、电源效果图
图25 电源效果图1(前面板)
图26 电源效果图2(后面板 - 工程中面板已修正孔位偏差)
图27 电源效果图2(输出状态:等待 | 输出 | 输入错误)
图28 电源效果图4(使用4mm香蕉头连接输出端)
参考文献
- SC8701
[1] 手册:SC8701 https://www.semiee.com/file/SouthChip/SouthChip-SC8701.pdf
[2] 开源工程:SC8701 buck-boost可调DC-DC验证 https://oshwhub.com/8bit_in_1byte/sc8701-ke-diaodc-dc
[3] 开源工程:636 - SC8701自动升降压车充 https://oshwhub.com/LoveTombSeries/LoveTomb636 - ESP32-S3-WROOM-1U-N16R8
[4] 手册:ESP32-S3技术规格书 https://www.espressif.com.cn/sites/default/files/documentation/esp32-s3_datasheet_cn.pdf
[5] 手册:ESP32-S3硬件设计指南 https://www.espressif.com.cn/sites/default/files/documentation/esp32-s3_hardware_design_guidelines_cn.pdf
[6] 手册:ESP32-S3技术参考手册 https://www.espressif.com.cn/sites/default/files/documentation/esp32-s3_technical_reference_manual_cn.pdf
[7] 手册:ESP32-S3-WROOM-1 & ESP32-S3-WROOM-1U 技术规格书 https://www.espressif.com.cn/sites/default/files/documentation/esp32-s3-wroom-1_wroom-1u_datasheet_cn.pdf
[8] 开源工程:ESP32-S3 UNO(我的工程!) https://oshwhub.com/carele/ESP32-S3hu-xin-ban - 数字电源设计
[9] 视频:全开源DIY|手搓1台Mini数控电源(上&下) https://www.bilibili.com/video/BV1oC4y1V7k1
[10] 视频:分析一位网友设计的电源板,ESP32主控,BUCK-BOOST,使用PD快充供电,具备升压和降压功能 https://www.bilibili.com/video/BV1GV411K7bN - TFT屏幕/TFT_eSPI库
[11] 博客:ESP32 Arduino 学习篇(五)TFT_eSPI库 https://blog.csdn.net/DOF526570/article/details/128859819
[12] 博客:TFT_eSPI使用 https://blog.csdn.net/qq_44633275/article/details/129149580
[13] 开源工程:st7789_1.3inch_240x240 https://oshwhub.com/sync.sh/extendboard_st7789_1-3inch_240x240 - INA219(虽然没能驱动)
[14] 博客:INA219电量监测芯片的使用经验和资料及使用步骤详细说明 https://www.elecfans.com/d/1067986.html
[15] 博客:INA219例程,可校准电流值误差(基于stm32) https://blog.csdn.net/m0_46175164/article/details/127013014
[16] 博客:INA219例程 https://blog.csdn.net/jgagdwp/article/details/79470158
[17] 开源项目:Adafruit INA219 Current Sensor Breakout https://learn.adafruit.com/adafruit-ina219-current-sensor-breakout - PWM模拟DAC
[18] 视频:2022年电赛,数模转换器DAC,PWM做DAC DAC8562 DAC8563 TLV5616,TLV5618,TLV5638,2022年大学生电子设计竞赛 https://www.bilibili.com/video/BV1VT411J7ru - EC11/Encoder库
[19] 博客:旋转编码器的工作原理及其与 Arduino 的接口 https://blog.csdn.net/sxstj/article/details/132244808
[20] 博客:玩转电机驱动——电机编码器 https://blog.csdn.net/weixin_43002939/article/details/124751083 - Onebutton库
[21] 博客:ESP32 Arduino(十一) 按键控制库 OneButton https://blog.csdn.net/DOF526570/article/details/128943669
(暂时想到这么多,后续继续更新)
附录
I.源代码(工程见附件压缩包)
#include
#include
#include
#include
#include
#include
#define ENCODER_OPTIMIZE_INTERRUPTS
#include
// 引脚定义
#define CeCtrl 17 // SC8701使能控制引脚 1 无效 - 0 有效
#define FbCtrl 18 // 电压反馈控制网络引脚
#define OutCtrl 13 // 输出MOS控制引脚 1 有效 - 0 无效
#define UinADC 5 // 输入电压采样ADC引脚
#define UoutADC 6 // 输出电压采样ADC引脚
#define UoutpADC 9 // 终端输出电压采样ADC引脚
#define I2C_SDA 11 // I2C SDA
#define I2C_SCL 12 // I2C SCL
#define Key1 48 // 编码器按键
#define KeyA 47 // 编码器A端
#define KeyB 21 // 编码器B端
#define Key2 14 // 输出控制按键
// 变量定义
double UinOffset=137.3825; // 输入采样偏置量
double correctUin=1.0248; // 输入电压采样修正系数
double UoutOffset=50.8339; // 输出采样偏置量
double correctUout=1.0384; // 输出电压采样修正系数
double UoutpOffset=0; // 终端输出采样偏置量
double correctUoutp=0; // 终端输出电压采样修正系数
uint16_t Uin=0; // 当前输入电压采样值0-4096 ~ 0-40.96V
uint16_t Uout=0; // 当前输出电压采样值0-4096 ~ 0-40.96V
uint16_t Uoutp=0; // 当前终端输出电压采样值0-4096 ~ 0-40.96V
uint16_t Iout=0; // 当前输出电流采样值
uint16_t UoutINA219=0; // INA219采样电压值
uint16_t IoutINA219=0; // INA219采样电流值
uint16_t PoutINA219=0; // INA219功率计算值
const unsigned char sampleTimes=4; // 均值采样次数
uint16_t UoutTemp[sampleTimes]; // 输出电压采样队列
uint16_t IoutTemp[sampleTimes]; // 输出电压采样队列
int UoutError0=0; // 当前时刻输出电压误差
int IoutError0=0; // 当前时刻输出电流误差
int UoutError1=0; // 前一时刻输出电压误差
int IoutError1=0; // 前一时刻输出电流误差
int UoutError2=0; // 前一时刻输出电压误差
int IoutError2=0; // 前一时刻输出电流误差
uint16_t UoutGoal=500; // 输出电压目标 - 默认5V
uint16_t IoutLim=5000; // 输出电流限制 - 默认5A
unsigned char UoutCtrlValue; // 输出电压反馈控制量
boolean SelectStatues=0; // 选中状态
boolean OutputStaus=0; // 输出状态标志
boolean InError=0; // 电源输入状态
uint16_t Ts=200; // 采样、上传、控制周期设置
unsigned char FPS=16; // TFT帧率
// TFT
TFT_eSPI tft = TFT_eSPI();
// Encoder
Encoder Enc(KeyA, KeyB);
long oldPosition=-999;
long newPosition=0;
void EncoderRead(){
newPosition = Enc.read();
if(newPosition != oldPosition){
if(SelectStatues){
if(newPosition > oldPosition){
UoutGoal+=50;
}
else{
UoutGoal-=50;
}
}
oldPosition=newPosition;
}
if(UoutGoal>=2500){
UoutGoal=2500;
}
else if(UoutGoal<=300){
UoutGoal=300;
}
}
// INA219
uint8_t addressINA219=0x80; // INA219地址
Adafruit_INA219 INA219(addressINA219);
void dataGetINA219(){
UoutINA219=INA219.getBusVoltage_V()*100;
Iout=INA219.getCurrent_mA()/10;
}
// uint16_t readRegister(uint8_t reg)
// {
// Wire.beginTransmission(addressINA219);
// Wire.write(reg);
// Wire.endTransmission();
// Wire.requestFrom(addressINA219, (uint8_t)2);
// uint16_t value = Wire.read();
// value <<= 8;
// value |= Wire.read();
// return value;
// }
// uint16_t writeRegister(uint8_t reg, uint16_t value)
// {
// Wire.beginTransmission(addressINA219);
// Wire.write(reg);
// Wire.write(value >> 8);
// Wire.write(value & 0xFF);
// return Wire.endTransmission();
// }
// void initINA219(){
// // 寄存器00设置 - 0x2C47 - 0010 1100 0100 0111
// uint16_t Reg00=0x2C47;
// writeRegister(00,Reg00);
// // 寄存器05设置 - 0x4000 - MAX 8A & I_LSB 0.00025A & P_LSB 0.005W
// uint16_t Reg05=0x4000;
// /*
// 00寄存器bit13:设置检测最大检测电压 0=16V,1=32V
// 00寄存器bit11-12:设置总线分流电阻最大的电压
// 00寄存器bit0-2:设置工作模式
// 05寄存器:设置基准值
// */
// }
// void dataGetINA219(){
// UoutINA219=readRegister(0x02)*4/10;
// //PoutINA219=readRegister(0x03);
// //IoutINA219=readRegister(0x04);
// }
// Onebutton
OneButton button1(Key1, true);
OneButton button2(Key2, true);
// 编码器按键单击 - 选中UoutGoal编辑
void button1click1(){
SelectStatues=!SelectStatues;
}
// 编码器按键双击 - UoutGoal自增50mV,超过2500变为300
void button1click2(){
UoutGoal+=50;
if(UoutGoal>=2550){
UoutGoal=300;
}
}
// 编码器长按 - UoutGoal自增10mV,超过2500变为300
void button1LongPress(){
UoutGoal+=10;
if(UoutGoal>=2510){
UoutGoal=300;
}
}
// 按键单机 - 输出开关
void button2click1(){
if(Uin>=950){
InError=0;
OutputStaus=!OutputStaus;
digitalWrite(OutCtrl,OutputStaus);
}
else{
InError=1;
}
}
// 数据刷新
uint16_t UoutTemp_t;
uint16_t IoutTemp_t;
unsigned char times=0; // 采样次数计数器
void dataGet(){
UoutTemp_t=0;
IoutTemp_t=0;
Uin=analogRead(UinADC)*correctUin+UinOffset;
if(Uin<= (UinOffset+1) ){
Uin=0;
}
//Uoutp=analogRead(UoutpADC)*correctUoutp+UoutpOffset;
//dataGetINA219();
//UoutINA219=INA.getBusVoltage();
if(times<=sampleTimes-1){
UoutTemp[times]=uint16_t( analogRead(UoutADC)*correctUout+UoutOffset );
//UoutTemp[times]=INA219.getBusVoltage_V();
//IoutTemp[times]=INA219.getCurrent_mA();
times++;
}
else{
for(unsigned char i=0;i<=sampleTimes-2;i++){
UoutTemp[i]=UoutTemp[i+1];
IoutTemp[i]=IoutTemp[i+1];
}
UoutTemp[sampleTimes-1]=uint16_t( analogRead(UoutADC)*correctUout+UoutOffset );
//IoutTemp[sampleTimes-1]=INA219.getCurrent_mA();
}
for(unsigned i=0;i<=times;i++){
UoutTemp_t+=UoutTemp[i];
IoutTemp_t+=IoutTemp[i];
}
// 记录当前数据
Uout=UoutTemp_t/times;
if(Uout<= (UoutOffset+1) ){
Uout=0;
}
Iout=IoutTemp_t/times;
}
// 数据上传
String message;
void dataUpload(){
message = String(Uin) + "#" + String(Uout) + "#" + String(Iout);
// 格式:0000#0000#0000;输入电压10mV#输出电压10mV#输出电流10mA
Serial.println(message);
}
// 数据接收
String inputString = "";
boolean stringComplete;
void serialEvent() {
while (Serial.available()) {
// get the new byte:
char inChar = (char)Serial.read();
// add it to the inputString:
inputString += inChar;
// if the incoming character is a newline, set a flag so the main loop can
// do something about it:
if (inChar == '\n') {
stringComplete = true;
}
if (stringComplete) {
if(inputString=="CTRL\n"){
button2click1();
}
else{
if(inputString.toInt() >= 300 | inputString.toInt() <= 2500){
UoutGoal=inputString.toInt(); // 串口电压设置
}
}
// clear the string:
inputString = "";
stringComplete = false;
}
}
}
// PID控制
boolean outputMode=0; // 输出模式 0 - 恒压控制;1 - 恒流控制
uint16_t UoutCtrlValueTemp=UoutGoal;
double Kp=0.1, Ti=125, Td=5; // 恒压PID参数设置
double KIp=1, TIi=1, TId=1; // 恒流PID参数设置
int Pterm=0, Itrem=0, Dterm=0;
void PidCtrl(){
// 增量式PID
Pterm=(UoutError0 - UoutError1);
Itrem=UoutError0;
Dterm=( UoutError0 - 2*UoutError1 +UoutError2);
UoutCtrlValueTemp=UoutCtrlValueTemp + Kp*( Pterm + Ts/Ti*Itrem + Td/Ts*Dterm );
if(UoutCtrlValueTemp>2540){
UoutCtrlValueTemp=2540;
}
else if(UoutCtrlValueTemp<220){
UoutCtrlValueTemp=220;
}
// Serial.println(UoutCtrlValueTemp);
UoutCtrlValue=255-(UoutCtrlValueTemp-220)*0.11;
//Serial.println(UoutCtrlValue);
analogWrite(FbCtrl,UoutCtrlValue);
// 记录误差
UoutError2=UoutError1;
IoutError2=IoutError1;
UoutError1=UoutError0;
IoutError1=IoutError0;
UoutError0=UoutGoal-Uout;
IoutError0=IoutLim-Iout;
}
// 定时器设置
hw_timer_t * timer1 = NULL; // 定时器1 采样、上传、控制刷新
hw_timer_t * timer2 = NULL; // 定时器2 屏幕刷新
// 定时器1 采样、上传、控制刷新
void IRAM_ATTR onTimer1(){
dataGet(); // 数据采样
serialEvent(); // 串口接收
if(OutputStaus){
PidCtrl(); // PID控制
}
if(Serial){
//dataUpload(); // 数据上传
Serial.println(Uout);
//Serial.println(Uoutp);
//Serial.println(UoutINA219);
//Serial.println(newPosition);
}
}
// TFT刷新
const unsigned char FirstLinePos=10; // 首行文字行坐标
const unsigned char LineSpace=25; // 文字行间距
void IRAM_ATTR onTimer2(){
//tft.fillScreen(TFT_BLACK); //屏幕全黑
//tft.setTextColor(TFT_WHITE,TFT_BLACK); //将字体颜色设置为白色,背景为黑色,将文本大小倍增设置为1
//tft.setTextSize(2); //字体大小
// tft.setCursor(10, 10, 2); //将“光标”设置在显示器的左上角(0,0),并选择2号字体
// tft.println(times);
tft.fillRect(56, 5, 120, 100, TFT_BLACK); // 数值清除
tft.setTextColor(TFT_WHITE,TFT_BLACK); // 设置文字颜色
// Uin
tft.setCursor(60, FirstLinePos);
if(Uin>=1000){
tft.println(Uin/100);
tft.setTextSize(1);
tft.setCursor(82, FirstLinePos+5);
tft.println(".");
tft.setCursor(87, FirstLinePos+5);
if( (Uin%100) >=10){
tft.println(Uin%100);
}
else{
tft.println("0");
tft.setCursor(93, FirstLinePos+5);
tft.println(Uin%10);
}
tft.setTextSize(2);
tft.setCursor(102, FirstLinePos);
tft.println("V");
}
else{
if(Uin>=100){
tft.println(Uin/100);
}
else{
tft.println("0");
}
tft.setTextSize(1);
tft.setCursor(70, FirstLinePos+5);
tft.println(".");
tft.setCursor(75, FirstLinePos+5);
if( (Uin%100) >=10){
tft.println(Uin%100);
}
else{
tft.println("0");
tft.setCursor(81, FirstLinePos+5);
tft.println(Uin%10);
}
tft.setTextSize(2);
tft.setCursor(90, FirstLinePos);
tft.println("V");
}
// Uout
tft.setCursor(60, FirstLinePos+LineSpace);
if(Uout>=1000){
tft.println(Uout/100);
tft.setTextSize(1);
tft.setCursor(82, FirstLinePos+LineSpace+5);
tft.println(".");
tft.setCursor(87, FirstLinePos+LineSpace+5);
if( (Uout%100) >=10){
tft.println(Uout%100);
}
else{
tft.println("0");
tft.setCursor(93, FirstLinePos+LineSpace+5);
tft.println(Uout%10);
}
tft.setTextSize(2);
tft.setCursor(102, FirstLinePos+LineSpace);
tft.println("V");
}
else{
if(Uout>=100){
tft.println(Uout/100);
}
else{
tft.println("0");
}
tft.setTextSize(1);
tft.setCursor(70, FirstLinePos+LineSpace+5);
tft.println(".");
tft.setCursor(75, FirstLinePos+LineSpace+5);
if( (Uout%100) >=10){
tft.println(Uout%100);
}
else{
tft.println("0");
tft.setCursor(81, FirstLinePos+LineSpace+5);
tft.println(Uout%10);
}
tft.setTextSize(2);
tft.setCursor(90, FirstLinePos+LineSpace);
tft.println("V");
}
// Iout
tft.setCursor(60, FirstLinePos+2*LineSpace);
if(Iout>=1000){
tft.println(Iout/100);
tft.setTextSize(1);
tft.setCursor(82, FirstLinePos+2*LineSpace+5);
tft.println(".");
tft.setCursor(87, FirstLinePos+2*LineSpace+5);
if( (Iout%100) >=10){
tft.println(Iout%100);
}
else{
tft.println("0");
tft.setCursor(93, FirstLinePos+2*LineSpace+5);
tft.println(Iout%10);
}
tft.setTextSize(2);
tft.setCursor(102, FirstLinePos+2*LineSpace);
tft.println("A");
}
else{
if(Iout>=100){
tft.println(Iout/100);
}
else{
tft.println("0");
}
tft.setTextSize(1);
tft.setCursor(70, FirstLinePos+2*LineSpace+5);
tft.println(".");
tft.setCursor(75, FirstLinePos+2*LineSpace+5);
if( (Iout%100) >=10){
tft.println(Iout%100);
}
else{
tft.println("0");
tft.setCursor(81, FirstLinePos+2*LineSpace+5);
tft.println(Iout%10);
}
tft.setTextSize(2);
tft.setCursor(90, FirstLinePos+2*LineSpace);
tft.println("A");
}
// UoutGoal
tft.setCursor(60, FirstLinePos+3*LineSpace);
tft.setTextColor(TFT_ORANGE,TFT_BLACK);
if(UoutGoal>=1000){
tft.println(UoutGoal/100);
tft.setTextSize(1);
tft.setCursor(82, FirstLinePos+3*LineSpace+5);
tft.println(".");
tft.setCursor(87, FirstLinePos+3*LineSpace+5);
if( (UoutGoal%100) >=10){
tft.println(UoutGoal%100);
}
else{
tft.println("0");
tft.setCursor(93, FirstLinePos+3*LineSpace+5);
tft.println(UoutGoal%10);
}
tft.setTextSize(2);
tft.setCursor(102, FirstLinePos+3*LineSpace);
tft.println("V");
}
else{
if(UoutGoal>=100){
tft.println(UoutGoal/100);
}
else{
tft.println("0");
}
tft.setTextSize(1);
tft.setCursor(70, FirstLinePos+3*LineSpace+5);
tft.println(".");
tft.setCursor(75, FirstLinePos+3*LineSpace+5);
if( (UoutGoal%100) >=10){
tft.println(UoutGoal%100);
}
else{
tft.println("0");
tft.setCursor(81, FirstLinePos+3*LineSpace+5);
tft.println(UoutGoal%10);
}
tft.setTextSize(2);
tft.setCursor(90, FirstLinePos+3*LineSpace);
tft.println("V");
}
// OutStatus
tft.fillRect(77, 105, 128, 128, TFT_BLACK); // 输出状态清除
if(InError){
tft.setCursor(78, FirstLinePos+4*LineSpace);
tft.setTextColor(TFT_YELLOW,TFT_BLACK);
tft.println("InEr");
}
else{
if(OutputStaus){
tft.setCursor(83, FirstLinePos+4*LineSpace);
tft.setTextColor(TFT_YELLOW,TFT_BLACK);
tft.println("OUT");
}
else{
tft.setCursor(78, FirstLinePos+4*LineSpace);
tft.setTextColor(TFT_BLUE,TFT_BLACK);
tft.println("WAIT");
}
}
// SelectStatus
if(SelectStatues){
if(UoutGoal/1000){
tft.fillRect(58, FirstLinePos+3*LineSpace+15, 55, 3, TFT_ORANGE);
}
else{
tft.fillRect(58, FirstLinePos+3*LineSpace+15, 45, 3, TFT_ORANGE);
}
}
}
void setup() {
// put your setup code here, to run once:
// 串口初始化
Serial.begin(115200);
delay(500);
// TFT初始化
tft.init(); //初始化
delay(500);
tft.setRotation(1); // 屏幕方向 - 旋转270°
tft.fillScreen(TFT_BLACK); // 屏幕全黑
tft.setTextColor(TFT_WHITE,TFT_BLACK); // 将字体颜色设置为白色,背景为黑色,将文本大小倍增设置为1
tft.setTextSize(2); // 字体大小
tft.setCursor(5, FirstLinePos);
tft.println("Uin");
tft.setCursor(5, FirstLinePos+LineSpace);
tft.println("Uout");
tft.setCursor(5, FirstLinePos+2*LineSpace);
tft.println("Iout");
tft.setCursor(5, FirstLinePos+3*LineSpace);
tft.println("Uset");
tft.setCursor(5, FirstLinePos+4*LineSpace);
tft.println("Status");
// 引脚初始化
pinMode(CeCtrl,OUTPUT); // SC8701使能引脚模式设置 - 输出
digitalWrite(CeCtrl,0); // SC8701使能设置 - 有效
pinMode(OutCtrl,OUTPUT); // 输出控制引脚模式设置 - 输出
digitalWrite(OutCtrl,0); // 默认输出状态 - 不输出
pinMode(FbCtrl,OutCtrl); // 电压反馈控制网络引脚模式设置 - 输出
UoutCtrlValue=255-(UoutGoal-250)*0.1;
// INA219初始化
Wire.begin(I2C_SDA, I2C_SCL);
delay(500);
//initINA219();
Wire.begin();
if (! INA219.begin()){
// while (1){
// Serial.println("could not connect. Fix and Reboot");
// delay(1000);
// }
Serial.println("could not connect. Fix and Reboot");
}
// Onebutton初始化
button1.attachClick(button1click1);
button1.attachDoubleClick(button1click2);
button1.attachLongPressStart(button1LongPress);
button2.attachClick(button2click1);
// 定时器初始化
// 定时器1
timer1 = timerBegin(1,80,true); // 初始化定时器-使用定时器1
timerAttachInterrupt(timer1,onTimer1,true); // 绑定定时器中断服务函数
timerAlarmWrite(timer1,Ts*1000,true); // 设置中断 间隔为采样周期
timerAlarmEnable(timer1); // 启动定时器
// 定时器2
timer2 = timerBegin(2,80,true); // 初始化定时器-使用定时器2
timerAttachInterrupt(timer2,onTimer2,true); // 绑定定时器中断服务函数
timerAlarmWrite(timer2,1000000/FPS,true); // 设置中断
timerAlarmEnable(timer2); // 启动定时器
}
void loop() {
// put your main code here, to run repeatedly:
button1.tick();
button2.tick();
dataGetINA219();
EncoderRead(); // 编码器读取
}
设计图

BOM


评论