ПИД-регулятор, не запускается

rules.zip (3,0 КБ)

Не в том порядке код вставили :)
//-----------------------------------------------------------------------

var PID = function(Input, Setpoint, Kp, Ki, Kd, ControllerDirection) {
    this.input = Input;
    this.mySetpoint = Setpoint;
    this.inAuto = false;
    this.setOutputLimits(0, 255); // default output limits
    this.SampleTime = 100; // default Controller Sample Time is 0.1 seconds
    this.setTunings(Kp, Ki, Kd);
    this.setControllerDirection(ControllerDirection);
    this.lastTime = this.millis() - this.SampleTime;

    this.ITerm = 0;
    this.myOutput = 0;
};

// Constants for backward compatibility
PID.AUTOMATIC = 1;
PID.MANUAL = 0;
PID.DIRECT = 1;
PID.REVERSE = 1;

PID.prototype.setInput = function(current_value) {
    this.input = current_value;
};

PID.prototype.setPoint = function(current_value) {
    this.mySetpoint = current_value;
};

PID.prototype.millis = function() {
    var d = new Date();
    return d.getTime();
};

/**
 * Compute()
 * This, as they say, is where the magic happens.  this function should be called
 * every time "void loop()" executes.  the function will decide for itself whether a new
 * pid Output needs to be computed.  returns true when the output is computed,
 * false when nothing has been done.
 */
PID.prototype.compute = function() {
    if (!this.inAuto) {
        return false;
    }
    var now = this.millis();
    var timeChange = (now - this.lastTime);
    if (timeChange >= this.SampleTime) {

        // Compute all the working error variables
        var input = this.input;
        var error = this.mySetpoint - input;
        this.ITerm += (this.ki * error);
        var dInput = input - this.lastInput;
        // Compute PID Output
        var output = (this.kp * error + this.ITerm - this.kd * dInput) * this.setDirection;

        if (output > this.outMax) {
            output = this.outMax;
        }
        else if (output < this.outMin) {
            output = this.outMin;
        }
        this.myOutput = output;

        // Remember some variables for next time
        this.lastInput = input;
        this.lastTime = now;
        return true;
    }
    else {
        return false;
    }
};

/**
 * SetTunings(...)
 * This function allows the controller's dynamic performance to be adjusted. 
 * it's called automatically from the constructor, but tunings can also
 * be adjusted on the fly during normal operation
 */
PID.prototype.setTunings = function(Kp, Ki, Kd) {
    if (Kp < 0 || Ki < 0 || Kd < 0) {
        return;
    }

    this.dispKp = Kp;
    this.dispKi = Ki;
    this.dispKd = Kd;

    this.SampleTimeInSec = (this.SampleTime) / 1000;
    this.kp = Kp;
    this.ki = Ki * this.SampleTimeInSec;
    this.kd = Kd / this.SampleTimeInSec;
};

/**
 * SetSampleTime(...)
 * sets the period, in Milliseconds, at which the calculation is performed	
 */
PID.prototype.setSampleTime = function(NewSampleTime) {
    if (NewSampleTime > 0) {
        var ratio = NewSampleTime / (1.0 * this.SampleTime);
        this.ki *= ratio;
        this.kd /= ratio;
        this.SampleTime = Math.round(NewSampleTime);
    }
};

/**
 * SetOutput( )
 * Set output level if in manual mode
 */
PID.prototype.setOutput = function(val) {
    if (val > this.outMax) {
        this.myOutput = val;
    }
    else if (val < this.outMin) {
        val = this.outMin;
    }
    this.myOutput = val;
};

/**
 * SetOutputLimits(...)
 * This function will be used far more often than SetInputLimits.  while
 * the input to the controller will generally be in the 0-1023 range (which is
 * the default already,)  the output will be a little different.  maybe they'll
 * be doing a time window and will need 0-8000 or something.  or maybe they'll
 * want to clamp it from 0-125.  who knows.  at any rate, that can all be done here.
 */
PID.prototype.setOutputLimits = function(Min, Max) {
    if (Min >= Max) {
        return;
    }
    this.outMin = Min;
    this.outMax = Max;

    if (this.inAuto) {
        if (this.myOutput > this.outMax) {
            this.myOutput = this.outMax;
        }
        else if (this.myOutput < this.outMin) {
            this.myOutput = this.outMin;
        }

        if (this.ITerm > this.outMax) {
            this.ITerm = this.outMax;
        }
        else if (this.ITerm < this.outMin) {
            this.ITerm = this.outMin;
        }
    }
};

/**
 * SetMode(...)
 * Allows the controller Mode to be set to manual (0) or Automatic (non-zero)
 * when the transition from manual to auto occurs, the controller is
 * automatically initialized
 */
PID.prototype.setMode = function(Mode) {
    var newAuto;
    if (Mode == PID.AUTOMATIC || Mode.toString().toLowerCase() == 'automatic' || Mode.toString().toLowerCase() == 'auto') {
        newAuto = 1;
    }
    else if (Mode == PID.MANUAL || Mode.toString().toLowerCase() == 'manual') {
        newAuto = 0;
    }
    else {
        throw new Error("Incorrect Mode Chosen");
    }

    if (newAuto == !this.inAuto) { //we just went from manual to auto
        this.initialize();
    }
    this.inAuto = newAuto;
};

/**
 * Initialize()
 * does all the things that need to happen to ensure a bumpless transfer
 * from manual to automatic mode.
 */
PID.prototype.initialize = function() {
    this.ITerm = this.myOutput;
    this.lastInput = this.input;
    if (this.ITerm > this.outMax) {
        this.ITerm = this.outMax;
    }
    else if (this.ITerm < this.outMin) {
        this.ITerm = this.outMin;
    }
};

/**
 * SetControllerDirection(...)
 * The PID will either be connected to a DIRECT acting process (+Output leads 
 * to +Input) or a REVERSE acting process(+Output leads to -Input.)  we need to
 * know which one, because otherwise we may increase the output when we should
 * be decreasing.  This is called from the constructor.
 */
PID.prototype.setControllerDirection = function(ControllerDirection) {
    if (ControllerDirection == 0 || ControllerDirection.toString().toLowerCase() == 'direct') {
        this.setDirection = 1;
    }
    else if (ControllerDirection == 1 || ControllerDirection.toString().toLowerCase() == 'reverse') {
        this.setDirection = -1;
    }
    else {
        throw new Error("Incorrect Controller Direction Chosen");
    }
};

/**
 * Status Functions
 * Just because you set the Kp=-1 doesn't mean it actually happened.  these
 * functions query the internal state of the PID.  they're here for display 
 * purposes.  this are the functions the PID Front-end uses for example
 */
PID.prototype.getKp = function() {
    return this.dispKp;
};

PID.prototype.getKd = function() {
    return this.dispKd;
};

PID.prototype.getKi = function() {
    return this.dispKi;
};

PID.prototype.getMode = function() {
    return this.inAuto ? "Auto" : "Manual";
};

PID.prototype.getDirection = function() {
    return this.controllerDirection;
};

PID.prototype.getOutput = function() {
    return this.myOutput;
};

PID.prototype.getInput = function() {
    return this.input;
};

PID.prototype.getSetPoint = function() {
    return this.mySetpoint;
};

module.exports = PID;

// Создаем виртуальное устройство
defineVirtualDevice("PIDTest", {
    title: "PID",
    cells: {
        // Значение
        enabled: {
            type: "range",
            value: 20,
            min: 15,
            max: 40 
        },
        // Уставка
        setPoint: {
            type: "range",
            value: 20,
            min: 15,
            max: 40 
        },
        // Вывод ПИД-а
        log1: {
            type: "text",
            value: ''
        },
        // Коэф. пропорциональной составляющей
        kp1: {
            type: "range",
            value: 1,
            min: 1,
            max: 100,
        },
        // Коэф интегральной составляющей
      	ki1: {
            type: "range",
            value: 0,
            min: 0,
            max: 100,
        }
    },
});

// Время вызова
var timeframe = 2000
// Максимальное значение выхода ПИД-а
var maxOutput = 10000;

// Инициализация ПИД-а
var temperature = dev["PIDTest/enabled"];
var temperatureSetpoint =  dev["PIDTest/setPoint"];
var Kp = dev["PIDTest/kp1"],
    Ki = dev["PIDTest/ki1"],
    Kd = 0;
// direct - работа на обогрев, reverse - на охлаждение
var ctr = new PID(temperature, temperatureSetpoint, Kp, Ki, Kd, 'reverse');
ctr.setSampleTime(timeframe);
ctr.setOutputLimits(0, maxOutput);
ctr.setMode('auto');

//цикл работы ПИД-а
var myControl = function() {
    // Актуализация (получение) параметров из панели
	var temperature = dev["PIDTest/enabled"]; 
  	var temperatureSetpoint =  dev["PIDTest/setPoint"]; 
  	var Kp = dev["PIDTest/kp1"],
        Ki = dev["PIDTest/ki1"];
    // Запись полученных/обновленных параметров в ПИД 	
  	ctr.setInput(temperature);
  	ctr.setPoint(temperatureSetpoint);
  	ctr.setTunings(Kp, Ki, Kd);
    // Работа ПИД-а
  	ctr.compute();
  	// Получение результата
  	var output = String(ctr.getOutput());
  	log("Output : " + output);

    dev["PIDTest/log1"] = output;
};
setInterval(myControl, timeframe);
1 лайк

Теперь ОК!
Спасибо!

Странное поведение ПИД с введением Ki
Если Т отклонить от целевой, то накапливается воздействие до пропорционального и потом продолжает накапливаться с интегральной.
При возврате Т к целевой пропорциональная часть воздействия снижается до 0, а накопленная интегральная так и “висит”.
Может я неправильно понимаю и интегральная должна уходить только после того как Т перейдет на другую сторону от целевой?

И чем дольше T была отклонена от целевой, тем дольше управляющее воздействие будет возвращаться к 0 при переходе Т на другую сторону. За это время она сильно уходит и теряется точность и смысл регулирования
Правильно же должно быть так что, как только Т сравнялась с целевой, то и воздействие сразу должно стать 0?

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

Категорически и настоятельно рекомендую забить в Яндексе ПИД регулятор и посмотреть как он работает))) там с картинками и графиками его хорошие примеры.

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

2 лайка

Да, спасибо!
Похоже зря “гнал”
Подключил к оборудованию и взлетело после нескольких итераций

После “интегрального загона” по идее перезагрузка должна вылечить долгий выход на нужные параметры управления?

А в максимуме тоже будете перезагружать?) Тогда уж проще человека посадить, и пид не нужен)))

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

PID.prototype.setOutputLimits = function(Min, Max) {
if (Min >= Max) {
return;
}
this.outMin = Min;
this.outMax = Max;

if (this.inAuto) {
    if (this.myOutput > this.outMax) {
        this.myOutput = this.outMax;
    }
    else if (this.myOutput < this.outMin) {
        this.myOutput = this.outMin;
    }

    if (this.ITerm > this.outMax) {
        this.ITerm = this.outMax;
    }
    else if (this.ITerm < this.outMin) {
        this.ITerm = this.outMin;
    }
}

};

И в качестве ограничивающего параметра нужно выбрать не this.outMax, а поменьше?

Простое решение, которое подойдёт в большинстве простых домашних регуляторов - в функции вычисления запретить накопление интеграла, когда управление уперлось в упор. При чем обратите внимание, что у вас есть два упора, два направления накопления и два варианта работы пида - на подогрев и на охлаждение. Ограничения надо прописать для каждого из вариантов. То есть запретить накопление в плюс, когда уперлось в плюс и хочет ещё расти в плюс, и запретить в минус, когда уперлось в минус и хочет уменьшаться в минус. Это не слова обороты, это конкретное действие или проверка кода. Если у вас упрется в плюс, и захочет уменьшаться, то ничего запрещать не надо, иначе из плюса не выйдет никогда. И ещё обратите внимание, что у функции сомпуте есть признак направления пида, где результат умножается на единицу или минус единицу, это тоже вносит коррективы в определения действий.
Правда можно упростить - не выходной результат умножать на минус 1, а ошибку умножать на минус 1 (если нужно). Тогда будет меньше условий для проверки и действия буду однотипные для действий с ограничениями.

А игра с побольше и поменьше особо не поможет.

PS: не было особо времени разбираться, но по-хорошему, там этот реверс или прямой (охлаждение обогрев) нафиг выкинуть, а направление задавать местами расположения заданной уставки и текущего значения - меняя их местами получается противоположный пид. Да и вообще там половина кода нафиг не нужна, а то что надо там руками допиливать)))

1 лайк


Еще вопрос к знатокам
Дифф.поправка имеет такой вид пилы с периодичностью выбросов в 15сек
Частота отсчетов 1 сек.
Что-то подсказывает, что это должны быть не экстремумы, а плавная функция коррекции
Или это я уже где-то запорол прогу?

Соответственно вид управляющего воздействия приобретает такое

Смотря что делали)

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

Немного переживаю за вентилятор после частотника, который получает такие всплески туда-сюда…

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

Внес корректировку в порядок формирования дифф составляющей конечной формулы.

Изменение
    // Compute all the working error variables
    var input = this.input;
    var error = this.mySetpoint - input;
    this.ITerm += (this.ki * error);
    var dInput = input - this.lastInput;
    if(dInput != 0) {
      this.DTerm = this.kd * dInput;
      log.debug("DTerm = " + this.DTerm);          
    } else {
      this.DTerm = this.DTerm;
      log.debug("DTerm = " + this.DTerm);
    }
    // Compute PID Output
    var output = (this.kp * error + this.ITerm - this.DTerm) * this.setDirection;

В таком виде дифференциальная составляющая оказывает влияние на управляющее воздействие не в момент изменения ИЗМЕНЕНИЯ, а отслеживает именно изменение, то есть пропорциональна скорости.

В результате дифф составляющая приобретает вид

а само управляющее воздействие такой


В совокупности с интегральной составляющей

функция регулировки выглядит так
image

Стало хоть немного похоже на то, что в методичках
Проверьте меня пожалуйста.

Бред чистой воды))) вы сделали два пропорциональных канала))) математически оно чуток иначе выглядит в прогремма, а сути не меняет.

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

Вот вы тут понятия подменяете. У вас чем выше температура, тем сильнее должен крутиться вентилятор. Всё. Я понимаю, что вы хотите сделать, но вы выдает желаемое за действительное: у вас Кп нефига нет, поэтому вентилятор не крутится так быстро, как вам хочется, и вы придумываете фишки - как бы заставить крутиться быстрее. Вот этой добавкой к управлению вы фактически подменили Кп. Потом его съест интеграл. И оно вроде бы типа не мешает. Но это задача Кп как бы)))) и именно Кп заставит крутиться быстро, если температура быстро поднимется - зависимость пропорциональная, и скорость мгновенная.