Бывают случаи, когда необходимо получить некоторые данные в том же месте кода, где делается их запрос. В этом случае используется вызов функции, при котором происходит ожидание возврата вычисленного значения. Программа, вызывающая такую функцию, приостанавливает свою работу до получения результата. Такое исполнение кода, при котором возвращаемое значение функции определено на момент завершения её работы, называется синхронным.
В ПЛК для работы с внутренней и внешней периферией принято использовать функциональные блоки, результат работы которых определяется специальными внутренними флагами. Момент завершения работы такого блока зависит не от окончания вызова его тела, а от состояния флагов. Такое исполнение кода называется асинхронным. Использование синхронных и асинхронных функций может иметь свои плюсы и минусы в зависимости от контекста. Одним из минусов асинхронной работы является зависимость от времени цикла задачи. В качестве примера можно привести запрос по протоколу modbus rtu.
Представим, что у нас есть сложное многозадачное приложение. Входные данные для алгоритма управления берутся с внешнего удалённого модуля ввода. Мы назначаем задаче обмена по протоколу какое-то время цикла, а в настройках протокола — значение для времени ожидания ответа. Вопрос: Какое значение таймаута ожидания ответа установить?
Дело в том, что при выполнении асинхронного запроса проверка наличия ответа будет осуществляться через время цикла, которое может зависеть также от сложности приложения. Может случиться ситуация, когда какая-то программа или задача однократно в случайное время работает дольше, чем значение таймаута, либо очерёдность их выполнения приводит к превышению времени ожидания. В таком случае мы получим пропуск ответа и нестабильную работу последовательного канала связи. Эту проблему можно решить, увеличивая время таймаута и число повторов при ошибке обмена, но это сразу сказывается на время реакции алгоритма управления. Ещё можно использовать синхронные запросы.
Пример асинхронного запроса
В качестве тестового будем использовать проект Raspberry.ReadCoilsAsyncTest.ST.project из git репозитория codesys, доступного для скачивания в виде архива (кнопка ZIP).
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
program PLC_PRG var constant READ_COILS : byte := 16#01; READ_DISCRETE_INPUTS : byte := 16#02; READ_HOLDING_REGISTERS : byte := 16#03; READ_INPUT_REGISTERS : byte := 16#04; WRITE_SINGLE_COIL : byte := 16#05; WRITE_SINGLE_REGISTER : byte := 16#06; WRITE_MULTIPLE_COILS : byte := 16#0F; WRITE_MULTIPLE_REGISTERS : byte := 16#10; MODE_IDLE: byte := 0; MODE_CONNECT: byte := 1; MODE_SET_DATA: byte := 2; MODE_REQUEST: byte := 3; MODE_CLOSE: byte := 4; MODE_ERROR: byte := 5; // Номера портов для устройств /dev/ttyUSBn Raspberry Pi 3. TTYS0: byte := 1; TTYS1: byte := 2; TTYS2: byte := 3; TTYS3: byte := 4; TTYS4: byte := 5; end_var var xBusy, xDone, xError: bool; ex: byte; mode: uint := MODE_CONNECT; timeout: time; start, stop, t: ulint; tmpbuf: array [ 0 .. 255 ] of byte; result: RTS_IEC_RESULT; hCom: RTS_IEC_RESULT; t1: ton; ComSettings: COM_Settings := ( sPort := TTYS0, ulBaudrate := SYS_BR_19200, byParity := SYS_NOPARITY, byStopBits := SYS_ONESTOPBIT, ulBufferSize := 255 ); ReadCoils: ModbusRequest := ( uiFunctionCode := READ_COILS ); end_var // Перед использованием необходимо: // - обновить устройство; // - обновить библиотеки; // - настроить параметры последовательного порта; case mode of MODE_CONNECT: // Настройка параметров соединения. // Таймаут ожидания ответа. timeout := t#50ms; // Открываем порт. hCom := SysComOpen( ComSettings.sPort, adr( result ) ); // Устанавливаем параметры порта. result := SysComSetSettings( hCom, adr( ComSettings ), 0 ); t1( in := false ); mode := sel( result = Errors.ERR_OK, MODE_ERROR, MODE_SET_DATA ); MODE_SET_DATA: SysTimeRtcHighResGet( start ); mode := MODE_REQUEST; MODE_REQUEST: // Выполняем запрос. ReadCoils( xExecute := true, usiSlaveAddr := 1, uiReadOffset := 0, uiReadLen := 32, hComPort := hCom, tTimeout := timeout, pReadBuf := adr( tmpbuf ), xBusy => xBusy, xDone => xDone, xError => xError, byModbusErrorCode => ex ); // Пришёл ответ на запрос. if xDone then SysTimeRtcHighResGet( stop ); // Фактическое время выполнения запроса, [мсек]. t := stop - start; // Ошибка. elsif xError then case ex of MB_ErrorCodes.RESPONSE_SUCCESS: ; MB_ErrorCodes.RESPONSE_TIMEOUT: ; MB_ErrorCodes.RESPONSE_INVALID_DATA: ; MB_ErrorCodes.REQUEST_FAILED_TO_SEND: SysComClose( hCom ) ; // Коды ошибок Modbus. else ; end_case // Запрос выполняется. elsif xBusy then return; end_if t1( in := false ); mode := sel( ex = MB_ErrorCodes.REQUEST_FAILED_TO_SEND, MODE_IDLE, MODE_ERROR ); // Интервал между запросами. MODE_IDLE: t1( in := true, pt := t#1s ); if t1.q then ReadCoils( xExecute := false ); mode := MODE_SET_DATA; end_if // Ошибка. MODE_ERROR: t1( in := true, pt := t#1s ); if t1.q then mode := MODE_CONNECT; end_if end_case |
Результат работы под отладчиком: t = 40 мсек (время цикла 20 мсек).
Пример синхронного запроса
Преобразуем программу так, чтобы тело функционального блока вызывалось в цикле. Просто зациклить вызов блока нельзя, т.к. при этом возрастёт нагрузка на CPU. Нужно отдавать процессорное время на выполнение других задач. В этом нам поможет функция SchedWaitSleep() из системной библиотеки CmpSchedule. Она осуществляет задержку, не нагружая процессор.
Проект: Raspberry.ReadCoilsSyncTest.ST.project .
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 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
program PLC_PRG var constant READ_COILS : byte := 16#01; READ_DISCRETE_INPUTS : byte := 16#02; READ_HOLDING_REGISTERS : byte := 16#03; READ_INPUT_REGISTERS : byte := 16#04; WRITE_SINGLE_COIL : byte := 16#05; WRITE_SINGLE_REGISTER : byte := 16#06; WRITE_MULTIPLE_COILS : byte := 16#0F; WRITE_MULTIPLE_REGISTERS : byte := 16#10; MODE_IDLE: byte := 0; MODE_CONNECT: byte := 1; MODE_SET_DATA: byte := 2; MODE_REQUEST: byte := 3; MODE_CLOSE: byte := 4; MODE_ERROR: byte := 5; // Номера портов для устройств /dev/ttyUSBn Raspberry Pi 3. TTYS0: byte := 1; TTYS1: byte := 2; TTYS2: byte := 3; TTYS3: byte := 4; TTYS4: byte := 5; end_var var xBusy, xDone, xError: bool; ex: byte; mode: uint := MODE_CONNECT; timeout: time; start, stop, t: ulint; sleepus: ulint := 100; tmpbuf: array [ 0 .. 255 ] of byte; result: RTS_IEC_RESULT; hCom: RTS_IEC_RESULT; t1: ton; ComSettings: COM_Settings := ( sPort := TTYS0, ulBaudrate := SYS_BR_19200, byParity := SYS_NOPARITY, byStopBits := SYS_ONESTOPBIT, ulBufferSize := 255 ); ReadCoils: ModbusRequest := ( uiFunctionCode := READ_COILS ); end_var // Перед использованием необходимо: // - обновить устройство; // - обновить библиотеки; // - настроить параметры последовательного порта; case mode of MODE_CONNECT: // Настройка параметров соединения. // Таймаут ожидания ответа. timeout := t#50ms; // Открываем порт. hCom := SysComOpen( ComSettings.sPort, adr( result ) ); // Устанавливаем параметры порта. result := SysComSetSettings( hCom, adr( ComSettings ), 0 ); t1( in := false ); mode := sel( result = Errors.ERR_OK, MODE_ERROR, MODE_REQUEST ); MODE_REQUEST: SysTimeRtcHighResGet( start ); repeat // Выполняем запрос. ReadCoils( xExecute := true, usiSlaveAddr := 1, uiReadOffset := 0, uiReadLen := 32, hComPort := hCom, tTimeout := timeout, pReadBuf := adr( tmpbuf ), xBusy => xBusy, xDone => xDone, xError => xError, byModbusErrorCode => ex ); SchedWaitSleep( sleepus ); until xDone or xError end_repeat SysTimeRtcHighResGet( stop ); // Фактическое время выполнения запроса, [мсек]. t := stop - start; // Ошибка. if xError then case ex of MB_ErrorCodes.RESPONSE_SUCCESS: ; MB_ErrorCodes.RESPONSE_TIMEOUT: ; MB_ErrorCodes.RESPONSE_INVALID_DATA: ; MB_ErrorCodes.REQUEST_FAILED_TO_SEND: SysComClose( hCom ) ; end_case end_if t1( in := false ); mode := sel( ex = MB_ErrorCodes.REQUEST_FAILED_TO_SEND, MODE_IDLE, MODE_ERROR ); // Интервал между запросами. MODE_IDLE: t1( in := true, pt := t#1s ); if t1.q then ReadCoils( xExecute := false ); mode := MODE_REQUEST; end_if // Ошибка. MODE_ERROR: t1( in := true, pt := t#1s ); if t1.q then mode := MODE_CONNECT; end_if end_case |
Результат работы под отладчиком: t ~= 16 мсек (время цикла 20 мсек).
При асинхронном запросе интервал обмена включает целое число времени цикла задачи, при синхронном этот интервал определяется моментом чтения ответа из приёмного буфера, либо установленным таймаутом, т.е. не зависит от структуры приложения. Минусом в таком подходе является то, что при синхронном запросе приложение останавливает свою работу на гораздо большее время. Это можно заметить, наблюдая статистику в мониторе конфигуратора задач во время работы.
Теоретическое значение интервала обмена на скорости 19200 для функции 0x01 при заданных параметрах равно: ( 8 + 9 ) * 10 / 19200 ~= 9 мсек.
Комментарии: