Управление печкой на ESP32

ESP32

 В статье Список программ для создания римской масляной лампады был описан контролер, реализованный на  Arduino Nano с программой управления и отображения графиков, написанной на Gambas и запускаемой на компьютере, подключенном через последовательный порт. Для реализации дистанционного беспроводного управления я подсоединил к Arduino Nano Raspberry Pi Zero W с запущенной программой, написанной на Gambas, и получил возможность управлять процессом с любого устройства в локальной сети через VNC. Это позволило использовать готовые решения и имеющиеся в наличии устройства и получить результат с наименьшими затратами труда и в кратчайшее время.  Однако, это решение содержало некую избыточность и представлялось слишком эклектичным. Поэтому я попытался получить аналогичный функционал, используя минимум деталей и программ, которые необходимо дополнительно устанавливать на компьютер или телефон. ESP-WROOM-32   по числу ядер и производительности сопоставим с Arduino Nano + Raspberry Pi Zero W, но все реализовано в одном корпусе, в котором еще есть и датчик Холла, который ранее мы использовали отдельно. Сразу замечу, что его использование в данной конструкции - это исключительно реализация идеи минимума компонентов. Учитывая низкую стоимость датчика тока на эффекте Холла и также необходимость мотать катушку электромагнита и печатать для нее крепление, при тиражировании данного устройства я бы не стал на это тратить время.

Силовая часть осталась неизменной и собрана на симисторе BTA24-800B, симисторной  оптопаре MOC3023, а в качестве детектора нуля используются две включенных навстречу транзисторных оптопары TLP521.

triac
triac
ESP32
ESP32
ESP32

Встречал сетования, что с ESP32 неудобно использовать макетные модули. Это не так, если соединить модули не вдоль, а поперек.

ESP32

Схемы собраны на двух платах. Одна управления и индикации и вторая силовая. Катушка наматывается проводом, который выдержит максимальный ток на ферритовый стержень, упирающийся в корпус микросхемы. Черные провода - сеть 220 В. Белые - нагреватель печки. Кроме того, на фотографии виден модуль подключения термопары типа К, выполненный на MAX6675.  Термопары типа К - это  хромель-алюмель, где алюмель обладает магнитными свойствами  и подключается к минусу платы.

esp32

Контролер может использоваться автономно без подключения компьютера или телефона. Ток регулируется положением потенциометра. На экране отображается текущая температура, ток в сантиАмперах и положение потенциометра в условных единицах. 4000 - ток равен нулю, 0 - ток максимальный и зависит от сопротивления нагревательного элемента. Испытывалась с током до 10А, хотя с таким радиатором, возможно, выдержит и больший ток. 4 светодиода внизу отображают режим, выбранный программно на компьютере. При необходимости касанием левой сенсорной кнопки управление передается потенциометру. Поэтому перед переключением убедитесь, что  его положение соответствует вашим ожиданиям. Светодиоды: белый - потенциометр, красный -  движок, аналогичный потенциометру в программе,  зеленый - плавное изменение тока, желтый - ПИД-регулятор. Касание правой кнопки при включении запустит контролер в режиме точки доступа. Режим полностью идентичен режиму работы в локальной сети, однако сигнал больше и дает заметную наводку на наш датчик тока. В этом режиме можно задать адрес локальной сети, который будет записан в EEPROM и будет считан при перезапуске. В процессе работы правая кнопка переключает индикацию. В частности, при старте в локальной сети высвечивается адрес устройства. Касание кнопки переключает на индикацию установленной температуры ПИД-регулятора и показывает скорость изменения температуры в градусах в час.

Программа

sketch:
#define PID_INTEGER
#include "GyverPID.h"
GyverPID regulator(0.1, 0.05, 0.01, 100); // коэф. П, коэф. И, коэф. Д, период дискретизации dt (мс)
#define ZERO_PIN 33 // пин детектора нуля
#define DIMMER_PIN 32 // управляющий пин симистора
hw_timer_t *My_timer = NULL;
int dimmer =9300; // переменная диммера
float ical1=22;
int ical2=35;
float ical1n=0;
float acs;
int t1;//пороговая температура при изменении тока
int v1=-10000;//скорость изменения тока. больше - медленнее. отрицательные значения ток увеличивается
int ti4;//переменная изменения интервала изменения тока
void IRAM_ATTR onTimer(){
digitalWrite(DIMMER_PIN, 1); // включаем симистор
}
void IRAM_ATTR isr() {
digitalWrite(DIMMER_PIN, 0); // выключаем симистор
timerAlarmWrite(My_timer, dimmer, 0);
timerRestart(My_timer);
timerAlarmEnable(My_timer); //Just Enable
//timerRestart(My_timer);
}
// чтение температуры (подключение к SPI)
#include <GyverMAX6675_SPI.h>
// перед подключением библиотеки можно
// задать скорость SPI в Гц (умолч. 1000000 - 1 МГц)
// для увеличения качества связи по длинным проводам
#define MAX6675_SPI_SPEED 300000

// указываем пин CS
// остальные подключены к аппаратному SPI (SCK/CLK(18) и MISO(19))
GyverMAX6675_SPI<5> sens;
#include <GyverOLED.h>
GyverOLED<SSD1306_128x64, OLED_BUFFER> oled;
//GyverOLED<SSD1306_128x64, OLED_NO_BUFFER> oled;
#include <LittleFS.h>
#include <GyverPortal.h>
GyverPortal ui(&LittleFS);
//GyverPortal ui;
#include <EEPROM.h>
struct LoginPass {
char ssid[20];
char pass[20];
};
LoginPass lp;
#define PLOT_SIZE 600
int16_t arr[2][PLOT_SIZE];
int data1 = touchRead(15);
int data3 = touchRead(13);
int tempc = 0;
int tempcold = 0;
int tempcold1 = 0;
int tempcold2 = 0;
int dth = 0;//скорость изменения температуры в градусах в час
int ida = hallRead();
int pda = 0;
int potValue = 0;
int nt=0;
float acsum =0;//сумма измерений тока
float acsumc =0;//сумма измерений тока при калибровке
int i;
int j;
int n=0;
int kk=2000;//количество замеров тока перед осреднением
int valNum;
int flag1=-1;
int flag2=0;
//float valSpin;
int valSlider =4000;
int valRad;
//int valSelect;
const char *names[] = {
"temp", "IсA", "PdA"
};

// конструктор
void build() {
GP.BUILD_BEGIN();
GP.THEME(GP_DARK);
GP.PAGE_TITLE("Управление печкой");
// список имён компонентов на обновление
// GP.UPDATE("t1,lb,lbb,lbbb,num,sld,rad");
GP.UPDATE("t1,lb,lbb,lbbb,num,num1,num2,sld,rad,lbbdt");

GP.TITLE("Печь", "t1");
GP.HR();
GP.LABEL("T PID: ");
GP.NUMBER("num", "", regulator.setpoint); GP.BREAK();
GP.LABEL("i+: ");
GP.NUMBER("num1", "", v1,"30%"); //GP.BREAK();
GP.LABEL("Ti: ");
GP.NUMBER("num2", "", t1,"20%"); GP.BREAK();

GP.RADIO("rad", 0, valRad); GP.LABEL("Pot"); //GP.BREAK();
GP.RADIO("rad", 1, valRad); GP.LABEL("SLIDER"); GP.BREAK();
GP.RADIO("rad", 2, valRad); GP.LABEL("I+"); //GP.BREAK();
GP.RADIO("rad", 3, valRad); GP.LABEL("PID"); GP.BREAK();


GP.LABEL("T: ");
GP.LABEL("NAN", "lb",GP_GREEN,24,0); //GP.BREAK();
GP.LABEL("I(сA): ");
GP.LABEL("NAN", "lbb",GP_GREEN,24); GP.BREAK();
GP.LABEL("dT/h: ");
GP.LABEL("NAN", "lbbdt"); //GP.BREAK();
GP.LABEL("Pot: ");
GP.LABEL("NAN", "lbbb"); GP.BREAK();
GP.SLIDER("sld", valSlider, 0, 4000); GP.BREAK();

// обновление из переменной (храним значение)

GP.AJAX_PLOT("plot3", names, 2, 9000, 6000,400,1); // 15 часов каждые 6 секунд
GP.AJAX_PLOT("plot2",names, 2, 600, 1000,400,1); //число графиков, число точек в графике, интервал между точками в мс
//GP.PLOT_DARK<2, PLOT_SIZE>("plot4", names, arr);
GP.BUTTON("btn", "Калибровка I0");
//GP.EMBED("/test1.txt");
GP.BOX_BEGIN();
GP.BUTTON_MINI("btn2", "Создать");
GP.BUTTON_MINI_DOWNLOAD("/test1.txt", "Скачать");
GP.BOX_END();
// GP.BUTTON_LINK("/test1.txt", "Открыть");
//GP.BUTTON_MINI_DOWNLOAD("/test1.txt", "Скачать");
GP.FORM_BEGIN("/login");
GP.TEXT("lg", "Login", lp.ssid);
GP.BREAK();
GP.TEXT("ps", "Password", lp.pass);
GP.SUBMIT("Submit");
GP.FORM_END();
GP.BUILD_END();
}
// временные переменные:
String valueString = "0";
//int pos1 = 0;
//int pos2 = 0;
TaskHandle_t Task1;
TaskHandle_t Task2;




void setup() {
regulator.setDirection(REVERSE); // направление регулирования (NORMAL/REVERSE). ПО УМОЛЧАНИЮ СТОИТ NORMAL
regulator.setLimits(0, 4024); // пределы (ставим для 8 битного ШИМ). ПО УМОЛЧАНИЮ СТОЯТ 0 И 255
regulator.setpoint = 35; // сообщаем регулятору температуру, которую он должен поддерживать
regulator.Kp = 30;
regulator.Ki = 3;
regulator.Kd = 3;

pinMode(23, OUTPUT);
pinMode(25, OUTPUT);
pinMode(26, OUTPUT);
pinMode(27, OUTPUT);
pinMode(ZERO_PIN, INPUT_PULLUP);
pinMode(DIMMER_PIN, OUTPUT);
//FALLING RISING
attachInterrupt(digitalPinToInterrupt(ZERO_PIN), isr, RISING);
digitalWrite(DIMMER_PIN, 0); // выключаем симистор
My_timer = timerBegin(0, 80, true);
timerAttachInterrupt(My_timer, &onTimer, true);
Serial.begin(115200);
EEPROM.begin(100);
EEPROM.get(0, lp);
Serial.println(lp.ssid);
Serial.println(lp.pass);
oled.init(); // инициализация
if (touchRead(15)<35) startap2();
else startup();

if (!LittleFS.begin()) Serial.println("FS Error");
ui.downloadAuto(true);
// подключаем конструктор и запускаем
ui.attachBuild(build);
ui.attach(action);
ui.start();

// Создаем задачу с кодом из функции Task1code(),
// с приоритетом 1 и выполняемую на ядре 0:
xTaskCreatePinnedToCore(
Task1code, /* Функция задачи */
"Task1", /* Название задачи */
10000, /* Размер стека задачи */
NULL, /* Параметр задачи */
1, /* Приоритет задачи */
&Task1, /* Идентификатор задачи,
чтобы ее можно было отслеживать */
0); /* Ядро для выполнения задачи (0) */
delay(500);

// Создаем задачу с кодом из функции Task2code(),
// с приоритетом 1 и выполняемую на ядре 1:
xTaskCreatePinnedToCore(
Task2code, /* Функция задачи */
"Task2", /* Название задачи */
10000, /* Размер стека задачи */
NULL, /* Параметр задачи */
1, /* Приоритет задачи */
&Task2, /* Идентификатор задачи,
чтобы ее можно было отслеживать */
1); /* Ядро для выполнения задачи (1) */
delay(500);
}
void action() {
if (ui.form("/login")) { // кнопка нажата
ui.copyStr("lg", lp.ssid); // копируем себе
ui.copyStr("ps", lp.pass);
EEPROM.put(0, lp); // сохраняем
EEPROM.commit(); // записываем
// WiFi.softAPdisconnect(); // отключаем AP
}
if (ui.click()) {
// проверяем компоненты и обновляем переменные

if (ui.clickInt("num", regulator.setpoint)) {
Serial.println(regulator.setpoint);
}

if (ui.clickInt("num1", v1)) {
Serial.println(v1);
}
if (ui.clickInt("num2", t1)) {
Serial.println(t1);
}
if (ui.clickInt("sld", valSlider)) {
Serial.print("Slider: ");
Serial.println(valSlider);
}

if (ui.clickInt("rad", valRad)) {
flag2=valRad;
Serial.print("Radio: ");
Serial.println(valRad);
}
if (ui.click("btn2")) {
File file = LittleFS.open("/test1.txt", "w");
for (int i = 0; i < n; i++) {
file.print(arr[0][i]);
file.print(",");
file.println(arr[1][i]);
}
file.close();
}
if (ui.click("btn")) {
ical1=ical1n;
Serial.println("Button click");
Serial.println(ical1n);
Serial.println(ical2);
}
}
pda=(4000-valSlider)/4;
if (ui.update("plot3")) {
int answ[] = {tempc, ida};
ui.answer(answ, 2);
}
if (ui.update("plot2")) {
int answ[] = {tempc, ida};
ui.answer(answ, 2);
}
// if (ui.update("plot2")) ui.answer(pda);
if (ui.update()) {
if (ui.update("lb")) ui.answer(tempc);
if (ui.update("lbb")) ui.answer(ida);
if (ui.update("lbbb")) ui.answer(potValue);
if (ui.update("lbbdt")) ui.answer(dth);
if (flag2 >1){
ui.updateInt("sld", valSlider);
}
ui.updateInt("rad", flag2);
}
}
void startap2() {
Serial.println("Portal start");

// запускаем точку доступа
WiFi.mode(WIFI_AP);
WiFi.softAP("WiFi Config");
Serial.println(WiFi.localIP());
}
void startup() {
WiFi.mode(WIFI_STA);
WiFi.begin(lp.ssid, lp.pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
}
// Функция Task1code:
void Task1code( void * pvParameters ){
Serial.print("Task1 running on core ");
// "Задача Task1 выполняется на ядре "
Serial.println(xPortGetCoreID());
for(;;){
data3 = touchRead(15);
//Serial.print(data1);
data1 = touchRead(13);
//Serial.println(data3);
if (data1<35){
nt=nt+1;
if (nt>100){
flag2 =0;
nt=0;
Serial.println("data1");
Serial.println(data1);
}
}
else nt=0;
if (data3<40){
data3 = touchRead(15);
if (data3<35){
static uint32_t tmr3;
if (millis() - tmr3 >= 2000) {
tmr3 = millis();
flag1 =flag1*-1;
Serial.println("data3");
Serial.println(data3);
}
}
}
if(i <= kk) {
acs =hallRead()-ical1;
if (acs>0) {
acsum += acs;
i+=1;
}
}
else {
ida = acsum*10/kk-ical2;
ida=20+4.2*ida-sq(ida)*0.008;
acsum = 0;
i=1;
}
//калибровка
if(j <= kk) {
acs =hallRead();
if (acs>0) {
acsumc += acs;
j+=1;
}
}
else {
ical1n = acsumc/kk;
acsumc = 0;
j=1; }
potValue = analogRead(34);
if(flag2 <1){//POT
dimmer = map(potValue, 0, 4100, 500, 9300);
digitalWrite(23, 0);
digitalWrite(25, 1);
digitalWrite(26, 0);
digitalWrite(27, 0);
}
else if (flag2 <2){ //SLIDER
dimmer= map(valSlider, 0,4000, 500, 9300);
digitalWrite(23, 1);
digitalWrite(25, 0);
digitalWrite(26, 0);
digitalWrite(27, 0);
}
else if(flag2<3){//I+
digitalWrite(23, 0);
digitalWrite(25, 0);
digitalWrite(26, 1);
digitalWrite(27, 0);
valSlider = map(dimmer, 500,9300, 0, 4000);
ti4=ti4+1;
if(ti4>abs(v1)){
ti4=0;
if (v1<0){
if (tempc<t1){
if (dimmer>1000){
dimmer=dimmer -1;
// Serial.println(dimmer);
// Serial.println(valSlider);
}
}
}
else{
if (tempc>t1){
if (dimmer<9000){
dimmer=dimmer +1;
}
}
}}
}
else if(flag2<4){//PID
digitalWrite(23, 0);
digitalWrite(25, 0);
digitalWrite(26, 0);
digitalWrite(27, 1);
static uint32_t tmr4;
if (millis() - tmr4 >= 800) {
tmr4 = millis();
if(regulator.setpoint < 100) regulator.setLimits(3000, 4024);
else regulator.setLimits(0, 4024);
regulator.input = tempc;
int grt = regulator.getResult() ;
dimmer = map(grt, 0, 4024, 500, 9300);
valSlider = grt;
}
}
static uint32_t tmr;
if (millis() - tmr >= 800) {
tmr = millis();
if (sens.readTemp()) {
tempc=sens.getTemp();
//Serial.println(tempc);
}
else Serial.println("Error"); // ошибка чтения или подключения - выводим лог
//delay(100);
static uint32_t tmr2;
if (millis() - tmr2 >= 60000) {
tmr2 = millis();
dth=(tempc-tempcold2)*20;
tempcold2=tempcold1;
tempcold1=tempcold;
tempcold=tempc;
}
static uint32_t tmr3;
if (millis() - tmr3 >= 60000) {
tmr3 = millis();
arr[0][n]=tempc;
arr[1][n]=ida;
n=n+1;
}
oled.clear();
oled.home();
oled.setScale(2);
oled.println(ida);
oled.setCursorXY(83, 0);
oled.println(potValue);
//oled.setCursorXY(106, 0);
//oled.println(tempc);
oled.setCursorXY(35, 17);
oled.setScale(1);
if (flag1<1){
oled.println(WiFi.localIP());
}
else {
oled.setCursorXY(0, 17);
oled.println(regulator.setpoint);
oled.setCursorXY(83, 17);
oled.println(dth);
}
oled.setCursorXY(10, 32);
oled.setScale(4);
oled.println(tempc);
oled.update();
}
}
}
// Функция Task2code:
void Task2code( void * pvParameters ){
Serial.print("Task2 running on core ");
// "Задача Task2 выполняется на ядре "
Serial.println(xPortGetCoreID());

for(;;){

ui.tick();

}
}
void loop() {

}

программаПрограмма собрана как из кубиков из библиотек с сайта GyverLibs - Arduino библиотеки от AlexGyver. Все эти библиотеки в том числе доступны через Менеджер библиотек в Arduino IDE Использованы: GyverPID, GyverMAX6675_SPI, GyverOLED, GyverPortal, EEPROM, LittleFS. Для работы без доступа к интернету необходимо записать данные в память ESP32 через пункт меню Arduino IDE: Инструменты / ESP32 Sketch Data Upload. При записи данных необходимо, чтобы монитор порта был закрыт. Данные в первую очередь нужны для построения графиков. Чтобы работать в локальной сети, небходимо использовать полную запись параметров и последний изменить на 1.

GP.AJAX_PLOT("plot3", names, 2, 9000, 6000,400,1); и в setup добавить ui.downloadAuto(true);

В программе можно задать температуру для ПИД-регулятора. Скорость изменения тока. Это параметр, который определяет интервал, через который будет подана команда на изменение значения диммера в фазовом регуляторе. Поэтому, чем число больше, тем медленнее. Отрицательные значения - ток увеличивается, поскольку уменьшается задержка перед включением симистора. Далее можно задать температуру, до которой будет изменяться ток. Если положительное значение задержки, то  ток будет уменьшаться, если отрицательное - то увеличиваться, при этом надо учитывать, что  печь - это очень инерционное устройство. Для достижения какой-то температуры надо выбрать меньшую температуру, при которой изменение тока  остановится, но при этом температура будет ещё некоторое время повышаться.  Любому току соответствует некая максимальная температура, которую он способен поддерживать. Плавно повышая ток, желательно достичь температуры, наиболее близкой к  желаемой и только после этого переходить к ее удержанию с помощью ПИД-регулятора. Далее расположены четыре радиокнопки, которые позволяют переключаться между режимами. Первый режим - это режим потенциометра на устройстве, второй режим -  движка  в программе, третий режим - плавного изменения тока и четвертый режим - PID регулятора.

Ниже расположены две строчки с текущими параметрами: это температура, ток в сантиАмперах, изменение температуры за час и положение потенциометра на устройстве.

Ползунок диммера позволяет, как задавать ток, так и отображать значение при автоматических режимах, таких как PID или изменение тока.

Графики отображают значение температуры и тока. Первый - в течение всего времени работы, второй - за последние 10 минут. Следует отметить, что графики рисуются браузером, и при его перезагрузке сбросятся. На приведенном рисунке показаны графики работы ПИД-регулятора, при задании нескольких последовательных температур.

Следующая кнопка отвечает за калибровку нулевого значения тока.

Программа каждую минуту запоминает значение температуры и тока, при необходимости можно записать эти данные в файл и сохранить его на диске вашего компьютера. За эту операцию отвечают следующие две кнопки.

Последние три строчки отвечают за параметры сети wi-fi, которые будут записаны в память и использованы при перезапуске устройства.

Скачать программу вместе с загружаемыми данными.

22.03.2024

Установите проигрыватель Flash

Облако тегов:
3D печать
Arduino
Raspberry Pi
Аэрофотосъемка
Байдарки
Геомеханика
История
Камеры
Макросъемка
Объективы
Освещение
Панорамы
Принадлежности
Принтеры
Программы
Сканеры
Стереосъемка
Фильтры
Фокусировка
Фотокубики
...
rss