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 таймеров, но теперь мы точно знаем, что они откусывают по кусочку от нашей производительности.

August 4, 2016

Доступ к портам ввода/вывода

TwinCAT позволяет превратить обычный ПК в PC-based Control System, то есть в итоге мы получим обычный ПК, который сможет управлять промышленными объектами. В общем, так и есть, но сам ПК по прежнему с нами и все его "плюшки" по прежнему готовы к употреблению.

Начнем с простого — графическая система. ПК позволяет запускать "тяжелые" графические системы: вы можете взять Unity3D, нарисовать "тяжелую", трехмерную, интерактивную визуализацию, а затем через TwinCAT.Ads API связать ее с ПЛК-программой реального времени. Ну, или просто запустить игру и убедиться, что она работает как на обычном десктопе:



Убедившись, что это работает, спустимся ниже — куда-нибудь ближе к железу ПК. Здесь TwinCAT предлагает нам, как специалистам умелым и разумным, прямой путь к железу ПК через порты/ввода.
Выдержка из справочной системы: прямой доступ к железу — не проблема, пока вы только читаете из портов. Проблемы начнутся, когда вы начнете писать в порты ввода/вывода железа.


Порты ввода/вывода


Доступ к железу ПК начинается с портов/ввода вывода, через которые мы общаемся с периферией. Там на системную шину выставляются адреса, затем данные, но если вы собрались работать с портами, то уже должны все это знать.

В справочной системе, для примера, приводится работа с "пищалкой" pc-speaker'ом; для работы которого сначала нужно записать значение делителя для системного таймера, а затем установить бит включения спикера (или сбросить его, когда он надоест своим пронзительным верещанием).

Для работы с портами используются две функции F_IOPortRead и F_IOPortWrite. Описывать их смысла мало, поэтому возьмем пример из справочной системы и заставим спикер исполнять простые мелодии. Полный проект доступен на GitHub'е в виде исходных кодов плеера и готовой к установке библиотеки.


Готовая библиотека


Чтобы было проще, я обернул функцию спикера в библиотечную функцию FB_PcSpeaker. Саму библиотеку Tc3_PcSpeaker (с единственной функцией) можно скачать и установить в систему.
Как устанавливать библиотеки расписано в нюансах библиотек в TwinCAT 3.

Единственная функция проста до приличия:

Speaker : FB_PcSpeaker;
F, L, D : REAL;
M       : REAL := 1; // duration multiplier

[...]

Speaker(
    Freq    := REAL_TO_DWORD( F ),
    Length  := REAL_TO_TIME ( L * M ),
    Delay   := REAL_TO_TIME ( D ),
    Execute := TRUE );

IF NOT Speaker.Busy THEN
    Speaker(Execute:= FALSE);
    // Next beep, please...
END_IF


Исполняем мелодии


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

VAR_GLOBAL CONSTANT
(* Star Wars Imperial March *) 
    Beeps : ARRAY[0..201] OF REAL := [392, 350, 100, 392, 350, 100, (* [...] *), 0, 0, 0];
END_VAR


Для извлечения нот из массива произвольной длины, будем извлекать ноты через... указатели, копируя значения до тех пор, пока не встретится набор из трех нулей. Почти как тип STRING, оканчивающийся нулем /0, только три нуля.
Можно было бы воспользоваться функцией UPPER_BOUND для получения размера массива, но использовать мощный, промышленный контроллер для воспроизведения мелодий уже не оптимально, так что продолжаем развлекаться...

pNotes : PVOID;
szR    : UDINT := SIZEOF(REAL);

[...]

MEMCPY(ADR(F), pNotes, szR);
pNotes := pNotes + szR;
MEMCPY(ADR(L), pNotes, szR);
pNotes := pNotes + szR;
MEMCPY(ADR(D), pNotes, szR);
pNotes := pNotes + szR;


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

Speaker.SwitchOff();

August 2, 2016

Упорядоченные структуры

В TwinCAT 3 система типов данных Data Type Unit (DUT) наконец-то эволюционировала во что-то вменяемое. Теперь она похожа на настоящую и ею удобно пользоваться.


Псевдонимы


ALIAS — позволяет называть другими именами стандартные типы данных.

TYPE FLOAT : REAL;
END_TYPE

или
TYPE DOUBLE : LREAL;
END_TYPE


Структуры


STRUCT — структуры, составные данные. Раньше нельзя было включать в них переменные ввода/вывода:


...а теперь можно:

TYPE ST_DATA :
STRUCT
    In  AT %I* : INT;
    Out AT %Q* : INT;
END_STRUCT
END_TYPE


Структуры стали ближе к классам — теперь их можно расширять:

TYPE ST_DATA_EX EXTENDS ST_DATA :
STRUCT
    Status : INT;
END_STRUCT
END_TYPE

Это сокращает количество кода и позволяет дополнять библиотечные типы данных без излишней копи-пасты:

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


Битовый поля


В структуры можно включать битовые поля, доступные по имени. Для этого существует специальный тип данных BIT. Он занимает ровно 1 бит. На переменную типа BIT нельзя ссылаться указателями и ссылками. Также нельзя использовать их в массивах.


Перечисления


Теперь это глобальная переменная, которая может принимать одно из заранее заданных значений.  Сравните:



Каждое значение имеет свое собственное имя. Имя самого перечисления (E_PI) стало пространством имен этих значений. Теперь нельзя употреблять имена значений (HISTORICAL) без имени перечисления (E_PI).

CASE pi OF
    E_PI.HISTORICAL:
        DrawCircle();

    E_PI.PRECISE:
        DrawRoundCircle();

    E_PI.WARTIME:
        DrawRectangleFast();
END_CASE


Можно задать базовый тип для перечисления, но на выбор только целые типы данных (byte, word, uint, int, и т. п.). Также нельзя использовать псевдонимы (ALIAS) в качестве базовых типов.

TYPE E_PI :
(
    HISTORICAL := 3,
    PRECISE    := 314,
    WARTIME    := 4
) INT;
END_TYPE

По умолчанию, в качестве базового типа выступает INT. Значение первого поля = 0, остальные последовательно увеличиваются на единицу.


Объединение


UNION — для неопределившихся в типе переменной.

TYPE VARIANT :
UNION
    Integer : INT;
    Float   : REAL;
    Double  : LREAL;
    Text    : STRING(4);
END_UNION
END_TYPE

В текущий момент времени, одна переменная позволяет хранить одно значение одного из перечисленных типов данных. Хранится только что-то одно: или целое, или текст, но не одновременно. Другие возможные типы в этот момент хранят мусор и не имеют вменяемого значения. Размер переменной вычисляется по длине самого длинного поля.

v : VARIANT;

[...]

v.Integer := 4;
v.Double  := 3.14;
v.Text    := 'PI';
v.Float   := 1.7;