Добрый день, уважаемый читатель!
В данной статье разберем основные принципы работы с интерфейсом RS-485 и протоколом Modbus RTU применительно к микроконтроллеру ESP32 при условии использования для программирования платформы (фреймворка) ESP-IDF.
Дабы было понятно, что к чему, совсем чуть-чуть пройдемся и по теоретическим аспектам, но данная статья отнюдь не претендует на полноту освещения вопросов электроники и спецификаций протокола modbus – скорее эти вопросы рассмотрены только для понимания того, что и как подключать и как с этим работать.
Несмотря на бурный расцвет беспроводных протоколов и технологий, я всё таки чаще предпочитаю старый добрый кабель, особенно в особенно ответственных устройствах, от которых требуется бесперебойная работа. Применение интерфейсов RS485 + Modbus открывает перед разработчиком возможность использования самых различных промышленных периферийных устройств – сенсоров, реле, дисплеев, метеостанций и т.д. с проводным подключением на достаточно большие расстояния. Про некоторые из них я уже не раз писал на данном сайте – например раз, два, три.
Чем хорош интерфейс 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, то наводка на этот провод может исказить сигнал относительно хорошо поглощающего наводки общего провода (“земли”). Кроме того, на сопротивлении длинного общего провода будет падать разность потенциалов земель – дополнительный источник искажений. А при дифференциальной передаче искажения не происходит. В самом деле, если два провода пролегают близко друг к другу, да еще перевиты, то наводка на оба провода одинакова. Потенциал в обоих одинаково нагруженных проводах изменяется одинаково, при этом информативная разность потенциалов остается без изменений.
Именно эта особенность позволяет создавать длинные линии связи (по стандарту – до 1200 метров) для цифровых устройств в условиях высокого уровня помех. Из-за этого данный стандарт широко применяется в промышленности.
Любое устройство для RS-485 (как и UART) можно разделить на приемник (Receiver) и передатчик (Transmitter). Строго говоря, существуют два варианта такого интерфейса – RS-422 и RS-485:
- RS-422 – полнодуплексный интерфейс. Прием и передача идут по двум отдельным парам проводов. На каждой паре проводов может быть только по одному передатчику и одному приемнику. Похожий принцип передачи используется в ethernet-сетях.
- RS-485 – полудуплексный интерфейс. Прием и передача идут по одной паре проводов с разделением по времени. В одной сети (на одной паре проводов) может быть много передатчиков, так как они могут отключаются в режиме приема – “один вещает, остальные внимают“. Примерная схема соединений может выглядеть как на рисунке ниже:
Выводы устройства согласования означают следующее:
- 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 закончим, этого будет вполне достаточно для понимания происходящего ниже. Если вам будет интересно “углубить и расширить” свои познания по данному вопросу, вот вам несколько ссылок:
- https://www.ivtechno.ru/articles-one?id=19,
- https://dmr.md/2019/01/11/rs-485/
- https://easyelectronics.ru/interfejs-rs-485.html
Ну а нам для того, чтобы подключить микроконтроллер к шине RS485, потребуется собственно описанный приемопередатчик, или иными словами преобразователь интерфейса TTL – RS485. Как правило, для этого используется микросхема Maxim MAX485 или её аналоги, но проще использовать готовые модули, о них и поговорим в следующем разделе.
Преобразователи интерфейса TTL – RS485
Для того, чтобы соединить две ESP32, можно использовать две микросхемы MAX485 или аналоги, о чем, собственно и говорит схема из примера использования протокола modbus для esp32:
Datasheet на MAX485 говорит нам, что эти микросхемы рассчитаны на напряжение питания 5В, а для 3.3В следует использовать другой вариант – MAX3485, но и самый обычный MAX485 прекрасно работает от 3,3В. По крайней мере я никаких сбоев и неудобств пока что не наблюдал.
Но Maxim Integrated не единственный производитель чипов для RS485, их сейчас выпускается очень и очень много, ну например:
- MAX485 & MAX3485 – Maxim Integrated : datasheet & datasheet
- SN75176 – Texas Instuments : datasheet
- CS48520S – Shanghai Chipanalog Microelectronics : datasheet
- и т.д.
Китайские производители чипов предлагают множество вариантов, я думаю, их даже не стоит перечислять.
Для подключения микросхемы приемопередатчика к микроконтроллеру может использоваться от двух до трех выводов:
- 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 уже имеет собственный механизм контроля целостности.
Подчиненные устройства прослушивают сообщения ведущего и просто отвечают в соответствии с инструкциями. Ведущий всегда управляет связью и может связываться напрямую с одним ведомым (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 раза больше времени, требуемого на передачу одного байта.
Адрес – понятно… Данные, контрольная сумма – тоже известные понятия. А что такое код функции? По коду функции устройство понимает, какой тип данных с него спрашивают. То есть код функции, условно говоря, – это своего рода типизация данных в рамках протокола 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.
Список функций можно найти в документации к стандарту:
На рисунке ниже показан пример сопоставления данных устройства с четырьмя типами регистров.
Тут следует упомянуть, что стандартом регламентируются только коды функций, а диапазоны адресов регистров производители вольны изобретать по своему. Главное чтобы они вмещались в формат 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 в режиме мастера должна осуществляться в такой последовательности:
- Вызываем функцию mbc_master_init() с параметром MB_PORT_SERIAL_MASTER для инициализации необходимых структур данных.
- Настраиваем необходимые режимы работы modbus через вызов mbc_master_setup()
- Указываем выводы микроконтроллера, которые будут использоваться для передачи данных и управления потоком – uart_set_pin(). В том числе RXD (прием данных), TXD (передача данных), а также выводы переключения приема/передачи (управления потоком данных) – RTS (Ready To Send – переключить на передачу) и CTS (Clear To Send – готовность канала связи к началу передачи данных).
- Запускаем modbus через mbc_master_start()
- Настраиваем режим работы порта 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 в режиме мастера должна осуществляться в такой последовательности:
- Вызываем функцию mbc_master_create_serial() и настраиваем все параметры контроллера: порт UART, режим, скорость передачи, четность, и т.д.
- Указываем выводы микроконтроллера, которые будут использоваться для передачи данных и управления потоком – uart_set_pin(). В том числе RXD (прием данных), TXD (передача данных), а также выводы переключения приема/передачи (управления потоком данных) – RTS (Ready To Send – переключить на передачу) и CTS (Clear To Send – готовность канала связи к началу передачи данных).
- Настраиваем режим работы порта UART через uart_set_mode().
- Запускаем 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 – смещение данных в структуре хранения (это мы обсудим чуть ниже, пока














