Showing posts with label TwinCAT 2. Show all posts
Showing posts with label TwinCAT 2. Show all posts

October 14, 2019

Git и макросы TwinCAT 2 (CoDeSys)

У нас тут настолько TwinCAT 3.1, что некоторые молодые специалисты уже не видели второй версии. Что не отменяет сопровождение старых проектов и желание использовать контроль версий. К тому же есть родня в лице CoDeSys, где все также, но немного по другому.

TwinCAT 2 и Git плохо совместимы из-за бинарного формата проектов TwinCAT 2: использовать можно, но то же diff так просто не получится. Еще есть ENI (ENginering Interface), но оно стоит денег, хотя и не понятно почему? Вероятно, потому что нужен, но если что, есть бесплатное на python, но я не проверял. К тому же оно для Visual SourceSafe и SVN, а нужен Git.

Кому не нужна история или просто лень читать, может сразу пройти на гитхаб — там выложены результаты в виде проекта Tc2_Git.


Что такое, как и почему Git?


Читать можно в произвольном порядке.
  1. Understanding Git (как устроен и работает Git):
  2. Сходство и различие между Mercurial и Git. Ответ на вопрос: почему именно Git?
  3. Git: наглядная справка.
  4. Про Git. Книга и полное описание работы с Git.
  5. Git How To. Интерактивный тур.


Экспорт


Первое, что необходимо сделать, это избавиться от бинарного формата проекта. В PLC Control есть возможность работать из командной строки. Оттуда же можно экспортировать проект из единого бинарного .pro файла в набор текстовых .exp файлов.

Про командную строку и командные файлы PLC Control можно прочитать в Command Line/Command File Commands. Интересно, что экспортировать можно как в один текстовый файл (project export), так и в несколько (project expmul): тогда в каждом, отдельном файле будет лежать отдельный модуль проекта. Разделение модулей по нескольким файлам в дальнейшем облегчит контроль, сравнение и откат изменений в проекте.

Кроме непосредственно экспорта понадобится еще ряд команд, поэтому без командных файлов не обойтись. И это будут не только системные батники-cmd, но и макросы PLC Control.


Макросы


Другая возможность — написать для среды разработки небольшую библиотеку макросов, добавить ее в проект и запускать экспорт непосредственно из меню PLC Control.

Я буду хранить библиотеку макросов в файле Git.mac. Название выбрано не случайно, но об этом чуть позже. Содержимое .mac файла:

MACRO
MacroName: 'Git Commit'
MacroMenu: '&Commit'
MacroCmd: 'file save'
MacroCmd: 'query off ok'
MacroCmd: 'replace yesall'
MacroCmd: 'project expmul ~project-filename-pro'
MacroCmd: 'system git add *'
MacroCmd: 'system git commit -m "%date%-%time%"'
END_MACRO

Кратко, что здесь происходит:
  1. Сохраняем текущие изменения в файл проекта, делаем все по тихому и без вопросов: query off ok; replace yesall.
  2. Экспортируем с разбивкой модулей по отдельным файлам .exp в подкаталог project-filename-pro с помощью project expmul.
  3. Добавляем все изменения на git stage.
  4. Коммитим с автоматическим комментарием в виде текущих даты-времени.
Все просто. Из недостатков — коммиты не содержат внятных комментариев и необходимость вручную вписать вместо project-filename-pro имя файла проекта. Например, есть project-name.pro, в .mac файле будет записано как project expmul ~project-name, то есть просто отбрасываем расширение файла.
Перед экспериментами не забудьте создать репозиторий через git init.

Добавляем макрос в рабочий проект. Идем в Project → Option → Macros и добавляем библиотеку макросов Git.mac через кнопку Macrolibrary → Include...



После этого появится новый раздел меню Edit  Macros → Git  Commit. Несмотря на то, что макрос назван Git Commit, система взяла за основу имя файла библиотеки макросов, то есть просто выдернула его из имени файла Git.mac. Поэтому название файла библиотеки имеет значение.



.Gitignore


Немного приберемся в каталоге проекта. Файл git-игнора может содержать следующие расширения файлов: *.dfr, *.pro, *.bak, *.ci, *.wbp. Эти файлы бинарные, поэтому лучше чтобы их не было в репозитории Git.


Git commit


Я предпочитаю использовать командную строку вместо макросов, поэтому сделал себе .cmd файл который сканирует каталог проекта в поисках .pro файлов, а затем каждый из них экспортирует в свою собственную папку вида ~имя-pro-файла. После экспорта командный файл автоматически коммитит изменения в локальный репозиторий Git. Количество .pro файлов не ограничено.

Командный файл +commit.cmd:

@echo off
SETLOCAL
set TcPlcCtrl="C:\TwinCAT\Plc\TCatPlcCtrl.exe"

if exist "*.pro" (
    FOR %%i IN ("*.pro") DO (
        CALL :ExportProject %%~ni
    )
)

del %cmd%

git add *
git commit -m '%date%-%time%'

exit /b

:ExportProject

set ProjectName=%1

set ProjectPath=%ProjectName%.pro
set cmd=__project_export.tmp
set ExportTo=~%ProjectName%

echo replace yesall > %cmd%
echo query off ok >> %cmd%
echo file open %ProjectPath% >> %cmd%
echo project expmul %ExportTo% >> %cmd%
echo file close >> %cmd%
echo file quit >> %cmd%

%TcPlcCtrl% /show hide /cmd %cmd%

exit /b

ENDLOCAL


Восстановление проекта


Если вы вдруг решите откатить текущие изменения git checkout или перескочить на другую ветку git branch, то сначала необходимо выполнить требуемые команды Git и только затем сделать импорт .exp файлов.

Я опять-таки всё автоматизировал, но только импорт. Еще раз сначала придется выполнить команды Git, и только затем выполнять +restore.cmd:

@echo off
SETLOCAL
set TcPlcCtrl="C:\TwinCAT\Plc\TCatPlcCtrl.exe"

choice /c YN /T 30 /D N /M "Do you want to restore working dir up to the last commit [default = N]"
if %ERRORLEVEL% EQU 2 EXIT 1

if exist "*.pro" (
    FOR %%i IN ("*.pro") DO (
        CALL :RestoreProject %%~ni
    )
)

del %cmd%
exit /b

:RestoreProject

set ProjectName=%1

set cmd=__project_import.tmp
set ProjectPath=%ProjectName%.pro
set ImportFrom=~%ProjectName%

echo replace yesall > %cmd%
echo query off ok >> %cmd%
echo file open %ProjectPath% >> %cmd%
echo project import %ImportFrom%\*.* >> %cmd%
echo file save
echo file close >> %cmd%
echo file quit >> %cmd%

%TcPlcCtrl% /show hide /cmd %cmd%

exit /b

ENDLOCAL

July 4, 2019

Activating System NC PTP Module

There is a special operation in TwinCAT 2 called 'level-up'. That is for PLCs based on Windows CE or Windows Compact only. You take plain PLC, level-up it and get PLC with Numerical motion Control Point-To-Point.

You have to own license key for this special level-up utility. This job was made special for service engineers but you can do it by yourself. Anyway you will get this:


Sometimes, after level-upping NC branch may absent in System Manager configuration. I.e. PLC already can do but System Manager does not allow to configure it.

Actually, we have rised the reg level but forgot to load TwinCAT system module.

TwinCAT 2 is a module (or composite) system. It consist of several low level drivers for different subsystems: TcIo.sys, TcRouter.sys, TcRTime.sys, TcNc.sys... In the case of WinCE, drivers are already integrated into the PLC image. You just have to plug-in this drivers.

The list of loading modules is located at \Hard Disk\TwinCAT\DefaultConfig.xml. Open it and insert new xml-section with TCNC module. See next:

Then we restart the PLC and get the result. It became much better:


Загрузка системного модуля NC PTP

В ПЛК на базе WinCE / Compact существует операция подъема уровня лицензии TwinCAT 2: был стандартный уровень PLC, а стал контроллер с NC PTP и возможностью управления движением.

Для подъема уровня TwinCAT используется специальная утилита, которая, конечно же, требует лицензионный ключ. Вообще, такой процедурой занимается сервисная служба, но можно и самому, а итогом послужит следующая картина:


Правда итог не так очевиден из System Manager: ветка NC может отсутствовать в конфигурации. Иными словами, контроллер уже может, а вот System Manager почему-то в упор не видит NC.

На самом деле уровень лицензии подняли, а модуль в TwinCAT еще не загрузили.

TwinCAT 2 — это модульная система. Она состоит из нескольких низкоуровневых драйверов для подсистем: TcIo.sys, TcRouter.sys, TcRTime.sys, TcNc.sys... В случае с WinCE драйверы уже встроены в образ, их просто нужно подключить.

Список модулей TwinCAT 2, загружаемых при старте системы, хранится в \Hard Disk\TwinCAT\DefaultConfig.xml. Открываем этот файл и дописываем загрузку модуля TCNC. Соответственно файл после редактирования будет выглядеть так:

<?xml version="1.0" encoding="UTF-8"?>
<TcBootProject>
    <Drivers>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCIO</ServiceName>
        </Driver>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCPLC</ServiceName>
        </Driver>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCRTIME</ServiceName>
        </Driver>
        <Driver>
            <Type>1</Type>
            <ServiceName>TCNC</ServiceName>
        </Driver>
    </Drivers>
    <InitCmds/>
</TcBootProject>


Перезапускаем ПЛК и смотрим на результат. Было → стало:


August 23, 2018

Очень длинный кейс

В продолжение темы экономии на абстракциях, я заметил необычное поведение отладчика TwinCAT 2 при пошаговом прохождении оператора CASE.

Программист, используя оператор CASE, заменяет спагетти из "ифов" (IF's) на красивую таблицу из структурированных блоков кода. Выполнение этих блоков зависит от значения одной единственной переменной. Именно так мы строим машину состояний, конечный автомат или просто разбиваем большой код на небольшие куски кода.

Мы ожидаем, что при входе в кейс, будет прочитано значение переменной, затем будет выполнен шаг с номером, хранящимся в этой переменной. Под дебагером же происходит нечто странное: происходит вход в кейс, а затем отладчик проходит по каждому шагу кейса, пока не дойдет до заданного шага и здесь, ожидаемо, начнет выполнять блок кода. Остальные участки кода он слава богу игнорирует, в том числе и те, что лежат условно "ниже" текущего шага (по тексту программы), но что если у нас кейс состоит из 1000 шагов? А то, что он проскачет по всем шагам, пока не достигнет нужного, и мы уже видели, что даже пустые вызовы подпрограмм, налагают дополнительную нагрузку на процессор ПЛК. Это поведение стоит проверить.

(* Я вынес анимированные иллюстрации в конец поста, чтобы не мелькали перед глазами. *)

Так как замерять производительность нужно на большом количестве вызовов и написать пару тысяч строк мне сложно и просто лень, да и времени на это жалко — я накидал программу на C#, которая генерирует другую программу, но уже на ST. Остается только скопировать из сгенерированного текстового файла output.txt код ST-программы, а затем вставить его в проект.

using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var writer = new StreamWriter("output.txt")) {
                writer.WriteLine("CASE i OF");

                for(int i = 0; i < 65000; i++)
                    writer.WriteLine($"{i}: c := c + 1;");

                writer.WriteLine("END_CASE");
            }
        }
    }
}


Выводы

  1. Количество шагов оператора CASE никак не влияют на производительность. Во время выполнения программы, при входе в оператор CASE, управление сразу же передают в текущий шаг кейса. Никаких посторонних прыжков не происходит. Это исключительно заморочки режима отладки, причем, только в TwinCAT 2.
  2. Не надо писать гигантские кейсы из 100500 шагов — отладчик падает, а проект собирается два часа.
  3. В TwinCAT 3 все нормально и с отладкой, и с производительность. Отладчик непосредственно переходит к исполняемому в данный момент шагу, игнорируя остальные блоки. Но отладчик все равно не переваривает 100500 шагов кейса и падает.
  4. Удобство языка программирования ST в его текстовости и в том, что вы можете генерировать его программы с помощью других языков программирования: C#, VB, Python, JS, ... автоматизируя рутинные действия. Похожий прием я использовал в проекте Tc3_PcSpeaker для работы со звуковой-пищалкой x86 контроллеров. Только программа была написана на питоне. 

TwinCAT 2




TwinCAT 3



August 22, 2018

Экономия на абстракциях

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

Что если разбивка на множество подпрограмм привносит дополнительную нагрузку на процессор ПЛК и снижает быстродействие?

Быстродействие важно и любые телодвижения в сторону абстракций, приводят к увеличению количества вызовов подпрограмм (функций, методов, других кусков кода). А ведь нам нужно успеть уложиться в миллисекунды базового времени (Base Time, в первую очередь), и только затем улучшить сопровождаемость (когда-нибудь потом, но лучше прямо сейчас).

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

Итак, исследуем влияние количества вызовов подпрограмм на загруженность процессора. Проверять TwinCAT 2 мне не очень-то и хотелось, но я проверил и не пожалел.

PROGRAM MAIN
VAR
    i: UINT;
END_VAR

FOR i := 0 TO 65000 DO
    PRG2();
END_FOR

(* ********************** *)

PROGRAM PRG2
VAR
    c : UINT;
END_VAR

;
// c := c + 1;
// c := c + 1;

Основная программа MAIN каждый цикл совершает ряд вызовов подпрограммы PRG2. Количество вызовов определяется количеством итераций цикла. Их максимальное число равно 65000 — как в примере выше. Так как нас в основном интересует нагрузка от ветвления программы, то подпрограмма PRG2 пустая. Точнее, она состоит из единственного оператора точка-с-запятой (;). Чтобы не было совсем банально, я разбавил подпрограмму парой строк со счетчиком (и это было правильным решением).

По вертикали (Y) — нагрузка процессора в процентах, по горизонтали (X) — количество вызовов подпрограммы за время одного цикла:
Ничего необычного: чем больше вызовов, тем больше нагрузка на процессор. При этом нужно учесть два факта: первый — подпрограмма PRG2 ничего не делает и нагрузка растет только за счет количества передачи управления из одной программы в другую; и второй — тест синтетический и в реальных ситуациях такого количеств вызовов за один цикл не бывает.

Теперь проверим TwinCAT 3:
...и он показывает более линейную зависимость.

Теперь об аномалиях второй версии TwinCAT. В подпрограмме PRG2 я убрал пустой оператор ";" и раскомментировал одну строку со счетчиком. Зафиксировал количество вызовов на числе 65000. Замерил нагрузку. Раскомментировал вторую строчку со счетчиком (теперь их две). Замерил нагрузку и впал в задумчивость.

Итак, внимание! С одной строкой счетчика, нагрузка составила 72%. С двумя строками счетчика — 43%. Больше кода — меньше нагрузка! Объяснить такое поведение можно только работой некоего оптимизирующего звена в компиляторе кода, но такая неявность, явно вводит в заблуждение. Я немедленно проверил то же самое в TwinCAT 3...

...и он по прежнему оказался линеен и предсказуем: 1 строка — 56%, две строки — 58%.


Выводы

  1. Вызовы подпрограмм добавляют нагрузку на процессор.
  2. Я решил, что не буду обращать на эту нагрузку внимание, так как ощутимой она становится только при каких-то очень больших числах вызовов за время одного цикла. Даже для CX8xxx серии.
  3. TwinCAT 3 стабильнее и предсказуемей, чем его вторая реализация.
  4. Upd. 29 мая 2019 года, TwinCAT 2 v2.11.2302 странности с "меньше кода - больше нагрузка" не наблюдаются. Возможно был баг с выравниванием переменных в памяти. 


P.S.: посчитаем вызовы


А как вообще прикинуть — сколько вызовов происходит в проекте?

Возьмем проект ЧПУ системы, она точно будет большой. Посчитаем скольку всего происходит вызовов подпрограмм (функций и т. п.) во всем проекте. Конечно же, за один цикл будут происходить не все вызовы, но и я собираюсь считать приблизительно, и по максимуму. Заодно отбросим вызовы, происходящие внутри библиотечных функций... что-то там в итоге должно сойтись.

Для начала, открываем проект и экспортируем его в единый файл экспорта *.exp. Затем, открываем EXP-файл в текстовом редакторе (Notepad++) и автозаменой удаляем все вхождения следующих строк: (*STRING(, IF (. Файл в итоге будет испорчен, но для нас важнее исключить посторонние элементы. Остается сделать глобальный поиск символа открывающей скобки (, который традиционно используется при вызове подпрограмм. Как вариант, поискать закрывающие скобки, а точнее конец вызова функции, что-то похожее на );

Количество вхождений искомых символов, будет приблизительно равно количеству вызовов функций и подпрограмм. Часть совпадений придется на математические формулы, но их не так много. В то же время часть вызовов мы все равно не обнаружим, так как они скрыты внутри библиотек и в других трудно доступных местах. Одно компенсирует другое, и в среднем получится более-менее правдоподобно.


На картинке выше подсвечены не все скобки, но они учтены и подсчитаны правильно.

В итоге, я нашел 560 вызовов с открывающими скобками и 433 с закрывающими. Это в два раза меньше 1000 (1-2% CPU, как в пустой программе) и совсем далеко от 65000 (56-58% CPU). Еще раз, это максимум, который можно вызвать, но который никогда не будет достигнут: программа разбита на шаги (машина состояний), в каждом из которых выполняется лишь небольшая часть всей технологической программы.

August 10, 2018

Виртуальная машина с Windows CE и TwinCAT

Когда под рукой нет "реального железа" — можно обойтись локальным рантаймом. Когда нужен рантайм под Windows CE (WEC, Embedded Compact) — мы идем за виртуальной машиной. В моем случае — это Oracle VirtualBox. Бекхофф же предлагает образ под Virtual PC и нам это только на руку.

Качаем с сайта Бекхофф образ виртуальной машины для Virtual PC. Образов несколько, они предназначены для различных версий TwinCAT (2, 3) и WEC (6, 7). Я выбрал под WEC 7 с TwinCAT 2.11R3 NCI(!): Beckhoff_VPC_WEC7_HPS_v502a_TC211R3_B2247.zip. Полагаю, что для остальных версий ситуация схожая (upd 24 авг 2018: да, для WEC7 TC3.1 всё аналогично).
Сходу образ не запустился. Пришлось обновить Virtual Box версии 5.1 до последней версии 5.2.16. Возможно проблема исключительно в моих системных настройках, но я предупредил.
Какие настройки Virtual Box необходимо задать? Кстати, не обязательно такие, просто эти проверены, работают и взяты из оригинального образа Virtual PC:

Name (произвольное название): WinCE 7
Operationg System: Other Windows (32-bit)
Base Memory: 512 MB
Video Memory: 8MB (virtualbox будет ругаться, что этого мало для полноэкранного режима)
Network Adapter →
        Attached to: Bridget Adapter
        Advanced → Adapter Type: Intel PRO/1000 MT Desktop (82540em)

Главное правильно задайте сетевой адаптер. В дальнейшем он понадобится для связи с внешним миром, а в качестве жесткого диска используйте .vhd файл из образа виртуальной машины. Выберите при создании пункт: Use an existing virtual hard disk file.


После этого у вас должна нормально запуститься виртуальная машина с Windows Embedded Compact 7 и TwinCAT 2.11R3 NCI. Но(!) без сетевой карты. Печально, что инженеры подложили нам двух свиней: во первых, это специальная версия TwinCAT, которая будет останавливаться каждый сутки (TC Daily Drop); во вторых, не установлены сетевые драйверы, которые позволили бы нам подключить периферию, да и вообще хоть как-то использовать виртуальную машину в качестве ПЛК. Исправляем.

Нам необходимы драйверы сетевой карты Intel e1000 для Windows CE. Вообще, это редкость и всяческий раритет, но гугл в помощь. Я нашел и выкачал комплект драйверов на сайте HPC:Factor в виде единого файла e1000ce5leg.exe. В целях безопасности, мы не будем запускать исполнимые .exe файлы из неизвестных источников (нельзя собирать грибы в незнакомом лесу), а просто распакуем его с помощью 7zip. Ссылка на перепакованный архив в подвале статьи. На поверку, это оказался обычный .cab архив, завернутый в MSZip. Осталось закинуть полученные файла на виртуальную машину.

И теперь мы воспользуемся преимуществом, подаренным нам виртуальной машиной Virtual PC. Образ жесткого диска от виртуальной машины, мы просто подключим к обычной настольной операционке. У меня Windows 10 Home, как дела с более старыми версиями — подсказать не могу.
7-zip версии 18.05 умеет распаковывать .vhd архивы, но не умеет добавлять в них файлы. Возможно другие архиваторы умеют это делать, и вам не понадобятся следующие телодвижения с подключением виртуальных дисков.
Итак, нам нужно "Управление дисками". Правой кнопкой по меню Пуск → "Управление дисками" или правой же кнопкой по Этому компьютеру → "Управление", а дальше разберетесь.

В управление дисками выбрать "Действие" → присоединить виртуальный жесткий диск.

Создайте в корне диска какой-нибудь каталог (temp, net, drivers, произвольные латинские буквы-цифры... главное, название не забудьте). Пусть будет netdrivers. Скопируйте в этот каталог содержимое архива e1000ce5leg.zip. В списке дисков кликните правой кнопкой по виртуальному диску → отключить виртуальный диск. Теперь можно запускать виртуальную машину.

Доступа к сети все еще нет, но теперь у нас есть драйверы. Запустите проводник: Start → Run... → explorer. Перейдите в каталог drivers, выделите все файлы → Edit-Copy. Перейдите в \Hard Disk\System → Edit-Paste. Запустите файл \Hard Disk\System\E1000CE5.reg для активации драйверов. Система предложит перезагрузиться → соглашайтесь.

После загрузки вы получите доступ к локальной сети и все соответствующие плюшки. Можно доустановить недостающие компоненты (подключая-отключая виртуальный жесткий диск), а затем сделать экспорт виртуальной машины и поделиться уже готовым образом через File → Export Appliance... Каталог с распакованными файлами драйвера (netdrivers в моем примере) можно удалить.



Архив с драйверами доступен здесь: e1000ce5leg.zip [77 Кб]

February 9, 2017

Указатель на ФБ

Указатели — это прямой путь в память контроллера. Путь — ничем не ограниченный и крайне опасный: одно не верное движение и вот вы уже в неразмеченной области памяти, отстреливаете себе ногу, вызывая исключение Page Fault! PLC is stopped. Что делать и как быть?

Начнем с простого:

PROGRAM MAIN
VAR
    A  : INT;
    B  : WORD;
    
    pA : POINTER TO INT;
    pB : POINTER TO WORD;
    dw : DWORD;
END_VAR

[...]

pA  := ADR(A);
pA^ := -1;


Переменная А в результате будет равна -1. При попытке сделать аналогичное с переменной B — pB^ := -1; получим ошибку компилятора еще на этапе сборки проекта. Объяснение простое — мы объявили pB как указатель на целый и всегда положительный тип WORD, а пытаемся пропихнуть число со знаком, типа INT. Компилятор бдит.


Выход за границу


Вообще, указателям можно присваивать все, что угодно, лишь бы справа был POINTER TO или DWORD (UDINT):

dw  := ADR(B);
pB  := dw;
pB^ := 123;

ADR возвращает число типа DWORD — это адрес переменной. Поэтому можно присвоить этот адрес указателю pB, а затем разыменовать указатель с помощью оператора ^ и присвоить новое значение для указанной переменной B. После всех действий B будет равен 123.

Оператор ^ применим только к указателям. Нельзя разыменовать переменную другого типа: dw^ := 123; — получим ошибку: '^' needs a pointer type. Поэтому хранить адрес можно в обычных переменных, но работать с адресами получится только через указатели.

Добавим перца — указателю можно присвоить любой адрес или любой другой указатель, указывающий на произвольный тип:

pA  := pB; (* pA теперь указывает на переменную 'B' типа WORD *)
pA^ := -2;

Результат: B = 65534 и ошибки здесь нет. Мы объявляли pA, как указатель на целое со знаком, то есть переменную типа INT, а переменная B — это целое беззнаковое, поэтому бит знака превратился в значимый разряд и дальше бла-бла-бла...

До сих пор у нас совпадал размер переменных — обе занимали ровно два байта. Что будет, если мы сделаем так:

A   : INT;
B   : BYTE;

[...]

pA  := pB; (* pA теперь указывает на переменную 'B' типа BYTE *)
pA^ := 1234;

B = 210;

...и опять без ошибки, но она может легко возникнуть, так как мы уже вышли за пределы переменной: 1234 занимает в памяти два байта, а мы записываем это число в переменную B типа BYTE, длиной один байт, как нам сообщает Капитан Очевидность. Таким образом легко совершить целый ряд безобразий: вылезти за пределы переменной или массива, вызвать сбой при обращении к странице памяти, остановить контроллер и технологический процесс. Завод встал, рабочие с факелами идут карать Франкенштейна.


Указатель на код


Точнее, на функциональные блоки, ведь на программы ссылаться нельзя.

Для этого эксперимента нам понадобится:
  • Глобальная переменная, значение которой будет изменяться функциональными блоками.
  • Два ФБ, по разному изменяющие содержимое глобальной переменной.
  • Два указателя, которые мы будем испытывать, сталкивая лбами.
  • Немного терпения разработчика.

А давайте как физики в эксперименте про квантовую телепортацию — назовем наши функциональные блоки "Алиса" и "Боб" (в алфавитном порядке):

VAR_GLOBAL
    g_Result : STRING := 'Хзкт';
END_VAR

[...]

FUNCTION_BLOCK Alice
VAR_INPUT
    Name : STRING; (* пригодится позднее, когда появится злой двойник *)
END_VAR

g_Result := 'Алиса';

[...]

FUNCTION_BLOCK Bob
g_Result := 'Боб';

[...]

PROGRAM MAIN
VAR
    Al  : Alice;
    Bo  : Bob;
    pAl : POINTER TO Alice;
    pBo : POINTER TO Bob;
END_VAR

[...]

pAl := ADR(Al);    (* Указатель на ФБ Алиса *)
pBo := ADR(Bo);    (* Указатель на ФБ Боб *)

pAl^(); (* ФБ можно вызывать через разыменованный указатель *)


Каждый из ФБ записывает в глобальную переменную g_Result соответствующее имя: в зависимости от того, чей экземпляр мы вызовем через указатель, мы получим разные имена в глобальной переменной. На этот раз, получим результат 'Алиса'.

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

pAl := ADR(Bo);  (* Указатель на ФБ Боб *)
pAl^();


И если вы подумали, что получился 'Боб', то вы не правы — g_Result по прежнему равно 'Алиса'. Система следит за типом указателя, не давая сменить его на неправильный, но делает это молча — никому, ни о чем не сообщая.

Давайте создадим второй экземпляр ФБ Alice и переведем указатель на него, то есть присвоим указателю указатель, но теперь правильного типа POINTER TO Alice:

VAR
    Al     : Alice := (Name := 'Alice Original');
    AlTwin : Alice := (Name := 'Alice Evil Twin');

[...]

pAl := ADR(Al);       (* Указатель на ФБ Алиса *)
pAl := ADR(AlTwin);   (* Указатель на ФБ близнеца Алисы *)
pAl^();


На этот раз, указатель изменился:



TwinCAT 3


Скопируем проект, с небольшими отличиями в объявлении глобальной переменной:

VAR_GLOBAL
    Result : STRING := 'Хзкт';
END_VAR

[...]

FUNCTION_BLOCK Alice
G.Result := 'Алиса';  (* аналогично для Боба *)

[...]
    
pAl := ADR(Al);       (* Указатель на ФБ Алиса *)
pBo := ADR(Bo);       (* Указатель на ФБ Боб *)

pAl := pBo;

pAl^();


В результате получим — 'Боб', то есть G.Result = 'Боб'. В TwinCAT 3 указатели... э-э-э, гибкие? изменчивые? динамические? отзывчивые?


Нечеловеческий эксперимент над роботами


Жуткий по сложности и непонятности эксперимент над указателями в TwinCAT 2:
  1. Ссылаемся указателем pA на функциональный блок Alice.
  2. Получаем адрес функционального блока Bo.
  3. Получаем группу:смещение указателя pA.
  4. Через функции ADS записываем (подменяем) адрес указателя pA.
  5. Разыменовываем указатель pA и вызываем функциональный блок.

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

PROGRAM MAIN
VAR
    ReadSymInfo : PLC_ReadSymInfoByName;
    SymInfo     : SYMINFOSTRUCT;
    WriteAds    : ADSWRITE;

    Al          : Alice;
    pAl         : POINTER TO Alice;
    Bo          : Bob;
    AddrBob     : UDINT;

    state       : UINT;
END_VAR

[...]

CASE state OF
0:
    pAl     := ADR(Al); (* 1 *)
    AddrBob := ADR(Bo); (* 2 *)
    state   := 100;

100: (* 3 *)
    ReadSymInfo(
        NETID   := '',
        PORT    := 801,
        SYMNAME := 'MAIN.pAl',
        START   := TRUE,
        SYMINFO => SymInfo);

    IF NOT ReadSymInfoByName.BUSY THEN
        ReadSymInfoByName(START := FALSE);
        state := 200;
    END_IF

200: (* 4 *)
    WriteAds(
        NETID   := '',
        PORT    := 801,
        IDXGRP  := SymInfo.idxGroup,
        IDXOFFS := SymInfo.idxOffset,
        LEN     := 4,
        SRCADDR := ADR(AddrBob),
        WRITE   := TRUE);

    IF NOT WriteAds.BUSY THEN
        WriteAds(WRITE := FALSE);
        state := 300;
    END_IF

300:
    pAl^(); (* 5 *)

END_CASE

В результате все равно получится 'Алиса' и это несмотря на то, что указатель, судя по адресу, указывает на 'Боба':



Предположения


В TwinCAT 2 указатели константные, плюс махинации с таблицей символьной информации (SymbolInfo), которая как-то неявно привязана к переменным. Код компилируется раз и навсегда, а вот данные переменных транслируются через специальную таблицу символов и могут изменяться. Указатели стоят где-то на стыке между кодом и переменными, поэтому адрес указателя мы изменить можем, но вызов кода уже не изменим, так как он уже скомпилирован. В общем, указатели в TwinCAT 2 только для данных и ограниченно для кода.

В TwinCAT 3 завезли объектно-ориентированное программирование с классами, методами и, самое главное, интерфейсами. Последние как раз нуждаются в полиморфизме и гибких указателях (vtable). Поэтому с указателями в TwinCAT 3 все ожидаемо-хорошо.

И там, и там можно устроить крах системы, выйдя за пределы выделенной памяти. Осторожнее там с указателями!

February 2, 2017

События под максимальной нагрузкой

Насколько сильно нагружают процессор ПЛК асинхронные функции TwinCAT.ADS API? Стоит ли пользоваться подпиской на переменные ПЛК-задачи и сидеть в ожидании уведомлений об изменившихся данных или сразу написать polling-велосипед? Я уже измерял скорость передачи данных через ADS.API под .NET, теперь посмотрим как нагружают систему самые тяжелый асинхронные функции событийной модели.

TcAdsClient client = new TcAdsClient();
client.Connect("5.24.134.118.1.1", 801);


Имя для области памяти


Стоит сразу уточнить, что событийная модель асинхронных функций ADS API работает не с переменными, а с областями памяти, которые получая имя, становятся переменными. Благодаря этому нюансу можно подписаться на изменения не только всей переменной, но и на изменения в части переменной (например, отслеживать изменения только в старшем байте переменной типа WORD).
Расчленение переменных возможно только до уровня байта, так как внутри идет побайтовое обращение к памяти.
Чтобы убедится в вышеизложенном — возьмем переменную и подпишемся на ее часть:

PROGRAM MAIN
VAR
    a : WORD;
END_VAR

[...]

a := a + 1;
a := a AND 16#00FF; (* А *)

Наращиваем переменную MAIN.а типа WORD каждый цикл на единицу, а старшую часть обнуляем (* А *) чтобы она оставалась неизменной. Для начала подпишемся на всю переменную целиком и убедимся, что система работает.


Адрес переменной


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

ITcAdsSymbol info = client.ReadSymbolInfo("MAIN.a");
Console.WriteLine($"Group:Offset = {info.IndexGroup}:{info.IndexOffset}");

Как результат получим адрес Group : Offset = 16448 : 0. В моем тестовом примере переменная в программе одна единственная, поэтому она расположена в начале области памяти, и поэтому ее смещение равно нулю.
В статье справочной системы "Index-Group/Offset" Specification of the PLC services можно найти упоминание, что память ПЛК-задачи начинается с адреса 0x00004040 = 16448.
Теперь, обращаясь через байтовые смещения, мы можем адресовать только часть переменной. Заменим имя этой переменной на адрес области памяти — Group: 0x4040, Offset: 0, длина 16 разрядов (тип ushort или WORD):

client.AdsNotificationEx += Client_AdsNotificationEx;
client.AddDeviceNotificationEx(0x4040, 0, AdsTransMode.OnChange, 0, 0, null, typeof(ushort));

[...]

private void Client_AdsNotificationEx(object sender, AdsNotificationExEventArgs e)
{
    Log(e.Value);
}


Система работает — как и было задумано: уведомления приходят в метод Client_AdsNotificationEx, значение переменной доступно через поле e.Value.


Часть вторая


Подпишемся теперь на младший байт переменной MAIN.a, то есть на ту половину переменной, которая увеличивается каждый цикл. Для этого мы изменим тип данных с ushort (16-разрядов — эквивалент WORD) на byte (8 разрядов):

client.AddDeviceNotificationEx(0x4040, 0, AdsTransMode.OnChange, 0, 0, null, typeof(byte));

Уведомления по прежнему приходят.

Теперь подпишемся на старшую половину переменной. Для этого достаточно увеличить смещение (Offset) на единицу, то есть перейти на один байт дальше:

client.AddDeviceNotificationEx(0x4040, 1, AdsTransMode.OnChange, 0, 0, null, typeof(byte));

Старший байт переменной искусственно обнуляется каждый цикл (* А *), поэтому старшая часть переменной остается неизменной. И всё — сообщения больше не приходят, кроме самого первого после подписки.
Так было задумано разработчиками — всегда приходит первое сообщение. До подписки мы не знали значение переменной, поэтому для нас оно было неопределенным. После подписки неизвестное значение сразу же может стать известным, поэтому система отправляет нам первое (и возможно единственное) сообщение о текущем значении переменной.

Теперь, когда мы знаем, что можно подписаться только на часть переменной, воспользуемся этим знанием и объявим большой массив. В дальнейшем, будем подписываться на его части, увеличивая количество частей с каждым разом все больше и больше. Параллельно будем отслеживать нагрузку процессора.


Подписываемся и замеряем


В качестве испытуемого возьмем CX9020. Начнем проверку с TwinCAT 2, а чуть позже сравним с третьей версией. Сейчас же идея такая: подписываемся на пару сотен переменных, а затем смотрим через System Manager нагрузку на процессор. Пишем код:

PROGRAM MAIN
VAR
    Arrgh : ARRAY [1..10000] OF WORD;
    i     : INT;
END_VAR

[...]

FOR i := 1 TO 1000 DO
    Arrgh[i] := Arrgh[i] + 1;
END_FOR


Подписываемся:

for (int i = 0; i < 200; i++)
    client.AddDeviceNotificationEx(0x4040, i * 2, AdsTransMode.OnChange, 0, 0, null, typeof(ushort));


Замеряем:


Очень много для программы, которая практически ничего не делает. Давайте попробуем приблизиться к заявленному пределу в 550 переменных (Асинхронные функции TwinCAT.Ads API):


Здесь 500 переменных. Результат ужасный.


Полл Шред


Не будем подписываться, будем опрашивать вручную.

[...]

Прошел час. Пропустим несколько итераций эксперимента и сразу рассмотрим опрос 10 000 переменных. Для этого нам просто необходим отдельный поток:

CancellationTokenSource cancelTokenSource;

private void StartButton_Click(object sender, EventArgs e)
{
    cancelTokenSource = new CancellationTokenSource();
    SynchronizationContext uiContext = SynchronizationContext.Current;
    Thread pollThread = new Thread(() => PollThread(cancelTokenSource.Token, uiContext));
    pollThread.Start();

[...]

private void PollThread(CancellationToken token, object uiContext)
{
    using (var client = new TcAdsClient())
    {
        client.Connect("5.24.134.118.1.1", 801);

        while (!token.IsCancellationRequested)
        {
            for (int i = 0; i < 10000; i++)
            {
                if (token.IsCancellationRequested)
                    goto EndOfThread;

                ushort a = (ushort)client.ReadAny(0x4040, i * 2, typeof(ushort));
            }

            Thread.Sleep(0);
        }

        EndOfThread:;
    }
}


Старт. Результат:



Это был регулярный опрос 10 000 переменных за раз. Если понравилось — давайте попробуем регулярно считывать целиком весь массив. Для этого заменим цикл for на чтение массива целиком:

ushort[] buf = new ushort[10000];
buf = (ushort[])client.ReadAny(0x4040, 0, buf.GetType(), new int[] { buf.Length });


Результат аналогичный — нагрузка не превышает 20%.


Грузите апельсины бочками


Асинхронные функции сильно нагружают систему контроллера. Для старших моделей ПЛК слово "сильно" можно заменить на "значительно", но легче от этого не станет. Тем не менее, если нужно следить за десятком переменных, то можно обойтись асинхронными функциями. Если же переменных много, то есть два выхода: сделать опрос вручную или объединить переменные в структуру/массив.

Рассмотрим второй вариант. Я изменил код и подписался сразу на целый массив из 10 000 ячеек типа WORD. Теперь, чтобы пришло сообщение, достаточно изменить значение только одной ячейки массива (или одного поля структуры, если мы объединили переменные в структуру):

ushort[] buf = new ushort[10000];
client.AddDeviceNotificationEx("Main.Arrgh", AdsTransMode.OnChange, 0, 0, null, buf.GetType(), new int[] { buf.Length});


Результат:



Вывод: перед отправкой упаковывайте данные в структуры и отслеживайте их как единое целое.


TwinCAT 3.1


В третьем твинкате все замечательно — можно смело выкинуть огорчения из предыдущих пунктов и смело использовать подписку на переменные. И хотя нагрузка на ПЛК снизилась в разы, асинхронные функции по прежнему дают ощутимую нагрузку на процессор ПЛК.

Подписываемся на 1000 переменных (контроллер тот же — CX9020):



Но(!) по прежнему нельзя подписаться на 10 000 переменных — большое количество подписок приведет к ошибке: "Ads-Error 0x751 : A hashtable overflow has occured". Это внутренняя ошибка переполнения словаря переменных в библиотеке TwinCAT.Ads, то есть по прежнему имеем ограничение на количество переменных. Мелочь, а неприятно.


Замыкаем


Пора найти плюсы и минусы и замкнуться в сферах применения:

Подписка на события (событийная модель):
+ Удобство использования: методы обработки данных вызываются только тогда, когда переменная изменилась.
+ Регулярность сообщений не зависит от программы на ПК.
+ Снижение нагрузки на сеть: обменом данными занимаются AMS-роутеры, которые отправляют только измененные данные.
– Ограничение на частоту уведомлений (не быстрее 1 миллисекунды).
– Ограниченное количество переменных: 550 штук — TwinCAT 2, несколько тысяч в TwinCAT 3.
– Чем больше переменных, тем выше нагрузка на процессор контроллера.

Регулярный опрос (поллинг):
+ Неограниченное количество переменных.
+ Низкая нагрузка на процессор ПЛК.
+ Максимальная скорость обмена данными (до 50 мкс, быстрее TwinCAT просто не умеет).
– Требуется вручную обмениваться и обрабатывать значения переменных.
– Дополнительная нагрузка на сеть. Запросы и ответы ходят по сети регулярно, независимо от того — изменились данные или нет.
– Дополнительная нагрузка на программу ПК и сложность в синхронизации данных в многопоточном приложении.

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

И в любом случае TwinCAT 3 работает быстрее и легче.

January 13, 2017

System Manager и четыре рантайма

При попытке создать несколько рантаймов и добавить несколько ПЛК-проектов, System Manager начинает капризничать, брыкаться, а в конце выдает ошибку: Runtime system is used by another project!



... и это не баг и не фича — это нюанс, который придется обойти.

Проект строится с помощью двух системных программ: System Manager — конфигуратор ПЛК и PLC Control в котором непосредственно разрабатывается программа контроллера. В какой-то момент разработки необходимо пробросить между ними мостик и познакомить их, передавать настройки из одной в другую и обратно. Для этого служат файлы с расширением .tpy, которые делает PLC Control при сборке (Project → Build) или пересборке (Project → Rebuild all) проекта.
Кроме файлов TPY, также существуют файлы TPA. Их создает System Manager, когда вы добавляете в конфигурацию новый проект или пересканируете уже существующие приекты (Rescan Project...). В эти TPA-файлы System Manager сохраняет конфигурацию переменных ввода/вывода (%I*, %Q*), заменяя звездочки (*) на реальные адреса.

При последующей пересборке проекта, PLC Control увидит файл TPA, созданный ранее System Manager'ом, и добавит в ПЛК-проект конфигурацию переменных, которую впоследствии можно увидеть в Resources → Global Variables → TwinCAT_Configuration.
В файле TPY находится полная информация о проекте, в том числе информация о рантайме, в котором планируется исполнять ПЛК-программу. К сожалению, невозможно выбрать нужный рантайм вручную, если вы разрабатываете проект без контроллера на столе и рядом с вами. По умолчанию система автоматически подставляет в проект первый рантайм, а затем сама же на это ругается, когда вы пытаетесь добавить второй, третий или четвертый проект в System Manager: ведь первый рантайм уже занят и нужно выбрать другой?

Чтобы покинуть тупиковую ситуацию, придется вручную отредактировать файл TPY, а конкретнее поле <Port> настроек роутинга <RoutingInfo> проекта <PlcProjectInfo>. Откройте в любом текстовом редакторе файл с расширением .tpy:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--This file is created by the TCatPlcCtrl automatically. Manually changes will be overwritten!-->
<PlcProjectInfo xmlns:p="http://www.beckhoff.com/2002/01/TcPlcProjectDesc">
    <ProjectInfo>
[...]
    </ProjectInfo>
    <RoutingInfo>
        <AdsInfo>
            <NetId>0.0.0.0.0.0</NetId>
            <Port>801</Port>
            <TargetName>
                <![CDATA[Target: Local (192.168.56.1.1.1), Run Time: 1]]>
            </TargetName>
        </AdsInfo>
    </RoutingInfo>

Номера портов жестко фиксированы: 1 — 801; 2 — 811; 3 — 821; 4 — 831. Достаточно заменить номер порта на соответствующий необходимому рантайму, а все остальные упоминания о рантайме можно оставить без изменений: на них система не обращает никакого внимание. Сохраните файл и без проблем добавьте его в конфигурацию System Manager'а.

November 3, 2016

Системный диалог даты-времени в Windows CE

Как быстро сделать ввод системной даты/времени при создании визуализации TwinCAT 2 под Windows CE? Нужно воспользоваться системным диалогом из панели управления — открыть  проводник explorer.exe, а дальше оператор сам найдет, исправит, настроит, испортит. Чтобы избежать такого поведения можно открывать только панель даты/времени.

1. Из меню Start запускаем notepadce.
2. Аккуратно вводим одну строчку, внимательно следя за пробелами и другими символами:

35#"ctlpnl.exe" \Windows\cplmain.cpl,13

35# — это количество символов в строке. Между ..ctlpnl.exe" и \Windows\... стоит пробел.

3. Файл сохраняем как datetime.lnk во флэш памяти контроллера (имя файла произвольное, если-что). Сохранить можно где-нибудь в \Hard Disk\datetime.lnk

4. Для элемента, при нажатии на который будет открываться панель настройки даты/времени, выставляем параметр визуализации (кавычки там обязательны):

Input → Execute program: "\Hard Disk\datetime.lnk"

Теперь при нажатии на элемент можно отображать только системную панель настройки даты/времени:



Можно открывать и другие апплеты панели управления:


PC Connection – 35#"ctlpnl.exe" \Windows\cplmain.cpl,0
Dialing – 35#"ctlpnl.exe" \Windows\cplmain.cpl,1
Keyboard – 35#"ctlpnl.exe" \Windows\cplmain.cpl,2
Password – 35#"ctlpnl.exe" \Windows\cplmain.cpl,3
Owner – 35#"ctlpnl.exe" \Windows\cplmain.cpl,4
Power – 35#"ctlpnl.exe" \Windows\cplmain.cpl,5
System – 35#"ctlpnl.exe" \Windows\cplmain.cpl,6
Display – 35#"ctlpnl.exe" \Windows\cplmain.cpl,7
Mouse – 35#"ctlpnl.exe" \Windows\cplmain.cpl,8
Stylus – 35#"ctlpnl.exe" \Windows\cplmain.cpl,9
Volume & Sounds – 35#"ctlpnl.exe" \Windows\cplmain.cpl,10
Input Panel – 35#"ctlpnl.exe" \Windows\cplmain.cpl,11
Remove Programs –  35#"ctlpnl.exe" \Windows\cplmain.cpl,12
Date/Time – 35#"ctlpnl.exe" \Windows\cplmain.cpl,13
Certificates – 35#"ctlpnl.exe" \Windows\cplmain.cpl,14
Accessibility – 35#"ctlpnl.exe" \Windows\cplmain.cpl,15


Для открытия CX Configuration или полностью Beckhoff CX Configuration Tool, нужно запустить другой апплет:

35#"ctlpnl.exe" \Windows\CxConfigCpl.cpl


Для юных исследователей доступен каталог \Windows, который может содержать (и содержит) другие апплеты панели управления в виде файлов с расширением .cpl