Урок 41. Разработка контроллера элемента Пельтье. ПИД регулятор температуры.

Структурная схема ПИД

Продолжим разработку контроллера элемента Пельтье начатую в уроке 36. Добавим к проекту регулятор температуры.

Предыдущий урок     Список уроков     Следующий урок

Регулятор температуры выполняет главную функцию холодильника – стабилизацию температуры в камере. Именно он во многом определяет основные параметры устройства: точность поддержания температуры и скорость реакции на возмущающие воздействия. Поэтому будем реализовывать регулятор температуры по закону ПИД регулирования.

 

Еще раз приведу структурную схему связи регуляторов мощности и температуры.

Структурная схема

Регулятор температуры получает измеренное значение температуры в камере, сравнивает его с заданным и формирует значение заданной мощности для регулятора мощности. Итак, в регуляторе температуры:

  • регулируемый параметр – температура в камере;
  • регулирующий элемент – заданная мощность для регулятора мощности.

Для того, чтобы стабилизировать температуру надо ее измерять. Поэтому первое, что добавим в программу - это измерение температуры в камере. Заодно реализуем измерение температуры радиатора горячей стороны элемента Пельтье.

 

Реализация измерения температуры в камере и температуры радиатора горячей стороны модуля Пельтье.

Температуру измеряем интегральными датчиками DS18B20. Достаточно подробно о подключении датчиков такого типа к Ардуино рассказано в уроке 26. Для тех, кто не знает, как работать с этими термодатчиками следует внимательно прочитать урок 26.

Подключим датчики DS18B20 к плате Ардуино по стандартной схеме. Еще в начале разработки мы решили, что будем использовать аналоговые выводы A2 и A3 в дискретном режиме.

Схема подклюючения термодатчиков DS18B20

Собранная схема у меня выглядит так.

Ардуино контроллер холодильника

Теперь будем дорабатывать программу.

Подключим библиотеку OneWire:

#include <OneWire.h>

Добавим переменные для измеренных температур:

float measureTempRef;  // измеренная температура в камере (< -200 - ошибка)
float measureTempRad;  // измеренная температура радиатора (< -200 - ошибка)

Создадим объекты OneWire (датчики температуры).

OneWire sensTempRef (16);  // датчик температуры в камере подключен к выводу 16 (A2)
OneWire sensTempRad (17);  // датчик температуры радиатора подключен к выводу 17 (A3)

При измерении температуры могут возникнуть аппаратные ошибки датчиков DS18B20. Для индикации этих ошибок не будем заводить отдельные признаки. Договоримся, что значение температуры менее – 200 C° говорит об аппаратной ошибки измерения.

Последовательность действий для измерения температуры с помощью датчиков DS18B20 описана в уроке 26. Она состоит из трех операций:

  • инициализация измерения температуры;
  • пауза не менее 750 мс;
  • чтение результата измерения из датчика и проверка контрольной суммы.

Инициализация измерения температуры реализуется 3 функциями библиотеки OneWire:

sensTempRef.reset();  // сброс шины 1-Wire
      sensTempRef.write(0xCC, 1); // пропуск ROM
      sensTempRef.write(0x44, 1); // инициализация измерения

Чтение результата измерения из датчика и проверки контрольной суммы требует следующих функций:

sensTempRef.reset();  // сброс шины 1-Wire
      sensTempRef.write(0xCC, 1); // пропуск ROM 
      sensTempRef.write(0xBE, 1); // команда чтения памяти датчика 
      sensTempRef.read_bytes(bufData, 9);  // чтение памяти датчика, 9 байтов
      if ( OneWire::crc8(bufData, 8) == bufData[8] ) {  // проверка CRC

Понятно, что блок инициализации надо выполнить в начале длинного цикла (1 сек) программы, а блок чтения результата ближе к концу длинного цикла. Время между выполнением этих блоков должно быть не менее 750 мс.

Но вопрос заключается в том, сколько времени требуется на выполнение функций класса OneWire. Можно ли для выполнения операций инициализации измерения и чтения результата использовать по одному циклу 20 мс или необходимо ”размазать” их на несколько циклов. Не забудем, что мы измеряем температуру с помощью двух датчиков DS18B20.

Я измерил время выполнения функций библиотеки OneWire. Методику измерения и результаты привел на форуме сайта в этой теме. Получилось, что:

  • операция инициализации измерения температуры требует 2063 мкс;
  • чтение результата с проверкой контрольной суммы занимает 6,8 мс.

В результате я решил инициализацию измерения температуры для обоих датчиков выполнить в одном цикле 20 мс на интервале 5.

      //--------------------------- интервал 5, инициализация измерения температуры
      // датчик в камере
      sensTempRef.reset();  // сброс шины 1-Wire
      sensTempRef.write(0xCC, 1); // пропуск ROM
      sensTempRef.write(0x44, 1); // инициализация измерения
      // датчик радиатора
      sensTempRad.reset();  // сброс шины 1-Wire
      sensTempRad.write(0xCC, 1); // пропуск ROM
      sensTempRad.write(0x44, 1); // инициализация измерения
    }

А чтение результата с проверкой контрольной суммы для датчиков температур в камере и радиатора сделал в отдельных интервалах 45 и 46.

    if (cycle20mcCount == 45) {
      //--------------------------- интервал 45, чтение датчика температуры камеры
      sensTempRef.reset();  // сброс шины 1-Wire
      sensTempRef.write(0xCC, 1); // пропуск ROM 
      sensTempRef.write(0xBE, 1); // команда чтения памяти датчика 
      sensTempRef.read_bytes(bufData, 9);  // чтение памяти датчика, 9 байтов

      if ( OneWire::crc8(bufData, 8) == bufData[8] ) {  // проверка CRC
        // правильно
        measureTempRef= (float)((int)bufData[0] | (((int)bufData[1]) << 8)) * 0.0625 + 0.03125; 
        if ( (measureTempRef < MEASURE_MIN_TEMP) || (measureTempRef > MEASURE_MAX_TEMP))
          measureTempRef= -300.;   // ошибка измерения  
      }
      else measureTempRef= -300.;   // ошибка измерения
    }

    if (cycle20mcCount == 46) {
      //--------------------------- интервал 46, чтение датчика температуры радиатора
      sensTempRad.reset();  // сброс шины 1-Wire
      sensTempRad.write(0xCC, 1); // пропуск ROM 
      sensTempRad.write(0xBE, 1); // команда чтения памяти датчика 
      sensTempRad.read_bytes(bufData, 9);  // чтение памяти датчика, 9 байтов

      if ( OneWire::crc8(bufData, 8) == bufData[8] ) {  // проверка CRC
        // правильно
        measureTempRad= (float)((int)bufData[0] | (((int)bufData[1]) << 8)) * 0.0625 + 0.03125; 
        if ( (measureTempRad < MEASURE_MIN_TEMP) || (measureTempRad > MEASURE_MAX_TEMP))
          measureTempRad= -300.;   // ошибка измерения  
      }
      else measureTempRad= -300.;   // ошибка измерения
    }

Полученные результаты проверяются еще на верхнее и нижнее максимальное значение. Если температура в нашей системе больше 100 или ниже 40 C° это явная ошибка.

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

      Serial.print(" t="); Serial.print(measureTempRef, 2);  // температура в камере
      Serial.print(" t="); Serial.print(measureTempRad, 2);  // температура радиатора

Полностью скетч программы с измерением температур в камере и радиатора можно загрузить по этой ссылке.

Загружаем скетч в плату, открываем монитор последовательного порта. Все работает.

Результаты измерения

Температура измеряется правильно.

 

Реализация ПИД регулятора температуры.

При разработке регулятора мощности (урок 39) я совершил не большую ошибку. Для быстрого выключения регулятора я использовал условие setPower == 0. Т.е. при заданной мощности равной 0 регулятор мгновенно выключался, интегральное звено сбрасывалось. Быстрое выключение требуется для реализации в будущем аварийных режимов.

Но заданная мощность может на короткое время стать равной 0 под действием дифференцирующей составляющей. Я изменил условие аварийного выключения регулятора мощности:

if ( setPower >= 0) {

}

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

Итак. Создаем все переменные и константы, необходимые для регулятора температуры.

#define koeffRegTmpInt 0.002 // интегральный коэффициент регулятора температуры  
#define koeffRegTmpPr 0.5 // пропорциональный коэффициент регулятора температуры  
#define koeffRegTmpDif 50 // дифференцирующий коэффициент регулятора температуры  

#define MAX_POWER 5.   // максимальная выходная мощность контроллера

float regPwrInt=0; // интегральное звено регулятора мощности
float maxSetPower=MAX_POWER; // максимальная заданная мощность
float regTmpInt=0; // интегральное звено регулятора температуры
float regTmpPr; // пропорциональное звено регулятора температуры
float regTmpDif; // дифференциальное звено регулятора температуры
float regTmpErr; // ошибка рассогласования температуры
float regTmpErrPrev; // предыдущая ошибка рассогласования температуры
float setTempRef; // заданная температура в камере

Поясню только параметры для ограничения мощности.

Константа MAX_POWER определяет максимальную мощность, которую способен создать контроллер. Это аппаратные ограничения на блок питания, ключевой стабилизатор, модуль Пельтье и т.п.

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

Сам регулятор выполним в цикле 20 мс на интервале 47, сразу после получения результатов измерения температуры.

    if (cycle20mcCount == 47) {
      //--------------------------- интервал 47, регулятор температуры в камере

      setTempRef= 20.;  // временно заданная температура
     
      regTmpErr= measureTempRef - setTempRef;  // вычисление ошибки рассогласования

      regTmpInt= regTmpInt + regTmpErr * koeffRegTmpInt; // интегральная часть 
      if ( regTmpInt > maxSetPower) regTmpInt= maxSetPower; // ограничение сверху
      if ( regTmpInt < 0 ) regTmpInt= 0;                    // ограничение снизу

      regTmpPr= regTmpErr * koeffRegTmpPr; // пропорциональная часть       
      regTmpDif= (regTmpErr - regTmpErrPrev) * koeffRegTmpDif;  // дифференцирующая часть   
      regTmpErrPrev= regTmpErr; // перегрузка предыдущей ошибки

      setPower= regTmpInt + regTmpPr + regTmpDif; // сумма составляющих
      if ( setPower > maxSetPower) setPower= maxSetPower; // ограничение сверху
      if ( setPower < 0 ) setPower= 0;                    // ограничение снизу       
    }

Я даже не знаю, что в нем пояснять. Все “разжевано” в предыдущем уроке. Обратите внимание на полярность ошибки рассогласования регулятора.

  • В регуляторе мощности при реальной мощности меньше заданной надо было увеличивать ШИМ. Больше ШИМ, больше мощность.
  • В регуляторе температуры мощность надо увеличивать при реальной температуре больше заданной. Мы охлаждаем, а не нагреваем. И чем больше мощность, тем меньше температура.

Остается вывести промежуточные и основные результаты работы регулятора на компьютер. Я решил, что интересно будет видеть:

  • ошибку рассогласования;
  • заданную мощность;
  • интегральную составляющую;
  • пропорциональную составляющую;
  • дифференциальную составляющую.

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

С выводом данных на компьютер через последовательный порт все не так просто. Функция Serial.print() помещает данные в передающий буфер последовательного порта. А встроенный класс Serial передает эти данные через аппаратный интерфейс UART. Но размер буфера Serial не безграничен.

Для Arduino UNO R3 размеры буферов приема и передачи составляют 64 байт. Поэтому при большем размере передаваемых данных мы должны делать паузы на передачу данных через аппаратный интерфейс UART. Т.е. для того, чтобы данные ”ушли” из буфера в UART. В нашем случае скорость передачи данных 19200 бод. Один байт передается за 0,5 мс. За цикл 20 мс будет передано 40 байтов.

За один цикл 20 мс все данные не передать. Надо передачу данных на компьютер разбивать на интервалы по 20 мс. Пока я выбрал 10 и 11 интервалы.

    if (cycle20mcCount == 10) {
      //--------------------------- интервал 10, передача информации на компьютер
      Serial.print("U="); Serial.print(measureU, 2);  // напряжение
      Serial.print(" I="); Serial.print(measureI, 2);  // ток
      Serial.print(" P="); Serial.print(measureP, 2);  // мощность
      Serial.print(" p="); Serial.print(regPwrInt, 2);  // интегральное звено регулятора мощности
      Serial.print(" t="); Serial.print(measureTempRef, 2);  // температура в камере
      Serial.print(" t="); Serial.print(measureTempRad, 2);  // температура радиатора
    }

    if (cycle20mcCount == 11) {
      //--------------------------- интервал 11, передача информации на компьютер
      Serial.print(" E="); Serial.print(regTmpErr, 2);  // ошибка рассогласования
      Serial.print(" W="); Serial.print(setPower, 2);  // заданная мощность
      Serial.print(" I="); Serial.print(regTmpInt, 2);  // интегральная часть
      Serial.print(" P="); Serial.print(regTmpPr, 2);  // пропорциональная часть
      Serial.print(" D="); Serial.print(regTmpDif, 2);  // дифференциальная часть
      Serial.println(" ");
    }

Все. Регулятор температуры готов. Можете загрузить итоговый скетч программы.

 

Проверка ПИД регулятора температуры.

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

А сейчас мы проверим принципиальную работу всех составляющих регулятора. Собственно системы с элементом Пельтье у нас еще нет.

 

Выбор коэффициентов регулятора.

Выберем коэффициенты, заодно разберем их физический смысл в нашей системе.

Выбор коэффициентов сильно зависит от конструкции системы охлаждения и прежде всего от инерционности системы.

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

Пропорциональный коэффициент. Я выбрал значение koeffRegTmpPr = 0,5. Это значит, что при ошибке рассогласования 1 C° пропорциональное звено даст 0,5 Вт на элементе Пельтье. Допустим, мы включили нагретый холодильник. Разница между заданной и реальной температурами 10 C°. В этом случае  пропорциональная составляющая мгновенно задаст регулятору мощности 5 Вт.

Интегральный коэффициент. Я выбрал koeffRegTmpInt = 0,002. Это значит, что при ошибке рассогласования 1 C° каждые 1 сек выходная мощность будет увеличиваться на 0,002 Вт. За 8 минут она увеличится на 1 Вт. При ошибке рассогласования 10 C° мощность будет увеличиваться в 10 раз быстрее, т.е. на 1 Вт за 50 сек.

Скорее это слишком большой коэффициент. В реальной системе я бы сделал его еще на порядок меньше.

Дифференцирующий коэффициент. Я задал koeffRegTmpDif = 50. Это значит, что при изменении ошибки рассогласования на 0,1 C° мощность изменится на 5 Вт.

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

Представьте себе. Температура увеличилась на 0,1 C°. Дифференцирующая составляющая выдала уменьшение заданной мощности на 5 вт. Но уменьшение мощности будет сформировано только  в течение одной единицы временной дискретности регулятора, 1 сек. Вряд ли система охлаждения успеет отработать. Еще есть регулятор мощности. В принципе его быстродействие позволяет отработать за такое время, но мы значительно снизили скорость его работы из соображений щадящего режима для модуля Пельтье.

Мы можем значительно увеличить дифференцирующий коэффициент. Тогда при медленном регуляторе мощности произойдет следующее. Допустим, при изменении ошибки рассогласования дифференцирующее звено даст такую составляющую, которая уменьшит заданную мощность до 0. Это значение будет держаться в течение 1 сек. Регулятор мощности у нас работает в цикле 20 мс, т.е. 50 проходов за 1 сек. Я хочу сказать, что в этом случае выброс дифференцирующей составляющей в какой-то мере запомнится в интегральном звене. Но как это будет работать, зависит от нескольких коэффициентов.

Есть еще проблема с дифференцирующим звеном. Датчик температуры у нас дискретный, с разрешением 0,0625 C°. Как и любой узел преобразования аналоговой величины в цифровую он имеет особенность, связанную со значением выходного кода на границе ступеней преобразования. Код на границах ”скачет” на плюс минус единицу преобразования. В нашем случае значение температуры будет ”дергаться” на 0,0625 C°. Это будет воспринято дифференцирующим звеном и не лучшим образом повлияет на работу регулятора в целом. Ниже я покажу этот эффект.

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

 

Проверка ПИД регулятора температуры на реальной схеме.

К прежней схеме добавились два датчика температуры. А в остальном все также: блок питания, плата Ардуино, импульсный регулятор, нагрузка 20 Ом.

Ардуино контроллер холодильника

Установил в программе заданную температуру 20 C°.

setTempRef= 20.;  // временно заданная температура

Загрузил скетч с выбранными коэффициентами в плату. Запустил монитор последовательного порта. Блок питания 12 В пока не подключал.

Результаты испытаний

Ошибка рассогласования E = 3.22 C°.

Пропорциональная составляющая должна быть 3,22 * koeffRegTmpPr = 3,22 * 0,5 = 1,61 Вт. Так и есть P=1.61.

Интегральная составляющая должна увеличиваться каждую секунду на 3,22 * koeffRegTmpInt = 3,22 *0,002 = 0,0064 Вт. То же все правильно. За 20 секунд (20 строчек) I увеличилось с 0 до 0,13.

В последних строчках видно, что ошибка рассогласования увеличилась на 0,08 C°. Дифференциальная составляющая должна быть 0,08 * koeffRegTmpDif = 0,06 * 50 = 3 Вт. Регулятор отработал правильно D=3,13. Небольшая неточность, объясняется тем, что мы выводим только 2 знака после запятой.

Установил заданную температуру 25 C° и увидел, что ошибка рассогласования стала отрицательной E=-1.53 C°, регулятор отключился, пропорциональное звено дало правильное значение -1,53 * 0,5 = -0,765 Вт.

Результаты испытаний

Вернул заданную температуру 20 C°. Подключил блок питания 12 В. Увидел, как меняется реальная мощность на нагрузке.

Результаты испытанийМощность растет и останавливается на заданном в программе пределе 5 Вт. Все правильно.

Скачек температуры на 0,07 C° дифференциальное звено отработало правильно, но на выходной мощности это не отразилось.

Результаты испытанийА вот те самые ”скачки” температуры на границе ступеней преобразования датчика.

Результаты испытанийВидно, как они отражаются на интеграторе регулятора мощности p, т.е. на ШИМ. Выше я писал об этом.

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

 

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

Предыдущий урок     Список уроков     Следующий урок

6 комментариев на «Урок 41. Разработка контроллера элемента Пельтье. ПИД регулятор температуры.»

  1. Вопрос к автору: А Вы не ошиблись со знаком дифференциальной составляющей? ведь при резком уменьшении сигнала рассогласования выходной дифференциальная составляющая должена уменьшаться? Или не так?

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

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

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

  3. Мне необходимо контролировать пельтье от перегрева, если они запаралленны, то контроль отсутствует, и большая вероятность что один будет больше нагреваться, чем другой, в итоге кпд ниже чем с контролем, а у меня работают они на предельных нагрузках и велика вероятность выхода из строя пельтье.

    • Контроль температуры горячей стороны это самая простая задача. Измеряйте температуру нескольким датчиками и по ним отрабатывайте защиту. В следующем уроке я сделаю защиту от перегрева.
      Кстати, может открыть тему на форуме сайта. Кто-то поделится своим опытом.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *