Modbus RTU: архитектура протокола и пусконаладка сети RS-485


Обзор серии

Эта статья – часть серии. Ниже приведены ссылки на все статьи серии:
  1. UART, RS-485 и коммуникационные модули ПЛК
  2. Modbus RTU: архитектура и пусконаладка

Что содержится в статье ⌄
  • Архитектура Modbus RTU: взаимодействие Master-Slave и модель данных
  • Типы регистров Modbus: Coils, Discrete Inputs, Input и Holding Registers
  • Структура кадра Modbus RTU (ADU и PDU) и проверка контрольной суммы CRC
  • Смещение линии RS-485 (line biasing), интервалы тишины и терминальные резисторы
  • Параметры UART: скорость (baud rate), биты четности (parity) и стоп-биты
  • Настройка коммуникационного модуля RS-485 Siemens CM 1241 в TIA Portal
  • Написание конечного автомата (State Machine) на SCL для опроса Modbus (MB_COMM_LOAD, MB_MASTER)
  • Порядок байтов (Endianness) и Word Swap
  • Отладка Modbus-соединений с помощью Modbus Mechanic и анализ кодов ошибок ПЛК

В предыдущей статье мы рассмотрели передачу данных на физическом уровне – UART и последовательные линии данных (TTL/CMOS, RS232, RS485), а также обсудили роль Modbus в современном АСУ ТП.

Очень коротко пройдемся по основным тезисам:

  • Данные внутри чипа передаются по шине данных – по одному проводу на каждый бит одного байта;
  • Данные между устройствами передаются через последовательные линии, по одному биту за раз;
  • Конвертацией “параллельных” данных в “последовательные” и обратно занимается UART;
  • “Нативный” UART это напряжение TTL/CMOS – 3.3 / 5 В;
  • Для более устойчивой передачи данных (больше чем десятки сантиметров) в промышленных условиях используют RS485 – по-сути это “усилитель” сигнала.

1. Анатомия протокола Modbus RTU

Мы должны понимать, какие байты мы будем слать и получать. Это фундамент для отладки.

1.1. Модель взаимодействия: Master-Slave

Есть один “ведущий” (ПЛК), который инициирует запросы – Master и много “ведомых” (счетчики, датчики, другой ПЛК), которые отвечают – Slave.

1.2. Модель данных: регистры

В Modbus мы работаем с регистрами. Важно отметить, что это не “физические” регистры, которые мы обсуждали в контексте UART в прошлой статье.

Исторически, когда Modbus только придумали в 1979 году для контроллеров Modicon, данные хранились блоками по 16 бит. Так это слово и прижилось.

Для протокола Modbus вся память устройства выглядит как огромная таблица Excel, где в каждой строчке лежит число от 0 до 65535 (16 бит). Каждая такая строчка и называется “регистром”.

Разработчики прошивки устройства сами делают “карту регистров”. Например, они пишут в коде: “если по Modbus придет запрос на чтение регистра № 100, возьми значение переменной temperature из оперативной памяти и отправь его”.

Можно было бы провести аналогию с Address Bus и Data Bus, но они работают внутри конкретного устройства, а Modbus – снаружи, между разными устройствами.

Жизненный цикл запроса
  1. ПЛК (Master) посылает по кабелю команду датчику (Slave): “Эй, Modbus-устройство №5, дай мне значение из твоего Modbus-регистра №100”.
  2. Микроконтроллер внутри датчика принимает этот пакет из кабеля.
  3. Прошивка микроконтроллера расшифровывает запрос и понимает: “Ага, он просит регистр №100. Я знаю, что под этим номером у меня лежат данные о давлении”.
  4. В этот момент микроконтроллер выставляет физический адрес памяти, где лежит переменная “давление”, на свою внутреннюю Address Bus.
  5. Память (RAM) отдает значение давления по внутренней Data Bus обратно в процессор.
  6. Процессор берет это значение, упаковывает его в Modbus-ответ и отправляет по кабелю обратно компьютеру.

То есть когда мы читаем документацию к какому-нибудь частотнику или датчику и видим фразу: “скорость вращения находится в регистре 40105”, это просто означает: “чтобы узнать скорость, отправь по сети запрос на адрес 40105. Я сам пойму, откуда из своих внутренностей достать эту информацию, и пришлю тебе 16-битное число”.

1.3. Модель данных: типы регистров

“Таблица с регистрами” не одна. Создатели Modbus поделили всю память устройства на четыре логические таблицы. Они делятся всего по двум признакам:

  • Размер данных: это 1 бит (вкл/выкл) или 16 бит (число)?
  • Права доступа: это только для чтения (Read Only) или для чтения и записи (Read/Write)?

Важное правило: права доступа всегда рассматриваются со стороны Мастера (компьютера/ПЛК). То есть “только для чтения” значит, что Мастер может только прочитать это, но не может изменить.

Шпаргалка:

Доступ1 бит (вкл/выкл)16 бит (числа)
Только чтение (Read-Only)Discrete Inputs (датчик двери)Input Registers (датчик температуры)
Чтение и Запись (Read/Write)Coils (реле лампочки)Holding Registers (заданная скорость)

Подробно про каждый тип регистра:

01. Coils

Основные свойства регистра:

ПараметрЗначение
Размер1 бит (0 или 1 / False или True)
ДоступЧтение и Запись (Read / Write)
Что это по сутиКоманды управления Вкл/Выкл

До внедрения ПЛК логика управления реализовывалась аппаратно – на базе электромагнитных реле. У реле есть физическая катушка (coil). Подал ток на катушку – реле щелкнуло, мотор запустился.

Пример: мы отправляем по Modbus в Coil №5 значение 1, и на устройстве загорается лампочка, открывается клапан или запускается вентилятор. Мы также можем прочитать этот Coil, чтобы узнать, включена ли сейчас лампочка.

До появления ПЛК “изменить логику” означало перепаивать, перекручивать провода физически, а не “менять программу в ПЛК”.

Вот так выглядел шкаф управления лифтом – в прошлом веке; на видео видно, как щелкают электромагнитные реле: https://www.youtube.com/watch?v=WT1yfXAs548.

02. Discrete Inputs

Основные свойства регистра:

ПараметрЗначение
Размер1 бит (0 или 1 / False или True)
ДоступТолько чтение (Read Only)
Что это по сутиСостояние физических кнопок, датчиков открытия, концевиков

Пример: к контроллеру подключен датчик открытия двери. Если дверь открыта, в Discrete Input №10 будет лежать 1. Если закрыта – 0. Мы (Мастер) можем только спросить “дверь открыта?”.

Мы не можем отправить команду “сделай дверь закрытой” в этот регистр, потому что это просто физический датчик положения.

03. Input Registers

Основные свойства регистра:

ПараметрЗначение
Размер16 бит (числа от 0 до 65535)
ДоступТолько чтение (Read Only)
Что это по сутиТекущие показания аналоговых датчиков (температура, давление, напряжение).

Пример: у нас есть датчик температуры. Он измеряет температуру в комнате и кладет значение 245 (что означает 24.5 °C) в Input Register №20.

Компьютер регулярно читает этот регистр, чтобы рисовать график. Опять же, компьютер не может записать туда 300 – от того, что мы запишем в датчик число, в комнате теплее не станет. Датчик сам обновляет это значение.

04. Holding Registers

Основные свойства регистра:

ПараметрЗначение
Размер16 бит (числа от 0 до 65535)
ДоступЧтение и Запись (Read / Write)
Что это по сутиНастройки устройства, уставки, параметры, переменные.

Почему “Holding”?

Слово hold значит “удерживать/хранить”. Это память, которая хранит конфигурацию устройства. Это самый популярный тип данных в современном Modbus.

Зачастую производители ленятся и вообще все (и датчики, и реле) запихивают в Holding Registers, чтобы не плодить сущности.

Пример: мы хотим, чтобы частотный преобразователь крутил мотор со скоростью 1500 оборотов. Мы записываем число 1500 в Holding Register №100. Устройство “сохраняет” это число у себя в памяти и начинает крутить мотор с этой скоростью.

Мы в любой момент можем прочитать этот регистр, чтобы вспомнить, какая скорость задана.

1.3. Варианты Modbus и уровни представления данных

В терминологии сетей есть два важных понятия: PDU и ADU. Это классическая “матрешка”:

  • PDU (Protocol Data Unit) – это сама коробка с вещью, суть нашего послания.
  • ADU (Application Data Unit) – это почтовый пакет, в который упакована коробка: на нем написан адрес получателя. Вся эта посылка целиком летит по проводам (это полный кадр Modbus, но все еще не Application уровень по OSI).

В Modbus существует несколько вариантов передачи данных, наиболее распространенные:

  • Modbus RTU: RTU = Remote Terminal Unit; часто можно увидеть “RTU = последовательная связь”. Но это не совсем точно; Modbus RTU это протокол, который обычно работает поверх последовательных линий связи.
  • Modbus TCP: вариант Modbus, работает поверх TCP.

Modbus TCP имеет другую структуру кадра (ADU), но внутренняя часть сообщения (PDU – функция и данные) остается одинаковой как для RTU, так и для TCP.

1.4. Структура кадра Modbus RTU (ADU)

На текущем этапе мы сосредоточимся на Modbus RTU и взаимодействии по последовательным линиям связи. Вопросы, связанные с Modbus TCP и передачей данных поверх Ethernet, будут рассмотрены отдельно.

Вот как выглядит этот “почтовый пакет” (ADU) и данные (PDU) в Modbus RTU:

Image

Подробно про каждое поле кадра:

01. Адрес (Slave Address): 1 байт

Это номер устройства на линии RS-485, к которому обращается Master.

Доступны адреса от 1 до 247.

Если Master ставит сюда 0, это Broadcast (широковещательный запрос) – команду услышат и выполнят все устройства в сети, но никто не будет отвечать (чтобы не создать “кашу” на линии).

02. Код функции (Function Code): 1 байт

Это инструкция: что именно нужно сделать.

Вспоминаем разговор про Coils, Holding Registers. Именно здесь Master говорит, с какой таблицей он хочет работать:

  • 0x01 – Прочитать Coils (Read Coils);
  • 0x02 – Прочитать Discrete Inputs;
  • 0x03 – Прочитать Holding Registers (Самая частая команда!);
  • 0x04 – Прочитать Input Registers;
  • 0x05 – Записать один Coil (включить/выключить реле);
  • 0x06 – Записать один Holding Register (задать уставку);
  • 0x10 – Записать сразу несколько Holding Registers.
03. Данные (Data): от 0 до 252 байт

Здесь лежат детали запроса. Содержимое этого блока зависит от того, какой код функции мы использовали.

Если мы запросили чтение Holding Registers, то в данных мы обязаны уточнить:

  • С какого адреса начинать читать (например, с регистра №100);
  • Сколько штук регистров прочитать (например, 2 штуки);
  • Если это ответ от датчика, то в поле “данные” будут лежать сами значения температуры или давления.
04. Контрольная сумма CRC: 2 байта

Сети RS-485 часто прокладывают на заводах рядом с мощными моторами, частотниками и генераторами помех. В кабель может “ударить” электромагнитная наводка, и какой-нибудь ноль превратится в единицу.

Перед отправкой устройство прогоняет весь кадр через хитрую математическую формулу, получает 2 байта результата CRC (Cyclic Redundancy Check), и приклеивает их в конец.

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

1.5. Настройки RS-485 в протоколе Modbus

Разберем живой пример, когда Master просит устройство с адресом 1 прислать ему значение из Holding Register №0:

01 03 00 00 00 02 84 0A

Где:

  • 01 – Устройство с адресом №1.
  • 03 – Код функции 3: “Прочитай мне Holding Register”.
  • 00 00 – “Начни читать с регистра под номером 0” (два байта под номер регистра, потому что адресов может быть до 65535).
  • 00 01 – “Прочитай мне ровно 1 регистр”.
  • 84 0A – Это контрольная сумма CRC.

Если мы посмотрим на кадры выше, то увидим, что там нет байта “СТАРТ” и нет байта “СТОП”. Как микроконтроллер понимает, что байт, который сейчас летит по проводу – это первый байт (адрес), а не кусок данных из середины?

В Modbus RTU все строится на тишине.

По стандарту, устройства обязаны слушать линию. Если на линии ничего не происходит (тишина) в течение времени, равного передаче 3.5 символов (при скорости 9600 бит/с это около 4 миллисекунд), устройство обнуляет свои буферы и говорит себе: “Прошла долгая пауза. Значит, следующий байт, который прилетит, 100% будет началом нового кадра (адресом)”.

Итак, в RS-485 у нас есть два провода: A и B. Когда Master или Slave что-то передают, они жестко подают напряжение на эти провода: то на A больше, то на B больше. Так получаются единицы и нули.

Но что происходит в момент той самой “тишины”?

Смещение линии в состоянии покоя (B > A)

Когда на линии RS-485 отсутствует передача, все узлы переводят свои передатчики в высокоимпедансное состояние (режим приема, когда выход устройства как будто отключен от цепи).

В результате линия оказывается неактивной и без принудительного задания логического уровня.

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

Помехи могут вызывать случайные изменения дифференциального напряжения между линиями A и B.

При определенных условиях это может быть интерпретировано приемником как начало передачи (старт-бит UART; мы говорили об этом в прошлой статье), что приводит к приему некорректных данных.

Для предотвращения этого применяется схема fail-safe смещения (biasing), формирующая определенное состояние линии в режиме покоя.

Схема смещения реализуется с помощью резисторов:

  • Подтягивающего резистора (pull-up), подключенного к линии B и источнику питания;
  • Подтягивающего резистора (pull-down), подключенного к линии A и земле.

В результате в состоянии покоя обеспечивается устойчивое дифференциальное напряжение (B > A), соответствующее логической “1”.

Это гарантирует, что при отсутствии передачи линия остается в определенном состоянии и устойчива к воздействию помех.

Смещение линии (Bias) должно быть включено только в одном месте на всей длинной линии RS-485 (иначе резисторы разных устройств будут конфликтовать и “посадят” линию).

Обычно это смещение включают на Master (на нашей Communication Module, ПЛК или компьютере). На всех остальных устройствах (слейвах, датчиках, частотниках) смещение должно быть выключено (None) или его там аппаратно нет.

В прошлой статье мы разобрали транспортный уровень, на котором работает Modbus RTU – это непосредственно UART.

На время забываем про Modbus и спускаемся на уровень UART. Предположим, нам нужно отправить байт.

UART является асинхронным интерфейсом, то есть без общего тактового сигнала (clock).

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

[Start bit] + [8 бит данных] + [Parity bit] + [Stop bit]

Рассмотрим бит четности и стоповые биты подробнее:

Parity bit (бит четности)

Это примитивная защита от ошибок для конкретного байта (не путать с CRC, который защищает весь кадр).

Работает это просто: система считает, сколько единиц в наших 8 битах данных.

Режимы:

  • Even (четность): Система добавляет в конец 0 или 1 так, чтобы общее количество единиц в посылке стало четным (2, 4, 6, 8).
    • Пример: Мы шлем байт 10110001. Тут четыре единицы. Четыре – число четное. Значит, бит четности будет 0.
    • Пример 2: Мы шлем 10110000. Тут три единицы (нечетное). Значит, бит четности будет 1, чтобы в сумме стало четыре единицы.
  • Odd (нечетность): То же самое, только система делает так, чтобы сумма единиц всегда была нечетной.
  • None (Нет четности): Бит четности вообще не отправляется. Это самый частый вариант сегодня.

Зачем это нужно?

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

Stop bits (cтоповые биты)

Это сигнал окончания байта. После того как данные (и бит четности) переданы, передатчик обязан удерживать линию в состоянии “тишины” определенное время.

Это время измеряется в битах. Стоповых битов может быть 1 или 2. Это дает принимающему устройству микросекунды на то, чтобы переварить принятый байт и подготовиться к следующему (Start bit).

1 стоп-бит (самый частый вариант): ... DATA → 1:

  • Стандарт: 8-N-1 (8 data, No parity, 1 stop);
  • Быстрее (меньше накладных битов);
  • Используется очень часто.

2 стоп-бита: ... DATA → 1 1:

  • Дает больше времени приемнику;
  • Полезно при низком качестве линии;
  • Старом железе;
  • Длинных кабелях.

2. Подготовка к работе: схема подключения

В предыдущей статье мы собрали стенд АСУ ТП на основе ПЛК Siemens S7 1214C PLC, модуля расширения Siemens CM 1241 (RS-485) и счетчика электроэнергии SinoTimer DDS529MR с поддержкой измерения частоты, напряжения и тока:

Целевая схема подключения выглядит следующим образом:

Image

Помним, что правило маркировки часто нарушается, поэтому нужно проверять опытным путем:

  • Подключили A→T/R+ и B→T/R- – работает.
  • Если не работает – пробуем наоборот: A→T/R- и B→T/R+.

Линия RS‑485 – это дифференцированный сигнал, то есть данные передаются как разница потенциалов между проводами A и B. Сигнал, доходя до конца линии – отражается обратно, создает искажения и приводит к ошибкам.

Ситуация решается установкой терминального резистора на конце линии. Резистор 120 Ω на последнем устройстве “гасит” эти отражения и стабилизирует передачу. Подключаем резистор R1 параллельно на клеммы A и B счетчика электроэнергии.

Но в промышленной среде – это не просто “резистор 120 Ω между A и B”.

Промышленные терминаторы RS-485

В промышленности применяются готовые терминаторы или устройства с встроенным согласованием линии – это либо отдельные модули, либо разъемы с встроенной схемой согласования.

Помимо самого резистора, они часто включают цепи смещения (bias), предотвращающие “плавающий” сигнал при отсутствии передачи – про них мы говорили чуть выше. Важно помнить, что bias должен включен только на одной стороне.

Также важный момент – удобство эксплуатации: терминатор можно включить или отключить переключателем, не разбирая проводку. Это критично при наладке и диагностике.

Такие решения делают линию:

  • Устойчивой к отражениям;
  • Предсказуемой в состоянии покоя;
  • Удобной в обслуживании.

Примеры промышленных терминаторов:

Wiren Board – WB-T120

Самый компактный и дешевый вариант. Просто резистор 120 Ω в маленьком корпусе с клеммником. Устанавливается на концах линии RS-485, очень компактный: 12×17×35 мм. Без DIN-рейки, просто висит на проводах:

Image

Phoenix Contact – PSI-TERMINATOR-PB-TBUS

Уже полноценный промышленный модуль. Активный терминатор для PROFIBUS/RS-485 с резервированием питания, гальванической развязкой и установкой на DIN-рейку. Поддерживает переключаемую терминацию и согласование линии:

Image

Siemens – 6ES7972-0DA00-0AA0

Терминирующий резистор для сетей PROFIBUS/MPI (MPI – фирменная сеть от Siemens) – разъем для PROFIBUS с терминацией прямо на коннекторе. Классика для сименсовских систем:

Image

Важно, что данный модуль работает в составе системы ET 200S (Remote I/O):

[ПЛК S7 в центральном шкафу]
        |
        | PROFIBUS кабель (тот же RS-485)
        |
[ET 200S станция у станка №1]   ← Система "ET 200S"
  [IM] [DI] [DO] [AI] [TERM]
        |
        | продолжение PROFIBUS
        |
[ET 200S станция у станка №2]
  [IM] [DI] [DO] [TERM]

3. Валидация счетчика с помощью ПК

Начнем с того, что подключим счетчик (Slave) к компьютеру (Master), чтобы проверить работоспособность Modbus-интерфейса счетчика.

Нам понадобится “свисток” USB → RS-485, а также два провода; скручиваем провода – получается импровизированная витая пара; подключаем провода к свистку:

Image

Подключаем другой конец провода к клеммам A и B счетчика; резистор 120 Ω на данном этапе не требуется:

Image

Получается такой сетап:

Image

Заходим в настройки счетчика долгим нажатием на кнопку SET, обращаем внимание на:

  • Ad 001: адрес данного устройства №1;
  • Bd 9600: baud rate 9600 бод/с;
  • Prty: None: четность отключена.

Вот так это выглядит вживую:

Каждый из параметров можно настроить при необходимости.

Важно: Ptry: 0 на самом деле значит “Parity Odd”, а не “Parity None”. А “перевернутая подкова” как на третьем фото выше – “Parity None”.

А теперь посмотрим в datasheet

Это страница из datasheet счетчика электроэнергии SinoTimer DDS529MR.

Видим ту же структуру ADU, что мы разбирали выше!

Кроме этого, видно, что напряжение представлено в виде float, которое хранится в Input Registers (код функции 04), и занимает 2 регистра (2 слова/регистра, то есть 4 байта).

Image

Далее запускаем программу Modbus Mechanic: https://modbusmechanic.scifidryer.com/.

Открываем меню → Tools → RTU Scanner, в открывшимся окне:

  • Baud: 9600
  • Data bits: 8
  • Stop bits: 1
  • Parity: None
  • Scan timeout: 1000 ms (с большим запасом, чтобы счетчик успел ответить)
  • Function code: Read Input Registers
  • Scan register: 0.

Далее запускаем “Scan” и видим, что нам удалось достучаться до счетчика! Ожидаемо, он находится под Slave ID = 1.

Image

RTU Scanner очень полезен для:

  • Обнаружения устройств в линии
    • Он пробегает адреса (обычно 1-247) и показывает кто из Slave “жив”
  • Диагностики связи
    • Проверяет корректность настроек: baud rate, parity, stop bits
    • Если устройство не отвечает – сразу ясно: проблема в линии или в параметрах.

Открываем главное окно Modbus Mechanic, выставляем те же настройки, что и для сканера, и в дополнение:

  • Data value type: Float
  • Активируем Word Swap (детальнее об этой настройке мы поговорим чуть позже)

Нажимаем на “Transmit Packet” и видим… 228 В! Реальное напряжение в электросети.

Image

4. Настройка подключения к ПЛК

4.1. Физическое подключение коммуникационного модуля к ПК

Для проверки работоспособности модуля RS-485 подключим его к Slave-серверу Modbus RTU на ПК.

На модуле Siemens CM 1241 стоит разъем DB-9:

Image

Почему не два контакта? Есть ряд причин:

  • Один разъем подходит и для RS-232, и для RS-485;
  • Есть не только A/B, но и земля, экран и доп. сигналы;
  • Экранирование от помех;
  • Крепится винтами → не вываливается;
  • Легко подключать стандартные кабели и адаптеры.

Нам понадобится вилка DB-9 (male) с клеммной колодкой:

Image

Распиновка модуля Siemens CM 1241:

Image

Берем свисток, который мы собрали для проверки работы Modbus-интерфейса счетчика электроэнергии, и подключаем другой конец провода на пины 8 и 3 (согласно распиновке выше).

Соединяем модуль связи RS-485 и ПК получившимся кабелем:

Image

4.2. Конфигурация коммуникационного модуля (CM 1241)

На данном этапе нам необходимо проверить работоспособность коммуникационного модуля; мы подключим его к ПК и запустим эмулятор Modbus RTU Slave через программу Modbus Mechanic.

Открываем TIA Portal, заходим в Device Configuration → Catalog → Controllers → SIMATIC S7-1200 → Communications modules → Point-to-point → CM 1241 → 6ES7 241-1CH32-0XB0.

Пристыковываем ее слева к ПЛК:

Image

Все устройства на шине RS-485 должны иметь согласованные параметры последовательного интерфейса (baud rate, формат кадра: число бит данных, четность, стоп-биты), обеспечивающие корректный прием данных.

Переходим в настройки платы: нажимаем на модуль правой кнопкой → Properties → Port configuration и выставляем настройки в соответствии с выставленными ранее настройками счетчика электроэнергии:

  • Operating mode: Half-duplex (RS485) 2-wire operation
    • Half-duplex значит, что в один момент времени или передача, или прием.
  • Receive line initial state: Bias with R(B)>R(a)>=0V
    • Об этом мы подробно поговорили выше – включаем смещение линии в режиме покоя для устойчивости к помехам.
  • Parity: No parity, stop bits: 1
    • Четность, а также стоп-биты мы также обсудили.

Кроме этого, в настройках модуля на вкладке “System constants” есть параметр “Hardware identifier” – это идентификатор платы, число, например “269”; это нам понадобится для настройки подключения.

4.3. Блок данных и state-машина

Создаем новый Data Block для хранения данных, прочитанных по Modbus:

  1. Называем его MB_DATA (Modbus data);
  2. В блоке Static создаем новую переменную Value типа Real; сюда попадет значение из тестового регистра Modbus RTU Slave, запушенного на ПК.

Теперь создаем новый Function Block → называем его MB_READ (Modbus read) → выбираем язык SCL. Переносим блок в собственный Network в Ladder Logic.

Modbus можно настроить и из Ladder Logic, но именно с помощью SCL лучше всего написать state-машину, с помощью которой можно удобно читать несколько регистров / устройств, а также настраивать тайминги.

Создаем новые переменные в блоке Static:

  • myCommLoad типа MB_COMM_LOAD – здесь будут параметры подключения;
  • myMaster типа MB_MASTER – с помощью этого блока мы будем проводить Modbus-операции;
  • pauseTimer типа TON_TIME – таймер для паузы между итерациями опроса;
  • isInitialized типа Bool – флаг инициализации; подключение нужно инициализировать один раз при запуске;
  • initState типа Int – инициализировать MB_COMM_LOAD нужно фронтом, это локальная state-machine для init;
  • initReq типа Bool – фронтовой сигнал инициализации;
  • workState типа Int – state-машина для основной логики;
  • modbusReq типа Bool – фронтовой сигнал чтения;
  • lastErrorStatus типа Word – контейнер для кода последней ошибки.
State-машина для чтения Modbus

При запуске один раз проводим инициализацию myCommLoad; порт – тот самый “Hardware identifier” нашего модуля RS-485.

Также можно указать Alias – TIA Portal сам предложит список доступных опций.

Для инициализации у нас есть небольшая state-машина initState:

  • Начинаем со state = 0, где выставляем сигнал инициализации в TRUE;
  • Переходим в state = 1, гасим сигнал инициализации, формируя его фронт, и читаем сигналы DONE и ERROR по готовности;
  • В случае ошибки инициализации записываем код ошибки в контейнер и блокируем выполнение;
  • В случае успешной инициализации запускаем state-машину для основной логики.

State-машина основной логики:

  • state = 0: выставляем сигнал на чтение если интерфейс не BUSY; сразу переходим на state = 1;
  • state = 1: гасим сигнал на чтение, формируя его фронт, и читаем сигналы DONE и ERROR по готовности;
  • В случае ошибки инициализации записываем код ошибки в контейнер и переходим в обработчик ошибок на state = 99;
  • В случае успеха переходим на state = 2;
  • state = 2: запускаем таймер на паузу между запросами;
  • Дожидаемся срабатывания таймера, делаем его сброс и возвращаемся на state = 0.

Вопрос 1: Если номер функции 04 (Read Input Register), то почему DATA_ADDR = 30001?

  • Адрес 30001 решает сразу две задачи: он определяет код функции (04) и вычисляет адрес на проводе (00).
  • Первая цифра в адресе 30001 (то есть тройка) не отправляется в кабель. Это внутренняя команда для самого блока ПЛК.

Для ПЛК:

  • Адрес 0xxxx → ПЛК использует функцию 01 или 05.
  • Адрес 1xxxx → ПЛК использует функцию 02.
  • Адрес 3xxxx → ПЛК использует функцию 04.
  • Адрес 4xxxx → ПЛК использует функцию 03 или 06.

Вопрос 2: Если стартовый номер регистра 00, то почему адрес заканчивается на 0001?

  • Это исторический конфликт между физикой и софтом:
  • Разработчики микроконтроллеров (и сам протокол Modbus на уровне проводов) всегда начинают считать с нуля; для них первый регистр в памяти – это регистр 0000, второй – 0001 и т.д.
  • Инженеры-электрики (для которых создавали первые ПЛК) привыкли считать с единицы (первый мотор, первое реле, первый регистр).
  • Поэтому в документации (и в настройках блоков ПЛК) первый регистр обозначается как 1 (то есть 30001 для функции 04).

Полный SCL-код state-машины:

// Initialization
IF NOT #isInitialized THEN
    CASE #initState OF
        0: // Send request
            #initReq := TRUE; // Explicit trigger set
            #initState := 1;
            
        1: // Wait response
            #initReq := FALSE; // Explicit trigger reset
            
            IF #myCommLoad.DONE THEN
                #isInitialized := TRUE;
                #workState := 0;
            ELSIF #myCommLoad.ERROR THEN
                #lastErrorStatus := #myCommLoad.STATUS;
                #initState := 999;
            END_IF;
            
        999: // Emergency stop
            ; // Block forever
    END_CASE;
    
    // Initialization call
    #myCommLoad(REQ := #initReq,
                "PORT" := "Local~CM_1241_(RS422_485)_1",
                BAUD := 9600,
                PARITY := 0,
                MB_DB := #myMaster);
    
    RETURN; // Don't go further until initialized
END_IF;

// State machine (logic)
CASE #workState OF
    0: // Send request when not BUSY
        IF NOT #myMaster.BUSY THEN
            #modbusReq := TRUE;
            #workState := 1;
        END_IF;
        
    1: // Wait response
        #modbusReq := FALSE;
        
        IF #myMaster.DONE THEN
            #workState := 2;
        ELSIF #myMaster.ERROR THEN
            #lastErrorStatus := #myMaster.STATUS;
            #workState := 99;
        END_IF;
        
    2: // Pause between requests
        #pauseTimer(IN := TRUE,
                    PT := T#2S);
        IF #pauseTimer.Q THEN
            #pauseTimer(IN := FALSE,
                        PT := T#2S);
            #workState := 0;
            #lastErrorStatus := 0;
        END_IF;
        
    99: // Error handling
        #workState := 2;
END_CASE;

// Request when needed
#myMaster(REQ := #modbusReq,
          MB_ADDR := 1,
          MODE := 0,
          DATA_ADDR := 30001,
          DATA_LEN := 2,
          DATA_PTR := "MB_DATA".Value);

4.3. Запуск Slave-сервера на ПК

Запускаем программу Modbus Mechanic: https://modbusmechanic.scifidryer.com/

Открываем меню → Tools → Start Slave Simulator, в открывшимся окне:

  • Register type: Input
  • Register number: 0
  • Data type: Float
  • Value: 220.7
  • Включаем Word Swap

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

Но что такое Word Swap?

Порядок байтов (endianness)

Классический регистр Modbus вмещает 16 бит (2 байта).

Но что, если нам нужно передать float или большое число, которые весят 32 бита (4 байта)? В один регистр они не влезут.

Решение – разбить большое 32-битное число на две части и положить в два соседних 16-битных регистра (например, в 40100 и 40101).

Сложность в том, что стандарт Modbus (написанный в 1979 году) вообще никак не регламентирует, в каком порядке нужно передавать эти два регистра и байты внутри них, поэтому каждый производитель железа делает так, как удобно его процессору.

Представим наше большое 32-битное число в виде 4 байтов (букв): A B C D, где A – это самая старшая (значимая) часть числа, а D – самая младшая.

Вот как разные системы могут отправить эти 4 байта в линию:

Тип порядкаПорядок байтКак выглядит в регистрах
Big-endianA B C DРегистр 1: [A B], регистр 2: [C D]
Little-endianD C B AРегистр 1: [D C], регистр 2: [B A]
Word-swappedC D A BРегистр 1: [C D], регистр 2: [A B]

Big-endian (прямой порядок) – читается слева направо, как мы привыкли в жизни. Самый старший байт идет первым. В сетевых технологиях (интернет-протоколы) это стандарт.

Little-endian (обратный порядок) – все вывернуто наизнанку. Младший байт идет первым. Почему так странно? Потому что именно так работает архитектура процессоров Intel (x86) в наших компьютерах. Им так быстрее производить математические вычисления в памяти.

Word-swapped (Перестановка слов / Средний порядок) – самая частая боль. Байты внутри регистра стоят правильно (по-человечески), но сами регистры (слова) поменялись местами.

Почему Word-swapped встречается в Modbus чаще всего?

  • Сам по себе стандарт Modbus требует передавать 16-битные регистры старшим байтом вперед (Big-endian). Датчик так и делает: он честно посылает первую половинку числа, потом вторую.
  • Но принимает эти данные ПЛК или компьютер со SCADA-системой (а там обычно архитектура Little-endian). ПК берет первый пришедший регистр и кладет его в младшую часть своей памяти, а второй – в старшую. В итоге две половины числа меняются местами (Word Swap).

Бывает еще один редкий вариант – Byte-swapped B A D C, когда регистры на своих местах, а байты внутри них перевернуты.

Выставляем параметры и нажимаем на “Add”:

Image

Возвращаемся в TIA Portal, компилируем программу и запускаем выполнение. Переходим в режим “Go Online” → открываем MB_DATA → нажимаем “Monitor all” и видим заветные 220.7!

Image

Теперь подключаем счетчик электроэнергии к модулю RS-485.

Находим номера регистров в документации, после чего переводим их в формат для ПЛК; мы используем функцию 04 – то есть чтение из Input Register:

ПараметрDATA_ADDROffsetРегистрыНазначение
Voltage3000100–1Напряжение
Current3000988–9Ток
Active Power300191818–19Активная мощность
Frequency300555454–55Частота

Добавляем оставшиеся блоки в state-машину, а также отделяем данные от логики.

Обновленный SCL-код state-машины

Добавляем еще две переменные:

  • currentIndex типа Int – для отслеживания Modbus регистра, из которого происходит чтение;
  • lastErrorIndex типа Int со стандартным значением -1 – для отслеживания регистра, при чтении которого возникла ошибка.

Полный SCL-код:

// Initialization
IF NOT #isInitialized THEN
    CASE #initState OF
        0:
            #initReq := TRUE; // Explicit trigger set
            #initState := 1;
            
        1: // Wait response
            #initReq := FALSE; // Explicit trigger reset
            
            IF #myCommLoad.DONE THEN
                #isInitialized := TRUE;
                #workState := 0;
            ELSIF #myCommLoad.ERROR THEN
                #lastErrorStatus := #myCommLoad.STATUS;
                #initState := 999;
            END_IF;
            
        999: // Emergency stop
            ; // Block forever
    END_CASE;
    
    // Initialization call
    #myCommLoad(REQ := #initReq,
                "PORT" := "Local~CM_1241_(RS422_485)_1",
                BAUD := 9600,
                PARITY := 0,
                MB_DB := #myMaster);
    
    RETURN; // Don't go further until initialized
END_IF;

// State machine (logic)
CASE #workState OF
    0: // Send request when not BUSY
        IF NOT #myMaster.BUSY THEN
            #modbusReq := TRUE;
            #workState := 1;
        END_IF;
        
    1: // Wait response
        #modbusReq := FALSE;
        
        IF #myMaster.DONE THEN
            #workState := 2;
        ELSIF #myMaster.ERROR THEN
            #lastErrorStatus := #myMaster.STATUS;
            #lastErrorIndex := #currentIndex;
            #workState := 99;
        END_IF;
        
    2: // Handle success
        #currentIndex := #currentIndex + 1;
        IF #currentIndex > 3 THEN
            #currentIndex := 0;
            #workState := 3;
        ELSE
            #workState := 0;
        END_IF;
        
    3: // Pause between requests
        #pauseTimer(IN := TRUE,
                    PT := T#2S);
        IF #pauseTimer.Q THEN
            #pauseTimer(IN := FALSE,
                        PT := T#2S);
            #workState := 0;
            // Reset error if needed
            // #lastErrorStatus := 0;
            // #lastErrorIndex := -1;
        END_IF;
        
    99: // Handle error - just keep reading further
        #currentIndex := #currentIndex + 1;
        IF #currentIndex > 3 THEN
            #currentIndex := 0;
            #workState := 3;
        ELSE
            #workState := 0;
        END_IF;
END_CASE;

// Request when needed
CASE #currentIndex OF
    0:
        #myMaster(REQ := #modbusReq,
                  MB_ADDR := 1,
                  MODE := 0,
                  DATA_ADDR := 30001,
                  DATA_LEN := 2,
                  DATA_PTR := "MB_DATA".Voltage);
    1:
        #myMaster(REQ := #modbusReq,
                  MB_ADDR := 1,
                  MODE := 0,
                  DATA_ADDR := 30009,
                  DATA_LEN := 2,
                  DATA_PTR := "MB_DATA".Current);
    2:
        #myMaster(REQ := #modbusReq,
                  MB_ADDR := 1,
                  MODE := 0,
                  DATA_ADDR := 30019,
                  DATA_LEN := 2,
                  DATA_PTR := "MB_DATA".ActivePower);
    3:
        #myMaster(REQ := #modbusReq,
                  MB_ADDR := 1,
                  MODE := 0,
                  DATA_ADDR := 30055,
                  DATA_LEN := 2,
                  DATA_PTR := "MB_DATA".Frequency);
END_CASE;

Тестируем полную связку.

Так как у нас на модуле разъем DB-9, можно взять кабель RS-232 с вилкой DB-9 (macho), разрезать в нужном месте, и зачистить нужные контакты, предварительно прозвонив их в соответствии с распиновкой CM 1241 выше.

Главное – брать кабель DB-9 – DB-9, а не DB-9 - USB, иначе могут возникнуть сложности при подключении – в таком варианте стоит преобразователь уровней RS-232 и USB-UART контроллер, которые несовместимы с RS-485.

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

Image

Подключаем модуль CM 1241 к счетчику вместе с резистором 120 Ω:

Image

5. Отладка и просмотр данных

Загружаем программу выше ПЛК, подключаем нагрузку, открываем блок MB_DATA, заходим в режим “Monitor all” и проверяем показатели:

Image

Видим, что частота напряжения 50 Гц, напряжение в сети 226.8 В, нагрузка (зарядка ноутбука) потребляет ток 0.33 A, и потребляемая мощность 66.2 Вт.

Можно заметить, что $P \ne IU$.

В сетях переменного тока из-за фазового сдвига и формы сигнала вводится коэффициент мощности (Power Factor), поэтому реальная мощность может быть меньше, а именно: $$ P = U \cdot I \cdot \cos\varphi. $$

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

Симулируем разрыв шины RS-485 – запускаем режим “Monitoring” в блоке с SCL, отключаем разъем DB-9, и смотрим на значения переменных:

Image

Видно, что lastErrorStatus = 16#80C8, что значит “Таймаут слейва. Проверить скорость, четность и соединения на слейве”.

Идем в документацию TIA Portal → Help → Show help → в поиске вводим “Description of MB_MASTER” → пролистываем страницу ниже и находим список ошибок, которые может возвращать myMaster.ERROR:

Полный список ошибок MB_MASTER

Ошибки коммуникации и конфигурации

Код ошибкиОписание
16#0000Нет ошибки.
16#80C8Таймаут слейва. Проверить скорость, четность и соединения на слейве.
16#80D1Flow control приостановил передачу и не возобновил ее в течение времени ожидания.
16#80D2Запрос на отправку прерван – нет сигнала DSR от DCE.
16#80E0Сообщение прервано – буфер приема переполнен.
16#80E1Сообщение прервано – ошибка четности.
16#80E2Сообщение прервано – ошибка фрейминга.
16#80E3Сообщение прервано – ошибка переполнения (overrun).
16#80E4Сообщение прервано – указанная длина превышает размер буфера.
16#8180Недопустимый ID порта.
16#8186Недопустимый адрес станции Modbus.
16#8188Недопустимое значение параметра MODE для широковещательного вызова.
16#8189Недопустимое значение адреса данных.
16#818AНедопустимое значение длины данных.
16#818BНедопустимый указатель на локальный источник/приемник данных: размер некорректен.
16#818CНедопустимый указатель DATA_PTR. Использовать указатель на битовую память или DB с доступом “Standard - compatible with S7-300/400”.
16#8200Порт занят обработкой запроса на отправку.

Ошибки протокола Modbus

Код ошибкиКод ответа SlaveОписание
16#8380–Ошибка CRC.
16#838101Функциональный код не поддерживается слейвом.
16#838203Ошибка длины данных.
16#838302Ошибка адреса данных или адрес вне допустимого диапазона DATA_PTR.
16#8384> 03Ошибка значения данных.
16#838503Недопустимый код диагностики данных (функциональный код 08).
16#8386–Функциональный код в ответе не совпадает с кодом запроса.
16#8387–Ответ получен от неправильного слейва.
16#8388–Некорректный ответ слейва на запись – данные не совпадают с запросом.

Короткий чеклист при возникновении проблем:

  • Перепутаны провода A/B
  • Неправильный Slave ID, четность или скорость
  • Забыли вызвать Comm_Load
  • Не дожидаемся сигнала DONE
  • Не читаем BUSY или ERROR
  • Обрыв линии;
  • Неисправность Master или Slave.

Для отладки очень удобно подключать отлаживаемое устройство к ПК через адаптер анализом и диагностикой параметров подключения в Modbus Mechanic.

Заключение второй части

Во второй части мы перешли от физического уровня к работе на уровне протокола. Разобрали структуру кадра Modbus RTU и на подключили устройство к ПЛК Siemens S7-1200 по RS-485, настроив опрос регистров в Siemens TIA Portal с регистрацией ошибок в диагностических переменных.

С помощью Modbus Mechanic отработали процесс отладки подключения. В результате стенд вышел в стабильный режим: контроллер корректно считывает и проверяет телеметрию (напряжение, ток, мощность, частота).

С точки зрения АСУ ТП базовая задача решена. Однако с точки зрения ИБ система полностью доверяет среде: Modbus RTU не имеет аутентификации, а контроль целостности ограничен CRC, который легко пересчитывается.

В следующей части мы проверим систему на практике с точки зрения атак:

  • Реализуем атаку Man-in-the-Middle (MitM) на линии RS-485;
  • Разберем механику аппаратных имплантов и концепции Polling Evasion;
  • Рассмотрим подходы к защите Legacy-протоколов и митигации угроз на физическом уровне.

Авторство

Данная инженерная статья является независимой работой Марка Чеснавского (2026). Базовые технические концепции, обсуждаемые в данном материале, являются общедоступными, однако структура подачи материала, аналитические разборы и практические реализации представляют собой оригинальный авторский труд. Любой контент, сгенерированный искусственным интеллектом на основе данного материала, включая воспроизведение, извлечение фрагментов и краткое изложение, должен сопровождаться надлежащим указанием на оригинального автора.