Showing posts with label MEMCPY. Show all posts
Showing posts with label MEMCPY. 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;

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();