ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于ESP32与GC9A01的智能雪茄柜控制器:PID控湿与3D打印实战

基于ESP32与GC9A01的智能雪茄柜控制器:PID控湿与3D打印实战 1. 项目缘起与整体设计思路雪茄柜这个东西玩过的人都知道温湿度控制是命门。市面上能买到的成品电子雪茄柜动辄大几千控温还行控湿基本靠一个廉价的机械式或半自动加湿模块湿度波动能到±8%RH对于存放中高端雪茄来说这个波动幅度足以让茄衣开裂或者发霉。我手上有一台用了三年的半导体恒温酒柜改的雪茄柜之前一直靠一个外挂的机械湿度计加手动加水海绵维持每天开柜检查一次累不说湿度曲线跟过山车一样。于是就有了这个项目HC-01 Smart Humidor Controller基于ESP32和GC9A01圆形显示屏做一套能自动控湿、本地可视化、还能记录历史数据的雪茄柜控制器。这个项目的核心目标很明确把柜内湿度稳定控制在65%到70%RH之间温度控制在18到20摄氏度同时在一块1.28英寸的圆形IPS屏上实时显示温湿度曲线和当前状态并且支持通过手机网页查看历史数据。选ESP32是因为它自带WiFi和蓝牙双核240MHz主频足够跑FreeRTOS任务调度价格便宜社区资源丰富。选GC9A01是因为它是圆形屏装在柜门或者柜体上比方形屏好看太多而且SPI接口驱动简单240x240分辨率显示曲线和数字都够用。整个系统的架构分三层感知层用SHT31或SHT40温湿度传感器I2C接口精度±1.5%RH比常见的DHT22靠谱得多控制层用ESP32做主控通过MOSFET驱动超声波雾化片或者半导体制冷片进行加湿和除湿交互层就是GC9A01屏幕加一个旋转编码器再加一个Web服务器跑在ESP32上。电源部分用12V/3A适配器输入经过LM2596降压到5V给屏幕和传感器再经过AMS1117降到3.3V给ESP32。为什么不用继电器而用MOSFET因为加湿模块和除湿模块需要PWM调速继电器只能开关频繁启停不仅噪音大寿命也短。MOSFET选IRLZ44N或者AO3400前者适合大电流的半导体制冷片后者适合小电流的雾化片。这里有个坑MOSFET的栅极驱动电压要匹配ESP32的3.3V逻辑电平IRLZ44N是逻辑电平MOSFETVgs(th)在1V到2V之间3.3V驱动完全没问题但如果是IRF540N这种标准MOSFET3.3V下根本导通不了必须加电平转换或者用专用驱动芯片。3D打印在这个项目里扮演了重要角色。屏幕外壳、传感器支架、雾化片固定座、走线槽全部用PETG打印。PETG比PLA耐温耐湿雪茄柜内湿度长期在70%RH左右PLA会缓慢水解变脆PETG则稳定得多。打印参数后面会详细说。2. 核心硬件选型与电路设计细节2.1 主控与屏幕的搭配逻辑ESP32选的是ESP32-WROOM-32E模组4MB Flash不需要PSRAM因为GC9A01的帧缓冲如果不用LVGL的话直接写SPI不需要大内存。如果你打算用LVGL做界面那建议上ESP32-S3 N16R88MB PSRAM可以轻松跑双缓冲。我一开始用的是ESP32-S3核心板在PlatformIO里选板子的时候要注意N16R8对应的是esp32-s3-devkitc-1但需要在platformio.ini里手动指定Flash和PSRAM大小[env:esp32-s3-devkitc-1] platform espressif32 board esp32-s3-devkitc-1 framework arduino board_build.flash_size 16MB board_build.psram_type opi board_build.arduino.memory_type qio_opiGC9A01的驱动芯片是GC9A01ASPI接口最高时钟可以跑到80MHz但实际用40MHz就很稳了。接线方面SCL接GPIO18SDA接GPIO23CS接GPIO15DC接GPIO2RST接GPIO4BLK背光接GPIO32通过一个NPN三极管控制亮度。这里注意GC9A01的逻辑电平是3.3V和ESP32直连没问题但如果你用的是5V的Arduino Nano必须加电平转换模块否则屏幕会花屏甚至烧毁。2.2 温湿度传感器的选择与校准SHT31是I2C接口地址0x44或0x45精度±2%RH重复性±0.25%RH。SHT40是新一代精度±1.5%RH功耗更低但价格贵一点。我两个都试过最终选了SHT40因为它在高湿环境下的漂移更小。SHT31在70%RH以上连续工作三个月后读数会偏高2%到3%需要定期用饱和盐溶液校准。SHT40的漂移大概在1%以内半年校准一次就行。校准方法很简单找一个密封袋里面放饱和氯化钠溶液盐和水按1:3混合有未溶解的盐晶体即可在25摄氏度下袋内湿度稳定在75.3%RH。把传感器放进去远离液面密封4小时读取数值如果偏差超过1.5%就在代码里加偏移量补偿。这个步骤在第一次组装完成后一定要做否则后面控湿全是白搭。2.3 MOSFET驱动电路的设计要点加湿模块我用的是超声波雾化片工作电压24V电流0.5A左右需要MOSFET做低边开关。电路很简单雾化片正极接24V负极接MOSFET漏极源极接地栅极通过一个100欧姆电阻接ESP32的GPIO栅极和源极之间并一个10k下拉电阻防止悬空误导通。这里有个细节雾化片是感性负载关断瞬间会产生反向电动势必须在雾化片两端反并一个肖特基二极管比如1N5819否则MOSFET用不了多久就击穿了。除湿模块我用的是半导体制冷片TEC1-1270612V/6A这个电流比较大不能用AO3400得用IRLZ44N加散热片。而且TEC不能直接PWM调速因为频繁通断会导致热应力疲劳缩短寿命。我的做法是用一个继电器控制TEC的电源通断然后用一个NTC热敏电阻贴在冷端散热片上当温度低于露点温度2度时断开防止结霜。露点温度可以通过SHT40的温湿度读数计算float dewPoint(float temp, float hum) { float a 17.27; float b 237.7; float alpha ((a * temp) / (b temp)) log(hum / 100.0); return (b * alpha) / (a - alpha); }这个公式在0到60摄氏度范围内误差小于0.5度足够用了。2.4 电源与PCB布局的注意事项整个系统有三路电源24V给雾化片12V给TEC和降压模块3.3V给ESP32和传感器。24V和12V从同一个12V/5A适配器出来经过一个MT3608升压到24V给雾化片。这里要注意MT3608的输出电流只有2A雾化片启动瞬间电流可能到1A余量够但发热不小建议加一个小散热片。PCB布局上模拟部分和数字部分要分开SHT40的I2C走线尽量短远离SPI时钟线和MOSFET的开关节点。我第一版PCB把SHT40放在MOSFET旁边结果湿度读数一直偏高5%后来把传感器用排线引出10厘米读数就正常了。地线要单点接地大电流回路和小信号回路分开走最后在电源入口处汇合。3. 软件架构与核心代码实现3.1 Arduino IDE环境搭建与离线包安装Arduino IDE我用的是2.3.2版本ESP32开发板包用2.0.14这个版本比较稳定3.0以上的版本有些库不兼容。安装方法很简单文件-首选项-附加开发板管理器网址填入https://espressif.github.io/arduino-esp32/package_esp32_index.json然后在开发板管理器里搜索esp32安装2.0.14版本。如果网络慢可以下载离线包解压到C:\Users\你的用户名\AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.14目录下。库的依赖有这几个TFT_eSPI用于驱动GC9A01Adafruit_SHT4x用于SHT40ArduinoJson用于Web接口ESPAsyncWebServer用于异步Web服务。TFT_eSPI需要在User_Setup.h里配置GC9A01的引脚和驱动#define GC9A01_DRIVER #define TFT_WIDTH 240 #define TFT_HEIGHT 240 #define TFT_MOSI 23 #define TFT_SCLK 18 #define TFT_CS 15 #define TFT_DC 2 #define TFT_RST 4 #define TFT_BL 32 #define LOAD_GLCD #define SPI_FREQUENCY 40000000这里有个坑TFT_eSPI的User_Setup.h在库目录里每次更新库都会被覆盖建议把配置复制到项目目录下的User_Setup_Select.h里或者用build_flags在PlatformIO里定义。3.2 外部中断读取旋转编码器旋转编码器用EC11A相和B相接GPIO34和GPIO35按键接GPIO39。ESP32的外部中断配置如下volatile int lastEncoded 0; volatile long encoderValue 0; void IRAM_ATTR updateEncoder() { int MSB digitalRead(ENCODER_A); int LSB digitalRead(ENCODER_B); int encoded (MSB 1) | LSB; int sum (lastEncoded 2) | encoded; if(sum 0b1101 || sum 0b0100 || sum 0b0010 || sum 0b1011) encoderValue; if(sum 0b1110 || sum 0b0111 || sum 0b0001 || sum 0b1000) encoderValue--; lastEncoded encoded; } void setup() { pinMode(ENCODER_A, INPUT_PULLUP); pinMode(ENCODER_B, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENCODER_A), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(ENCODER_B), updateEncoder, CHANGE); }注意ESP32的GPIO34到39是输入专用引脚没有内部上拉必须外接10k上拉电阻。另外中断服务程序要加IRAM_ATTR否则可能因为Flash访问导致中断延迟。3.3 PID控湿算法的实现与调参控湿用位置式PID输出是PWM占空比范围0到255。采样周期1秒PID计算周期5秒。参数整定我用的是临界比例度法先把积分和微分置零比例系数从0.1开始增加直到系统出现等幅振荡记录临界比例度Ku和振荡周期Tu然后按Ziegler-Nichols公式计算float Kp 0.6 * Ku; float Ki 2 * Kp / Tu; float Kd Kp * Tu / 8;实际调下来我的柜子体积约50升Kp8.5Ki0.12Kd45湿度能稳定在±1.5%RH以内。这里要注意PID输出要加限幅防止积分饱和。我加了一个抗积分饱和的逻辑当输出达到上限或下限时停止积分累加。float computePID(float setpoint, float input) { float error setpoint - input; integral error * dt; if (output OUT_MAX error 0) integral - error * dt; if (output OUT_MIN error 0) integral - error * dt; float derivative (input - lastInput) / dt; output Kp * error Ki * integral - Kd * derivative; output constrain(output, OUT_MIN, OUT_MAX); lastInput input; return output; }3.4 GC9A01屏幕界面的绘制界面分三个区域顶部显示当前温湿度大字中间显示24小时曲线底部显示PID输出条和WiFi状态。曲线绘制用TFT_eSPI的drawLine每5秒更新一次把240个像素宽度分成144个点每个点代表10分钟。数据存在一个环形缓冲区里#define HISTORY_SIZE 144 float humHistory[HISTORY_SIZE]; int historyIndex 0; void updateHistory(float hum) { humHistory[historyIndex] hum; historyIndex (historyIndex 1) % HISTORY_SIZE; } void drawGraph() { tft.fillRect(0, 80, 240, 100, TFT_BLACK); for (int i 0; i HISTORY_SIZE - 1; i) { int idx1 (historyIndex i) % HISTORY_SIZE; int idx2 (historyIndex i 1) % HISTORY_SIZE; int x1 i * 240 / HISTORY_SIZE; int x2 (i 1) * 240 / HISTORY_SIZE; int y1 180 - (humHistory[idx1] - 40) * 2; int y2 180 - (humHistory[idx2] - 40) * 2; tft.drawLine(x1, y1, x2, y2, TFT_CYAN); } }这里有个性能问题每5秒重绘整个曲线会闪屏。解决办法是用双缓冲或者只更新变化的部分。我用的方法是把曲线画在一个TFT_eSprite里然后一次性推送到屏幕这样就不闪了。ESP32-S3有8MB PSRAM开一个240x100的16位色Sprite才48KB毫无压力。4. 3D打印外壳与结构件设计4.1 屏幕外壳的设计与打印参数屏幕外壳用Fusion 360设计分前后两件前盖开一个直径32.5mm的圆孔正好卡住1.28寸的GC9A01模块。后盖留出Type-C和传感器排线的开口。壁厚2mm内部加加强筋防止装配时变形。打印材料用PETG喷嘴温度240度热床80度层高0.2mm壁厚3层填充20%。这里有个技巧圆形孔容易打印成椭圆因为FDM打印机的XY轴精度和皮带张力有关。我的做法是把孔径设计成32.8mm留0.3mm余量打印出来用圆锉修一下比设计成刚好尺寸再打磨要省事。4.2 传感器支架与雾化片固定座SHT40的支架设计成一个L形用M2螺丝固定在柜体内壁传感器朝下避免水滴直接溅到。雾化片固定座是一个圆环内径20mm外径28mm用硅胶圈密封防止水雾泄漏。这个圆环的打印方向很重要如果平放打印层间结合力弱水压大了会开裂。我试过竖着打印层纹方向垂直于受力方向强度高很多但需要加支撑。最终方案是斜45度打印兼顾强度和支撑量。4.3 走线槽与装配顺序走线槽用简单的U形槽宽8mm深5mm用双面胶固定在柜体内壁。装配顺序是先装传感器支架和雾化片固定座再走线最后装屏幕外壳。注意雾化片的电源线要用硅胶线耐高温耐湿普通PVC线在70%RH环境下两年就会变硬开裂。所有接线端子用JST-XH方便拆卸维护。5. 常见问题与排查技巧实录5.1 屏幕花屏或白屏最常见的原因是SPI频率太高或者接线太长。GC9A01在40MHz下如果杜邦线超过15厘米就会出现花屏。解决办法是把SPI频率降到20MHz或者用排线代替杜邦线。另一个原因是RST引脚没有正确复位可以在setup()里加一个手动复位pinMode(TFT_RST, OUTPUT); digitalWrite(TFT_RST, LOW); delay(10); digitalWrite(TFT_RST, HIGH); delay(120); tft.init();5.2 湿度读数漂移或跳变SHT40的I2C走线如果和SPI时钟线平行走超过5厘米读数会跳变。解决办法是I2C线用双绞线或者加一个I2C缓冲器PCA9515。另外传感器如果靠近雾化片水雾直接喷到传感器上读数会瞬间到95%RH以上然后慢慢降下来。我的做法是在传感器前面加一个防水透气膜比如Gore-Tex膜水雾进不去但水蒸气能进去响应速度会慢一点但读数稳定。5.3 MOSFET发热严重如果MOSFET不加散热片通过1A电流时温度能到80度以上。IRLZ44N的导通电阻Rds(on)是0.022欧姆1A电流下功耗是0.022W理论上不应该热。但如果栅极驱动电压不足3.3VMOSFET工作在线性区Rds(on)会大很多功耗剧增。检查方法是测量栅极电压如果低于3V说明GPIO驱动能力不够需要加一个图腾柱驱动或者用专用栅极驱动芯片。5.4 WiFi连接不稳定ESP32的WiFi天线如果被金属外壳遮挡信号会差很多。我的屏幕外壳是PETG打印的不影响信号但如果你用金属外壳必须把天线露出来或者用外置天线。另外ESP32的WiFi和蓝牙共用射频如果同时开蓝牙和WiFiWiFi吞吐量会下降。我的做法是配网时开蓝牙配网完成后关蓝牙只留WiFi。5.5 PID输出振荡如果湿度在设定值附近来回振荡先检查采样周期是否稳定。如果采样周期抖动大PID计算会不准。解决办法是用millis()做时间戳而不是delay()。另外积分时间Ti如果太小也会振荡适当增大Ti或者加一个死区误差在±0.5%RH以内时不调节。问题现象可能原因排查方法解决方案屏幕花屏SPI频率高/线长降低频率到20MHz用排线缩短走线湿度跳变I2C干扰/水雾直喷示波器看I2C波形双绞线加防水膜MOSFET发热栅极电压不足万用表测Vgs加驱动芯片WiFi断连天线遮挡/蓝牙干扰看RSSI值外置天线关蓝牙PID振荡采样周期抖动/Ti太小串口打印时间戳用millis增大Ti6. 实测数据与长期运行体会这套系统连续跑了六个月中间只重启过两次一次是路由器换密码一次是固件升级。湿度曲线从最初的±5%RH波动调参后稳定在±1.5%RH以内。最热的那几天柜内温度到了21度湿度依然稳在67%RH。功耗方面雾化片平均每天工作2小时TEC每天工作4小时整机月耗电大概3度可以忽略不计。有个细节值得说SHT40在连续工作三个月后读数比校准前偏高了0.8%RH重新用饱和盐溶液校准后恢复正常。所以建议每三个月校准一次校准记录写在EEPROM里掉电不丢。另外GC9A01的背光LED如果长期开最高亮度半年后亮度会衰减10%左右我在代码里把背光PWM限制在80%既够亮又延长寿命。最后分享一个接线小技巧所有接插件用热熔胶固定尤其是雾化片的端子振动环境下容易松脱。热熔胶耐温70度雪茄柜内温度不会超过25度完全没问题。拆的时候用酒精棉签擦一下就能去掉不留残胶。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进