Showing posts with label type punning. Show all posts
Showing posts with label type punning. Show all posts

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;