June 17, 2015

Машина состояний EtherCAT

Машина состояний EhterCAT (EtherCAT State machine (ESM) -- каждое устройство EtherCAT может и обязано находиться в одном из предопределенных состояний. В каждом из состояний мастером шины производятся определенные действия. Даже в случае ошибки или аварии устройство переходит в заранее предусмотренное состояние и выставляет флаг ошибки. Если устройство не поддерживает аппаратный μС-контроллер, то оно должно эмулировать машину состояний EtherCAT.

  • Init (I) - нет синхронного обмена PDO; мастер может писать в информационные регистры подчиненных.
  • Pre-Operational (Pre-Op, PREOP, P) -- нет синхронного обмена PDO; мастер конфигурирует подчиненных; обмен данными через асинхронные почтовые слоты.
  • Safe Operational (Safe-Op, SAFEOP, S) - обмен данными через асинхронные почтовые слоты; синхронный обмен PDO -- только входа; выхода в безопасном состоянии ("Safe State").
  • Operational (OP, O) -- полностью рабочий режим; синхронный обмен данными через PDO: как входа, так и выхода.
  • Bootstrap (B) -- опциональный режим для "заливки" новых прошивок.

Часто используются сокращения или если хотят показать направление переключения:
  • IP: Init → Pre-Op
  • PI: Pre-Op → Init
  • OI: Op → Init
  • SO: SAFEOP → OP

Для инициализации подчиненного устройства, достаточно выполнить проход по состояниям: Init → Pre-Op → Safe-Op → Op (что сокращенно будет выглядеть как "I-P-S-O"). Здесь состояние Safe-Op не обязательно, но в стандарте фигурирует.

Для переконфигурации устройства необходимо вернуть устройство в состояние Init, а затем выполнить всю цепочку сначала: I-P-S-O. В результате прохода по этой последовательности, мастер заново переконфигурирует подчиненное устройство и гарантированно сбросит все ошибки устройства (если причина ошибки была устранена).

Смена состояния подчиненного устройства (или когда мастер управляет сам собой) происходит не сразу, а посредством управляющего регистра, т. е. сначала мастер запрашивает требуемое состояние, и только затем подчиненный какое-то время пытается изменить свое состояние, по результату изменяя значение регистра состояния. Если подчиненный не смог переключиться в требуемое состояние, то в итоге получится разное значение для запрошенного состояния подчиненного (Requested State) и текущего состояния подчиненного (Current State). Этот процесс хорошо отслеживается на закладке "Online" в System Manager.

Множество тонких настроек поведения EtherCAT-устройства доступны во вкладке "EtherCAT", диалог "Advanced Settings...".


Библиотека TcEtherCAT.lib


Библиотека входит в стандартный комплект TwinCAT и содержит множество низкоуровневых и чуть-выше функций для работы по шине EtherCAT:
  • CoE Interface (CAN over EtherCAT) -- работа с параметрами подчиненных устройств у которых внутренняя шина -- CAN: многие модули расширения, дискретные и аналоговый входа/выхода и пр.
  • SoE Interface (SERCOS over EtherCAT) -- функции работы с параметрами SERCOS-устройств; например, сервоусилители серии AX5000.
  • FoE Interface (File over EtherCAT) -- функции заливки прошивки в устройства.
  • Множество других функций: диагностика рабочих счетчиков (WcState), подсчет контрольных сумм, функции конвертеры для распределенных часов и т. п.

Для переключения состояния подчиненного можно воспользоваться функциями FB_Ec[Get|Req|Set]SlaveState. Для мастера -- аналогично, только Slave нужно заменить на Master.

Функция Get вернет текущее состояние в виде структуры ST_EcSlaveState, которая состоит из двух битовых полей:
  • deviceState - состояние подчиненного устройства.
  • linkState - состояние линии связи.

В библиотеке доступны следующие константы состояний: EC_DEVICE_STATE_INIT, EC_DEVICE_STATE_PREOP, EC_DEVICE_STATE_xxx, ...

Перед определением текущего состояния необходимо обработать поле deviceState битовой маской EC_DEVICE_STATE_MASK. Недостаточно просто вытащить из структуры поле и сравнить его с константой состояния! Например, выясняем, что в настоящий момент устройство не способно полноценно работать:

IF ( curState AND EC_DEVICE_STATE_MASK ) <> EC_DEVICE_STATE_OP THEN


Кроме текущего состояния, поле deviceState содержит биты-флаги ошибок. Для них специальной маски нет, поэтому проверить устройство на ошибки можно следующим образом:

IF ( currState.deviceState AND 16#70 ) > 0 THEN


Подробнее о состоянии устройства можно прочитать в описании структуры ST_EcSlaveState.


Изменение состояния подчиненного устройства

Для изменения состояния устройства, в библиотеке существуют два набора функций: FB_EcReqSlaveState и FB_EcSetSlaveState. Разница в том, что Req отправляет запрос и заканчивает свою работу. Set же будет ожидать, пока устройство не переключится в необходимое состояние и, если этого не произойдет в течение времени tTimeout, выбросит ошибку: 16#745; (*ADSERR_DEVICE_CLIENT_TIMEOUT*)
Если в проекте не требуются нюансы работы с EtherCAT-шиной, то лучше воспользоваться функциями Set.
Не нужно изобретать свою функцию, когда уже есть готовая Set: она использует Req как основу, организуя внутри себя опрос состояния подчиненного устройства.

June 14, 2015

OPC-сервер

OPC-сервер покупается на каждый контроллер отдельно, затем устанавливается. Существует два вида:
  • OPC DA/XML-DA -- только для настольных операционных систем: XP, 7, Embedded Standart, и т. п. [TS6120]. Для его работы необходим DCOM и пр. системные сервисы, и это единственная причина, почему его нет для компактных операционок.
  • OPC UA (IEC 62541) -- не только для настольных [TS6100], но и для компактных: CE, Emb. Compact, ... [TS6100-0030]
Если тип OPC-сервера не важен, то стоит выбрать OPC UA.
Обе версии настраиваются через XML-файл. Существует конфигуратор, который умеет создавать этот XML-файл и активировать его на ПЛК. Немного про настройку можно прочитать в "OPC сервер: настройка и тестирование".

June 9, 2015

Форматы времени и даты

Посмотрим на полный список форматов времени и даты. Немного об этом было в ST и C#, или туда и обратно, теперь рассмотрим подробнее.
Все переменные типа даты, времени, промежутков, длительности и т. п. хранятся в ПЛК как 32-х разрядные целые, беззнаковые.

TIME


Длительность или промежуток времени в миллисекундах. Аналогом в C# можно считать TimeSpan.

Формат:
:= T#521h33m23s231ms;
:= TIME#521h33m23s231ms;

Время в строку: TIME_TO_STRING(T#12ms);
Результат типа STRING: 'T#12ms'

Строка во время: STRING_TO_TIME('T#127ms');
Результат типа TIME: T#127ms


DATE


Только дата  — как число секунд, прошедших с 1 января 1970 года.

Формат:
:= D#1993-06-12;
:= DATE#1993-06-12;

Дата в строку: DATE_TO_STRING(D#2002-08-18);
Результат типа STRING: 'D#2002-08-18'


DATE_AND_TIME или DT


Дата и время — как число секунд, прошедших с 1 января 1970 года. В C# можно использовать DateTime, не забывая, что в ST дата-время начинаются в Unix-стиле с 1970 года, а в C# с нулевого года.

Формат:
:= DT#1993-06-12-15:36:55;
:= DATE_AND_TIME#1993-06-12-15:36:55;

Дата-время в строку: DT_TO_STRING(DT#1998-02-13-14:20);
Результат типа STRING: 'DT#1998-02-13-14:20'

Несложно избавиться в строке от префиксов T#DT#, TOD# и т. п. Функция DELETE входит в стандартную библиотеку STANDART.lib. Удаляем слева n-символов:

str := DELETE(str, 2, 1); (*   T#... TIME_TO_STRING *)
str := DELETE(str, 3, 1); (*  DT#... DT_TO_STRING   *)
str := DELETE(str, 4, 1); (* TOD#... TOD_TO_STRING  *)


TIME_OF_DAY или TOD


Время дня: количество миллисекунд, прошедших начиная от начала дня — от ноля часов и ноля минут (00:00).

Формат:
:= TOD#12:34:56.123;
:= TIME_OF_DAY#12:34:56.123;

Время дня в строку: TOD_TO_STRING(TOD#14:01:05.123);
Результат типа STRING: 'TOD#14:01:05.123'



FILETIME (T_FILETIME)


Структура из двух DWORD (udint) полей. 64-х разрядное целое, содержит число 100-наносекундных интервалов с 1 января 1601 года (UTC). Для вменяемой работы с этим форматом (распаковки), существуют вспомогательные функции FILETIME_TO_DTFILETIME_TO_SYSTEMTIME, и в обратную сторону [...]_TO_FILETIME.


SYSTEMTIME (TIMESTRUCT)


Удобная структура с большим количеством удобных 16-разрядных полей типа WORD (uint):
  • wYear — год, 1970..2106;
  • wMonth — месяц, 1..12 (1 - январь, 2 - февраль и т. д); 
  • wDayOfWeek — день недели, 0..6 (0 - воскресенье (!), 1 - понедельник, ...);
  • wDay — день месяца, 1..31;
  • wHour — час, 0..23;
  • wMinute — минуты, 0..59;
  • wSecond — секунды, 0..59;
  • wMilliseconds — миллисекунды, 0..999.

Существует функция для преобразования строки в системное время: STRING_TO_SYSTEMTIME( 'YYYY-MM-DD-hh:mm:ss.xxx' ). На выходе получаем TIMESTRUCT.

Формат входной строки жестко фиксирован: через разделители '-' и ':' должны быть введены все цифры, а недостающие — заменены нулями:
  • YYYY — год,1601..9999;
  • MM — месяц, 01..12;
  • DD — день, 01..31;
  • hh — час, 00..23;
  • mm — минуты, 00..59;
  • ss — секунды, 00..59;
  • xxx — миллисекунды, 000..999.

Например:

ts := STRING_TO_SYSTEMTIME( '2012-01-30-05:12:09.567' );


Обновлено: 5 февраля 2017 г.

June 8, 2015

Энергонезависимые переменные

Рано или поздно контроллер выключается. Иногда - внезапно, тогда содержимое оперативной памяти теряется. Для таких случаев существует несколько видов специальных переменных, способных сохранять (и восстанавливать) данные в моменты без электричества.
Несколько видов переменных существует из-за ограниченности флэш-памяти на количество записей. Жесткие диски, внешние базы данных, сетевые переменные и пр. - хорошая альтернатива, но не всегда доступны.

RETAIN
По сути применимы только для серии BC. Можно и в других случаях, но неудобно, т. к. сохраняют свое значение только при корректном завершении работы TwinCAT. Не спасут при внезапном пропадании электричества (*). Совершенно не экономят ресурс флэш-памяти. На контроллерах BC сохраняют свое значение при изменении значения переменной и могут удлинить время цикла (если нужно).

PERSISTENT
Аналогично RETAIN, но сохраняются при вызове специального функционального блока (FB_WritePersistentData из TcUtilities.lib), либо при корректном выключении TwinCAT (*). Экономят ресурс флэш-памяти.
При использовании RETAIN и PERSISTENT данных, значения переменных по прежнему сбрасываются на флэш-диск. TwinCAT самостоятельно восстанавливает данные и может отследить нарушение целостности сохраненных данных (об этом можно узнать из специальной системной переменной: SYSTEMINFO). Остальные подробности - в справочной системе: TX1200 | TwinCAT PLC.

NOV/DP-RAM


Nonvolatile DualPort RAM - энергонезависимая двухпортовая  память.

Память двухпортовая, т. е. состоит из двух компонентов: оперативная память и долговременная память, плюс - собственный источник питания. Все это, в совокупности, позволяет сохранять переменные в любой момент, сколько угодно раз, а при отключении питания, успеть сбросить и хранить данные в долговременной памяти за счет собственного источника питания. Автоматически.

Но(!) не все контроллеры поддерживают этот вид памяти.

Настраивается и конфигурируется в системной конфигурации линковкой выходных переменных ПЛК-проекта с выходными переменными NovRAM. Дальше работает автоматически, поэтому на младших контроллерах постоянная работа с переменными этой памяти может вызвать ощутимую нагрузку на процессор ПЛК. Для снижения нагрузки существует специальный функциональный блок, для записи блоков данных только при необходимости (FB_NovRamReadWriteEX).


Односекундные бесперебойники


Теперь вернемся к звездочке в скобках (*): ограничение на аккуратное завершение работы TwinCAT снимается "односекундным бесперебойником" (S-UPS: capacitive seconds UPS). За счет энергии, накопленной в ионисторах, эта опция позволяют подпитывать контроллер некоторое время, достаточное для сброса данных во флэш-память. Для работы этой функции необходимо обрабатывать функцию FB_S_UPS из библиотеки TcSUPS.Lib.

Но(!), опять-таки, не все контроллеры и не для всех.
Для всех видов переменных, при использовании файловых фильтров EWF или FBWF, необходимо разрешить на запись каталог Boot. Это происходит автоматически при включении фильтров, но проверить, не помешает.

June 1, 2015

Free Run не нужен

Со временем выработалось правило: "Free Run не нужен", но пытливые умы все-равно пытаются приспособить что-то незнакомое, под что-нибудь полезное.

Устройства при включенной опции Free Run пытаются оставаться в состоянии OP даже в режиме конфигурации, и часто им это удается, но задачи (Tasks) системы реального времени конечно же не работают. Поэтому единственное применение этой опции - подергать за выхода в режиме конфигурации при еще не написанной ПЛК-программе.
Кстати, иногда модули расширения реагируют на такое безобразие "как-то не так", что заставляет разработчика искать несуществующую ошибку.
Кроме того, существует режим синхронизации "free run", но это уже другая история, не имеющая никакого отношения к этому окошку.


Как подергать за входа без Free Run?


Если "Free Run не нужен", но хочется подергать за аппаратные выхода, можно воспользоваться веткой "Additional Tasks". Она позволяет создавать задачи, которые умеют перегонять данные, и только, что собственно и нужно, чтобы в рабочем режиме (Run Mode) обеспечить активность шины, избегая такого сообщения:



Создаем дополнительную задачу, а в ней какую-нибудь переменную, которую затем линкуем с переменной состояния EtherCAT-шины:


При создании дополнительной задачи, не забываем сразу же выставить опцию "Auto start", т. к. в отличии от ПЛК-задач, дополнительные задачи автоматически не стартуют.
После этого можно активировать конфигурацию и наслаждаться полноценным рабочим режимом (Run Mode) с активной шиной и устройствами.

May 25, 2015

ActiveX в HMI

В HMI визуализацию можно добавить компонент ActiveX. Если вы не умеете работать с ActiveX компонентами, не знаете C/C++ и не умеете создавать DLL-файлы, можете забыть про эту возможность и просто написать свое собственное приложение, которое будет "выгребать" данные через ADS.API. В общем, именно так и нужно поступить, так как ActiveX морально устарел.

Нюансы вставки ActiveX-компонента:
  • Только пассивный элемент, только отображение.
  • Только настольные операционные системы: Windows XP, 7 и старше.
  • Не работает под Windows CE и другие Сompact версии.
  • Основная цель - вставка компонента отображения HTML-страниц (Microsoft WebBrowser), но можно и другие.



На вкладке Methodcalls сосредоточены настройки вызова различных методов интерфейса ActiveX-компонента:



Webbrowser


Вкладка для компонентов, поддерживающих интерфейс IWebBrowser:
  • Variable for URL - переменная, содержащая строку адреса веб-сайта (URL).
  • Condition for call - старт загрузки html-страницы по переднему фронту TRUE, заданной переменной. Если переменная не задана - компонент будет обновляться каждый цикл ПЛК! Лучше - задать.


Additional Call


Позволяет настроить вызов других методов ActiveX-компонента:
  • Dll for Call - полный путь к DLL-файлу, который в свою очередь будет вызывать какие-то методы компонента.
  • Methodidentification - строка, содержащая название метода, который будет вызываться внутри DLL. Это не само название метода, а подсказка для внутренностей DLL-библиотеки - что необходимо делать (см. пример ниже).
  • Parameter - дополнительные параметры для передачи в DLL-библиотеку.
  • Condition for call - старт вызова по переднему фронту заданной переменной.

DLL-библиотека должна экспортировать функцию ExecuteActiveXCall:

void ExecuteActiveXCall(IUnknown* pUnk, char* pszId, char* pszParam,
char* pszReturnBuffer, int nReturnBufferSize, DWORD* pdwReturnFlag);

  • pszId - строка из переменной Methodidentification.
  • pszParam - строка из переменной Parameter.
  • pUnk - ссылка на интерфейс для работы с методами ActiveX-компонента.
  • pszReturnBuffer, nReturnBufferSize и pdwReturnFlag - не используются.


Пример DLL-библиотеки


Данный пример показывает, как может выглядеть простейшая DLL-библиотека, позволяющая вызывать другие методы ActiveX-компонента. Здесь GoBack и GoForward позволяют переходить на страницу вперед или на страницу назад, при условии, что компонент поддерживает интерфейс IWebBrowser.

Названия IWebBrowser|GoBack и IWebBrowser|GoForward никак не связаны с названиями методов компонента. Разработчик может использовать произвольные, удобные для него названия:

#include "stdafx.h" 
#include <unknwn.h> 
#include <exdisp.h> 
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) 
{
    return TRUE; 
}

extern "C" __declspec (dllexport) void ExecuteActiveXCall(IUnknown* pUnk, char* pszId, char* pszParam, char* pszReturnBuffer, int nReturnBufferSize, DWORD* pdwReturnFlag) 
{ 
    if (strcmp(pszId, "IWebBrowser|GoBack") == 0)
    { 
        IUnknown* pNewUnk; 
        IWebBrowser* pwb; 
        pUnk->QueryInterface(IID_IWebBrowser, (void**) &pNewUnk);
        pwb = (IWebBrowser*) pNewUnk;
        if (pwb) 
        { 
            pwb->GoBack(); 
            pwb->Release(); 
        }
    }
    else
    {
        if (strcmp(pszId, "IWebBrowser|GoForward") == 0)
        { 
            IUnknown* pNewUnk; 
            IWebBrowser* pwb; 
            pUnk->QueryInterface(IID_IWebBrowser, (void**) &pNewUnk);
            pwb = (IWebBrowser*) pNewUnk;
            if (pwb) 
            { 
                pwb->GoForward(); 
                pwb->Release(); 
            } 
        } 
    }
}


Функция добавления ActiveX-компонента - это стандартная функция CoDeSys PLC Control; она может использоваться и в контроллерах других производителей.

May 8, 2015

Обмен данными через ADS.API, ST и C#

При работе с ADS.API и.NET необходимо учитывать, что у данных есть длина: причем ПЛК-задачи используют 16-разрядное слово, а C# 32-х. В справочной системе есть краткая таблица соответствия типов. Сделаем ее длинношеее.

STC#Знак.Длина
BYTE, USINTbyte+8 бит
INTshort±16 бит
WORD, UINTushort+16 бит
DINTint±32 бита
DWORD, UDINTuint+32 бита
REALfloat±32 бита
LREALdouble±64 бита

Ничего сложного, для базовых типов нужно учитывать разницу в длине слов. Специфические типы данных DT и TIME требуют особого подхода. STRING и ARRAY - особого подхода с элементами эквилибристики. Работа со структурами STRUCT - тема отдельного разговора.


Дата и время


TIME в языке ST - это промежуток времени (длина), DT - дата и время события (момент). Соответственно в C# для TIME подходит TimeSpan, для DT - DateTime.

TIME хранится в памяти ПЛК как 32-х разрядное, целое, беззнаковое число, содержащее количество миллисекунд:

int htime = client.CreateVariableHandle("MAIN.t");
UInt32 ttime = 0;
ttime = (UInt32)client.ReadAny(htime, ttime.GetType());
TimeSpan span = TimeSpan.FromMilliseconds(ttime);

Я специально использовал тип данных UInt32, чтобы был виден размер данных. По желанию его можно заменить на uint.

С DT немного сложнее, но не сильно - это опять-таки 32-х разрядное, целое, беззнаковое, содержащее количество секунд, прошедшее с 1 января 1970 года:

int hdate = client.CreateVariableHandle("MAIN.td");
int tdate = 0;
tdate = (int)client.ReadAny(hdate, tdate.GetType());
DateTime dt = new DateTime(1970, 1, 1).AddSeconds(tdate);
Console.WriteLine(string.Concat("DT td = ", dt.ToLongDateString(), " / ", dt.ToLongTimeString()));

Я немного расширил пример выводом даты и времени в консоль.


Массивы данных


Сразу отмечу, что строки - это одномерные массивы символов. Нет никакой разницы между работой со строками и массивами.
В старых версиях TwinCAT необходимо было учитывать, что в конце строки стоит символ '\0' и соответственно увеличивать на единицу размер массива под строку. Начиная со второй версии, про нулевой символ и увеличение размера можно забыть: просто передавайте длину строки в символах.
Для чтения и записи массивов используется дополнительный параметр, передающий количество элементов массива. Для одномерного массива - число элементов, для двумерного - число строк, затем число столбцов.

VAR
    str   : STRING      := 'abcdef';
    strT  : T_MaxString := '01234abcdef';
    arr   : ARRAY [1..5] OF WORD;
    arr2D : ARRAY [1..3, 1..5] OF WORD;
END_VAR

string    str   = "";
string    strT  = "";
ushort[]  arr   = new ushort[5];
ushort[,] arr2d = new ushort[3, 5];

str   = (string)   (client.ReadAny(h_str,   str.GetType(),   new int[] { 80   }));
strT  = (string)   (client.ReadAny(h_strT,  strT.GetType(),  new int[] { 255  }));
arr   = (ushort[]) (client.ReadAny(h_arr,   arr.GetType(),   new int[] { 5    }));
arr2d = (ushort[,])(client.ReadAny(h_arr2D, arr2d.GetType(), new int[] { 3, 5 }));


При записи строк - длину указывать необходимо, т. к. можно передавать только часть строки, а вот при записи массивов наоборот - длину передавать не только не нужно, но и нельзя:

client.WriteAny(h_str,   str,   new int[] { str.Length });
client.WriteAny(h_arr,   arr);
client.WriteAny(h_arr2D, arr2d);


Все переменные сразу


Существует возможность прочитать список всех переменных ПЛК-программы: получить исчерпывающую информацию об окружении переменной, ее типе, группе, смещении в памяти, родительской структуре и пр. В терминологии TwinCAT - это называется "Symbol Information". Даже комментарий, оставленный в строке с объявленной переменной, будет доступен через информацию окружения.

// Загружаем информацию о всех доступных переменных
TcAdsSymbolInfoLoader symbolLoader = client.CreateSymbolInfoLoader();
TcAdsSymbolInfoCollection infoCollection = symbolLoader.GetSymbols(true);

foreach (TcAdsSymbolInfo symInfo in infoCollection)
{
    string  name        = symInfo.Name;
    string  shortName   = symInfo.ShortName;
    string  type        = symInfo.Type;
    int     size        = symInfo.Size;
    string  comment     = symInfo.Comment;
    long    iGroup      = symInfo.IndexGroup;
    long    iOffset     = symInfo.IndexOffset;
    
    TcAdsSymbolInfo parentInfo = symInfo.Parent;

    Console.WriteLine(string.Concat(
        name,                   Environment.NewLine,
        shortName,              Environment.NewLine,
        type,                   Environment.NewLine,
        size,                   Environment.NewLine,
        comment,                Environment.NewLine,
        iGroup, ":", iOffset,   Environment.NewLine
        ));
}

Name и ShortName ничем, к сожалению, не отличаются, хотя должны. Поля Parent и xxxSubSymbol (в примере выше его нет) организуют переменные в иерархию.

Подробнее про назначение полей можно прочитать в справочной системе "TcAdsSymbolInfo Members".

May 7, 2015

Как готовить синхронные функции ADS.API

Сборник рецептов как корректно обращаться с базовыми синхронными функциями ADS.API.


Инициализация


PROGRAM MAIN
VAR
    PLCVar : INT;
END_VAR
...
private int hVar;
private TcAdsClient adsClient;
private short plcVar;
...

TcAdsClient adsClient = new TcAdsClient(); 


Подключаемся к ПЛК-рантайму №1.
Для TwinCAT2 порт = 801, для TwinCAT3 порт = 851.

adsClient.Connect("172.16.3.217.1.1", 801); // TwinCAT 2
adsClient.Connect("172.16.3.217.1.1", 851); // TwinCAT 3


Подключение


Как таковой связи еще нет, мы всего-лишь подключились к локальному роутеру. Поэтому, перед созданием дескрипторов переменных, необходимо как-то поймать момент, когда переменные ПЛК уже доступны. Произойдет это после запуска ПЛК-задачи контроллера:

StateInfo info = new StateInfo(AdsState.Invalid, 0);
while (info.AdsState != AdsState.Run)
{
    try
    {
        info = adsClient.ReadState();
    }
    catch { }

    Thread.Sleep(100); // ждем 100мс перед следующей проверкой
}
// OnConnected(); // отправляем в родительский поток сообщение о подключении

Такой кусок кода нужно запустить в отдельном потоке и дождаться сообщения о благополучном запуске задачи. В C#.NET 4.5 удобно использовать конструкции async/await.


Синхронное чтение


Создаем дескриптор для переменной из контроллера

try
{
    hVar = adsClient.CreateVariableHandle("MAIN.PLCVar");
}
catch(Exception ex)
{
    MessageBox.Show(ex.Message);
}


Немного исправим способ из справочной системы.

plcVar = (short) adsClient.ReadAny(hVar, typeof(short));          // Было
plcVar = (short) adsClient.ReadAny(hVar, plcVar.GetType());       // Стало *
plcVar = (short)(adsClient.ReadAny(hVar, plcVar.GetType()) ?? 0); // Избыточный вариант **

(*) typeof(short) лучше превратить в plcVar.GetType(), это заставит разработчика проинициализировать переменную до ее первого использования.
(**) ReadAny() возвращает object который всегда не-null, иначе выбрасывается исключение, т. е. null не бывает никогда, поэтому можно избежать использования nullable-типов (short?) с дальнейшей проверкой на null или более простой конструкции "??".


Обработка ошибок


Так как до- или во время чтения/записи может произойти обрыв связи или другие неприятности, необходима обработка исключений.

try
{
    plcVar = (short)adsClient.ReadAny(hVar, plcVar.GetType());
}
catch {}

Но, здесь нет анализа ошибки.


try
{
    plcVar = (short)adsClient.ReadAny(hVar, plcVar.GetType());
}
catch(Exception ex)
{
    MessageBox.Show(ex.Message);
}

Вместо анализа ошибки, закидает пользователя и рабочий стол сообщениями об ошибке.


AdsErrorCode errCode = AdsErrorCode.NoError;
try
{
    plcVar = (short)adsClient.ReadAny(hVar, plcVar.GetType());
}
catch(AdsErrorException ex)
{
    errCode = ex.ErrorCode;
}
catch(Exception genex)
{
    // Произошло что-то не связанное с ADS.API
}

if (errCode != AdsErrorCode.NoError)
{
    switch(ex.ErrorCode)
    {
        case AdsErrorCode.ClientSyncTimeOut:
            // Обрыв связи
            // OnDisconnected();
            break;
        case AdsErrorCode.DeviceSymbolNotFound:
            // Переменная пропала (например, была стерта программы)
            // OnInvalidData();
            break;
        default:
            // Все, что не требует обработки
            // OnGeneralError();
            break;
    }
}

Вариант правильный, но очень длинный. На каждое чтение/запись такое не напишешь. Поэтому для чтения данных мастерят "обертки", берущие на себя всю рутину обработки ошибок и пр.

April 29, 2015

Большая клавиатура навсегда

Windows CE способно работать как с небольшими, так и с большими разрешениями экрана; поэтому в системе предусмотрены две экранные клавиатуры (Soft Input Panel (SIP): поменьше и побольше. В более новой Windwos CE 6 уже предусмотрено переключение клавиатур, и система этот выбор запоминает при перезагрузке. У более старой "пятерки" с памятью совсем плохо. Нужен костыль.

  1. "Start" → "Run..." → "regedit" → OK
  2. Открываем ветку: HKEY_CURRENT_USER\ControlPanel\SIP
  3. Правим значение параметра: DefaultIm
Большая клавиатура: {a523dfc7-1a7e-4af6-991a-510e75847828}
Маленькая клавиатура: {42429667-ae04-11d0-a4f8-00aa00a749b9}
Осталось перезагрузить контроллер.

April 28, 2015

Перформанс ADS.API под .NET

Как утверждает мой проводник, на этом компьютере есть библиотека TwinCAT.Ads.dll, которая позволяет обращаться к данным ПЛК через протокол ADS. Библиотека автоматически устанавливается вместе с Твинкатом (как вторым, так и третьим) и кроме нее, есть еще неплохой набор для множества других языков программирования.



Она удобная, есть документация и примеры. Вопрос в том - насколько она быстра?

Вопрос номер два - при наличии большого ассортимента контроллеров и гигантского количества "персоналок", как определить быстродействие и от чего отталкиваться?


Методика


На помощь спешит детерминированность ПЛК-цикла. Время цикла системы реального времени жестко выдерживается, значит мы можем посчитать сколько ПЛК-циклов библиотека ADS.API будет пропускать (или не будет) при использовании быстрого контроллера и быстрого ПК.

Берем ПКЛ Beckhoff CX2020 (подходит для ЧПУ систем) и ноутбук на базе Core i7-3630QM (позиционировался как игровой). Этого достаточно, чтобы "разогнаться" до 50 микросекунд, а меньше и не бывает.

Простейшая ПЛК-программа с заданным временем цикла, содержащая примитивный счетчик +1 каждый цикл. Будем раз-за-разом считывать его значение и замерять разницу между предыдущим и текущим значениями. При разнице в 1, мы получим время считывания равное одному ПЛК-циклу (его значение известно); при разнице в 2, мы получим время считывание равное двум ПЛК-циклам; при 10, соответственно 10 циклов на считывание одной переменной и т. д. Длина переменной счетчика типа UDINT = 4 байта. Чуть позже попробуем считывать строку длиной 100 байт и массив подлиннее.



Быстродействие


Синхронное чтение с помощью ReadAny() одной переменной длинной UDINT занимает 5..10мс. При асинхронном чтении с помощью уведомлений [event driven] и функции AddDeviceNotification(), можно разогнаться до 1мс: меньше не получится - это ограничение функционала библиотеки. Следует учесть, что такое поведение справедливо для обмена данными в UI-потоке: там же, где происходит обработка интерфейса пользователя (интерфейса как графического, так и консольного).

Синхронный обмен можно разогнать, если запустить поллинг (постоянный опрос) в параллельном потоке и считывать из него данные по мере необходимости. В таком случае можно "разогнаться" до 50мкс и даже намного быстрее, что конечно же бессмысленно т. к. ПЛК не сможет выдать данные быстрее своего цикла.


Объем


Оценка скорострельности бессмысленна без объема перекачиваемых данных. В тестах выше использовалась единственная переменная типа UDINT размером 4 байта. Что произойдет, если взять данных да побольше?

Количество последовательных синхронных операций ведут себя линейно - чем больше запросов, тем больше времени понадобится: два запроса удвоят время, три - утроят и т. д. Что если в одном запросе передать больше данных? Выше мы хотели посмотреть на строки и массивы, но т. к. это одно и то же (строки - это массивы символов с нулем в конце), то тестировать будем массивы разной длины.
Уточнение для педантичных разработчиков: я проверил работу со строками разной длины - оно работает аналогично линейным одномерным массивам байт. Никаких подводных камней.
И тут внезапно ждет печаль и разочарование - получить какие-то точные данные не получается: время запросов начинает сильно плавать как в синхронных, так и в асинхронных запросах. Вполне нормальное явление, когда детерминированное встречается с недетерминированным, но хотелось бы рекомендаций:
  1. Асинхронное однопоточное чтение быстрее синхронного однопоточного.
  2. Многопоточное синхронное чтение быстрее однопоточного асинхронного. "Сверхбыстродействие" можно получить созданием отдельного потока для поллинга синхронными функциями.
  3. Массив можно прочитать/записать за один вызов функции и за такое же время, как для одной простой переменной.
  4. Как следствие, для передачи большого количества переменных, их желательно упаковать в массив. Упаковать - не значит сжать, просто разместить.
  5. Начиная с размера массива в 16 килобайт, время запроса увеличивается нелинейно - становится чуть длиннее. Это связано с пакетной отправкой запросов и прочей сетевой кухней.
  6. После 16 килобайт, чем длиннее массив, тем больше времени понадобится на чтение/запись. До 16 килобайт объем данных на время обмена не влияет.
  7. 16 килобайт получено эмпирически для CX2020 и размера сетевого пакета "по умолчанию".

Представленная методика конечно же кривая, косая и неточная, но позволяет хоть как-то оценить и представить быстродействие обмена данными через ADS.API. К тому же, позволяет выбрать тип обмена: синхнронный, многопоточный, событийный асинхронный и т. п.