Полезные программы — различия между версиями
(не показано 13 промежуточных версий этого же участника) | |||
Строка 10: | Строка 10: | ||
Таймер сравнивает свое текущее состояние с состоянием на входе: | Таймер сравнивает свое текущее состояние с состоянием на входе: | ||
* текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0. | * текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0. | ||
− | * текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время | + | * текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время onDelay = 0. |
− | + | ||
<!--T:3--> | <!--T:3--> | ||
+ | Для работы таймера необходимо завести 2 DS регистра типа Unix time и Bit и присвоить им имена (алиасы) вида "<Имя1>", "<Имя1>_out". В первом будет хранится время след. переключения, во втором - текущее состояние таймера. | ||
+ | |||
<syntaxhighlight lang="Lua"> | <syntaxhighlight lang="Lua"> | ||
Строка 40: | Строка 41: | ||
end | end | ||
end -- | end -- | ||
− | return curTmrState | + | return curTmrState -- output rerurned as bool for simple usage in IFs |
+ | <!--T:4--> | ||
end -- function | end -- function | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Пример использования: | ||
+ | |||
+ | Например есть два дискретных входа с датчиками реле низкого и высокого давления. Их состояние читается и переводится в переменную типа bool для упрощения последующих операторов сравнения. Функция также возвращает bool, чтобы ее можно было сразу использовать в операторах сравнения без дополнительных операторов if. | ||
+ | |||
+ | |||
+ | <!--T:6--> | ||
+ | <syntaxhighlight lang="Lua"> | ||
+ | DEBUG(" hotWater timer call ") | ||
+ | local DI2 = (GetReg(39) == 1) -- hotwater low pressure | ||
+ | local DI3 = (GetReg(40) == 1) -- hotwater high pressure | ||
+ | local hotWater = TimerOnOff((DI2 or DI3), 15, 15, "Tmr2") -- 15 seconds delay for both on and off . | ||
+ | |||
+ | <!--T:7--> | ||
+ | -- or like this | ||
+ | if TimerOnOff(((GetReg("hotWaterLowPressure") == 1) or (GetReg(hotWaterHighPressure) == 1)), 15, 2, "Tmr2") then | ||
+ | -- do something | ||
+ | end -- simple timer in one row ! | ||
+ | |||
+ | <!--T:8--> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Строка 323: | Строка 346: | ||
if (not run_state) and run_status then | if (not run_state) and run_status then | ||
WriteReg("P43StartTime", now); -- Время старта привода №П43 | WriteReg("P43StartTime", now); -- Время старта привода №П43 | ||
− | + | end | |
<!--T:32--> | <!--T:32--> | ||
Строка 423: | Строка 446: | ||
3-х точечным называется способ управления позицией клапана, сервопривода, задвижки и т.п., когда для управления приводом используются 3 провода - "общий", "питание - ВВЕРХ", " питание - ВНИЗ". Такие приводы могут быть (а могут и нет) оснащены также концевыми датчиками положения. Иногда при отсутствии датчиков положения и низких требованиях к точности позиционирования может применяться алгоритм, когда привод уезжает вниз или вверх (либо по 1 датчику положения либо подачей одной команды на время превышающее время полного хода клапана), инициализирует координату, а потом отрабатывает заданную позицию. | 3-х точечным называется способ управления позицией клапана, сервопривода, задвижки и т.п., когда для управления приводом используются 3 провода - "общий", "питание - ВВЕРХ", " питание - ВНИЗ". Такие приводы могут быть (а могут и нет) оснащены также концевыми датчиками положения. Иногда при отсутствии датчиков положения и низких требованиях к точности позиционирования может применяться алгоритм, когда привод уезжает вниз или вверх (либо по 1 датчику положения либо подачей одной команды на время превышающее время полного хода клапана), инициализирует координату, а потом отрабатывает заданную позицию. | ||
<p>Для определения промежуточных положений используется расчетное значение, определяемое из характеристики исполнительного механизма "время полного хода", которое можно определить и экспериментальным путем. | <p>Для определения промежуточных положений используется расчетное значение, определяемое из характеристики исполнительного механизма "время полного хода", которое можно определить и экспериментальным путем. | ||
− | + | </p> | |
<br> | <br> | ||
Ниже приведен вариант 3-х точечного управления для привода с 2-мя концевыми датчиками. | Ниже приведен вариант 3-х точечного управления для привода с 2-мя концевыми датчиками. | ||
− | + | <syntaxhighlight lang = "lua"> | |
+ | R = GetReg -- redefine function names | ||
+ | WR = WriteReg | ||
− | + | function main (userId) | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
+ | now = os.time() -- global | ||
− | + | local manOpenCmd, manCloseCmd = R("manOpenCmd"), R("manCloseCmd") -- separate flags for manual control | |
− | + | local autoOpenCmd, autoCloseCmd = R("autoOpenCmd"), R("autoCloseCmd") -- system's output to valve | |
− | + | local openSw, closeSw = (R("valveOpenSw") == 1) , (R("valveCloseSw") == 1) | |
− | + | local pullUpFlag, pullDownFlag = (R("pullUpFlag") == 1 ), (R("pullDownFlag") == 1 ) ; | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
+ | local curPosition = Round(R("valveCurPos") + MotionTimer(autoOpenCmd, autoCloseCmd, "Tmr5")) ; DEBUG_("curPosition calc. as "..curPosition) | ||
− | + | -- filter and check limit sw | |
− | if | + | if (curPosition >= 100) or openSw then |
+ | curPosition = 100 | ||
+ | if openSw then | ||
+ | WR("pullUpFlag", 1 ) | ||
+ | pullUpFlag = true | ||
+ | end | ||
+ | end | ||
+ | if ((curPosition < 0) or closeSw) then | ||
+ | curPosition = 0 | ||
+ | if closeSw then | ||
+ | WR("pullDownFlag", 1 ) | ||
+ | pullDownFlag = true | ||
+ | end | ||
+ | end | ||
+ | -- pulling | ||
+ | if (curPosition == 100) and not (openSw) and not pullUpFlag then | ||
+ | curPosition = 99 ; DEBUG_(" pulling up ") | ||
+ | end | ||
+ | if (curPosition == 0 ) and not (closeSw) and not pullDownFlag then | ||
+ | curPosition = 1 ; DEBUG_(" pulling down ") | ||
+ | end | ||
− | + | WR("valveCurPos", curPosition) -- store new position | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
+ | -- AUTO MODE ---- | ||
+ | local valveSp = R("valveSp") ; DEBUG_("valveSp "..valveSp) | ||
+ | local positionError = (curPosition - valveSp) | ||
− | + | if (positionError ~= 0) and ( autoOpenCmd ~= 1) and (autoCloseCmd ~= 1) then | |
− | end | + | -- reset pulling flags on new cycle of motion |
− | + | WR("pullDownFlag" , 0 ); WR("pullUpFlag", 0) | |
+ | end | ||
− | + | if (R("distribAutoMode") == 1) then | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | WR("manOpenCmd", 0 ) -- clear manual cmds | |
− | + | WR("manCloseCmd", 0) | |
− | + | ||
− | + | if (positionError == 0) then | |
− | + | WR("autoOpenCmd", 0 ) | |
− | + | WR("autoCloseCmd", 0) -- clear cmds no error | |
− | + | elseif (positionError > 0) then | |
− | + | DEBUG_("GO DOWN because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError)) | |
− | + | WR("autoOpenCmd", 0 ) | |
− | + | WR("autoCloseCmd", 1) -- go down | |
− | + | ||
− | + | elseif (positionError < 0) then | |
− | + | DEBUG_("GO UP because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError)) | |
+ | WR("autoOpenCmd", 1 ) | ||
+ | WR("autoCloseCmd", 0) -- go up | ||
+ | else | ||
+ | DEBUG_("Undefined if !!! in valve control ") | ||
+ | end | ||
+ | --- MANUAL MODE ----- | ||
+ | else | ||
+ | -- just redirect manual cmd | ||
+ | WR("autoOpenCmd", manOpenCmd ) | ||
+ | WR("autoCloseCmd", manCloseCmd) | ||
+ | |||
+ | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
end -- main | end -- main | ||
+ | function MotionTimer(openCmd, downCmd, tmrAlias) | ||
+ | --[[ | ||
+ | remembers last call time | ||
+ | return motion quant passed since last call if there were motion | ||
+ | then quant added to current position | ||
+ | --]] | ||
− | + | local FULL_PATH_TIME = 180 -- sec. | |
− | + | local KOEF = 100 / FULL_PATH_TIME | |
− | + | local quant = 0 | |
− | + | ||
− | + | local motion = { flag = ((openCmd == 1) or (downCmd == 1)), dir = (openCmd == 1), lastTimeStamp = R(tmrAlias)} | |
− | + | DEBUG_(" motion.flag = "..tostring(motion.flag).." dir "..tostring(motion.dir)..os.date("%c", lastTimeStamp)) | |
− | + | local outAlias = tmrAlias.."_out" | |
− | -- | + | local curTmrState = (R(outAlias) == 1) ; DEBUG_("cur "..tmrAlias.." State = "..tostring(curTmrState)) |
− | + | ||
+ | if motion.flag then | ||
+ | quant = (now - R(tmrAlias)) * KOEF | ||
+ | if not motion.dir then | ||
+ | quant = quant * (-1) | ||
+ | end | ||
+ | DEBUG_("quant calculated as = "..quant) | ||
+ | WR(outAlias, 1) -- in motion now | ||
+ | |||
+ | else | ||
+ | DEBUG_("no motion lasts = ") | ||
+ | WR(outAlias, 0) | ||
+ | quant = 0 | ||
+ | end -- if | ||
− | + | WR(tmrAlias, now) -- store last call time | |
− | + | return quant | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
+ | end -- function | ||
− | + | -- округлление | |
− | + | function Round(var) | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | function | + | |
− | + | local integer, fraction = math.modf(var) | |
− | + | ||
− | + | if fraction >= 0.5 then | |
− | + | integer = integer + 1 | |
− | + | end | |
− | + | ||
− | + | return math.floor(integer) | |
− | + | ||
− | + | end | |
+ | thisScriptID = 14 | ||
+ | function DEBUG_(str) | ||
+ | -- ERROR("entered DEBUG_ in 35 script") | ||
+ | local i = 0 | ||
+ | local tmp = "" | ||
+ | local debug_id = R("debug_IDs") | ||
+ | -- ERROR(debug_id) | ||
+ | local capture_mask = "%s+(%d+)%s+" | ||
− | + | while true do | |
− | + | i,_, tmp = string.find(debug_id, capture_mask, i+1) | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | if i == nil then | |
− | + | -- ERROR("not found !") | |
− | + | break | |
− | + | end -- не найдено | |
− | + | if (tmp ~= "0") then | |
− | + | -- найдено проверить на совпадение | |
− | + | if (tmp == tostring(thisScriptID)) then | |
− | if ( | + | DEBUG(str) |
− | -- | + | -- ERROR("found own id , tmp = "..tmp) |
− | + | return 0 | |
− | + | end | |
− | + | end | |
− | + | ||
− | + | ||
end | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | end -- DEBUG_ | |
− | + | ------------------------------ | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
</syntaxhighlight> | </syntaxhighlight> | ||
</translate> | </translate> |
Текущая версия на 10:34, 5 октября 2018
Содержание
- 1 Полезные программы
- 1.1 Запуск скрипта по фронта или срезу дискретного сигнала
- 1.2 Реализация универсального таймера (TON TOFF)
- 1.3 Cигнализация (Звуковая, релейная, sms, viber, telegram) об ошибках связи
- 1.4 Скользящее среднее
- 1.5 ПИД - регулятор
- 1.6 Счетчик моточасов
- 1.7 Алгоритм чередования по времени (циркуляция с функцией АВР)
- 1.8 3-х точечное управление клапанами, сервоприводами и др.
Полезные программы
Запуск скрипта по фронта или срезу дискретного сигнала
Скрипт нужно вызываеть по изменению регистра. Внутри скрипта нужно сделать проверку текущего состояния этого регистра и выполнять соответветсующие действия либо по фронту (текущее состояние =1), либо по срезу (=0). Пример скрипта:
Реализация универсального таймера (TON TOFF)
Таймер сравнивает свое текущее состояние с состоянием на входе:
- текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0.
- текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время onDelay = 0.
Для работы таймера необходимо завести 2 DS регистра типа Unix time и Bit и присвоить им имена (алиасы) вида "<Имя1>", "<Имя1>_out". В первом будет хранится время след. переключения, во втором - текущее состояние таймера.
function TimerOnOff(bool_input, onDelay, offDelay, tmrAlias)
-- bool seconds seconds string
local outAlias = tmrAlias.."_out"
local curTmrState = (GetReg(outAlias) == 1) ; DEBUG("curTmr State "..tostring(curTmrState))
DEBUG("bool_input = "..tostring(bool_input))
if (bool_input == curTmrState) then
WriteReg(tmrAlias, now) ; DEBUG("timer input match state ")
return curTmrState -- as bool
elseif bool_input then
DEBUG("countdown for onDelay = "..(onDelay - (now - GetReg(tmrAlias))))
if now - GetReg(tmrAlias) > onDelay then
WriteReg(outAlias, 1)
DEBUG("detected ON state after delay ")
return true
end
else
DEBUG("countdown for offDelay = "..(offDelay - (now - GetReg(tmrAlias))))
if (now - GetReg(tmrAlias) > offDelay) then
WriteReg(outAlias, 0)
DEBUG("detected OFF state after delay ")
return false
end
end --
return curTmrState -- output rerurned as bool for simple usage in IFs
end -- function
Пример использования:
Например есть два дискретных входа с датчиками реле низкого и высокого давления. Их состояние читается и переводится в переменную типа bool для упрощения последующих операторов сравнения. Функция также возвращает bool, чтобы ее можно было сразу использовать в операторах сравнения без дополнительных операторов if.
DEBUG(" hotWater timer call ")
local DI2 = (GetReg(39) == 1) -- hotwater low pressure
local DI3 = (GetReg(40) == 1) -- hotwater high pressure
local hotWater = TimerOnOff((DI2 or DI3), 15, 15, "Tmr2") -- 15 seconds delay for both on and off .
-- or like this
if TimerOnOff(((GetReg("hotWaterLowPressure") == 1) or (GetReg(hotWaterHighPressure) == 1)), 15, 2, "Tmr2") then
-- do something
end -- simple timer in one row !
Cигнализация (Звуковая, релейная, sms, viber, telegram) об ошибках связи
Можно анализировать скриптом время скана и при выходе его за допустимый предел, сигнализировать об этом разными способами. Ниже приведен пример обработки большого времени скана с сигнализацией в буфер сообщений и по Viber.
cntdownFlag = false; -- флаг обратного отчета таймера
timeStmp = 0; -- метка времени
msgSent = false; -- флаг отправки сообщения
function main (userId)
-- Читаем входные
local scan = GetReg(34); -- время скана
local c0 = GetReg(42); -- номер неработающего соединения
local SCANLIMIT = GetReg(886); -- предел времения скана
local SCANDELAY = GetReg(887); -- задержка реагирования на ошибку
--
if (scan == nil) or (c0 == nil) or (SCANLIMIT == nil) or (SCANDELAY == nil) then
ERROR("scan / c0 was read as nil");
return 0;
end
-- читаем время
local now = os.time();
-- шаблон сообщения
local msg1 = "Cкан тайм большой "..tostring(scan).." ".."ms, ошибка в соед. "..tostring(c0);
--
if (scan > SCANLIMIT) then -- скан выше нормы
if not cntdownFlag then
cntdownFlag = true;
timeStmp = now;
else
if (now - timeStmp) > SCANDELAY then
if not msgSent then
AddAlertMessage(msg1);
SendViberMessage(398044391, msg1); -- Женя
-- SendViberMessage(642997589, msg1); -- Игорь
SendViberMessage(335584075, msg1); -- Костя
msgSent = true;
end
end
end
else -- скан в норме
if (cntdownFlag == true) and msgSent then
AddInfoMessage("Скан вернулся к норме ");
SendViberMessage(398044391, "Скан вернулся к норме ");
SendViberMessage(335584075, "Скан вернулся к норме ");
msgSent = false;
end
cntdownFlag = false;
end
end -- main
В WebHMI имеются buzzer для подачи звукового сигнала, и выходные реле 2 шт. , которыми можно управлять для сигнализации (выдать на сигнальную колонну либо в ПЛК сигнал о проблеме).
Скользящее среднее
Скользящее среднее полезно для сглаживания значений параметров, имеющих шумы, пульсации.
Алгоритм скользящего среднего:
в начале работы фильтра на выборке в N значений идет подсчет арифметического среднего значения, по достижении конца выборки один элемент отбрасывается (путем деления суммы на длину очереди), вместо него добавляется новый и сумма снова делится на длину очереди.
-- глобальные переменные, сохраняют значения между вызовами программы
mav_len = 20; -- длина очереди
queue_fill = 0; -- индекс заполнения очереди
av_sum = 0; -- аккумулятор ск. среднего
function main (userId)
local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- читаем значение параметра
if (queue_fill < mav_len) then -- очередь не заполнена
av_sum = av_sum + in_value; -- накапливаем сумму
queue_fill = queue_fill +1; -- и индекс
else -- очередь полная дальше будет движение по очереди
tmp_var = av_sum / mav_len; -- запомнить один элемент
av_sum = av_sum - tmp_var + in_value; -- вычесть его и добавить новый
end
--
if (queue_fill == mav_len ) then
out_value = av_sum / mav_len; -- посчитать ск. среднее
else
out_value = av_sum / queue_fill; -- среднее арифм.
end
WriteReg("Tout_mav", out_value); -- Наружная температура среднее
end
ПИД - регулятор
Пример реализации ПИД регулятора в WebHMI:
G_LIMIT = 100 -- ограничение выхода регулятора
--
function main (userId)
-- локальные переменные
local now = os.time()
local nexTime = GetReg("nextPidTime")
local CYCLE_TIME = GetReg("pidCycleTime")
-- параметры регулятора
local Kp = GetReg("Kp") -- Пропорц. коэф. (DS1400@WH Valve control)
local Ti = GetReg("Ki") -- Интегральный коэф. (DS1404@WH Valve control)
local Td = GetReg("Kd") -- Дифф. коэф. (DS1408@WH Valve control)
local Err, dErr, iSum_Limit = 0, 0, 0
local Int_sum = GetReg("pidIntegral") -- интегральный накопитель
local intPart = 0 -- инт. часть формулы
local G = GetReg("pidOut") -- выход регулятора
-- процесс
local PV = GetReg(1436) -- Мощность (PWR0@Scylar 8 INT)
local Sp = GetReg("targetPowerSp")
DEBUG_("seconds left for PID cycle = "..tostring(nexTime - now))
-- условие работы
local auto = (GetReg("auto_mode") == 1) -- АВТ. РЕЖИМ ВКЛЮЧЕН (ds1176@WH Global)
local heatDemand = (GetReg("heatDemand") == 1)
if auto then
if heatDemand then
-- РЕГУЛЯТОР
if (now >= nexTime) then
DEBUG_("PID compute cycle")
WriteReg("nextPidTime", now + CYCLE_TIME)
Err = Sp - PV -- вычисляем ошибку
DEBUG_("sp pv Err = "..Sp.." "..PV.." "..Err)
dErr = Err - GetReg("pidPrevError") -- вычисляем производную ошибки
DEBUG_("dErr = "..dErr)
-- проверяем интегральное насыщение
iSum_Limit = (G_LIMIT * Ti / Kp) / 5
DEBUG_("iSum_Limit = "..iSum_Limit)
-- ПИД - регулятор
--проверка на 0 инт. составляющей
DEBUG_("prev Int_sum = "..Int_sum)
if (intPart <= iSum_Limit) and (intPart >= 0.0) then
Int_sum = Int_sum + Err -- накапливаем интеграл ошибки
DEBUG_("added error to Int_sum ")
elseif Int_sum < 0 then
Int_sum = 0
else
Int_sum = iSum_Limit -- ограничиваем интегральную составляющую
end
if (Ti == 0) then
intPart = 0
else
intPart = (1/Ti)*Int_sum
end
DEBUG_("new Int_part = "..intPart)
G = Kp * (Err + intPart + Td*dErr)
DEBUG_("Calculated G as "..G)
G = Round(G)
DEBUG_("Rounded G as "..G)
-- проверка выхода за диапазон
if G < 0 then
G = 0
end
if G > G_LIMIT then
G = G_LIMIT
end
WriteReg("pidPrevError", Err) -- запомнить предыдущую ошибку для след. скана
WriteReg("dErr", dErr) -- запомнить предыдущую ошибку для след. скана
WriteReg("pidIntegral", intPart) -- запомнить интегральную составляющую
WriteReg("pidOut", G)
WriteReg("posSPinput", G) -- дать уставку на клапан
end -- time stamp
else
DEBUG_("no heatDemand") -- вывести ПИД выход
G = 0
end -- heatDemand
-- DEBUG_("PID_out = "..G) -- вывести ПИД выход
WriteReg("pidOut", G)
WriteReg("posSPinput", G) -- дать уставку на клапан
end -- if auto
end -- main
-- округлление
function Round(var)
local integer, fraction = math.modf(var)
if fraction >= 0.5 then
integer = integer + 1
end
return math.floor(integer)
end
------ debug printing ------
thisScriptID = 45
function DEBUG_(str)
--ERROR("entered DEBUG_ in"..thisScriptID.." script");
local i = 0;
local tmp = "";
local id = tostring(thisScriptID);
local debug_id = GetReg("debug_IDs");
local capture_mask = "%s+(%d+)%s+"
while true do
i,_, tmp = string.find(debug_id,capture_mask,i+1)
if (i == nil) then
break -- не найдено
end
-- найдено
if (tmp ~= "0") then
-- найдено проверить на совпадение
if (tmp == tostring(thisScriptID)) then
DEBUG(str)
return 0
end
end
end
end -- DEBUG_
------------------------------
Данный алгоритм является типовым для применения в ПЛК. Поскольку регулятор выполняется через равные интервалы времени, т.е. дифф. и инт. составляющие всегда вычисляются в одном масштабе времени, поэтому делить и умножать их на время для получения производной и интеграла необязательно, можно подбирать постоянные времени Ti, Td. В данном алгоритме Ti является обратной величиной (чем больше ее величина, тем меньше вклад интегральной ошибки)
Счетчик моточасов
Счетчик моточасов удобен для автоматической генерации сообщения о необходимости регламенных работ для узла оборудования, смены ведущего насоса в насосной группе для выравнивания наработки и т.п.
Пример реализации счетчика моточасов на Lua в WebHMI (программа выполняется в каждом скане):
-- глобальные переменные, сохраняются между вызовами скрипта
run_state = false; -- для запоминания текущего состояния
function main (userId)
-- локальные переменные
local check_mask = tonumber("0000100000000000",2); -- маска для проверки бита вращения в частотном приводе FC 51 Danfoss
local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- результат проверка как переменная типа bool
local now = os.time(); -- текущее время системы
local time_diff = 0; -- разница во времени между текущим временем и временем последнего вызова
-- ловим фронт события включения механизма для инициализации
if (not run_state) and run_status then
WriteReg("P43StartTime", now); -- Время старта привода №П43
end
-- считаем время
if run_state then
time_diff = (now - GetReg("P43StartTime")); -- посчитать разницу времени
WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- увеличить счетчик моточасов
WriteReg("P43StartTime", now); -- переписать начальную точку времени
end
run_state = run_status;
end
Регистры хранения моточасов и метки времени нужно делать энергонезависимыми.
Алгоритм чередования по времени (циркуляция с функцией АВР)
Данный алгоритм используется в системах, где необходимо чередовать работу механизмов (насосы, вентиляторы, кондиционеры) по времени, либо по моточасам (наработке). Для примера используется набор из 2-х установок, которые необходимо чередовать по времени. Если на какой-то установке возникает ошибка, тогда алгоритм начинает работать только по рабочей (функция АВР). Пример настройки необходимых регистров приведены ниже:
Для простоты и наглядности лучше разбивать скрипты на функциональные модули, которые можно быстро проанализировать и расположить их выполнение в нужном порядке в списке программ. Первый скрипт смотрит на ошибки и если их нет, чередует работу кондиционеров по времени.
CIRCULATION_TIME = 30; -- на время отладки время = 30 сек.
function main (userId)
--[[
если нет ошибок, то чередуем работу по времени циркуляции
если есть ошибка на одном из кондиционеров, то он исключается из чередования
если ошибки на обоих, то стоим на месте
--]]
local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- Кондиционер 1 ошибка (DS101@webmi)
local switchTime = GetReg("switchTime"); -- Время следующего переключения (DS103@webmi)
local now = os.time();
local curActiveAC = GetReg("activeAC"); -- Активный кондиционер (DS100@webmi)
if (not acError1) and (not acError2) then
-- работаем по циркуляции
if (now >= switchTime) then
if (curActiveAC == 1) then
WriteReg("activeAC", 2);
else
WriteReg("activeAC", 1);
end
WriteReg("switchTime", now + CIRCULATION_TIME);
end
elseif acError1 and (not acError2) then
WriteReg("activeAC", 2);
elseif acError2 and (not acError1) then
WriteReg("activeAC", 1);
else
WriteReg("activeAC", 0);
end -- if no errors
end -- main
Второй скрипт смотрит какой кондиционер сейчас активен, и выполняет необходимые действия. В скрипте это просто отладочная печать, но здесь могут быть команды управления инфракрасным передатчиком для выдачи нужной команды, запись в журнал сообщений и переключении и т.п.
function main (userId)
--[[
включает выбранный кондицинер, в зависимости от указателя
--]]
local pointer = GetReg("activeAC"); -- Активный кондиционер (DS100@webmi)
if (pointer==0) then
DEBUG("все выключаем");
return 0;
elseif
(pointer==1) then
DEBUG("включаем 1-й кондиционер");
else
DEBUG("включаем 2-й кондиционер");
end -- if
end
Также здесь нужен будет скрипт, который будет выставлять флаги ошибок работы по неким условиям, читаю состояние автоматов защиты, регистры ошибок по интерфейсу и т.п.
3-х точечное управление клапанами, сервоприводами и др.
3-х точечным называется способ управления позицией клапана, сервопривода, задвижки и т.п., когда для управления приводом используются 3 провода - "общий", "питание - ВВЕРХ", " питание - ВНИЗ". Такие приводы могут быть (а могут и нет) оснащены также концевыми датчиками положения. Иногда при отсутствии датчиков положения и низких требованиях к точности позиционирования может применяться алгоритм, когда привод уезжает вниз или вверх (либо по 1 датчику положения либо подачей одной команды на время превышающее время полного хода клапана), инициализирует координату, а потом отрабатывает заданную позицию.
Для определения промежуточных положений используется расчетное значение, определяемое из характеристики исполнительного механизма "время полного хода", которое можно определить и экспериментальным путем.
Ниже приведен вариант 3-х точечного управления для привода с 2-мя концевыми датчиками.
R = GetReg -- redefine function names
WR = WriteReg
function main (userId)
now = os.time() -- global
local manOpenCmd, manCloseCmd = R("manOpenCmd"), R("manCloseCmd") -- separate flags for manual control
local autoOpenCmd, autoCloseCmd = R("autoOpenCmd"), R("autoCloseCmd") -- system's output to valve
local openSw, closeSw = (R("valveOpenSw") == 1) , (R("valveCloseSw") == 1)
local pullUpFlag, pullDownFlag = (R("pullUpFlag") == 1 ), (R("pullDownFlag") == 1 ) ;
local curPosition = Round(R("valveCurPos") + MotionTimer(autoOpenCmd, autoCloseCmd, "Tmr5")) ; DEBUG_("curPosition calc. as "..curPosition)
-- filter and check limit sw
if (curPosition >= 100) or openSw then
curPosition = 100
if openSw then
WR("pullUpFlag", 1 )
pullUpFlag = true
end
end
if ((curPosition < 0) or closeSw) then
curPosition = 0
if closeSw then
WR("pullDownFlag", 1 )
pullDownFlag = true
end
end
-- pulling
if (curPosition == 100) and not (openSw) and not pullUpFlag then
curPosition = 99 ; DEBUG_(" pulling up ")
end
if (curPosition == 0 ) and not (closeSw) and not pullDownFlag then
curPosition = 1 ; DEBUG_(" pulling down ")
end
WR("valveCurPos", curPosition) -- store new position
-- AUTO MODE ----
local valveSp = R("valveSp") ; DEBUG_("valveSp "..valveSp)
local positionError = (curPosition - valveSp)
if (positionError ~= 0) and ( autoOpenCmd ~= 1) and (autoCloseCmd ~= 1) then
-- reset pulling flags on new cycle of motion
WR("pullDownFlag" , 0 ); WR("pullUpFlag", 0)
end
if (R("distribAutoMode") == 1) then
WR("manOpenCmd", 0 ) -- clear manual cmds
WR("manCloseCmd", 0)
if (positionError == 0) then
WR("autoOpenCmd", 0 )
WR("autoCloseCmd", 0) -- clear cmds no error
elseif (positionError > 0) then
DEBUG_("GO DOWN because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError))
WR("autoOpenCmd", 0 )
WR("autoCloseCmd", 1) -- go down
elseif (positionError < 0) then
DEBUG_("GO UP because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError))
WR("autoOpenCmd", 1 )
WR("autoCloseCmd", 0) -- go up
else
DEBUG_("Undefined if !!! in valve control ")
end
--- MANUAL MODE -----
else
-- just redirect manual cmd
WR("autoOpenCmd", manOpenCmd )
WR("autoCloseCmd", manCloseCmd)
end
end -- main
function MotionTimer(openCmd, downCmd, tmrAlias)
--[[
remembers last call time
return motion quant passed since last call if there were motion
then quant added to current position
--]]
local FULL_PATH_TIME = 180 -- sec.
local KOEF = 100 / FULL_PATH_TIME
local quant = 0
local motion = { flag = ((openCmd == 1) or (downCmd == 1)), dir = (openCmd == 1), lastTimeStamp = R(tmrAlias)}
DEBUG_(" motion.flag = "..tostring(motion.flag).." dir "..tostring(motion.dir)..os.date("%c", lastTimeStamp))
local outAlias = tmrAlias.."_out"
local curTmrState = (R(outAlias) == 1) ; DEBUG_("cur "..tmrAlias.." State = "..tostring(curTmrState))
if motion.flag then
quant = (now - R(tmrAlias)) * KOEF
if not motion.dir then
quant = quant * (-1)
end
DEBUG_("quant calculated as = "..quant)
WR(outAlias, 1) -- in motion now
else
DEBUG_("no motion lasts = ")
WR(outAlias, 0)
quant = 0
end -- if
WR(tmrAlias, now) -- store last call time
return quant
end -- function
-- округлление
function Round(var)
local integer, fraction = math.modf(var)
if fraction >= 0.5 then
integer = integer + 1
end
return math.floor(integer)
end
thisScriptID = 14
function DEBUG_(str)
-- ERROR("entered DEBUG_ in 35 script")
local i = 0
local tmp = ""
local debug_id = R("debug_IDs")
-- ERROR(debug_id)
local capture_mask = "%s+(%d+)%s+"
while true do
i,_, tmp = string.find(debug_id, capture_mask, i+1)
if i == nil then
-- ERROR("not found !")
break
end -- не найдено
if (tmp ~= "0") then
-- найдено проверить на совпадение
if (tmp == tostring(thisScriptID)) then
DEBUG(str)
-- ERROR("found own id , tmp = "..tmp)
return 0
end
end
end
end -- DEBUG_
------------------------------