Решил написать данную статью с связи с повышенным интересом посетителей нашего сайте к работе с протоколом Modbus в плате Arduino. Ранее на нашем сайте уже рассматривалась работа с протоколом Modbus в платах Arduino (в которых вы можете посмотреть описание данного протокола и работу с Modbus RTU по интерфейсу RS-485 в Arduino):
- последовательная связь по протоколу Modbus RS-485 с Arduino (ведомой) - статья, которая как раз и вызвала огромный интерес посетителей нашего сайта. В ней рассматривается реализация команды 0x10 — запись значений в несколько регистров хранения (Preset Multiple Registers) в протоколе Modbus;
- последовательная связь по протоколу Modbus RS-485 с Arduino (ведущей) - в ней рассматривается реализация команды 0x06 — запись значения в один регистр хранения (Preset Single Register) в протоколе Modbus;
Но в комментариях к данным статья посетители нашего сайта просили также рассмотреть реализацию команд 0x03 (чтение значений из нескольких регистров хранения, Read Holding Registers) и 0x04 (чтение значений из нескольких регистров ввода, Read Input Registers) - именно эти команды чаще всего и используются для считывания информации с различных датчиков, подключенных к плате Arduino по протоколу Modbus. Эти вопросы мы и постараемся рассмотреть в данной статье.
К сожалению, в настоящий момент (на момент написания данной статьи) у меня временно нет доступа к оборудованию чтобы на практике проверить решения, описанные в данной статье, поэтому пока картинок с собранным проектом и видео, демонстрирующее работу проекта, не будет (возможно, они появятся в дальнейшем). Если у кого то получится собрать описанные в данной статье решения, то просьба прислать мне фото и видео собранных проектов - я их добавлю в статью и укажу что вы их автор. Также в комментариях приветствуется любая конструктивная критика описанных в статье решений и можете задавать вопросы по материалу статьи - по возможности буду стараться на них ответить.
Самый обширный материал по использованию протокола Modbus в англоязычном сегменте интернета я нашел на сайте instructables - могу перевести ряд статей с данного сайта если нужно. Но здесь необходимо чтобы вы подсказали мне какие статьи с данного сайта представляют наибольшую ценность для русскоязычных пользователей сети, потому что я сам пока не могу решить какие из них самые интересные/востребованные. Также, если вы найдете другие интересные статьи в интернете на английском языке, касающиеся использования протокола Modbus, то могу рассмотреть вопрос их перевода для нашего сайта (раз уж эта тематика вызвала такой интерес).
Общие принципы проекта
Итак, предположим у нас есть несколько датчиков (температуры, влажности, освещенности, движения и т. д.), которые работают про протоколу Modbus. Ими мы и хотим управлять (считывать с них информацию) с помощью нашей платы Arduino. Эти решения могут применяться для создания так называемого "умного дома", но не только - им можно найти и множество других применений. Достоинствами такого решения является дешевизна, универсальность (поскольку протокол Modbus сейчас весьма популярен) и, если мы используем протокол Modbus по интерфейсу RS-485 - то возможность связать в единую сеть достаточно удаленные друг от друга объекты (до 1200 метров).
Схематично изобразить данную ситуацию для случая управления (считывания информации) платой Arduino одним датчиком по протоколу Modbus RTU через интерфейс RS-485 можно с помощью следующего рисунка (картинка взята с сайта https://forum.arduino.ua/viewtopic.php?id=1081 - там вкратце вы можете прочитать суть описываемой проблемы):
Для считывания информации с датчика в данном случае плата Arduino должна работать в качестве ведущей (Master) в протоколе Modbus RTU, а для считывания информации можно использовать команды 0x03 (чтение Holding Registers) и 0x04 (чтение Input Registers) данного протокола.
На основе анализа информации по данным вопросам в сети интернет для решения поставленной задачи (считывание информации с датчиков в Arduino по протоколу Modbus) лично мне больше всего понравились библиотеки ModbusRtu и ArduinoModbus. Их использование для данной задачи мы и рассмотрим в статье.
Использование библиотеки ModbusRtu для считывания информации с датчиков
Использование библиотеки Modbus RTU мы уже рассматривали на нашем сайте в этой статье, но в ней мы рассмотрели только использование команды 0х10 протокола Modbus, теперь же рассмотрим реализацию с ее помощью команды 0x03 (чтение Holding Registers/регистров хранения).
Пример использования библиотеки ModbusRtu для считывания информации с ведомого (slave) устройства расположен по данному адресу - с его помощью можно передать запрос на считывание массива данных с ведомого устройства, работающего по протоколу Modbus (кому интересно, пример практического применения использования данного фрагмента кода можно посмотреть здесь - http://forum.amperka.ru/threads/modbus-master.18586/). В примере рассмотрено использование протокола Modbus по интерфейсам USB или RS232 - для доработки примера под интерфейс RS-485 можно использовать решения из уже упоминавшейся статьи. Если вы хотите опробовать работу примера с помощью эмулятора на компьютере, то в качестве подобного эмулятора автор библиотеки рекомендует использовать программу Modbus slave, которую можно скачать по адресу - https://www.modbusdriver.com/diagslave.html.
Далее приведу код этого примера с переведенными комментариями. Если будет что то не получаться, пишите в комментариях внизу статьи, будем пробовать разобраться.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
#include <ModbusRtu.h> // data array for modbus network sharing (массив данных, который мы хотим считывать) uint16_t au16data[16]; uint8_t u8state; /** * Modbus object declaration (объявление объекта Modbus) * u8id : node id = 0 for master, = 1..247 for slave (адрес ведущего или ведомого) * port : serial port (последовательный порт) * u8txenpin : 0 for RS-232 and USB-FTDI (0 для RS-232 и USB-FTDI) * or any pin number > 1 for RS-485 (> 1 для RS-485) */ Modbus master(0,Serial,0); // this is master and RS-232 or USB-FTDI (ведущий, RS-232 или USB-FTDI) /** * This is an structe which contains a query to an slave device * (структура (класс), которая содержит запрос к ведомому устройству) */ modbus_t telegram; unsigned long u32wait; void setup() { Serial.begin( 19200 ); // baud-rate at 19200 (бодовая скорость) master.start(); master.setTimeOut( 2000 ); // if there is no answer in 2000 ms, roll over (если нет ответа более 2000 мс, продолжаем) u32wait = millis() + 1000; u8state = 0; } void loop() { switch( u8state ) { case 0: if (millis() > u32wait) u8state++; // wait state (ждем состояния) break; case 1: telegram.u8id = 1; // slave address (адрес ведомого) telegram.u8fct = 3; // function code (this one is registers read) (код функции) telegram.u16RegAdd = 1; // start address in slave (стартовый адрес в ведомом устройстве - с которого мы будем начинать считывать) telegram.u16CoilsNo = 4; // number of elements (coils or registers) to read (число элементов (регистров), которые мы хотим считать) telegram.au16reg = au16data; // pointer to a memory array in the Arduino (указатель в памяти на массив Arduino) master.query( telegram ); // send query (only once) (передаем запрос) u8state++; break; case 2: master.poll(); // check incoming messages (проверяем поступившее сообщение) if (master.getState() == COM_IDLE) { // если в ответ пришла "пустота" - то есть slave нам не ответил u8state = 0; u32wait = millis() + 100; } break; } } |
На мой взгляд, в этом примере все достаточно логично: сначала мы передаем запрос ведомому с помощью функции master.query, а затем, когда значение переменной u8state станет равным 2, мы считываем данные от ведомого с помощью функции master.poll в наш массив au16data. Далее считанные данные из этого массива мы можем использовать по своему усмотрению - передавать их в окно монитора последовательной связи, выполнять какие-либо команды и т.д.
Также для библиотеки ModbusRtu доступен расширенный пример считывания информации с ведомого устройства, в котором кроме команды 0x03 (чтение Holding Registers/регистров хранения) рассмотрено также использование команды 0x06 (запись значения в один регистр хранения, Preset Single Register) - актуально для датчиков/программируемых контроллеров, которые поддерживают такую возможность. Далее приведен код этого примера с переведенными комментариями.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#include <ModbusRtu.h> uint16_t au16data[16]; //!< (массив данных, который мы хотим считывать) uint8_t u8state; //!< machine state (номер состояния) uint8_t u8query; //!< pointer to message query )указатель на сообщение с запросом) /** * Modbus object declaration (объявление объекта Modbus) * u8id : node id = 0 for master, = 1..247 for slave (адрес ведущего или ведомого) * port : serial port (последовательный порт) * u8txenpin : 0 for RS-232 and USB-FTDI (0 для RS-232 и USB-FTDI) * or any pin number > 1 for RS-485 (> 1 для RS-485) */ Modbus master(0,Serial,0); // this is master and RS-232 or USB-FTDI (ведущий, RS-232 или USB-FTDI) /** * (структура (класс), которая содержит запрос к ведомому устройству) * This is an structe which contains a query to an slave device */ modbus_t telegram[2]; unsigned long u32wait; void setup() { // telegram 0: read registers (считываем регистры) telegram[0].u8id = 1; // slave address (адрес ведомого) telegram[0].u8fct = 3; // function code (this one is registers read) (код функции) telegram[0].u16RegAdd = 0; // start address in slave (стартовый адрес в ведомом устройстве - с которого мы будем начинать считывать) telegram[0].u16CoilsNo = 4; // number of elements (coils or registers) to read (число элементов (регистров), которые мы хотим считать) telegram[0].au16reg = au16data; // pointer to a memory array in the Arduino (указатель в памяти на массив Arduino) // telegram 1: write a single register (записываем информацию в один регистр хранения) telegram[1].u8id = 1; // slave address (адрес ведомого) telegram[1].u8fct = 6; // function code (this one is write a single register) (код функции) telegram[1].u16RegAdd = 4; // start address in slave telegram[1].u16CoilsNo = 1; // number of elements (coils or registers) to read (число элементов (регистров), которые мы хотим записать) telegram[1].au16reg = au16data+4; // pointer to a memory array in the Arduino (указатель в памяти на массив Arduino) Serial.begin( 19200 ); // baud-rate at 19200 (бодовая скорость) master.start(); master.setTimeOut( 5000 ); // if there is no answer in 5000 ms, roll over (если нет ответа более 5000 мс, продолжаем) u32wait = millis() + 1000; u8state = u8query = 0; } void loop() { switch( u8state ) { case 0: if (millis() > u32wait) u8state++; // wait state (ждем) break; case 1: master.query( telegram[u8query] ); // send query (only once) (передаем запрос) u8state++; u8query++; if (u8query > 2) u8query = 0; break; case 2: master.poll(); // check incoming messages (проверяем поступившее сообщение) if (master.getState() == COM_IDLE) { u8state = 0; u32wait = millis() + 1000; } break; } au16data[4] = analogRead( 0 ); } |
В данном примере мы с помощью команды master.query(telegram[u8query]) в зависмости от значения переменной u8query передаем ведомому устройству запрос либо на считывание информации, либо на запись одиночного регистра.
Использование библиотеки ArduinoModbus для считывания информации с датчиков
Библиотека ArduinoModbus рекомендована официальным сообществом Arduino для работы с протоколом Modbus. Информация с кратким описанием данной библиотеки (на английском языке) и доступных в ней функций находится на данной странице.
Для тех, кто хочет освоить работу с данной библиотекой, можно посмотреть пример ее использования для считывания информации с датчика температуры и влажности по протоколу Modbus RTU с помощью интерфейса RS-485. Тип используемого датчика приведен в комментариях вверху примера. Считывание данных температуры и влажности происходит каждые 5 секунд, после чего считанные данные передаются в окно монитора последовательной связи. Пример рассчитан на использование специального шилда для Arduino для работы по интерфейсу RS-485 (MKR 485 shield), для использования модуля MAX485 TTL to RS485 его необходимо немного переделать в соответствии с решениями из уже упоминавшейся статьи. Далее приведен текст этого примера с переведенными комментариями.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <ArduinoModbus.h> float temperature; float humidity; void setup() { Serial.begin(9600); while (!Serial); Serial.println("Modbus Temperature Humidity Sensor"); // start the Modbus RTU client (начало работы клиента Modbus RTU) if (!ModbusRTUClient.begin(9600)) { Serial.println("Failed to start Modbus RTU Client!"); while (1); } } void loop() { // send a Holding registers read request to (slave) id 1, for 2 registers // передаем запрос на считывание 2-х Holding registers (регистров хранения) ведомому устройству с id (идентификатором) 1 if (!ModbusRTUClient.requestFrom(1, HOLDING_REGISTERS, 0x00, 2)) { Serial.print("failed to read registers! "); Serial.println(ModbusRTUClient.lastError()); } else { // If the request succeeds, the sensor sends the readings, that are // stored in the holding registers. The read() method can be used to // get the raw temperature and the humidity values. // если запрос будет успешным, то датчик передаст в ответ измеренные значения // которые хранятся в holding registers. Для считывания этих значений ("сырых", необработанных) // температуры и влажности можно использовать метод read() short rawtemperature = ModbusRTUClient.read(); short rawhumidity = ModbusRTUClient.read(); // To get the temperature in Celsius and the humidity reading as // a percentage, divide the raw value by 10.0. // чтобы получить значение температуры в градусах Цельсия, а значения влажности // в процентах, разделим полученные с датчика значения на 10 temperature = rawtemperature / 10.0; humidity = rawhumidity / 10.0; Serial.println(temperature); Serial.println(humidity); } delay(5000); } |
Как видите, текст примера достаточно простой и я думаю он не вызовет у вас затруднений при реализации.
Следующий пример библиотеки ArduinoModbus, который я счел нужным рассмотреть в данной статье (его код доступен на этой странице), демонстрирует почти все возможные операции в протоколе Modbus:
- writeCoilValues() - запись регистров флагов;
- readCoilValues() - считывание регистров флагов;
- readDiscreteInputValues() - считывание дискретных входов;
- writeHoldingRegisterValues() - запись регистров хранения;
- readHoldingRegisterValues() - считывание регистров хранения;
- readInputRegisterValues() - считывание регистров ввода.
Далее приведен код этого примера с переведенными комментариями.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
#include <ArduinoRS485.h> // ArduinoModbus зависит от библиотеки ArduinoRS485 #include <ArduinoModbus.h> int counter = 0; void setup() { Serial.begin(9600); while (!Serial); Serial.println("Modbus RTU Client Kitchen Sink"); // start the Modbus RTU client (старт работы клиента Modbus RTU) if (!ModbusRTUClient.begin(9600)) { Serial.println("Failed to start Modbus RTU Client!"); while (1); } } void loop() { writeCoilValues(); readCoilValues(); readDiscreteInputValues(); writeHoldingRegisterValues(); readHoldingRegisterValues(); readInputRegisterValues(); counter++; delay(5000); Serial.println(); } void writeCoilValues() { // set the coils to 1 when counter is odd // устанавливаем регистры флагов (coils) в 1 когда значение counter нечетно byte coilValue = ((counter % 2) == 0) ? 0x00 : 0x01; Serial.print("Writing Coil values ... "); // write 10 Coil values to (slave) id 42, address 0x00 // записываем 10 регистров флагов ведомому с идентификатором (адресом) 42, с адреса 0x00 ModbusRTUClient.beginTransmission(42, COILS, 0x00, 10); for (int i = 0; i < 10; i++) { ModbusRTUClient.write(coilValue); } if (!ModbusRTUClient.endTransmission()) { Serial.print("failed! "); Serial.println(ModbusRTUClient.lastError()); } else { Serial.println("success"); } // Alternatively, to write a single Coil value use: (для записа одного регистра флагов) // ModbusRTUClient.coilWrite(...) } void readCoilValues() { Serial.print("Reading Coil values ... "); // read 10 Coil values from (slave) id 42, address 0x00 // считываем 10 регистров флагов с ведомого с идентификатором (адресом) 42, начиная с адреса 0x00 if (!ModbusRTUClient.requestFrom(42, COILS, 0x00, 10)) { Serial.print("failed! "); Serial.println(ModbusRTUClient.lastError()); } else { Serial.println("success"); while (ModbusRTUClient.available()) { Serial.print(ModbusRTUClient.read()); Serial.print(' '); } Serial.println(); } // Alternatively, to read a single Coil value use: (для считвания одного регистра флагов) // ModbusRTUClient.coilRead(...) } void readDiscreteInputValues() { Serial.print("Reading Discrete Input values ... "); // read 10 Discrete Input values from (slave) id 42, address 0x00 // считываем 10 дискретных входов с ведомого с идентификатором (адресом) 42, начиная с адреса 0x00 if (!ModbusRTUClient.requestFrom(42, DISCRETE_INPUTS, 0x00, 10)) { Serial.print("failed! "); Serial.println(ModbusRTUClient.lastError()); } else { Serial.println("success"); while (ModbusRTUClient.available()) { Serial.print(ModbusRTUClient.read()); Serial.print(' '); } Serial.println(); } // Alternatively, to read a single Discrete Input value use: (для считывания одного дискретного входа) // ModbusRTUClient.discreteInputRead(...) } void writeHoldingRegisterValues() { // set the Holding Register values to counter Serial.print("Writing Holding Registers values ... "); // write 10 coil values to (slave) id 42, address 0x00 // записываем 10 регистров хранения ведомому с идентификатором (адресом) 42, начиная с адреса 0x00 ModbusRTUClient.beginTransmission(42, HOLDING_REGISTERS, 0x00, 10); for (int i = 0; i < 10; i++) { ModbusRTUClient.write(counter); } if (!ModbusRTUClient.endTransmission()) { Serial.print("failed! "); Serial.println(ModbusRTUClient.lastError()); } else { Serial.println("success"); } // Alternatively, to write a single Holding Register value use: (для записи одного регистра хранения) // ModbusRTUClient.holdingRegisterWrite(...) } void readHoldingRegisterValues() { Serial.print("Reading Input Register values ... "); // read 10 Input Register values from (slave) id 42, address 0x00 // считываем 10 регистров хранения с ведомого с идентификатором (адресом) 42, начиная с адреса 0x00 if (!ModbusRTUClient.requestFrom(42, HOLDING_REGISTERS, 0x00, 10)) { Serial.print("failed! "); Serial.println(ModbusRTUClient.lastError()); } else { Serial.println("success"); while (ModbusRTUClient.available()) { Serial.print(ModbusRTUClient.read()); Serial.print(' '); } Serial.println(); } // Alternatively, to read a single Holding Register value use: (для считывания одного регистра хранения) // ModbusRTUClient.holdingRegisterRead(...) } void readInputRegisterValues() { Serial.print("Reading input register values ... "); // read 10 discrete input values from (slave) id 42, // считываем 10 регистров ввода с ведомого с идентификатором (адресом) 42, начиная с адреса 0x00 if (!ModbusRTUClient.requestFrom(42, INPUT_REGISTERS, 0x00, 10)) { Serial.print("failed! "); Serial.println(ModbusRTUClient.lastError()); } else { Serial.println("success"); while (ModbusRTUClient.available()) { Serial.print(ModbusRTUClient.read()); Serial.print(' '); } Serial.println(); } // Alternatively, to read a single Input Register value use: (для считывания одного регистра ввода) // ModbusRTUClient.inputRegisterRead(...) } |
Соотвественно, функция для выполнения команды 0x03 протокола Modbus (чтение значений из нескольких регистров хранения, Read Holding Registers) в представленном фрагменте кода приведена в строках с 128 по 148, а функция для выполнения команды 0x04 (чтение значений из нескольких регистров ввода, Read Input Registers) в представленном фрагменте кода приведена в строках с 150 по 170.
7 111 просмотров
Пока не разобрался как записывать данные в регистры и передавать их по запросу мастера.
По просьбе на форуме сделал опрос ардуино с помощью файла Excel:
https://www.cyberforum.ru/asutp/thread3170852-page3.html
Пост №49 и №50.
На ардуино сделан вольтметр, он пишет показания в буфер, которые передаётся по запросу мастера.
Здравствуйте!
Появилась задача: работать с реальным прибором по протоколу Modbus RTU с помощью файла Excel.
Реального прибора нет, а покупать его за 14000 руб. жалко. Приборы производства "ОВЕН" передают данные как в целочисленном формате, так и в формате с плавающей запятой flat32. Передаётся 4 байта, причём старшим байтом вперёд.
Предполагаю сделать на Arduino, используя терморезистор в качестве датчика температуры. Отправку фрейма я уже реализовал: https://www.cyberforum.ru/asutp/thread3170852-page3.html
Остаётся реализовать передачу данных из Arduino.
Есть предположение, что промышленный прибор считывает показания подключенных датчиков в цикле, а приём - передачу данных по Modbus осуществляет по прерыванию.
Добрый день. К сожалению с подобными задачами не сталкивался в своей практической деятельности, поэтому ничего конструктивного по вашей проблеме подсказать не могу. Но будем признательны если вы отпишитесь нам здесь если вам все таки удастся данную проблему решить.
Разумеется, если у вас будут другие вопросы, на которые я смогу ответить, то я с радостью на них отвечу
Не могу понять, какое число будет передано мастеру readHoldingRegisterValues() или ModbusRTUClient.inputRegisterRead(...) ?
readHoldingRegisterValues() - это просто функция для считывания регистров хранения, а ModbusRTUClient.inputRegisterRead(...) - это альтернативная функция для считывания только одного регистра хранения. В данный момент в статье из этих двух функций приведен пример использования только функции readHoldingRegisterValues()
Непонятно, как полудуплекс (пин10) с картинки переключается на прием/передачу.
С одного контакта Ардуино происходит происходит одновременное управление контактами DE & RE модуля RS-485 - эти контакты управляют разрешением работы передатчика и приемника модуля
здраствуйте
подскажите где\куда смотреть по реализации hart protocl на ардуинке ардуино харт модем
здравствуйте всем,
кто подскажет про HART protocol и как егоприклеить к ардуинке
К сожалению, пока не подскажу. Но на будущее буду иметь ввиду, если появится возможность, добавлю такую статью на сайт
Огромное СПАСИБО! Будем разбираться....
Спасибо и вам за то, что оценили мой труд. Если у вас в процессе практической работы по освещенным в статье вопросам будут возникать предложения и дополнения, просьба написать их здесь чтобы другим посетителям нашего сайта было проще разобраться в материале статьи