Проектирование инфракрасной паяльной станции (Часть 1)

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

На данном этапе я приступил к реализации алгоритма поддержания заданной температуры (ПИД регулятор). Интересно? Добро пожаловать под «кат».

Регулировать температуру я решил с помощью самодельного фазового регулятора. Видео можно посмотреть ниже.

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

Позже я решил поэкспериментировать с отладочной платой Nucleo F410RB. Она уже несколько месяцев пылилась на полке моего шкафа. Мне показалось интересным попробовать mbed os с её многопоточностью для реализации алгоритма управления паяльной станцией. Забегая вперед скажу, что mbed os оставила о себе положительное впечатление.

Панель управления

Еще до начала реализации, когда паяльная станция была только в мыслях, я начал задумываться: «Каким бы мне хотелось видеть панель управления этой станцией?» Самым простым было бы налепить разных кнопочек и индикаторов, но хотелось чего — то более современного. Я начал искать подходящие сенсорные панели и наткнулся на дисплей Nextion. Это сенсорный экран, совмещенный с микроконтроллером. Управление дисплеем осуществляется по UART. Его преимуществом является графический редактор интерфейса «Nextion Editor» , в котором можно быстро сверстать все необходимое.

Интерфейс приложения делится на страницы. На каждой странице можно добавлять различные элементы (кнопки, текстбоксы, графики, лейблы и.т.д). У каждого элемента есть свои свойства и события. Например, кнопка «Старт» на фотографии выше имеет обработчик события «Touch Press Event», в котором содержится следующий код:

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 в дисплей

// 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 нужно включить еще и в нем. Файл при этом примет следующий вид:

; 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. Раздел «Управление мощностью».  Мгновенная мощность вычисляется по следующей формуле:

2018-11-03_20-01-49

Количество выделенного тепла

В общем виде получаем, что максимальное количество  тепла за период от 0 до Pi равно

2018-11-03_20-21-22

Вместо Pi запишем T*Pi, где T — коэффицент от 0 до 1 для того чтобы получить значение тепла на отрезке от 0 до T*Pi

2018-11-03_20-39-39

Получается, что при T=1 мы получем максимальное количество тепла за время от 0 до Pi

2018-11-03_20-49-08

Введем переменную Q, принимающую значения от 0 до 1. Таким образом мы получим уравнение, в которое можно вместо Q подставить нужное значение количества тепла, например, 0.1, что соответствует 10%. Решив его получим значение T, которое покажет какую часть периода нужно оставить, чтобы получить эти 10% тепла.

2018-11-03_20-53-23

Решал уравнение я по методу автора статьи с помощью консольного приложения qtcreator. В результате получил массив значений. Порядковый номер соответствует требуемой мощности от 0 до 250. Значение соответствует требуемой задержке. Например d[200]=3363 говорит, что для того чтобы получить мощность 200 необходима задержка 3363мкс.

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\0";
    }
    return a.exec();
}
Результат работы программы

Ради интереса построил график в excel, на котором отражена разбивка по 10% в соответствии с полученным массивом.

Фазовый регулятор. Код.

Фазовый регулятор описан классом PowerControl. Он содержит обработчик прерывания ZeroCross_ и пять таймеров. По одному для каждого нагревателя.

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, он взводит пять таймеров, по одному на каждый нагреватель. Время для каждого берется из массива, который мы вычислили заранее.

 

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]);
} 
А вот обработчики для первого таймера:
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тек.

ПИД регулятор состоит из трех составляющих.

  1. Пропорциональная составляющая — разность температур, умноженная на пропорциональный коэффициент. Эта составляющая позволяет работать нагревателю, пока есть разность температур.(Error*Kp)
  2. Дифференцирующая составляющая — ошибка на предыдущем шаге минус текущая ошибка и всё это умноженное на дифференцирующий коэффициент. (Error(пред.) — Error)*Kd)
  3. Интегрирующая составляющая — сумма всех ошибок Error, умноженная на интегрирующий коэффициент.

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

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

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

В моей программе ПИД регулятор оформлен ввиде класса. Он содержит таймер, который через равные промежутки времени рассчитывает значения мощности и выставляет эту мощность на фазовом регуляторе.

/*
* Автор - Железняков Андрей
* Сайт - 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
/*
* Автор - Железняков Андрей
* Сайт - 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();
}

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

Дополнительные материалы

У этой записи один комментарий

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

Закрыть меню