发作品签到
专业版

#第九届立创电赛#3D打印耗材温湿度监测-干燥组件

工程标签

4.0k
0
0
33

简介

基于ESP32-C3设计的3D打印耗材温湿度监测-干燥组件。使用SHT30温湿度传感器检测耗材干燥箱内温湿度、使用PTC加热器主动干燥。

简介:基于ESP32-C3设计的3D打印耗材温湿度监测-干燥组件。使用SHT30温湿度传感器检测耗材干燥箱内温湿度、使用PTC加热器主动干燥。

开源协议

CC BY-NC-SA 4.0

创建时间:2024-07-16 17:23:43更新时间:2024-07-17 13:47:40

描述

* 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();
      }
    }
  }
}

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

附件

序号文件名称下载次数
1
实机演示240717.mp4
106
2
HeaterControler.zip
118
3
干燥组件_风嘴.SLDPRT
32
4
干燥组件_控制器盖.SLDPRT
16
5
干燥组件_控制器座.SLDPRT
16
6
干燥组件_风嘴.STL
16
7
干燥组件_控制器盖.STL
18
8
干燥组件_控制器座.STL
19
9
Resistance.m
31
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

评论

全部评论(1
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航