Стоит задача - спроектировать самодельную инфракрасную паяльную станцию. Станция состоит из двух нагревателей: верхнего и нижнего. Нагреватели должны нагревать плату в соответствии с определенным профилем температуры. Температура будет определяться по показаниям термопары (одной или нескольких).
На данном этапе я приступил к реализации алгоритма поддержания заданной температуры (ПИД регулятор). Интересно? Добро пожаловать под "кат".
Регулировать температуру я решил с помощью самодельного фазового регулятора. Видео можно посмотреть ниже.
https://www.youtube.com/watch?v=jSGbDmSUytAhttps://www.youtube.com/watch?v=-QZWYIpy9j0https://www.youtube.com/watch?v=hXBmD3VHRQo
Сначала, для управления будущей паяльной станцией я решил задействовать Arduino. Предполагалось, что фазовым регулятором будет управлять одна плата Arduino nano, а остальной функционал будет обеспечиваться второй платой. В видео я как раз экспериментировал с фазовым регулятором на базе Arduino.
Позже я решил поэкспериментировать с отладочной платой Nucleo F410RB. Она уже несколько месяцев пылилась на полке моего шкафа. Мне показалось интересным попробовать mbed os с её многопоточностью для реализации алгоритма управления паяльной станцией. Забегая вперед скажу, что mbed os оставила о себе положительное впечатление.
Панель управления
Еще до начала реализации, когда паяльная станция была только в мыслях, я начал задумываться: "Каким бы мне хотелось видеть панель управления этой станцией?" Самым простым было бы налепить разных кнопочек и индикаторов, но хотелось чего - то более современного. Я начал искать подходящие сенсорные панели и наткнулся на дисплей Nextion. Это сенсорный экран, совмещенный с микроконтроллером. Управление дисплеем осуществляется по UART. Его преимуществом является графический редактор интерфейса "Nextion Editor" , в котором можно быстро сверстать все необходимое.
Интерфейс приложения делится на страницы. На каждой странице можно добавлять различные элементы (кнопки, текстбоксы, графики, лейблы и.т.д). У каждого элемента есть свои свойства и события. Например, кнопка "Старт" на фотографии выше имеет обработчик события "Touch Press Event", в котором содержится следующий код:
1 2 3 4 5 |
print "xsd=" print h1.val print " " page2.tempg.val=h1.val page 2 |
Первые три строчки кода выводят в UART порт строку "xsd=89 ", где 89 - температура, установленная слайдером. page2.tempg.val=h1.val задает значение элемента tempg, который находится на второй странице и отображает требуемую температуру. h1 - это слайдер. page 2 - команда перехода на вторую странцу интерфейса.
Полный набор инструкций можно посмотреть на https://www.itead.cc/wiki/Nextion_Instruction_Set
А вот небольшой пример кода, который наоборот присылает данные из nucleo в дисплей
1 2 3 4 5 6 7 8 |
// tempn - значение температуры, отображаемое на экране Nextion (экран со слайдером) s2.printf("tempn.val=%d%c%c%c",temp,255,255,255); // отправляем три одинаковых точки на график s2.printf("add 1,0,%d%c%c%c",temp,255,255,255); s2.printf("add 1,0,%d%c%c%c",temp,255,255,255); s2.printf("add 1,0,%d%c%c%c",temp,255,255,255); // tempz - значение текущей температуры, отображаемое на странице Nextion с графиком s2.printf("tempz.val=%d%c%c%c",temp,255,255,255); |
Здесь стоит обратить внимание, что все команды, отправляемые в дисплей должны оканчиваться последовательностью FF FF FF (255 255 255).
Выбор среды разработки для mbed os
С отладочной платой и панелью управления определился, теперь нужно подумать о среде программирования.Первое моё знакомство с mbed os началось с официального сайта и официального онлайн компилятора. Он находится по адресу https://os.mbed.com/compiler/.
Онлайн компилятор предоставляет очень простой способ программирования отладочных плат nucleo. Всё, что нужно - подключить плату к компьютеру(она определится как флешка) и загрузить в нее скомпилированный файл программы. Единственное неудобство такой разработки - постоянное подключение к интернету.
Сейчас есть очень удобный offline метод. Проект называется Platformio. Я установил версию на базе VSCode. По отзывам - это наиболее удобная платформа. Она из коробки настроена для работы со многими отладочными платами. Моя -nucleo F410RB тоже есть в списке. Для моей платы platformio сразу же предлагает mbed в качестве фреймворка.
Как уже говорил ранее, в своём проекте я планирую использовать RTOS (real time operating system). Здесь крылся один из подводных камней Platformio. Нельзя просто взять и добавить в main.cpp строчку #include "rtos.h". Сначала я так и сделал. При этом проект напрочь отказывался собираться. Поискав информацию по теме в интернете я обнаружил, что в проекте platformio есть файл platformio.ini. Поддержку rtos нужно включить еще и в нем. Файл при этом примет следующий вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env:nucleo_f410rb] platform = ststm32 board = nucleo_f410rb framework = mbed build_flags = -DPIO_FRAMEWORK_MBED_RTOS_PRESENT |
Программная реализация
Фазовый регулятор. Немного теории.
Теперь проект компилируется и запускается. Пора приступать к написанию кода. В предыдущих видео я управлял фазовым регулятором с arduino. Теперь попробуем переписать тоже самое, только на mbed os.
Как мы помним, фазовый регулятор обрезает часть синусоиды переменного тока, что приводит к уменьшению выдаваемой мощности.
Таким образом мне нужно задавать задержку включения симистора в микросекундах относительно пересечения синусоиды с 0. Здесь есть один важный нюанс. Я знаю, что полупериод длится 10000мкс. Допустим я хочу включить нагреватель на 10% мощности. Какую задержку мне нужно задать? Если я задам задержку в 1000 мкс(10% от полупериода), то мощность не будет равна 10%, так как площадь части синусоиды вначале периода меньше, чем, допустим, в середине.
На помощь мне пришла статья на habr.com. Раздел "Управление мощностью". Мгновенная мощность вычисляется по следующей формуле:
Количество выделенного тепла
В общем виде получаем, что максимальное количество тепла за период от 0 до Pi равно
Вместо Pi запишем T*Pi, где T - коэффицент от 0 до 1 для того чтобы получить значение тепла на отрезке от 0 до T*Pi
Получается, что при T=1 мы получем максимальное количество тепла за время от 0 до Pi
Введем переменную Q, принимающую значения от 0 до 1. Таким образом мы получим уравнение, в которое можно вместо Q подставить нужное значение количества тепла, например, 0.1, что соответствует 10%. Решив его получим значение T, которое покажет какую часть периода нужно оставить, чтобы получить эти 10% тепла.
Решал уравнение я по методу автора статьи с помощью консольного приложения qtcreator. В результате получил массив значений. Порядковый номер соответствует требуемой мощности от 0 до 250. Значение соответствует требуемой задержке. Например d[200]=3363 говорит, что для того чтобы получить мощность 200 необходима задержка 3363мкс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); //# (T*Pi)/2 - sin(2*T*Pi)/4 = Q*Pi/2 double T = 1; double in; double ev; for( int i = 250; i >= 0; i-- ) { in = i*M_PI/2/250; ev = (T*M_PI)/2-sin(2*T*M_PI)/4; while( fabs(ev-in) > 0.00001 ) { T -= 0.000001; ev = (T*M_PI)/2-sin(2*T*M_PI)/4; } std::cout<<"d["; std::cout<<i; std::cout<<"]="; std::cout<<abs((1-T)*10000); std::cout<<";n "; } return a.exec(); } |
Результат работы программы
Ради интереса построил график в excel, на котором отражена разбивка по 10% в соответствии с полученным массивом.
Фазовый регулятор. Код.
Фазовый регулятор описан классом PowerControl. Он содержит обработчик прерывания ZeroCross_ и пять таймеров. По одному для каждого нагревателя.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class PowerControl { private: int dim1,dim2,dim3, dim4, dimup; //мощность от 0 до 250 (0-минимальная; 250 - максимальная) Ticker t1,t2,t3,t4,t5; // таймеры для влкючения и выключения симисторов InterruptIn ZeroCross_;// прерывание срабатывает по сигналу от детектора 0 /*массив D[Q]=t, где Q- мощность от 0 до 250(0-минимальная; 250-максимальная) t- задержка в микросекундах * чем больше задержка, тем познее включится симистор, тем меньше мощность. Значения t посчитаны заранее, чтобы получить линейное приращение мощности * ссылка на статью https://habr.com/post/145991/ */ int d[251]; DigitalOut h1_,h2_,h3_,h4_,h5_; // выводы для включения симисторов void Crossing(void); // обработчик прерывания детектора 0 void DimHeater1UP(); // Обработчик таймера, включающий симистор void DimHeater1Down();// обработчик таймера, выключающий симистор void DimHeater2UP(); void DimHeater2Down(); void DimHeater3UP(); void DimHeater3Down(); void DimHeater4UP(); void DimHeater4Down(); void DimHeater5UP(); void DimHeater5Down(); void setD(void); public: /* * Конструктор PowerControl * ZeroCross - пин, на который приходит сигнал с детектора 0 * h1...hup - пины, на которых формируется управляющий сигнал для симисторов */ PowerControl(PinName ZeroCross, PinName h1, PinName h2, PinName h3, PinName h4, PinName hup); void SetDimming(int d1, int d2, int d3, int d4, int dup); // Задает мощность для каждого из каналов от 0 до 250 }; |
Посмотрим на обработчик прерывания. Как только синусоида проходит через 0, он взводит пять таймеров, по одному на каждый нагреватель. Время для каждого берется из массива, который мы вычислили заранее.
1 2 3 4 5 6 7 8 9 |
void PowerControl::Crossing() { //d[Q] - в массиве хранятся значения микросекунд для мощности Q, Q=0 - минимальная мощность, Q=250 - максимальная мощность t1.attach_us(callback(this,&PowerControl::DimHeater1UP),d[dim1]); t2.attach_us(callback(this,&PowerControl::DimHeater2UP),d[dim2]); t3.attach_us(callback(this,&PowerControl::DimHeater3UP),d[dim3]); t4.attach_us(callback(this,&PowerControl::DimHeater4UP),d[dim4]); t5.attach_us(callback(this,&PowerControl::DimHeater5UP),d[dimup]); } |
А вот обработчики для первого таймера:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void PowerControl::DimHeater1UP() { h1_ = 1; // подаем на управлюящий первым симистором выход единицу t1.detach();// взводим таймер заново, чтобы через 300мкс снять управляющий сигнал с симистора t1.attach_us(callback(this,&PowerControl::DimHeater1Down),5); } // DimHeater1Down() обработчик преывания, отключающий управляющий сигнал первого симистора // сам симистор отключится не сразу, а как только синусоида перейдет через 0 значение void PowerControl::DimHeater1Down() { h1_ = 0; t1.detach(); } |
Как видите, с помощью таймеров задача управления пятью нагревателями решается гораздо проще, чем в предыдущем варианте на ардуино. При этом весь функционал содержится внутри одного класса. В программе нам всего лишь нужно будет создать экземпляр этого класса и задать необходимую мощность.
ПИД регулятор
Для установки требуемой температуры будем использовать Пропорционально-интегрально-дифференцирующий регулятор. Введем такое понятие, как "ошибка".
Ошибка - разность температур (заданной и текущей) Error = tзад - tтек.
ПИД регулятор состоит из трех составляющих.
- Пропорциональная составляющая - разность температур, умноженная на пропорциональный коэффициент. Эта составляющая позволяет работать нагревателю, пока есть разность температур.(Error*Kp)
- Дифференцирующая составляющая - ошибка на предыдущем шаге минус текущая ошибка и всё это умноженное на дифференцирующий коэффициент. (Error(пред.) - Error)*Kd)
- Интегрирующая составляющая - сумма всех ошибок Error, умноженная на интегрирующий коэффициент.
Для некоторых систем достаточно только одной - пропорциональной составляющей. Например, я пока использую вместо керамического нагревателя лампочку накаливания. Она нагревает небольшой металлический предмет. В этом случае система почти не имеет инерции. И пропорциональный регулятор поддерживает заданную температуру достаточно точно.
Керамические нагреватели нагреваются дольше чем лампочка, но и остывают дольше. Таким образом появляется инерция, и пропорциональный регулятор будет перегревать систему, температура будет колебаться возле заданного значения. Здесь нам уже потребуется дифференцирующая составляющая. Она позволит предвидеть перегрев и подойти к настраиваемой температуре более плавно.
Как только мы начнем использовать пропорциональную и дифференцирующую составляющую, возникнет ситуация, когда они установят равновесие немного раньше заданного значения. В этом случае нам поможет интегрирующая составляющая. Она за несколько шагов накопит ошибку системы и добавит недостающую мощность. Температура снова установится на заданном значении.
В моей программе ПИД регулятор оформлен ввиде класса. Он содержит таймер, который через равные промежутки времени рассчитывает значения мощности и выставляет эту мощность на фазовом регуляторе.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* * Автор - Железняков Андрей * Сайт - itworkclub.ru * Класс pid представляет реализацию ПИД регулятора для нагревателя * Класс содержит таймер, который постоянно вычисляет требуемую мощность нагревателя * исходя из текущей и требуемой температуры. */ #ifndef PID_H #define PID_H #include "mbed.h" #include "max6675.h" #include "PowerControl.h" class pid { private: int kp; // коэффицент пропорционального регулятора int ki; int kd; int previousError; int integral; max6675 &max; // ссылка на объект термопары PowerControl &pcontrol; // ссылка на фазовый регулятор float requered_temp; // требуемая температура volatile float current_temp;// текущая температура RtosTimer *tim2; // таймер вызывает функцию Compute для вычисления мощности volatile int power; // рассчитанная мощность static void Compute(void const *arguments); // функция вычисляет можность исодя из заданной и текущей температуры public: pid(max6675 &m, PowerControl &pc,int kp_, int kd_, int ki_); float ReadTemp(); // возвращает текущую температуру не опрашивая термопару void SetTemperature(float t_); // задает требуемую температуру int Power(); // возвращает рассчитанную мощность float temp(); // возвращает текущую температуру из датчика max6675 }; #endif |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
/* * Автор - Железняков Андрей * Сайт - itworkclub.ru * Класс pid представляет реализацию ПИД регулятора для нагревателя * Класс содержит таймер, который постоянно вычисляет требуемую мощность нагревателя * исходя из текущей и требуемой температуры. */ #include "pid.h" /* pid::pid(max6675 &m,int kp_) * в конструктор передаем ссылку m на темопару max6675, ссылку pc на фазовый регулятор и коэффиценты регулятора kp_, kd_, ki_ */ pid::pid(max6675 &m, PowerControl &pc,int kp_, int kd_, int ki_):max(m), pcontrol(pc) { kp = kp_; kd = kd_; ki = ki_; previousError = 0; integral =0; requered_temp=30; //заданная температура по умолчанию power = 0; // tim2- таймер, который считывает температуру и вычисляет мощность по алгоритму ПИД регулятора tim2= new RtosTimer(Compute, this); tim2->start(1000); } float pid::ReadTemp() { return current_temp; } void pid::SetTemperature(float t_) { requered_temp = t_; } void pid::Compute(void const *arguments) { pid *self = (pid*)arguments; int error,x; self->current_temp = self->temp(); error = self->requered_temp-self->current_temp; x = (error - self->previousError)*self->kd; self->previousError = error; x+=error*self->kp; self->integral+=self->ki*error; x+=self->integral; if (x>0){ if (x<=249) self->power = x;; if (x>249) self->power = 249; } else{ self->power = 0; } self->pcontrol.SetDimming(self->power,self->power,1,1,1); } int pid::Power() { return power; } float pid::temp() { return max.read_temp(); } |
На данном этапе у меня получился пропорциональный регулятор температуры. Как только в моём распоряжении появятся керамические нагреватели, буду настраивать ПИД регулятор, подбирать необходимые коэффициенты.
Уведомление: Проектирование инфракрасной паяльной станции (Часть 2) — ITWORKCLUB
Какие коэффициенты Вы используете для
int kp;
int ki;
int kd;
?
Я так понял проект заброшен. А жаль. Идея хорошая. Была.