Перейти к содержимому

Работа с шиной RS485 и протоколом Modbus RTU на ESP32

Метки:

Добрый день, уважаемый читатель!

В данной статье разберем основные принципы работы с интерфейсом RS-485 и протоколом Modbus RTU применительно к микроконтроллеру ESP32 при условии использования для программирования платформы (фреймворка) ESP-IDF.

Дабы было понятно, что к чему, совсем чуть-чуть пройдемся и по теоретическим аспектам, но данная статья отнюдь не претендует на полноту освещения вопросов электроники и спецификаций протокола modbus – скорее эти вопросы рассмотрены только для понимания того, что и как подключать и как с этим работать.

Несмотря на бурный расцвет беспроводных протоколов и технологий, я всё таки чаще предпочитаю старый добрый кабель, особенно в особенно ответственных устройствах, от которых требуется бесперебойная работа. Применение интерфейсов RS485 + Modbus открывает перед разработчиком возможность использования самых различных промышленных периферийных устройств  – сенсоров,  реле, дисплеев, метеостанций и т.д. с проводным подключением на достаточно большие расстояния. Про некоторые из них я уже не раз писал на данном сайте – например раз, два, три.

Примеры периферийных устройств для шины RS485

Чем хорош интерфейс RS-485 для DIY – так это тем, что на один и тот же кабель витой пары, протянутый по всему дому, можно подключить большое количество разнообразных устройств – от сенсоров до исполнительных устройств, с централизованным управлением с одного контроллера. Но применение шины RS485 не ограничивается только подключением датчиков и периферии – используя протокол Modbus можно связать два или несколько микроконтроллеров между собой.


Интерфейс RS-485

Почему в названии статьи указаны два термина – RS-485 и Modbus? Дело в том, что интерфейс RS-485 (другое название – EIA/TIA-485) – один из наиболее распространенных стандартов физического уровня связи. Физический уровень – это канал связи и способ передачи сигнала, который определяет каким образом передаются электрические сигналы по проводам; а протокол Modbus определяет программную реализацию – то есть каким образом происходит программный обмен данными. В русскоязычной среде слово интерфейс часто заменяется словом “шина” – шина RS485.

В основе интерфейса RS-485 лежит принцип дифференциальной (балансной) передачи данных. Суть его заключается в передаче одного сигнала по двум проводам. Причем по одному проводу (условно A) идет оригинальный сигнал, а по другому (условно B) – его инверсная копия. Другими словами, если на одном проводе “1”, то на другом “0” и наоборот. Таким образом, между двумя проводами витой пары всегда есть разность потенциалов: при “1” она положительна, при “0” – отрицательна.

Источник: https://www.ivtechno.ru/articles-one?id=19

Зачем нужно такое нерациональное использование проводов? Дело в том, что именно такой способ передачи обеспечивает высокую устойчивость к синфазной помехе. Синфазной называют помеху, действующую на оба провода линии одинаково. К примеру, электромагнитная волна, проходя через участок линии связи, наводит в обоих проводах потенциал примерно одинаковой величины. Если сигнал передается потенциалом в одном проводе относительно общего, как в RS-232, то наводка на этот провод может исказить сигнал относительно хорошо поглощающего наводки общего провода (“земли”). Кроме того, на сопротивлении длинного общего провода будет падать разность потенциалов земель – дополнительный источник искажений. А при дифференциальной передаче искажения не происходит. В самом деле, если два провода пролегают близко друг к другу, да еще перевиты, то наводка на оба провода одинакова. Потенциал в обоих одинаково нагруженных проводах изменяется одинаково, при этом информативная разность потенциалов остается без изменений.

Источник: https://habr.com/ru/companies/milandr/articles/540084/

Именно эта особенность позволяет создавать длинные линии связи (по стандарту – до 1200 метров) для цифровых устройств в условиях высокого уровня помех. Из-за этого данный стандарт широко применяется в промышленности. 

Любое устройство для RS-485 (как и UART) можно разделить на приемник (Receiver) и передатчик (Transmitter). Строго говоря, существуют два варианта такого интерфейса – RS-422 и RS-485:

  • RS-422 – полнодуплексный интерфейс. Прием и передача идут по двум отдельным парам проводов. На каждой паре проводов может быть только по одному передатчику и одному приемнику. Похожий принцип передачи используется в ethernet-сетях.
  • RS-485 – полудуплексный интерфейс. Прием и передача идут по одной паре проводов с разделением по времени. В одной сети (на одной паре проводов) может быть много передатчиков, так как они могут отключаются в режиме приема – “один вещает, остальные внимают“. Примерная схема соединений может выглядеть как на рисунке ниже: 

Источник: https://habr.com/ru/companies/milandr/articles/540084/

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

  • DI (driver input) – цифровой вход передатчика
  • DE (driver enable) – разрешение работы передатчика;
  • RO (receiver output) – цифровой выход приемника;
  • RE (receiver enable) – разрешение работы приемника;
  • A – прямой дифференциальный вход/выход линии связи;
  • B – инверсный дифференциальный вход/выход линии связи;

Выводы DI и RO RS-485 подключаются к любому свободному порту UART (универсальный асинхронный приемопередатчик) микроконтроллера. Цифровой выход приемника (RO) подключается к порту приемника UART (RX). Цифровой вход передатчика (DI) к порту передатчика UART (TX).

Поскольку на дифференциальной стороне приемник и передатчик соединены, то во время приема нужно отключать передатчик, а во время передачи – приемник. Для этого служат управляющие входы – разрешение приемника (RE) и разрешения передатчика (DE). Так как вход RE инверсный, то его можно соединить с DE и переключать приемник и передатчик одним сигналом с любого порта контроллера. При уровне “0” – работа на прием, при “1” – на передачу. 

На этом с теоретическими сведениями по интерфейсу RS-485 закончим, этого будет вполне достаточно для понимания происходящего ниже. Если вам будет интересно “углубить и расширить” свои познания по данному вопросу, вот вам несколько ссылок:

Ну а нам для того, чтобы подключить микроконтроллер к шине RS485, потребуется собственно описанный приемопередатчик, или иными словами преобразователь интерфейса TTL – RS485. Как правило, для этого используется микросхема Maxim MAX485 или её аналоги, но проще использовать готовые модули, о них и поговорим в следующем разделе.

 


Преобразователи интерфейса TTL – RS485

Для того, чтобы соединить две ESP32, можно использовать две микросхемы MAX485 или аналоги, о чем, собственно и говорит схема из примера использования протокола modbus для esp32:

Datasheet на MAX485 говорит нам, что эти микросхемы рассчитаны на напряжение питания 5В, а для 3.3В следует использовать другой вариант – MAX3485, но и самый обычный MAX485 прекрасно работает от 3,3В. По крайней мере я никаких сбоев и неудобств пока что не наблюдал.

Но Maxim Integrated не единственный производитель чипов для RS485, их сейчас выпускается очень и очень много, ну например:

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

Для подключения микросхемы приемопередатчика к микроконтроллеру может использоваться от двух до трех выводов:

  • RxD (Receive Data) – прием данных. Вход на микроконтроллере, выход на приемнике данных из линии связи. Поток данных, входящий в ваш микроконтроллер или комтупер. Иногда может быть обозначен как RD или RX.
  • TxD (Transmit Data) – передача данных. Выход на микроконтроллере, вход на передатчике. Поток данных, исходящий из вашего микроконтроллера или комтупера. Иногда может быть обозначен как TD или TX.
  • RTS (Ready To Send) – переключить на передачу. Служит сигналом для микросхемы передатчика внимать на TXD и транслировать данные в линию связи.

Строго говоря, есть ещё один специальный сигнал управления потоком данных – CTS (Clear To Send), обозначающий готовность приемника к началу передачи данных. Обычно вывод CTS одного устройства соединяется с RTS другого, и используется в стандарте связи RS-232. В шинах RS-485 я лично такого не встречал, так как это потребовало бы еще одного или двух проводов.

 

Однако в DIY-проектах вместо микросхем зачастую проще и удобнее использовать готовые модули – они имеют относительно небольшие размеры, на них уже имеются все необходимые элементы защиты, к ним удобно подключать витую пару, и некоторые из них имеют автоматический переключатель направления передачи – то есть вам не нужно использовать отдельный вывод RTS для управления потоком. На Ali я встречал массу различных вариантов, для себя давно уже выбрал вот такой (кликните на картинке для увеличения):

Этот модуль имеет на своем борту чип MAX485ESA (хм, наверняка не оригинальный), и кроме собственно микросхемы приемопередатчика всю необходимую “обвязку” и элементы защиты от помех в линии – супрессоры и самовосстанавливающиеся предохранители. А также установлена микросхема 74HC14 или CD4069 (содержит 6 логических элементов НЕ), на которой собраны буфер и схема управления потоком – любой сигнал на контакте TXD на время, определяемое RC-цепочкой, переключает чип в режим передачи. Может быть, это и не самая надежная схема, зато простая и рабочая в большинстве DIY-применений.

Если же данная схема автоматического переключения режима передачи по каким либо причинам не походит – стоит пожертвовать дополнительным GPIO микроконтроллера, благо библиотека ESP Modbus (а о ней, собственно, и статья) поддерживает “ручное” управление передачей данных по шине.

 

Схема модуля, найденная на просторах сети. Кстати: данная схема содержит ошибку – линия А+ должна быть подтянута к питанию, а не к земле, как на данной схеме. Соответственно вывод 7 подтянут к земле, вывод 6 – к питанию.

В сети иногда попадаются отзывы, что модули с CD4096 – “плохие” и “не работают”, однако у меня таких проблем пока не наблюдалось. Может быть потому, что я их подключаю не к пяти-вольтовым Arduino, а “неправильно” – к 3.3 В ??? Если вы не уверены, и у вас есть опасения, что такая микросхема не будет работать – ищите версии на 74HC14.

Строго говоря, я зачастую использую этот модуль “не правильно” – напряжение питания у него по факту только 3,3В, а следовательно и напряжение в линиях A+ и B- не превышает этого уровня. По “правильному” нужно бы подключать эту плату к питанию 5В, но при этом придется, соответственно, подключать линии данных к микроконтроллеру через согласователь логических уровней.

Если вы планируете использовать другие модули, следует учитывать, что далеко не все из них способны работать при напряжении питания 3,3В. Для подключения таких модулей к ESP32 (и другим контроллерам с питанием 3.3В) придется использовать дополнительные устройства согласования уровней. Для линии RxD (прием данных) вполне можно использовать простейший делитель напряжения, а вот для TxD (и RTS, если используется) придется использовать схему посложнее, например как на рисунке ниже.

Если вы планируете использовать шину в условиях сильных помех, рекомендуется выполнить линии A / B с гальванической развязкой. Для этого можно найти высокоскоростные оптроны или специализированные микросхемы типа ADuM140… или аналоги.

Ну а мы на этом с электроникой закончим, и плавно переходим к программной части. 


Протокол Modbus

Modbus — это протокол обмена сообщениями прикладного уровня. Он обеспечивает связь клиент/сервер между устройствами, подключенными к различным типам шин или сетей. Modbus был представлен в 1979 году компанией Modicon (ныне Schneider Electric). Это был открытый стандарт, работающий по интерфейсу RS-232. Позже появилась реализации протокола для интерфейсов RS-485 и Modbus TCP. Протокол быстро набрал популярность, и многие производители стали внедрять его в своих устройствах. Позже права на протокол были переданы некоммерческой организации Modbus Organization, которая до сегодняшнего дня владеет стандартом.

Modbus — это прикладной протокол, определяющий правила структуры обмена сообщениями и организации данных, независимые от среды передачи данных. Традиционный последовательный Modbus — это протокол на основе регистров, определяющий транзакции сообщений, происходящие между главным (master) и подчиненными устройствами (slave) (но при использовании Modbus TCP/IP допускается использование нескольких ведущих устройств).

Существует множество вариантов протоколов Modbus, в списке ниже перечислены только самые популярные из них:

  • Modbus ASCII – Данные кодируются символами из таблицы ASCII и передаются в шестнадцатеричном формате. Начало каждого пакета обозначается символом двоеточия, а конец — символами возврата каретки и переноса строки. Это позволяет использовать протокол на линиях с большими задержками и оборудовании с менее точными таймингами.
  • Modbus RTU – Данные кодируются в двоичный формат, и разделителем пакетов служит временной интервал – интервал времени больше двух байтовых кадров при при приёме рассматривается как окончание пакета. Поэтому данный протокол довольно критичен к задержкам и не может работать, например, на модемных линиях. При этом, накладные расходы на передачу данных меньше, чем в Modbus ASCII, так как длина сообщений меньше. Modbus RTU — наиболее распространенная реализация Modbus.
  • Modbus TCP/IP – Структура пакетов схожа с Modbus RTU, данные также кодируются в двоичный формат, и упаковываются в обычный TCP-пакет, для передачи по IP-сетям. Проверка целостности, используемая в Modbus RTU, не применяется, так как TCP уже имеет собственный механизм контроля целостности.

Источник: https://habr.com/ru/companies/advantech/articles/450234/

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

  • 0 – широковещательный адрес, с помощью которого мастер обращается сразу ко всем ведомым устройствам
  • 1 ~ 247 – индивидуальные адреса slave-устройств, которые могут назначаться как программно, так и с помощью микропереключателей или перемычек. Однако иногда в некоторых устройствах диапазон разрешенных для выбора адресов и вовсе ограничен диапазоном 1 ~ 127, но и этого более чем достаточно.
  • 248 ~ 255 – данная группа адресов зарезервирована и не может быть использована

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

  • адрес slave  – по умолчанию обычно установлен адрес 1, но его можно изменить (программно) от 1 до 127 или 247.
  • скорость передачи – по умолчанию на китайских датчиках обычно установлена скорость 4800 или 9600 бод.

Для предварительной настройки параметров таких устройств можно и нужно подключить их к компьютеру. Для этого как минимум потребуется адаптер-переходник RS485 – USB. На Ali их великое множество. А также потребуется любая программа – modbus terminal, которая умеет работать с протоколом Modbus RTU: Modbus Poll, Modscan32/64, Termite или другая. Если вы еще не сталкивались с этим, советую прочитать вот эту статью на Хабре. В данной статье я не буду описывать все эти программы и работу с ними, упомяну только, что лично я пользуюсь Modbus Poll (на текущий момент у меня стоит версия 9.5.1, так как более новые – не работают корректно).

 

В данной статье далее мы будем рассматривать только протокол Modbus RTU, так как он является самым популярным вариантом реализации. По крайней мере если на ali видишь датчик или устройство с надписью RS485, то это наверняка означает, что в нем будет использован именно протокол Modbus RTU.

Каждое сообщение (фрейм) Modbus RTU состоит из 4 полей (частей): адрес, код функции, данные, контрольная сумма. Между отдельными сообщениями должна быть пауза такой продолжительности, которая как минимум в 3,5 раза больше времени, требуемого на передачу одного байта.

Источник: https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf

Адрес – понятно… Данные, контрольная сумма – тоже известные понятия. А что такое код функции? По коду функции устройство понимает, какой тип данных с него спрашивают. То есть код функции, условно говоря, – это своего рода типизация данных в рамках протокола modbus.

В описании стандарта Modbus используются терминология, унаследованная от языков релейной логики. Так, например, некоторые регистры называются катушками (англ. coil). Протокол Modbus позволяет устройствам сопоставлять данные с четырьмя типами регистров, которые различаются различными префиксами:

  • Discrete Inputs — дискретные (двоичные) входы устройства, доступные для master-а только для чтения. То есть данный тип может хранить только “0” или “1”. Отзываются на код функции «02» — read discrete inputs или чтение группы регистров. Диапазон адресов таких входов обычно бывает с 10001 по 19999 (хотя стандарт это не определяет).
  • Coils — дискретные выходы устройства, либо какие-либо внутренние битовые значения. Доступны для чтения и записи. Имеют несколько функций: «01» — read coils или чтение группы бит, «05» — write sinlge coil или запись одного бита, «15» (0x0F) — write multiple coils или запись группы бит. Диапазон адресов регистров обычно: с 20001 по 29999. 
  • Input Registers — 16-битные входы устройства. Доступны master-у только для чтения. Имеют функцию «04» — read input register или чтение группы регистров. Обычный диапазон адресов регистров с 30001 по 39999.
  • Holding Registers — 16-битные выходы устройства, либо его внутренние данные. Доступны для чтения и записи. Имеет несколько функций: «03» — read holding registers или чтения группы регистров, «06» — write single register или запись одного регистра, «16» (0x10) — write multiple registers или запись группы регистров. Диапазон адресов регистров по умолчанию с 40001 по 49999. 

Список функций можно найти в документации к стандарту:

Источник: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf

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

Тут следует упомянуть, что стандартом регламентируются только коды функций, а диапазоны адресов регистров производители вольны изобретать по своему. Главное чтобы они вмещались в формат uint16 – то есть от 0 до 65535. Однако большинство производителей, с устройствами которых мне удалось пообщаться на практике, все таки придерживаются перечисленных выше диапазонов.

Как видим, input и holding регистры позволяют передавать только 16-битные числа. А как быть с другими типами данных? Очень просто – данные большей размерности передаются в нескольких соседних регистрах подряд. Пример: китайский датчик SD123-T10. Но порядок передачи байт может быть разным, каждый производитель решает по своему – кто то передает данные “вперед ногами”, кто-то “головой”. Поэтому программы для работы с modbus обычно предусматривают разные варианты:

Обычно это должно быть указано в техническом паспорте устройства. Хотя какой у китайцев техпаспорт – так, смех один.

На этом закончим с теоретической частью, и переходим к практической реализации – как со всем этим работать на ESP32 с использованием фреймворка ESP-IDF. Поскольку я активно использую PlatformIO, то все примеры приведены для данной IDE; но их можно перенести и в Espressif IDE.

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


Библиотека ESP-Modbus

В ESP-IDF для работы с протоколом Modbus имеется библиотека Espressif ESP-Modbus (esp-modbus), которая поддерживает работу в сетях на основе интерфейсов передачи данных RS485, Wi-Fi и Ethernet. По сути библиотека ESP-Modbus является клоном библиотеки FreeModbus, портированной на ESP32, даже название компонента так и осталось – freemodbus.

Ранее эта библиотека входила в состав ESP-IDF, но начиная с версии ESP-IDF v5.0, компонент freemodbus был перенесен из ESP-IDF в отдельный репозиторий: github.com/espressif/esp-modbus. Поэтому в проектах на ESP-IDF v5.0 и выше требуется подключать указанный компонент отдельно.

Делается это очень просто – в каталог src нужно добавить файл idf_component.yml следующего содержания:

dependencies:
  espressif/esp-modbus:
    version: "^1.0"

Что удивительно – если вы используете PlatformIO, то в platformio.ini в данном случае ничего дублировать /  добавлять не требуется. При первой компиляции проекта этот компонент будет скачан с GitHub и помещен в папку managed_components проекта.

В версии ESP-IDF v 4.x этот файл, в принципе, не требуется. НО! Реализация esp-modbus в старых версиях ESP-IDF откровенно кривовата, и работает плохо (проверено). Поэтому даже в старых версиях лучше использовать новую версию компонента. Для этого требуется отключить из сборки старую версию компонента freemodbus. Для этого добавьте в файл CMakeLists.txt проекта следующую строку:

cmake_minimum_required(VERSION 3.16.0)
set(EXCLUDE_COMPONENTS freemodbus)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(rs485_modbus)

Библиотека, как и следовало ожидать, умеет работать в двух режимах – master (управлять другими подчиненными устройствами на шине) и slave (выполнять команды старшего и отвечать на запросы). Для подключения библиотеки к проекту необходимо добавить #include "esp_modbus_master.h" или #include "esp_modbus_slave.h" в файл проекта, из которого будет осуществляться работа с протоколом, в зависимости от того, в каком режиме вы будете с ним работать. Но лучше сделать так: #include "mbcontroller.h", так как данный заголовочный файл включает оба варианта сразу.

 


Версии Espressif ESP-Modbus

update 2025-06-16

В настоящий момент на GitHub доступны две версии Espressif ESP-Modbus: 1.0.18 и 2.0.x. Версия 1.0.18, судя по всему, является последней в ветке 1.0 и далее не будет поддерживаться. 

Чем они отличаются? В версиях 2.0.x необходимо везде указывать handle modbus-контроллера, с которым мы имеем дело. Это даже хорошо, так как появилась возможность использовать два master-контроллера на одной и той же ESP. 

// Отправляем запрос
err = mbc_master_send_request(
  master_handle,                  // Указатель на хендл контроллера
  &request,                       // Параметры запроса
  &buffer[0]                      // Указатель на начало буфера
);

И немного изменилась процедура инициализации – о чем будет подробнее рассказано ниже.

Но есть одна “небольшая” проблемка: в новой ветке разработчики заблокировали запуск master-режима, если таблица дескрипторов (параметров) клиентов modbus не настроена! Тем самым заблокировали возможность работы с esp_modbus “по простому”, через использование функции mbc_master_send_request(). И судя по всему – это их принципиальная позиция.

Поэтому лично мое решение – оставаться на версии 1.0.18 как можно дольше. Пока что она меня устраивает полностью.

Для принудительного использования версии 1.0.18 укажите:

dependencies:
  espressif/esp-modbus:
    version: "1.0.18"

Для принудительного использования версии 2.0.x укажите:

dependencies:
  espressif/esp-modbus:
    version: "^2.0"

 


Выбор UART-порта для работы c RS485

Поскольку физический обмен данными по шине RS485 идет через последовательный порт UART, одновременно с инициализацией modbus нам необходимо настроить какой-либо из портов UART. ESP32 поддерживает работу с тремя портами UART одновременно (по крайней мере, “классика”), UART0 при этом всегда используется как “системный” для прошивки и вывода отладочных сообщений, поэтому нам остается UART1 и UART2. В примерах я буду использовать UART1.

 


Подопытный кролик

Для примера я взял обычный RS485 датчик температуры и влажности, внутри которого стоит сенсор SHT30. Неплохой сенсор, кстати.

Работает это чудо китайской электронной промышленности на скорости 9600 бод и имеет адрес 1 (по умолчанию, конечно – эти параметры можно изменить). Регистр температуры у него расположен по адресу 0x0001, регистр влажности – 0x0000. Оба регистра – holding, то есть читать их нужно командой 0x03.


Настройка Modbus RTU версии 1.0.x в master-режиме

Настройка Modbus устаревшей версии 1.0.13~1.0.18 в режиме мастера должна осуществляться в такой последовательности:

  1. Вызываем функцию mbc_master_init() с параметром MB_PORT_SERIAL_MASTER для инициализации необходимых структур данных.
  2. Настраиваем необходимые режимы работы modbus через вызов mbc_master_setup()
  3. Указываем выводы микроконтроллера, которые будут использоваться для передачи данных и управления потоком – uart_set_pin(). В том числе RXD (прием данных), TXD (передача данных), а также выводы переключения приема/передачи (управления потоком данных) – RTS (Ready To Send – переключить на передачу) и CTS (Clear To Send – готовность канала связи к началу передачи данных).
  4. Запускаем modbus через mbc_master_start()
  5. Настраиваем режим работы порта UART через uart_set_mode().

Причем именно в такой последовательности! При попытке изменить порядок действий (например 4 и 5) – процесс неизменно завершается ошибкой.

Здесь, наверное, стоит поподробнее рассмотреть структуру mb_communication_info_t, которая используется для настройки режимов работы:

/**
 * @brief Device communication structure to setup Modbus controller
 */
typedef union {
    // Вариант для связи поверх UART
    struct {
        mb_mode_type_t mode;                    /*!< Режим связи Modbus : MB_MODE_RTU или MB_MODE_ASCII */
        uint8_t slave_addr;                     /*!< Адрес ведомого устройства Modbus (для ведущего можно оставить 0) */
        uart_port_t port;                       /*!< Номер порта UART : UART_NUM_0, UART_NUM_1 и т.д. */
        uint32_t baudrate;                      /*!< Скорость передачи данных */
        uart_parity_t parity;                   /*!< Режим четности : UART_PARITY_DISABLE / UART_PARITY_EVEN / UART_PARITY_ODD */
        uint16_t dummy_port;                    /*!< Фиктивное поле для выравнивания записи, не используется */
    };
    // Вариант для связи поверх TCP/UDP
    struct {
        mb_mode_type_t ip_mode;                /*!< Режим связи Modbus : MB_MODE_TCP или MB_MODE_UDP */
        uint8_t slave_uid;                     /*!< Адрес ведомого устройства Modbus для UID */
        uint16_t ip_port;                      /*!< Номер IP порта */
        mb_tcp_addr_type_t ip_addr_type;       /*!< Тип адреса : IPv4 или IPv6 */
        void* ip_addr;                         /*!< Таблица адресов Modbus для подключения */
        void* ip_netif_ptr;                    /*!< Ссылка на сетевой интерфейс NETIF */
    };
} mb_communication_info_t;

Допустим, я выбрал для работы с UART выводы 16 и 17, управление потоком у меня не используется, скорость передачи данных – 9600. Тогда процедура настройки будет выглядеть так:

// Указатель на обработчик протокола modbus
void* modbus_master = NULL;

//  Настраиваем Modbus в master-режиме через порт UART1
void init_modbus_master()
{
  // Инициализация RS485 и Modbus
  ESP_ERROR_CHECK(mbc_master_init(MB_PORT_SERIAL_MASTER, &modbus_master));
  
  // Настраиваем Modbus
  mb_communication_info_t comm;
  memset(&comm, 0, sizeof(comm));
  comm.mode = MB_MODE_RTU;            // Режим Modbus RTU
  comm.port = UART_NUM_1;             // Порт UART1
  comm.baudrate = 9600;               // Скорость 9600
  comm.parity = UART_PARITY_DISABLE;  // Контроль четности отключена
  ESP_ERROR_CHECK(mbc_master_setup((void*)&comm));
  
  // Настраиваем выводы UART: TX=16, RX=17, RTS и CTS не используются
  ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, 16, 17, -1, -1));
  
  // Запускаем Modbus
  ESP_ERROR_CHECK(mbc_master_start());
  
  // Настраиваем режим HALF_DUPLEX
  ESP_ERROR_CHECK(uart_set_mode(UART_NUM_1, UART_MODE_RS485_HALF_DUPLEX));
}

Можно уже начинать отправлять запросы к slave, то есть принимать и передавать данные.

В примерах инициализации Modbus Master встречается вызов ещё одной функции – mbc_master_set_descriptor(). В моем пример её нет, почему? Потому что для самого простого способа чтения и записи регистров она не требуется. Да и на практике я её тоже никогда не использую.

 


Настройка Modbus RTU версии 2.0.x в master-режиме

Настройка Modbus версии 2.0.x в режиме мастера должна осуществляться в такой последовательности:

  1. Вызываем функцию mbc_master_create_serial() и настраиваем все параметры контроллера: порт UART, режим, скорость передачи, четность, и т.д.
  2. Указываем выводы микроконтроллера, которые будут использоваться для передачи данных и управления потоком – uart_set_pin(). В том числе RXD (прием данных), TXD (передача данных), а также выводы переключения приема/передачи (управления потоком данных) – RTS (Ready To Send – переключить на передачу) и CTS (Clear To Send – готовность канала связи к началу передачи данных).
  3. Настраиваем режим работы порта UART через uart_set_mode().
  4. Запускаем modbus через mbc_master_start()

В примерах обычно так: 1-2-4-3, но я проверил 1-2-3-4 тоже работает (в отличие от версии 1.0.x).

Структура mb_communication_info_t довольно сложная:

/**
 * @brief Device communication structure to setup Modbus controller
 */
typedef union 
{   
    mb_comm_mode_t mode;            /*!< mode option to check the communication object type*/
    mb_common_opts_t common_opts;   /*!< Common options for communication object. */
#if (CONFIG_FMB_COMM_MODE_TCP_EN)
    mb_tcp_opts_t tcp_opts;         /*!< tcp options for communication object */
#endif
#if (CONFIG_FMB_COMM_MODE_ASCII_EN || CONFIG_FMB_COMM_MODE_RTU_EN)
    mb_serial_opts_t ser_opts;      /*!< serial options for communication object */
#endif
} mb_communication_info_t;

Но в режиме COMM_MODE_ASCII или COMM_MODE_RTU нас интересует только mb_serial_opts_t ser_opts.

struct _port_serial_opts {
    mb_mode_type_t mode;            /*!< Modbus communication mode */
    uart_port_t port;               /*!< Modbus communication port (UART) number */
    uint8_t uid;                    /*!< Modbus slave address field (dummy for master) */
    uint32_t response_tout_ms;      /*!< Modbus slave response timeout */
    uint64_t test_tout_us;          /*!< Modbus test timeout (reserved) */
    uint32_t baudrate;              /*!< Modbus baudrate */
    uart_word_length_t data_bits;   /*!< Modbus number of data bits */
    uart_stop_bits_t stop_bits;     /*!< Modbus number of stop bits */
    uart_parity_t parity;           /*!< Modbus UART parity settings */
} __attribute__((__packed__));

 

Допустим, я выбрал для работы с UART выводы 16 и 17, управление потоком у меня не используется, скорость передачи данных – 9600. Тогда процедура настройки будет выглядеть так:

// Хэендл мастер-контроллера
static void *master_handle = NULL;

...

// Настраиваем modbus контроллер
mb_communication_info_t modbus_config = {0};
modbus_config.ser_opts.port = UART_NUM_1;
modbus_config.ser_opts.mode = MB_RTU;
modbus_config.ser_opts.baudrate = 9600;
modbus_config.ser_opts.parity = MB_PARITY_NONE;
modbus_config.ser_opts.response_tout_ms = 1000;
modbus_config.ser_opts.data_bits = UART_DATA_8_BITS;
modbus_config.ser_opts.stop_bits = UART_STOP_BITS_1;

// 1. Создаем master-контроллер на COM-порту
esp_err_t err = mbc_master_create_serial(
  &modbus_config,   // Указатель на config
  &master_handle    // Указатель на переменную, в которую будет записан хендл контроллера
);
if ((err != ESP_OK) || (master_handle == NULL)) {
  ESP_LOGE("MODBUS", "MB controller initialization failure: %d %s", (int)err, esp_err_to_name(err));
  return;
};

// 2. Настраиваем выводы COM-порта
err = uart_set_pin(
  UART_NUM_1,       // Номер COM-порта
  17,               // Номер вывода TXD
  16,               // Номер вывода RXD
  -1,               // Номер вывода RTS
  -1                // Номер вывода CTS
);
if (err != ESP_OK) {
  ESP_LOGE("MODBUS", "UART1 set pins failure: %d %s", (int)err, esp_err_to_name(err));
  return;
};

// 3. Устанавливаем режим Half Duplex для COM-порта
err = uart_set_mode(
  UART_NUM_1,     // Номер COM-порта
  UART_MODE_RS485_HALF_DUPLEX // Режим Half Duplex RS485
);
if (err != ESP_OK) {
  ESP_LOGE("MODBUS", "UART1 set mode failure: %d %s", (int)err, esp_err_to_name(err));
  return;
};

// 4. Запускаем modbus контроллер
err = mbc_master_start(
  master_handle     // Указатель на хендл контроллера
);
if (err != ESP_OK) {
  ESP_LOGE("MODBUS", "MB controller start failure: %d %s", (int)err, esp_err_to_name(err));
  return;
};

ESP_LOGI("MAIN", "Modbus started");

А вот здесь без mbc_master_set_descriptor() уже не обойтись! Если вы не добавили хотя бы один дескриптор, шаг 4 всегда будет выдавать ошибку…

То есть перед тем, как начинать настройку мастера, совершенно необходимо добавить хотя бы один дескриптор с помощью mbc_master_set_descriptor().

 


Чтение и запись регистров в режиме master “по простому”

Обратите внимание! Этот метод будет работать только в версиях 1.0.x. В версиях 2.0.x необходимо добавить хотя бы один дескриптор параметра перед запуском.

Если вы впервые столкнулись c Modbus на ESP32 и попробуете разобраться с протоколом Modbus по примерам, предоставляемым компанией Espressif к своей библиотеке, то наверняка подумаете, что это кошмар. Какие-то дескрипторы, параметры, обработчики, сплошная мешанина массивов и указателей со смещениями. Да, с этим всем можно разобраться, но… по большому счету в большинстве случаев всё это не нужно.

Достаточно отправить запрос к slave-устройству посредством базовой функции mbc_master_send_request() и дождаться ответа. Эта функция выполняет блокирующий запрос Modbus, что есть функция отправляет запрос и ждет ответа до тех пор, пока ведомое устройство не ответит или не наступит таймаут. Соответственно, задача, в контексте которой был осуществлен вызов mbc_master_send_request(), будет ожидать её завершения. Но таймауты не такие уж и большие, чтобы это сильно нарушало работу автоматики.

Для вызова указанной функции придется предварительно настроить и передать ей соответствующую структуру mb_param_request_t, которая выглядит так:

/**
 * @brief Modbus register request type structure
 */
typedef struct {
    uint8_t slave_addr;             /*!< Адрес slave устройства */
    uint8_t command;                /*!< Код команды (функции) */
    uint16_t reg_start;             /*!< Адрес первого регистра */
    uint16_t reg_size;              /*!< Количество регистров */
} mb_param_request_t;

Рассмотрим наш датчик в качестве примера. Еще раз напомню данные его некоторых регистров:

  • регистр температуры –  0x0001
  • регистр влажности – 0x0000
  • типы регистров – holding, то есть читать их нужно командой 0x03

Допустим, я хочу прочитать только данные температуры с датчика. Тогда код будет выглядеть так:

// Читаем температуру с сенсора
void read_sensor_data()
{
  // Буфер под данные
  int16_t value;
  // Настраиваем параметры запроса
  mb_param_request_t request_cfg = {
    .slave_addr = 0x01,     // Адрес устройства на шине
    .command    = 0x03,     // Чтение holding регистра
    .reg_start  = 0x0001,   // Адрес первого регистра
    .reg_size   = 0x0001    // Количество регистров подряд
  };
  // Отправляем запрос
  ESP_ERROR_CHECK(mbc_master_send_request(modbus_master, &request_cfg, &value));
  // Преобразовываем данные в нужный вид и выводим в лог
  ESP_LOGI("RS485", "Temperature: %f", (float)value/10.0);
}

Примечание: для версии ветки 1.0.x аргумент modbus_master при вызове функции следует исключить.

Просто? Конечно. Нужно только “помнить” коды операций, например 0x03 – чтение holding регистра, 0x06 – запись holding регистра и т.д. Но, как правило, все это имеется в документации, так что проблем возникнуть не должно.

Приведу ещё код app_main() моего примера:

void app_main() 
{
  init_modbus_master();
  while (1) {
    read_sensor_data();
    vTaskDelay(pdMS_TO_TICKS(3000));
  }
}

Хорошо, давайте прочитаем температуру и влажность сразу, за одну операцию. Это не сложнее предыдущего способа:

// Читаем температуру и влажность с сенсора
void read_sensor_data()
{
  // Буфер под данные
  int16_t value[2];
  // Настраиваем параметры запроса
  mb_param_request_t request_cfg = {
    .slave_addr = 0x01,     // Адрес устройства на шине
    .command    = 0x03,     // Чтение holding регистров
    .reg_start  = 0x0000,   // Адрес первого регистра
    .reg_size   = 0x0002    // Количество регистров подряд
  };
  // Отправляем запрос
  ESP_ERROR_CHECK(mbc_master_send_request(modbus_master, &request_cfg, &value));
  // Преобразовываем данные в нужный вид и выводим в лог
  ESP_LOGI("RS485", "Temperature: %f, humidity: %f", (float)value[1]/10.0, (float)value[0]/10.0);
}

Примечание: для версии ветки 1.0.x аргумент modbus_master при вызове функции следует исключить.

Результат выполнения:

I (312) spi_flash: detected chip: generic
I (315) spi_flash: flash io: dio
I (320) main_task: Started on CPU0
I (330) main_task: Calling app_main()
I (330) uart: queue free spaces: 20
I (350) RS485: Temperature: 28.200000, humidity: 41.300000
I (3380) RS485: Temperature: 28.200000, humidity: 41.200000
I (6400) RS485: Temperature: 28.200000, humidity: 41.200000
I (9420) RS485: Temperature: 28.200000, humidity: 41.200000

Точно также можно не только читать регистры, но и записывать их. 

Допустим, я хочу сменить адрес slave устройства:

// Изменим адрес сенсора
void set_address(uint16_t new_address)
{
  // Настраиваем параметры запроса
  mb_param_request_t request_cfg = {
    .slave_addr = 0x01,     // Адрес устройства на шине
    .command    = 0x06,     // Запись holding регистра
    .reg_start  = 0x0100,   // Адрес регистра адреса
    .reg_size   = 0x0001    // Количество регистров подряд
  };
  // Отправляем запрос
  ESP_ERROR_CHECK(mbc_master_send_request(modbus_master, &request_cfg, &new_address));
}

Примечание: для версии ветки 1.0.x аргумент modbus_master при вызове функции следует исключить.

После этого не забываем, что адрес устройства уже другой.

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


Чтение и запись регистров в режиме master с использованием таблицы параметров (дескрипторов)

Теперь давайте рассмотрим вариант, усердно продвигаемый разработчиками, то есть с использованием таблицы дескрипторов характеристик и параметров. Что такое таблица дескрипторов? Это заранее определенный перечень всех используемых в работе регистров для всех подключенных slave устройств. С подробным описанием, блекджеком и шлюхами…

Ключом к созданию такой таблицы является CID – Characteristic ID. Если вы знакомы с СУБД, то это примерно как ключевое поле таблицы. Их мы должны определить в самую первую очередь.

// Перечисление всех возможных регистров (параметров)
enum {
  CID_HOLD_HUMIDITY = 0,
  CID_HOLD_TEMPERATURE,
  CID_HOLD_ADDRESS,
  CID_COUNT
};

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

Далее описываем саму таблицу характеристик. Таблица включает следующие поля:

  • CID – уникальный идентификатор характеристики (в пределах устройства, разумеется)
  • Param Name – понятное имя параметра
  • Units – единица измерения
  • Modbus Slave Addr – адрес устройства на шине
  • Modbus Reg Type – тип регистра
  • Reg Start – начальный адрес регистра характеристики
  • Reg Size – размер характеристики в регистрах (если, например, одна характеристика занимает два регистра подряд)
  • Instance Offset – смещение данных в структуре хранения (это мы обсудим чуть ниже, пока