1. Назначение

С помощью контроллера CCU825-PLС и функции программируемой логики (ПЛК) реализована система контроля и регулирования температуры воздуха в помещениях, оборудованных системой приточной вентиляции.

На следующих рисунках показано два варианта системы — с каналами догрева и без.

Вентилятор обдува калорифера (двигатель турбины) и жалюзи подачи воздуха управляются одним сигналом.

vent scheme
Рисунок 1. Система приточной вентиляции без каналов догрева
vent scheme with wings
Рисунок 2. Система приточной вентиляции с каналами догрева

2. Режимы работы

Режимы работы задаются следующими профилями в конфигурации контроллера. Показаны настройки профилей для системы приточной вентиляции без каналов догрева.

  • ОСТАНОВКА

    Номер профиля задается в программе константой COMMAND_STOP.

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

    command stop
  • ЛЕТО

    Номер профиля задается в программе константой COMMAND_SUMMER.

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

    Вентилятор включен, жалюзи открыты, КЗР полностью закрыт.

    command summer
  • РАБОТА_24_25

    Номер профиля задается в программе константой COMMAND_WORK1.

    command work1
  • РАБОТА_27_28

    Номер профиля задается в программе константой COMMAND_WORK2.

    command work2
  • РАБОТА_30_31

    Номер профиля задается в программе константой COMMAND_WORK3.

    command work3

В рабочих режимах перед включением вентилятора и открытием жалюзи осуществляется прогрев калорифера до значения температуры обратной воды, которое задано в программе константой TEMP_WATER_HEAT.

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

3. Формирование сигналов управления

Реализован закон управления как в приборе ОВЕН ТРМ33.

Управление запорно-регулирующим клапаном (КЗР) производится при помощи двух сигналов (КЗРоткр. и КЗРзакр.) c широтно-импульсной модуляцией (ШИМ).

\$D_i = 2.5 * K * (E_i + tau * DeltaE_i)\$

\$D_i\$ — длительность управляющего импульса в миллисекундах.

\$E_i = T_{уст.} - T_i\$ — величина рассогласования в текущем шаге регулирования.

\$DeltaE_i = E_i - E_{i-1}\$ — величина изменения рассогласования по сравнению с предыдущим вычислением \$D_{i-1}\$.

\$K\$ и \$tau\$ — коэффициенты регулятора.

Коэффициент \$K\$ (общий коэффициент усиления) определяет чувствительность регулятора как к величине рассогласования контролируемой им температуры, так и к скорости ее изменения.

Коэффициент \$tau\$ (коэффициент при дифференциальной составляющей) определяет чувствительность регулятора к резким изменениям контролируемой им температуры.

Направление перемещения КЗР определяется по знаку, полученному при вычислении \$D_i\$. При положительном значении \$D_i\$ формируется управляющий импульс на открытие КЗР, а при отрицательном значении — управляющий импульс на его закрытие.

При значениях \$D_i\$, численно больших шага регулирования, сигнал управления выдается непрерывно.

При управлении процессами с медленно изменяющимися во времени параметрами возможны ситуации, при которых температура объекта в течение шага регулирования будет меняться незначительно. В этом случае дифференциальная составляющая регулятора \$DeltaE_i = 0\$ перестает оказывать влияние на длительность управляющих импульсов, что может негативно отразиться на качестве регулирования. Во избежание таких ситуаций предусмотрена возможность увеличения интервала времени между соседними вычислениями \$D_i\$ и \$D_{i+1}\$. При этом длительность управляющего импульса вычисляется не в каждом шаге регулирования, а с пропуском некоторого их числа. В пропускаемых (для вычислений) шагах длительность импульсов управления остается неизменной и равной \$D_i\$. Параметр \$S\$ определяет в каком по счету шаге регулирования будет производиться последующее вычисление \$D_{i+1}\$.

4. Настройка коэффициентов регулятора

В процессе работы для достижения оптимального качества регулирования температуры может потребоваться изменение заданных для регулятора параметров настройки — \$S\$, \$K\$, \$tau\$.

Оптимальный выбор коэффициентов настройки регулятора позволяет максимально быстро и практически без перерегулирования температуры вывести объект на заданную уставку.

transit quality
Рисунок 3. Качество переходного процесса

5. Результаты работы

transit 1
Рисунок 4. Переходные процессы в системе без каналов догрева
transit 2
Рисунок 5. Переходные процессы в системе с каналами догрева
steady
Рисунок 6. Установившийся режим в системе с каналами догрева

6. Листинг программы для системы без каналов догрева

# Параметр S
const S = 1;

# Коэффициент K
const K_MAIN = 200;
# Коэффициент tau
const TAU_MAIN = 10;

# Шаг регулирования (в миллисекундах)
const CONTROL_STEP = 6000;

const S_IDLE = 60000 / CONTROL_STEP;

const FRACT = 8;
const K_2_5 = 640; # round(2.5 * 2^FRACT)

# Термодатчик уличный
const INPUT_TEMP_OUTDOOR = 1;
# Термодатчик на обратке
const INPUT_TEMP_WATER = 2;
# Термодатчик в канале
const INPUT_TEMP_MAIN_VENT_DUCT = 4;
# Термодатчик в помещении
const INPUT_TEMP_INDOOR = 5;

# Температура прогрева калорифера
const TEMP_WATER_HEAT = 60 << FRACT;

# Открытие КЗР
const OUTPUT_MAIN_VALVE_OPEN = 5;
# Закрытие КЗР
const OUTPUT_MAIN_VALVE_CLOSE = 6;

# Минимальная длительность управляющего импульса КЗР (в миллисекундах)
const VALVE_MIN_PULSE = 300;
# Длительность импульса полного открытия КЗР (в миллисекундах)
const VALVE_FULL_OPEN_PULSE = 105000;
# Длительность импульса полного закрытия КЗР (в миллисекундах)
const VALVE_FULL_CLOSE_PULSE = 105000;
# Длительность первого импульса закрытия КЗР в режиме ожидания (в миллисекундах)
const VALVE_IDLE_FIRST_CLOSE_PULSE = 90000;
# Длительность импульса открытия КЗР в режиме ожидания (в миллисекундах)
const VALVE_IDLE_OPEN_PULSE = 2000;
# Длительность импульса закрытия КЗР в режиме ожидания (в миллисекундах)
const VALVE_IDLE_CLOSE_PULSE = 2000;

# Вентилятор/жалюзи
const OUTPUT_FAN = 1;

# Капиллярный датчик на калорифере
const FREEZE = $EVT_INPUT3_ACTIVE;
# Пожарный датчик
const FIRE = $EVT_INPUT6_ACTIVE;

# Профиль ОСТАНОВКА
const COMMAND_STOP = $EVT_PROFILE3_APPLIED;
# Профиль ЛЕТО
const COMMAND_SUMMER = $EVT_PROFILE4_APPLIED;
# Профиль РАБОТА1
const COMMAND_WORK1 = $EVT_PROFILE5_APPLIED;
# Профиль РАБОТА2
const COMMAND_WORK2 = $EVT_PROFILE6_APPLIED;
# Профиль РАБОТА3
const COMMAND_WORK3 = $EVT_PROFILE7_APPLIED;

const MODE_IDLE = 0;
const MODE_SUMMER = 1;
const MODE_HEAT = 2;
const MODE_WORK = 3;
const MODE_FREEZE = 4;

var mode;
var main_valve_step, prev_main_valve_error, prev_main_valve_pulse;

proc main()
{
    if S < 1 {
        return;
    }

    var e = $get_event_id();

    if e == $EVT_INIT {
        init();
    } else if e == $EVT_TIMER1 {
        main_valve_control_step();
    } else if e == FREEZE {
        mode_freeze();
    } else if e == FIRE {
        mode_idle();
    } else if e == COMMAND_STOP {
        mode_idle();
    } else if e == COMMAND_SUMMER {
        mode_summer();
    } else if e == COMMAND_WORK1 {
        mode_work_or_heat();
    } else if e == COMMAND_WORK2 {
        mode_work_or_heat();
    } else if e == COMMAND_WORK3 {
        mode_work_or_heat();
    }
}

proc init()
{
    $set_event_mask($EM_INPUT | $EM_PROFILE);
    $set_timer(1, CONTROL_STEP / 100);
    apply_command(COMMAND_STOP);
}

proc mode_idle()
{
    set_mode(MODE_IDLE);
    fan_off();
    main_valve_close_pulse(VALVE_IDLE_FIRST_CLOSE_PULSE);
}

proc mode_freeze()
{
    set_mode(MODE_FREEZE);
}

proc mode_summer()
{
    if get_temp_outdoor() > get_temp_outdoor_high_limit() {
        set_mode(MODE_SUMMER);
        fan_on();
        main_valve_close_pulse(VALVE_FULL_CLOSE_PULSE);
    }
}

proc mode_work_or_heat()
{
    if mode == MODE_WORK {
        mode_work();
    } else {
        mode_heat();
    }
}

proc mode_work()
{
    set_mode(MODE_WORK);
    fan_on();
}

proc mode_heat()
{
    set_mode(MODE_HEAT);
    fan_off();
    main_valve_open_pulse(VALVE_FULL_OPEN_PULSE);
}

proc main_valve_control_step()
{
    if mode == MODE_SUMMER {
        return;
    }

    if mode == MODE_FREEZE {
        return;
    }

    var t = get_temp_water();
    var t_sp_low = get_temp_water_low_limit();
    var t_sp_high = get_temp_water_high_limit();
    var t_sp = midpoint(t_sp_low, t_sp_high);

    if mode == MODE_IDLE {
        idle_control_step(t, t_sp, t_sp_low, t_sp_high);
        return;
    }

    if mode == MODE_HEAT {
        if t < get_temp_water_heat() {
            return;
        } else {
            mode_work();
        }
    }

    if t > t_sp_low {
        t = get_temp_main_vent_duct();
        t_sp_low = get_temp_main_vent_duct_low_limit();
        t_sp_high = get_temp_main_vent_duct_high_limit();

        if t > t_sp_low && t < t_sp_high {
            return;
        }

        t_sp = midpoint(t_sp_low, t_sp_high);
    }

    var d = calc_main_valve_pulse(t, t_sp);

    if abs(d) < VALVE_MIN_PULSE {
        return;
    }

    if d > 0 {
        main_valve_open(d);
    } else {
        main_valve_close(-d);
    }
}

proc idle_control_step(t, t_sp, t_sp_low, t_sp_high)
{
    main_valve_step = main_valve_step + 1;

    if main_valve_step < S_IDLE {
        return;
    }

    main_valve_step = 0;

    var e = t_sp - t;
    var de = e - prev_main_valve_error;

    prev_main_valve_error = e;

    if t > t_sp_high && de <= 0 {
        main_valve_close_pulse(VALVE_IDLE_CLOSE_PULSE);
    } else if t < t_sp_low && de >= 0 {
        main_valve_open_pulse(VALVE_IDLE_OPEN_PULSE);
    }
}

fun calc_main_valve_pulse(t, t_sp)
{
    main_valve_step = main_valve_step + 1;

    if main_valve_step < S {
        return prev_main_valve_pulse;
    }

    main_valve_step = 0;

    var e = t_sp - t;
    var de = e - prev_main_valve_error;

    prev_main_valve_error = e;

    var d = calc_pulse(K_MAIN, TAU_MAIN, e, de);

    if abs(prev_main_valve_pulse) < VALVE_MIN_PULSE {
        d = d + prev_main_valve_pulse;
    }

    prev_main_valve_pulse = d;
    return d;
}

fun calc_pulse(k, tau, e, de)
{
    return K_2_5 * k * (e + tau * de) >> FRACT * 2;
}

fun get_temp_water_heat()
{
    return TEMP_WATER_HEAT;
}

fun get_temp_water()
{
    return $get_sensor_value(INPUT_TEMP_WATER, FRACT);
}

fun get_temp_water_low_limit()
{
    return $get_sensor_low_limit(INPUT_TEMP_WATER, FRACT);
}

fun get_temp_water_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_WATER, FRACT);
}

fun get_temp_main_vent_duct()
{
    return $get_sensor_value(INPUT_TEMP_MAIN_VENT_DUCT, FRACT);
}

fun get_temp_main_vent_duct_low_limit()
{
    return $get_sensor_low_limit(INPUT_TEMP_MAIN_VENT_DUCT, FRACT);
}

fun get_temp_main_vent_duct_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_MAIN_VENT_DUCT, FRACT);
}

fun get_temp_outdoor()
{
    return $get_sensor_value(INPUT_TEMP_OUTDOOR, FRACT);
}

fun get_temp_outdoor_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_OUTDOOR, FRACT);
}

proc main_valve_open_pulse(d)
{
    valve_open_pulse(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc main_valve_close_pulse(d)
{
    valve_close_pulse(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc main_valve_open(d)
{
    valve_open(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc main_valve_close(d)
{
    valve_close(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc valve_open(n_open, n_close, d)
{
    if d < CONTROL_STEP {
        valve_open_pulse(n_open, n_close, d);
    } else {
        valve_open_state(n_open, n_close);
    }
}

proc valve_close(n_open, n_close, d)
{
    if d < CONTROL_STEP {
        valve_close_pulse(n_open, n_close, d);
    } else {
        valve_close_state(n_open, n_close);
    }
}

proc valve_open_pulse(n_open, n_close, d)
{
    $set_output_state(n_close, $OFF);
    valve_pulse(n_open, d);
}

proc valve_close_pulse(n_open, n_close, d)
{
    $set_output_state(n_open, $OFF);
    valve_pulse(n_close, d);
}

proc valve_pulse(n, d)
{
    $set_output_pulse(n, $ON, d / 100);
}

proc valve_open_state(n_open, n_close)
{
    $set_output_state(n_close, $OFF);
    $set_output_state(n_open, $ON);
}

proc valve_close_state(n_open, n_close)
{
    $set_output_state(n_open, $OFF);
    $set_output_state(n_close, $ON);
}

proc fan_on()
{
    $set_output_state(OUTPUT_FAN, $ON);
}

proc fan_off()
{
    $set_output_state(OUTPUT_FAN, $OFF);
}

proc set_mode(m)
{
    mode = m;
    reset_calc_state();
}

proc reset_calc_state()
{
    main_valve_step = 0;
    prev_main_valve_error = 0;
    prev_main_valve_pulse = 0;
}

proc apply_command(c)
{
    $apply_profile(c - $EVT_PROFILE1_APPLIED + 1);
}

fun abs(x)
{
    if x < 0 {
        return -x;
    } else {
        return x;
    }
}

fun midpoint(x, y)
{
    return (x + y) / 2;
}

7. Листинг программы для системы с каналами догрева

# Параметр S
const S = 1;

# Коэффициент K основного канала
const K_MAIN = 200;
# Коэффициент tau основного канала
const TAU_MAIN = 10;

# Коэффициент K левого канала догрева
const K_LEFT = 20;
# Коэффициент tau левого канала догрева
const TAU_LEFT = 10;

# Коэффициент K правого канала догрева
const K_RIGHT = 20;
# Коэффициент tau правого канала догрева
const TAU_RIGHT = 10;

# Шаг регулирования (в миллисекундах)
const CONTROL_STEP = 6000;

const S_IDLE = 60000 / CONTROL_STEP;

const FRACT = 8;
const K_2_5 = 640; # round(2.5 * 2^FRACT)

# Термодатчик уличный
const INPUT_TEMP_OUTDOOR = 1;
# Термодатчик на обратке
const INPUT_TEMP_WATER = 2;
# Термодатчик в основном канале
const INPUT_TEMP_MAIN_VENT_DUCT = 4;
# Термодатчик в левом канале догрева
const INPUT_TEMP_LEFT_VENT_DUCT = 5;
# Термодатчик в правом канале догрева
const INPUT_TEMP_RIGHT_VENT_DUCT = 6;
# Термодатчик в помещении
const INPUT_TEMP_INDOOR = 7;

# Температура прогрева калорифера
const TEMP_WATER_HEAT = 60 << FRACT;

# Открытие основного КЗР
const OUTPUT_MAIN_VALVE_OPEN = 3;
# Закрытие основного КЗР
const OUTPUT_MAIN_VALVE_CLOSE = 4;

# Открытие левого КЗР
const OUTPUT_LEFT_VALVE_OPEN = 5;
# Закрытие левого КЗР
const OUTPUT_LEFT_VALVE_CLOSE = 6;

# Открытие правого КЗР
const OUTPUT_RIGHT_VALVE_OPEN = 2;
# Закрытие правого КЗР
const OUTPUT_RIGHT_VALVE_CLOSE = 7;

# Минимальная длительность управляющего импульса КЗР (в миллисекундах)
const VALVE_MIN_PULSE = 300;
# Длительность импульса полного открытия КЗР (в миллисекундах)
const VALVE_FULL_OPEN_PULSE = 105000;
# Длительность импульса полного закрытия КЗР (в миллисекундах)
const VALVE_FULL_CLOSE_PULSE = 105000;
# Длительность первого импульса закрытия КЗР в режиме ожидания (в миллисекундах)
const VALVE_IDLE_FIRST_CLOSE_PULSE = 90000;
# Длительность импульса открытия КЗР в режиме ожидания (в миллисекундах)
const VALVE_IDLE_OPEN_PULSE = 2000;
# Длительность импульса закрытия КЗР в режиме ожидания (в миллисекундах)
const VALVE_IDLE_CLOSE_PULSE = 2000;

# Вентилятор/жалюзи
const OUTPUT_FAN = 1;

# Капиллярный датчик на калорифере
const FREEZE = $EVT_INPUT3_ACTIVE;
# Пожарный датчик
const FIRE = $EVT_INPUT8_ACTIVE;

# Профиль ОСТАНОВКА
const COMMAND_STOP = $EVT_PROFILE3_APPLIED;
# Профиль ЛЕТО
const COMMAND_SUMMER = $EVT_PROFILE4_APPLIED;
# Профиль РАБОТА1
const COMMAND_WORK1 = $EVT_PROFILE5_APPLIED;
# Профиль РАБОТА2
const COMMAND_WORK2 = $EVT_PROFILE6_APPLIED;
# Профиль РАБОТА3
const COMMAND_WORK3 = $EVT_PROFILE7_APPLIED;

const MODE_IDLE = 0;
const MODE_SUMMER = 1;
const MODE_HEAT = 2;
const MODE_WORK = 3;
const MODE_FREEZE = 4;

var mode;
var main_valve_step, prev_main_valve_error, prev_main_valve_pulse;
var left_valve_step, prev_left_valve_error, prev_left_valve_pulse;
var right_valve_step, prev_right_valve_error, prev_right_valve_pulse;

proc main()
{
    if S < 1 {
        return;
    }

    var e = $get_event_id();

    if e == $EVT_INIT {
        init();
    } else if e == $EVT_TIMER1 {
        main_valve_control_step();
        left_valve_control_step();
        right_valve_control_step();
    } else if e == FREEZE {
        mode_freeze();
    } else if e == FIRE {
        mode_idle();
    } else if e == COMMAND_STOP {
        mode_idle();
    } else if e == COMMAND_SUMMER {
        mode_summer();
    } else if e == COMMAND_WORK1 {
        mode_work_or_heat();
    } else if e == COMMAND_WORK2 {
        mode_work_or_heat();
    } else if e == COMMAND_WORK3 {
        mode_work_or_heat();
    }
}

proc init()
{
    $set_event_mask($EM_INPUT | $EM_PROFILE);
    $set_timer(1, CONTROL_STEP / 100);
    apply_command(COMMAND_STOP);
}

proc mode_idle()
{
    set_mode(MODE_IDLE);
    fan_off();
    main_valve_close_pulse(VALVE_IDLE_FIRST_CLOSE_PULSE);
    left_valve_close_pulse(VALVE_FULL_CLOSE_PULSE);
    right_valve_close_pulse(VALVE_FULL_CLOSE_PULSE);
}

proc mode_freeze()
{
    set_mode(MODE_FREEZE);
}

proc mode_summer()
{
    if get_temp_outdoor() > get_temp_outdoor_high_limit() {
        set_mode(MODE_SUMMER);
        fan_on();
        main_valve_close_pulse(VALVE_FULL_CLOSE_PULSE);
        left_valve_close_pulse(VALVE_FULL_CLOSE_PULSE);
        right_valve_close_pulse(VALVE_FULL_CLOSE_PULSE);
    }
}

proc mode_work_or_heat()
{
    if mode == MODE_WORK {
        mode_work();
    } else {
        mode_heat();
    }
}

proc mode_work()
{
    set_mode(MODE_WORK);
    fan_on();
}

proc mode_heat()
{
    set_mode(MODE_HEAT);
    fan_off();
    main_valve_open_pulse(VALVE_FULL_OPEN_PULSE);
}

proc main_valve_control_step()
{
    if mode == MODE_SUMMER {
        return;
    }

    if mode == MODE_FREEZE {
        return;
    }

    var t = get_temp_water();
    var t_sp_low = get_temp_water_low_limit();
    var t_sp_high = get_temp_water_high_limit();
    var t_sp = midpoint(t_sp_low, t_sp_high);

    if mode == MODE_IDLE {
        idle_control_step(t, t_sp, t_sp_low, t_sp_high);
        return;
    }

    if mode == MODE_HEAT {
        if t < get_temp_water_heat() {
            return;
        } else {
            mode_work();
        }
    }

    if t > t_sp_low {
        t = get_temp_main_vent_duct();
        t_sp_low = get_temp_main_vent_duct_low_limit();
        t_sp_high = get_temp_main_vent_duct_high_limit();

        if t > t_sp_low && t < t_sp_high {
            return;
        }

        t_sp = midpoint(t_sp_low, t_sp_high);
    }

    var d = calc_main_valve_pulse(t, t_sp);

    if abs(d) < VALVE_MIN_PULSE {
        return;
    }

    if d > 0 {
        main_valve_open(d);
    } else {
        main_valve_close(-d);
    }
}

proc left_valve_control_step()
{
    if mode != MODE_WORK {
        return;
    }

    var t = get_temp_left_vent_duct();
    var t_sp_low = get_temp_left_vent_duct_low_limit();
    var t_sp_high = get_temp_left_vent_duct_high_limit();

    if t > t_sp_low && t < t_sp_high {
        return;
    }

    var t_sp = midpoint(t_sp_low, t_sp_high);

    var d = calc_left_valve_pulse(t, t_sp);

    if abs(d) < VALVE_MIN_PULSE {
        return;
    }

    if d > 0 {
        left_valve_open(d);
    } else {
        left_valve_close(-d);
    }
}

proc right_valve_control_step()
{
    if mode != MODE_WORK {
        return;
    }

    var t = get_temp_right_vent_duct();
    var t_sp_low = get_temp_right_vent_duct_low_limit();
    var t_sp_high = get_temp_right_vent_duct_high_limit();

    if t > t_sp_low && t < t_sp_high {
        return;
    }

    var t_sp = midpoint(t_sp_low, t_sp_high);

    var d = calc_right_valve_pulse(t, t_sp);

    if abs(d) < VALVE_MIN_PULSE {
        return;
    }

    if d > 0 {
        right_valve_open(d);
    } else {
        right_valve_close(-d);
    }
}

proc idle_control_step(t, t_sp, t_sp_low, t_sp_high)
{
    main_valve_step = main_valve_step + 1;

    if main_valve_step < S_IDLE {
        return;
    }

    main_valve_step = 0;

    var e = t_sp - t;
    var de = e - prev_main_valve_error;

    prev_main_valve_error = e;

    if t > t_sp_high && de <= 0 {
        main_valve_close_pulse(VALVE_IDLE_CLOSE_PULSE);
    } else if t < t_sp_low && de >= 0 {
        main_valve_open_pulse(VALVE_IDLE_OPEN_PULSE);
    }
}

fun calc_main_valve_pulse(t, t_sp)
{
    main_valve_step = main_valve_step + 1;

    if main_valve_step < S {
        return prev_main_valve_pulse;
    }

    main_valve_step = 0;

    var e = t_sp - t;
    var de = e - prev_main_valve_error;

    prev_main_valve_error = e;

    var d = calc_pulse(K_MAIN, TAU_MAIN, e, de);

    if abs(prev_main_valve_pulse) < VALVE_MIN_PULSE {
        d = d + prev_main_valve_pulse;
    }

    prev_main_valve_pulse = d;
    return d;
}

fun calc_left_valve_pulse(t, t_sp)
{
    left_valve_step = left_valve_step + 1;

    if left_valve_step < S {
        return prev_left_valve_pulse;
    }

    left_valve_step = 0;

    var e = t_sp - t;
    var de = e - prev_left_valve_error;

    prev_left_valve_error = e;

    var d = calc_pulse(K_LEFT, TAU_LEFT, e, de);

    if abs(prev_left_valve_pulse) < VALVE_MIN_PULSE {
        d = d + prev_left_valve_pulse;
    }

    prev_left_valve_pulse = d;
    return d;
}

fun calc_right_valve_pulse(t, t_sp)
{
    right_valve_step = right_valve_step + 1;

    if right_valve_step < S {
        return prev_right_valve_pulse;
    }

    right_valve_step = 0;

    var e = t_sp - t;
    var de = e - prev_right_valve_error;

    prev_right_valve_error = e;

    var d = calc_pulse(K_RIGHT, TAU_RIGHT, e, de);

    if abs(prev_right_valve_pulse) < VALVE_MIN_PULSE {
        d = d + prev_right_valve_pulse;
    }

    prev_right_valve_pulse = d;
    return d;
}

fun calc_pulse(k, tau, e, de)
{
    return K_2_5 * k * (e + tau * de) >> FRACT * 2;
}

fun get_temp_water_heat()
{
    return TEMP_WATER_HEAT;
}

fun get_temp_water()
{
    return $get_sensor_value(INPUT_TEMP_WATER, FRACT);
}

fun get_temp_water_low_limit()
{
    return $get_sensor_low_limit(INPUT_TEMP_WATER, FRACT);
}

fun get_temp_water_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_WATER, FRACT);
}

fun get_temp_main_vent_duct()
{
    return $get_sensor_value(INPUT_TEMP_MAIN_VENT_DUCT, FRACT);
}

fun get_temp_main_vent_duct_low_limit()
{
    return $get_sensor_low_limit(INPUT_TEMP_MAIN_VENT_DUCT, FRACT);
}

fun get_temp_main_vent_duct_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_MAIN_VENT_DUCT, FRACT);
}

fun get_temp_left_vent_duct()
{
    return $get_sensor_value(INPUT_TEMP_LEFT_VENT_DUCT, FRACT);
}

fun get_temp_left_vent_duct_low_limit()
{
    return $get_sensor_low_limit(INPUT_TEMP_LEFT_VENT_DUCT, FRACT);
}

fun get_temp_left_vent_duct_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_LEFT_VENT_DUCT, FRACT);
}

fun get_temp_right_vent_duct()
{
    return $get_sensor_value(INPUT_TEMP_RIGHT_VENT_DUCT, FRACT);
}

fun get_temp_right_vent_duct_low_limit()
{
    return $get_sensor_low_limit(INPUT_TEMP_RIGHT_VENT_DUCT, FRACT);
}

fun get_temp_right_vent_duct_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_RIGHT_VENT_DUCT, FRACT);
}

fun get_temp_outdoor()
{
    return $get_sensor_value(INPUT_TEMP_OUTDOOR, FRACT);
}

fun get_temp_outdoor_high_limit()
{
    return $get_sensor_high_limit(INPUT_TEMP_OUTDOOR, FRACT);
}

proc main_valve_open_pulse(d)
{
    valve_open_pulse(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc main_valve_close_pulse(d)
{
    valve_close_pulse(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc left_valve_open_pulse(d)
{
    valve_open_pulse(OUTPUT_LEFT_VALVE_OPEN, OUTPUT_LEFT_VALVE_CLOSE, d);
}

proc left_valve_close_pulse(d)
{
    valve_close_pulse(OUTPUT_LEFT_VALVE_OPEN, OUTPUT_LEFT_VALVE_CLOSE, d);
}

proc right_valve_open_pulse(d)
{
    valve_open_pulse(OUTPUT_RIGHT_VALVE_OPEN, OUTPUT_RIGHT_VALVE_CLOSE, d);
}

proc right_valve_close_pulse(d)
{
    valve_close_pulse(OUTPUT_RIGHT_VALVE_OPEN, OUTPUT_RIGHT_VALVE_CLOSE, d);
}

proc main_valve_open(d)
{
    valve_open(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc main_valve_close(d)
{
    valve_close(OUTPUT_MAIN_VALVE_OPEN, OUTPUT_MAIN_VALVE_CLOSE, d);
}

proc left_valve_open(d)
{
    valve_open(OUTPUT_LEFT_VALVE_OPEN, OUTPUT_LEFT_VALVE_CLOSE, d);
}

proc left_valve_close(d)
{
    valve_close(OUTPUT_LEFT_VALVE_OPEN, OUTPUT_LEFT_VALVE_CLOSE, d);
}

proc right_valve_open(d)
{
    valve_open(OUTPUT_RIGHT_VALVE_OPEN, OUTPUT_RIGHT_VALVE_CLOSE, d);
}

proc right_valve_close(d)
{
    valve_close(OUTPUT_RIGHT_VALVE_OPEN, OUTPUT_RIGHT_VALVE_CLOSE, d);
}

proc valve_open(n_open, n_close, d)
{
    if d < CONTROL_STEP {
        valve_open_pulse(n_open, n_close, d);
    } else {
        valve_open_state(n_open, n_close);
    }
}

proc valve_close(n_open, n_close, d)
{
    if d < CONTROL_STEP {
        valve_close_pulse(n_open, n_close, d);
    } else {
        valve_close_state(n_open, n_close);
    }
}

proc valve_open_pulse(n_open, n_close, d)
{
    $set_output_state(n_close, $OFF);
    valve_pulse(n_open, d);
}

proc valve_close_pulse(n_open, n_close, d)
{
    $set_output_state(n_open, $OFF);
    valve_pulse(n_close, d);
}

proc valve_pulse(n, d)
{
    $set_output_pulse(n, $ON, d / 100);
}

proc valve_open_state(n_open, n_close)
{
    $set_output_state(n_close, $OFF);
    $set_output_state(n_open, $ON);
}

proc valve_close_state(n_open, n_close)
{
    $set_output_state(n_open, $OFF);
    $set_output_state(n_close, $ON);
}

proc fan_on()
{
    $set_output_state(OUTPUT_FAN, $ON);
}

proc fan_off()
{
    $set_output_state(OUTPUT_FAN, $OFF);
}

proc set_mode(m)
{
    mode = m;
    reset_calc_state();
}

proc reset_calc_state()
{
    main_valve_step = 0;
    prev_main_valve_error = 0;
    prev_main_valve_pulse = 0;

    left_valve_step = 0;
    prev_left_valve_error = 0;
    prev_left_valve_pulse = 0;

    right_valve_step = 0;
    prev_right_valve_error = 0;
    prev_right_valve_pulse = 0;
}

proc apply_command(c)
{
    $apply_profile(c - $EVT_PROFILE1_APPLIED + 1);
}

fun abs(x)
{
    if x < 0 {
        return -x;
    } else {
        return x;
    }
}

fun midpoint(x, y)
{
    return (x + y) / 2;
}