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

Работа с шиной 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 (а о ней, собственно, и статья) поддерживает “ручное” управление передачей данных по шине.

 

Схема модуля, найденная на просторах сети

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

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

Если вы планируете использовать шину в условиях сильных помех, рекомендуется выполнить линии 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.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", так как данный заголовочный файл включает оба варианта сразу.

 


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

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

 


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

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

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

 


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

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

  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().

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

Здесь, наверное, стоит поподробнее рассмотреть структуру 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(). В моем пример её нет, почему? Потому что для самого простого способа чтения и записи регистров она не требуется. Да и на практике я её тоже никогда не использую.

 


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

Если вы впервые столкнулись 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(&request_cfg, &value));
  // Преобразовываем данные в нужный вид и выводим в лог
  ESP_LOGI("RS485", "Temperature: %f", (float)value/10.0);
}

Просто? Конечно. Нужно только “помнить” коды операций, например 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(&request_cfg, &value));
  // Преобразовываем данные в нужный вид и выводим в лог
  ESP_LOGI("RS485", "Temperature: %f, humidity: %f", (float)value[1]/10.0, (float)value[0]/10.0);
}

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

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(&request_cfg, &new_address));
}

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

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

 


Чтение и запись регистров в режиме 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 – смещение данных в структуре хранения (это мы обсудим чуть ниже, пока 0)
  • Data Type – тип данных
  • Data Size – размер данных в байтах
  • Parameter Options – опции параметров, например минимум и максимум
  • Access Mode – режим доступа PAR_PERMS_READ, PAR_PERMS_WRITE и т.д.

Исходя из вышеизложенного примера, получаем такую таблицу:

// Макрос для объявления строк
#define STR(fieldname) ((const char*)(fieldname))
// Параметры могут использоваться как битовые маски или ограничения параметров
#define OPTS(min_val, max_val, step_val) { .min = min_val, .max = max_val, .step = step_val }

// Описание таблицы параметров (для всех slave-устройств сразу)
const mb_parameter_descriptor_t device_parameters[] = {
    // { CID, Param Name, Units, Modbus Slave Addr, Modbus Reg Type, Reg Start, Reg Size, Instance Offset, Data Type, Data Size, Parameter Options, Access Mode}
    {CID_HOLD_HUMIDITY, STR("Humidity"), STR("%rH"), 0x01, MB_PARAM_HOLDING, 0x0000, 1, 
      0, PARAM_TYPE_U16, PARAM_SIZE_U16, OPTS(0,0,0), PAR_PERMS_READ},
    {CID_HOLD_TEMPERATURE, STR("Temperature"), STR("C"), 0x01, MB_PARAM_HOLDING, 0x0001, 1, 
      0, PARAM_TYPE_U16, PARAM_SIZE_U16, OPTS(0,0,0), PAR_PERMS_READ},
    {CID_HOLD_ADDRESS, STR("Address"), STR("-"), 0x01, MB_PARAM_HOLDING, 0x0100, 1, 
      0, PARAM_TYPE_U16, PARAM_SIZE_U16, OPTS(0,255,1), PAR_PERMS_READ_WRITE},
};

// Вычисление количества параметров в таблице
const uint16_t num_device_parameters = (sizeof(device_parameters)/sizeof(device_parameters[0]));

В функцию инициализации Modbus не забудем добавить строчку:

...
  
// Регистрируем таблицу параметров
ESP_ERROR_CHECK(mbc_master_set_descriptor(&device_parameters[0], num_device_parameters));

Но и это еще не все. Пишем новую функцию чтения характеристик.

В примере ниже мы учитываем, что все наши регистры одного типа и имеют одинаковый размер. Поэтому мы смело можем использовать один и тот же буфер для чтения данных. Вначале получаем указатель на строку таблицы характеристик с помощью mbc_master_get_cid_info(), а затем по этому указателю читаем данные с устройства посредством mbc_master_get_parameter(uint16_t cid, char *name, uint8_t *value, uint8_t *type).

// Читаем характеристики по таблице
void read_modbus_parameters()
{
  // Буфер под данные (в нашем примере все данные одного размера)
  int16_t temp_data = 0;
  uint8_t type = 0;
  // Указатель на запись таблицы параметров
  const mb_parameter_descriptor_t* param_descriptor = NULL;

  // Запрашиваем все возможные параметры
  for (uint16_t cid = 0; cid < CID_COUNT; cid++) {
    // Получаем указатель на запись таблицы параметров
    esp_err_t err = mbc_master_get_cid_info(cid, &param_descriptor);
    if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) {
      // Читаем данные параметра
      err = mbc_master_get_parameter(param_descriptor->cid, (char*)param_descriptor->param_key, (uint8_t*)&temp_data, &type);
      // Выводим на экран
      if (err == ESP_OK) {
        ESP_LOGI("RS485", "Characteristic #%d %s (%s) value = %d read successful.",
          param_descriptor->cid, (char*)param_descriptor->param_key, (char*)param_descriptor->param_units, temp_data);
      } else {
        ESP_LOGE("RS485", "Characteristic #%d (%s) read fail, err = 0x%x (%s).",
          param_descriptor->cid, (char*)param_descriptor->param_key, (int)err, (char*)esp_err_to_name(err));
      };
    };
  };
}

Проверяем, всё работает:

I (310) spi_flash: detected chip: generic
I (313) spi_flash: flash io: dio
I (318) main_task: Started on CPU0
I (328) main_task: Calling app_main()
I (328) uart: queue free spaces: 20
I (348) RS485: Characteristic #0 Humidity (%rH) value = 449 read successful.
I (368) RS485: Characteristic #1 Temperature (C) value = 256 read successful.
I (388) RS485: Characteristic #2 Address (-) value = 1 read successful.
I (3398) RS485: Characteristic #0 Humidity (%rH) value = 448 read successful.
I (3418) RS485: Characteristic #1 Temperature (C) value = 256 read successful.
I (3438) RS485: Characteristic #2 Address (-) value = 1 read successful.

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

Примечание: для записи параметров используйте соответствующую функцию mbc_master_set_parameter(uint16_t cid, char *name, uint8_t *value, uint8_t *type).

 

Хорошо, а что если характеристики имеют разные типы значений и разные размеры? Или если нужно хранить последние полученные характеристики? Усложним пример.

Создадим новый тип данных (упакованную структуру) для хранения данных с какого-либо slave – устройства. Например так:

// Структура данных для хранения данных регистров
#pragma pack(push, 1)
typedef struct
{
  uint16_t humidity;
  uint16_t temperature;
  uint16_t address;
} holding_reg_params_t;
#pragma pack(pop)
holding_reg_params_t holding_reg_params = {0};

// Макросы для получения смещения параметра в соответствующей структуре.
#define HOLD_OFFSET(field) ((uint16_t)(offsetof(holding_reg_params_t, field) + 1))

Обратите внимание: структура должна быть обязательно “упакована”, дабы избежать ошибок при вычислении смещения полей.

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

Теперь добавим в таблицу информацию о том, по какому смещению хранится то или иное значение в записи – HOLD_OFFSET(…):

// Описание таблицы параметров (для всех slave-устройств сразу)
const mb_parameter_descriptor_t device_parameters[] = {
    // { CID, Param Name, Units, Modbus Slave Addr, Modbus Reg Type, Reg Start, Reg Size, Instance Offset, Data Type, Data Size, Parameter Options, Access Mode}
    {CID_HOLD_HUMIDITY, STR("Humidity"), STR("%rH"), 0x01, MB_PARAM_HOLDING, 0x0000, 1, 
      HOLD_OFFSET(humidity), PARAM_TYPE_U16, PARAM_SIZE_U16, OPTS(0,0,0), PAR_PERMS_READ},
    {CID_HOLD_TEMPERATURE, STR("Temperature"), STR("C"), 0x01, MB_PARAM_HOLDING, 0x0001, 1, 
      HOLD_OFFSET(temperature), PARAM_TYPE_U16, PARAM_SIZE_U16, OPTS(0,0,0), PAR_PERMS_READ},
    {CID_HOLD_ADDRESS, STR("Address"), STR("-"), 0x01, MB_PARAM_HOLDING, 0x0100, 1, 
      HOLD_OFFSET(address), PARAM_TYPE_U16, PARAM_SIZE_U16, OPTS(0,255,1), PAR_PERMS_READ_WRITE},
};

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

// Читаем характеристики по таблице
void read_modbus_parameters()
{
  uint8_t type = 0;
  // Указатель на запись таблицы параметров
  const mb_parameter_descriptor_t* param_descriptor = NULL;

  // Запрашиваем все возможные параметры
  for (uint16_t cid = 0; cid < CID_COUNT; cid++) {
    // Получаем указатель на запись таблицы параметров
    esp_err_t err = mbc_master_get_cid_info(cid, &param_descriptor);
    if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) {
      // Получаем указатель на хранилище данных параметра: указатель на глобальную переменную + смещение - 1
      void* temp_data_ptr = (void*)&holding_reg_params + param_descriptor->param_offset - 1;
      // Читаем данные параметра
      err = mbc_master_get_parameter(param_descriptor->cid, (char*)param_descriptor->param_key, (uint8_t*)temp_data_ptr, &type);
      // Выводим на экран
      if (err == ESP_OK) {
        ESP_LOGI("RS485", "Characteristic #%d %s (%s) value = %d read successful.",
          param_descriptor->cid, (char*)param_descriptor->param_key, (char*)param_descriptor->param_units, *(int16_t*)temp_data_ptr);
      } else {
        ESP_LOGE("RS485", "Characteristic #%d (%s) read fail, err = 0x%x (%s).",
          param_descriptor->cid, (char*)param_descriptor->param_key, (int)err, (char*)esp_err_to_name(err));
      };
    };
  };
}

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

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

 


Работа с modbus в slave-режиме 

Иногда необходимо не читать данные с подчиненных устройств, а наоборот, отправлять данные по команде ведущего устройства. Для чего это может понадобится?

  • Например, можно создать метеостанцию, которая будет считывать данные с одного или нескольких датчиков, а затем “отдавать” их по шине RS485 на центральное устройство. Тем самым можно решить проблему коротких проводов для I2C-датчиков.
  • Можно передавать данные с одной ESP32 на другую – таким образом головное устройство может собирать данные с периферийных, но более надежным способом, чем через WiFi и локальный MQTT-брокер. 

API Modbus slave устроено похожим образом – вы должны заранее:

  • определить таблицу характеристик (регистров),
  • объединить регистры в группы (области, зоны…),
  • создать для данных характеристик соответствующие переменные (хранилища данных)
  • зарегистрировать все это хозяйство перед запуском режима slave

Всё, больше ничего не требуется. При получении запроса от мастера API само прочитает данные из указанных переменных (хранилища данных) и отправит ведущему.

Приступим к реализации. Для начала настроим Modbus и UART-порт, почти как в случае с master-ом:

// Инициализация RS485 и Modbus в режиме slave
ESP_ERROR_CHECK(mbc_slave_init(MB_PORT_SERIAL_SLAVE, &modbus_slave));
  
// Настраиваем Modbus
mb_communication_info_t comm;
memset(&comm, 0, sizeof(comm));
comm.mode = MB_MODE_RTU;            // Режим Modbus RTU 
comm.slave_addr = 1;                // Адрес устройства
comm.port = UART_NUM_1;             // Порт UART1
comm.baudrate = 9600;               // Скорость 9600
comm.parity = UART_PARITY_DISABLE;  // Контроль четности отключена
ESP_ERROR_CHECK(mbc_slave_setup((void*)&comm));
  
// Настраиваем выводы UART: TX=16, RX=17, RTS и CTS не используются
ESP_ERROR_CHECK(uart_set_pin(UART_NUM_1, 16, 17, -1, -1));

Как видите, очень похоже. Только везде master сменилось на slave. И теперь дополнительно необходимо указать адрес slave-устройства. При необходимости и желании его (адрес) можно не задавать “жестко” в коде, а считывать из NVS, например.

Теперь нужно позаботиться о регистрах и картофелехранилищах. Допустим мое устройство будет обрабатывать 16 holding – регистров, начиная с адреса 0x0000.

// Количество регистров подряд
#define MB_REG_HOLDING_CNT    (16)                   
// Адрес первого регистра в области
#define MB_REG_HOLDING_START  (0x0000)                

// Массив для хранения данных регистров
uint16_t holding_reg_area[MB_REG_HOLDING_CNT] = {0}; 

Теперь укажем modbus эти регистры с помощью mbc_slave_set_descriptor(mb_register_area_descriptor_t descr_data):

// Указываем области регистров, которые будет обслуживать slave устройство
mb_register_area_descriptor_t reg_area;
reg_area.type = MB_PARAM_HOLDING;                    // Тип регистров в области
reg_area.start_offset = MB_REG_HOLDING_START;        // Начальный адрес области в протоколе Modbus
reg_area.address = (void*)&holding_reg_area[0];      // Указатель на массив данных, в которых хранятся данные регистров
reg_area.size = sizeof(holding_reg_area);            // Размер области хранения данных в байтах
ESP_ERROR_CHECK(mbc_slave_set_descriptor(reg_area));

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

Продолжаем настройку – запускаем modbus slave:

// Запускаем Modbus
ESP_ERROR_CHECK(mbc_slave_start());
  
// Настраиваем режим HALF_DUPLEX
ESP_ERROR_CHECK(uart_set_mode(UART_NUM_1, UART_MODE_RS485_HALF_DUPLEX));

И на этом, собственно, все…. Никакой отправкой и получением данных “вручную” заниматься не придется. API само считает данные из holding_reg_area и отправит их по запросу.

А как же совместный доступ к данным из других задач? Совершенно правильный вопрос! При доступе к holding_reg_area придется оборачивать это добро в критическую секцию, дабы в этот момент Modbus API не попытался сунуться в эту же самую область памяти:

// Мьютекс для защиты области хранения данных регистров от изменения из другой задачи
static portMUX_TYPE holding_reg_area_lock = portMUX_INITIALIZER_UNLOCKED;

// Чтение или изменение данных в массиве из другой задачи
void task_exec(void* param) 
{
  ...
  portENTER_CRITICAL(&holding_reg_area_lock);
  holding_reg_area[2] = 10;
  portEXIT_CRITICAL(&holding_reg_area_lock);
  ...
}

Ок. Следующий вопрос. А как можно узнать, что приходил сосед и брал молоток мастер и запрашивал данные. Для этого есть специальная функция – mbc_slave_check_event(). Она ждет заданного в единственном аргументе события, и на это время полностью блокирует выполнения цикла текущей задачи. Как только событие произойдет – программа начнет выполняться дальше. Уточнить данные, что именно произошло, можно с помощью другой функции – mbc_slave_get_param_info(). Например так:

// Цикл обработки запросов от мастера
mb_param_info_t reg_info;
while (1) {
  // Ждем событий чтения или записи регистра от ведущего с блокировкой цикла
  mb_event_group_t event = mbc_slave_check_event(MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD);
  // Получаем данные о регистре
  ESP_ERROR_CHECK_WITHOUT_ABORT(mbc_slave_get_param_info(&reg_info, pdMS_TO_TICKS(100)));
  // Что-то делаем... например считаем количество обращений мастера к любым регистрам
  portENTER_CRITICAL(&holding_reg_area_lock);
  holding_reg_area[0] += 1;
  portEXIT_CRITICAL(&holding_reg_area_lock);
};

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

 


Настройки SDK Config

Все это работает очень даже замечательно. Но!

Бывают случаи, когда подключенное slave устройство вдруг начинает отвечать казалось бы неверными данными, причем это недоразумение устраняется только путем перезагрузки всей системы. Полазив по форумам, я понял, что данная ситуация иногда возникает из-за сбоя в кольцевом буфере UART-порта – там происходит какой-то сдвиг данных ну и все пошло-поехало наперекосяк. В глубины причин такого поведения я не полез, мне достаточно было найти решение проблемы. А для решения данной проблемы оказалось достаточно поднять приоритет задачи Modbus до высоких значений, дабы второстепенные задачи не отвлекали modbus от обработки данных.

Итак, если вы заметили такие странности, выполните следующие настройки. Запустите SDK Config pio run -t config и ищем раздел (Top) → Component config → Modbus configuration:

Нажмите для увеличения

В данном разделе довольно много параметров, но в нашем случае нам нужен только Modbus port task priority. Достаточно поднять его значение выше всех остальных прикладных задач, например до 20~22, и проблема будет решена. Можете попробовать “поиграться” с другими параметрами, но только если понимаете на что они влияют. Мой принцип здесь таков – “не трож настройки, пока все работает“. Не забудьте сохранить изменение и перекомпилируйте проект.

 


Ну а на этом я заканчиваю данное повествование, разрешите откланяться, с вам был Александр aka kotyara12. Благодарю за ваше внимание.

PS: Если вдруг я чего-то упустил, или что-то не понятно, или в чем-то ошибся, где-то пропустил запятую – не стесняйтесь сообщить в комментариях или в чате telegram. Буду исправлять и исправляться, или дополнять. Но если статья будет перенесена на Дзен – там, как правило, статьи я уже не исправляю и не дополняю, просто потому что в процессе правок на нескольких ресурсах легко запутаться.

Ну а исходники к примерам, как всегда, доступны на GitHub: github.com/kotyara12/dzen

 


Ссылки

  1. modbus.org
  2. ESP-Modbus Library :: Espressif Documentation
  3. ESP-Modbus Master API :: Espressif Documentation
  4. ESP-Modbus Slave API :: Espressif Documentation

 

💠 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 5 из 5, всего 5 оценок ]

6 комментариев для “Работа с шиной RS485 и протоколом Modbus RTU на ESP32”

  1. Аркадий

    Всё очень хорошо написано, особенно, что касается нюансов ESP-IDF. Спасибо!
    Хотелось бы уточнить, что Discret Inputs и Coils – это не регистры, а именно биты в битовом адресном пространстве устройства. Т.е. каждый бит имеет свой адрес от 0 до 65535. И в спецификации modbus диапазоны адресов для разных данных (Inputs, Coils, Holding Registers …) никак не регламентированы. На адрес отводится лишь два байта в PDU.
    Скорость передачи тоже никак не устанавливается – может быть любая, какую потянут и на какую рассчитаны устройства на шине. В режиме RTU байты в сообщении должны идти одним куском и интервал времени больше двух байтовых кадров при при приёме рассматривается как окончание пакета. А при передаче условлено, что интервал межу двумя последовательными пакетами делать не менее 3,5 кадров.
    Да, ещё иногда используется широковещательные (broadcast) сообщениия от ведущего. Для этого используется адрес 255. Ведомые на такое сообщение не отвечают.
    И ещё, есть так называемые “исключения” (exclude) – это неприятности, которые могут случиться с ведомым. Например, ведущий неверно передал адрес регистра ведомого или ведомый настолько сильно занят, что способен передать только короткий ответ с указанием на это обстоятельство.
    И ещё не очень приятный момент: в протоколе используется порядок байт big-endian, т.е. на шину первым выдаётся старший байт двухбайтового числа (кроме CRC16 в конце пакета – там первым идёт младший).
    В общем, описанные вещи должны решаться и решаются на уровне библиотеки.
    По китайскому преобразователю TTL-485 тоже хочу сказать, что не очень то доверял бы автопереключению направления, который там реализован. Может, на небольших скоростях он и работает, а на 115200, например, – не уверен. У меня такой лежит уже давно, всё хочу его погонять с пристрастием и с осциллографом, да на длинном проводе. Справедливости ради, следует сказать, что есть в природе микросхема MAX13487 (те же 8 ног) – там тоже реализовано автопереключение, но несколько по-другому (контролируется еще непостредственно состояние шины)

    1. По поводу порядка байт данных, что то я не помню что бы в протоколе modbus это регламентировалось. Сейчас пробежался по протоколу и не нашёл. CRC да, регламентировано – младший первый.
      И по опыту могу сказать, что каждый производитель делает как хочет порядок в данных.

  2. Аркадий

    Вот, ещё обнаружил неточность в своём же комментарии. Здесь https://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
    пишут, что для широковещательных сообщений используют адрес 0, адреса с 1 по 247 могут выбирать себе устройства на шине (ведомые), а адреса с 247 по 255 вообще зарезервированы. Но всё это, я так понял, носит рекомендательный характер.

  3. Большое спасибо за статью. Полностью согласен с тем, что пример с библиотекой Modbus от Espressif с использованием таблицы параметров тяжел для освоения. В частности, в примере данные, передающиеся через Holdings, представлены типами float, а датчик, который есть у меня, выдает данные типа двухбайтных целых. Чтобы модифицировать пример от Espressif пришлось повозиться.
    Поэтому отдельное спасибо за главу “Чтение и запись регистров в режиме master “по простому”. Представляется, что во многих случаев для приложений DIY такого механизма будет достаточно.
    По примеру с таблицами парамеров в этой статье. Полезным было бы добавить также информацию о директиве #define OPTS() { }, о том, как работают её параметры.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *