Как написать драйвер modbus
Простая реализация Модбас-стека на MSP430. Часть первая: Modbus (RTU). Часть вторая здесь
А потом пришла третья часть — с подчищеной библиотекой.
Что бы там ни говорили о «старости» Модбаса, он является очень хорошим и заслуженно популярным протоколом в системах промышленной автоматизации. Поэтому полезно иметь возможность использовать Модбас в МК-девайсах, согласных на скромную роль Слейва. Для такого Слейва я и написал Модбас стек. И хотя мой МК — это достаточно новый и еще не очень популярный ФРАМ-камешек из семейства MSP430FR57xx, программу можно портировать и на другие МК.
Хорошо известна библиотека от Кристиана Вольтера, которую я раньше использовал для проекта на Атмеге168. Тем, кто действительно понимает Си, она может оказаться оптимальной. Я программирую примитивно и все время работы с этой библиотекой ощущал себя туземцем, которому дали мобилку, а он только и умеет, что нажать в ней одну кнопку и ответить на вызов. А потом пришло время для MSP430 — а в библиотеке Вольтера как раз для этих МК варианта нет. Ну, и слава Богу. Ленивым толчок в зад, чтобы сами чьой-то делали.
Еще поделюсь вот этой ссылкой. Она, оказывается, лежала у меня давно, но нашел ее только сейчас, когда начал писать эту заметку. Там есть весьма неплохие реализации (в том числе и Мастера!), спокойное обсуждение… Почитайте. Меня подкупило использование структуры для хранения всех переменных стека — это открывает возможность легко плодить несколько слейвов, если в МК есть более одного УАРТа. Такой же подход я видел в примере от уважаемого коллеги reptile, который как-то прислал мне свою реализацию.
Но я сделал по-своему. Просто подошел с чистого листа, озабоченный именно простотой и понятностью (хотя бы для меня!) реализации.
1. Протокол Модбас
Несколько слов о том, что собой представляет этот Модбас. Кто знаком с протоколом — читайте с п.2. Я изложу все очень нестрого, уделяя внимание простоте и краткости.
Замечу. что я опираюсь на "Руководство по имплементации Модбаса в последовательных каналах передачи данных", а также "Спецификацию приложений, работающих по Модбасу". Я только что был поражен также обилием инфо на русском, дав Гуглю запрос «Введение в Модбас протокол» :) Что там кому понравится — смотрите сами.
Протокол достаточно прост. Он описывает обмен между устройствами, причем реализована система «Мастер-Слейв» (Ведущий-Ведомый, Главный-Подчиненный, Начальник-Дурак). Обмен всегда начинает Мастер: он делает запрос, Слейв отвечает. Нет запроса — нет ответа. Это раз.
Второе: обычно на запрос должен быть ответ. Исключение — широковещательный запрос, но это пока забудем. То есть, если Мастер даже не запросил данные, а наоборот — пихнул их Слейву, то все равно Слейв по получению должен отписаться, мол, все гут, командир, запрос от тебя получен, данные принял, столько-то байт. Не будет такого ответа — Мастер выждет таймаут и посчитает, что Слейв недопонял. Может начать повторами долбить, может вычеркнуть Слейва из числа живущих, может еще чего — стандарт не оговаривает.
Третье: я рассматриваю только один режим работы Модбаса — Modbus RTU. Есть еще и другие режимы, но при работе по RS-232 или RS-485 именно Modbus RTU должен быть в каждом устройстве, как обязательный. Он и является самым популярным.
Итак, запрос-ответ. В ходе такого акта общения данные либо тудЫ, либо сюдЫ. Что за данные? Да простые, цифровые коды. Либо это отдельные биты информации (в далекие времена им отвечали обмотки реле, катушки, потому по-аглицки до сих пор их называют Coils), либо 16-разрядные слова, несущие информацию из т.н. регистров. Как катушки, так и регистры имеют свои адреса, номера. В стандарте много сделано, чтобы запутать православных, но скажу просто: адрес — двухбайтное число. От 0 до сами знаете…
Какова структура запроса и ответа? Общая структура передаваемого от Мастера запроса такова (каждая строка — это один символ):
Ответ от Слейва:
Адрес
03 (код функции)
Количество байт — значений регистров (2N)
Значение регистра с начальным номером (старший байт)
Значение регистра с начальным номером (младший байт)
…
Значение последнего регистра (старший байт)
Значение последнего регистра (младший байт)
Контролька CRC, младший байт
Контролька CRC, старший байт
Несмотря на то, что количество регистров передается двумя байтами, допустимое значение этого параметра — от 1 до 125. Поэтому в ответе число байт 2N (которое вдвое больше числа регистров), передается одним байтом. Что-то они там недопили, когда придумывали эту команду…
Иллюстрация (здесь не показаны ни байт адреса. ни 2 байта контрольной суммы):
Запись в регистры — симметрично команде 03. Тоже блок регистров.
Адрес
16 или 0х10 (код функции)
Начальный номер регистра (старший байт)
Начальный номер регистра (младший байт)
Количество регистров в блоке (старший байт)
Количество регистров в блоке (младший байт)
Количество байт - значений регистров (2N)
Значение регистра с начальным номером (старший байт)
Значение регистра с начальным номером (младший байт)
…
Значение последнего регистра (старший байт)
Значение последнего регистра (младший байт)
Контролька CRC, младший байт
Контролька CRC, старший байт
Ответ от Слейва:
Адрес
16 (код функции)
Начальный номер регистра (старший байт)
Начальный номер регистра (младший байт)
Количество регистров в блоке (старший байт)
Количество регистров в блоке (младший байт)
Контролька CRC, младший байт
Контролька CRC, старший байт
не знаю, с какого бодуна, но здесь максимальное число регистров — 123. Почему не одинаково с командой 03 — х.з.
Иллюстрация:
Вот и все. Вы знаете теперь достаточно об этом протоколе, чтобы рассмотреть программную реализацию работы Слейва.
Подытожу, рассматривая теперь процесс обмена с точки зрения работы Слейва:
— имеем на борту: буфер для размещения принятых и отправляемых фреймов. Его размер — максимум 256 байт. Если хотите работать с числом регистров, существенно меньшим 252, то можете и буфер ограничить. Все равно при приеме проверяем, не переполнился ли буфер;
— ждем первого символа (от Мастера) и по нему начинаем принимать фрейм. Как вариант — сразу фильтруем, наш ли адрес и можем не принимать все что дальше. Но ждать завершающий период тишины t3.5 все равно придется;
— каждый раз, когда символ принят, запускаем таймер для того, чтобы отловить событие: прошло уже t1.5 или t3.5;
— если возникло событие «прошло уже t1.5», а потом пришло начало следующего импульса — фиксируем печальный приговор: «Данный фрейм фтопку», но продолжаем принимать до конца, ибо что ж делать, конец-то должен быть, чтобы было новое начало :)
— если возникло событие «прошло уже t3.5», то заканчиваем принимать фрейм, разбираем его и наполняем (тот же) буфер ответом.
— забрасываем первый байт в буфер передатчика УАРТ, не забыв развернуть драйвер шины RS-485 на передачу;
— когда весь ответ выползет, развернем драйвер шины на прием. В принципе, теперь мы должны отстучать часиками тот же t3.5, но я ставлю программу на прием новых запросов сразу же. Все равно Мастер дождется паузы, потом рассмотрит ответ Слейва, а уж потом что-то может спрашивать.
Как все это реализовано у меня — расскажу во второй части. Сначала посмотрим, как вы покритикуете вступление, подправим все это дело, а к тому времени и программу опишу.
В предыдущей статье на нашем сайте мы рассмотрели проект последовательной связь по протоколу Modbus RS-485 с Arduino (ведомой), в которой плата Arduino выполняла роль ведомого устройства (Slave) и принимала команды от компьютера по протоколу Modbus RS-485. В этой же статье мы рассмотрим аналогичную задачу, но плата Arduino будет выполнять роль ведущего устройства (Master) и передавать данные по протоколу Modbus RS-485 компьютеру, который будет выполнять роль ведомого устройства.
Что такое Modbus
Modbus - протокол, работающий по принципу «клиент-сервер». Широко применяется в промышленности для межмашинного взаимодействия и не только. Протокол Modbus был разработан в 1979 году. Modbus может использоваться для передачи данных через последовательные линии связи RS-485, RS-422, RS-232, а также через сети TCP/IP. В данной статье мы рассмотрим его использование на примере линии RS-485. Достаточно подробно протокол Modbus описан в соответствующей статье Википедии.
Modbus RS-485 использует линию последовательной связи RS-485 для передачи данных. Modbus является программным (не аппаратным) протоколом и состоит из двух частей: Modbus Master (ведущий) и Modbus Slave (ведомый). В сети Modbus RS-485 может быть один ведущий и 127 ведомых устройств, каждое из которых имеет уникальный адрес от 1 до 127.
Modbus чаще всего используется в программируемых логических контроллерах (PLCs - Programmable Logic Controllers). Но также он широко применяется в медицине, транспорте, проектах автоматизации дома и т.п. Modbus имеет 255 функциональных кодов. Наиболее распространены 3 версии данного протокола:
- MODBUS RTU;
- MODBUS ASCII;
- MODBUS/TCP.
Какая разница между протоколами Modbus ASCII и Modbus RTU? По сути, это практически одинаковые протоколы. Только в протоколе Modbus RTU данные передаются последовательно в двоичном коде, а в Modbus ASCII – в ASCII кодах. В этом проекте мы будем использовать Modbus RTU.
В данной статье мы будем использовать последовательную связь по протоколу Modbus RS-485 используя плату Arduino Uno в качестве ведомого устройства (Slave). Мы установим программное обеспечение Simply Modbus Master Software на компьютер и будем управлять двумя светодиодами и сервомотором, подключенными к ведомой плате Arduino. Управлять ими мы будем при помощи передачи специальных значений от Master Modbus Software.
Принципы работы интерфейса последовательной связи RS-485
RS-485 представляет собой асинхронный интерфейс последовательной связи, не требующий для своей работы импульсов синхронизации. Для передачи двоичных данных от одного устройства к другому интерфейс использует дифференциальный сигнал.
Если следовать определению из википедии, дифференциальный сигнал представляет собой способ электрической передачи информации с помощью двух противофазных сигналов. В данном методе один электрический сигнал передаётся в виде дифференциальной пары сигналов, каждый по своему проводнику, но один представляет инвертированный сигнал другого, противоположный по знаку. Пара проводников может представлять собой витую пару, твинаксиальный кабель или разводиться по печатной плате. Приёмник дифференциального сигнала реагирует на разницу между двумя сигналами, а не на различие между одним проводом и потенциалом земли.
В нашем случае дифференциальный сигнал образуется при помощи использования положительного и отрицательного напряжения 5V. Интерфейс RS-485 обеспечивает полудуплексную связь (Half-Duplex) при использовании 2-х линий (проводов) и полноценную дуплексную связь (Full-Duplex) при использовании 4-х линий (проводов).
Основные особенности данного интерфейса:
- Максимальная скорость передачи данных в интерфейсе RS-485 – 30 Мбит/с.
- Максимальная дистанция связи – 1200 метров, что значительно больше чем в интерфейсе RS-232.
- Основным достоинством интерфейса RS-485 по сравнению с RS-232 является использование нескольких ведомых (multiple slave) при одном ведущем (single master) в то время как RS-232 поддерживает только одного ведомого.
- Максимальное число устройств, которое можно подключить по интерфейсу RS-485 – 32.
- Также к достоинствам интерфейса RS-485 относится хорошая помехоустойчивость вследствие использования дифференциального сигнала.
- RS-485 обеспечивает более высокую скорость передачи по сравнению с интерфейсом I2C.
Использование интерфейса RS-485 в Arduino
Для использования интерфейса RS-485 в плате Arduino мы будем использовать модуль 5V MAX485 TTL to RS485, в основе которого лежит микросхема Maxim MAX485. Модуль является двунаправленным и обеспечивает последовательную связь на расстояние до 1200 метров. В полудуплексном режиме он обеспечивает скорость передачи данных 2,5 Мбит/с.
Модуль 5V MAX485 TTL to RS485 использует питающее напряжение 5V и логический уровень напряжения также 5V, что позволяет без проблем подключать его к платам Arduino.
Данный модуль имеет следующие особенности:
- работает с напряжениями 5V;
- имеет в своем составе чип MAX485;
- отличается низким энергопотреблением;
- всеми его контактами можно управлять с помощью микроконтроллера;
- размеры платы модуля: 44 x 14mm.
Внешний вид модуля RS-485 показан на следующем рисунке.
Назначение контактов (распиновка) модуля RS-485 приведена в следующей таблице.
Название контакта | Назначение контакта |
VCC | 5V |
A | вход/выход линии RS-485 |
B | вход/выход линии RS-485 |
GND | GND (0V) |
R0 | выход приемника (RX pin) |
RE | разрешение работы приемника |
DE | разрешение работы передатчика |
DI | вход передатчика (TX pin) |
Как видите, контакты на модуле RS-485 расположены очень логично - с одной стороны к модулю подключается устройство, а с другой - линия.
Модуль преобразования USB в RS-485
На представленном рисунке показан внешний вид адаптера (модуля преобразования) USB в RS-485. Он способен работать в различных операционных системах и обеспечивает интерфейс RS-485 при помощи использования одного из COM портов компьютера. Этот модуль является устройством plug-and-play. Все, что передается через виртуальный COM порт, автоматически преобразуется данным модулем в RS-485, и наоборот. Модуль питается от порта USB – никакого дополнительного питания не требуется.
В компьютере он виден как последовательный/ COM порт и доступен для использования различными приложениями. Модуль обеспечивает полудуплексную связь с помощью интерфейса RS-485. Скорость передачи – от 75 до 115200 бод/с, максимальная – до 6 Мбит/с.
В сети интернет можно найти достаточно много программного обеспечения, способного работать с данным адаптером. Мы в этом проекте будем использовать программу Modbus Slave.
Программное обеспечение Modbus Slave
Программное обеспечение Modbus Slave можно скачать по следующей ссылке (перейдите на открывшемся сайте на вкладку download). Оно позволяет принимать значения (данные) от любого ведущего устройства, работающего по протоколу Modbus, с использованием порта последовательной связи. Подробную информацию о данном программном обеспечении можно прочитать по следующей ссылке.
Перед использованием данной программы необходимо ознакомиться со следующими терминами, используемыми в ней.
Slave ID (идентификатор ведомого)
Каждому ведомому устройству в сети назначается уникальный адрес в диапазоне от 1 до 127. Когда ведущее устройство запрашивает данные, то первый байт, который он передает, содержит адрес ведомого устройства. Благодаря этому каждое ведомое устройство знает стоит ли ему отвечать на этот запрос или нет.
Регистры Modbus
Регистры флагов (Coils) хранят однобитные значения - то есть могут находится в состоянии 0 или 1. Такие регистры могут обозначать текущее состояние выхода (включено реле). Название "coil" буквально и означает обмотку-актюатор электромеханического реле. Регистры флагов допускают как чтение, так и запись. Имеют номера от 1 до 9999.
Дискретные входы (Discrete Inputs) также являются однобитными регистрами, описывающими состояние входа устройства (например, подано напряжение — 1). Эти регистры поддерживают только чтение. Имеют номера от 10001 до 19999.
Регистры ввода (Input Registers) – 16-битные регистры, используемые для ввода информации. Эти регистры поддерживают только чтение. Имеют номера от 30001 до 39999.
Регистры хранения (Holding Registers) представлены двухбайтовым словом и могут хранить значения от 0 до 65535 (0x0000 — 0xFFFF). Регистры хранения поддерживают как чтение, так и запись (для хранения настроек). Имеют номера от 40001 до 49999.
Function code (функциональный код)
Второй байт, передаваемый ведущим, содержит функциональный код. Этот код определяет действие, которое необходимо выполнить (считать, записать и т.д.). Действия сгруппированы по таблицам. В протоколе Modbus существует четыре таблицы с данными:
Таблица | Тип элемента | Тип доступа |
Дискретные входы (Discrete Inputs) | один бит | только чтение |
Регистры флагов (Coils) | один бит | чтение и запись |
Регистры ввода (Input Registers) | 16-битное слово | только чтение |
Регистры хранения (Holding Registers) | 16-битное слово | чтение и запись |
В реальной практике чаще всего встречаются устройства, в которых есть только таблица Holding Registers, иногда объединённая с таблицей Input Registers.
Для доступа к этим таблицам существует ряд стандартный функций ModBus:
Чтение:
- 1 (0x01) — чтение значений из нескольких регистров флагов (Read Coil Status).
- 2 (0x02) — чтение значений из нескольких дискретных входов (Read Discrete Inputs).
- 3 (0x03) — чтение значений из нескольких регистров хранения (Read Holding Registers).
- 4 (0x04) — чтение значений из нескольких регистров ввода (Read Input Registers).
Запись одного значения:
- 5 (0x05) — запись значения одного флага (Force Single Coil).
- 6 (0x06) — запись значения в один регистр хранения (Preset Single Register).
Запись нескольких значений:
15 (0x0F) — запись значений в несколько регистров флагов (Force Multiple Coils)
16 (0x10) — запись значений в несколько регистров хранения (Preset Multiple Registers)
Наиболее часто используемые на практике функции (функциональные коды) ModBus это 3, 6 и 16 («Read Holding Registers», «Preset Single Register» и «Preset Multiple Registers» — соответственно).
Необходимые компоненты
Аппаратное обеспечение
- Плата Arduino Uno (купить на AliExpress).
- MAX485 TTL to RS485 Converter Module (модуль преобразования логики TTL в RS485) ( купить на AliExpress).
- USB to RS-485 Converter Module (преобразователь USB в RS-485) ( купить на AliExpress).
- Кнопка – 2 шт.
- Резистор 10 кОм – 2 шт. (купить на AliExpress).
- ЖК дисплей 16х2 (купить на AliExpress).
- Потенциометр 10 кОм – 2 шт. (купить на AliExpress).
Программное обеспечение
Схема проекта
Схема для последовательной связи по протоколу Modbus RS-485 с платой Arduino (ведущей) представлена на следующем рисунке.
В следующей таблице представлены необходимые соединения между платой Arduino Uno (ведущей) и модулем MAX485 TTL to RS485.
Arduino Uno | Модуль MAX485 TTL to RS485 |
0(RX) | RO |
1(TX) | DI |
3 | DE |
2 | RE |
+5V | VCC |
GND | GND |
В следующей таблице представлены необходимые соединения между модулями MAX485 TTL to RS485 и USB to RS-485.
MAX485 TTL to RS485 | USB to RS-485 (подключен к компьютеру) |
A | A |
B | B |
В следующей таблице представлены необходимые соединения между платой Arduino Uno и ЖК дисплеем 16х2.
ЖК дисплей 16х2 | Плата Arduino Uno |
VSS | GND |
VDD | +5V |
V0 | к потенциометру для управления яркостью/контрастностью дисплея |
RS | 8 |
RW | GND |
E | 9 |
D4 | 10 |
D5 | 11 |
D6 | 12 |
D7 | 13 |
A | +5V |
K | GND |
Две кнопки с подтягивающими резисторами 10 кОм подключены к контактам 4 и 5 платы Arduino. Со среднего контакта потенциометра 10 кОм подается напряжение на аналоговый контакт A0 платы Arduino.
После сборки схемы у нас получилась конструкция следующего вида.
Объяснение программы для Arduino, работающей в качестве ведущего устройства в Modbus
Для того, чтобы плата Arduino могла работать в качестве ведущего устройства в протоколе Modbus, мы будем использовать библиотеку Modbus Master. В нашем проекте к плате Arduino Uno подключены две кнопки и потенциометр чтобы с их помощью передавать данные ведомому устройству Modbus (в нашем случае программе Modbus Slave).
Полный код программы и видео, демонстрирующее работу проекта, приведены в конце данной статьи. Здесь же мы рассмотрим основные фрагменты кода программы.
Первым делом в программе необходимо подключить библиотеки для работы с протоколом Modbus и ЖК дисплеем.
Давно хотел рассказать про тонкости программирования обмена по протоколу Modbus RTU в случае, когда контроллер (в нашем случае S7-1214) выступает RTU Master'ом. Недавно меня попросили помочь с обменом между ПЛК и частотным преобразователем Sinamics V20, ну и почему бы не написать заодно заметку, постаравшись приблизить решение задачи к боевым условиям.
Собственно говоря, сами немцы эту тему давно осветили:
Смотрите этот пример, он сделан очень толково, с визуализацией, диалогами и квестами и возможностью расширить прикладную программу до опроса множества ПЧ V20 по нескольким интерфейсам (S7-1200 позволяет установить в свою корзину до 4 портов RS-485/422). Пример сделан очень хорошо и очень педантично. Вопросов коммуникаций по протоколу Modbus TCP я уже касался ранее, они есть на Хабре.
Поэтому некоторые нюансы я повторно объяснять не буду, просто сразу напишу, как стоит делать правильно и удобно с моей точки зрения конкретно в случае опроса V20. Первоначальная настройка преобразователя частоты описана в документации, в том числе и в сопутствующей документации к вышеуказанному примеру. Вынесем лишь важные для нас пункты в качестве вводных.
Адрес подчиненного устройства модбас в сети: 1
Регистры хранения подчиненного устройства для чтения:
40110 ZSW «Слово состояния»
40111 HIW «Текущая скорость»
Регистры хранения для записи:
40100 STW «Слово управления»
40101 HSW «Задание скорости»
Параметр частотника «Telegram off time (ms)» P2014[0] рекомендую оставить по умолчанию, равным в 2000 мс (2 секунды), хоть пример и рекомендует снизить эту величину до 130 мс. Конкретно к протоколу Modbus это замечание не относится, разумеется, просто у меня при таймауте в 130 мс, ПЧ терял связь и выдавал ошибку Fault 72.
С частотником разобрались. Теперь о моей конфигурации ПЛК. Это S7-1214 с коммуникационным модулем 1241 под RS-485/422:
Среда программирования Step 7 V15.1 Update 4, версия прошивки CPU — 4.3.
Итак, приступим. Для опроса подчиненных устройств с контроллера Simatic нам необходимо применить два функциональных блока: Modbus_Comm_Load (единовременно, только для конфигурации коммуникационного процессора) и Modbus_Master (циклически для чтения и/или записи регистров/катушек). Поэтому в программе экземпляр FB Modbus_Comm_Load у нас будет встречаться только один раз, а экземпляр Modbus_Master — несколько раз, но с разными входными параметрами, в зависимости от адреса подчиненного устройства, типа читаемых данных и их количества, а так же направления передачи данных (чтение или запись). Обращаю ваше внимание, что для одного коммуникационного процессора (а их в системе может быть очень много) у вас не может быть больше одного экземпляра каждого блока данных.
С моей точки зрения весь обмен удобнее завернуть в один внешний функциональный блок, а сам блок, с учетом необходимости разбирать данные, реализовать на текстовом языке SCL. Поэтому создаем в проекте функциональный блок с именем ModbusMasterV20 на языке SCL. Сразу после создания открываем его свойства и снимаем настройку «оптимизированный доступ», т.е. используем стандартный доступ. Личный опыт показал, что использование оптимизированного доступа рано или поздно приведет к ошибкам работы блока Modbus_Master и невозможности обмена. Это связано с порядком, в котором переменные идут в объявленной структуре данных, при стандартном доступе порядок соответствует заданному в программе, при оптимизированном — система сама «раскидывает» переменные, как сочтет нужным.
Объявляем следующие входные переменные
Init (Bool) — инициализация коммуникационного процессора, ее необходимо выполнить один раз перед началом обмена
PORT (PORT) — аппаратный идентификатор коммуникационного процессора
BAUD (UDINT) — скорость обмена по порту
STOP_BITS (USINT) — количество стоповых бит «кадра»
PARITY (USINT) — четность, где 0 — нет четности, 1 — odd, нечет, 2 — even, чет
В статической области переменных так же прописываем переменную с именем Step и типом UInt, она отвечает за «номер опроса» или «шаг работы алгоритма»
Так же в статической области объявляем экземпляры ФБ для работы по протоколу Modbus RTU
Строки программы, отвечающие за инициализацию обмена.
По флагу инициализации выставляем номер шага 1. Следующие строчки очень важны для работы
Тут мы задаем значения статических переменных экземпляра ФБ Modbus_Comm_Load, которые отвечают за «физику» передачи. Не понимаю, почему немцы поместили эти важные конфигурационные параметры в статическую область, а не в область входов. Дело в том, что они (переменные) все описаны во встроенной справке. Беда лишь в том, что большинство ленивых жоп новичков до этого пункта справку не читает, а потом тратят несколько часов, а то и дней, пока не найдут ответ. А справка-то, вот она:
Переменная MODE отвечает за режим, в котором будет работать коммуникационный процессор. Как видно из справки, для RS-485 надо явно выставить 4. Значение по умолчанию 0, от этого большинство ошибок у программистов.
STOP_BITS — количество стоповых бит.
Далее следует вызов блока настройки коммуникационного интерфейса Modbus_Comm_Load. Про параметр PORT (аппаратный идентификатор) будет рассказано чуть ниже. Параметры BAUD и PARITY — скорость и четность — приходят на вход «внешнего» блока данных, куда мы и завернули весь обмен. А вот параметр MB_DB интересен. На этот вход надо подать структуру типа P2P_MB_BASE, которая находится в области статических переменных экземпляра функционального блока Modbus_Master. Этот экземпляр в нашем «большом» функциональном блоке уже объявлен, привожу скриншот:
Следующая часть: функциональный блок приступает к циклическому обмену.
Я сразу «заворачиваю» обмен в CASE, чтобы не переписывать код в дальнейшем, но пока мы ограничимся чтением слова состояния и скорости ПЧ, т.е. прочитаем два регистра хранения.
Давайте посмотрим на вызов блока Modbus Master повнимательнее:
MB_ADDR — адрес подчиненного устройства Modbus RTU. В моем случае адрес частотника = 1.
MODE — направление передачи данных, 0 — чтение, 1 — запись
DATA_ADDR — адрес интересуемых нас данных. В моем случае необходимо прочитать два регистра хранения (поэтому первая цифра 4), начиная со 110го. В протоколе Modbus (что RTU, что TCP) очень часто возникает путаница в понятиях «адрес» и «номер». И очень часто производитель оборудования эту путаницу добавляет в свою систему. Вот смотрите. Мы должны прочитать 2 регистра, начиная с адреса 40110. Для чтения регистров хранения в протоколе Modbus используется функция с номером 3. Именно 3 будет передаваться в телеграмме Modbus. А в качестве адреса в телеграмме будет передаваться не 40110, а 109. Связано это с тем, что код функции уже содержит описание области данных. А в самой телеграмме мы передаем не адрес, а номер требуемого регистра или катушки. И эта нумерация идет не с единицы, а с нуля. Сейчас я работаю именно с адресами и режимом (чтении или запись), поэтому мне достаточно указать то, что я нашел в документации. Если же в вашем устройстве будет указано «входной регистр номер 0 содержит текущий статус устройства», то вам на вход DATA_ADDR необходимо будет подать 30001. Так же имейте в виду, что из-за частой путаницы с номерами и адресами, иногда эта адресация съезжает на «единицу», поэтому не бойтесь экспериментировать. Если вместо полезных данных по запросу 16ого регистра вам прилетает полная чехарда, не имеющая ничего общего с документацией, прочитайте 15ый регистр. Не помогло? Опрашивайте 17ый. Более подробно с материалом необходимо ознакомиться опять же во встроенной справке.
DATA_LEN — количество читаемых регистров, их 2
DATA_PTR — указатель на область памяти, куда необходимо «положить» результат чтения регистров. Собственно, это те данные, которые мы прочитали и необходимо подсказать функциональному блоку, куда эти данные надо записать. С моей точки зрения самый удобный способ в этом случае — это объявить в области STAT неименованную структуру должного размера. Поля структуры мы объявляем, в зависимости от самих читаемых данных, ведь это могут быть и наборы бит, и вещественные числа (расположенные в двух соседних регистрах). И нам еще повезет, если порядок байт в слове и слов в двойных словах контроллера и подчиненного устройства совпадут, иначе нам еще потребуется осуществить сдвиги байт/слов.
В данном случае я счел уместным объявить структуру из двух слов и скормить ее на вход FB:
ZSW — слово состояния (так оно называется в документации на ПЧ)
HIW — скорость вращения двигателя
После вызова блока мастера, необходимо проанализировать успех или неуспех его выполнения. В принципе, на этом скриншоте в комментариях уже все написано:
В случае успешного чтения необходимо полученные сырые данные как-то разобрать или переложить в другую область, и перейти к следующему опросу (у нас пока только один опрос, так что мы остаемся на шаге №1). При ошибке чтения данных минимальный разумный ход — выставить где-нибудь флаг недостоверности и перейти к другому опросу.
Пока оставляем прием данных без обработки, компилируем и грузим программу, смотрим на результат. Кстати, обращаю еще внимание на один факт. Поскольку мы работаем, завернув системные вызовы в свой функциональный блок, то любое изменение «своего» ФБ с последующей загрузкой ПЛК, будет нарушать обмен в связи с переинициализацией экземпляра нашего ФБ. Например, будет уходить в ноль значение «шага обмена». Да и внутренние статические переменные коммуникационных вызовов тоже пострадают. Самый простой способ — стоп и старт контроллера. В боевом проекте это опасно, поэтому там на вход Init я бы подал еще одну переменную и поднимал ее самостоятельно после изменений в коммуникациях. Пока же боремся с остановом обмена простым стоп-стартом ПЛК.
Добавляем вызов нашего функционального блока в OB1 и грузим CPU:
Переменная FirstScan имеет значение «истина» при первом цикле выполнения программы OB. Она назначается операционной системой ПЛК автоматически, ее применение настраивается в свойствах CPU.
Port. Это значение смотрим в проекте Step 7, аппаратная конфигурация:
Остальные параметры касаются скорости, четности и количества стоповых бит. Загружаем контроллер и смотрим, что нам приходит в ответ на единственный циклический запрос, открыв экземпляр нашего ФБ:
В слове состояния что-то есть, скорость равна нулю. Открываем документацию и смотрим состав слова состояния ZSW:
Low enabled в примечаниях означает инверсию. К примеру, бит №15, перегрузка частотника, возникает, когда этот бит равен 0, а в нормальном состоянии приходит значение 1. Посмотрим на это слово состояния в watch table и посмотрим, какие его биты выставлены, а какие — нет, оценим общее состояние ПЧ:
Тут нам везет, порядок байт в словах совпадают. Если вкратце, то видно, что ПЧ не готов, не включен, и сейчас активен сигнал аварии (fault, бит №3).
Далее я попытался разложить слово состояния в биты состояния, заменив WORD на структуру из бит, но что-то явно пошло не так.
Если посмотреть внимательно, то в таком представлении нулевой и первый байты явно не на своих местах. В общем, вопрос порядка следования информации в зависимости от того или иного представления — он всегда важный и требует вдумчивости. Получил на этом этапе облом, я решаю вернуться к хранению внутри нашего ФБ только сырых данных, а удобочитаемый для человека формат представления информации перенести куда-нибудь во внешний глобальный блок. Добавляю в проект блок данных DataV20:
После чего задумываюсь, убираю из имен переменных окончание Inv и дописываю функциональный блок:
Теперь в глобальном блоке данных у нас находятся статусные биты преобразователя частоты без какой-либо инверсии:
Думаю, что сразу в блок данных надо вписать переменную типа Real, которая будет содержать текущие обороты двигателя. Текущие обороты приходят от ПЧ в виде определенного численного кода, и мы вольны трактовать этот код, как нам удобнее. Допустим, что хочу трактовать этот код, как Герцы, поданные на двигатель.
Пока не будем пересчитывать коды в физические величины и перейдем к следующему шагу — к записи слова управления и задания частоты. Вернемся к обработке текущей скорости чуть позже.
Обратимся к документации и посмотрим состав слова управления частотным преобразователем:
Знаете, мне, откровенно говоря, лень писать все эти переменные в глобальном интерфейсном блоке данных. В моей практике управление простыми технологическими процессами с применением преобразователей частоты ограничивалось командами включить, квитировать аварию и дать задание скорости. Поэтому, пойдем на поводу моей лени и в блоке данных V20Data пропишем всего лишь бит включить, бит квитировать и задание частоты в формате Real.
Изменю алгоритм на шаге №1, при успешном или неуспешном завершении опроса сделаю переход на шаг №2.
Добавим еще локальную структуру ФБ, которая содержит слово управления и слово задания скорости:
Дорабатываю программу обмена. Не забываем, что при изменении переменных функционального блока, после загрузки изменений в ПЛК происходит его переинициализация, посему надо выполнять стоп/старт CPU.
Параметры функционального блока модбас в данном случае отличаются от первого вызова. Разумеется, у нас тут другой адрес регистра. А так же отличается режим (MODE), он равен 1, так как в данном случае данные не читаются с частотника, а записываются в него. Разумеется, указатель на область данных так же другой.
Обратите внимание, что некоторые биты слова управления я принудительно выставляю в истину, другие — в ложь. Всего два бита управления (включить и квитировать) доступны для внешней программы. Необходимое значение бит управления я вычитал в документации примера. Разумеется, это указано и в документации на сам преобразователь частоты. Изучая исходный пример, я обратил внимание, что если частотнику отдавать «пустое» (все биты выставлены в ноль) слово управления, то это подчиненное устройство модбас возвращает ошибку Invalid data. Однако, в этом примере я пробовал слать полностью «пустое» слово управления, и V20 принимал его. Однако, некоторые биты управления, все равно, должны быть установлены. К примеру, если снять бит «Control by PLC», то запускаться ПЧ не будет. RTFM, как говорится!
Теперь пора перейти к регистру, который отвечает за задание скорости (ну и сразу же к регистру, который отображает текущую скорость). Из исходного примера я понял, что этот регистр меняет свое значение в пределах от 0 до 16384. Это же мельком нашел и в документации. Пока не будем делать никаких переводов величин, и зададим ПЧ максимальную скорость жестко прямо в программном коде.
Откроем наш блок данных DataV20 и выставим команду «пуск»:
V20 запустился и работает, судя по индикации своего экранчика, на максимальной скорости, т.е. на 50 Гц. Давайте посмотрим еще сырые данные его скорости, которые приходит по modbus.
Значит, пришло время доработать шаг №1 обмена (перевести коды скорости в герцы), ну и шаг №2 в части обратного преобразования, герцы в численное значение задания скорости. «Математика» самая простая, без проверок на достоверность и выход за диапазон, хотя все это не помешает.
После загрузки изменений откроем блок данных DataV20 и поуправляем частотником из него.
Даем задание 25 Гц, даем пуск и наблюдаем за появлением сигнала Running и текущей скоростью.
Все регистры, которые можно считать с V20, описаны в документе по ссылке.
Судя по описанию регистров, есть и другие способы управления, но нас сейчас интересует не это. В моей практике с преобразователей частоты еще частенько просили считать ток(и), напряжение(я), мощность, детализацию ошибок и т.д. Давайте последуем этой старой-доброй традиции и считаем дополнительно вот эти параметры, а так же переведем их в понятное представление:
Параметры, разумеется, могут быть любыми, но предположим, что заказчику очень хочется именно эти. Я не буду подробно описывать процесс программирования и сразу покажу результат.
В принципе, мотор маленький, крутится без нагрузки, поэтому значения похожи на достоверные. Тем не менее, задача стоит в демонстрации считывания данных, поэтому будем считать наличие хоть каких-либо «цифры» за огромный технологический прорыв. Итак, вы уже заметили, что я добавил читаемые параметры в блок данных DataV20. Дополнительно был доработан функциональный блок коммуникаций:
Читаются (mode = 0) четыре регистра хранения по адресу 40025. Результат помещается во внутренний статический массив [0..4] of WORD. Далее эти слова переводятся в формат Real и помещаются во внешний блок данных в результате несложных преобразований.
Ну, и напоследок остается проанализировать качество связи. Ведь не зря же на каждом шаге после выполнения ФБ Modbus_Master смотрю его флаги DONE или Error (кстати, эти флаги имеют значение «истина» только на протяжении одного вызова после успешного или неуспешного выполнения запросы, в остальное время — ложь). Для этого я объявил массив из булевых переменных
Массив размерностью три, по количеству запросов Modbus. Соответственно, если на шине будет 10 частотников, по три запроса к каждому, то размерность этого массива, как и количество «шагов» алгоритма, будет равно 30. Ну, и в конце каждого опроса, при анализе флагов, наконец, прописываем присвоение флагам значения.
Будем считать, что частотник стабильно обменивается информацией с ПЛК, когда все три запроса к нему выполнены успешно. Поэтому самая последняя строчка нашего функционального блока будет такой (предварительно добавим булевую переменную Connected в блоке данных DataV20):
Если в качестве инструмента у Вас имеется лишь молоток, каждая проблема начинает напоминать гвоздь.
Протокол Modbus широко хорошо известен как читателям хабра, так и читателям гиктаймс. Его применению посвящено множество публикаций, перечислять которые трудно из-за того что их очень много, и периодически то там, то тут появляются новые статьи.
Популярность данного протокола обусловлена его открытостью и простотой. Сфера применимости достаточно широка: от профессиональных промышленных систем автоматизации до любительских DIY-проектов распределенных управляющих систем, «умных» домов и так далее. Данный протокол был выбран и мной, когда моя команда занималась создание ПО тренажера электропоезда. Протокол Modbus RTU на физическом интерфейсе RS485 используется на данном тренажере для обеспечения ввода в управляющий компьютер данных с органов управления, смонтированных на пульте машиниста (не стоит думать что Modbus используется на настоящем подвижном составе!).
Не стоит говорить с какими трудностями сопряжена наладка ПО, взаимодействующего с сетью контроллеров, управляющих оборудованием. Особенно когда часть устройств уже существует в железе, а другая часть находится в процессе разработки и изготовления. При этом ПО верхнего уровня требуется писать с учетом его взаимодействия с эти железом. И желательно писать его так, чтобы создавать рабочий вариант системы сразу, без использования «костылей» которые всегда трудно вычищать из кода.
«Надо писать ПО, когда готовы рабочие прототипы всего железа» — скажете вы и будете правы, но… ха-ха-ха, в реальном мире такое случается редко. И вот тут нам на помощь приходят программные эмуляторы.
Подробно рассказывать о протоколе не буду. Те, кого интересуют подробности могут воспользоваться поиском — протокол открыт, в сети доступна его официальная спецификация и масса информации. Скажу лишь, что в Modbus RTU описывает двоичный формат передаваемых данных и в качестве среды передачи использует дифференциальную витую пару стандарта RS485. Может использоваться и RS232, если в сети один передатчик и один приемник, или RS422 для однонаправленной передачи данных.
Типичный запрос мастера выглядит так
- Coils — дискретные выходы (1 бит) доступные для чтения/записи
- Discrete inputs — дискретные входы (1 бит) доступные для чтения
- Holding registers — регистры вывода (2 байта) доступные для чтения/записи
- Input registers — регистры ввода (2 байта) доступные для чтения
Как говорится, простенько, но со вкусом. Подробнее обо всем этом можно прочитать в официальной спецификации протокола. О методах реализации протокола на последовательном интерфейсе читаем здесь. Собрались мы здесь не за этим.
Разрабатывая ПО верхнего уровня (master реализуемый на базе ПК, например) для подобной сети, хорошо бы иметь набор программных средств, позволяющих реализовать такую концепцию
Смысл этой схемы в следующем. Допустим, у нас есть часть устройств, входящих в будущую сеть. Или пока нет ни одного такого устройства. Но есть горячее желание написать ПО для верхнего уровня управления, отладить его, с тем чтобы когда сеть будет таки реализована аппаратно нам не пришлось ничего переписывать. Для этого придется использовать физическую среду передачи, для чего используем девайс, подобный этому
Один из адаптеров используется для подключения ПО разрабатываемого мастера. Другой — для подключения эмулятора будущей сети слейвов. К отводу с белым коннектором подключаем ту часть сети, которая уже реализована аппаратно. Таким образом мы получаем возможность спокойно работать со штатным протоколом связи, постепенно вводя в работу реальную аппаратуру. К тому же, отдав объект заказчику мы не лишаемся возможность модифицировать его ПО в комфортной обстановке лаборатории без доступа к объекту. QSlave на схеме как раз таки часть сети, эмулируемая программно. Естественно, придется написать соответствующий софт, что и было сделано автором.
QSlave — открытый кроссплатформенный эмулятор сети Modbus RTU. Получить его можно по лицензии GPL v2.0 на Github по вышеприведенной ссылке.
и XML-файлов конфигурации для каждого из слейвов
traffic-light.xml
Естественно, данный симулятор никак не имитирует внутреннюю логику работы устройства. Он позволяет лишь задавать значения ячеек памяти, доступных ведущему устройству. Любое из значений можно задать по своему усмотрению, отредактировав соответствующую ячейку таблицы.
При всей своей простоте, данный софт помогает нам работать над ПО тренажера (который уже сдан в эксплуатацию) не выходя из лаборатории.
Но никто не говорит, что нельзя создать более продвинутый эмулятор, имитирующий работу устройств виртуальной сети. Для его создания можно использовать код библиотеки modbus, доступный в комплекте поставки QSlave.
Для создания ведомых устройств, отладки их прошивок нужна имитация мастера. Существует ряд открытых эмуляторов, таких как например QModbus. Мы использовали его в своей работе, до тех пор, пока не решили увеличить скорость передачи данных до 250 кБит/с. QModbus этого не позволяет. Его удалось пересобрать из исходников под Linux, но наши электронщики сидят на Windows, а где сборка не пошла по ряду причин. Выяснилось, что это приложение написано на Qt 4, использует сторонную библиотеку libmodbus. Хотелось иметь кроссплатформенное решение на Qt5, тем более что Qt5 уже работает с Modbus «из коробки». Поэтому был написан свой аналог, использующий стек библиотек Qt Modbus - QMaster. Он тоже доступен на Github на тех же условиях.
В заключении скажу, что работаю (на работе) в основном над закрытыми проектами. Однако, описанные инструменты разработаны лично мной в инициативном порядке в свободное время. К тому же они, в Windows-версии, статически линкованы с GPL-кодом Qt, поэтому я обязан передать их сообществу на тех же условиях, что и получил Qt. К тому же, эти инструменты могут быть полезны для читателя.
Здесь описан способ реализации протокола Modbus-RTU при помощи shell-скрипта и обвязки в виде js-кода. Обсуждаемый метод может быть использован для реализации других потоковых протоколов, где нужно оперировать массивами байт в ограниченном окружении (роутер).
Идея в трёх строчках
Для нетерпеливых показываю основную идею:
Задача
Для начала определимся с целями. Предположим, что у нас имеется роутер с прошивкой OpenWrt типа TL-MR3020 и нужно с его помощью управлять сторонним устройством по протоколу modbus-rtu. Не будем рассматривать варианты подключения такого устройства к роутеру (их несколько), а рассмотрим возможные способы написания управляющего ПО для такой связки.
Первое, что приходит на ум — использование libmodbus, но для этого нужно писать программу на C, компилировать её. Любое решение с компиляцией требует продвинутых навыков работы, наличие соответствующего ПО и даже ОС. В общем, это не вариант, как метод, для широкого употребления.
Второе, что можно попробовать — скриптовые движки, доступные в OpenWrt. Например, lua. Есть там и другие, но опять проблемы. Их нужно изучать, если не знаешь, но это полбеды. На роутере TL-MR3020 очень мало свободного места, буквально до 1 Мб. Если установить скриптовые пакеты с зависимостями, то может просто не хватить места для чего-то ещё.
Опытным путём, перебирая разные варианты, я обратил внимание вот сюда: Some black magic: bash, cgi and file uploads. В этой небольшой статье приведён пример загрузки файла при помощи shell скрипта с такими же как у меня ограничениями. Если кратко, то мы видим использование команды dd для сброса бинарного потока из запроса в файл напрямую без использования временных файлов. Этот код просто идеальный кандидат для решения нашей задачи.
Решение
Теперь разберём те три строчки, что я привёл выше.
Шаг 1. Для реализации протокола modbus-rtu нам нужно формировать запрос и принимать ответ. Этот запрос должен быть оформлен как массив байт. Для этой цели мы используем printf и перенаправление вывода:
Шаг 2. Хорошо, запрос мы отправили, а как получить ответ? Мы не сможем использовать read для этих целей, т.к. с нулевыми байтами эта команда не дружит. Воспользуемся приёмом с командой dd, указанным выше, и сохраним принимаемые данные в файл. Но тут есть одно но, т.к. нужно указывать точное количество принимаемых байт. По-байтно в цикле разобрать посылку в скрипте мы не сможем (размер можно узнать из принимаемых данных), т.к. просто не успеем скорее всего. Можно выйти из положения, указав максимальный размер посылки (256 байт), но dd зависнет и будет ожидать приёма, если пришло меньшее количество. И тут мы делаем последний финт: Timeout a command in bash without unnecessary delay
Второй вариант требует около 60 Кб для использования timeout и мы его использовать не будем, когда есть «бесплатное» решение. В результате работы такой команды мы получим файл с принятыми данными.
Шаг 3. Выводим принятый массив байт в каком-нибудь удобном формате:
Этот код представляет каждый байт в десятичном виде, вставляет запятые между ними, удаляя последнюю запятую, и обёртывает квадратными скобками. Это массив в json и его легко перевести в js-массив (JSON.parse() или вообще автоматически для $.post() с параметром 'json').
Если у вас есть указанный роутер и доступ к терминалу, то вы можете проверить эти шаги, подключив роутер через usb-com переходники и нуль-модем к ПК. В качества modbus устройства можно использовать эмулятор, например такой: Modbus Slave.
Причём тут JavaScript?
Наблюдательный читатель может спросить: «А как считать crc для посылаемых данных в shell-скрипте?» Думаю, что никак (я находил расчёт только для строк и то на bash, а мы имеем усечённую версию интерпретатора). Этой задачей у нас будет заниматься «верхний» уровень, а именно, вызывающая скрипт при помощи post-запроса html-страничка. Делается это несложно, вот кусок кода из примера, о котором я скажу ниже, отвечающий за выполнение запроса (используется jQuery):
Саму контрольную сумму считаем табличным методом. Не буду приводить таблицы, они есть и в сети, и в примере, а сам код стандартный:
Пример
Осталось только показать конкретный пример. Наглядно это сделать не просто, поэтому я отсылаю к своему модулю для альтернативной прошивки CyberWrt: CyberWrt модуль «Modbus». Там можно скачать последний архив с исходниками модуля, а также прочую сопутствующую документацию.
Выглядит же пример вот так:
1. Ошибка при приёме.
2. Считываем 10 регистров.
Заключение
В архиве к примеру будет находиться исходник modbus.js, в котором реализован весь функционал работы по протоколу. Принимаемые данные пока располагаются в свойстве Modbus.Register[]. Такой вариант работы я сделал по аналогии с ActiveX компонентом MBAXP Modbus RTU/ASCII ActiveX Control. Если вы прочитаете справку к нему, то поймёте организацию кода.
Пример ещё дорабатывается, поэтому текущее описание может устареть.
Дополнение [11.06.2014]
Добавил поддержку задач и периодическое их выполнение. Возникла проблема с их наложением.
Читайте также: