Во многих темах камрад Валенок постоянно говорит о том, что если мы хотим читать (или писать) много регистров подряд в CodeSys v2.3 по Modbus, то надо использовать модули STRING. Когда я про это читал, то я примерно понял о чём идёт речь - читать кучку байтов, и собирать из неё значения переменных.
На форуме эта мысль проскакивает в куче тем, но никто не даёт рабочего (или понятного) примера. В одной из тем я обещался написать пост на блоге, когда разберусь. До поста - далеко, поэтому пишу кратко на форум.
В чём особенность работы CodeSys v2.3?
Основная её особенность в том, что каждый элемент, который мы добавляем в Конфигурацию ПЛК, всегда опрашивается (читается/пишется) одним отдельным запросом.
Поэтому если мы для 8 каналов модуля аналоговых вводов добавим в конфигурацую ПЛК 8 значений FLOAT, и ещё и для статуса измерения канала добавим 8 WORD - то CodeSys будет забрасывать этот модуль 8 + 8 = 16 отдельными запросами.
Если поглядеть в спецификацию протокола ModBus RTU, то мы увидим, что на уровне байтиков, которые передаются по линии данных, то один запрос на чтение регистра занимает (ориентируюсь на эту статью из инета https://ipc2u.ru/articles/prostye-re.../modbus-rtu/):
* 1 Байт = Адрес устройства
* 1 Байт = Команда (шо делать)
* 2 Байта = Начальный адрес регистра (для чтения данных) - меняется только он
* 2 Байта = Сколько штук регистров читать
* 2 Байта = CRC (проверка верности данных)
То есть, на чтение одного регистра нам надо пихнуть в линию запрос длиной 1 + 1 + 2 + 2 + 2 = 8 байтов.
Ответ модуля на один наш запрос будет представлять такую кучу байтов:
* 1 Байт = Адрес устройства
* 1 Байт = Команда или код ошибки
* 1 Байт = Длина данных дальше - у нас будет равно 2, так как один регистр модуля = 2 байтам
* 2 Байта = Значение регистра, который мы читаем
* 2 Байта = CRC (проверка верности данных)
То есть, один ответ модуля с одним регистром займёт 1 + 1 + 1 + 2 + 2 = 7 байтов.
А теперь множим это на число регистров (16 штук): 16 * 8 + 16 * 7 = 128 + 112 = 240 курвичных байтов у нас уходит на то, чтобы обменяться инфой с модулем!
И байты-то ладно. А в линии ещё есть и разные паузы, и работает передача данных так (очень условно): Пауза - Запрос - Пауза - Ответ - Пауза. И эти паузы сжирают время, которое мы могли бы потратить на опрос других модулей.
Как народ выходит из проблемы? Ведь CodeSys v2.3 не поддерживает групповое чтение регистров?
В CodeSys v2.3 есть возможность читать байты (не регистры!) как STRING[79].
В конфигурацию ПЛК можно добавить "String input module" или "String output module", в котором указать начальный адрес регистра и число байт для чтения. Один регистр занимает два байта.
В этом случае такой STRING[] будет прочитан одним запросом Modbus целиком.
Ограничение этого STRING[] - в 79 байтов. Больше прочитать нельзя и в этом случае вам придётся разбивать ваши регистры на два или более STRING[].
Применить такой метод получится только если все регистры модуля IO идут подряд (или с небольшими пропусками): 0, 1, 2, 3, 4... и так далее.
Есть некоторые устройства (например датчики WirenBoard WB-MSW), которые имеют пропуски в карте регистров, условно: 0, 1... 3, 4, 5... 8, 9, 10, 11. Если такие устройства позволяют читать несуществующие регистры (WirnBoard - нет), то даже в этом случае можно запросить подряд регистры с 0 до 11 в моём примере.
Как рассчитывать длину байт и регистров?
Один регистр WORD/INT занимает 2 байта
Один регистр DWORD/LONG/FLOAT занимает 4 байта
Вот пример моего подсчёта для одного канала модуля аналоговых входов (МВ110-224.8А):
То есть, на один канал модуля МВ110-224.8А нам надо 12 байт.Код:Байт Назначение Порядок Байт 1 = Положение десятичной точки = (LSB) 2 = Положение десятичной точки = (HSB) 3 = Целое значение измерения = (LSB) 4 = Целое значение измерения = (HSB) 5 = Статус измерения канала = (LSB) 6 = Статус измерения канала = (HSB) 7 = Циклическое время измерения = (LSB) 8 = Циклическое время измерения = (HSB) 9 = Измеренное значение Float32 = Старшая часть (LSB) 10 = Измеренное значение Float32 = Старшая часть (HSB) 11 = Измеренное значение Float32 = Младшая часть (LSB) 12 = Измеренное значение Float32 = Младшая часть (HSB)
Значит, если мы хотим прочитать все 8 каналов этого модуля, нам понадобится 12 х 8 = 96 байт.
У нашего STRING[] имеется ограничение в 79 байт. Значит, в данном случае мы разделим наши каналы на два STRING[]
Я разделил так, чтобы один STRING[] был максимально заполнен: 6 каналов (72 байта) + 2 канала (24 байта).
К этим каналам STRING прямо в конфигурации ПЛК мы привязываем переменные, и дальше работаем с ними.
Как обрабатывать такие данные?
В приложенном примере я написал свою версию обработки с подробными комментариями.
Особенность моей версии в следующем. Из-за странных преобразований типов в CodeSys нельзя просто так взять и обращаться к переменной, которая приязана к такому каналу: она видится как строка, а нам нужны байты.
Народ на форуме делает ручную привязку, объявляя в коде программы массив байтов и привязывая его через команду AT %QB...
Мне этот способ не нравится, так как я не хочу следить за изменением адресов в конфигурации ПЛК, если я туда решу что-то добавить или изменить.
Поэтому я (по аналогии со старым добрым языком СИ) использовать копирование буферов в память при помощи функции SysMemCpy() из библиотеки SysMem. Я копирую заданный мне буфер в свой, и разбираю его побайтно, забирая столько байтов, сколько мне необходимо:
Далее я использую операции с битами (SHL, SHR) и побитные операторы (AND, OR) для того, чтобы склеить число из двух байтов.Код:VAR_INPUT pData : DWORD; (* Указатель (ADR) на переменную начала буфера данных *) pDataSize : DWORD; (* Максимальная длина данных от модуля - 12 байт *) END_VAR SysMemCpy(ADR(pBuffer), pData, pDataSize);
Например, чтобы получить WORD из двух байтов, я делаю так:
Младший байт (pBuffer[1]) идёт здесь первым, поэтому я его просто склеиваю через ORКод:CSParseMV8A.ValDigPoint := ((BYTE_TO_WORD(pBuffer[1]) OR SHL(BYTE_TO_WORD(pBuffer[2]), 8)));
Старший байт идёт здесь вторым, поэтому его надо сдвинуть влево на 8 бит (из 16#0012 превратить в 16#1200) при помощи оператора SHR и снова склеить с нужным нам числом.
Вот так склеивается Float32 (REAL):
Я завёл ещё один массив из 4 байтов, в который подставляю байты для сборки Float в нужном порядке (он указан в коде в комментариях), а потом при помощи SysMemCpy представляю этот массив как 4 байта памяти и копирую его в значение типа Float.Код:VAR pFloat32Val : ARRAY [1..4] OF BYTE; (* Буфер для сборки переменной типа Float32 побайтно = 4 байта *) END_VAR (* Теперь клеим Float32 по схеме из таблички выше *) (* Просто пихаем в наш буфер нужные байты в нужном порядке [3] [4] [1] [2] *) pFloat32Val[1] := pBuffer[11]; pFloat32Val[2] := pBuffer[12]; pFloat32Val[3] := pBuffer[9]; pFloat32Val[4] := pBuffer[10]; (* А теперь копируем этот кусочек памяти в нашу переменную REAL - длина = 4 байта (размер REAL) *) SysMemCpy(ADR(CSParseMV8A.ValFloat), ADR(pFloat32Val), 4);
Аналогичным образом я делаю запись в модуль аналогового вывода МУ110-224.6У.
Я разбираю число WORD (от 000,0% до 100,0% - от 0 до 1000) на два байта (старший и младший), а потом пихаю их в буфер из массва байтов.А уже этот буфер снова копирую в переменную, привязанную к каналу String Output module:
Для того, чтобы было удобно работать, я написал функции, которые всю работу делают за меня. Им надо только подпихнуть ссыку на переменную канала STRING[] и длину байт:Код:VAR (* Тест записи в каналы модуля МУ110-224.6У *) testBuffer : ARRAY [1..12] OF BYTE; (* Буфер, который мы будем передавать данные для каналов модуля *) testValueCh1 : WORD := 1000; (* 100,0% для канала *) testValueCh2 : WORD := 1000; testValueCh3 : WORD := 1000; END_VAR testBuffer[1] := WORD_TO_BYTE(testValueCh1 AND 16#00FF); (* Младший байт *) testBuffer[2] := WORD_TO_BYTE(SHR(testValueCh1 AND 16#FF00, 8)); (* Старший байт *) testBuffer[3] := WORD_TO_BYTE(testValueCh2 AND 16#00FF); (* Младший байт *) testBuffer[4] := WORD_TO_BYTE(SHR(testValueCh2 AND 16#FF00, 8)); (* Старший байт *) SysMemCpy(ADR(TestAQ), ADR(testBuffer), 12);
Кое-где моя логика несовершенна и немного накручена. Возможно, позже я что-то исправлю и переделаю более изящно.Код:(* Это для аналоговых входов *) (* Здесь каждый кусочек данных одного входв занимат 12 байт Поэтому номер канала можно вычислить, если домножать 12 на номер канала, считая с нуля НЕ забываем о том, что наши каналы разбиты на два куска STRING[]: с 1 по 6 и с 7 по 8! *) TestAIModule1 := CSParseMV8A(ADR(TestAI1) + (12 * 0), 12, FALSE); (* Канал 1 *) TestAIModule2 := CSParseMV8A(ADR(TestAI1) + (12 * 5), 12, FALSE); (* Канал 6 *) TestAIModule3 := CSParseMV8A(ADR(TestAI2) + (12 * 1), 12, FALSE); (* Канал 8 *) (* А тут ещё скучнее и проще: один модуль ввода параметров электросети = один кусок STRING[] *) TestPhaseL1 := CSParseME1Ch(ADR(testMEML1), 42); TestPhaseL2:= CSParseME1Ch(ADR(testMEML2), 42); TestPhaseL3:= CSParseME1Ch(ADR(testMEML3), 42);
Однако сейчас весь пример работает (что видно на скриншотах), и даже на отрицательных значениях Float32 парсится корректно.
Кому надо - пользуйтесь и дорабатывайте под себя!