Issues with Serial2 in a Photon2 / P2 and ModbusClient library

I'm having issues interfacing with the ParticleModbusClient RS-485 library here: GitHub - particle-iot/ParticleModbusClient: This is an Particle library for communicating with Modbus servers over RS232/485 (via RTU protocol)

I'm specifically interfacing with Serial2 on my board and am having trouble with messages getting all the way through. It appears to be some timing issue with Serial2.write and Serial2.flush commands.

It seems that the entire full length message is not being transmitted to the RS-485 chip.

See Modbus Slave communication window, which is showing the repetitive polls being different every time.

If I modify the library by adding a delay after the Serial Flush command and it fixes the issue!

MBSerial.flush(); //Wait for transmission to get completed

delay(50);

if (MBUseEnablePin == 1) {  //Switch RS485 driver back to receiving mode.
	setReceiveMode();
}

Here see the RX and TX message being sent back by Modbus Slave program after adding the delay after Flush:

I believe this is an issue on the RTL872x, and affects all serial ports, and also SPI. The problem is that there are two FIFOs, the byte-oriented user FIFO, and also an internal FIFO that is responsible for outputting the actual bits over the wire.

The issue is that the queue will be empty, or the DMA callback is called when the user FIFO is empty, not when all bytes have successfully been transmitted over the wire.

This doesn't matter if all you are doing is refilling the queue. However, if you are using queue empty to change direction in RS485 or de-assert CS in SPI, that will be an issue.

At this time, the only solution is adding a delay.

I'm not 100% sure that is what is happening, however. I ran this test program on a Photon 2 running Device OS 5.8.2:

#include "Particle.h"

SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);

SerialLogHandler logHandler(LOG_LEVEL_INFO);

const pin_t signalPin = D2;
const int baudRate = 1200;

const std::chrono::milliseconds testPeriod = 5s;
unsigned long lastTest = 0;

void runTest();

void setup() {
    Serial1.begin(baudRate);

    pinMode(signalPin, OUTPUT);
    digitalWrite(signalPin, LOW);

    Particle.connect();
}

void loop() {
    if (millis() - lastTest >= testPeriod.count()) {
        lastTest = millis();
        runTest();
    }
}

void runTest() {
    digitalWrite(signalPin, HIGH);

    int bufferSize = Serial1.availableForWrite();

    unsigned long start = millis();

    for(int ii = 0; ii < 64; ii++) {
        char c = 0x20 + ii;
        Serial1.write(c);        
    }
    unsigned long writeComplete = millis();

    while(Serial1.availableForWrite() < bufferSize) {
    }

    unsigned long empty = millis();

    digitalWrite(signalPin, LOW);

    Log.info("writeComplete=%lu empty=%lu", writeComplete - start, empty - start);

}

The buffer empty is very close to the end of the transmission, but it does not appear to be before it.

Log messages:

0000310532 [app] INFO: writeComplete=1 empty=532
0000315532 [app] INFO: writeComplete=1 empty=532
0000320533 [app] INFO: writeComplete=2 empty=532
0000325533 [app] INFO: writeComplete=1 empty=533

The statement about the SPI DMA callback is definitely true on the P2/Photon 2/M-SoM but the serial issue could be something else.

It does appear to be a difference in behavior with Serial2.

Since you were using flush() instead of availableForWrite() I changed the code and it looks the same for Serial1:

However this is what it looks like for Serial2 using flush(). The signalPin is deasserted far before the transmission is complete, but only for Serial2.

Here's the Serial2 code (D4 = Serial2 TX, D6 = signalPin):

#include "Particle.h"

SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);

SerialLogHandler logHandler(LOG_LEVEL_INFO);

const pin_t signalPin = D6;
const int baudRate = 1200;

const std::chrono::milliseconds testPeriod = 5s;
unsigned long lastTest = 0;

void runTest();

void setup() {
    Serial2.begin(baudRate);

    pinMode(signalPin, OUTPUT);
    digitalWrite(signalPin, LOW);

    Particle.connect();
}

void loop() {
    if (millis() - lastTest >= testPeriod.count()) {
        lastTest = millis();
        runTest();
    }
}

void runTest() {
    digitalWrite(signalPin, HIGH);

    int bufferSize = Serial2.availableForWrite();

    unsigned long start = millis();

    for(int ii = 0; ii < 64; ii++) {
        char c = 0x20 + ii;
        Serial2.write(c);        
    }
    unsigned long writeComplete = millis();

    // while(Serial2.availableForWrite() < bufferSize) {}

    Serial2.flush();

    unsigned long empty = millis();

    digitalWrite(signalPin, LOW);

    Log.info("writeComplete=%lu empty=%lu", writeComplete - start, empty - start);

}


Incidentally, Serial3 also works fine. It's just Serial2.

#include "Particle.h"

SYSTEM_MODE(SEMI_AUTOMATIC);
SYSTEM_THREAD(ENABLED);

SerialLogHandler logHandler(LOG_LEVEL_INFO);

const pin_t signalPin = D6;
const int baudRate = 1200;

const std::chrono::milliseconds testPeriod = 5s;
unsigned long lastTest = 0;

void runTest();

void setup() {
    // Serial3 TX is S0/D15/MOSI
    Serial3.begin(baudRate);

    pinMode(signalPin, OUTPUT);
    digitalWrite(signalPin, LOW);

    Particle.connect();
}

void loop() {
    if (millis() - lastTest >= testPeriod.count()) {
        lastTest = millis();
        runTest();
    }
}

void runTest() {
    digitalWrite(signalPin, HIGH);

    int bufferSize = Serial3.availableForWrite();

    unsigned long start = millis();

    for(int ii = 0; ii < 64; ii++) {
        char c = 0x20 + ii;
        Serial3.write(c);        
    }
    unsigned long writeComplete = millis();

    while(Serial3.availableForWrite() < bufferSize) {}

    // Serial3.flush();

    unsigned long empty = millis();

    digitalWrite(signalPin, LOW);

    Log.info("writeComplete=%lu empty=%lu", writeComplete - start, empty - start);

}

1 Like

Thanks for your in-depth analysis and for confirming my suspicions with logic @rickkas7 .

I've designed two P2 boards that both offer RS-485. One uses Serial3 (no issues) and the other uses Serial2.

If I'm reading your replies correctly, you are suggesting to use availableForWrite() instead of flush() for Serial2? I'll give that a shot and see if I have any issues.

This seems high enough priority (a Serial UART not working as it should) to add to the bug fixes list for future DeviceOS versions. Is it possible to fix Flush in DeviceOS code?

Replacing flush() with availableForWrite() is not working unfortunately:

	while(MBSerial.availableForWrite() < ku8MaxBufferSize) {}

	// MBSerial.flush(); //Wait for transmission to get completed
	// delay(50);

No, availableForWrite behaves the same way as flush() on Serial2.

I believe it's a problem in how Serial2 works on the P2/Photon 2. All of the other ports uses a dedicated UART peripheral, but Serial2 uses the generic high speed serial port, which is also used for SPI. It appears that it behaves differently.

Serial3 shares the same pins as SPI, but Serial3 uses an internal pin mux to switch the port between the dedicated UART peripheral and the SPI peripheral, which is why that behaves differently.

2 Likes

Yep, it appears so.

So for now, the fix is to add a delay (not really a solution for my application), change my board design to Serial1 or Serial3, or potentially wait for a DeviceOS solution/fix?

Hi Bryan,

We're discussing this with engineering tomorrow during our triage meeting. Will get back to you with feedback once the team have investigated.

1 Like

Engineering is investigating - possibly a quick fix!

1 Like

Excellent - thank you!