CMB27 / ModbusRTUMaster

A library to implement the master/client portion of the Modbus RTU protocol on Arduino
MIT License
49 stars 7 forks source link

Arduino Giga not working as master #14

Closed Cybergrany closed 2 weeks ago

Cybergrany commented 8 months ago

Hello, and thanks for providing this excellent and up to date Modbus library.

I'm having problems using my Arduino giga as a master in a network of Arduino nano's. I've tried various implementations, from the examples provided in your library to even more basic read/write operations for all data types.

I would have thought the issue could be in my network (wiring/rs485 modules etc), but oddly enough simply porting the master code onto one of the Nano's, and the slave code on the Giga works flawlessly.

For now as a workaround I'm using a nano as a "relay" device to send instructions between the giga and other nanos but as you'd expect this is getting extremely cumbersome (not to mention the IO overheads.

I unfortunately don't have any other arduino boards to hand, so can't see if it's an issue with the Giga, and other Modbus libraries don't work at all with my setup.

Thanks in advance!

Cybergrany commented 8 months ago

Update: I found the debug code you provided in this closed issue

It's simply timing out every time, I've upped the Giga master's timeout but no result

When I connect the Giga to my computer and monitor the serial traffic this is what it sends each update: Rx:000004-46 61 69 6C 75 72 65 3A 20 54 69 6D 65 6F 75 74 3A 20 34 35 0D 0A 01 01 00 00 00 03 7C 0B

CMB27 commented 8 months ago

There is a known issue with the ModbusRTUSlave library with the Arduino UNO R4 Minima, where the Serial.flush() command does not work correctly. Serial.flush() is supposed to delay until all the bytes in the transmit buffer are sent. In the library, this is used to control the timing of the DE pin. The issue is that Serial.flush() is not waiting, causing the DE pin to be set LOW before the transmission is complete.

ModbusRTUMaster is also affected by this issue.

I have not tested either of these libraries with the Arduino Giga, but it sounds like it could be the same issue. A work-around is suggested in the issue.

Cybergrany commented 8 months ago

Thanks for your reply, I got very excited seeing your simple work-around, however it doesn't seem to work for me. A slight difference after adding the while loop to ModbusRTUMaster::_writeRequest and ModbusRTUSlave::_writeResponse is that the slave Nano sends a TX when the master pings it. No valid data is being received however.

I'll try and do some traffic sniffing when I get a chance

Cybergrany commented 8 months ago

Here's what I see when I snoop the serial on the slave nano (ID:1, Write single coil)

Rx01 05 00 01 FF 00 DD FA
Tx01 00 00 00 03 7C 38

Online parsers here and here show that the slave is seeing and sending the expected data.

The Giga isn't even seeing the serial stream, it just times out every time with the check while (!_serial->available()) in ModbusRTUMaster::_readResponse. If you think it's worth it I'll wire up a serial snoop on the Giga side as well. I've tried a few different MAX485 boards and wires to rule them out, but same result, and it doesn't explain why things work perfectly when I reverse master/slave roles.

Another thing I'm observing which may be of interest; I have the DE/RE pins on both my MAX485 boards hooked up to LEDs to monitor traffic. When experimenting with adjusting delays as suggested in the issue you mentioned, the LED's for both slave and master light up simultaneously, for a longer period as set by the while loop. Shouldn't the LEDs be firing consecutively?

CMB27 commented 7 months ago

I think it would be worthwhile to see what's happening on the Giga side.

Yes, the LEDs should be firing consecutively. If both devices have their DE pins high at the same time, the RS-485 bus will be jammed.

Cybergrany commented 7 months ago

Thanks again for your reply.

On second look it turns out the Tx from the slave was from a Modbus debug program I had running in the background on my computer, apologies for any confusion caused. The nano is seeing a correct modbus request, but not sending a response, although the Nano's on-board TX light and DE/RE LED's on its MAX485 are flashing.

I've done a lot of experimenting with timing in your master/slave libraries but can't get this behaviour to change. The DE/RE LED's still seem to fire instantaneously, even with a long (non blocking) delay in ModbusRTUMaster::_writeRequest To test this further, I added constant +5v to the DE/RE pins on the master MAX485, which resulted in the same behaviour at the slave end. I'm not sure at this time what the slave is trying to TX, my serial sniffer isn't picking up any data.

I decided to swap the slave/master roles (giga=slave, nano=master, exact same code) to recheck things, and discovered that the nano works as before (read/write), yet for some reason the timeout flag is being triggered every time. I hadn't noticed this as I wasn't looking for it when I swapped roles last time.

Going back to using the Nano as a slave, I tried a new board, the Mega 2560R3 in case the Giga/Nano microprocessor difference was causing problems. Exact same issue.

Before you ask, throughout this time I've been trying different MAX485 chips to rule out faulty ones.

Can I ask what setup you use for testing? I'd like to replicate it on my end and go from there as I'm currently tearing my hair out trying to get this to work. The project I'm working on would greatly benefit from many cheap arduino boards talking to a more capable master (Mega/Giga, anything with more than one serial port)

Also let me know if there's anywhere I can donate to the project! I appreciate the responses here and the up-to-date library and would love to show it :)

Cybergrany commented 7 months ago

Update:

Seems my issue has a few layers. I changed my wiring so master and slave RE is grounded all the time (as opposed to connected to DE pin), and now I seem to have write success from my Giga (writeSingleCoil returns true)

I've been experimenting with reading the serial stream directly using the following modification to ModbusRTUMaster::_readResponse

do {
    if (_serial->available()) {
      startTime = micros();
      _buf[numBytes] = _serial->read();
      Serial.print(_buf[numBytes]); //Print serial stream
      numBytes++;
    }
  } while (micros() - startTime <= _charTimeout && numBytes < MODBUS_RTU_MASTER_BUF_SIZE);

  Serial.println("END"); //Mark end
  while (micros() - startTime < _frameTimeout);

In my loop, I do one read request and a write, with 750ms in between. Here's the output:

16:14:23: Read: 11000301882191255END
16:14:23: Failure
16:14:23: Write: 15012550221250END
16:14:23: Success

Where Failure and Success are parsed from checking if (writeSingleCoil or 'readCoils' return true)

What would the expected stream from the slave look like? Am I missing some data here?

CMB27 commented 7 months ago

Using

Serial.print(_buf[numBytes], HEX);
Serial.print(" ");

instead of

Serial.print(_buf[numBytes]);

would yield more useful output. I can't tell where one byte ends and the next begins. I'm pretty sure your first byte is a 1, but it could be an 11 or 110.

CMB27 commented 7 months ago

As for my test setup, I use a shield I designed that has some test IO and RS-232 and RS-485 circuitry on it.

I have a collection of Arduino boards I use for testing; the Giga is not among them, however the Mega 2560 is. I have tested the Mega with the ModbusRTUMaster library, so it should work without any modifications.

What code are you uploading? Can you share?

CMB27 commented 7 months ago

I don't have anything setup for accepting donations and I have no plans to. Though I am glad that my work here can be of some service.

Cybergrany commented 7 months ago

Thanks again for your replies and time.

Using

Serial.print(_buf[numBytes], HEX);
Serial.print(" ");

instead of

Serial.print(_buf[numBytes]);

would yield more useful output.

My apologies, decoding information at this level isn't my area of expertise.

Here's the updated output (I've also been testing the other operations to see the data received):

11:01:03: Write Coils: 1 F 0 0 0 2 1 0 DE 97 1 F 0 0 0 2 D4 A END
11:01:03: Failure
11:01:04: Read input regs: 1 4 0 0 0 2 71 CB 1 4 4 0 1 0 1 6B 84 END
11:01:04: Failure
11:01:04: Read discrete inputs: 1 2 0 0 0 2 F9 CB 1 2 1 3 E1 89 END
11:01:04: Failure
11:01:05: Write regs: 1 10 0 0 0 2 4 0 13 0 1C 3 A3 1 10 0 0 0 2 41 C8 END
11:01:05: Failure
11:01:05: Write singlecoil : 1 5 0 0 FF 0 8C 3A 1 5 0 0 FF 0 8C 3A END
11:01:05: Failure

As far as my modbus decoders are concerned, this is gibberish, any idea what might be happening here?

I'm using a slightly modified version of your test code. I'm cycling through each function slowly so I can keep up with the output:

Master.ino


#include "ModbusRTUMaster.h"

const byte potPins[2] = {A0, A1};
const byte buttonPins[2] = {2, 3};
const byte ledPins[4] = {5, 6, 7, 8};
const byte dePin = 53;

ModbusRTUMaster modbus(Serial1, dePin); // serial port, driver enable pin for rs-485

bool coils[2];
bool discreteInputs[2];
uint16_t holdingRegisters[2];
uint16_t inputRegisters[2];

void setup() {

    pinMode(potPins[0], INPUT);
    pinMode(potPins[1], INPUT);
    pinMode(buttonPins[0], INPUT_PULLUP);
    pinMode(buttonPins[1], INPUT_PULLUP);
    pinMode(ledPins[0], OUTPUT);
    pinMode(ledPins[1], OUTPUT);
    pinMode(ledPins[2], OUTPUT);
    pinMode(ledPins[3], OUTPUT);

  modbus.setTimeout(1000);
  modbus.begin(9600, SERIAL_8N1);

    Serial.begin(9600);

    Serial.begin(9600);
    while (!Serial);
}

// The loop function is called in an endless loop
unsigned long ts = 0;
int step = 0;

void loop() {

    holdingRegisters[0] = map(analogRead(potPins[0]), 0, 1023, 0, 255);
    holdingRegisters[1] = map(analogRead(potPins[1]), 0, 1023, 0, 255);
    coils[0] = !digitalRead(buttonPins[0]);
    coils[1] = !digitalRead(buttonPins[1]);

    if(millis() - ts > 350){

        ts = millis();

          switch(step){
              case 0:
                  Serial.print("Write regs: ");
                  debug(modbus.writeMultipleHoldingRegisters(1, 0, holdingRegisters, 2));
                  step++;
                  break;
              case 1:
                  Serial.print("Write singlecoil : ");
                  debug(modbus.writeSingleCoil(1, 0, 1));
                  step++;
                  break;
              case 2:
                  Serial.print("Write Coils: ");
                  debug(modbus.writeMultipleCoils(1, 0, coils, 2));
                  step++;
                  break;
              case 3:
                  Serial.print("Read input regs: ");
                  debug(modbus.readInputRegisters(1, 0, inputRegisters, 2));
                  step++;
                  break;
              case 4:
                  Serial.print("Read discrete inputs: ");
                  debug(modbus.readDiscreteInputs(1, 0, discreteInputs, 2));
                  break;
              default:
                  step = 0;
              }

        }
}
  bool debug(bool modbusRequest) {
    if (modbusRequest == true) {
      Serial.println("Success");
    }
    else {
      Serial.print("Failure");
      if (modbus.getTimeoutFlag() == true) {
        Serial.print(": Timeout");
        modbus.clearTimeoutFlag();
      }
      else if (modbus.getExceptionResponse() != 0) {
        Serial.print(": Exception Response ");
        Serial.print(modbus.getExceptionResponse());
        switch (modbus.getExceptionResponse()) {
          case 1:
            Serial.print(" (Illegal Function)");
            break;
          case 2:
            Serial.print(" (Illegal Data Address)");
            break;
          case 3:
            Serial.print(" (Illegal Data Value)");
            break;
          case 4:
            Serial.print(" (Server Device Failure)");
            break;
          default:
            Serial.print(" (Uncommon Exception Response)");
            break;
        }
        modbus.clearExceptionResponse();
      }
      Serial.println();
    }
    Serial.flush();
    return modbusRequest;
  }

Slave.ino


#include "ModbusRTUSlave.h"

const byte potPins[2] = {A0, A1};
const byte buttonPins[2] = {9, 3};
const byte ledPins[4] = {5, 6, 7, 8};
const byte dePin = 2;

ModbusRTUSlave modbus(Serial, dePin);  // serial port, driver enable pin for rs-485

bool coils[2];
bool discreteInputs[2];
uint16_t holdingRegisters[2];
uint16_t inputRegisters[2];

void setup() {

  modbus.configureCoils(coils, 2);                       // bool array of coil values, number of coils
  modbus.configureDiscreteInputs(discreteInputs, 2);     // bool array of discrete input values, number of discrete inputs
  modbus.configureHoldingRegisters(holdingRegisters, 2); // unsigned 16 bit integer array of holding register values, number of holding registers
  modbus.configureInputRegisters(inputRegisters, 2);     // unsigned 16 bit integer array of input register values, number of input registers

  modbus.begin(1, 9600);

//  for(int i = 0; i < 2; i++){//Init arrays to debug reads
//    coils[i] = 0;
//    discreteInputs[i] = 0;
//    holdingRegisters[i] = 0;
//    inputRegisters[i] = 0;
//  }

    Serial.begin(9600);
}

int i = 0;

void loop() {
  inputRegisters[0] = map(analogRead(potPins[0]), 0, 1023, 0, 255);
  inputRegisters[1] = map(analogRead(potPins[1]), 0, 1023, 0, 255);
  discreteInputs[0] = !digitalRead(buttonPins[0]);
  discreteInputs[1] = !digitalRead(buttonPins[1]);

  modbus.poll();

//  analogWrite(ledPins[0], holdingRegisters[0]);
//  analogWrite(ledPins[1], holdingRegisters[1]);
//  digitalWrite(ledPins[2], coils[0]);
//  digitalWrite(ledPins[3], coils[1]);
}

And I've made slight modifications to your library, marked with comments

void ModbusRTUMaster::_writeRequest(uint8_t len) {
    unsigned long startTime = 0; //Timing fix
  uint16_t crc = _crc(len);
  _buf[len] = lowByte(crc);
  _buf[len + 1] = highByte(crc);
  if (_dePin != NO_DE_PIN) digitalWrite(_dePin, HIGH);
  startTime = micros(); //Timing fix
  _serial->write(_buf, len + 2);
  _serial->flush();
  while (micros() - startTime < (_charTimeout * (len + 2))); //Timing fix
  if (_dePin != NO_DE_PIN) digitalWrite(_dePin, LOW);
}

uint16_t ModbusRTUMaster::_readResponse(uint8_t id, uint8_t functionCode) {
  unsigned long startTime = millis();
  uint16_t numBytes = 0;
  while (!_serial->available()) {
    if (millis() - startTime >= _responseTimeout) {
      _timeoutFlag = true;
      return 0;
    }
  }
  do {
    if (_serial->available()) {
      startTime = micros();
      _buf[numBytes] = _serial->read();
      Serial.print(_buf[numBytes], HEX); //Print serial stream
      Serial.print(" "); //Space between each hex
      numBytes++;
    }
  } while (micros() - startTime <= _charTimeout && numBytes < MODBUS_RTU_MASTER_BUF_SIZE);

  Serial.println("END"); //Mark end
  while (micros() - startTime < _frameTimeout);
  if (_serial->available() || _buf[0] != id || (_buf[1] != functionCode && _buf[1] != (functionCode + 128)) || _crc(numBytes - 2) != _bytesToWord(_buf[numBytes - 1], _buf[numBytes - 2])) return 0;
  else if (_buf[1] == (functionCode + 128)) {
    _exceptionResponse = _buf[2];
    return 0;
  }
  return (numBytes - 2);
}

Slave:

void ModbusRTUSlave::_writeResponse(uint8_t len) {
    unsigned long startTime = 0; //Timing fix
  if (_buf[0] != 0) {
    uint16_t crc = _crc(len);
    _buf[len] = lowByte(crc);
    _buf[len + 1] = highByte(crc);
    if (_dePin != NO_DE_PIN) digitalWrite(_dePin, HIGH);
    startTime = micros(); //Timing fix
    _serial->write(_buf, len + 2);
    _serial->flush();
    while (micros() - startTime < (_charTimeout * (len + 2))); //Timing fix
    if (_dePin != NO_DE_PIN) digitalWrite(_dePin, LOW);
    while(_serial->available()) {
      _serial->read();
    }
  }
}
Cybergrany commented 7 months ago

Ok, took me a while but I've noticed the serial output is just missing leading 0's. With this in mind, it seems readResonse is reading the request (italic) and response (bold) from the stream

11:49:22: Write singlecoil : 01 05 00 00 FF 00 8C 3A 01 05 00 00 FF 00 8C 3A END 11:49:22: Failure 11:49:23: Write Coils: 01 0F 00 00 00 02 01 00 DE 97 01 0F 00 00 00 02 D4 0A END 11:49:23: Failure 11:49:23: Read input regs: 01 04 00 00 00 02 71 CB 01 04 04 00 00 00 00 FB 84 END 11:49:23: Failure 11:49:23: Read discrete inputs: 01 02 00 00 00 02 F9 CB 01 02 01 03 E1 89 END 11:49:23: Failure 11:49:24: Write regs: 01 10 00 00 00 02 04 00 13 00 1C 03 A3 01 10 00 00 00 02 41 C8 END 11:49:24: Failure

The stream only contains the request if I disconnect the TX from the slave. If I disconnect either dePin or RX I just get a timeout

Cybergrany commented 7 months ago

Update: the problem's definitely an odd one, but I have a workaround. It seems grounding the RE pin on the master made serial always available, which sort of explains the request echo. I wasn't able to get rid of it by changing DE pin timings, it seems to happen across all my MAX485 boards, and I can't find anyone online who's experienced echoing within the MAX485

For now, I've created a fix I'm reasonably happy with, just by filtering the echo in the Master code:

uint16_t ModbusRTUMaster::_readResponse(uint8_t id, uint8_t functionCode, uint8_t rlen) {
    rlen++;
  unsigned long startTime = millis();
  uint16_t numBytes = 0, incount = 0;
  while (!_serial->available()) {
    if (millis() - startTime >= _responseTimeout) {
      _timeoutFlag = true;
      return 0;
    }
  }
  do {
    if (_serial->available()) {
      startTime = micros();

      uint8_t i= _serial->read(); //temporary hold input

      if(incount>rlen){ //If greater than request length, we likely have an echo
          _buf[numBytes] = i;

          char formattedByte[3]; // Buffer to hold formatted byte
          sprintf(formattedByte, "%02X", _buf[numBytes]);
          Serial.print(formattedByte); // Print formatted byte
          Serial.print(" ");
          numBytes++;
      }else{
          _buf[incount] = i;//Write first bit of stream in case we don't have echo
      }

      incount++;

    }
  } while (micros() - startTime <= _charTimeout && numBytes < MODBUS_RTU_MASTER_BUF_SIZE);

  Serial.println("END"); //Mark end
  while (micros() - startTime < _frameTimeout);
  if (_serial->available() || _buf[0] != id || (_buf[1] != functionCode && _buf[1] != (functionCode + 128)) || _crc(numBytes - 2) != _bytesToWord(_buf[numBytes - 1], _buf[numBytes - 2])) return 0;
  else if (_buf[1] == (functionCode + 128)) {
    _exceptionResponse = _buf[2];
    return 0;
  }
  return (numBytes - 2);
}

Hopefully this still works with more devices connected, I'll do some testing and get back to you

AndreyIv1972 commented 5 months ago

I had a similar problem. After sending the request I clear the receive buffer.

void ModbusRTUMaster::_writeRequest(uint8_t len) { uint16_t crc = _crc(len); _buf[len] = lowByte(crc); _buf[len + 1] = highByte(crc); if (_dePin != NO_DE_PIN) digitalWrite(_dePin, HIGH); _serial->write(_buf, len + 2); _serial->flush(); if (_dePin != NO_DE_PIN) digitalWrite(_dePin, LOW); _clearRxBuffer(); //+ }

Cybergrany commented 2 weeks ago

Forgot I hadn't closed this, I used _flushCompensationDelay from the Slave library:

void ModbusRTUMaster::_writeRequest(uint8_t len) {
  uint16_t crc = _crc(len);
  _buf[len] = lowByte(crc);
  _buf[len + 1] = highByte(crc);
  if (_dePin != NO_DE_PIN) digitalWrite(_dePin, HIGH);
  _serial->write(_buf, len + 2);
  _serial->flush();
  #ifdef FLUSH_COMPENSATION_DELAY
  **delayMicroseconds(_flushCompensationDelay);**
  #endif
  if (_dePin != NO_DE_PIN) digitalWrite(_dePin, LOW);
}