Давно хотел попинать полумертвое существо
UNION, которому вообще сложно найти красивое и практичное применение, но тут в тему появилась статья Стефана Хеннекена о возможности
расширять UNION через наследование в TwinCAT 3.
Итак, тип
UNION — это как бы структура, но не она. Все ее элементы занимают одно адресное пространство, поэтому записывая данные в один элемент, мы перезаписываем целиком или частично данные остальных элементов. В структурах эти элементы называются полями, здесь же сложно использовать термин "поле" потому что адресное поле по сути — одно единственное, а переменных несколько и они могут быть разного типа и разного размера — что, впрочем, тоже является одной из ключевых фишек этого типа данных.
Можно пытаться убедить не применять
UNION, что он устарел и больше не нужен, но мы по прежнему одной ногой в эмбедед, где все-таки приходится иногда экономить память (первое применение), использовать трюки с преобразованием типов данных (второе применение) и где только-только появилось ООП (привет CoDeSys 3 и TwinCAT 3).
Расчленение данных
TYPE ST_Word:
STRUCT
Lo : BYTE; (* младший байт *)
Hi : BYTE; (* старший байт *)
END_STRUCT
END_TYPE
TYPE U_Word:
UNION
Bytes : ST_Word;
Value : WORD;
END_UNION
END_TYPE
Объявив переменную как
UNION, мы получили структуру, внутри которой два элемента (Bytes и Value) хранятся в одном адресном пространстве. Записывая, что-либо в переменную
Value мы также записываем эти данные и в
Bytes. Верно и обратное — записывая данные в поля структуры
Bytes : ST_WORD мы одновременно заполняем данными переменную
Value.
Это очень похоже на синонимы (
ALIAS), но с разными типами данных, и на самом деле никакой одновременности записи в разные переменные здесь нет — просто данные лежат в одном месте и разные
пересекающиеся части этого места называются по разному. К сожалению, построчная запись в объявлении
UNION часто сбивает с толку разработчиков, и они забывают, что данные переменных хранятся в ячейках памяти, и контроллер работает с ячейками памяти, а не с какими-то там именами человеческих переменных.
PROGRAM MAIN
VAR
yourWord : U_Word;
hi : BYTE;
lo : BYTE;
END_VAR
yourWord.Value := 16#AABB;
hi := yourWord.Bytes.Hi; // == 16#AA
lo := yourWord.Bytes.Lo; // == 16#BB
Записываем в поле
Value 16-разрядное число, которое накладыватся на 16-разрядную структуру
Bytes : ST_WORD, содержащую два байтовых поля. Таким образом записанное значение разбивается на две однобайтных части: старший байт и младший байт. И никаких битовых операций для доступа к байтам
WORD.
В TwinCAT 3 появился механизм наследования, который мы можем применить к
UNION:
TYPE U_Word EXTENDS ST_Word:
UNION
Value : WORD;
END_UNION
END_TYPE
Код становится лаконичнее, так как отпала необходимость в указании промежуточной структуры Bytes:
PROGRAM MAIN
yourWord.Value := 16#AABB;
hi := yourWord.Hi; // == 16#AA
lo := yourWord.Lo; // == 16#BB
Одно поле
Картинка лучше тысячи слов. Возьмем скарпель указателей и пройдемся по плитке памяти:
Адреса элементов Value, Lo и адрес в pYourWord (это указатель на
UNION) совпадают, так как они расположены в начале области памяти выделенной под
UNION. Указатель конечно же не расположен, а ссылается, но нас интересуют адреса, а не механизм работы. Теперь сравните адреса ячеек Lo и Hi (
pYourWord_Lo и
pYourWord_Hi, соответственно). Они различаются ровно на один байт, так как каждая из них занимает ровно один байт, а расположены они последовательно друг за другом.
Если этого недостаточно, то вот вам код:
PROGRAM MAIN
VAR
oneWord : WORD;
yourWord : U_WORD;
pYourWord : ULINT;
pYourWord_Value : PVOID;
pYourWord_Lo : LWORD;
pYourWord_Hi : LWORD;
wordLo : BYTE;
wordHi : BYTE;
END_VAR
pYourWord := ADR(yourWord);
pYourWord_Value := ADR(yourWord.Value);
pYourWord_Lo := ADR(yourWord.Lo);
pYourWord_Hi := ADR(yourWord.Hi);
oneWord := yourWord.Value;
MEMCPY(
ADR(wordLo), (* <-- *) pYourWord_Lo,
SIZEOF(wordLo));
MEMCPY(
ADR(wordHi), (* <-- *) pYourWord_Hi,
SIZEOF(wordHi));
Вместо типов
LWORD и
ULINT здесь лучше использовать
POINTER TO U_WORD и
POINTER TO BYTE, но мне хотелось убедиться, что указатели теперь 64-х разрядные и что для хранения адресов подходят 64-х разрядные типы:
LWORD,
ULINT, PVOID. Последний, кстати, как и
обещали в TwinCAT 3 стал 64-х разрядным.
PVOID — это синоним для UDINT в TwinCAT 2 (32-х разрядные адреса) или для ULINT в TwinCAT 3 (64-х разрядные адреса). Всегда используйте его как универсальное хранилище адреса, так как независимо от версии TwinCAT, вы всегда получите переменную с правильным типом данных. Вот еще один пример правильного использования ALIAS.
Заодно посмотрите на смертельно опасную функцию
MEMCPY, позволяющую писать куда угодно в память.
Другие применения UNION
Экономим память — храним данные разных типов в одном месте. Главное, точно знать какие данные сохранены в текущий момент времени и вовремя их перезагружать.
Реинтерпретация типа данных
Мы хотим считать, что вот тот тип данных сейчас не
REAL, а
DWORD. Например, чтобы без лишних преобразований пересылать его через Modbus:
TYPE U_Real :
UNION
Re : REAL;
Dw : DWORD;
END_UNION
END_TYPE
Для работы используем поле
Re, а для отправки данных
Dw. И никакого преобразования данных, все происходит автоматически.
Variant
Или когда мы не знаем какой тип данных будет использован во время работы программы. Этот же прием можно использовать для создания псевдо-полиморфизма в TwinCAT 2.
Сначала готовим болванку для хранения данных разного типа:
TYPE U_VarObject :
UNION
AsInteger : INT;
AsFloat : REAL;
AsDouble : LREAL;
LikeString : STRING;
END_UNION
END_TYPE
Затем описываем (перечисляем) доступные типы данных:
TYPE E_VarType :
(
Integer,
Float,
Double,
String255
);
END_TYPE
Упаковываем все это в структуру:
TYPE ST_Variant :
STRUCT
TypeIs : E_VarType;
Value : U_VarObject;
END_STRUCT
END_TYPE
Работает как-то так:
PROGRAM MAIN
VAR
xVar : ST_Variant;
Counter : INT;
Text : STRING;
END_VAR
CASE xVar.TypeIs OF
E_VarType.Integer:
Counter := xVar.Value.AsInteger;
E_VarType.Float:
;
E_VarType.Double:
;
E_VarType.String255:
Text := xVar.Value.LikeString;
END_CASE
Синонимы
Очень похоже на вычленение данных, но будем использовать по принципу
ALIAS:
TYPE ST_Vector3R:
STRUCT
X : REAL;
Y : REAL;
Z : REAL;
END_STRUCT
END_TYPE
TYPE U_Vector3R EXTENDS ST_Vector3R:
UNION
E : ARRAY [1..3] OF REAL;
END_UNION
END_TYPE
Теперь координаты вектора доступны для нас как по именам, так и по индексам:
PROGRAM MAIN
VAR
Vec : U_Vector3R;
i : UINT;
END_VAR
FOR i := 1 TO 3 DO
Vec[i] := i * 100;
END_FOR
Vec.Y := 0.0;