小王同学基于ESP32的恒温自动控制系统
2023寒假一起练平台(5)- 小王同学基于ESP32的恒温自动控制系统,使用PID控制算法,控制电阻发热。
标签
嵌入式系统
Arduino
ESP32
软件设计
six
更新2023-03-27
河南科技大学
593

2023寒假一起练平台(5)- 小王同学基于ESP32的恒温自动控制系统

本次选择的是项目4 - 实现一个恒温自动控制系统

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

 

一、开发版介绍

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 Wi­Fi 模组
  • 内置 ESP32­S2 系列芯片,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供电。

Fis76JnDCgZixkiVDU8POkM7Q3P0

 

2、输入、输出扩展板介绍:

本扩展板包含如下功能:

  • 按键、旋转编码器输入 - 以模拟信号的方式
  • 双电位计控制输入 - 以数字信号的方式
  • RGB三色LED显示
  • 1.44寸128*128 LCD,SPI总线访问
  • MMA7660三轴姿态传感器
  • 电阻加热
  • 温度传感器
  • 与ESP32-S2核心模块的接口

FsxLaGm-2BHTVnsw4LMDm9Y7n12R

二、设计思路

    正如扩展板的原理图所示的加热-->控温:首先控制MOS管导通,使得电阻流过电流产生热量,进而使电路板升温,再使用温度传感器将温度值读出,根据当前的温度值不断地进行反馈控制MOS管通断,使电路板温度保持在设定值范围。

     在ESP32-S2的扩展板上,电阻最大加热功率P=U²/R = 3.3*3.3/(68//68//68//68) ≈ 0.64W

    Fghmwfqqs5hGrIDpeHxs1TLd2dR6

根据如上所示的设计思路,结合ESP32-S2芯片特性,绘制出大体上的软件流程:

         FoNQ0rqFdX5tb2slhYzrp_kReiP5

 

三、开发环境准备

1.Platform IO with Visual Studio Code,用于编写代码及下载程序。

2.ComAssistant,用于串口调试

 

四、项目任务的代码实现

      本次项目使用Arduino框架开发,Arduino上手容易,简单快捷。

      综合项目的要求,首先确定项目的具体实现需求:

      1.读取温度传感器

      2.驱动屏幕进行显示

      3.产生PWM波形驱动MOS管导通,通过电流给电阻加热

      4.使用编码器/按键进行温度的设置

 

      首先是温度传感器,型号为NST112-DSTR,这是一款超小封装的高精度低功耗温度传感器,通过I2C接口数字进行访问以获取温度。该传感器的用法也较为简单,只需配置两个寄存器即可。

#include "Arduino.h"
#include "Wire.h"//IIC库

#define NST112_ADDRESS 0x48     //温度传感器地址
#define GET_ADDR 0X00//读寄存器
#define SET_ADDR 0X01//写寄存器
#define VAL_BASE 0.0625//基准读数
#define NUM_BIT 12//数据位数
#define CFG_CMD_0 0b01100000//数据高
#define CFG_CMD_1 0b11100000//数据低//8HZ


void NST112_Init()
{
    Wire.begin();//默认引脚加入

    Wire.beginTransmission(NST112_ADDRESS);
    Wire.write(SET_ADDR);//进入设置寄存器
    Wire.write(CFG_CMD_0);//写寄存器1
    Wire.write(CFG_CMD_1 | (NUM_BIT==13?(1<<4):0));//写寄存器2
    Wire.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 outline
    tft.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 colour
    tft.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 length
        int tl = 15;

        // Coodinates of tick to draw
        float 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 fill
        float 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 limits
        if (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 limits
        if (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 length
        if (i % 25 != 0)
            tl = 8;

        // Recalculate coords incase tick lenght changed
        x0 = 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 tweaks
        if (i % 25 == 0)
        {
            // Calculate label positions
            x0 = 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 scale
        sx = 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 part
        if (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 right
    tft.drawString("℃", M_SIZE * (230 - 40), M_SIZE * (119 - 20), 2); // Units at bottom right
        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
    // tft.drawRect(1, M_SIZE * 3, M_SIZE * 236, M_SIZE * 126, TFT_BLACK);     // Draw bezel line
    tft.drawRect(1, M_SIZE * 3, M_SIZE * 236, M_SIZE * 126, TFT_WHITE); // Draw bezel line

    plotNeedle(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 stops
    if (value > 110)
        value = 110;

    // Move the needle until new value reached
    while (!(value == old_analog))
    {
        if (old_analog < value)
            old_analog++;
        else
            old_analog--;

        if (ms_delay == 0)
            old_analog = value; // Update immediately if delay is 0

        float sdeg = map(old_analog, -10, 110, -150, -30); // Map value to angle
        // Calculate tip of needle coords
        float 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 image
        tft.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 erase
        ltx = 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 needle
        tft.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 postion
        if (abs(old_analog - value) < 10)
            ms_delay += ms_delay / 5;

        // Wait before next update
        delay(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为逆时针旋转,图三为编码器中央按键,图四为普通按键

FpPhPT8SbHhpNX4DWI3dCblX39qW

Fq6sqCKFpBqGNwy8epYvY2Kek_tu

FqaS6yIy77YPFdSQqZfHoSKcqLTf

Fkgc_NElieD0zuSbxeB_M827e5sI

      可以看出来编码器在进行不同方向的旋转时,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”字样。

FpI8rhvriiF__hu6ynnHD3rrGQdmFrtJ-DiHZuZyINHvbbK61-BfpMPF

FqfHn39Qjgld7FsLCRuNXRq1jhHs

 

六、遇到的主要难题及解决方法

      感觉该项目的主要难点就在编码器/按键方面,通过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

附件下载
ESP32S2_Project.rar
包含了所有源码,使用PlatformIo打开即可
团队介绍
一枚普通在校大学生
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号