August 17, 2016

Тысячи TON

Иногда нужно подождать несколько миллисекунд или переждать час-другой. В таком случае на помощь приходит стандартный блок TON. Так ли он прост или есть нюансы?

Погружаясь в глубины TcBase.lib, туда, где живет наш функциональный блок, мы вскоре ударяемся о дно: внутренности блока TON прячутся в толще TwinCAT, а хвост ведет к ядру операционной системы. Как же нам узнать каким образом отсчитываются интервалы времени: банальный счетчик или что-то посерьезнее? У меня есть догадки, и я попробую подойти к вопросу медленно и с другой стороны.

Нафантазируем систему в которой необходимо много таймеров с разными интервалами срабатывания и различными временами старта. Под словом "много" я принимаю порядки 100, 1000, 10'000, ... и чуть позже станет понятно зачем мне необходимо так много.

Для испытаний возьмем CX9020 (ARM, WinCE) и TwinCAT 2, а затем попробуем старшего брата — CX5010 (x86, WinCE) и наконец CX2030 с настольной операционной системой и TwinCAT 3. Для всех тестов настройки будут по умолчанию: базовое время = 1мс, цикл ПЛК-программы = 10мс.

Загрузим тестовую программу в которой каждый цикл обрабатывается очень много таймеров (одновременно):

PROGRAM MAIN
VAR CONSTANT
    HOW_MANY_TONS : UDINT := 100;
    START_TIME    : TIME  := t#100ms;
    NEXT_PLUS     : TIME  := t#1ms;
END_VAR
VAR
    i      : UDINT;
    pt     : TIME := START_TIME;
    timers : ARRAY [1..HOW_MANY_TONS] OF TON;
    state  : UINT;
END_VAR

[...]

CASE state OF
0:
    FOR i := 1 TO HOW_MANY_TONS DO
        timers[i](IN:= FALSE, PT := pt);
        pt := pt + NEXT_PLUS;
    END_FOR

    state := 100;

100:
    FOR i := 1 TO HOW_MANY_TONS DO
        timers[i](IN:= TRUE);
        IF timers[i].Q THEN
            timers[i](IN := FALSE);
        END_IF
    END_FOR
END_CASE

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



Запускаем 100 таймеров — нагрузка возрастает до 23%:



Предварительный вывод: таймеры нагружают процессор. Увеличиваем порядок до 1000... и система ведет себя неадекватно и небрежно, отнимая время у операционной системы:




Может создаться впечатление, что вызывается слишком много ФБ за один цикл, но на самом деле это не так, и для опровержения я создал ФБ, который интерфейсом повторяет TON, но внутри просто наращивает переменную каждый цикл. Результат совершенно другой: при вызове 1000 имитаций нагрузка не растет, а остается на прежнем уровне 21%.

Вывод: TON не так прост внутри, как снаружи. Внутри ФБ производится какая-то тяжелая работа.


Старшие братья и TwinCAT 3


Можно предположить, что такое поведение свойственно только младшим контроллерам или происходит только на ARM-платформе, но и это не так.

Как более производительный, CX5010 отваливается на 10'000 таймерах. Правда он при этом выдает более ровную нагрузочную кривую, но нам интересно не это. Для CX2030 и настольной операционной системы, можно ожидать лучшего, но там процессор мощнее, поэтому есть рост количества таймеров, но нагрузка все равно ощущается как нечто постороннее. Зато TwinCAT 3 наконец-то не отжирает время у операционной системы. На картинке ниже виден переход от порядка 10'000 к 100'000; система реального времени уже не справляется, но терпеливо держит планку на заданном ограничении 80%:



Объяснение


Похоже, что TON ведет в глубины операционной системы к системным таймерам и может быть даже к высокоточному таймеру HPET. Так что же делать?


Что делать?


Во первых, если от таймеров нужна не сверхточность, а их количество — можно сделать один "продолжительный" таймер и всюду использовать его внутреннее поле ElapsedTime, которое показывает сколько времени прошло с начала запуска таймера... Или может быть сделать свой простой таймер со счетчиком-инкрементом?

Во вторых, зачем вам в проекте столько таймеров? Не забывайте про этот пункт. Я пытаюсь понять из чего сделан TON, а не улучшить его, разработав готовое решение для своих проектов.


Все-таки делаем свой таймер


Попробуем доработать нашу имитацию до полноценного таймера. Я назвала его TONLi, потому что он облегченный (light) и потому, что есть LTON (long, еще более длинный).

FUNCTION_BLOCK TONLi
VAR_INPUT
    IN        : BOOL;
    PT        : TIME;
    TaskIndex : BYTE := 1;
END_VAR
VAR_OUTPUT
    Q         : BOOL;
END_VAR
VAR
    state     : BYTE;
    endCount  : UDINT;
END_VAR

[...]

IF IN THEN
    IF state = 0 THEN
        Q := FALSE;
        endCount :=
            SystemTaskInfoArr[TaskIndex].cycleCount +
            TIME_TO_UDINT(PT) * 10000 / SystemTaskInfoArr[TaskIndex].cycleTime;
        state := 10;
    END_IF
ELSE
    state := 0;
END_IF

IF state = 10 THEN
    IF SystemTaskInfoArr[TaskIndex].cycleCount >= endCount THEN
        Q := TRUE;
        state := 255;
    END_IF
END_IF

Интерфейс почти такой же: на выходе потеряли ElapsedTime (с ним будет чуть медленнее из-за дополнительных расчетов); на входе теперь нужно уточнять номер текущей задачи. Ее можно узнать с помощью функции GETCURTASKINDEX. Об этом чуть позже.
В TwinCAT 3 нужно заменить SystemTaskInfoArr на _TaskInfo и можете убрать ссылку на TcSystem.lib. Подробнее читайте в справочной системе о PlcTaskSystemInfo.

Я все еще сделать немного сложнее, чем можно было: если взять обычный счетчик, то придется вызывать таймер каждый цикл и малейший пропуск собьет весь счет времени. Если же использовать .сycleCount — вы всегда будете знать сколько циклов уже прошло (длина цикла фиксирована еще на этапе настройки системы и не меняется во время работы). В этом наш таймер похож на стандартный TON.


_TaskInfo


Чтобы в случае пропуска циклов, не терять кусок прошедшего времени, нам нужен некий независимый отсчет времени. Его мы возьмем из информации о ПЛК-задаче. В большинстве (но не во всех!) CX-системах эта информация сохраняется в структуре, расположенной в меркерной области памяти. Обратите внимание на комментарий — настоящий адрес структуры может отличаться от приведенного (the real address may differ!):

SystemTaskInfoArr AT %MB32832 (*The real address may differ!*) : ARRAY [1..4] OF SYSTEMTASKINFOTYPE;

Что в этой структуре поможет нам сейчас или пригодится в будущем:
  • firstCycle : BOOL — задача запущена и отрабатывает свой первый цикл.
  • cycleTime : UDINT — длина цикла заданной ПЛК-задачи в 100 наносекундных отрезках. Если разделить это число на 10'000 — получится значение в миллисекундах.
  • lastExecTime : UDINT — количество 100нс интервалов отработанных предыдущим циклом. Не длина предыдущего цикла (эта длина для всех одинакова), а сколько реально он работал. Задача простаивала остаток времени.
  • cycleCount : UDINT — необходимый нам счетчик циклов, прошедших с момента старта ПЛК-задачи.


Новый таймер под микроскопом


Будем проводить испытания на CX9020 и 10'000 таймерах:


Все еще работает, а ведь раньше срезался на 1000 штуках. Еще раз: было 100 — стало 10'000. Проверим на CX5010, а лучше сравним TON (левая половина графика) и TONLi (правая половина):



Вряд ли кому-то понадобятся тысячи TON таймеров, но теперь мы точно знаем, что они откусывают по кусочку от нашей производительности.

No comments

Post a Comment

Note: Only a member of this blog may post a comment.