September 27, 2017

Расширение UNION

Давно хотел попинать полумертвое существо 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;