Введение
За время работы инженером-программистом ПЛК очень часто в ходе разработки находились не самые очевидные, но довольно простые и красивые решения как типовых, так и специализированных задач. В этой статье хочу поделиться опытом и рассказать как сделать разработку под ПЛК приятнее и эффективнее.
Об опыте автора
Опыт работы с ПЛК: 3 года.
Разработка под ПЛК: Beckhoff CX series, SE Modicon M221, WAGO 750 series.
Среды разработки: TwinCAT 3, EcoStruxure Machine Expert-Basic, CODESYS V2.3.
Основная часть опыта приходится на ST+TwinCAT 3, который базируется на CODESYS и IEC 61131.
Статью решил написать так как покидаю OT и перехожу в мир IT. Хочется поделится опытом, чтобы эти 3 года не прошли даром.
Среда разработки
Если часто приходиться комментировать части кода - то узнайте какое сочетание клавиш позволит вам это сделать, это сэкономит много времени. В TwinCAT XAE Shell для комментирования выделенного кода: Ctrl+K+C и Ctrl+K+U для расскомментирования.
Обезвредьте кнопку Stop, чтобы случайно не остановить ПЛК, иногда такое случайное нажатие может привести к нежелательным последствиям. В TwinCAT XAE Shell можно выбрать какие кнопки выводить на toolbar. После локальной отладки программы рекомендую скрыть кнопку остановки ПЛК.
Structured Text
STRING vs WSTRING
В TwinCAT 3 есть возможность использовать Unicode строки. Они могут пригодиться, если необходимо передовать специфические символы, но без необходимости лучше не использовать WSTRING.
STRING | WSTRING | |
Format | ASCII | Unicode |
Size of character | BYTE (1 byte) | WORD (2 bytes) |
Terminator | Null character | 0 |
Date and time
Почти в любом проекте необходимо знать точное время, вычислять временные интервалы. Часто работа с временем и датами доставляет много проблем и боли. Для себя я нашёл решение, уверен, оно упростит многим жизнь.
F_GetSystemTime() (Функция из модуля Tc2_System)
Эта функция может быть использована для считывания метки времени операционной системы. Временная метка представляет собой 64-разрядное целое значение с точностью до 100 нс. Помимо прочего, его можно использовать для синхронизации задач или измерения времени. Одна единица соответствует 100 нс. Время представляет собой количество интервалов в 100 нс с 1 января 1601 года.
Хранятся отметки в переменных типа ULINT. Зная всё это мы можем без труда рассчитывать интервалы времени с точностью до 100нс! Нужно просто найти разность между отметками.
К сожалению, стандартных функций для преобразования отметки в тип DATETYPE я не нашёл, поэтому пришлось реализовать такую функцию самостоятельно:
(*
:Description: Convert time since 1 January 1601 in 100 ns to DATE_AND_TIME (Преобразует время с 1 Января 1601 года в 100 нс в DATE_AND_TIME)
:Usability: Convert timestamp to datetime
:Note: check then nSystemType more then 01.01.1970 00:00:00
Version history:
Kozhemaykin E. A. | Creating | 16.08.2021;
*)
FUNCTION F_SystemTimeToDT : DT
VAR CONSTANT
SECONDS_BETWEEN_1601_AND_1970 : ULINT := 11_644_473_600;
END_VAR
VAR_INPUT
nSystemTime : ULINT; // One unit is 100 ns since 1 January 1601
END_VAR
VAR
nSeconds : ULINT;
END_VAR
nSeconds := (nSystemTime / 10_000_000) - SECONDS_BETWEEN_1601_AND_1970;
F_SystemTimeToDT := ULINT_TO_DT(nSeconds);
Как видно из кода, сложность заключалась в расчёте интервала между начальным отсчётом системного времени ПЛК и типа DATETIME.
Функция для получения текущей даты/времени в формате DATETIME
(*
:Description: Return datetime now in format DATE_AND_TIME (DT)
:Usability: For getting datetime now in format DATE_AND_TIME (DT)
Version history:
Kozhemaykin E. A. | Creating | 16.08.2021;
*)
FUNCTION F_DateTimeNow : DT
F_DateTimeNow := F_SystemTimeToDT(F_GetSystemTime());
Функция для получения прошедшего времени в формате TIME
(*
:Description: Time passed since tStart (Прошло времени c tStart)
:Usability: If need check how long time past
Version history:
Kozhemaykin E. A. | Creating | 16.08.2021;
*)
FUNCTION F_TimePassed : TIME
VAR_INPUT
tStart: ULINT; (* Время начала в 100нс от 01.01.1601,
текущее время в данном формате предоставляет функция F_GetSystemTime()*)
END_VAR
F_TimePassed := ULINT_TO_TIME((F_GetSystemTime() - tStart) / 10000);
Числовые константы
Большинство документаций по обмену по промышленным протоколам содержит шестнадцатиричные адреса регистров, номера функций, обозначения комманд и т.д. Для битовых операций необходимо представлять числа в двоичном виде. Чтобы эффективно решать задачи, где приходиться отходить от десятичной системы счисления необходимо знать о возможности задания константных чисел заданного типа в заданной системе счисления.
В общем виде задание числовой константы выглядит так:
{datetype}#{numeral system}#value
Пример: DINT#16#A1
Числовые значения могут быть двоичными числами, восьмеричными числами, десятичными числами или шестнадцатеричными числами. Если целое значение не является десятичным числом, его основание должно быть записано перед целочисленной константой, за которой следует символ хэша (#). Для шестнадцатеричных чисел цифры для чисел от 10 до 15, как обычно, представлены буквами A-F.
Типом этих числовых значений может быть BYTE, WORD, DWORD, SINT, USINT, INT, UINT, DINT, UDINT, REAL или LREAL.
ANY type
В языках программирования со статической типизацией довольно сложно делать универсальные функции/функциональные блоки. Когда мне поставили задачу собирать и анализировать различные данные, я решил, что копировать функциональные блоки и изменять в них только тип входного значения - не лучший вариант. Тогда появилась идея приводить все типы к одному и по объективным причинам это тип LREAL.
При реализации функции или метода вы можете объявлять входные данные (VAR_INPUT) как переменные с типом данных ANY. Далее вы можете получить указатель на значение, тип данных и размер переданной на этот вход переменной.
Структура типа данных ANY
TYPE AnyType :
STRUCT
// the type of the actual parameter
typeclass : __SYSTEM.TYPE_CLASS ;
// the pointer to the actual parameter
pvalue : POINTER TO BYTE;
// the size of the data, to which the pointer points
diSize : DINT;
END_STRUCT
END_TYPE
Кроме типа ANY существуют также дочерние типы:
Хочу обратить внимание что на вход типа ANY не может быть подана константа, поэтому в некоторых случаях придётся создавать дополнительную переменную.
Зная про этот тип мне удалось реализовать функцию, которая приводила данные разных типов к LREAL.
Функция по преобразованию числовых типов в LREAL
(*
:Description: Convert ANY_NUM and ANY_BIT to LREAL
:Usability: For development universal functions
:Note:
Valid types is:
ANY_NUM:
- ANY_REAL: REAL, LREAL
- ANY_INT: USINT, UINT, UDINT, ULINT, SINT, INT, DINT, LINT
ANY_BIT:
- BYTE, WORD, DWORD, LWORD
Version history:
Kozhemaykin E. A. | Creating | 01.06.2021;
Kozhemaykin E. A. | {CLASS_TO_LREAL -> TO_LREAL | 03.11.2021;
*)
FUNCTION F_AnyNumToLREAL : LREAL
VAR_INPUT
AnyNum: ANY; // Variable for converting, need have address
END_VAR
VAR
pReal : POINTER TO REAL; // pointer to a variable of the type REAL
pLReal : POINTER TO LREAL; // pointer to a variable of the type LREAL
pUSInt : POINTER TO USINT; // pointer to a variable of the type USInt
pUInt : POINTER TO UINT; // pointer to a variable of the type UInt
pUDInt : POINTER TO UDINT; // pointer to a variable of the type UDInt
pULInt : POINTER TO ULINT; // pointer to a variable of the type ULInt
pSInt : POINTER TO SINT; // pointer to a variable of the type SInt
pInt : POINTER TO INT; // pointer to a variable of the type Int
pDInt : POINTER TO DINT; // pointer to a variable of the type DInt
pLInt : POINTER TO LINT; // pointer to a variable of the type LInt
pByte : POINTER TO BYTE; // pointer to a variable of the type Byte
pWord : POINTER TO WORD; // pointer to a variable of the type Word
pDWord : POINTER TO DWORD; // pointer to a variable of the type DWord
pLWord : POINTER TO LWORD; // pointer to a variable of the type LWord
END_VAR
VAR_OUTPUT
OrginalType: __SYSTEM.TYPE_CLASS;
bInvalidType: BOOL := FALSE;
END_VAR
// Real numbers
IF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_REAL) THEN
pReal := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_REAL;
F_AnyNumToLREAL := TO_LREAL(pReal^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_LREAL) THEN
pLReal := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_LREAL;
F_AnyNumToLREAL := pLReal^;
// Bit's numbers
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_BYTE) THEN
pByte := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_BYTE;
F_AnyNumToLREAL := TO_LREAL(pByte^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_WORD) THEN
pWord := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_WORD;
F_AnyNumToLREAL := TO_LREAL(pWord^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_DWORD) THEN
pDWord := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_DWORD;
F_AnyNumToLREAL := TO_LREAL(pDWord^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_LWORD) THEN
pLWord := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_LWORD;
F_AnyNumToLREAL := TO_LREAL(pLWord^);
// Unsigned integers
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_USINT) THEN
pUSInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_USINT;
F_AnyNumToLREAL := TO_LREAL(pUSInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_UINT) THEN
pUInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_UINT;
F_AnyNumToLREAL := TO_LREAL(pUInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_UDINT) THEN
pUDInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_UDINT;
F_AnyNumToLREAL := TO_LREAL(pUDInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_ULINT) THEN
pULInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_ULINT;
F_AnyNumToLREAL := TO_LREAL(pULInt^);
// Signed integers
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_SINT) THEN
pSInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_SINT;
F_AnyNumToLREAL := TO_LREAL(pSInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_INT) THEN
pInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_INT;
F_AnyNumToLREAL := TO_LREAL(pInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_DINT) THEN
pDInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_DINT;
F_AnyNumToLREAL := TO_LREAL(pDInt^);
ELSIF (AnyNum.TypeClass = __SYSTEM.TYPE_CLASS.TYPE_LINT) THEN
pLInt := AnyNum.pValue;
OrginalType := __SYSTEM.TYPE_CLASS.TYPE_LINT;
F_AnyNumToLREAL := TO_LREAL(pLInt^);
//Invalid type
ELSE
F_AnyNumToLREAL := 0;
bInvalidType := TRUE;
END_IF
REFERENCE
Все знают про указатели (POINTER) и связанные с ними проблемы, так вот многие из них можно избежать, если использовать ссылки(REFERENCE):
Ссылки проще в использовании: ссылку не нужно разыменовывать (с помощью ^), чтобы получить доступ к содержимому объекта, на который ссылается ссылка.
Более чистый синтаксис для передачи значений: Если вход является ссылкой, то нет необходимости писать ADDR(value).
В отличие от указателей, для ссылок компилятор проверяет типы данных при передаче значений.
Стоит отметить, что не всегда ссылкой можно заменить указатель, но когда это возможно, то сделайте это.
Pragmas
Инструкции pragma влияют на свойства переменных, относящихся к процессу компиляции или предкомпиляции. Не поленитесь просмотреть возможности каждого типа pragmas - обязательно найдёте что-то полезное для своего проекта.
Типы pragmas:
Message pragmas
Attribute pragmas
Conditional pragmas
Region pragma
Pragmas for warning suppression
Union
Union - тип структуры, который позволяет представлять значение в разных типах данных. Данная структура полезна при отладке кода а также при обработке входных значений.
В случае, если нужно обращаться к битам, то это можно сделать через точку. Но у этого способа я вижу огромный недостаток: нет возможности итерироваться по битам. Если нужно разобрать переменную на байты или по 16-бит или другим сложным образом, то вместо написания сложных функций попробуйте сначала сделать это с помощью Union.
SEL, MIN, MAX, LIMIT
Многим программистам ПЛК часто не хватает синтаксического сахара, которого много в других языках программирования. На примере функции SEL хочется показать, что возможно этот "сахар" в виде тернарного оператора не особо нужен.
Если вам нужно выбрать значение в зависимости от условия, выможете сделать это в одну строку:
value := SEL(condition, if false, if true);
Если вам нужно ограничить значение сверху и/или снизу, это также можно сделать в одну строку:
value := MIN(value, max_limit);
value := MAX(value, min_limit);
or
value := LIMIT(min_limit, value, max_limit);
Многие функции и операторы, которых нам не хватает уже написаны - нужно только поискать.
Заключение
В статье описано,то на что лично мне захотелось обратить внимание (ООП решил не трогать). Буду рад если мой опыт принесёт кому-то пользу. Попрошу при использовании предоставленных функций оставлять продолжать version history.
Делитесь своим опытом в комментариях. Чтобы быть в курсе событий и общаться с коллегами предлагаю перейти по ссылкам: тг-канал proPLC, тг-чат proPLC.
Надеюсь не забанят
Нет войне