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);
Теперь ОК!
Спасибо!
Странное поведение ПИД с введением Ki
Если Т отклонить от целевой, то накапливается воздействие до пропорционального и потом продолжает накапливаться с интегральной.
При возврате Т к целевой пропорциональная часть воздействия снижается до 0, а накопленная интегральная так и “висит”.
Может я неправильно понимаю и интегральная должна уходить только после того как Т перейдет на другую сторону от целевой?
И чем дольше T была отклонена от целевой, тем дольше управляющее воздействие будет возвращаться к 0 при переходе Т на другую сторону. За это время она сильно уходит и теряется точность и смысл регулирования
Правильно же должно быть так что, как только Т сравнялась с целевой, то и воздействие сразу должно стать 0?
Третий момент, который вижу
Если Т была длительное время с другой стороны от целевой, то при переходе ее в область регулирования накопленная интегральная составляющая долго не дает управляющего воздействия - оно просто 0.
Приходится Т отклонять в приличное значение, чтобы возникло управляющее воздействие
Категорически и настоятельно рекомендую забить в Яндексе ПИД регулятор и посмотреть как он работает))) там с картинками и графиками его хорошие примеры.
Интеграл ведёт себя правильно. Единственное, конкретно в этом ПИДе есть несколько багов, например, если у вас отклонение загнало управление в упор, то интеграл продолжит копится, и потом очень долго будет оттуда выбираться. Но вам в любом случае сначала надо разобраться как пид работает по сути, судя по сообщениям выше вы не понимаете: нет, если отклонение нулевое, то выходной сигнал не обязательно будет равен нулю. Картинки и разъяснения как работает пид в интернете очень наглядны.
Да, спасибо!
Похоже зря “гнал”
Подключил к оборудованию и взлетело после нескольких итераций
После “интегрального загона” по идее перезагрузка должна вылечить долгий выход на нужные параметры управления?
А в максимуме тоже будете перезагружать?) Тогда уж проще человека посадить, и пид не нужен)))
Какое решение можно применить для сброса интеграла при долгом нахождении в сильном отклонении?
Возможно это предусмотрено в этой функции?
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: не было особо времени разбираться, но по-хорошему, там этот реверс или прямой (охлаждение обогрев) нафиг выкинуть, а направление задавать местами расположения заданной уставки и текущего значения - меняя их местами получается противоположный пид. Да и вообще там половина кода нафиг не нужна, а то что надо там руками допиливать)))
Еще вопрос к знатокам
Дифф.поправка имеет такой вид пилы с периодичностью выбросов в 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;
В таком виде дифференциальная составляющая оказывает влияние на управляющее воздействие не в момент изменения ИЗМЕНЕНИЯ, а отслеживает именно изменение, то есть пропорциональна скорости.
В результате дифф составляющая приобретает вид
а само управляющее воздействие такой
В совокупности с интегральной составляющей
функция регулировки выглядит так
Стало хоть немного похоже на то, что в методичках
Проверьте меня пожалуйста.
Бред чистой воды))) вы сделали два пропорциональных канала))) математически оно чуток иначе выглядит в прогремма, а сути не меняет.
Я ещё раз вам говорю, что у вас нефига не настроен пропорциональный и интегральный каналы. Дифференциальный работал правильно, только он нафиг вентилятору не нужен, у вас там не тяжёлый орган, который нужно хорошенько рвануть, чтобы он сдвигался лучше и реще. Так же молчу о том, что нет механизма контроля ограничений накопления интеграла.
Вот вы тут понятия подменяете. У вас чем выше температура, тем сильнее должен крутиться вентилятор. Всё. Я понимаю, что вы хотите сделать, но вы выдает желаемое за действительное: у вас Кп нефига нет, поэтому вентилятор не крутится так быстро, как вам хочется, и вы придумываете фишки - как бы заставить крутиться быстрее. Вот этой добавкой к управлению вы фактически подменили Кп. Потом его съест интеграл. И оно вроде бы типа не мешает. Но это задача Кп как бы)))) и именно Кп заставит крутиться быстро, если температура быстро поднимется - зависимость пропорциональная, и скорость мгновенная.