#第九届立创电赛#3D打印耗材温湿度监测-干燥组件
简介
基于ESP32-C3设计的3D打印耗材温湿度监测-干燥组件。使用SHT30温湿度传感器检测耗材干燥箱内温湿度、使用PTC加热器主动干燥。
简介:基于ESP32-C3设计的3D打印耗材温湿度监测-干燥组件。使用SHT30温湿度传感器检测耗材干燥箱内温湿度、使用PTC加热器主动干燥。开源协议
:CC BY-NC-SA 4.0
描述
* 1、项目功能介绍
想必耗材受潮导致的拉丝一定困扰着每一位3D打印爱好者,热床上的炒面和盘丝洞总能引发人们的沉思。
干燥箱和温湿度计已经成为每一位3D打印爱好者必备的道具。
图1 再也不会笑了.jpg(一堆温湿度计)
传统的干燥剂+密封箱组合的干燥箱只能维持耗材的受潮程度,并不能使耗材表现变好,随着时间的推移耗材依旧会出现质量变差的情况。
使用具有主动干燥(加热)功能的干燥箱能够降低耗材的受潮情况,在一定程度上能够改善打印拉丝、缺陷问题。
某宝上有许多具有主动功能的干燥箱,但每每看到图片下的价格,总会使我沉思,我在想,3D打印爱好者的钱是不是大风刮来的?
虽然与打印机和高质量耗材的价格相比,这样的定价并不算昂贵,但简单的加热元件、控制器及外壳能卖到如此高昂的价格实在让我难以接受。(觉得贵是我的问题,私密马赛)
一番思想斗争后还是认为不能给厂商捐款,决定自己动手。
也有部分DIY方案采用果蔬烘干器进行改造,但是220V的供电以及不能原生匹配耗材让我望而却步。
本干燥组件在常用的5L米桶干燥箱基础上进行附加,做小幅度改造以获得温湿度监测+主动干燥的功能。
以下是验证后实物展示:
图2 实物展示图1
图3 实物展示图2
图4 控制模块PCB展示
*2、项目属性
项目首次公开;项目为原创;项目未曾在其他比赛中获奖;项目未在学校参加过答辩。
* 3、开源协议
CC BY-NC-SA 4.0
*4、硬件部分
- 4.1 基本特性
有效输入电压:10-20V(10V为软件设定欠压保护下限,可更改;20V为PTC元件电气性能限制)
干燥温度范围:0-255℃(由软件设置;回答我,你不会真的加热到255℃,对吧?)
设计最大功率:Type-C输入 - 60W;DC输入 - 80W(可以通过软件设置限制输出功率)
人机交互:0.96 OLED显示、EC11编码器交互
- 4.2 系统结构
本组件主要由MCU、电源部分、主动干燥部分、温湿度监测以及人机交互部分构成。
系统结构图如下所示:
图5 系统结构图
- 4.3 硬件设计
· MCU采用ESP32-C3-MINI-1-N4模组,自带USB支持JTAG,用过都说好!不是第一次在项目中使用改模组,但每一次在项目中使用总能学习到新的内容。
片上有12位ADC,用于PT1000和输入电压的采样(飘也是真的飘,不校准完全不能用)。
改模组具备WIFI功能,后续可以考虑接入物联网等。
其余功能随后续内容描述。
图6 MCU部分原理图
· 电源部分,输入采用DC插口+Type-C的方案;主供电VBUS直接供给PTC加热器、风扇;主供电经过DC-DC降压获得3.3V电压用于ESP32-C3、OLED等的供电。
DC插口与VBUS间接入肖特基二极管SS54,做防反接、防倒灌;接入TVS,防止输入电压大于20V损坏PTC。Type-C后接CH224K,用于PD诱骗,Rset设置为NC,诱骗20V电压。
DC-DC采用Buck拓扑、使用MP2359芯片控制(杰盛微可白嫖&Pin2Pin芯片很多)。其中,FB电压为0.8V,通过电阻分压得到。
分压电阻使用Matlab编写遗传算法程序,带入电压条件迭代求得,理论误差为0V。
图7 FB电阻计算
但注意输出电压可能不完全等于3.3V,并且后续ADC校准需要视情况更改校准参数。(本项目测试中实际电压为3.345V,由LCSC530+万用表测量得到)
图8 电源部分原理图
· 主动干燥部分由PTC加热控制、风扇控制和PT1000测量组成。
PTC加热控制、风扇控制采用NMOS开关电路实现。风扇电流较小,使用AO3400 NMOS;PTC所需电流较大,但也仅有数A,一般DFN-8(5x6)封装的NMOS都能满足条件。
PT1000测量则是与1k电阻构成分压电路,测量PT1000与1k电阻中间点的电压。(懒了,不想加运放做量程变换,直接软件算吧)
图9 主动干燥部分原理图
· 温湿度监测部分采用盛思锐SHT30温湿度传感器,以I2C总线进行数据交互。
· 人机交互部分由0.96" OLED及其驱动电路、EC11编码器组成。
本项目OLED选型为SH1106 128x64 0.96" 30Pin I2C协议OLED。一般30Pin 0.96" OLED均兼容;EC11编码器柄高随意。
SHT30与OLED使用同一个硬件I2C。
在之前的设计当中,分别使用了两对引脚,计划使用两组I2C总线。但在程序编写过程当中,调用库函数难以实现Wire1的使用(对不起,我的代码水平雀食不行),因此改为同一对引脚、同一个硬件I2C总线。
使用软件I2C进行屏幕的调用延迟较大,会在后续的软件部分说明。
图10 温湿度监测部分&人机交互部分原理图
*5、软件部分
- 5.1 软件设计思路
本项目基于Arduino框架开发,开发环境为VSCode PlatformIO。
项目中,将程序设计整体分为以下几个部分:显示(U8g2)、交互输入(Encoder、OneButton)、温湿度传感器(Adafruit_SHT31)、电压采样(analogRead)、风扇及PTC输出控制(analogWrite、PID)。
交互输入通过在主循环中调用库函数实现;显示、数据采样、输出控制则是每隔一定间隔执行一次。
- 5.2 软件设计
· 交互输入实现相对简单,分别通过调用Encoder.h、OneButton.h库函数实现。参照以上库提供的例程实现相应的按键和编码器功能。
· 数据采样及输出控制中包含两个部分,分别为 需要严格保持时间间隔恒定执行 和 时间间隔要求宽松两种。
其中,PT1000的温度采样、PTC输出控制是需要严格保持时间间隔恒定执行的部分。这是因为该部分涉及PID控制,详细PID原理在此处不过多解释。而其他的数据采样和风扇控制不需要严格进行控制。
因此,将上述两种类型分别采用不同方法实现。设置一个定时器(项目中时间为200ms),需要严格时间控制的部分放置于定时器内;同时定义一个中断标志,当发生定时器中断时标志变为true,在主循环中由if判断标志并执行时间要求宽松的部分。
· 显示也属于较为宽松的部分,并且其刷新需要占用较长时间,因此同上将其放置在if判断标志后执行。
起初想将所有函数置于定时器中断服务函数中执行,但是这样导致了报错并循环重启(推测定时器再次发生中断时服务函数还未执行完,但在ESP32-S3的开发中没有遇到这样的情况)。因此,将函数分为时间严格、时间宽松两部分分别执行。
- 5.3 交互逻辑
交互部分包含OLED的显示和EC11编码器的输入。
· 菜单逻辑
本项目中菜单分为两级,第一级菜单用于显示当前系统的情况(温湿度、输入电压、系统状态);第二级菜单用于设置主动干燥时的参数(加热温度、加热时间、风扇及PTC的功率限制)。
程序设计中,使用一个bool型变量判断菜单的层级;分别进入两级菜单,print对应的项目并请求对应的数值,使用String()转化为字符串后以“+”连接一次性进行绘制输出。
二级菜单需要对具体的项目进行编辑,多了一个选中状态的选项,因此增加一个unsigned char型变量用于存储选中目标序号,范围为0-3。当目标序号=项目序号时,在该行末端输出一个“*”以作为选中标记。
在二级菜单的程序编写中,使用了结构体,大大简化了编写程序的复杂程度(一级菜单部分凑合用,就懒得改了),在结构体中存储项目名称、单位、数值和调整刻度。
使用for循环配合结构体print即可大大压缩显示使用的程序内容。
程序如下所示:
struct element{
char* name;
char* unit;
unsigned char value;
unsigned char scale;
};
element Setting_Element[] = {
{"加热温度: ","℃",60,1},
{"加热时间: ","min",0,1},
{"功率限制: ","",32,8},
{"风扇转速: ","",64,8},
};
菜单显示逻辑如下所示:
图11 菜单显示逻辑
菜单显示效果如下所示:
图12 菜单显示效果
· 输入逻辑
输入主要分为编码器输入和按键输入。编码器通过旋转可以增大或减小数值;按键则通过功能复用分别实现选中、切换效果。
OneButton库提供了多种按键状态,本项目中仅使用按键单击和按键长按两种(由于主循环中显示占用时间较长,导致双击识别较不准确,故舍弃)。
Encoder库可以记录当前编码器的位置,通过将当前位置与上一时刻位置进行比较可以判断数值增减(实际使用中还存在一定抖动)。
输入交互逻辑如下图所示:
图13 输入交互逻辑
- 5.4 输出控制
输出控制分为PTC输出和风扇输出两部分。其中,风扇输出简单调用analogWrite即可;PTC输出为了控制输出温度恒定需要采用PID控制。
PID控制采用增量式PID,即在前一时刻控制量基础上调整控制量数值,这样的控制方法代码量比较小(个人感觉)。
PID部分代码如下所示:
// PID
float Kp=0.1, Ti=125, Td=5;
uint16_t P_Term=0, I_Term=0, D_Term=0;
void PidCtrl(){
// PID项计算
P_Term = Error_Temperature[0] - Error_Temperature[1];
I_Term = Error_Temperature[0];
D_Term = Error_Temperature[0] - 2*Error_Temperature[1] + Error_Temperature[2];
OUT_Status = OUT_Status + Kp*( P_Term + Ts/Ti*I_Term + Td/Ts*D_Term );
// 功率限制
if(OUT_Status > Setting_Element[2].value){
OUT_Status = Setting_Element[2].value;
}
analogWrite(OUT,OUT_Status);
// Serial.println(String(OUT_Status)+"*"+String(Error_Temperature[0]));
// 温度误差更新
Error_Temperature[2] = Error_Temperature[1];
Error_Temperature[1] = Error_Temperature[0];
Error_Temperature[0] = Setting_Element[0].value - temperature_PT1000;
}
- 5.5 PT1000测量
PT1000用于测量PTC加热器的温度。与SHT30测温不同,箱内温度在加热后需要一定时间才能上升,具有迟滞性且具有局部温度差异的情况。
若依靠SHT30测量值作为PID控制依据,很可能导致PTC过度加热,使得PTC出口附近温度过高、耗材融化。因此需要额外增加一个温度传感器用于测量PTC温度,使温度控制在合理范围。
PT1000在0℃时阻值为1kΩ,随温度上升增大阻值。但其关系并非线性,因此需要对其进行一定处理。
考虑到加热温度多处于40~80℃,因此本项目抛弃40℃以下、80℃以上的测量准确性,采用局部拟合的方式得到温度与采样值之间的关系。
根据PT1000温度计算公式,建立温度与电压的实际曲线,然后截取40-80℃部分使用Matlab cftool进行一阶线性拟合。拟合效果如下图所示:
图14 PT1000拟合效果
其中绿线(局部一阶拟合)是黑线(一阶拟合函数)的部分,在图中重合。
但由于没有能够进行温度校准的工具,因此无法判断拟合的效果。并且由于ESP32-C3的ADC存在偏移,测量结果必然存在一定误差。
*6、BOM清单
- 6.1 装配明细
表1 装配明细
序号 | 项目 | 数量 | 备注 |
1 | 6CM风扇 | 1 | 厚度不限 |
2 | 6x3磁铁 | 8 | 安装时注意对吸 |
3 | M4x4x5.5土八螺母 | 4 | |
4 | M4x14内六角螺栓 | 4 | |
5 | M3x3x4.5土八螺母 | 4 | |
6 | M3x12铜柱 | 1 | |
7 | M3x8铜柱 | 1 | |
8 | M3x4平头螺丝 | 2 | |
9 | PT1000 | 1 | |
10 | XH2.54-2P端子线 | 1 | |
11 | 四氟管 | 1 | 一小段,外径4mm(玩3D打印的大家都有吧?) |
12 | M3x10螺丝 | 2 | 圆头或平头 |
13 | M3x8螺丝 | 4 | 沉头最佳 |
14 | 铜鼻子 | 2 | 孔内径>3mm |
15 | PTC加热器 | 1 | 12V 50W |
16 | 控制器模块 | 1 | |
17 | 风嘴 | 1 | 打印件,见附件 |
18 | 控制器座 | 1 | 打印件,见附件 |
19 | 控制器盖 | 1 | 打印件,见附件 |
图15 模块组成展示
- 6.2 模块装配
图16 装配爆炸图(部分)
安装时,5L米桶需要进行打孔处理,两个为PTC加热器接线柱、一个为四氟管伸出孔、方孔用于伸出XH座子和端子线。如下图所示:
图17 打孔标记
*7、大赛LOGO验证
图18 大赛Logo验证
* 8、演示您的项目并录制成视频上传
视频见附件。
- 8.1 项目附加说明
在实物验证过程中,发现了部分不合理之处,不推荐直接对该项目进行复刻,建议做一定修改再行复刻。以下是待改进的点:
1.风扇噪声。风扇控制使用analoagWrite,PWM频率较低,在运行时存在较大的噪声,后续尝试改为ESP32的ledc控制。
2.风扇与PT1000连接。当前使用的XH插接位置较差,装配需要镊子辅助,应该将插座留在侧面方便连接;或者增加副板、更换PogoPin实现免插接并降低漏气可能。
3.屏幕刷新率较低。当前使用的是硬件I2C方案,虽然相较于原有的软件I2C有极大的改善,但是刷新时间仍较长,还有很大的改进空间。(在代码是也可以优化,个人软件水平一般,能力有限)
4.温湿度与主动干燥没有联动。当前还没有加入联动,例如设置湿度阈值进行主动干燥。
5.干燥剂放置。原先计划将干燥剂以磁吸方式吸附在风扇另一侧,但实际安装中空间过小,无法放入。
本项目仅抛砖引玉,提供一些设计思路供各位大佬参考。
附录
- 附录1 项目程序
#include
#include
#include
#include
#include "Adafruit_SHT31.h"
#define ENCODER_OPTIMIZE_INTERRUPTS
#include
#include "OneButton.h"
// IO Setting
#define OUT 4
#define FAN 5
#define UIN_IN 0
#define PT1000_IN 1
// Variable Definition
float temperature = 0; // 箱内温度
float humidity = 0; // 箱内湿度
uint16_t UIN = 0; // UIN读数
uint16_t PT1000 = 0; // PT1000读数
float voltage_UIN = 0; // UIN电压
float temperature_PT1000 = 0; // PT1000温度
unsigned char OUT_Status = 0; // PTC输出状态
float Error_Temperature[3] = {0}; // 温度误差队列
const uint16_t Ts = 200; // 采样、上传、控制周期设置
bool FLAG_timIT0 = 0; // timer0中断标志
bool System_Status = 0; // 系统状态 0 - 待机;1 - 加热
bool List_Level = 0; // 菜单层级 0 - 主页;1 - 设置
bool Choose_Status = 0; // 选中状态
bool UIN_Error = 0; // 输入电压过低标志
unsigned char UIN_threshold = 10; // 电压阈值
unsigned char Choose_Targrt = 0; // 选中目标
uint16_t heating_times = 0; // 加热时间计数器
uint16_t heating_sum_time = 0; // 加热计数器最大值
struct element{
char* name;
char* unit;
unsigned char value;
unsigned char scale;
};
element Setting_Element[] = {
{"加热温度: ","℃",60,1},
{"加热时间: ","min",0,1},
{"功率限制: ","",32,8},
{"风扇转速: ","",64,8},
};
// U8G2
#define SDA 7
#define SCL 6
// U8G2_SH1106_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ SCL, /* data=*/ SDA, /* reset=*/ U8X8_PIN_NONE);
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// SHT30
Adafruit_SHT31 sht31 = Adafruit_SHT31();
#define SHT30_SDA 2
#define SHT30_SCL 3
// Encoder
#define KeyA 20
#define KeyB 21
Encoder Enc(KeyB, KeyA);
long oldPosition=-999;
long newPosition=0;
void EncoderRead(){
if(Enc.read() != oldPosition) {
newPosition = Enc.read();
Serial.println(newPosition);
if(List_Level){
if(Choose_Status){
if(newPosition>oldPosition){
Setting_Element[Choose_Targrt].value+=Setting_Element[Choose_Targrt].scale;
}
else{
Setting_Element[Choose_Targrt].value-=Setting_Element[Choose_Targrt].scale;
}
}
else{
if(newPosition>oldPosition){
Choose_Targrt+=1;
}
else{
Choose_Targrt-=1;
}
if(Choose_Targrt>3){
Choose_Targrt=0;
}
}
}
oldPosition=newPosition;
}
}
// OneButton
#define Key1 10
OneButton button(Key1, true);
// 按键单击
void Click1() {
Serial.println("click");
List_Level = !List_Level;
Choose_Status = 0;
Serial.println(List_Level);
}
// 按键长按
void longPressStart() {
Serial.println("longPressStart");
if(!List_Level){
if(1){//voltage_UIN > UIN_threshold){
System_Status = !System_Status;
heating_times = 0;
heating_sum_time = Setting_Element[1].value *300;
}
else{
UIN_Error = 1;
}
}
else{
Choose_Status = !Choose_Status;
}
}
// 数据读取
void dataRead() {
temperature = sht31.readTemperature();
humidity = sht31.readHumidity();
UIN = analogRead(UIN_IN);
voltage_UIN = UIN*0.006046; // 1/4096*3.3*8.5 取决于ESP32-C3系数需要自行校准
// PT1000 = analogRead(PT1000_IN);
}
// PID
float Kp=0.1, Ti=125, Td=5;
uint16_t P_Term=0, I_Term=0, D_Term=0;
void PidCtrl(){
// PID项计算
P_Term = Error_Temperature[0] - Error_Temperature[1];
I_Term = Error_Temperature[0];
D_Term = Error_Temperature[0] - 2*Error_Temperature[1] + Error_Temperature[2];
OUT_Status = OUT_Status + Kp*( P_Term + Ts/Ti*I_Term + Td/Ts*D_Term );
// 功率限制
if(OUT_Status > Setting_Element[2].value){
OUT_Status = Setting_Element[2].value;
}
analogWrite(OUT,OUT_Status);
// Serial.println(String(OUT_Status)+"*"+String(Error_Temperature[0]));
// 温度误差更新
Error_Temperature[2] = Error_Temperature[1];
Error_Temperature[1] = Error_Temperature[0];
Error_Temperature[0] = Setting_Element[0].value - temperature_PT1000;
}
// 定时器设置
hw_timer_t *timer0 = NULL; // 定时器0 采样、上传、控制刷新
// 状态更新
void onTimer0() {
FLAG_timIT0 = 1;
PT1000 = analogRead(PT1000_IN) *0.85+52;
temperature_PT1000 = (PT1000-2070)/3.11;
// Serial.println(temperature_PT1000);
heating_times++;
if(System_Status){
PidCtrl();
if(heating_times >= heating_sum_time){
System_Status = 0;
heating_times = 0;
Setting_Element[2].value = 0;
}
}
// Serial.println(PT1000);
// Serial.println(temperature_PT1000);
}
unsigned char ITtimes=0; // 用于屏幕刷新的计数器
void setup() {
// put your setup code here, to run once:
// IO Setting
pinMode(OUT,OUTPUT);
pinMode(FAN,OUTPUT);
pinMode(PT1000_IN,INPUT);
pinMode(UIN_IN,INPUT);
// Serial
Serial.begin(115200);
// SHT30
Wire.begin(SHT30_SDA,SHT30_SCL);
if (!sht31.begin(0x44)) {
while (1) {
Serial.println("SHT31 sensor not found!");
}
}
// U8G2
u8g2.begin();
u8g2.enableUTF8Print();
u8g2.setFont(u8g2_font_wqy14_t_gb2312a);
button.reset();//清除一下按钮状态机的状态
button.attachClick(Click1);
button.attachLongPressStart(longPressStart);
// 定时器初始化
// 定时器0
timer0 = timerBegin(0,80,true); // 初始化定时器-使用定时器1
timerAttachInterrupt(timer0,onTimer0,true); // 绑定定时器中断服务函数
timerAlarmWrite(timer0,Ts*1000,true); // 设置中断 间隔为采样周期
timerAlarmEnable(timer0); // 启动定时器
}
void loop() {
// put your main code here, to run repeatedly:
EncoderRead();
button.tick();
if(FLAG_timIT0){
FLAG_timIT0 = 0;
dataRead();
if(System_Status){
analogWrite(FAN,Setting_Element[3].value);
}
else{
analogWrite(FAN,0);
analogWrite(OUT,0);
}
ITtimes++;
if(!List_Level){
if(ITtimes >= 4){
ITtimes = 0;
u8g2.clearBuffer();
u8g2.setCursor(0, 15);
u8g2.print("箱内温度: "+String(temperature)+"℃");
u8g2.setCursor(0, 31);
u8g2.print("箱内湿度: "+String(humidity)+"%");
u8g2.setCursor(0, 47);
u8g2.print("输入电压: "+String(voltage_UIN)+"V");
u8g2.setCursor(0, 63);
if(UIN_Error){
u8g2.print("系统状态: 电压过低");
u8g2.drawHLine(0, 63, 128);
}
else{
if(System_Status){
u8g2.print("系统状态: 加热中");
}
else{
u8g2.print("系统状态: 待机");
}
}
u8g2.sendBuffer();
}
}
else{
if(ITtimes >= 1){
ITtimes = 0;
u8g2.clearBuffer();
for(unsigned char i=0; i<=3; i++){
u8g2.setCursor(0, (i+1)*16-1);
u8g2.print(Setting_Element[i].name+String(Setting_Element[i].value)+Setting_Element[i].unit);
if(Choose_Targrt == i){
if(Choose_Status){
u8g2.setCursor(112, (i+1)*16-1);
u8g2.print("*");
}
u8g2.drawHLine(0, (i+1)*16-1, 128);
}
}
u8g2.sendBuffer();
}
}
}
}
评论