Недорогой RS-485 интерфейс для Arduino/ESP8266/ESP32

RS485 интерфейс позволяет обеспечить помехоустойчивое соединение с оборудованием на расстоянии до 1200 метров. Вопрос, как обеспечить безопасное подключение микроконтроллера к оборудованию, работающему по RS485 интерфейсу?

При разрядах молний электронное оборудование, соединенное длинным кабелем, не раз выгорало. Кроме того оборудование RS485 нередко работает на напряжениях выше, чем 3,5 V или 5 V на которых работают микроконтроллеры, а, соответственно, может выдать в линию повышенное напряжение, которое выведет из строя чип RS485.

Arduino RS485 Shield или бюджетная плата RS485 не обеспечивают защиты чипа конвертера интерфейса MAX485 от подобных напастей, а, соответственно, непригодны для промышленного использования. В предыдущих статьях я рассматривал некоторые Arduino RS485 Shields. В частности, подробно рассмотрел работу с недорогим Arduino Shield с Aliexpress.

В этой статье я рассмотрю очень недорогие модули обеспечивающие RS485 интерфейс и при этом достаточно защищенные для использования в промышленном оборудовании.

XY-485 и XY-017

Начну с довольно дорогого и качественного модуля XY-485. Очень качественно собран, на нем распаяны разъемы, хорошо промаркирован. В общем, когда держишь в руках, чувствуется, что модуль собирается на приличном заводе.

Модуль конверера RS485 интерфейса XY-485

Сразу отмечу, что в модуле контроль приема/передачи производится автоматически. Т.е. не нужно, как в ранее рассмотренном Arduino 485 Shield, управлять вручную установкой в HIGH/LOW пинов RE/DE. В статье есть подробный пример кода. Им же я воспользуюсь для работы с XY-485 с автоматическим контролем приема/передачи.

Более дешевая плата XY-017, брат-близнец XY-485. На ней не распаяны разъемы, хуже маркировка, но все-же платы очень похожи друг на друга по компонентам и размещению элементов. Разводка немного отличается, но не сильно.

Обратим внимание на наличие предохранителей и стабилитронов в обеих вариантах модулей, защищающих выходы микросхемы MAX485 от ударов молний, перенапряжений и ошибок при соединении, когда RS485 оборудование неверно подключается к A+ и B-.

Кроме того на плате распаяны два светодиода Rx/Tx индицирующие отправку/получение данных. Удобно.

Проверю работу Arduino с этим модулем. Для экспериментов воспользуюсь модулем RX-485, Arduino Uno и ранее не раз использованным термодатчиком, работающим по RS485, протокол Modbus.

Возьму код из предыдущего примера и уберу строки управляющие RE и DE пинами.

#include "ModbusMaster.h"

#define Slave_ID       1 

// instantiate ModbusMaster object
ModbusMaster node;

void setup()
{
  // Modbus communication runs at 9600 baud
  Serial.begin(9600, SERIAL_8N1);

  // Modbus slave ID 1
  node.begin(Slave_ID, Serial);
}

void loop()
{
  // Read 2 registers starting at 0x01
  uint8_t result = node.readInputRegisters(0x01, 2);
  if (getResultMsg(result))
  {
    Serial.println();
    
    double res_dbl = node.getResponseBuffer(0)/10;
    String res = "Temperature: " + String(res_dbl) + " C\r\n";
    res_dbl = node.getResponseBuffer(1)/10;
    res += "Humidity: " + String(res_dbl) + " %";
    Serial.println(res);
 }
 delay(10000);
}

bool getResultMsg(uint8_t result)
{
  String tmpstr2;

  switch (result) {
  case node.ku8MBSuccess:
    return true;
    break;
  case node.ku8MBIllegalFunction:
    tmpstr2 = "Illegal Function";
    break;
  case node.ku8MBIllegalDataAddress:
    tmpstr2 = "Illegal Data Address";
    break;
  case node.ku8MBIllegalDataValue:
    tmpstr2 = "Illegal Data Value";
    break;
  case node.ku8MBSlaveDeviceFailure:
    tmpstr2 = "Slave Device Failure";
    break;
  case node.ku8MBInvalidSlaveID:
    tmpstr2 = "Invalid Slave ID";
    break;
  case node.ku8MBInvalidFunction:
    tmpstr2 = "Invalid Function";
    break;
  case node.ku8MBResponseTimedOut:
    tmpstr2 = "Response Timed Out";
    break;
  case node.ku8MBInvalidCRC:
    tmpstr2 = "Invalid CRC";
    break;
  default:
    tmpstr2 = "Unknown error: " + String(result);
    break;
  }
  Serial.println(tmpstr2);
  return false;
}

Схема соединения Arduino Uno с XY-485:

Arduino UnoXY-485
Rx (PIN 0)TXD
Tx (PIN 1)RXD
5V VCC
GNDGND
Термодатчик XY-485
A+A+
B-B-
GNDБлок питания —
VCCБлок питания +5 V

Sketch успешно загружается в Arduino, но в мониторе серийного порта идут ошибки «Response Timed Out». Никакое шаманство не помогает: менял библиотеки для работы с RS485, подключал «землю», подавал питание и на датчик и на XY-485 от USB порта, перечитал массу статей на английском и русском и т.д. Ничего не помогало, никак не мог найти в чем проблема. Затем, случайно на youtube увидел короткий обзор модуля XY-017.

В ролике услышал решение в одной фразе:

«На XY-017 попутаны местами Tx и Rx».

Честно говоря, я бы никогда не подумал, что на столь классно выполненном XY-485 будет допущена столь серьезная ошибка.

Ровно такая-же история с платой XY-017. По крайней мере мне достался такой экземпляр. Чувствуется производители «списывали» друг у друга. 🙂

Меняю местами Rx и Tx, пытаюсь загрузить sketch в Arduino Uno и получаю ошибку:

avrdude: stk500_getsync() attempt 10 of 10: not in sync: resp=0x00
An error occurred while uploading the sketch

Выдергиваю на время «заливки» sketch-а пин Rx и повторяю upload sketch-а. После успешного upload возвращаю Rx назад и «о, чудо», все начинает работать, получаю данные с датчика температуры и влажности. В общем, правильное соединение непривычное 🙂

Arduino UnoXY-485
Rx (PIN 0)RXD
Tx (PIN 1)TXD

Если все соединения с Arduino Uno выполнены верно, питание нормально подается на модуль и pin-ы к которым вы подключили плату не pullup, т.е. не подвешены на + питания, то на плате XY-485 светодиод RXD не должен постоянно гореть

Я запитывал модуль XY-485 и от 3,3 V и от 5 V непосредственно от платы Arduino Uno запитанной от USB порта и от отдельного БП. В обеих случаях все работает нормально. Данные с дачика поступают.

XY-485 c ESP8266

Самая важная вещь при подключении модуля XY-485 к плате с распаянным ESP8266:

Подключайте XY-485 к блоку питания на 3,3 В, чтобы не спалить GPIO ESP8266!

Можно подключать модуль к соответствующему выводу платы на которой распаян ESP8266. Практически на всех девелоперских платах есть вывод 3,3 В. Хотя я предпочитаю запитывать от отдельного БП.

Казалось бы при переносе RX/TX с Arduino Uno на входы RX/TX ESP8266 проблем возникнуть не должно. Но это в теории. 🙁 На практике схема сразу перестает работать. Идет ошибка «Invalid Slave ID», хотя если бы не было соединения с датчиком ошибка была бы «Response Timed Out».

Помучавшись с аппаратным последовательным портом сэмулировал RX/TX на свободных контактах GPIO с помощью библиотеки ESP SoftwareSerial от Peter Lerup (@plerup). На обычных GPIO поддерживается скорость дo 115200 и несколько SoftwareSerial портов. Поскольку это программная эмуляция последовательного порта, лучше использовать низкие скорости.

С программной эмуляцией последовательного порта все работает стабильно, параметры с датчика считываются. При заземлении всех элементов схемы (Wemos D1, XY-485, термодатчика RS485) И запитывании XY-485 и термодатчика от отдельного БП, а не от Wemos D1 подключенного по USB к ПК ошибка «Invalid Slave ID» не появляется.

#include "ModbusMaster.h"
#include "SoftwareSerial.h" //https://github.com/plerup/espsoftwareserial  

#define Slave_ID    1 
//#define RX_PIN      14  //D5
#define RX_PIN      15  //D8
//#define TX_PIN      12  //D6    
#define TX_PIN      13  //D7
   
SoftwareSerial swSer(RX_PIN, TX_PIN, false, 128);

// instantiate ModbusMaster object
ModbusMaster node;

void setup()
{
  //********** CHANGE PIN FUNCTION  TO TX/RX ********** 
  //GPIO 1 (TX) swap the pin to a TX.
  //GPIO 7 (TXD) swap pint to a TX.  
  //pinMode(7, FUNCTION_4); 
  //GPIO 3 (RX) swap the pin to a RX.
  //GPIO 8 (RX) swap the pin to a RX.
  //pinMode(8, FUNCTION_4); 
  //***************************************************

  // Modbus communication runs at 9600 baud
  Serial.begin(9600, SERIAL_8N1);
  //Serial.setRxBufferSize(128); //Change default 256 to 128
  
  swSer.begin(9600);

  // Modbus slave ID
  //node.begin(Slave_ID, Serial);
  node.begin(Slave_ID, swSer);
}

Этот код работает относительно стабильно. Функция loop() и getResultMsg() не изменялись. Я не стал убирать неработающие куски кода, которые использовались в процессе экспериментов, а лишь закомментировал. Возможно, будет полезно.

Выбранный GPIO 15 (D8 на Wemos D1) оказался не очень удачным. Он служебный и при аплоаде скетча на нем должен быть выставлен определенный уровень. Из-за этого при аплоаде всегда возникает ошибка. Пришлось перенести на D1 (GPIO5).

Проблема возникает, если, например, снять один из PIN-ов RX/TX. Если ModbusMaster не получает данных, то выпадает в стек. В качестве workaround можно использовать временное отключение watchdog при опросе датчика.

  // Read 2 registers starting at 0x01
  ESP.wdtDisable();
  uint8_t result = node->readInputRegisters(0x01, 2);
  ESP.wdtEnable(1);

Другой обходной вариант — включить в секцию setup строчку:

node.begin(Slave_ID2, Serial2);
node.idle(yield);

Динамическое создание объектов SoftwareSerial и ModbusMaster

В некоторых случаях статическое создание объектов не годится. Нужно создавать их динамически. В этом случае исходный код будет выглядеть следующим образом:

#include "ModbusMaster.h"
#include "SoftwareSerial.h"

#define Slave_ID    1 
#define RX_PIN      15  //D8
#define TX_PIN      13  //D7

SoftwareSerial* swSer; //(RX_PIN, TX_PIN, false, 128);

// instantiate ModbusMaster object
ModbusMaster *node;

void setup()
{
  // Modbus communication runs at 9600 baud
  swSer = new SoftwareSerial(RX_PIN, TX_PIN, false, 128);
  
  Serial.begin(9600, SERIAL_8N1);
  //Serial.setRxBufferSize(128); //Change default 256 to 128
  
  swSer->begin(9600);

  node = new ModbusMaster();
  
  // Modbus slave ID 1
  node->begin(Slave_ID, *swSer);
}

void loop()
{
  Serial.println("Try to read data");
  // Read 2 registers starting at 0x01
  ESP.wdtDisable();
  uint8_t result = node->readInputRegisters(0x01, 2);
  ESP.wdtEnable(1);
  
  if (getResultMsg(*node, result))
  {
    Serial.println();
    
    double res_dbl = node->getResponseBuffer(0)/10;
    String res = "Temperature: " + String(res_dbl) + " C\r\n";
    res_dbl = node->getResponseBuffer(1)/10;
    res += "Humidity: " + String(res_dbl) + " %";
    Serial.println(res);
 }
 delay(1000);
}

В функции

bool getResultMsg(ModbusMaster node, uint8_t result)

добавился один аргумент.

Передача объекта через void*

Иногда возникает необходимость в библиотеке объявить свойство в классе, которому в дальнейшем надо будет передать ссылку на объект. Небольшой пример, как это сделать.

// instantiate ModbusMaster object
void* p; //ModbusMaster *node;

void setup()
{
  // Modbus communication runs at 9600 baud
  swSer = new SoftwareSerial(RX_PIN, TX_PIN, false, 128);
  
  Serial.begin(9600, SERIAL_8N1);
  //Serial.setRxBufferSize(128); //Change default 256 to 128
  
  swSer->begin(9600);

  p = new ModbusMaster();

  ModbusMaster* node = static_cast<ModbusMaster*>(p);
  node->begin(Slave_ID, *swSer);
}

Fritzing part for RS485 (XY-017 board)

XY-017 Fritzing part *.fzpz
XY-017 Fritzing part *.fzpz

Не нашел fritzing part для RS485 (XY-017 платы), поэтому нарисовал свой вариант.

Spread the love
Запись опубликована в рубрике IT рецепты, IT решения для бизнеса с метками , , , . Добавьте в закладки постоянную ссылку.

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