Сушилка для овощей и фруктов на ардуино нано.

Любая вещь, особенно любимая, в свое время когда-то вошла в нашу жизнь, и не важно как, не важно когда, важно — любима!

К чему это я? Эта сушилка для овощей и фруктов разрабатывалась без привычной в таких случаях спешки, и совсем не было задачи сделать универсальное устройство с кучей возможных функций. Основной упор — эргономика.

Хотелось устройство над управлением которым не нужно думать и искать справоник или инструкцию по эксплуатации. Поэтому к управлению доступно меню на 9 вариантов в одном из них есть возможность выставить температуру и всё.

Остальное спрятано в глубине кода скетча , скорее всего, нужно этот код подробно объяснить, хотя он и обильно закоментирован.  Желающие просто повторить могут пропустить описание просто скачав код — сушилка для овощей и фруктов подробно описана на странице сайта с точки зрения изготовления прототипа.

Описание блоков кода.

Собственно,, сам код:

// Farik 14.05.2022
// https://farion.top/sushilka-dlya-fruktov/
// потужність нагріву регулюється PWM
// регулювання темератури відбувається по ПІД принципу
// датчик температури DS18B20
// преривання по порту від SCL та переривання від натискання кнопки
// два варіанти вихідного нагрівального елементу : 12 вольтові автодампи
// в високочастотному PWM режимі та вихід на опторозв'язку симістора
// або твердотільне реле з періодом PWM в десяток секунд
// використані напрацювання ардуїно спільноти та готові бібліотеки 
// include the library code:
#include 
#include  
#include 
LiquidCrystal_I2C lcd(0x21,16,2);   // Устанавливаем дисплей классика 0x27
#include 
MicroDS18B20 sensor2;           // датчик 
#include 
unsigned long f_day;                // змінна для визначення скільки годин сушка ввімкнена
static char day[3];                 // массив символів для виводу на єкран кількості діб
static char l_temp[4];              // масив символів для виводу температури на екран
float f_power;
static char power[2];               // массив символів для виводу на єкран потужності нагріву
double Setpoint;                    // підпрограмма ПІД регулятора змінна
double Input, Output = 95;          // підпрограмма ПІД регулятора змінна
double Kp=40, Ki=7, Kd=1.5;         // коєффіцієнти ПІД регулятора 
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); //
int i_power = 100;                  // PWM для нагрівача
float Temperature = 32.0;           // змінна температури
float temper;                       // значення температури зитане з датчика в цьому циклі
float korrectemp = 0.66;            // коррекція датчика температури по еталонному термометру
                                    // по еталонному термометру у вас буде інше значення у мене +0.66
char* fruit[9]{"herbs","plants","bread","yogurt","veget","fruit", // масив слів для виводу на єкран
                    "corn","fish","t\xDF""C"};  // режиму в якому зараз знаходиться пристрій
const float fruit_temp[8]{34,41,43,46,52,57,    // масив температур для кожного режиму
                    68,68};                     // роботи пристрою
volatile int choice;                  // змінна режиму - прапорець
int choice_tic;                      // змінна перевірки еепром
volatile bool f_isrgf = false;       // нажимання кнопки енкодеру
volatile int d_isr = 0;
float man_temp;                      // вручну задається температура
float iman_temp;                     // змінна перевірки що записано в еепром перед перезаписом
static char manual_[3] ;             // масив символів температури для лсд
#include <avr/interrupt.h>           // для ассемблерного прерывания
#include <avr/io.h>                  // для ассемблерного прерывания
#include "GyverEncoder.h" 
#define CLK 3                        // initialize the library with the numbers of the interface pins
#define DT A1
#define SW 2
Encoder enc1(CLK, DT, SW); 
//  Определяем массив элементы которого будут хранить различную информацию в зависимости от режима
const int powerWATT[65] = {0,32,45,55,64,71,78,84,90,95,100,105,110,114,119,123,127,131,135,
                          139,142,146,149,152,156,159,162,165,168,171,174,177,180,183,185,
                          188,190,193,196,198,201,203,206,208,211,213,215,218,220,222,225,
                          227,229,231,233,235,237,239,241,243,245,247,249,252,255};               
                                      // powerWATT массив приведения мощности к длительности цикла ШИМ 
                                      // диапазон от 0 до 64 что соответствует от 0% до 100% мощности                                    
volatile int PwWidth;                 // Переменная PwWidth для программного ШИМ по прерываниям от таймера 1 диапазон от 0 до 64 что соответствует от 0% до 100% мощности
volatile int powerQ;                  // переменная периода для программного ШИМ по прерываниям от таймера 1 диапазон от 0 до 64
int i = 0;                            // змінна для плавного запуску ламп розжарювання
void setup()
{
      TCCR2B = 0b00000001;                                    // x1 настройка таймера прерываний на аппаратный ШИМ
      TCCR2A = 0b00000011;                                    // fast настройка таймера прерываний на аппаратный ШИМ
      TCCR1A = 0b00000011;                                    // 10bit настройка таймера прерываний на программный ШИМ 15 Гц
      TCCR1B = 0b00001101;                                    // x1024 настройка таймера прерываний на программный ШИМ
      TIMSK1 |= 1<<TOIE1;                                     // бит TOIE1 у регистра TIMSK1 дозволяємо прерывание по переполнению счетчика
      pinMode(12, OUTPUT);                                    // шим выход на тиристор с привязкой к полуволнам сети период 10,2 секунды
      pinMode(13, OUTPUT);                                    // шим инверсный выход на тиристор с привязкой к полуволнам сети период 10,2 секунды    
      pinMode(8, OUTPUT);
      pinMode(A0, INPUT); 
      pinMode(A1, INPUT); 
      pinMode(A2, INPUT);
  lcd.init();                     
  lcd.backlight();                                             // Включаем подсветку дисплея
  lcd.print("fruit dehydrator");
  lcd.setCursor(1, 1);
  lcd.print("year 2022");
  lcd.setCursor(11, 1);
  lcd.print("Farik");
  Serial.begin(9600);
  attachInterrupt(1, isr, FALLING);           // дозволяємо прерывание на D2 пине! CLK у энка
  attachInterrupt(0, isrgf, LOW);             // дозволяємо прерывание на D3 кнопка енкодера
  do                                          // холодні лампи розжарювання викликають спрацювання
    {                                         // захисту блока живлення від перевантаження тому попередньо
      analogWrite(11,i);                      // розігріємо їх плавно за рахунок PWM
      delay(100);                             // тайм 
      i++ ;                                   // інкремент+ 
    } while (i < 255);                        // максимальна потужність нагріву
  lcd.clear();
  myPID.SetMode(AUTOMATIC);                   // режим роботи підпрограмми ПІД регулювання потужності
  EEPROM.get(10, choice);
  EEPROM.get(12, man_temp);
}
void isr() {                                   // подпрограмма обработки прерывания для энкодера
            if (digitalRead(DT)) d_isr = -1;
            else d_isr = 1;                     // отработка в прерывании
            }
void isrgf() {
                 f_isrgf = true;
              }
ISR(TIMER1_OVF_vect)                           // подпрограмма обработки прерывания от переполнения TIMER1 период 66мс
     {
          powerQ++;                            // инкремент переменной счетчика цыкла по каждому 40 прерыванию тамера то есть раз в 0.04 с
          TIFR1 |= 0<<TOV1;                    // Timer1 INT Flag Reg: Сброс флага прерывания Timer Overflow Flag 
     }
void loop()
{
   lcd.setCursor(0,0);
   lcd.print("Temp");
   lcd.setCursor(7, 0);
   lcd.print("\xDF""C");
   lcd.setCursor(10, 0);
   lcd.print("W ");   
   lcd.setCursor(15, 0);
   lcd.print("%");
   lcd.setCursor(0,1); 
   lcd.print("      ");
   lcd.setCursor(0,1); 
   lcd.print(fruit[choice]);
   if (choice == 8)                            // якщо ми знаходимся в підпункті ручне керування
     {                                         // заданною температурою, виводимо на єкран значення
           lcd.setCursor(4,1);                 // цієї температури - позиція на єкрані 2 рядок 4 символ
           lcd.print("  ");                    // затираємо пробілами все що раніше було на єкрані
           lcd.setCursor(4,1);                 // в наших позиціях та повторно позиціонуєм курсор
           dtostrf(man_temp,2, 0, manual_);    // підготуєм для єкрану всього 2 знаки після коми нуль знаків
           lcd.print(manual_);                 // виводим масив символів
     }  
   lcd.setCursor(10, 1);
   lcd.print("day"); 
   lcd.setCursor(14, 1);
   f_day = 1 + millis()/86400000;   // яка доба йде з моменту загрузки
   dtostrf(f_day,2, 0, day);        // всього 2 знаки після коми нуль знаків
   lcd.print("   ");
   lcd.setCursor(14, 1);
   lcd.print(day);
   sensor2.requestTemp();           // запрашиваем новое измерение 
   temper = sensor2.getTemp();      // зчитали температуру
   temper = temper + korrectemp;    // підкорегуємо покази датчика DS18
   dtostrf(temper,2, 0,l_temp);     // підготуємо температуру для виводу на єкран
   lcd.setCursor(5, 0);
   lcd.print("  ");                 // очистка єкрана    
   lcd.setCursor(5, 0);
   lcd.print(l_temp);               // масив виводу температури на єкран
   f_power = i_power;
   dtostrf((f_power/255)*(f_power/255)*100,2, 0, power); // перерахунок періоду
                                    // в потужність нагріву p=u*u/r 
                                    // та переведем змінну в масив символів
                                    // для виводу на єкран
   lcd.setCursor(12, 0);
   lcd.print("   ");                // очистка єкрана                                
   lcd.setCursor(12, 0);
   lcd.print(power);                // процент від максимальної потужності нагрівача   
//---------------збираємо данні для обчислювання-----------------  
    Input = temper;                           // читається з датчика температури 
    if (choice == 8) Temperature = man_temp;  //  якщо температура вручну
    else Temperature = fruit_temp [choice];   //  якщо режим є в пам'яті

//---------------PID----------------- 
   Setpoint = Temperature;
   myPID.Compute();
   Serial.print("температура ");
   Serial.print(temper);  
      Serial.print(" задали ");
   Serial.println(Temperature); 
//---------------PWM-----------------    
   i_power = map(Output, 0, 255, 80, 255);     // отриманий результат це період PWM ріжем знизуб бо при потужності 
   if (i_power<81) i_power = 0; // меньшій, наш нагріва не має ніякого сенсу - ККД нуль analogWrite(11,i_power); // вихід низковольтного нагрівача PwWidth = powerWATT[ map (i_power,0,255,0,64)]; // задаем длительность периода ШИМ по прерываниям от таймера 2 из массива в соответствии с переменной мощности if (powerQ > 255) powerQ = 0;                        // организуем цыкл периода ШИМ по таймеру с периодом 256
         if (PwWidth >= powerQ && PwWidth) 
            {
               digitalWrite(13,HIGH);                      // Если ШИМ по таймеру должен быть включен выводим сигнал на 12 пин
               digitalWrite(8,HIGH);                         // ВИХІД з піна 13 або 8 на анод катод на землю
               digitalWrite(12,LOW);                       // длительность высокого уровня сигнала на выходе ШИМ по таймеру исключим ситуацию 0%
            }                                               //ВИХІД анод на плюс живлення катод на пін 12
         else 
             {
                digitalWrite(13,LOW);                     // Если ШИМ по таймеру должен быть включен выводим сигнал на 12 пин
                digitalWrite(8,LOW);                      // ВИХІД з піна  13 або 8 на анод катод на землю
                digitalWrite(12,HIGH);                    //ВИХІД анод на плюс живлення катод на пін 12
             } 
//---------------Енкодер-----------------
   if (choice == 8)                                 // якщо виставлений режим ручне керування температурою
      {                                             // 
        if (!d_isr)                                 // якщо змінна має нульове значення
          {                                         // то нічого не робимо
          }                                         // зовсім нічого
          else                                      // якщо ж d_isr не дорівнює нулю
            {man_temp = man_temp + d_isr;           // змінюємо значення виставленої температури 
            d_isr = 0;                              // звісно, обнулимо прапорець після відпрацювання прериванняя
            }                                       //
      }                                             //
   if (man_temp > 75)                               // якщо ми докрутили руку до 75 градусів
      {                                             // цього достатньо не дозволяємо перевищувати 75 градусів
       man_temp = 75;                               //      
      }                                             //
   if (man_temp < 25)                               // якщо докрутилися до менше ніх 25 градусів 
      {                                             // залишимо 25 градусів це все таки сушка а не холодильник
       man_temp = 25;                               //      
      }                                             //     
   if (f_isrgf)                                     // якщо прапорець натискання кнопки не нуль
    {                                               // змінимо режим
      if (choice == 0) choice = 8;                  // якщо прапорець режиму був нульовим, слідующе значення 
      else choice--;                                // повинно бути 8 - отработка в прерывании
      f_isrgf = false;                              // скидаємо прапорець
      i = 0;                                        // при зміні режиму дуже велика вірогідність ввімкнути на всю  
      do                                            // холодні лампи розжарювання та викликаюти спрацювання
        {                                           // захисту блока живлення від перевантаження тому попередньо
          analogWrite(11,i);                        // розігріємо їх плавно за рахунок PWM
          delay(30);                                // тайм 
          i++ ;                                     // інкремент+ 
        } while (i < 255);                          // максимальна потужність нагріву
     }                                              //
    EEPROM.get(10, choice_tic);                     // зчитаємо з пам'яті змінну за адресою 10
   if (choice_tic != choice) EEPROM.put(10, choice);// перевіримо з теперішнім значенням choice пишем в пам'ять якщо відрізняється
    EEPROM.get(12, iman_temp);                      // якщо man_temp текущая така ж як в пам'яті ЕЕПРОМ
   if (iman_temp != man_temp) EEPROM.put(12, man_temp);// нічого не робимо, якщо інша - пишем нове значення 
                                                      // пишeм в еепром настройку 


}

условно программу можно разделить на блоки кода , отвечающие каждый за своё:

  • ПИД регулятор
  • блок ШИМ
  • обработка енкодера
  • блок индикации на ЛСД
  • блок датчика температуры
  • куски  кода предотвращения аварийных ситуаций
  • запись настроек в енергонезависимую память

Проще всего начать с датчика температуры.

Блок датчика температуры.

Стандартная подпрограмма получения температуры с датчика DS18B20 дополнена корректирующим коэффициентом. Для arduino nano лишних пару строчек кода, а сушилка для овощей и фруктов получила плюсик к карме. Ведь согласитесь, приятнее наблюдать правильную температуру чем каждый раз вспоминать о погрешности датчика DS18B20, пусть в данном применении это и не слишком важно.

После запроса к датчику на измерение температуры — sensor2.requestTemp()  и считывания показаний температуры temper = sensor2.getTemp() к полученному значению добавляем  temper = temper + korrectemp предварительно выисленное по эталонному термометру значение ошибки датчика. Если нужно, то со знаком минус , например : -0,24.

ПИД регулятор.

Стандартная программа, написана открытым кодом  проблемма только в подборе коеффициентов :

  1. Kp=40
  2. Ki=7
  3. Kd=1.5

если Вы хоть примерно понимаете, что это значит и что на что влияет — можете их изменить на своё усмотрение, если нет — оставляем так как есть — сушилка для овощей и фруктов прекрасно отрабатывает с точностью не хуже 3°С.

Сама подпрограмма получает в качестве аргументов ссылку на переменные :

  • Input — текущее значение температуры
  • Setpoint — заданная в программе температура — та что нам нужна

и сохраняет результат в переменной Output, приём все операции производятся путем передачи ссылки на переменную в памяти что исключает необходимость в дополнительных глобальных переменных.

После запуска подпрограммы myPID.Compute() в переменной Output будет храниться результат математических вычмслений по ПИД алгоритму.

ШИМ (PWM) на arduino.

Не буду слишком скромничать — данная программа выдавила из возможностей nano в плане PWM максимум и даже больше чем я изначально планировал. Так получилось, что в плане ШИМ у меня были два свежих в памяти проекта , оба требовали специффического подхода к возможностям ардуины,  а именно , аппаратных модулей ШИМ. А сушилка для овощей и фруктов , как оказалось в процессе проектирования, запросила обе наработки, ну , как говориться, лишь бы в масть!

сушилка для овощей и фруктов LCD екран
экран в рабочем режиме

Итак — у меня два варианта нагревательных элементов , оба они требуют разного подхода к процессу регилирования мощности! Кроме этого, мощность будем регулировать по сложному математическому алгоритму, и тут есть куча ньюансов!

Опустим инженерные изыскания и перейдём к результату:

  • нагреватель из автомобильных 12 вольтовых ламп накаливания
  • ТЕН на 220 вольт

Для ТЕНа нужен регулятор который будет максимально лояльным к электропомехам в сети (ведь нагрузка постоянно включается-выключается, и это не меньше полкилловата) и обезопасит пользователя — ведь это все таки 220 Вольт.

Для автоламп нужно лояльное отношение к ресурсу  (в режиме вкл-выкл лампа накаливания долго не проживёт) и адекватное отношение к большим токам коммутации.

Пришлось менять стандартные настройки ШИМ и таймеров , ведь 500 Герц не подходит ни к первому варианту, ни ко второму.

TCCR2B = 0b00000001;
TCCR2A = 0b00000011;
TCCR1A = 0b00000011;
TCCR1B = 0b00001101;

и вводить работу по прерыванию от переполнения счетчика
TIMSK1 |= 1<<TOIE1;

Аппаратный ШИМ (PWM) ардуино нано.

После изменения коэффициента деления счетчика для ШИМ — дальше можно использовать стандартную для arduino функцию — на выходе мы получаем высокую частоту , достаточную чтобы не рябило в глазах и отсутствие бросков тока в момент включения ламп. Регулировка происходит плавно.

analogWrite(11,i_power);

Но есть одно но! при маленьких длительностях включения лампы практически не накаляются и температура колбы явно ниже требуемой для работы. Поэтому когда алгоритм ПИД регулирования выдаёт маленькие мощности,  нагреватель есть смысл вообще выключить, при этом шкалу регулировки растянуть чтобы не потерять линейность регулировки.

i_power = map(Output, 0, 255, 80, 255);
if (i_power<81) i_power = 0;

за растягивание шкалы отвечает код i_power = map(Output, 0, 255, 80, 255);

Получая от PID алгоритма значение от 0 до 255 PWM arduino работает в диапазоне от 80 до 255. Строка if (i_power<81) i_power = 0; принудительно выключает блок нагрева задав период ШИМ равным нулю.

Програмный ШИМ (PWM) на ардуино нано.

 

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

ISR(TIMER1_OVF_vect)

при переполнении счетчика  инкрементирует переменную

powerQ++;

это происходит в подпрограмме прерывания, в основном цикле программы мы работаем с powerQ — текущим знаением переменной и PwWidth — заданной длительностью включения питания (точнее сказать — числом тактов цыкла powerQ на которые нужно подать напряжение на нагрузку)

if (PwWidth >= powerQ && PwWidth)
{
digitalWrite(13,HIGH);
digitalWrite(12,LOW);
}
else
{
digitalWrite(13,LOW);
digitalWrite(12,HIGH);
}

этот блок кода включает нагрузку пока счетчик цикла powerQ  лежит в диапазоне от 0 до PwWidth и выключает в диапазоне от PwWidth до 255. Таким образом реализуется програмный ШИМ алгоритм.

Сложная конструкция условия PwWidth >= powerQ && PwWidth , а именно дополнительное умножение условия && PwWidth дает возможность выключить полностью нагрузку если  PwWidth равно нулю.

Ограничим переменную powerQ диапазоном 0-255

if (powerQ > 255) powerQ = 0;

эта строчка сбрасывает в ноль значение переменной при превышении  значения 255. Это часть  программы , отвечающая за генерацию медленного ШИМ для активной мощной нагрузки.

Линеаризация шкалы мощности нагревателя.

PID алгоритм работает с температурой, она в свою очередь зависит от мощности нагрева, а PWM работает с напряжением. Мощность соотносится с квадратом напряжения, Поэтому есть смысл линеаризировать шкалу регулировки. Я уже описывал подробно регулировку мощности с помощью PWM применительно к активной нагрузке.

Пока у нас инертность нагревателя много меньше инертности системмы, нелинейностью соотношения мощности и значения ШИМ  можно легко принебречь, а когда период ШИМ очень большой , как в данном случае, очень сложно будет настроить ПИД — ведь за время одного периода регулирования ШИМ ПИД может совершить несколько пересчётов.

За приведение в соответствие вышеизложенного отвечает массив powerWATT  и блок кода

PwWidth = powerWATT[ map (i_power,0,255,0,64)];

переотраженный изначально и обработанный диапазон i_power от 0 до 255 проэцируется на массив значений переменной powerWATT от 0 до 64.

Ох уж эта сушилка для овощей и фруктов — срабатывание защиты блока питания.

Это касается только низковольтных ламп накаливания, высоковольтный нагреватель с таким не сталкивается.

сушилка для овощей и фруктов - электронная начинка
скоммутированная схема

Мощный современны качественный 100 Ваттный блок  питания, несомненно, имеет токовую защиту низковольтной части. Даже четыре лампы накаливания по 10 Ватт имеют суммарный пусковой ток больше 8 Ампер. В итоге, при включении холодных ламп на 100% мощность, срабатывает токовая защита.

Дабы предотвратить сию неприятность, сушилка для овощей и фруктов имеет  вмонтированый програмный код, который запускает холодные лампы плавно доводя их мощность до 100% . С помощью того же аппаратного ШИМа , что и использует  основная программа ПИД.

do
{
analogWrite(11,i);
delay(100);
i++ ;
} while (i < 255);

Думаю тут всё понятно. Пока не выйдем на 100% мощность, выполняется цикл. А потом значение ШИМ изменится в соответствии с логикой програмного кода, нити накала за это время не успеют остыть.

LCD 16×2

Какой бы навороченной не была сушилка для овощей и фруктов и сколько б я не запихнул в неё микроконтроллеров, это не автономное устройство. Должно быть оперативное информирование о режимах девайса. На экран устройства попала такая инормация:

  • текущая температура с датчика DS18B20
  • мощность нагревателя в процентах от максимальной
  • предустановленный режим роботы
  • сутки с момента включения сушки в сеть

Думаю что нет особого смысла подробно описывать весь код вывода на ЛСД. Я обращу внимание только на некоторые специффические детали.  Перед выводом информации на экран его — экран — нужно очистить. Но очищаем мы только те знакоместа, информацию в которых мы будем менять прямо сейчас. Например:

lcd.setCursor(14, 1);
f_day = 1 + millis()/86400000;
dtostrf(f_day,2, 0, day);
lcd.print(»   «);
lcd.setCursor(14, 1);
lcd.print(day);

Очистка LCD 1602 это запись на нужных знакоместах пробелов. Для того, чтобы экран не моргал, информацию нужно писать сразу после очистки знакомест.

Преобразование типов переменных для вывода на экран.

При выводе на экран вещественных чисел нужно преобразовывать их в набор литералов , например dtostrf(f_day,2, 0, day) преобразует  переменную с плавающей точкой в переменную day типа  char длинной в 3 символа. Два знака всего и ноль знаков после запятой — это dtostrf. Аналогично выводится температура с датчика.

Режим работы устройства выводится блоком кода:

lcd.setCursor(0,1);
lcd.print(»      «);
lcd.setCursor(0,1);
lcd.print(fruit[choice]);
if (choice == 8)
{
lcd.setCursor(4,1);
lcd.print(»    «);
lcd.setCursor(4,1);
dtostrf(man_temp,2, 0, manual_);
lcd.print(manual_);
}

переменная choice это флаг режима терморегулятора. В зависимости от значения этой переменной, на экран выводится нужный элемент массива fruit. Кроме того, если выбран режим руного ввода температуры, то после вывода элемента  «t\xDF»»C» , ещё выводится и заданная пользоватнелем температура manual_ . Она в свою очередь преобразовывается для приемлимого для вывода  вида функцией  dtostrf.

Осообенности вывода спецсимволов на экран.

Чуток подробней распишу конструкцию «t\xDF»»C». На LCD 1602 эти три знакоместа выглядят так t°C.  Конструкция \xDF — это адресация к ячейке памяти 1602 на символ °. То бишь это слово состоит из двух отдельных — первое слово t\xDF из двух символов t и  \xDF и второе слово из одного символа С. Они склеиваются в одно при выводе, но если написать например t\xDFC на экране будет абракодабра — ЛСД воспримет DFC как адрес символа. Нельзя , кстати и написать вместо «t\xDF»»C» значение «t°C» — ASCII код символа на компьютере не соответствует коду LCD 16×2.

Преобразование периода  PWM в процент мощности на экране LCD.

 

Закон Ома приведенный к мощности говорит нам следующее P = U2/R. Напряжение в случае PWM это отношение длительностей включенного времени к времени периода ШИМ. Мощность связана с квадратом напряжения, поэтому применяется следующий код

f_power = i_power;
dtostrf((f_power/255)*(f_power/255)*100,2, 0, power);
lcd.setCursor(12, 0);
lcd.print(»  «);
lcd.setCursor(12, 0);
lcd.print(power);

строка f_power = i_power; делает две вещи — во первых запись во временную переменную значения текущего периода, во-вторых преобразует целое значение в значение с плавающей точкой. Если не произвести перевод с целочисленного значения, после деления на 255 мы получим либо 1 либо 0, а не ожидаемое дробное значение.

Функция dtostrf традиционно переведет цыфры в набор символов для вывода на ЛСД. lcd.print(»  «); — очистка экрана и lcd.print(power); — вывод значения на экран.

Работа arduino с энкодером.

Наша сушилка для овощей и фруктов управляется всего одной крутилкой — энкодером. Для длинного кода с множеством затратных по таймингу операций по последовательным  интерфейсам это та ещё задача.

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

Обработка нажатия кнопки.

Работа с крутилкой организована с помощью прерываний.

attachInterrupt(0, isrgf, LOW);

прерывание на D3 по нажатию кнопки SW. В подпрограмме обработки прерывания от SW

void isrgf() {
f_isrgf = true;
}

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

if (f_isrgf)
{
if (choice == 0) choice = 8;
else choice—;
f_isrgf = false;
i = 0;
do
{
analogWrite(11,i);
delay(30);
i++ ;
} while (i < 255);
}

в основном цыкле программы проверяем был ли выставлен флажок if (f_isrgf)  и если f_isrgf не равна нулю (что означает что  был факт прерывания — то есть был акт нажатия кнопки, и если всё-таки был, то меняем значение режима работы — сушилка для овощей и фруктов переходит на работу в следующем по номеру режиме.

else choice—;

При этом не будем забывать , режимов работы у нас всего 9, поэтому зацикливаем переменную

if (choice == 0) choice = 8;

присваивая ей значение 8 после 0. И не забываем сбрасывать флажок f_isrgf = false. При смене режима, есть вероятность врубить холодные лампы накаливания на полную мощность  и нарваться на срабатывание защиты блока питания от перегрузки, поэтому запустим плавный разогрев ламп

i = 0;
do
{
analogWrite(11,i);
delay(30);
i++ ;
} while (i < 255);
}

Обработка поворота энкодера.

Поворот ручки энкодера вызывает прерывание на D2 дл обработки этого события я разрешаю прерывания по падению фронта импульса на контакте CLK.

attachInterrupt(1, isr, FALLING);

Процедура определения направления вращения и отработки поворота энкодера описана миллион раз в интернете например тут , поэтому не буду вдаваться в теорию и перейду к выводам. Если на спаде импульса на D2, то есть при наступлении события FALLING, на втором контакте энкодера DT был высокий уровень сигнала — логическая единица, то вращение было в одну сторону, если на DT в этот момент был логический ноль — то вращение было в другую сторону.

В зависимости от места размещения крутилки, есть смысл делать либо декремент как у меня :

if (digitalRead(DT)) d_isr = -1;
else d_isr = 1;

либо заменить на инкремент  в коде :

if (digitalRead(DT)) d_isr = 1;
else d_isr = -1;

если удобнее разместить енкодер под левую руку.

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

if (choice == 8)

иначе игнорируем любые повороты ручки крутилки. Если был поворот энкодера if (!d_isr) то прибавляем ( либо отнимаем ) к заданной температуре 1 градус man_temp = man_temp + d_isr и после этого обнуляем переменную d_isr = 0. Дальше блок кода который не позволяет температуре покинуть зону от 25 до 75 градусов. Это логично , ведь  сушилка для овощей и фруктов вряд ли нужна при температурах ниже 25 — летом на улице обычно выше температура — и выше 75 градусов — при большей температуре это уже будет скорее приготовление пищи чем сушка свежых фруктов.

Запись настроек в енергонезависимую память arduino.

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

EEPROM.get(10, choice_tic) сравниваем с текущим значением и если отличается — пишем новое значение if (choice_tic != choice) EEPROM.put(10, choice) , если не отличается оставляем то что было. Такая же ситуация и с заданной температурой EEPROM.get(12, iman_temp) сравниваем if (iman_temp != man_temp) и пишем EEPROM.put(12, man_temp) либо ничего не меняем.