Issue Receiving Full Data from PLC in Arduino Project with 7-Segment Display

Hi all,

I am working on a project where I aim to receive data from a PLC and display it on a 4-digit 7-segment display. I have tested my code by sending data from a Python program, and it works flawlessly. The display shows the data correctly, and I also receive the acknowledgment as expected.

However, when I connect the PLC to the Arduino, the 7-segment display stops showing the expected data, and it seems I am not receiving the full data from the PLC. I have attached the data received from the PLC as seen in the Arduino Serial Monitor for reference

#include <SoftwareSerial.h>
#include <Arduino.h>
#include <math.h>

#define RE_DE 4 // RS-485 send/receive control pin
#define RX 2    // RS-485 RO to Arduino pin 2
#define TX 3    // RS-485 DI to Arduino pin 3

SoftwareSerial rs485(RX, TX);

byte receivedData[64];  // Buffer to store received data
int receivedIndex = 0;

// Define shared pins for all shift registers
const int latchPin = 10;
const int clockPin = 13;
const int dataPin = 5;

// Segment patterns for digits 0-9
const byte segmentData[] = {
  0b00111111, // 0
  0b00000110, // 1
  0b01011011, // 2
  0b01001111, // 3
  0b01100110, // 4
  0b01101101, // 5
  0b01111101, // 6
  0b00000111, // 7
  0b01111111, // 8
  0b01101111  // 9
};

// Number of digits per display
const int numDigits = 4;

// Number of displays
const int numDisplays = 6;

// Array to hold the data to be displayed
byte displayBuffer[numDigits * numDisplays] = {0};

void setup() {
  pinMode(RE_DE, OUTPUT);
  digitalWrite(RE_DE, LOW); // Set to receive mode
  rs485.begin(9600);        // Start RS-485 communication at 9600 baud
  Serial.begin(9600);       // For debugging
  
  // Set shared pins as output
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
}

void loop() {
  static unsigned long lastByteTime = 0;
  static const unsigned long timeout = 50; // 50 ms timeout for message completion

  // Read incoming data
  while (rs485.available()) {
    if (receivedIndex < sizeof(receivedData)) {
      receivedData[receivedIndex++] = rs485.read();
      lastByteTime = millis(); // Update the time of the last received byte
    }
  }

  // Check if the message is complete or timed out
  if (receivedIndex > 0 && millis() - lastByteTime > timeout) {
    Serial.println("Full message received:");
    printHex(receivedData, receivedIndex);

    // Process the message
    processMessage(receivedData, receivedIndex);

    // Reset buffer for the next message
    receivedIndex = 0;
  }

  // Update all displays based on the received data
  updateDisplays();
}

// Function to process the received message
void processMessage(byte* data, int length) {
  if (length < 9) { // Ensure at least the minimum length for header and payload
    Serial.println("Error: Message too short.");
    return;
  }

  // Extract and display data for six displays
  int byteCount = data[6]; // Byte count
  if (byteCount != 12) {  // Ensure correct byte count for six displays (2 bytes each)
    Serial.println("Error: Incorrect byte count.");
    return;
  }

  for (int i = 0; i < 6; i++) {
    int highByte = data[7 + (i * 2)];
    int lowByte = data[8 + (i * 2)];
    int value = (highByte << 8) | lowByte;

    // Update the display buffer for the corresponding display
    updateDisplayBuffer(value, i + 1);  // Pass the value and display number
  }

  // Send acknowledgment after processing the message
  sendAcknowledgment();
}

// Function to send acknowledgment response
void sendAcknowledgment() {
  // Example acknowledgment message: 01 10 00 01 00 06 11 CB
  byte ackMessage[] = {0x01, 0x10, 0x00, 0x01, 0x00, 0x06, 0x11, 0xCB};

  // Switch to transmit mode
  digitalWrite(RE_DE, HIGH);

  // Send acknowledgment message
  rs485.write(ackMessage, sizeof(ackMessage));

  // Ensure the data is transmitted before switching back
  rs485.flush();

  // Switch back to receive mode
  digitalWrite(RE_DE, LOW);

  Serial.println("Acknowledgment sent.");
}

// Function to update the buffer for a specific display
void updateDisplayBuffer(int number, int display) {
  // Calculate the starting position in the buffer for the specific display
  int startShiftRegister = (display - 1) * numDigits;

  // Flag to indicate if a non-zero digit has been encountered
  bool hasNonZeroDigit = false;

  // Store the digits in the buffer from most significant to least significant
  for (int i = numDigits - 1; i >= 0; i--) {
    int digit = number / pow(10, i);
    digit %= 10;
    // Suppress leading zeros
    if (digit != 0 || hasNonZeroDigit || i == 0) {
      hasNonZeroDigit = true;
      displayBuffer[startShiftRegister + (numDigits - 1 - i)] = segmentData[digit];
    } else {
      // If it's a leading zero and hasn't encountered a non-zero digit, turn off the segments
      displayBuffer[startShiftRegister + (numDigits - 1 - i)] = 0b00000000;  // Turn off all segments
    }
  }
}

// Function to update all displays at once
void updateDisplays() {
  // Prepare to send data to all shift registers
  digitalWrite(latchPin, LOW);

  // Shift out all data from the buffer in reverse order
  for (int i = (numDigits * numDisplays) - 1; i >= 0; i--) {
    shiftOut(dataPin, clockPin, MSBFIRST, displayBuffer[i]);
  }

  // Latch the data to display the updated numbers
  digitalWrite(latchPin, HIGH);
  delayMicroseconds(100);
  digitalWrite(latchPin, LOW);
}

// Utility function to print data in hex format
void printHex(byte* data, int length) {
  for (int i = 0; i < length; i++) {
    if (data[i] < 0x10) Serial.print("0");
    Serial.print(data[i], HEX);
    Serial.print(" ");
  }
  Serial.println();
}

output from PLC in arduino serial monitor

error data from plc.txt (25.0 KB)

expected output from PLC in ardino :
00F79FBFBDDBBFDBD9AF9B4100B7FF5FBDD9D9A9DBDB2D1A00

Also I have join PLC directly with PC the help of RS-485 to USB converter I was able to received correct data from it but I am not able to identify the problem with arduino and PLC.

Circuit Diagram

Serial communication is slow; it might very well be that a byte is still being transferred so not yet available and the while loop will terminate.

You can improve your code using the following approach

  while (receivedIndex < sizeof(receivedData)) {
    if (rs485.available()) {
      receivedData[receivedIndex++] = rs485.read();
      lastByteTime = millis(); // Update the time of the last received byte
    }
  }

Note that this is blocking code.

To properly implement a timeout, you can improve the above

  while (receivedIndex < sizeof(receivedData)) {
    if (receiveIndex != 0 && millis() - lastByteTime >= timeout)
    {
      // timeout occured
      break;
    }
    if (rs485.available()) {
      receivedData[receivedIndex++] = rs485.read();
      lastByteTime = millis(); // Update the time of the last received byte
    }
  }

Not tested, probably needs some polishing.

You can study Robin's updated Serial Input Basics to get some ideas for an approach without while-loops. Although it was written with text communication in mind, ideas might still apply.

1 Like

I have tried this but am still receiving the same output.

Do you have the format of the message?

please post your new code based on Serial Input Basics Example and the serial output you get.

Let's add more questions.

  • Full description of protocol used by the PLC.

i will receive this data from plc
01 10 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 F4 77

break down of data:
01 is slave address
10 is function code ( write multiple registers)
0001 is starting address of word register
0006 is total number of registers
0C is data byte count
Then all data.
F477 Last CRC

How often? Once a second? Faster? On request?

That does not seem to match the below. Probably result from another command or so.

I can't fully help you because I don't have RS485 modules. The below code was tested on a Mega and only demonstrates a means to receive data over serial. I started the code using Serial3 to eliminate possible side effects of SoftwareSerial; once that was working reliably I changed it to SoftwareSerial.

The reading is implemented as a finite state machine. If you need explanations, you will have to let me know.

Read function

// internal state of finite state machine
enum class RCVSTATE
{
  WAITING,
  HEADER,
  DATA,
  CRC
};

/*
  finite state machine to receive data over rs485
  Returns:
    status
*/
RCVSTATUS rs485Read()
{
  // status of state machine to be reported back to calling function
  static RCVSTATUS rs = RCVSTATUS::READHEADER;
  // internal state of state machine
  static RCVSTATE state = RCVSTATE::WAITING;
  // time last byte was received (for timeout)
  static uint32_t lastReceiveTime;
  // index where to store in buffer
  static uint8_t index;
  // length of data following the header
  static uint8_t dataLength;

  // check for timeout
  if (state != RCVSTATE::WAITING && millis() - lastReceiveTime >= timeout)
  {
    state = RCVSTATE::WAITING;
    index = 0;
    dataLength = 0;
    rs = RCVSTATUS::ERR_TIMEOUT;
    return rs;
  }

  // if there is data
  if (rs485.available())
  {
    lastReceiveTime = millis();
    byte b = rs485.read();
    printByte(b);

    // check for buffer overflow
    if (index == sizeof(buffer))
    {
      state = RCVSTATE::WAITING;
      index = 0;
      dataLength = 0;
      rs = RCVSTATUS::ERR_OVERFLOW;
      return rs;
    }

    // finite state machine
    switch (state)
    {
      case RCVSTATE::WAITING:
        // clear the buffer
        memset(buffer, 0, sizeof(buffer));
        // store received byte
        buffer[index++] = b;
        // continue with other header bytes
        state = RCVSTATE::HEADER;
        // to be reported to caller
        rs = RCVSTATUS::READHEADER;
        break;
      case RCVSTATE::HEADER:
        buffer[index++] = b;
        // if we have received the full header
        if (index == headerLength)
        {
          // get the length of the actual data
          dataLength = buffer[headerLength - 1];
          // continue reading data bytes
          state = RCVSTATE::DATA;
          // report back to caller
          rs = RCVSTATUS::READDATA;
        }
        break;
      case RCVSTATE::DATA:
        buffer[index++] = b;
        // if all data bytes are received
        if (index == headerLength + dataLength)
        {
          // continue reading the crc
          state = RCVSTATE::CRC;
          // report back to caller
          rs = RCVSTATUS::READCRC;
        }
        break;
      case RCVSTATE::CRC:
        // 2 bytes
        buffer[index++] = b;
        // if we got the two bytes of the crc
        if (index == headerLength + dataLength + 2)
        {
          index = 0;
          dataLength = 0;
          // begin from the start
          state = RCVSTATE::WAITING;
          // report back to caller
          rs = RCVSTATUS::COMPLETE;
        }
        break;
    }
  }
  return rs;
}

The rs485Read() can return one of the following values

// return codes from finite statemachine
enum class RCVSTATUS
{
  COMPLETE,
  //WAITING,
  READHEADER,
  READDATA,
  READCRC,
  ERR_TIMEOUT,
  ERR_OVERFLOW,
};

Once you can calculate a CRC, you can expand it with a value for a CRC error (e.g. ERR_CRC). In the RCVSTATE::CRC state you can check the CRC and either return RCVSTATUS::COMPLETE if the crc matches or RCVSTATUS::ERR_CRC on error.

You need to call this function constantly from loop() as demonstrated below. It detects the change in the status so serial monitor does not get spammed with errors.

Demo loop()

void loop()
{
  static RCVSTATUS lastReceiveStatus = RCVSTATUS::COMPLETE;
  RCVSTATUS rs = rs485Read();
  if (rs != lastReceiveStatus)
  {
    Serial.println();
    Serial.print(F("Status changed from "));
    printStatus(lastReceiveStatus);
    Serial.print(F(" to "));
    printStatus(rs);
    Serial.println();

    if (rs == RCVSTATUS::COMPLETE)
    {
      Serial.print(F("Received: "));
      for (uint8_t cnt = 0; cnt < sizeof(buffer); cnt++)
      {
        if (buffer[cnt] < 0x10)
        {
          Serial.print(F("0"));
        }
        Serial.print(buffer[cnt], HEX);
        Serial.print(" ");
      }
      Serial.println();
    }
  }

  lastReceiveStatus = rs;
}

Full code

//#define rs485 Serial3
#ifndef rs485
#include <SoftwareSerial.h>
#define TX 11
#define RX 10
SoftwareSerial rs485(RX, TX);
#endif


const uint16_t timeout = 50;
const uint8_t headerLength = 7;

// return codes from finite stayemachine
enum class RCVSTATUS
{
  COMPLETE,
  //WAITING,
  READHEADER,
  READDATA,
  READCRC,
  ERR_TIMEOUT,
  ERR_OVERFLOW,
};

// internal state of finite state machine
enum class RCVSTATE
{
  WAITING,
  HEADER,
  DATA,
  CRC
};

byte buffer[64];

void setup()
{
  Serial.begin(115200);
  rs485.begin(9600);
  Serial.println();
  Serial.println(F("receiverDemo"));
}

void loop()
{
  static RCVSTATUS lastReceiveStatus = RCVSTATUS::COMPLETE;
  RCVSTATUS rs = rs485Read();
  if (rs != lastReceiveStatus)
  {
    Serial.println();
    Serial.print(F("Status changed from "));
    printStatus(lastReceiveStatus);
    Serial.print(F(" to "));
    printStatus(rs);
    Serial.println();

    if (rs == RCVSTATUS::COMPLETE)
    {
      Serial.print(F("Received: "));
      for (uint8_t cnt = 0; cnt < sizeof(buffer); cnt++)
      {
        if (buffer[cnt] < 0x10)
        {
          Serial.print(F("0"));
        }
        Serial.print(buffer[cnt], HEX);
        Serial.print(" ");
      }
      Serial.println();
    }
  }

  lastReceiveStatus = rs;
}

/*
  finite state machine to receive data over rs485
  Returns:
    status
*/
RCVSTATUS rs485Read()
{
  // status of state machine to be reported back to calling function
  static RCVSTATUS rs = RCVSTATUS::READHEADER;
  // internal state of state machine
  static RCVSTATE state = RCVSTATE::WAITING;
  // time last byte was received (for timeout)
  static uint32_t lastReceiveTime;
  // index where to store in buffer
  static uint8_t index;
  // length of data following the header
  static uint8_t dataLength;

  // check for timeout
  if (state != RCVSTATE::WAITING && millis() - lastReceiveTime >= timeout)
  {
    state = RCVSTATE::WAITING;
    index = 0;
    dataLength = 0;
    rs = RCVSTATUS::ERR_TIMEOUT;
    return rs;
  }

  // if there is data
  if (rs485.available())
  {
    lastReceiveTime = millis();
    byte b = rs485.read();
    printByte(b);

    // check for buffer overflow
    if (index == sizeof(buffer))
    {
      state = RCVSTATE::WAITING;
      index = 0;
      dataLength = 0;
      rs = RCVSTATUS::ERR_OVERFLOW;
      return rs;
    }

    // finite state machine
    switch (state)
    {
      case RCVSTATE::WAITING:
        // clear the buffer
        memset(buffer, 0, sizeof(buffer));
        // store received byte
        buffer[index++] = b;
        // continue with other header bytes
        state = RCVSTATE::HEADER;
        // to be reported to caller
        rs = RCVSTATUS::READHEADER;
        break;
      case RCVSTATE::HEADER:
        buffer[index++] = b;
        // if we have received the full header
        if (index == headerLength)
        {
          // get the length of the actual data
          dataLength = buffer[headerLength - 1];
          // continue reading data bytes
          state = RCVSTATE::DATA;
          // report back to caller
          rs = RCVSTATUS::READDATA;
        }
        break;
      case RCVSTATE::DATA:
        buffer[index++] = b;
        // if all data bytes are received
        if (index == headerLength + dataLength)
        {
          // continue reading the crc
          state = RCVSTATE::CRC;
          // report back to caller
          rs = RCVSTATUS::READCRC;
        }
        break;
      case RCVSTATE::CRC:
        // 2 bytes
        buffer[index++] = b;
        // if we got the two bytes of the crc
        if (index == headerLength + dataLength + 2)
        {
          index = 0;
          dataLength = 0;
          // begin from the start
          state = RCVSTATE::WAITING;
          // report back to caller
          rs = RCVSTATUS::COMPLETE;
        }
        break;
    }
  }
  return rs;
}

/*
  print a byte in hex followed by a space
  In:
    byte to print
*/
void printByte(uint8_t b)
{
  if (b < 0x10)
  {
    Serial.print(F("0"));
  }
  Serial.print(b, HEX);
  Serial.print(" ");
}

/*
  print rs485Read() status 
  In:
    status to print
*/
void printStatus(RCVSTATUS status)
{
  switch (status)
  {
    case RCVSTATUS::COMPLETE:
      Serial.print(F("COMPLETE"));
      break;
    case RCVSTATUS::READHEADER:
      Serial.print(F("READHEADER"));
      break;
    case RCVSTATUS::READDATA:
      Serial.print(F("READDATA"));
      break;
    case RCVSTATUS::READCRC:
      Serial.print(F("READCRC"));
      break;
    case RCVSTATUS::ERR_TIMEOUT:
      Serial.print(F("TIMEOUT"));
      break;
    case RCVSTATUS::ERR_OVERFLOW:
      Serial.print(F("OVERFLOW"));
      break;
  }
}

I wrote a basic PLC simulator that sends the specified data packet; board used was a Leonardo. It has the options to

  1. Send the complete message.
  2. Send a partial message.
  3. Add a delay between two random bytes in the message.

Basic simulator

uint8_t message[] = { 0x01, 0x10, 0x00, 0x01, 0x00, 0x06, 0x0C, 0x16, 0xBD, 0x16, 0xBE, 0x16, 0xBE, 0x16, 0xBF, 0x16, 0xC0, 0x16, 0xC1, 0xF4, 0x77 };
void setup()
{
  Serial.begin(115200);
  Serial1.begin(9600);

  while (!Serial) {}

  Serial.println(F("PLC sim 0.1"));
}

void loop()
{
  uint8_t byteCnt = 0;
  uint8_t num;

  if (Serial.available())
  {
    char ch = Serial.read();
    switch (ch)
    {
      case 's':
        // send
        byteCnt = Serial1.write(message, sizeof(message));
        Serial.print(byteCnt);
        Serial.println(F(" bytes transferred"));
        break;
      case 'p':
        // partial
        num = random(sizeof(message) - 2);
        byteCnt = Serial1.write(message, num);
        Serial.print(byteCnt);
        Serial.println(F(" bytes transferred"));
        break;
      case 't':
        // add a timeout
        num = random(sizeof(message) - 2);
        byteCnt = 0;
        for (uint8_t cnt = 0; cnt < sizeof(message); cnt++)
        {
          byteCnt += Serial1.write(message[cnt]);
          if (cnt == num)
          {
            delay(500);
          }
        }
        Serial.print(byteCnt);
        Serial.println(F(" bytes transferred"));
        break;
    }
  }
}

Below the serial monitor output for the Mega using SoftwareSerial. The sequence in the Leonardo's serial monitor was spsssppss as one text; that basically sends data without any delays between packets and hence the errors, even for the last two full packets were send.

10:34:00.328 -> 
10:34:00.328 -> receiverDemo
10:34:00.328 -> 
10:34:00.328 -> Status changed from COMPLETE to READHEADER
10:34:12.832 -> 01 10 00 01 00 06 0C 
10:34:12.873 -> Status changed from READHEADER to READDATA
10:34:12.873 -> 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 
10:34:12.873 -> Status changed from READDATA to READCRC
10:34:12.873 -> F4 77 
10:34:12.873 -> Status changed from READCRC to COMPLETE
10:34:12.873 -> Received: 01 10 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 F4 77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
10:34:54.418 -> 01 
10:34:54.418 -> Status changed from COMPLETE to READHEADER
10:34:54.418 -> 10 01 10 00 01 00 
10:34:54.418 -> Status changed from READHEADER to READDATA
10:34:54.470 -> 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 F4 77 01 10 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 F4 77 01 10 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 F4 77 
10:34:54.552 -> Status changed from READDATA to OVERFLOW
10:34:54.552 -> 01 
10:34:54.552 -> Status changed from OVERFLOW to READHEADER
10:34:54.552 -> 10 00 01 00 01 10 
10:34:54.552 -> Status changed from READHEADER to READDATA
10:34:54.552 -> 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 01 
10:34:54.552 -> Status changed from READDATA to READCRC
10:34:54.552 -> 10 00 
10:34:54.552 -> Status changed from READCRC to COMPLETE
10:34:54.552 -> Received: 01 10 00 01 00 01 10 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 01 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
10:34:54.552 -> 01 
10:34:54.552 -> Status changed from COMPLETE to READHEADER
10:34:54.600 -> 00 06 0C 16 BD 16 
10:34:54.600 -> Status changed from READHEADER to READDATA
10:34:54.600 -> BE 16 BE 16 BF 16 C0 16 C1 F4 77 01 10 00 01 06 16 BD 16 16 16 16 
10:34:54.600 -> Status changed from READDATA to READCRC
10:34:54.600 -> 77 
10:34:54.634 -> Status changed from READCRC to TIMEOUT
==
10:35:02.216 -> 01 
10:35:02.216 -> Status changed from TIMEOUT to READHEADER
10:35:02.254 -> 10 00 01 00 06 0C 
10:35:02.254 -> Status changed from READHEADER to READDATA
10:35:02.254 -> 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 
10:35:02.254 -> Status changed from READDATA to READCRC
10:35:02.254 -> F4 77 
10:35:02.254 -> Status changed from READCRC to COMPLETE
10:35:02.254 -> Received: 01 10 00 01 00 06 0C 16 BD 16 BE 16 BE 16 BF 16 C0 16 C1 F4 77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

After that sequence I did send one more complete packet (after the ==; note the jump in the timestamp) to demonstrate that the Mega code can gain sync again with the PLC data.