Skip to content

Opta / mbed boards: last TX byte truncated because endTransmission() drops DE before UART shift register empties #75

Description

@dorkmo

On boards using the Arduino mbed core (Arduino Opta WiFi, Opta RS485, Portenta H7, Nano 33 BLE, etc.), RS485.endTransmission() drops the DE (driver-enable) line before the UART has finished clocking the final byte out of the shift register. The result: the last byte of every transmission is truncated on the wire, the slave's CRC check fails, and the slave silently rejects the frame.

The default _postDelay of 0 µs is the trigger. The library does call _serial->flush(), but on the mbed core Serial1.flush() returns when the TX buffer is empty — before the UART transmit-complete (TXC) flag asserts. DE then drops mid-byte.

This affects every Modbus RTU user on the Opta. The workaround is well-known in the community but not discoverable without burning a day on a logic analyzer.

Affected boards (confirmed or strongly suspected)

  • Arduino Opta WiFi (AFX00002) — confirmed
  • Arduino Opta RS485 (AFX00001) — same RS-485 hardware, expected same behavior
  • Likely also: Portenta H7 + Portenta Machine Control, Nano 33 BLE shields, any board where flush() does not block on TXC

Reproduction

Minimal sketch (Opta WiFi, any Modbus RTU slave at 9600 8N2 — we used a SunSaver MPPT via Morningstar MRC-1):

#include <ArduinoRS485.h>
#include <ArduinoModbus.h>

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000) {}
  ModbusRTUClient.begin(9600, SERIAL_8N2);
  ModbusRTUClient.setTimeout(500);
  // <-- comment this line out to reproduce the bug
  // RS485.setDelays(0, 1200);
}

void loop() {
  uint16_t v = 0;
  if (ModbusRTUClient.requestFrom(1, HOLDING_REGISTERS, 0x0008, 1) &&
      ModbusRTUClient.available()) {
    v = ModbusRTUClient.read();
    Serial.print("OK 0x"); Serial.println(v, HEX);
  } else {
    Serial.print("FAIL err="); Serial.println(ModbusRTUClient.lastError());
  }
  delay(1000);
}

Without setDelays(0, 1200): every poll fails with lastError() == 4 (timeout). Logic-analyzer capture shows the final byte of the request is truncated to ~half its normal width.

With setDelays(0, 1200): every poll succeeds.

Root cause analysis

In RS485.cpp:

size_t RS485Class::endTransmission() {
  _serial->flush();
  if (_postDelay > 0) {
    delayMicroseconds(_postDelay);
  }
  if (_dePin != -1) {
    digitalWrite(_dePin, LOW);
  }
  // ...
}

On AVR cores, HardwareSerial::flush() polls the UART data-register-empty and the transmit-complete flag, so DE drops cleanly after the last byte clears the shift register. On the mbed core, arduino::UART::flush() (and the underlying mbed::SerialBase) only wait for the software TX buffer to drain; the hardware shift register is still clocking when the call returns.

The current default of _postDelay = 0 therefore works on AVR but breaks on mbed.

Suggested fixes (in order of preference)

  1. Block on hardware TX-complete inside endTransmission() on mbed.
    Use the mbed SerialBase API or write directly to the peripheral's TXC flag rather than relying on Serial1.flush() semantics. This is the correct, baud-rate-independent fix.

  2. Set a sane default _postDelay for mbed/Opta boards.
    Computed from baud rate, e.g. one character time + margin:

    #if defined(ARDUINO_OPTA) || defined(ARDUINO_PORTENTA_H7_M7) || \
        defined(ARDUINO_NANO33BLE)
      // 11 bits/char @ baud, in microseconds, plus 10% margin
      _postDelay = (11UL * 1100000UL) / baudrate;
    #endif

    At 9600 baud this is ~1260 µs (matches our empirically-validated 1200 µs); at 115200 it is ~105 µs.

  3. At minimum: document this in the README under a "Known issues" or "Board-specific notes" section so users find it before the bug-hunt.

Workaround for current users

After ModbusRTUClient.begin() (which re-initializes the serial port and overwrites RS485 settings), call:

RS485.setDelays(0, 1200);   // 1200 µs covers 9600 8N1 (1042 µs) and 8N2 (1146 µs)

Note: it must be called after ModbusRTUClient.begin(), not before, because begin() re-applies its own RS485 configuration.

References

Environment

  • arduino-cli 1.x
  • Core: arduino:mbed_opta (latest)
  • Library: ArduinoRS485 (latest from master)
  • Library: ArduinoModbus (latest from master)
  • Board: Arduino Opta WiFi (AFX00002), FQBN arduino:mbed_opta:opta

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic: codeRelated to content of the project itselftype: imperfectionPerceived defect in any part of project

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions