Why are RS485 frames corrupted between ESP32 and ESP8266?

Hello,

In my project, I’m sending data over RS485 from an ESP32 to an ESP8266 using an SP3485 transceiver. Unfortunately, I’m facing the following issue: I send a request from the ESP32 every minute, and every few minutes the ESP32 sends a frame to the ESP8266, but the frame arrives corrupted. For example, instead of receiving 0x05, the ESP8266 sometimes gets 0x04 or 0x06. Because of this, the checksum is invalid, and the ESP8266 doesn’t send a response.

I’d like to understand what might be causing these transmission errors and whether there’s a way to fix or mitigate them. The baud rate cannot be changed and is set to 115200.

Post both codes and a drawing or description of how the circuits are connected and powered.

If the code is clean, possible causes are missing termination or biasing, timing issues with DE/RE control, or signal integrity problems on the RS485 bus.

Code ESP32:

#include <WiFi.h>
#include <HTTPClient.h>
#include <HardwareSerial.h>
#include "time.h"
#include <Preferences.h>

// ---------------- RS485 ----------------
HardwareSerial rs485(1); // UART1
#define RX_PIN 16
#define TX_PIN 17

uint8_t masterID = 0x3F;
uint8_t slaveID  = 0x00;

// WiFi dane
const char* ssid = "";
const char* password = "";

// Serwer API
const char* serverURL = "";
// ---------------- Rekordy ----------------
Preferences prefs;
unsigned long recordNumber = 0;

// ---------------- NTP ----------------
const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 2 * 3600; // UTC+2
const int   daylightOffset_sec = 0;

// ---------- Komendy ----------
struct Command {
  uint8_t cmd1;
  uint8_t cmd2;
  uint8_t length;
  String description;
  String lastValue;
};

Command commands[] = {

  {0x01, 0x00, 4,  "Status procesu X", ""},
  {0x02, 0x00, 4,  "Pozostały czas (s)", ""},
};

#define IN_COUNT 9
Command inCommands[IN_COUNT];

#define INFO_COUNT 8
Command infoCommands[INFO_COUNT];

// ---------------- Zmienne globalne ----------------
int currentCommand = 0;
bool roundInProgress = false;
unsigned long lastPacketTime = 0;
unsigned long lastFullRoundTime = 0;
const unsigned long packetInterval = 1500;
const unsigned long fullRoundInterval = 56000;
const unsigned long responseTimeout = 5000; // 5 sek. na odpowiedź
const int maxRetries = 3;                  // max ilość ponowień całej rundy
int retryCount = 0;
unsigned long roundStartTime = 0;
uint8_t Prog_No = 0;
uint8_t Segment_No = 0;
uint32_t remainingTimeStr = 0;

int sentCommands = 0;       // liczba wysłanych komend
int receivedResponses = 0;  // liczba odebranych odpowiedzi

// ---------------- Funkcje pomocnicze ----------------
void setupCommands() {
  for (int i = 0; i < IN_COUNT; i++) {
    inCommands[i] = {0x05, uint8_t(i), 7, "IN" + String(i), ""};
  }
  for (int i = 0; i < INFO_COUNT; i++) {
    infoCommands[i] = {0x0C, uint8_t(i), 7, "Infobox" + String(i), ""};
  }
}
float decodeFloatBE(uint8_t *buf) {
  uint8_t temp[4];
  temp[0] = buf[3];
  temp[1] = buf[2];
  temp[2] = buf[1];
  temp[3] = buf[0];
  float f;
  memcpy(&f, temp, 4);
  return f;
}
// HEX debug
void printHex(const char* prefix, uint8_t* data, size_t len) {
  Serial.print(prefix);
  for (size_t i = 0; i < len; i++) {
    Serial.printf("%02X ", data[i]);
  }
  Serial.println();
}

// Funkcja wysyłania ramki (bez DE/RE)
void sendFrame(uint8_t* frame, size_t len) {
  rs485.write(frame, len);
  rs485.flush();

  // DEBUG HEX
  printHex("MASTER -> ", frame, len);
}

// ---------------- Setup ----------------
void setup() {
  Serial.begin(115200);

  // wg dokumentacji -> 8E1
  rs485.begin(115200, SERIAL_8N1, RX_PIN, TX_PIN);

  setupCommands();

  // Odczyt rekordu
  prefs.begin("recstore", false);
  recordNumber = prefs.getULong("record", 514081);
  Serial.println("Odczytany rekord startowy: " + String(recordNumber));

  // WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected!");

  // NTP
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}


// ---------------- Loop ----------------
void loop() {
  unsigned long now = millis();

  // Odczyt aktualnego czasu
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    // start nowej rundy na początku każdej minuty
    if (timeinfo.tm_sec == 56 && !roundInProgress) {
      roundInProgress = true;
      currentCommand = 0;
      lastPacketTime = 0;
      sentCommands = 0;
      receivedResponses = 0;
      retryCount = 0;
      roundStartTime = now;
      Serial.printf("Start rundy o %02d:%02d\n", timeinfo.tm_hour, timeinfo.tm_min);
    }
  }

  // Wysyłanie kolejnych pakietów
  if (roundInProgress && now - lastPacketTime >= packetInterval) {
    sendNextPacket();
    lastPacketTime = now;
  }

  // Timeout i retry całej rundy
  if (roundInProgress && (now - roundStartTime > responseTimeout)) {
    if (receivedResponses < sentCommands) {
      if (retryCount < maxRetries) {
        Serial.println("Timeout! Ponawiam rundę...");
        currentCommand = 0;
        lastPacketTime = 0;
        sentCommands = 0;
        receivedResponses = 0;
        roundStartTime = now;
        retryCount++;
      } else {
        Serial.println("Przekroczono maxRetries, kończę rundę.");
        roundInProgress = false;
      }
    }
  }

  // Sprawdzenie czy komplet danych
  int totalCommands = sizeof(commands) / sizeof(commands[0]) + IN_COUNT + INFO_COUNT;
  if (roundInProgress && currentCommand >= totalCommands && receivedResponses >= sentCommands) {
    roundInProgress = false;
    sendJSON(); // pełna runda OK
  }

  // Odbiór danych RS485
  if (rs485.available()) {
    uint8_t buffer[256];
    size_t len = rs485.readBytes(buffer, sizeof(buffer));
    printHex("SLAVE -> ", buffer, len);
    parseResponse(buffer, len);
  }
}

// ---------------- Wysyłanie pakietu ----------------
void sendNextPacket() {
  int totalCommands = sizeof(commands) / sizeof(commands[0]) + IN_COUNT + INFO_COUNT;
  if (currentCommand >= totalCommands) return;

  int PACKET_SIZE = 10;
  int remaining = totalCommands - currentCommand;
  int count = min(remaining, PACKET_SIZE);

  uint8_t frame[2 + count * 2 + 1];
  int idx = 0;
  frame[idx++] = slaveID;
  frame[idx++] = masterID;
  frame[idx++] = count * 2;

  for (int i = 0; i < count; i++) {
    Command cmd;
    if (currentCommand < sizeof(commands) / sizeof(commands[0]))
      cmd = commands[currentCommand];
    else if (currentCommand < sizeof(commands) / sizeof(commands[0]) + IN_COUNT)
      cmd = inCommands[currentCommand - sizeof(commands) / sizeof(commands[0])];
    else
      cmd = infoCommands[currentCommand - sizeof(commands) / sizeof(commands[0]) - IN_COUNT];

    frame[idx++] = cmd.cmd1;
    frame[idx++] = cmd.cmd2;
    currentCommand++;
    sentCommands++; // liczymy wysłane
  }

  uint8_t sum = 0;
  for (int i = 0; i < idx; i++) sum += frame[i];
  frame[idx++] = sum;

  sendFrame(frame, idx);
}

// ---------------- Parsowanie odpowiedzi ----------------
void parseResponse(uint8_t* data, size_t len) {
  if (len < 4) return;
  uint8_t recvMaster = data[0];
  uint8_t recvSlave  = data[1];
  uint8_t dataLen    = data[2];
  if (recvMaster != masterID || recvSlave != slaveID) return;
  if (len < 3 + dataLen + 1) return;

  int pos = 3;
  while (pos < 3 + dataLen) {
    uint8_t cmd1 = data[pos++];
    uint8_t cmd2 = data[pos++];

    if (cmd1 == 0x00) { // ASCII
      String strVal = "";
      for (int i = 0; i < 8; i++) strVal += char(data[pos++]);
      storeValue(cmd1, cmd2, strVal);
      receivedResponses++;
    } else if (cmd1 == 0x01) { // Status procesu X
      uint8_t byte0 = data[pos++];
      uint8_t byte1 = data[pos++];
      Prog_No = data[pos++];
      Segment_No = data[pos++];
      String val = "Prog:" + String(Prog_No) + " Seg:" + String(Segment_No);
      storeValue(cmd1, cmd2, val);
      receivedResponses++;
    } else if (cmd1 == 0x02) { // Pozostały czas
      uint32_t t = ((uint32_t)data[pos] << 24) |
                   ((uint32_t)data[pos+1] << 16) |
                   ((uint32_t)data[pos+2] << 8)  |
                   (uint32_t)data[pos+3];
      pos+=4;
      remainingTimeStr = t;
      storeValue(cmd1, cmd2, String(t));
      receivedResponses++;
    } else if (cmd1 == 0x05 || cmd1 == 0x0C) { // IN / Infobox
      float val = decodeFloatBE(&data[pos]);
      pos += 4;
      uint8_t unit = data[pos++];
      uint8_t status = data[pos++];
      storeValue(cmd1, cmd2, String(val));
      receivedResponses++;
    } else if (cmd1 == 0x09) {
      storeValue(cmd1, cmd2, String(data[pos++]));
      receivedResponses++;
    } else {
      while (pos < 3 + dataLen) pos++;
      receivedResponses++;
    }
  }
}

// ---------------- Zapis wartości ----------------
void storeValue(uint8_t cmd1, uint8_t cmd2, String val) {
  for (int i = 0; i < sizeof(commands) / sizeof(commands[0]); i++)
    if (commands[i].cmd1 == cmd1 && commands[i].cmd2 == cmd2) commands[i].lastValue = val;
  for (int i = 0; i < IN_COUNT; i++)
    if (inCommands[i].cmd1 == cmd1 && inCommands[i].cmd2 == cmd2) inCommands[i].lastValue = val;
  for (int i = 0; i < INFO_COUNT; i++)
    if (infoCommands[i].cmd1 == cmd1 && infoCommands[i].cmd2 == cmd2) infoCommands[i].lastValue = val;
}

// ---------------- Czas lokalny ----------------
void getLocalDateTime(String &dateStr, String &timeStr) {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) {
    dateStr = "unknown";
    timeStr = "unknown";
    return;
  }
  char dateBuff[20];
  char timeBuff[20];
  strftime(dateBuff, sizeof(dateBuff), "%d/%m/%Y", &timeinfo);
  strftime(timeBuff, sizeof(timeBuff), "%H:%M:%S", &timeinfo);
  dateStr = String(dateBuff);
  timeStr = String(timeBuff);
}

// ---------------- Wysyłanie JSON ----------------
void sendJSON() {
  if (WiFi.status() != WL_CONNECTED) return;

  String dateStr, timeStr;
  getLocalDateTime(dateStr, timeStr);

  String json = "{";
  json += "\"record\":\"" + String(recordNumber) + "\",";
  json += "\"date\":\"" + dateStr + "\",";
  json += "\"loc_time\":\"" + timeStr + "\",";
  json += "\"vt00\":\"" + infoCommands[0].lastValue + "\",";
  json += "\"sp00\":\"" + infoCommands[1].lastValue + "\",";
  json += "\"vt01\":\"" + infoCommands[3].lastValue + "\",";
  json += "\"vaccum1\":\"" + infoCommands[7].lastValue + "\",";
  json += "\"program\":\"" + String(Prog_No) + "\",";
  json += "\"segment\":\"" + String(Segment_No) + "\",";
  json += "\"rem_time\":\"" + String(remainingTimeStr) + "\",";
  json += "\"bottom_l\":\"" + inCommands[0].lastValue + "\",";
  json += "\"bottom_r\":\"" + inCommands[1].lastValue + "\",";
  json += "\"top_l\":\"" + inCommands[2].lastValue + "\",";
  json += "\"top_r\":\"" + inCommands[3].lastValue + "\",";
  json += "\"ai2_1\":\"" + inCommands[4].lastValue + "\",";
  json += "\"ai2_2\":\"" + inCommands[5].lastValue + "\",";
  json += "\"ai2_3\":\"" + inCommands[6].lastValue + "\",";
  json += "\"vaccum2\":\"" + inCommands[7].lastValue + "\",";
  json += "\"gas_pres\":\"" + inCommands[8].lastValue + "\"";
  json += "}";

  HTTPClient http;
  http.begin(serverURL);
  http.addHeader("Content-Type", "application/json");
  int httpResponseCode = http.POST(json);

  if (httpResponseCode > 0) {
    Serial.println("POST success: " + String(httpResponseCode));
    recordNumber++;
    prefs.putULong("record", recordNumber);
  } else {
    Serial.println("POST failed: " + String(httpResponseCode));
  }

  http.end();
}

Code Esp8266:

#include <SoftwareSerial.h>

SoftwareSerial rs485(D1, D2); // RX, TX

uint8_t slaveID = 0x00;
uint8_t masterID = 0x3F;

const char model[] = "ESP8266";
const char version[] = "v1.0.0";
const char serialNumber[] = "SN123456";

uint8_t processStatus[4] = {0b01010010, 0x01, 0x02, 0x03};
float inValues[16];
uint8_t inUnits[16];
uint8_t inStatus[16];

void setup() {
  Serial.begin(115200);
  rs485.begin(115200);
  Serial.println("SLAVE: Start");
  randomSeed(analogRead(A0)); // do losowania wartości
}

void loop() {
  if (rs485.available()) {
    uint8_t buffer[128];
    size_t len = rs485.readBytes(buffer, sizeof(buffer));

    Serial.print("SLAVE <- Otrzymano: ");
    printHex(buffer, len);

    if (len < 4) return;

    uint8_t recvSlaveID = buffer[0];
    uint8_t recvMasterID = buffer[1];
    uint8_t dataLen = buffer[2];

    // suma kontrolna
    uint8_t sum = 0;
    for (int i = 0; i < 3 + dataLen; i++) sum += buffer[i];
    if (sum != buffer[3 + dataLen]) {
      Serial.println("SLAVE: Błąd sumy kontrolnej!");
      return;
    }

    if (recvSlaveID != slaveID) return;

    // losowanie nowych danych
    for (int i = 0; i < 16; i++) {
      inValues[i] = random(200, 400) / 10.0; // np. 20.0–40.0 °C
      inUnits[i] = 0; // 0 = °C
      inStatus[i] = 0b01000000; // przykładowy status OK
    }

    // odpowiedź
    uint8_t response[128];
    int idx = 0;
    response[idx++] = masterID;
    response[idx++] = slaveID;

    int lenPos = idx++;
    int dataStart = idx;

    // obsługa komend
    for (int i = 3; i < 3 + dataLen; i += 2) {
      uint8_t cmd1 = buffer[i];
      uint8_t cmd2 = buffer[i + 1];

      response[idx++] = cmd1;
      response[idx++] = cmd2;

      if (cmd1 == 0x00) { // tekstowe dane
        if (cmd2 == 0x01) memcpy(&response[idx], model, 8);
        else if (cmd2 == 0x02) memcpy(&response[idx], version, 8);
        else if (cmd2 == 0x03) memcpy(&response[idx], serialNumber, 8);
        else memset(&response[idx], 0, 8);
        idx += 8;
      }
      else if (cmd1 == 0x01 && cmd2 == 0x00) { // status procesu
        memcpy(&response[idx], processStatus, 4);
        idx += 4;
      }
      else if (cmd1 == 0x02 && cmd2 == 0x00) { // czas pozostały
        uint32_t timeLeft = 3600;
        writeUint32BE(&response[idx], timeLeft);
        idx += 4;
      }
      else if ((cmd1 == 0x05 || cmd1 == 0x0C) && cmd2 <= 0x0F) { // INxx / Infobox
        int inIdx = cmd2;
        writeFloatBE(&response[idx], inValues[inIdx]); // float big endian
        idx += 4;
        response[idx++] = inUnits[inIdx];
        response[idx++] = inStatus[inIdx];
      }
      else if (cmd1 == 0x09 && cmd2 == 0x00) { // wyjście cyfrowe
        response[idx++] = 0x01;
      }
      else {
        response[idx++] = 0x00;
      }
    }

    response[lenPos] = idx - dataStart;

    uint8_t sumResp = 0;
    for (int i = 0; i < idx; i++) sumResp += response[i];
    response[idx++] = sumResp;

    rs485.write(response, idx);
    rs485.flush();

    Serial.print("SLAVE -> Odpowiedź: ");
    printHex(response, idx);
  }
}

// ---------- Funkcje pomocnicze ----------
void writeFloatBE(uint8_t* buf, float val) {
  uint32_t tmp;
  memcpy(&tmp, &val, sizeof(float));
  buf[0] = (tmp >> 24) & 0xFF;
  buf[1] = (tmp >> 16) & 0xFF;
  buf[2] = (tmp >> 8) & 0xFF;
  buf[3] = tmp & 0xFF;
}

void writeUint32BE(uint8_t* buf, uint32_t val) {
  buf[0] = (val >> 24) & 0xFF;
  buf[1] = (val >> 16) & 0xFF;
  buf[2] = (val >> 8) & 0xFF;
  buf[3] = val & 0xFF;
}

void printHex(uint8_t* data, size_t len) {
  for (size_t i = 0; i < len; i++) {
    Serial.printf("%02X ", data[i]);
  }
  Serial.println();
}

Drawing:

It seems that both your ESP32 master and ESP8266 slave codes lack proper DE/RE control for the SP3485 transceivers, which is essential to switch between transmit and receive ➜ without it, the bus can float or experience contention, causing exactly the sporadic byte corruption you observed.

On the ESP32, HardwareSerial at 115200 baud is reliable, but still requires DE/RE toggling.

On the ESP8266, using SoftwareSerial at 115200 is inherently unreliable due to timing limitations, so hardware UART is recommended.

I think both sides should use a GPIO to assert DE during transmission and deassert it immediately afterward, and the ESP8266 should ideally switch to HardwareSerial.

SP3485 module I use have a auto-direction controle so I can’t connect DE/RE.

Why?
Can you replace esp8266 with esp32?

Sadly i don’t have another ESP32. The ESP8266 is emulating another device to which the ESP32 will be connected. The device operates at 115200, so I wanted to work at the default speed in order to resemble as closely as possible the situation in which the ESP32 will be running.

Then try with hardware serial (without using usb-serial for serialprints).

Common ground along the lines could help.
And the rs485 line might need termination resistors. Not all modules have it on board.

Is the corrupted byte always the same one? First one?

Ok, I’ll give it a try, the corrupted bit is random. Here few examples what slave received.

Think it goes wrong in readBytes().

What does this print? (I expect a value less than packet size = 4)

Analysis

If there are 3 bytes available of the 4 to receive, these will be copied in buffer by readBytes and in the next Loop these 3 bytes are ignored. So good bytes are dropped because the packet is not complete.

Better is to wait until at least 4 bytes are in the buffer (still not fool proof, just a bit better)

void loop() {
  if (rs485.available() >= 4) {
    uint8_t buffer[128];
    size_t len = rs485.readBytes(buffer, 4);

    Serial.print("SLAVE <- Otrzymano: ");
    printHex(buffer, len);

A robust solution should do something like

  • read one byte at the time
  • check if this is a PACKET_START byte (define a start of the packet byte)
  • if so collect the next N bytes as payload and CRC (one byte at the time)
  • check CRC
  • process data
2 Likes

OK so I would explore the software serial challenge.

Not only that, but you should be using EspSoftwareSerial

That said there is no reason you could not just use the UART0 swap() pins on the esp8266 (D7 & D8) and switch between the default and swap pins for Serial debug output or even use swSerial on the default pins for debug output instead of swapping back and forth.

Aren't those the 'auto-switching' transceivers ?

1 Like

That's clever idea.

hehe it just popped into my head. I remembered vaguely that you can also route one of the UART tx to a different pin other than the swap pins, hoping it might be UART1 tx (default on D4) but it wasn't then that popped into my head and i can't think of a reason why it shouldn't work.

Yep - I had missed that from OP’s description