在智能家居和工业控制中,“温度控制”几乎无处不在。从恒温鱼缸、咖啡机,到加热台、烘干箱,温度稳定都是关键。但要做到自动感知 + 智能调节 + 状态显示,并不简单。
本项目基于 ESP32 控制平台,通过 温度采集 + PID 自动调节 + 可视化显示 的组合,实现了一个真正意义上的 恒温自动控制系统。它不仅能自动检测环境温度,还能根据设定值智能控制加热元件,让系统温度始终保持在稳定范围内。简单来说,就是让设备“有脑子”,能自己“判断冷热、控制加热”!
开发板介绍
1)ESP32-S2 WiFi模块简介
ESP32-S2 是一款高度集成、高性价比、低功耗、主打安全的单核 Wi-Fi SoC,具备强大的功能和丰富的 IO 接口。使用乐鑫ESP-IDF开发环境,我们可以通过USB对其编程,作为带wifi的MCU单独使用,也可以烧录AT固件,作为WiFi透传模块与RP2040游戏机套件结合使用。
ESP32-S2 WiFi模块是物联网、可穿戴电子设备和智能家居等应用场景的理想选择,另搭配输入控制、输出显示以及传感器感知和控制的套件,使其功能更加完善。
该模块板载了:
-
ESP32-S2-MINI-1模组; -
这是一款2.4 GHz WiFi 模组; -
内置 ESP32S2 系列芯片,Xtensa® 单核 32 位 LX7 微处理器; -
内置芯片叠封 4 MB flash,可叠封 2 MB PSRAM; -
37 个 GPIO,具有丰富的外设; -
板载 PCB 天线。
配套的ESP32 S2 开发板除了ESP32wifi模组之外还集成了USB TYPE -C接口,两个按键,一个电源指示灯,一个用户LED灯,2排10pin的排针,将重要IO引出。使用USB供电或通过排针3.3V供电。
2)输入、输出扩展板介绍
本扩展板包含如下功能:
-
按键、旋转编码器输入 – 以模拟信号的方式 -
双电位计控制输入 – 以数字信号的方式 -
RGB三色LED显示 -
1.44寸128*128 LCD,SPI总线访问 -
MMA7660三轴姿态传感器 -
电阻加热 -
温度传感器 -
与ESP32-S2核心模块的接口
设计思路
正如扩展板的原理图所示的加热–>控温:首先控制MOS管导通,使得电阻流过电流产生热量,进而使电路板升温,再使用温度传感器将温度值读出,根据当前的温度值不断地进行反馈控制MOS管通断,使电路板温度保持在设定值范围。
在ESP32-S2的扩展板上,电阻最大加热功率P=U²/R = 3.3*3.3/(68//68//68//68) ≈ 0.64W。
根据如上所示的设计思路,结合ESP32-S2芯片特性,绘制出大体上的软件流程:
开发环境准备
1.Platform IO with Visual Studio Code,用于编写代码及下载程序。
代码实现
本次项目使用Arduino框架开发,Arduino上手容易,简单快捷。
综合项目的要求,首先确定项目的具体实现需求:
1)读取温度传感器;
2)驱动屏幕进行显示;
3)产生PWM波形驱动MOS管导通,通过电流给电阻加热;
4)使用编码器/按键进行温度的设置。
首先是温度传感器,型号为NST112-DSTR,这是一款超小封装的高精度低功耗温度传感器,通过I2C接口数字进行访问以获取温度。该传感器的用法也较为简单,只需配置两个寄存器即可。
void NST112_Init(){Wire.begin();//默认引脚加入Wire.beginTransmission(NST112_ADDRESS);Wire.write(SET_ADDR);//进入设置寄存器Wire.write(CFG_CMD_0);//写寄存器1Wire.write(CFG_CMD_1 | (NUM_BIT==13?(1<<4):0));//写寄存器2Wire.endTransmission();//结束}float NST112_Read(){byte MSB;byte LSB;uint16_t value;float temp;Wire.beginTransmission(NST112_ADDRESS);Wire.write(GET_ADDR);//进入温度寄存器Wire.endTransmission();//结束Wire.requestFrom(NST112_ADDRESS,2);while(Wire.available()){MSB = Wire.read();LSB = Wire.read();value = ((MSB<<8)|LSB) >>(NUM_BIT==13?3:4);temp = ((float)value)*VAL_BASE;}return temp ;}
屏幕的驱动IC是ST7735,1.44寸128*128分辨率 ,使用SPI接口驱动屏幕进行显示,为了方便,选用了TFT-espi驱动库,还在屏幕上绘制了模拟指针的仪表盘,这样可以更为直观的看到温度的变化。
// #########################################################################// 在屏幕上绘制模拟仪表 Draw the analogue meter on the screen// #########################################################################void analogMeter(){// Meter outlinetft.fillRect(0, 0, M_SIZE * 239, M_SIZE * 131, TFT_GREY);// tft.fillRect(1, M_SIZE * 3, M_SIZE * 234, M_SIZE * 125, TFT_WHITE);tft.fillRect(1, M_SIZE * 3, M_SIZE * 234, M_SIZE * 125, TFT_BLACK);// tft.setTextColor(TFT_BLACK); // Text colourtft.setTextColor(TFT_WHITE); // Text colour// Draw ticks every 5 degrees from -50 to +50 degrees (100 deg. FSD swing)for (int i = -50; i < 51; i += 5){// Long scale tick lengthint tl = 15;// Coodinates of tick to drawfloat sx = cos((i - 90) * 0.0174532925);float sy = sin((i - 90) * 0.0174532925);uint16_t x0 = sx * (M_SIZE * 100 + tl) + M_SIZE * 120;uint16_t y0 = sy * (M_SIZE * 100 + tl) + M_SIZE * 150;uint16_t x1 = sx * M_SIZE * 100 + M_SIZE * 120;uint16_t y1 = sy * M_SIZE * 100 + M_SIZE * 150;// Coordinates of next tick for zone fillfloat sx2 = cos((i + 5 - 90) * 0.0174532925);float sy2 = sin((i + 5 - 90) * 0.0174532925);int x2 = sx2 * (M_SIZE * 100 + tl) + M_SIZE * 120;int y2 = sy2 * (M_SIZE * 100 + tl) + M_SIZE * 150;int x3 = sx2 * M_SIZE * 100 + M_SIZE * 120;int y3 = sy2 * M_SIZE * 100 + M_SIZE * 150;// Yellow zone limits//if (i >= -50 && i < 0) {// tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_YELLOW);// tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_YELLOW);//}// Green zone limitsif (i >= 0 && i < 25){tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_ORANGE);tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_ORANGE);}// Orange zone limitsif (i >= 25 && i < 50){tft.fillTriangle(x0, y0, x1, y1, x2, y2, TFT_RED);tft.fillTriangle(x1, y1, x2, y2, x3, y3, TFT_RED);}// Short scale tick lengthif (i % 25 != 0)tl = 8;// Recalculate coords incase tick lenght changedx0 = sx * (M_SIZE * 100 + tl) + M_SIZE * 120;y0 = sy * (M_SIZE * 100 + tl) + M_SIZE * 150;x1 = sx * M_SIZE * 100 + M_SIZE * 120;y1 = sy * M_SIZE * 100 + M_SIZE * 150;// Draw tick// tft.drawLine(x0, y0, x1, y1, TFT_BLACK);tft.drawLine(x0, y0, x1, y1, TFT_WHITE);// Check if labels should be drawn, with position tweaksif (i % 25 == 0){// Calculate label positionsx0 = sx * (M_SIZE * 100 + tl + 10) + M_SIZE * 120;y0 = sy * (M_SIZE * 100 + tl + 10) + M_SIZE * 150;switch (i / 25){case -2:tft.drawCentreString("0", x0 + 7, y0 - 6, 1);break;case -1:tft.drawCentreString("25", x0 + 2, y0, 1);break;// case 0: tft.drawCentreString("50", x0, y0, 1); break;case 0:tft.drawCentreString("50", x0, y0 + 2, 1);break;case 1:tft.drawCentreString("75", x0, y0, 1);break;case 2:// tft.drawCentreString("100", x0 - 2, y0 - 4, 1);tft.drawCentreString("100", x0 - 7, y0 - 10, 1);break;}}// Now draw the arc of the scalesx = cos((i + 5 - 90) * 0.0174532925);sy = sin((i + 5 - 90) * 0.0174532925);x0 = sx * M_SIZE * 100 + M_SIZE * 120;y0 = sy * M_SIZE * 100 + M_SIZE * 150;// Draw scale arc, don't draw the last partif (i < 50)// tft.drawLine(x0, y0, x1, y1, TFT_BLACK);tft.drawLine(x0, y0, x1, y1, TFT_WHITE);}// tft.drawString("%RH", M_SIZE * (3 + 230 - 40), M_SIZE * (119 - 20), 2); // Units at bottom righttft.drawString("℃", M_SIZE * (230 - 40), M_SIZE * (119 - 20), 2); // Units at bottom righttft.pushImage((M_SIZE * 120)-16,(M_SIZE * 75)-8,32,32,sheshidu);// tft.drawCentreString("℃", M_SIZE * 120, M_SIZE * 75, 4); // Comment out to avoid font 4// tft.drawRect(1, M_SIZE * 3, M_SIZE * 236, M_SIZE * 126, TFT_BLACK); // Draw bezel linetft.drawRect(1, M_SIZE * 3, M_SIZE * 236, M_SIZE * 126, TFT_WHITE); // Draw bezel lineplotNeedle(28, 0); // Put meter needle at 0}// #########################################################################//更新针位置//这个功能是在针移动时阻塞,时间取决于ms_delay//如果在针扫描区域内绘制文本,则10ms可最大程度地减少针闪烁//如果文本不在扫描区域,则较小的值可以,对于即时移动为零,但看起来不现实...(注: 满刻度偏转的100增量)// #########################################################################void plotNeedle(int value, byte ms_delay){// tft.setTextColor(TFT_BLACK, TFT_WHITE);tft.setTextColor(TFT_WHITE, TFT_BLACK);char buf[8];dtostrf(value, 4, 0, buf);// tft.drawRightString(buf, 33, M_SIZE * (119 - 20), 2);if (value < -10)value = -10; // Limit value to emulate needle end stopsif (value > 110)value = 110;// Move the needle until new value reachedwhile (!(value == old_analog)){if (old_analog < value)old_analog++;elseold_analog--;if (ms_delay == 0)old_analog = value; // Update immediately if delay is 0float sdeg = map(old_analog, -10, 110, -150, -30); // Map value to angle// Calculate tip of needle coordsfloat sx = cos(sdeg * 0.0174532925);float sy = sin(sdeg * 0.0174532925);// Calculate x delta of needle start (does not start at pivot point)float tx = tan((sdeg + 90) * 0.0174532925);// Erase old needle imagetft.drawLine(M_SIZE * (120 + 24 * ltx) - 1, M_SIZE * (150 - 24), osx - 1, osy, TFT_BLACK);tft.drawLine(M_SIZE * (120 + 24 * ltx), M_SIZE * (150 - 24), osx, osy, TFT_BLACK);tft.drawLine(M_SIZE * (120 + 24 * ltx) + 1, M_SIZE * (150 - 24), osx + 1, osy, TFT_BLACK);// Re-plot text under needle// tft.setTextColor(TFT_BLACK, TFT_WHITE);tft.setTextColor(TFT_WHITE, TFT_BLACK);tft.pushImage((M_SIZE * 120)-16,(M_SIZE * 75)-8,32,32,sheshidu);// tft.drawCentreString("℃", M_SIZE * 120, M_SIZE * 75, 4); // // Comment out to avoid font 4// Store new needle end coords for next eraseltx = tx;osx = M_SIZE * (sx * 98 + 120);osy = M_SIZE * (sy * 98 + 150);// Draw the needle in the new postion, magenta makes needle a bit bolder// draws 3 lines to thicken needletft.drawLine(M_SIZE * (120 + 24 * ltx) - 1, M_SIZE * (150 - 24), osx - 1, osy, TFT_RED);tft.drawLine(M_SIZE * (120 + 24 * ltx), M_SIZE * (150 - 24), osx, osy, TFT_MAGENTA);tft.drawLine(M_SIZE * (120 + 24 * ltx) + 1, M_SIZE * (150 - 24), osx + 1, osy, TFT_RED);// Slow needle down slightly as it approaches new postionif (abs(old_analog - value) < 10)ms_delay += ms_delay / 5;// Wait before next updatedelay(ms_delay);}}
产生PWM波形则使用的是ESP32S2微控制器芯片的LEDC功能,LED 控制器 (LEDC) 主要用于控制 LED,也可产生 PWM 信号用于其他设备的控制。LEDC的高速通道模式在硬件中实现,可以自动且无干扰地改变 PWM 占空比,使用起来也是非常方便的。只需要设置LEDC的通道,分辨率,频率,最后将该通道与相应的GPIO端口绑定即可使用。
pinMode(PWM_Pin, OUTPUT);ledcSetup(ledChannel, PWM_freq, resolution);ledcAttachPin(PWM_Pin, ledChannel);
当PWM波形为高电平时,驱动MOS管开启,此时电阻流过电流开始发热,当PWM波形为低电平时,驱动MOS管关闭,此时电阻没有电流不发热。这样,通过控制PWM波形的占空比,就可以控制电阻的平均功率,进而控制发热温度。
在这个过程中,使用经久不衰的PID算法,计算出在设定温度下,相应的PWM波形占空比,并且随着实际温度自动进行调整,最终使其达到设定的温度。
在PID算法中,还采用了积分分离的方式,防止温度超调,但是在最终的测试中,由于温控系统惯性环节较大,仅使用PD两项的系数就能达到较好的目标。
//单闭环位置PID//pid结构—目标-实际float SinglePID_Position(Position_PID *pid, float Target, float Measure){float Err; //误差if (pid == NULL){// SerialBT.println("pid is NULL");return 0.0;}Err = Target - Measure; //目标值减实际值/* 积分分离 */if (abs(Err) > (pid->Integraldead_zone)) //积分绝对值大于 积分盲区{pid->index = 0; //积分指数赋值0}else{pid->index = 1; //积分指数赋值1}pid->Output = pid->Kp * Err + pid->Kd * (Err - pid->Last_Err); //pd输出pid->Integral += pid->Ki * Err * pid->index; //积分累加constrain(pid->Output, pid->OutputMin, pid->OutputMax);pid->Integral = constrain(pid->Integral, pid->I_outputMin, pid->I_outputMax); ////积分限幅pid->Output += pid->Integral; //pid完整的输出pid->Output = constrain(pid->Output, pid->OutputMin, pid->OutputMax); ////输出限幅pid->Last_Err = Err; //保存上一次控制量return pid->Output; //返回pid控制量}
在该套控制系统中,由于温度传感器自动更新的频率为8Hz,为保证系统的一致性,所以PID程序的执行频率也应为8Hz,因此创建一个定时任务,每隔125ms运行一次即可,包含温度读取,PID计算,更新占空比。
void callback2() //回调函数2,读取温度并计算PWM值进行调整{Temp_now = NST112_Read();Temp_OUT_PWM += SinglePID_Position(&Position_PID_Temp, Temp_target, Temp_now);Temp_OUT_PWM = constrain(Temp_OUT_PWM, 0, 4095);if (Heating == 0){Temp_OUT_PWM = 0;tft.setCursor(90, 86, 2);tft.print("Off");}else{ledcWrite(ledChannel, (uint32_t)Temp_OUT_PWM);if (abs(Temp_now - Temp_target) < 1.0F){digitalWrite(LED_R_Pin, 0);tft.setCursor(90, 86, 2);tft.print(" Ok ");}else{digitalWrite(LED_R_Pin, 1);tft.setCursor(90, 86, 2);tft.print(" On");}}sprintf(Serial_buff, "{Temp}%.2f,%.2f\r\n", Temp_target, Temp_now, Temp_OUT_PWM);Serial.print(Serial_buff);sprintf(Serial_buff, "{PWM}%.2f\r\n", Temp_OUT_PWM);Serial.print(Serial_buff);// Serial.print(",");// Serial.println(Heating);tft.setCursor(30, 70, 2);tft.println(Temp_target);// tft.setCursor(84, 20, 2);tft.setCursor(30, 86, 2);tft.println(Temp_now);// tft.setCursor(20, 70, 2);tft.setCursor(30, 102, 2);tft.println(Temp_OUT_PWM);plotNeedle((int)Temp_now, 0); // Put meter needle at 0// tft.print(Heating);}
最后是编码器、按键的部分,由于扩展板电路设计比较奇特,按键和编码器使用的是电阻网络分压的方案,因此使用ADC读取相应引脚的数值,即可判断出是某个按键按下,但是编码器比较特殊,将编码器的AB相及中央按键均接入了该电阻网络,单次的读取并不能体现是否旋转,需要高频率地进行ADC采样,根据结果进行判断正/反转。
下图为采样频率200Hz的ADC采集数据。图1为顺时针旋转,图2为逆时针旋转,图3为编码器中央按键,图4为普通按键。
可以看出来编码器在进行不同方向的旋转时,ADC检测到的电压台阶顺序是不一样的,因此根据这个顺序,在符合电压最低处时判断上一台阶的电压值,即可分析出是哪个方向的旋转。
事先使用手动方式测量出各个操作ADC的测量值,再根据此值判断即可。
//ADC的几个测量值,对应无操作、编码器旋转(3个)、编码器按键按下、普通按键按下。uint16_t ADC_Val[6] = {7120, 6800, 6175, 6490, 5865, 2140};//得到按键的值void key_scan(){ADC0 = analogRead(ADC_Pin);Serial.print("{A}");Serial.println(ADC0);if ((ADC0 > (ADC_Val[0] - ADC_error)) && (ADC0 < (ADC_Val[0] + ADC_error))) //未按下{for (uint8_t i = 0; i < (sizeof(key) / sizeof(KEY)); ++i){key[i].val = 1;}}else if ((ADC0 > (ADC_Val[1] - ADC_error)) && (ADC0 < (ADC_Val[1] + ADC_error))){key[0].val = 0;}else if ((ADC0 > (ADC_Val[2] - ADC_error)) && (ADC0 < (ADC_Val[2] + ADC_error))){key[1].val = 0;}else if ((ADC0 > (ADC_Val[3] - ADC_error)) && (ADC0 < (ADC_Val[3] + ADC_error))){key[2].val = 0;}else if ((ADC0 > (ADC_Val[4] - ADC_error)) && (ADC0 < (ADC_Val[4] + ADC_error))){key[3].val = 0;}else if ((ADC0 > (ADC_Val[5] - ADC_error)) && (ADC0 < (ADC_Val[5] + ADC_error))){key[4].val = 0;}else{}for (uint8_t i = 0; i < (sizeof(key) / sizeof(KEY)); ++i) //变更状态{if (key[i].last_val != key[i].val) //发生改变{key[i].last_val = key[i].val; //更新状态if (key[i].val == LOW){key_msg.id = i;// key_msg.pressed = true;}}}if ((key_msg.id == 0) || (key_msg.id == 1) || (key_msg.id == 2)){if ((key_msg.id == 1)) //转到中间1的时候,判断上一次的值{switch (Encoder_Val_last){case 0: //右转Encoder_Direction = 1;Temp_target += Add_Val;break;case 2: //左转Encoder_Direction = 0;Temp_target -= Add_Val;break;default:break;}}Encoder_Val_last = key_msg.id;key_msg.id = (-1);}}
功能展示
按下编码器中央按键时,切换开启/关闭加热,按下普通按键时,切换需要设置的温度位数(10、1、0.1),顺时针旋转编码器增加设定值,逆时针旋转编码器减少设定值。
当温度达到设定温度的±1℃时(原先要求的是±3℃,这里稍微提高了一下控制精度),亮起红灯,并在屏幕上显示“OK”字样。
项目总结
感觉该项目的主要难点就在编码器/按键方面,通过ADC读值的方式确定按键的状态,这样的好处也很明显,就是只用一个GPIO的端口即可完成多个按键的读取再有就是关于PID的部分,由于温控系统的大惯性环节,当PWM占空比升高后,温度上升并没有那么迅速,因此PID中的微分系数需要比较大,才能使得系统稳定。
完善计划
作为一个基本能够使用的恒温自动控制系统,已基本满足要求,但是还是有许多不足之处可以完善,比如在界面不够精美,按键交互不太完善,抗干扰性能不强等,由于时间关系暂未解决,后续的话有时间再抽空完善一下,这样总体的感觉也会提升。
参考资料
1)Arduino:https://www.arduino.cc/
2)PlatformIO:https://platformio.org/
3)乐鑫信息科技:
https://www.espressif.com.cn/
4)高精度、低功耗数字温度传感器NST112:
https://www.novosns.com/news-center-57
5)积分分离PID控制算法:
https://blog.csdn.net/songyulong8888/article/details/117389191
点击 “阅读原文” 查看项目