Нагрев
Итак, самое главное, что должен делать контроллер печи – это управлять нагревательным элементом.
При этом управлять напрямую мощностью нагрева через подачу различного напряжения («чуть-чуть», вполсилы включить) – мы не можем. Точнее, можем, конечно – например, поставить какой-нибудь подстроечный резистор – но вся проблема в том, что вся энергия, которую мы «отберем» у печки, пойдет в тепло на этот самый резистор (смотрим закон Ома, определение мощности и страдаем -_-).
Так что единственное, что мы можем делать – это включать-выключать весь нагревательный элемент.
Попробуем рассмотреть, как это можно сделать:
- Включение-выключение
Это самое простое, что можно придумать: печка остыла – включаем нагрев, печка достигла нужной температуры – выключаем нагрев, печка снова остыла – включаем нагрев и так далее.
Результат у нас будет примерно такой:
Рисунок 1. Динамика изменения температуры во времени при включении-выключении нагрева.
Красным на рисунке отображено состояние, когда нагрев включен, черным – выключен.
Так, при нагреве мы «перескакиваем» нужное нам значение температуры – даже после отключения печки температура по инерции продолжает повышаться; затем, когда печка начинает остывать и доходит до нужного нам значения, мы снова включаем нагрев – но вот незадача, температура все равно падает, опять же по инерции! Так и приходится работать по синусоиде – получается не очень-то красиво.
- Простой ШИМ
Мы определили, что включать нагрев чуть-чуть мы не можем – но у нас есть такая вещь, как ШИМ!
ШИМ – широтно-импульсная модуляция; «это способ кодирования аналогового сигналa путём изменения ширины (длительности) прямоугольных импульсов несущей частоты».
Теперь попробуем объяснить понятней: мы не можем задавать напряжение на ножке микроконтроллера – у нас есть либо вариант «логическая 1» - оно же напряжение питания – либо «логический 0» - подтяжка к земле.
Однако если мы будем в течение не очень большого времени посылать то 1, то 0, то «в среднем» будет уже другая ситуация:
Рисунок 2. Пример ШИМ с различной скважностью.
Посмотрим характеристики ШИМа:
Рисунок 3. Характеристики ШИМ.
Период – это длительность прямоугольного импульса
Скважность – это соотношение длительности логической 1 к периоду.
Квант ШИМа – это минимальная длительность логической 1 в импульсе.
Если говорить об аппаратных ШИМах в avr, то квант ШИМа – один «тик» соответствующего таймера.
Используя ШИМ, мы сможем, оперируя включением-выключением, перейти к работе с мощностью: сделать нагрев 10%, 50%, 90%...
Понятно, что для каждой температуры в пределах разумного мы можем подобрать такое значение мощности, при котором бы эта самая температура оставалась неизменной, то есть Wнагрева = Wрассеивания (как в кухонных духовках).
Теперь основная проблема – а как же сопоставить эти самые мощность (W) и температуру (T)
Мы знаем, как зависит температура в печке от мощности: в идеальных условиях это линейная зависимость T = a * W + b, а вот в реальности – увы, из-за конвекции воздуха зависимость преобретает примерно следующий вид:
Рисунок 4.Зависимость температуры от мощности нагрева.
Сначала, когда разница между температурой печи и температурой окружающей среды небольшая, сохраняется примерно линейная зависимость, однако при возрастании температуры функция зависимости все больше напоминает формулу .
Вроде бы все хорошо, и решение, в принципе, найдено: каким-нибудь образом – например, калибровкой или настройкой – мы сопоставим мощность температуре. будем использовать ШИМ, и тогда у нас практически не будет перевалов через заданную температуру, мы будем спокойно нагревать печку такой силой, чтобы температура не менялась.
Но, к сожалению, реальность разбивает наши мечты.
Вся проблема в том, что частота сети у нас – 50 Гц. То есть, мы не можем включать-выключать питание нагревательного элемента чаще, чем 1/50 с, более того, лучше не делать этого чаще, чем 1/16 – 1/8 с. Это проблема номер раз – таким образом, и квант ШИМа должен быть не быстрее, чем 1/16 секунды.
Таймер/счётчик 0 и таймер/счётчик 2 в асинхронном режиме уже заняты (так, в асинхронном режиме квант ШИМа получается 8/32768 ˜ 0.00024 с – очень мало); попробуем рассчитать квант ШИМа для обычного 16-разрядного таймера с максимальным предделителем 1024: получается 1024/8000000 = 0.000128 c. Слишком быстро! Но можно считать квант ШИМа как два тика таймера, как три… Оптимальным числом будет являться 1000 – тогда квант будет 1024/8000000*1000 = 0.128 с. Неплохо вроде, жить можно… А теперь ещё немного разочарования: тТаймер у нас 16-разрядный, то есть считает до 65535; но если квант ШИМа – это 1000 тиков таймера, то… Получаем 65635/1000 = 65 возможных значений ШИМа. Маловато будет!
Так что аппаратный таймер, похоже, нам не светит. Будем делать программный, сами, тогда сможем задать и длительность кванта ШИМа, и количество «делений» ШИМа.
Едем дальше.
Представим, что мы измеряем температуру от 0 до 300 градусов с точностью 0.5 градуса – то есть, всего 600 возможных значений.
Чтобы точно сопоставить температуре мощность, нужно использовать градацию мощности большую, чем градация температуры, иначе мы получим такую ситуацию: например, нам нужно поддерживать в печке температуру 100 ˚С; при этом при значении ШИМа «1000» поддерживается температура 99.5 ˚С, а при значении «1001» - температура 100.5 ˚С. Проблема? Проблема!
Тогда или мы делаем градаций ШИМа в несколько раз больше, чем градаций температуры – например, раз в 10 – но тогда понятно, что период ШИМа затянется на много секунд, и это будет очень неторопливая печка, или, например, можно использовать ШИМ вместе с выключением – нагрели до 100.5 градусов, выключили, снова нагрели… Тут тоже будет «синусоида», как и в первом варианте – простом включении-выключении, но уже с меньшей амплитудой.
- Комбинированный ШИМ
Как же реализовать ШИМ, который будет точно подходить к заданному значению температуры, и при этом у него будет не очень большой период?
А вот как – через «чередование»: можно сделать так, чтобы значение ШИМа было то 1000, то 1001 – если смотреть предыдущий пример.
Рисунок 5.Схема комбинированного ШИМ.
Таким образом, в зависимости от доли того или иного значения ШИМа мы будем ближе к той или иной температуре (так, если будем каждый раз чередовать 1000 и 1001 значение, то получим среднее – 100 ˚С; если будем 3 раза пускать ШИМ со значением 1000, а на четвертый делать значение 1001, то получим температуру 99.75 ˚С и т.д.)
При этом, если бы мы могли работать с аппаратным ШИМом, нужно было бы переключать значения не очень часто (уж точно не каждый цикл).
По сути своей, комбинированный ШИМ – это ШИМ в ШИМе).
Итак, попробуем сперва реализовать программный ШИМ, который будет включать-выключать нагревательный элемент с заданной частотой.
Рисунок 6.Схема виртуальной машины ШИМ нагревательного элемента.
Получаем следующий заголовочный файл:
//управление нагревательным элементом с использованием ШИМ-а; включается "нулём" #ifndef HEAT_ELEMENT_H #define HEAT_ELEMENT_H #include "ioavr.h" #include "inavr.h" #include "stdint.h" #include "Timings.h" //максимальное значение счётчика печки #define HE_PWM_MAX_COUNTER (CLOCK_TIMER_INTERRUPTS_PER_SECOND * 8) //состояния ШИМ-а печки typedef enum {hpwmOff, hpwmLevelOn, hpwmWaitLevelOff, hpwmLevelOff, hpwmWaitLevelOn} THE_PWM_State; typedef struct { //физическое расположение вывода для управления нагревательным элементом: DDR, PORT и номер вывода uint8_t *heDdr; uint8_t *hePort; uint8_t hePinMask; uint8_t heIsOn: 1; //флаг "печь включена" THE_PWM_State hePWMState; //текущее состояние ШИМ volatile uint8_t hePWMCounter; //счётчик ШИМ uint8_t hePWMLevel; //уровень ШИМ } TVMHeatElement; extern TVMHeatElement VMHeatElement; //прерывание для управления нагревательным элементом (счётчик ШИМ) void HEMachineInterrupt(); //инициализация виртуальной машины управления нагревательным элементом void HEMachineInit(uint8_t *HeatDdr, uint8_t *HeatPort, uint8_t HeatPinNumber); //виртуальная машина управления нагревательным элементом void HEMachine(); //включение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOn(); //выключение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOff(); //установка уровня ШИМ нагревательного элемента void HESetPWMLevel(uint8_t PWMLevel); #endif
Файл .c:
#include "HeatElement.h" TVMHeatElement VMHeatElement; //включение нагревательного элемента #pragma inline=forced void HEOn() { *(VMHeatElement.hePort) &= ˜VMHeatElement.hePinMask; } //выключение нагревательного элемента #pragma inline=forced void HEOff() { *(VMHeatElement.hePort) |= VMHeatElement.hePinMask; } //включение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOn() { VMHeatElement.heIsOn = 1; } //выключение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOff() { VMHeatElement.heIsOn = 0; HEOff(); } //инициализация виртуальной машины управления нагревательным элементом void HEMachineInit(uint8_t *HeatDdr, uint8_t *HeatPort, uint8_t HeatPinNumber) { VMHeatElement.heDdr = HeatDdr; VMHeatElement.hePort = HeatPort; VMHeatElement.hePinMask = 1 << HeatPinNumber; //настраиваем вывод нагревательного элемента: на выход с подтяжкой (нагревательный элемент выключен) *VMHeatElement.hePort |= VMHeatElement.hePinMask; *VMHeatElement.heDdr |= VMHeatElement.hePinMask; VMHeatElement.heIsOn = 0; VMHeatElement.hePWMCounter = 0; VMHeatElement.hePWMLevel = 0; VMHeatElement.hePWMState = hpwmOff; } //прерывание для управления нагревательным элементом (счётчик ШИМ) void HEMachineInterrupt() { if ((VMHeatElement.hePWMState != hpwmOff) && (VMHeatElement.hePWMCounter < HE_PWM_MAX_COUNTER)) VMHeatElement.hePWMCounter++; } //виртуальная машина управления нагревательным элементом void HEMachine() { switch (VMHeatElement.hePWMState) { case hpwmOff: //проверяем на необходимость включения if (VMHeatElement.heIsOn) { VMHeatElement.hePWMCounter = 0; VMHeatElement.hePWMState = hpwmLevelOn; } break; case hpwmLevelOn: //включаем нагревательный элемент HEOn(); VMHeatElement.hePWMState = hpwmWaitLevelOff; break; case hpwmWaitLevelOff: //проверяем, не нужно ли выключить машину if (VMHeatElement.heIsOn == 0) { VMHeatElement.hePWMState = hpwmOff; HEOff(); } //проверяем, не нужно ли выключить нагревательный элемент (дошли до "уровня" ШИМа) else if (VMHeatElement.hePWMCounter >= VMHeatElement.hePWMLevel) VMHeatElement.hePWMState = hpwmLevelOff; break; case hpwmLevelOff: HEOff(); VMHeatElement.hePWMState = hpwmWaitLevelOn; break; case hpwmWaitLevelOn: //проверяем, не нужно ли выключить машину if (VMHeatElement.heIsOn == 0) { VMHeatElement.hePWMState = hpwmOff; HEOff(); } //проверяем, не нужно ли включить нагревательный элемент (дошли до максимума счётчика ШИМа) else if (VMHeatElement.hePWMCounter == HE_PWM_MAX_COUNTER) { VMHeatElement.hePWMCounter = 0; VMHeatElement.hePWMState = hpwmLevelOn; } break; } } //установка уровня ШИМ нагревательного элемента void HESetPWMLevel(uint8_t PWMLevel) { if (PWMLevel < HE_PWM_MAX_COUNTER) VMHeatElement.hePWMLevel = PWMLevel; else VMHeatElement.hePWMLevel = HE_PWM_MAX_COUNTER; }
Добавляем в основном файле функцию прерывания в прерывание часового таймера, и пишем основную функцию:
//прерывание таймера 2 на часовом кварце #pragma vector = TIMER2_OVF_vect __interrupt void ClockTimerInterrupt(void) { LogicalButtonMachineInterrupt(); //работа с счётчиком длительности нажатия для логических кнопок EncoderCounterMachineInterrupt(); FurnaceTimerInterrupt(); HEMachineInterrupt(); } ... void main( void ) { DDRB = 0; PORTB = 0; DDRD = 0; PORTD = 0; DDRC = 0; PORTC = 0; DisplayInit(); InitButtonsAndEncoder(); ADCInit(); FurnaceTimerInit(); HEMachineInit((uint8_t*)&DDRC, (uint8_t*)&PORTC, 1); HESetPWMLevel(HE_PWM_MAX_COUNTER / 3); HEMachineOn(); FastTimerInit(); ClockTimerInit(); while(1) { HEMachine(); } }
Рисунок 6. Пример работы ШИМ.
Период ШИМ – 8 секунд, как и планировалось.
Обратите внимание, в функции выключения машины нагрева помимо снятия флага IsOn мы принудительно выключаем физический элемент - на всякий случай!
Теперь задача посложнее: научиться выбирать подходящий уровень ШИМ.
Поразмыслив, мы решили реализовать ПИД-регулятор.
Если кратко, то на вход ПИД-регулятор в нашем случае получает разницу между заданной и текущей температурами, а на выход выдаётся уровень ШИМ-а для нагревательного элемента.
ПИД-регулятор включает в себя, соответственно, пропорциональную (P), интегральную (I) и дифференциальную (D) составляющую.
В общем случае формула ПИД-регулятора выглядит так:
Рисунок 7. Формула ПИД-регулятора.
Где e(t) – текущее значение ошибки, то есть разница между заданной и текущей температурой, а Kp, Ki , Kd – соответственно, коэффициенты ПИД-регулятора. Выглядит жутко, да.
Для программной реализации мы будем использовать дискретные формулы:
u(t) = P (t) + I (t) + D (t)
P(t) = Kp * e (t)
I(t) = I (t — 1) + Ki * e (t)
D(t) = Kd * {e (t) — e (t — 1)}
Таким образом, пропорциональная составляющая зависит исключительно от разницы между текущей и заданной температуры, интегральная – накапливает ошибку, и постепенно её влияние возрастает (работает как бы «с запаздыванием»), а дифференциальная составляющая пропорциональна темпу изменений («придает ускорение»). Отметим сразу, что коэффициент интегральной составляющей обычно меньше единицы; при этом получение данных и, соответственно, регулирование уровня ШИМ должно производиться через равные промежутки времени – поэтому машина контроля за уровнем нагрева будет выглядеть примерно так же, как и машина отображения кухонного таймера или машина отображения текущей температуры – в прерывании будет некоторый счётчик, и по достижению максимума в основной программе будет запускаться функция регулировки уровня ШИМ нагрева печи.
Добавим в библиотеку ADCTemperature переменную, хранящую заданную температуру.
Так выглядит заголовочный файл:
//управление нагревательным элементом с использованием ШИМ-а; включается "нулём" #ifndef HEAT_ELEMENT_H #define HEAT_ELEMENT_H #include "ioavr.h" #include "inavr.h" #include "stdint.h" #include "Timings.h" #include "ADCTemperature.h" //максимальное значение счётчика ШИМ #define HE_PWM_MAX_COUNTER (CLOCK_TIMER_INTERRUPTS_PER_SECOND * 8) //маскимальное значение счётчика контроля (частота вызова ПИД-регулятора) #define HE_PID_MAX_COUNTER (CLOCK_TIMER_INTERRUPTS_PER_SECOND) //максимальная энергия нагрева #define HE_MAX_ENERGY 2000 //состояния ШИМ-а печки typedef enum {hpwmOff, hpwmLevelOn, hpwmWaitLevelOff, hpwmLevelOff, hpwmWaitLevelOn} THE_PWM_State; typedef struct { //физическое расположение вывода для управления нагревательным элементом: DDR, PORT и номер вывода uint8_t *heDdr; uint8_t *hePort; uint8_t hePinMask; uint8_t heIsOn; //флаг "печь включена" uint8_t heOnNeedControl; //посылка о необходимости пересчётка уровня ШИМ THE_PWM_State hePWMState; //текущее состояние ШИМ volatile uint8_t hePWMCounter; //счётчик ШИМ uint8_t hePWMLevel; //уровень ШИМ volatile uint8_t hePIDCounter; //счётчик частоты регулирования уровня ШИМ double hePIDPCoef; double hePIDICoef; double hePIDDCoef; double hePIDIValue; double hePIDTemperatureDiff; } TVMHeatElement; extern TVMHeatElement VMHeatElement; //прерывание для управления нагревательным элементом (счётчик ШИМ) void HEMachineInterrupt(); //инициализация виртуальной машины управления нагревательным элементом void HEMachineInit(uint8_t *HeatDdr, uint8_t *HeatPort, uint8_t HeatPinNumber); //виртуальная машина управления нагревательным элементом void HEMachine(); //включение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOn(); //выключение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOff(); //установка уровня ШИМ нагревательного элемента void HESetPWMLevel(uint8_t PWMLevel); #endif
Файл .c:
#include "HeatElement.h" TVMHeatElement VMHeatElement; //включение нагревательного элемента #pragma inline=forced void HEOn() { *(VMHeatElement.hePort) &= ˜VMHeatElement.hePinMask; } //выключение нагревательного элемента #pragma inline=forced void HEOff() { *(VMHeatElement.hePort) |= VMHeatElement.hePinMask; } //включение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOn() { VMHeatElement.heIsOn = 1; } //выключение виртуальной машины управления нагревательным элементом #pragma inline=forced void HEMachineOff() { VMHeatElement.heIsOn = 0; HEOff(); } //инициализация виртуальной машины управления нагревательным элементом void HEMachineInit(uint8_t *HeatDdr, uint8_t *HeatPort, uint8_t HeatPinNumber) { VMHeatElement.heDdr = HeatDdr; VMHeatElement.hePort = HeatPort; VMHeatElement.hePinMask = 1 << HeatPinNumber; //настраиваем вывод нагревательного элемента: на выход с подтяжкой (нагревательный элемент выключен) *VMHeatElement.hePort |= VMHeatElement.hePinMask; *VMHeatElement.heDdr |= VMHeatElement.hePinMask; VMHeatElement.heIsOn = 0; VMHeatElement.hePWMCounter = 0; VMHeatElement.hePWMLevel = 0; VMHeatElement.hePWMState = hpwmOff; VMHeatElement.hePIDCounter = 0; VMHeatElement.hePIDPCoef = 5; VMHeatElement.hePIDICoef = 0; VMHeatElement.hePIDDCoef = 0; VMHeatElement.hePIDIValue = 0; VMHeatElement.hePIDTemperatureDiff = 0; } //прерывание для управления нагревательным элементом (счётчик ШИМ) void HEMachineInterrupt() { if (VMHeatElement.heIsOn) { //увеличение счётчика ШИМ if (VMHeatElement.hePWMCounter < HE_PWM_MAX_COUNTER) VMHeatElement.hePWMCounter++; //увеличение счётчика ПИД-регулятора if (VMHeatElement.hePIDCounter < HE_PID_MAX_COUNTER) VMHeatElement.hePIDCounter++; } } //установка уровня ШИМ нагревательного элемента void HESetPWMLevel(uint8_t PWMLevel) { if (PWMLevel < HE_PWM_MAX_COUNTER) VMHeatElement.hePWMLevel = PWMLevel; else VMHeatElement.hePWMLevel = HE_PWM_MAX_COUNTER; } //всмопогательная функция - виртуальная машина управления ШИМ #pragma inline=forced void HEPWMMachine() { switch (VMHeatElement.hePWMState) { case hpwmOff: //проверяем на необходимость включения if (VMHeatElement.heIsOn) { VMHeatElement.hePWMCounter = 0; VMHeatElement.hePWMState = hpwmLevelOn; } break; case hpwmLevelOn: //включаем нагревательный элемент HEOn(); VMHeatElement.hePWMState = hpwmWaitLevelOff; break; case hpwmWaitLevelOff: //проверяем, не нужно ли выключить машину if (VMHeatElement.heIsOn == 0) { VMHeatElement.hePWMState = hpwmOff; HEOff(); } //проверяем, не нужно ли выключить нагревательный элемент (дошли до "уровня" ШИМа) else if (VMHeatElement.hePWMCounter >= VMHeatElement.hePWMLevel) VMHeatElement.hePWMState = hpwmLevelOff; break; case hpwmLevelOff: HEOff(); VMHeatElement.hePWMState = hpwmWaitLevelOn; break; case hpwmWaitLevelOn: //проверяем, не нужно ли выключить машину if (VMHeatElement.heIsOn == 0) { VMHeatElement.hePWMState = hpwmOff; HEOff(); } //проверяем, не нужно ли включить нагревательный элемент (дошли до максимума счётчика ШИМа) else if (VMHeatElement.hePWMCounter == HE_PWM_MAX_COUNTER) { VMHeatElement.hePWMCounter = 0; VMHeatElement.hePWMState = hpwmLevelOn; } break; } } //всмопогательная функция - виртуальная машина ПИД-регулятора #pragma inline=forced void HEPIDMachine() { double currTemperatureDiff, Energy; if ((VMHeatElement.heIsOn) && (VMHeatElement.hePIDCounter >= HE_PID_MAX_COUNTER)) { //пересчитываем уровень ШИМ if (ADCGetNewTemperatureValue()) //если есть новое значение температуры { //пересчитываем разницу температур currTemperatureDiff = ADCTemperature.atSetTemperature - ADCTemperature.atCurrTemperature; //пересчитываем I-составляющую VMHeatElement.hePIDIValue += VMHeatElement.hePIDICoef * currTemperatureDiff; Energy = (VMHeatElement.hePIDPCoef * currTemperatureDiff) + //P-составляющая VMHeatElement.hePIDIValue + //I-составляющая (VMHeatElement.hePIDDCoef * (currTemperatureDiff - VMHeatElement.hePIDTemperatureDiff)); //D-составляющая VMHeatElement.hePIDTemperatureDiff = currTemperatureDiff; //ограничиваем мощность между 0 и максимально допустимой if (Energy < 0) Energy = 0; else if (Energy > HE_MAX_ENERGY) Energy = HE_MAX_ENERGY; //устанавливаем новый уровень ШИМ HESetPWMLevel((uint8_t)((double)(Energy / HE_MAX_ENERGY) * HE_PWM_MAX_COUNTER)); VMHeatElement.hePIDCounter = 0; } } } //виртуальная машина управления нагревательным элементом void HEMachine() { HEPWMMachine(); HEPIDMachine(); }
Вот проект. Для того, чтобы проверить его работу, в функцию машины PID-регулятора можно добавить отображение значения текущего уровня ШИМ нагрева на дисплее, и попробовать нагреть конец термопары - тогда уровень ШИМ будет постепенно снижаться; прошивка такой программы лежит тут.
Основная проблема – настроить коэффициенты ПИД-регулятора. Есть несколько способов: самый простой и горячо любимый – метод научного тыка. О нём хорошо рассказано у наших товарищей.
Также есть метод Циглера-Никольса – он требует экспериментальных данных на реальном объекте: сначала используем P-регулятор (убираем I- и D-составляющие), P-коэффициент увеличиваем до тех пор, пока на выходе системы (это показания текущей температуры) не установятся колебания с постоянной амплитудой. Тогда текущее значение P-коэффициента фиксируется (Kp*), также измеряется период установившихся колебаний (T).
Значения коэффициентов ПИД-регулятора будут рассчитываться по следующим формулам:
Kp = 0.60 * Kp*
Ki = 1.2 * Kp* / T
Kd = 0,075 * Kp* * T
После установки
Автор - Moriam =ˆˆ=