温度不再失控:ESP32 恒温系统从 0 到 1

在嵌入式系统中,温度控制是一个非常经典且具有代表性的应用场景,它综合考验了传感器采集、实时控制、外设驱动以及系统调度等多方面能力。

本项目以 ESP32-S2 为核心,构建了一套完整的 恒温自动控制系统:通过 I2C 温度传感器实时采集环境温度,利用 PWM 控制加温电阻的功率输出,并结合旋转编码器实现目标温度的人机交互设定。同时,系统将实时温度与设定温度以曲线和数值形式显示在 LCD 屏上,并通过 RGB 彩灯直观反馈温控状态。

项目描述

IO扩展板上有一处加温电阻,将加热区域用物体(纸巾等)包裹起来,通过电流给电阻加热,并通过温度传感器感知板上温度的变化,测温以及在LCD屏上的温度显示。

要求:使用按键设定目标温度,并且通过程序控制加热功率,使得温度尽快尽量稳定的维持在目标温度。温度偏离设定温度±3°C彩灯变为红色。

(注意,加热电阻满占空比开启后温度较高)

 

根据项目的要求,实现一个恒温自动控制系统。通过电流给IO扩展板上加温电阻加热,并通过温度传感器感知板上温度的变化,并在LCD屏上显示温度及曲线。用旋转编码器设定目标温度,并且通过程序控制加热功率。温度偏离设定温度±3°C彩灯变为红色。

FoBQboa4ERzA5RWKzJb_6eF9Mkza

开发环境:Arduino 1.8.19

编程语言:C

FuJ7CNIH57Gy5G5dW9dReuSnV29-

本次实验的实验框图如上所示,以ESP32为核心,通过各种通讯方式与外设相互通信。其中温度传感器和旋转编码器为输入,LCD、加温电阻和LED为输出。

Fh6O7pQq78ARRl_iMAB06kwDygd4

本次设计的软件流程图如上所述,首先对各种外设进行初始化。通过定时器进行任务调度,包括读取温度,读取输入,根据输入执行相应的温度控制,将波形和数值通过LCD显示,并控制加温电阻和LED。

硬件介绍

ESP32-S2 是一款高度集成、高性价比、低功耗、主打安全的单核 Wi-Fi SoC,具备强大的功能和丰富的 IO 接口。使用乐鑫ESP-IF开发环境,我们可以通过USB对其编程,作为带wifi的MCU单独使用,也可以烧录AT固件,作为WiFi透传模块与RP2040游戏机套件结合使用。


ESP32-S2 WiFi模块是物联网、可穿戴电子设备和智能家居等应用场景的理想选择,另搭配输入控制、输出显示以及传感器感知和控制的套件,使其功能更加完善。

配套的ESP32 S2 开发板除了ESP32wifi模组之外还集成了USB TYPE -C接口,两个按键,一个电源指示灯,一个用户LED灯,2排10pin的排针,将重要IO引出。使用USB供电或通过排针3.3V供电。


本扩展板包含如下功能:

  • 按键、旋转编码器输入 – 以模拟信号的方式

  • 双电位计控制输入 – 以数字信号的方式

  • RGB三色LED显示

  • 1.44寸128*128 LCD,SPI总线访问

  • MMA7660三轴姿态传感器

  • 电阻加热

  • 温度传感器

  • 与ESP32-S2核心模块的接口

Fm2D9uG9ctxBtMjxIbZFGc4tRnhl

实现的功能

1.PWM控加温电阻

加温电阻的原理为,通过V_HEAT控制mos管的开关,从而控制加温电阻的电流的通断。当mos管导通时,电阻由于电流导通发热,温度升高;当模式管关断时,电阻上没有流经电流,电阻的温度受热辐射的影响,温度降低,向环境温度靠近。


V_HEAT的预留接口在J2 MSP430_IF上,需要用杜邦线将其与J6 STEP_IF(NP)的CA_PWD管脚相连接。


当当前温度低于设定温度时,PWM占空比根据温度差设定,加温电阻升温;当当前温度高于环境温度时,PWM占空比为0,相当于接地状态。

int cal_val;if(meai<target){		cal_val = (target-meai)*100;if(cal_val>255)			cal_val = 255;		analogWrite(ca_pwd, cal_val);// digitalWrite(ca_pwd, HIGH);	}else{		analogWrite(ca_pwd, 0);		digitalWrite(ca_pwd, LOW);	}

温度的控制最好通过PID算法进行控制,但是由于环境比较理想,要求的精度也不是很高,这里只用了P进行实现。


2.I2C温度读取温度

首先对测温电路进行研究,观察原理图可以看到,温度测量芯片的通信协议为I2C协议。


根据数据手册可以了解到,由于A0连接GND,所以设备地址为1001000。这也说明同一个I2C总线上最多只能挂载四个温度检测芯片。

DEVICE TWO-WIREADDRESS   A0 PIN CONNECTION
1001000   Ground
1001001   VDD
1001010   SDA
1001011   SCL


之后根据I2C的读写时序就可以进行通信了。其中指针寄存器的定义如下表所示,这里选择默认的温度寄存器。

BIT NO.   Name   Description
Bits 7:2   NA   P2 to P7mustalwaysbe 0 during the write command.
Bits 1:0   Pointer[1:0]   00:Temperature register(default)(R only)01: Configuration register(R/w)
                                    10: TLowregister(R/W)
                                    11: THIGH register(R/W)


这里采用的是12bit编码,最低位的单位是0.0625,即T0的单位是0.0625,T4的单位是1,而所以需要对第二个字节的读数除以256。

float readTemperaturec(){    float temp = 0;    uint16_t t = 0;    Wire.beginTransmission(NTS112_ADDRESS);    Wire.write(0x00);    Wire.endTransmission();    Wire.requestFrom((uint16_t)NTS112_ADDRESS, (uint8_t)2);    t = Wire.read();    temp = (float)t;    t = Wire.read();    temp +=(float)t / 256.00;    Wire.endTransmission();    return temp;}

I2C的实现代码参考群里的一位大佬,由于聊天记录找不到了,无法特别致谢,在此表示抱歉。

 

3.旋转编码器设定温度

首先分析旋转编码器的原理,可以看到,他的输出接的是ESP32的模拟IO口。这里可以根据电阻网络分析是哪一个按键按下。


这里直接用ADC去读取相应的值,然后进行分析。结果如下:

  • 静态误差小于100;

  • 旋转误差大于300;

  • 旋转按键大约1200;

  • 按键大约4900。


4.旋转编码器按键切换调整

由于旋转编码器左旋和右旋的差异很小,尽管可以通过提高ADC的采样频率的方式来具体分析出旋转的方式,但是为了简化开发流程,这里选择使用按键来切换温度的加减调试,而不是通过左旋和右旋。也就是说,在某一个确定的模式下,左旋和右旋的调整结果是一致的,只有当按键按下以后,才会切换调整的方向。

int myinput() {  	int res = 0;//   int ana_t0 = 0;//   int ana_t1 = 0;  	int diff = 0;	ana_t0 = ana_t1;  	ana_t1 = analogRead(A0);  	diff = ana_t0 - ana_t1;  if (diff > 4000)    res = BUTT;  else if (diff > 1000)    res =  PRES;  else if (diff > 300)    res =  REVO;  else if (diff > 300 || (diff < -300 && diff > -1000))    res =  REVO;  else    res =  0;  return res;}

根据观察的规律对ADC的采样值进行判断,差值决定哪个外设有效。旋转编码器的左转和右转实际上波形是不一致的,但是由于我采用的采样率较低,所以没有进行区分。

int meai = 0;	meai = ((int)(mea*100))/100;if(Timer_fun_flag == true){		mea = readTemperaturec();	// read temperature		in1 = myinput();			// input detection	}switch(in1){case BUTT:break;case PRES:			dire_rota = !dire_rota;// target = target_temp;break;case REVO:if(dire_rota)				target_temp = target_temp + 1;else				target_temp = target_temp - 1;if(target_temp > 100)				target_temp = 100;else if(target_temp < 0)				target_temp = 0;break;default:break;	}

通过旋转调整要设定的温度。


5.延时确认温度设置

温度调整完以后,需要确定调整后的温度为目标温度。这里本来想通过按键进行相应的确定,后来将方案更改为延时确定。只要一定时间内温度不再变化,那么当前的调整温度会同步为目标温度。当然也可以采用实时确定的方式,但是我感觉那样不优雅。

if(set_val_flag == true){		set_val_flag = false;if(target_temp_temp != target_temp)			target_temp_temp = target_temp;else			target = target_temp;	}

通过定时器产生flag信号。若当前设定温度和实际设定温度不一致,则更新实际设定温度。

 

6.SPI LCD屏显示温度

LCD的开发花费了很大的功夫,一开始采用的是Adafruit的ST7735的库,但是由于开发板的原理图设计为软件SPI,而软件SPI由于Arduino的开发方式下速度很慢,导致屏幕刷新率低,一直看到屏幕在闪烁,所以之后切换为TFT_eSPI库。


在显示上面,按照题目的要求在LCD上显示温度。为了能够更加好看一点,所以采用了双温度曲线的方案,既显示设定的温度改变曲线和当前设定温度,也显示实际温度曲线和当前的实际温度。

int i = 0;   if(disp_flag == true){      disp_flag = false;  		// update  		temp_real[ARR_LEN-1] = meai;  		temp_set[ARR_LEN-1] = target;  		for(i=0; i<ARR_LEN-1; i++){  			temp_real[i] = temp_real[i+1];  			temp_set[i] = temp_set[i+1];  		}   }		uint16_t transparent = 0;		img.createSprite(128, 128);					// then create the sprite		img.setColorDepth(8);         	// Set colour depth first		img.fillSprite(0);		img.setTextColor(TFT_GREEN);		img.setCursor(5, 0);		img.print(mea);		img.setCursor(45, 0);		img.setTextColor(TFT_MAGENTA);		img.println(target_temp);		img.fillCircle(3, 124-temp_real[0], CIR_RDS, TFT_GREEN);		img.drawCircle(3, 124-temp_set[0], 	CIR_RDS, TFT_MAGENTA);for(i=1; i<ARR_LEN; i++){			img.fillCircle(3+i*SPACED, 124-temp_real[i], CIR_RDS, TFT_GREEN);			img.drawCircle(3+i*SPACED, 124-temp_set[i], 	CIR_RDS, TFT_MAGENTA);			img.drawLine(3+(i-1)*SPACED, 124-temp_real[i-1], 	3+i*SPACED, 124-temp_real[i], TFT_GREEN);			img.drawLine(3+(i-1)*SPACED, 124-temp_set[i-1], 	3+i*SPACED, 124-temp_set[i], TFT_MAGENTA);		}  // Push sprite to TFT screen CGRAM at coordinate x,y (top left corner)  // Specify what colour is to be treated as transparent (black in this example)  img.pushSprite(0, 0);  // Delete Sprite to free memory, creating and deleting takes very little time.  img.deleteSprite();// }

每次显示之前更新最新的数据。实际温度用绿色显示,曲线为绿色实点。设定温度为紫色显示,曲线为紫色虚点。


7.RGB彩灯点亮

RGB的点亮由高低电平驱动。根据原理图可以分析得到,三个灯都是低电平有效的,可以采用低电平的驱动方式。

if(meai-target>3 || target-meai>3){		digitalWrite(led3, LOW);	}else{		digitalWrite(led3, HIGH);	}

当测量温度和设定温度的差距大于三度时,点亮红色LED,否则关闭红色LED。

Fl7ocV-sdZHnpdwy2je9szltQDMi


8.定时器中断

为了实现中断的效果,采用定时器中断的方式。

void Timer_fun(){	Timer_fun_flag = true;
cnt_tim_disp++; cnt_tim_set_val++;if(cnt_tim_disp>=10){ cnt_tim_disp = 0; disp_flag = true; }if(cnt_tim_set_val>=10){ cnt_tim_set_val = 0; set_val_flag = true; }}

定时器的功能只用来计数和生成标志位,最终的功能实现仍由主函数实现。

总结

在设计中遇到的主要问题是LCD的刷新。一开始采用的是Adafruit的ST7735的库,但是由于开发板的原理图设计为软件SPI,而软件SPI由于Arduino的开发方式下速度很慢,导致屏幕刷新率低,一直看到屏幕在闪烁,所以之后切换为TFT_eSPI库。

 

在本次设计过程中,存在着一些妥协,很多功能由于时间关系没有实现的特别完美,是后续的改善方向。

  • 本来希望用esp的工具进行开发的,但是在VSCODE中安装时产生了错误,多次尝试后均无果,最后只能选择Arduino的开发方式。有机会还是希望用原生的工具进行开发;

  • 温度控制如果用PID可能曲线和精度会更加理想,需要进行进一步的开发;

  • 旋转编码器的左旋右旋设定为一样的功能,可以通过提高采样精度来区分;

  • 后续找相关示例,做一个开机动画的,将开机动画融入到主代码中。

图片

点击阅读原文查看项目

发表评论