Современные ПЛК, как и другие подобные устройства на производстве, обладают возможностями восстановления после сбоя. Имеются в виду программные средства восстановления состояния системы управления до момента перед сбоем, приведшем к перезагрузке. Если аппаратная часть исправна, то необходимо «подхватить» управление системой и привести её в безопасное состояние, либо продолжить выполнение алгоритма, если это возможно.
Чтобы продолжить работу программы нужно восстановить состояние алгоритма управления до сбоя. Для этого у ПЛК есть специальные средства, наличие которых определено стандартами. Codesys имеет специальные области определения для хранимых переменных и системную библиотеку, предоставляющую функции для записи и восстановления содержимого этих областей. Мы обсудим работу с областью retain переменных на примере Raspberry Pi.
Область retain переменных
Одним из типов переменных, сохраняющих своё значение при завершении работы программы, являются retain переменные. Почитать про свойства различных типов подобных переменных можно на справочной страничке (англ.): Remanent Variables — RETAIN, PERSISTENT .
Нас будет интересовать несколько вещей, связанных с retain переменными:
- как их объявить;
- как заставить их работать;
- какого размера они могут быть;
- как их применять.
С объявлением всё просто, достаточно добавить после var слово retain:
1 2 3 |
var retain n: int; end_var |
С размером немного сложнее. По умолчанию Codesys для Raspberry Pi ограничивает размер области retain переменных значением примерно в 4 КБ. Это выяснится при попытке загрузить программу, имеющую большую retain область, в устройство.
Определение границ различных областей памяти находится в файле описания устройства, имеющим окончание .devdesc.xml. При установке пакета для Raspberry Pi этот файл автоматически был добавлен в список доступных ПЛК. Предлагаю изменить этот файл таким образом, чтобы был доступен любой размер области retain переменных.
Для этого нужно выполнить следующую последовательность действий:
Шаг 1. Нужно извлечь файл CODESYS Control for Raspberry Pi SL.devdesc.xml из пакета CODESYS Control for Raspberry PI 3.5.x.x.package, который на самом деле является архивом.
Шаг 2. Найти в файле поиском по ключевому слову retain область с описанием секций памяти и привести её к следующему виду:
1 2 3 4 5 6 7 8 9 10 |
<ts:section name="memory-layout"> <ts:setting name="retain-size" type="integer" access="visible"> <ts:value>0</ts:value> </ts:setting> <ts:setting name="dynamic-retain" type="boolean" access="visible"> <ts:value>true</ts:value> </ts:setting> <ts:setting name="dynamic-persistent" type="boolean" access="visible"> <ts:value>true</ts:value> </ts:setting> |
Здесь выше параметра с именем dynamic-retain добавлен код:
1 2 3 |
<ts:setting name="retain-size" type="integer" access="visible"> <ts:value>0</ts:value> </ts:setting> |
Этот код на самом деле позволяет Codesys динамически вычислять размер области retain переменных при компиляции.
Шаг 3. После этого нужно открыть репозиторий устройств в среде разработки Codesys и установить устройство, используя изменённый файл CODESYS Control for Raspberry Pi SL.devdesc.xml. При этом обновятся параметры уже установленного устройства. Больше этот файл не понадобится, т.к. при установке он копируется в систему. Теперь можно объявлять массивы произвольного размера, если это нужно.
Сохранение и восстановление retain области
Одного только объявления недостаточно, чтобы всё работало. Необходим специальный код поддержки, основанный на функциях из системной библиотеки CmpApp. Пример такого кода можно обнаружить на некоторых форумах: здесь и здесь. Два источника одного кода, если вдруг кому-то понадобится изобрести свой велосипед.
Может возникнуть вопрос: Почему поддержка retain переменных не встроена в среду разработки полностью?
Потому что среда исполнения может выполняться на различных платах и операционных системах. Всего не предусмотришь, поэтому адаптацию под конкретный ПЛК должен делать программист-разработчик.
Предлагаю свой вариант адаптации, основанный на указанных выше источниках. Код поддержки retain переменных для Raspberry Pi оформлен в виде функционального блока TRetainControl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
function_block TRetainControl var constant PATH_FS: string := '/var/opt/codesys/PlcLogic/'; end_var var_input SaveFilterTime: time := t#5s; // Период сохранения ForceSave: bool := false; // Мгновенное сохранение всех Retain-переменных (без кэширования). end_var var_output ResIEC: RTS_IEC_RESULT; // Код состояния AttemptSaveCounter: dint; // Счетчик записей Retain-переменных на носитель end_var var Init: bool; dwCRCSaved, dwCRC: dword; RetainAreaSize, RetainFileSize: __xword; synccmd: string := 'sync -f '; fileName: string; rt: R_TRIG; result: RTS_IEC_RESULT; pRetainArea: pointer to byte; pCurrentApp: pointer to APPLICATION; t1: ton := ( PT := SaveFilterTime ); if not Init then pCurrentApp := AppGetCurrent( adr( ResIEC ) ); if ResIEC <> Errors.ERR_OK then return; end_if RetainAreaSize := AppGetAreaSize( pApp := pCurrentApp, uiType := DA_RETAIN, pResult := adr( ResIEC ) ); if ResIEC <> Errors.ERR_OK then return; end_if pRetainArea := AppGetAreaAddress( pApp := pCurrentApp, uiType := DA_RETAIN, pResult := adr( ResIEC ) ); if ResIEC <> Errors.ERR_OK then return; end_if fileName := concat( PATH_FS, pCurrentApp^.szName ); fileName := concat( fileName, '/' ); fileName := concat( fileName, pCurrentApp^.szName ); fileName := concat( fileName, '.ret' ); synccmd := concat( synccmd, fileName ); result := AppRestoreRetainsFromFile( pCurrentApp, fileName ); case result of Errors.ERR_OK: ; // Signature mismatch of an api function. Errors.ERR_SIGNATURE_MISMATCH: ; // File error. e.g. cannot open a file for writing because it could be write protected. Errors.ERR_FILE_ERROR: RetainFileSize := SysFileGetSize( fileName, adr( result ) ); if result <> Errors.ERR_NO_OBJECT then ResIEC := result; return; end_if else ResIEC := result; return; end_case dwCRCSaved := CRC32Finish( ulCRC := CRC32Update( ulCRC := CRC32Init(), pData:= pRetainArea, ulSize:= RetainAreaSize ) ); Init := true; end_if rt( CLK := ForceSave ); dwCRC := CRC32Finish( ulCRC:= CRC32Update( ulCRC := CRC32Init(), pData:= pRetainArea, ulSize:= RetainAreaSize ) ); t1( in := dwCRC <> dwCRCSaved, PT := SaveFilterTime ); if t1.q or rt.q then // Сохраняем в файл. ResIEC := AppStoreRetainsInFile( pCurrentApp, fileName ); if ResIEC = Errors.ERR_OK then // Запоминаем контрольную сумму. dwCRCSaved := dwCRC; // В конфигурационном файле Codesys RTS должно быть разрешение на запуск // внешних программ: // [SysProcess] // Command=AllowAll if rt.q then SysProcessExecuteCommand( synccmd, adr( ResIEC ) ); end_if end_if // Считаем количество попыток сохранения. AttemptSaveCounter := AttemptSaveCounter + 1; // Сбрасываем таймер. t1( in := false ); end_if |
Экземпляр этого функционального блока должен выполняться в отдельной задаче и иметь высокий приоритет, а в первом цикле по результату выполнения давать разрешение на выполнение остального кода приложения, т.к. при первом его вызове восстанавливается область retain переменных.
Правильная работа в режиме принудительного сохранения (ForceSave) требует разрешения на выполнение внешних программ. Для этого нужно в пользовательском файле конфигурации /etc/CODESYSControl_User.cfg добавить/изменить следующие строки:
1 2 |
[SysProcess] Command=AllowAll |
Эта настройка позволяет выполнять любую команду из программы Codesys.
Алгоритм работы функционального блока требует отдельного разбора, поэтому поясню кратко что он делает. При первом вызове происходит формирование имени файла для хранения и попытка восстановления из этого файла области retain переменных. Если всё прошло успешно, то при следующих вызовах проверяется изменилась ли область и истёк или нет минимальный интервал записи в файл. Этим интервалом можно управлять (SaveFilterTime), он позволяет экономнее расходовать ресурсы накопителя (циклы записи), на котором расположен файл. Если интервал истёк или используется принудительное сохранение (ForceSave), то содержимое области retain записывается в файл.
Таким образом, регулярное выполнение этого функционального блока формирует «снимки» области retain переменных, сохраняемые в специальном файле. Если произойдёт перезагрузка устройства или исполняемого файла Codesys RTS, то значения переменных будут восстановлены, кроме случаев, оговоренных в справочной системе.
Путь к файлу формируется таким образом, чтобы имя файла было такое же как у приложения (с расширением .ret) и он находился рядом с файлом приложения. Дело в том, что проект может содержать несколько работающих одновременно приложений и в каждом может использоваться retain область.
Тестовый проект
Предлагаю в качестве примера использовать проект: Raspberry.RetainTest.ST.project .
В проекте объявлена область retain переменных с одной переменной n, которая инкрементируется каждую секунду. По истечении минуты вызывается скрипт рестарта сервиса Codesys, имитируя «мягкий» сбой. При этом в файл лога /var/opt/codesys/log.txt добавляется запись времени рестарта и значение retain переменной. По файлу лога можно оценить правильность работы с retain переменными и какое время такой проект может проработать без ошибок.
В этом проекте используется функция Restart(), которая выполняет команду (запуск скрипта):
1 |
sudo /var/opt/codesys/restart.sh |
Этот скрипт необходимо самостоятельно создать в папке /var/opt/codesys и назначить ему исполняемые права.
1 2 |
#!/bin/bash sudo systemctl restart codesyscontrol & |
Сразу скажу, метод управляемого рестарта можно использовать для обхода ограничения демо версии Codesys для Raspberry Pi (2 часа работы), если пауза в несколько секунд не повлияет на работу системы. Для этого нужно предусмотреть в алгоритме программы «нейтральное» безопасное состояние, в котором осуществлять перезагрузку и восстановление работы программы. Переход в это состояние выполнять по таймеру. Такая особенность работы существенно ограничит области применения и усложнит код, но позволяет рассматривать Raspberry Pi не только как обучающую платформу.
Приложение
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
program PLC_PRG var cnt: dint; cmd: string( 256 ); result: RTS_IEC_RESULT; t1, t2: ton; RetainControl: TRetainControl := ( SaveFilterTime := t#5s ); end_var var retain n: int := 0; end_var RetainControl( ResIEC => result, AttemptSaveCounter => cnt ); t1( in := not t1.q, pt := t#1s ); if t1.q then n := n + 1; end_if t2( in := not t2.q, pt := t#1m ); if t2.q then RetainControl( ForceSave := true ); cmd := 'echo `date "+%Y-%m-%d %H:%M:%S"` restart codesys, n='; cmd := concat( cmd, int_to_string(n) ); cmd := concat( cmd, ', savecnt=' ); cmd := concat( cmd, dint_to_string( RetainControl.AttemptSaveCounter ) ); cmd := concat( cmd, ' >> /var/opt/codesys/log.txt' ); SysProcessExecuteCommand( cmd, adr( result ) ); Restart(); end_if |
Спасибо за статьи про CodeSys на Raspberry. В русскоязычном интернете их очень мало.
Хотел задать вопрос вы не знаете как реализовать отправку email писем на малине из codesys c поддержкой ssl/tsl шифрования?
Таким не занимался. Если письмо можно отправить, используя shell, то используйте функцию вызова внешних программ SysProcessExecuteCommand().
Вячеслав, а у Вас рестарт процесса codesysa
sudo systemctl restart codesyscontrol &
сразу заработал? Мне пришлось скрипт переписать.
Я контролировал работу программы по файлу лога log.txt. Необходимо соблюсти все условия для срабатывания скрипта. Во-первых, разрешить выполнение внешних программ, во-вторых, установить исполняемые права на файл скрипта. Не забыть также перезагрузить RTS, чтобы произошли изменения конфигурации. Дальше периодически наблюдать за содержимым файла лога. Тестировал около дня.
А не могли ли Вы показать листинг своего скрипта /etc/init.d/codesyscontrol ?
Я только что проверил работу по инструкции выше на Codesys 3.5.13.10 с RTS 3.5.13.20. Перезагрузка происходит. Лог файл содержит следующий текст:
2018-12-12 15:05:04 restart codesys, n=45, savecnt=12
2018-12-12 15:06:11 restart codesys, n=91, savecnt=12
2018-12-12 15:07:13 restart codesys, n=137, savecnt=12
2018-12-12 15:08:15 restart codesys, n=183, savecnt=12
2018-12-12 15:09:17 restart codesys, n=228, savecnt=12
2018-12-12 15:10:19 restart codesys, n=273, savecnt=12
2018-12-12 15:11:21 restart codesys, n=319, savecnt=12
Вячеслав, с одним вопросом не поможете?
в логе codesyscontrol.log такие сообщения
1547254171, 0x00000002, 4, 16, 40, Retain restore from file failed: [Application]
1547254171, 0x00000002, 2, 1288, 49, Retain data are initialized now of [Application]
1547254177, 0x00000002, 1, 0, 2, Application [Application] loaded via [Bootproject]
поля лога такие
;
;Timestamp, CmpId, ClassId, ErrorId, InfoId, InfoText
;
Можно ли отловить это событие (event)? куда смотреть? CmpApp EventIDs смотрел, что то не нашел…. И что такое CmpId, ClassId, ErrorId, InfoId?
Разбором внешних файлов codesys не занимался. Нужно стараться обойтись внутренними документированными возможностями codesys. Отловить ошибку инициализации retain переменных можно и с помощью TRetainControl. Переменная ResIEC должна вернуть значение отличное от нуля в случае ошибки. В первом цикле можно попробовать прочитать несколько раз и если не получилось, то назначить принудительно безопасные значения, переведя систему в начальное состояние.
Спасибо за вашу работу. Мне очень помогло!