一、创意方向介绍
目前,在科技进步日新月异、人民群众精神文化需求突飞猛进的今天,控温这一基础性需求被越来越广泛的要求。不仅在以往普遍需要的生物、医药方向,诸如微生物培养、药物保存等;而且在一些以往被忽略的领域,如黑胶碟片的保存、Homelab中的精密器件、小型原子钟等等。这些需求往往需要一定的温度准确性,却对空间的需求不高。如果购买传统的大型控温柜,不仅成本高昂,并且存在大量的能源和空间浪费。
以根据珀尔贴原理制成的珀尔帖模块作为基础,制成的小型恒温箱可以很好的满足这类需求,并能大幅提升可靠性和易维护性。珀尔帖模块根据输入电压的不同可以精准控制功率及制热或制冷,相比传统的压缩机配合电阻丝的制冷/热方式,可以大幅降低系统复杂性、空间占用。作为半导体电子器件的珀尔帖模块同时还具有压缩机等机械装置不具备的快速响应能力。因而,可以大幅降低对系统控制算法的要求和静态系统功耗,同时,还能增加控温的精准程度。综上,在小型恒温箱上,珀尔帖模块具备软件复杂度低、硬件复杂度低、功耗低、控温精准、体积小等优点。
二、总体设计
2.1 总体框图
总体框图如图2.1,采用上位机和受控端配合的方式。上位机方面,由于控制较为简单,使用QT编写一命令行作为上位机,也可以选用任意网络调试助手。受控端方面,珀尔帖模块功率相对保温箱来说功率较小,因此本项目没有选用连续性较好的恒流驱动方式,而是采用更为简单的继电器控制。通信方面,作为非工业场景,认为电磁干扰可以忽略,采用WIFI作为通信方式,同时采用以太网作为备用。以太网不可用的情况下,也可以更换为拨码开关。
图2.1 总体框图
系统的电源树不单独画图,由一可调电源直接给出两路电压信号。珀尔帖和继电器采取12V供电,由于供电距离约为两米,线缆电阻不可忽略,修正供电电压为13.5V。主控和其他模块接收5V供电。
2.2 器件选型
2.2.1 主控及WIFI模块
该系统对控制算法的响应时间和过充等要求不高,即主控的性能要求可以忽略,为了控制整体系统的复杂度和可靠性,选择集成WIFI通信的MCU。综合考虑价格、功耗和系统复杂度等方面,选择ESP32C6作为系统主控。其具有以下几点优点:
1、系统集成低功耗wifi6通信。
2、价格较为便宜。
3、可以通过Arduino和MicroPython开发,难度较低。
4、具有低功耗外设,如串口等。
2.2.2 珀尔帖模块
珀尔帖模块的要求只有一个,即在较低的价格内尽量寻求最大的功率,选择ATS-TEC40-33-006,价格约158元,功率高达88w。
2.2.3 温度传感器
高精度温度传感器价格普遍较高,降低温度精度要求至0.5℃以下,选择串口通信的SHT30温湿度模块。开发简单,精度约为0.1℃,集成湿度传感,可在零下及湿度较大的场景下工作。与此同时,价格不高,约为25元左右。
2.2.4 以太网(第二控制)
作为WIFI失效后的第二控制单元,需要其尽可能可靠,系统复杂度低。选取CH32V307作为第二控制单元的主控,其片上集成完整的以太网控制器,可以单片实现以太网通信,同时主频及功耗较低。
三、受控端设计
3.1 硬件连接
图3.1为受控端硬件连接图,展现了主控ESP32C6与各主要部件的连接方式及数据流向。
图3.1 受控端硬件连接图
3.2 主控设计
为方便个人开发者复现本项目,主控采用较为简单的Arduino框架开发,采用循环、串口中断和外部中断联合调度。
上电后,会初始化各外设,包含串口用来调试,低功耗串口并打开中断用于接收温度传感器信息,IO输出,用于控制继电器,进而控制珀尔帖模块,及外部中断,用于接收第二控制单元的控制信息。代码如下。
void setup() {
Serial.begin(115200);
Serial.println();
LPUART.begin(115200, SERIAL_8N1, LPUART_RX, LPUART_TX);
// 设置 LPUART 接收中断
LPUART.onReceive(lpuartRxHandler);
// WiFi.mode(WIFI_STA);
// WiFi.setSleep(false); //关闭STA模式下wifi休眠,提高响应速度
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Connected");
Serial.print("IP Address:");
Serial.println(WiFi.localIP());
server.begin(22333); //服务器启动监听端口号22333
pinMode(10, OUTPUT);
pinMode(11, OUTPUT);
pinMode(2, INPUT);
pinMode(3, INPUT);
attachInterrupt(2, callback, CHANGE);
attachInterrupt(3, callback, CHANGE);
}
主循环主要包含两部分内容,一是WIFI通信,二是系统控制。WIFI通信部分首先检查有无客户端连接,如果有,检查客户端是否处于连接状态,没有则关闭该链接。如果有,检查是否有可读的数据,如果有就逐位读取,以当前数据是否是换行判断该条数据是否结束。读取完整条数据后,根据首位分类控制命令,再做相应的解析,并执行相应的控制。系统控制部分,检查温度传感器是否有更新,如果有,解析温度传感器的数值,判断当前温度与设定温度间的关系,并执行相应的控制。代码如下。
void loop() {
if (temp_refresh) {
temp = (lpuart_data[7] - 48) * 10 + lpuart_data[8] - 48 + (lpuart_data[10] - 48) / 10;
Serial.println(temp);
Serial.println(temp_set);
if (sys_switch) {
if (temp < temp_set) {
digitalWrite(10, 0);
digitalWrite(11, 0);
} else if (temp > temp_set) {
digitalWrite(10, 1);
digitalWrite(11, 1);
} else if (temp == temp_set) {
digitalWrite(10, 1);
digitalWrite(11, 0);
}
} else {
digitalWrite(10, 1);
digitalWrite(11, 0);
}
temp_refresh = 0;
}
WiFiClient client = server.available(); //尝试建立客户对象
if (client) //如果当前客户可用
{
if (temp_refresh) {
temp = (lpuart_data[7] - 48) * 10 + lpuart_data[8] - 48 + (lpuart_data[10] - 48) / 10;
Serial.println(temp);
Serial.println(temp_set);
if (sys_switch) {
if (temp < temp_set) {
digitalWrite(10, 0);
digitalWrite(11, 0);
} else if (temp > temp_set) {
digitalWrite(10, 1);
digitalWrite(11, 1);
} else if (temp == temp_set) {
digitalWrite(10, 1);
digitalWrite(11, 0);
}
} else {
digitalWrite(10, 1);
digitalWrite(11, 0);
}
temp_refresh = 0;
}
Serial.println("[Client connected]");
String readBuff;
while (client.connected()) //如果客户端处于连接状态
{
if (temp_refresh) {
temp = (lpuart_data[7] - 48) * 10 + lpuart_data[8] - 48 + (lpuart_data[10] - 48) / 10;
Serial.println(temp);
Serial.println(temp_set);
if (sys_switch) {
if (temp < temp_set) {
digitalWrite(10, 0);
digitalWrite(11, 0);
} else if (temp > temp_set) {
digitalWrite(10, 1);
digitalWrite(11, 1);
} else if (temp == temp_set) {
digitalWrite(10, 1);
digitalWrite(11, 0);
}
} else {
digitalWrite(10, 1);
digitalWrite(11, 0);
}
temp_refresh = 0;
}
if (client.available()) //如果有可读数据
{
char c = client.read(); //读取一个字节
//也可以用readLine()等其他方法
readBuff += c;
if (c == '\n') //接收到回车符
{
client.print("Received: " + readBuff); //向客户端发送
Serial.println("Received: " + readBuff); //从串口打印
if (readBuff[0] == 't') {
temp_set = (readBuff[1] - 48) * 10 + readBuff[2] - 48;
sys_switch = 1;
Serial.print("set on:"); //从串口打印
Serial.print(temp_set); //从串口打印
Serial.print("\n"); //从串口打印
} else if (readBuff[0] == 'f') {
sys_switch = 0;
Serial.println("set off"); //从串口打印
}
readBuff = "";
}
}
}
client.stop(); //结束当前连接:
Serial.println("[Client disconnected]");
}
}
低功耗串口中断的回调函数中,是接收不定长的串口数据,同样以换行作为判断标志,如果有换行就认为数据结束,不再接收,并设定温度传感器数据更新。代码如下。
void IRAM_ATTR lpuartRxHandler() {
if ((cnt > 40) || (cnt < 0)) {
cnt = 0;
// memset(lpuart_data, 0, 20);
}
while (LPUART.available()) {
lpuart_data[cnt] = LPUART.read();
if (lpuart_data[cnt] == '\n') {
lpuart_data[cnt] = '\0';
// Serial.println(lpuart_data);
cnt = 0;
temp_refresh = 1;
// memset(lpuart_data, 0, 20);
break;
}
cnt += 1;
}
}
在外部中断中,首先读取当前IO值,判断是否与上次相同,避免线缆松动导致的误判,然后根据IO设定系统参数。代码如下。
void callback() {
int switch_state = digitalRead(3);
int temp_state = digitalRead(2);
if ((switch_state != switch_last) || (temp_state != temp_last)) {
if (switch_state) {
if (temp_state) {
temp_set = 0;
sys_switch = 1;
} else {
temp_set = 30;
sys_switch = 1;
}
} else {
sys_switch = 0;
}
}
temp_last=temp_state;
switch_last=switch_state;
}
3.3 以太网控制器设计
CH32V307仅能通过官方的SDK进行开发,主要使用接收中断进行系统调度。系统上电之初会对各外设进行初始化,然后进入主循环,保活以太网连接。代码如下。
int main(void)
{
u8 i;
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200); //USART initialize
printf("TcpServer Test\r\n");
printf("SystemClk:%d\r\n", SystemCoreClock);
printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
printf("net version:%x\n", WCHNET_GetVer());
if ( WCHNET_LIB_VER != WCHNET_GetVer()) {
printf("version error.\n");
}
WCHNET_GetMacAddr(MACAddr); //get the chip MAC address
printf("mac addr:");
for(i = 0; i < 6; i++)
printf("%x ",MACAddr[i]);
printf("\n");
TIM2_Init();
i = ETH_LibInit(IPAddr, GWIPAddr, IPMask, MACAddr); //Ethernet library initialize
mStopIfError(i);
if (i == WCHNET_ERR_SUCCESS)
printf("WCHNET_LibInit Success\r\n");
#if KEEPALIVE_ENABLE //Configure keep alive parameters
{
struct _KEEP_CFG cfg;
cfg.KLIdle = 20000;
cfg.KLIntvl = 15000;
cfg.KLCount = 9;
WCHNET_ConfigKeepLive(&cfg);
}
#endif
GPIO_INIT();
Dac_Init();
memset(socket, 0xff, WCHNET_MAX_SOCKET_NUM);
WCHNET_CreateTcpSocketListen(); //Create TCP Socket for Listening
while(1)
{
/*Ethernet library main task function,
* which needs to be called cyclically*/
WCHNET_MainTask();
/*Query the Ethernet global interrupt,
* if there is an interrupt, call the global interrupt handler*/
if(WCHNET_QueryGlobalInt())
{
WCHNET_HandleGlobalInt();
}
}
}
当接收到来自以太网的数据时,会触发接收中断,根据数据来控制CH32执行相应的动作。
void WCHNET_DataLoopback(u8 id)
{
#if 1
u8 i;
u32 len;
u32 endAddr = SocketInf[id].RecvStartPoint + SocketInf[id].RecvBufLen; //Receive buffer end address
if ((SocketInf[id].RecvReadPoint + SocketInf[id].RecvRemLen) > endAddr) { //Calculate the length of the received data
len = endAddr - SocketInf[id].RecvReadPoint;
}
else {
len = SocketInf[id].RecvRemLen;
}
char data[10];
uint8_t control[4];
uint8_t port;
uint16_t port_bit;
uint16_t dac_value;
if ((SocketInf[id].RecvReadPoint + SocketInf[id].RecvRemLen) > endAddr) { //Calculate the length of the received data
len = endAddr - SocketInf[id].RecvReadPoint;
}
else
{
len = SocketInf[id].RecvRemLen;
}
if(len>5){
len=5;
}
memcpy(data,SocketInf[id].RecvReadPoint,len);
control[0]=(int)data[0]-48;
control[1]=(int)data[1]-48;
control[2]=(int)data[2]-48;
control[3]=(int)data[3]-48;
switch(control[0])
{
case 5:
port=control[1]*10+control[2];
port_bit=1<<(port);
GPIO_WriteBit(GPIOA,port_bit,control[3]);
printf("GPIOA.PORT%d\n",port);
break;
case 6:
port=control[1]*10+control[2];
port_bit=1<<(port);
GPIO_WriteBit(GPIOB,port_bit,control[3]);
printf("GPIOB.PORT%d\n",port);
break;
case 7:
port=control[1]*10+control[2];
port_bit=1<<(port);
GPIO_WriteBit(GPIOC,port_bit,control[3]);
printf("GPIOC.PORT%d\n",port);
break;
case 8:
port=control[1]*10+control[2];
port_bit=1<<(port);
GPIO_WriteBit(GPIOD,port_bit,control[3]);
printf("GPIOD.PORT%d\n",port);
break;
case 9:
port=control[1]*10+control[2];
port_bit=1<<(port);
GPIO_WriteBit(GPIOE,port_bit,control[3]);
printf("GPIOE.PORT%d\n",port);
break;
default:
dac_value=control[0]*1000+control[1]*100+control[2]*10+control[3];
printf("dac:%d\n",dac_value);
DAC_SetChannel1Data(DAC_Align_12b_R, dac_value);
break;
}
i = WCHNET_SocketSend(id, (u8 *) SocketInf[id].RecvReadPoint, &len); //send data
if (i == WCHNET_ERR_SUCCESS) {
WCHNET_SocketRecv(id, NULL, &len); //Clear sent data
}
#else
u32 len, totallen;
u8 *p = MyBuf;
len = WCHNET_SocketRecvLen(id, NULL); //query length
printf("Receive Len = %02x\n", len);
totallen = len;
WCHNET_SocketRecv(id, MyBuf, &len); //Read the data of the receive buffer into MyBuf
while(1){
len = totallen;
WCHNET_SocketSend(id, p, &len); //Send the data
totallen -= len; //Subtract the sent length from the total length
p += len; //offset buffer pointer
if(totallen)continue; //If the data is not sent, continue to send
break; //After sending, exit
}
#endif
}
3.4 以太网控制单元的PCB设计
本设计中以CH32V307为基础的以太网控制单元的PCB为自行绘制,其余部分均为购买成品模块后连线。PCB采用4层板设计,内部两层为内电层,即VDD和GND。以太网口注意布线长度差距不要过大。电源部分注意多电源的切换问题。
四、最终效果
经测试,室温22℃下,半小时内可以到达22℃~45℃区间内任意温度,一小时内可以到达20~50区间内任意温度。开环运行时,极限温度约为12-60℃。为确保安全,防止阻燃泡沫箱自燃,未尝试极限温度。成品如图。
五、注意事项及展望
注意事项:
1、请使用阻燃泡沫箱,防止泡沫自燃。
2、注意密封,非密封条件下内外热交换会损耗大量能量。
3、请注意长距离下的导线电阻。
展望:
1、系统复杂度要求不高的场景可以使用恒流驱动,降低输出状态改变时的开关功耗并延长珀尔帖模块的寿命。
2、系统的温度极限受限于珀尔帖模块功率,采用双核或三核可能可以创造零下空间。
3、内部采用陶瓷或合金内胆,做好热隔离,采用水冷等方式,系统的最高温可以在安全的情况下突破60.
4、更换拓展性更好的主控,使系统增加显示及触摸控制部分。