Troubleshooting Modbus TCP-to-RTU Bridge Failures on RS-485

Daniel Price13 min read
ModbusOther ManufacturerTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

The bridge accepted TCP connections at 192.168.0.189:502, but its first workers timed out before processing a request; once the wait was extended, the request reached the RTU slave and the response showed extra 0x00 bytes. Correct RS-485 bias polarity and pair wiring resolved the bench problem; a later field trace succeeded against a different RTU address.

Which physical RS-485 path must carry the request?

The data path is a Modbus TCP client, over Wi-Fi/Ethernet routing, to the bridge’s TCP server; the bridge then queues an RTU request through Serial2 and its RS-485 transceiver to the slave. A TCP connection proves only the first hop. It does not prove the UART, transceiver direction control, cable polarity, or RTU response path works.

The bench setup initially used two leftover wires. The operator later installed UTP cable and switched the A/B connections until the RTU exchange worked. RS-485 is differential: reversing the pair prevents the receiver from seeing the intended polarity, even when the TCP side is healthy. Confirm the transceiver’s polarity markings rather than relying on A/B lettering alone; A/B conventions vary across equipment.

Inspect the bus termination and idle-state bias separately. The reported correction was to bias the bridge end, acting as the RTU client, with 680 Ω from A+ to 5 V and 680 Ω from B− to ground. Bias the bus at one point only. Use 120 Ω termination at both physical ends as the intended arrangement; a single termination may work on a short cable or at low bus speed, but it is not the general two-end arrangement.

Bus component Reported arrangement Commissioning check
Bias 680 Ω: A+ to 5 V; 680 Ω: B− to ground One bias point only, placed at the bridge/client end in this setup
Termination 120 Ω at each bus end Confirm the actual physical ends and avoid extra terminators
Pair UTP pair used in the final working test Match polarity markings at both transceivers

The initial resistor installation put A to ground and B to 5 V, opposite the later recommended polarity. The response bytes continued to contain extra zeros until the polarity and pair wiring were corrected. Check the idle bus and compare raw RTU bytes before changing TCP settings.

Which UART and direction-control settings match the transceiver?

The example configures UART2 as Serial2, with receive on GPIO16, transmit on GPIO17, 9600 baud, and SERIAL_8N1. These settings describe this example, not universal settings for another board or slave; compare each one with the actual transceiver wiring and slave serial configuration.

Serial2.begin(9600, SERIAL_8N1, 16, 17);
ModbusClientRTU MB(Serial2, 4, 2000);
MB.setTimeout(2000);

DE/RE control switches the transceiver between transmitting a query and listening for the reply. A direction-control or UART-pin mismatch can produce a healthy Wi-Fi connection but no valid RTU exchange. The first setup also sends Serial2.println("test"). Remove that diagnostic output: ASCII bytes on the Modbus pair can be interpreted as bus traffic and disrupt a slave or client. Use the USB/debug serial port for messages instead. The example’s yield() call was described as unnecessary in the loop wrapper; removing it is cleanup, not a remedy for corrupted RTU frames.

  1. Trace GPIO16, GPIO17, and GPIO4 from the board to the transceiver’s receive, transmit, and direction-control connections.
  2. Compare baud rate and framing with the RTU slave configuration, then remove non-Modbus output from Serial2.
  3. Send a request and inspect the UART/RS-485 exchange for a transmitted query followed by a received response.

Proceed only when the physical connection and serial settings match the installed hardware and the bus carries bytes in both directions.

Which TCP endpoint accepts the client request?

The example configures station mode, connects to Wi-Fi, and prints the resulting local address. Its static network values are 192.168.0.189, gateway 192.168.0.1, and subnet mask 255.255.255.0. The bridge starts its server on port 502, the port shown in the example for Modbus TCP requests.

Setting Example value Check at the client
Bridge IP 192.168.0.189 Use the address printed after Wi-Fi connects
Gateway 192.168.0.1 Confirm the client has a route to the bridge network
Subnet mask 255.255.255.0 Confirm the configured network matches the installation
TCP port 502 Connect to this port, not the OTA port

The log printed Ready, the IP address, and then the bridge’s server-start message. The client in the report could establish a TCP connection, so basic addressing and server reachability were present. That result did not establish that it sent a valid Modbus TCP request or that the server worker remained alive long enough to process one.

  1. Read the bridge’s actual address from its startup output; do not assume the configured static address took effect if configuration failed.
  2. Set the client destination to that address and TCP port 502.
  3. Send one Modbus request and distinguish a TCP connect/accept log from a request-processing log.

Continue to the RTU mapping check only after the client can reach the intended server endpoint.

How does the bridge map the TCP unit ID to RTU?

The bench configuration calls attachServer(1, 10, ANY_FUNCTION_CODE, &MB). Its startup log, (RTU): 01->0A, shows the bridge mapping TCP unit ID 01 to RTU server address 0A (decimal 10). The TCP client must address the bridge-side unit ID; the bridge uses the configured remote RTU address on the serial hop.

Do not carry the bench address into a different installation without checking the slave. A later field trace shows a request sent to RTU address (decimal 20), whereas the bench setup used (decimal 10). Configure the remote address for the actual slave. A mismatch can leave TCP fully connected while the RTU client waits for a response from the wrong address.

The bridge can register a wildcard function-code mapping or explicit function codes. The example uses ANY_FUNCTION_CODE; another log shows explicit registrations for 03, 04, 06, and 10. The listServer() output reports what the bridge registered. Selecting a function code that the TCP server accepts does not repair a bad RS-485 frame, wrong slave address, or missing response.

Observed mapping/setting Meaning for this hop Decision
01->0A TCP unit 1 maps to RTU address 10 Use only for the bench slave configured at address 10
RTU request starts 14 Field trace targets address 20 Map the field bridge to the actual address 20
03 04 06 10 Explicit function-code registrations in one test Check listServer() against the client operation

Verify both the TCP unit ID and the RTU destination in the trace. A configured mapping is correct only when the outgoing serial request contains the slave’s address.

Does the server stay open long enough to receive a request?

The initial call is MBbridge.start(port, 4, 600); the log reported a worker timeout of 600. The server accepted a connection, then logged that the worker stopped due to timeout before a request was processed. Increasing the bridge wait to 20 seconds produced a worker timeout of 20000 and allowed the later request to reach the RTU client.

Observed wait Observed behavior Use in diagnosis
600 (log: timeout=600) Worker stopped before the operator sent a request Too short for the test/client timing in that run
20000 (20 seconds) Worker stayed active and forwarded a request Useful diagnostic wait for reproducing the exchange
2000 RTU client timeout Set separately through MB.setTimeout(2000) Applies to the RTU response wait, not the TCP worker wait

These waits control different portions of the path. A longer TCP worker lifetime gives the client time to submit a request; the RTU timeout governs how long the serial client waits for its slave. Do not treat the 20-second diagnostic value as a universal production setting. Set the application’s client timing deliberately, and keep the RTU response timeout appropriate for the slave’s measured response behavior.

  1. Reproduce the initial failure while watching the worker-start and worker-stop messages.
  2. For diagnosis, use the reported 20-second bridge wait so the client can send after connecting.
  3. Confirm that a request reaches getWorker and the RTU queue; then set operating timeouts to match actual client and slave response timing.

The proof is a request-processing log before worker shutdown, not merely an accepted TCP connection.

Which log proves the request crossed each hop?

Compile with -DLOG_LEVEL=6 to obtain the verbose messages used in the diagnosis. Read them in order. The early output showed Wi-Fi ready and a server connection accepted, followed by a worker stopping due to timeout; it contained no valid-request processing. After the wait was extended, the log showed getWorker, bridgeWorker forwarding a request, the RTU queue pulling it, bytes sent, bytes received, and a TCP response.

Trace point Evidence to find What it confirms
TCP listener Accepted connection A client opened a TCP connection
Request dispatch getWorker: Worker found and Request (...) sent The server parsed a request and selected a mapping
RTU client addToQueue, Request sent The bridge handed a request to the RTU client and transmitted it
RTU receive Raw buffer received and Data response Bytes returned from the serial side; inspect their framing
TCP reply worker: Data response and response bytes The bridge returned a response to its TCP client

This sequence isolates the stopping point. If the trace ends at accept, inspect request timing and the client’s Modbus TCP transmission. If dispatch occurs but the RTU client times out, investigate unit mapping, UART/transceiver wiring, bus polarity, and slave response. If raw bytes arrive but begin or end with extra bytes, diagnose the physical RTU layer before changing the TCP client.

The report’s first capture showed TCP requests leaving the computer, but the bridge log did not yet prove that the server parsed them. The later trace did: for example, Request (0A/03) sent was followed by an RTU request, received bytes, and a generated TCP response. Confirm the outgoing RTU request itself before attributing a failure to the TCP client.

What do the extra 0x00 bytes say about the RTU frame?

One received buffer was 00 0A 03 08 00 08 00 10 00 18 00 20 79 2F 00. The intended response data began at 0A, but a leading 00 preceded the RTU address and another 00 followed the CRC bytes. A later single-register reply similarly arrived as 00 0A 03 02 00 00 1D 85 00. Those leading and trailing bytes make the received frame invalid for normal RTU parsing; the RTU client generated an exception response rather than a successful register reply.

For a Modbus RTU response, the slave address and function code begin the frame, data follows according to the function, and the CRC occupies the final two bytes. An extra byte before the address shifts the parser’s frame boundary; an extra byte after the CRC means the CRC is no longer at the end of the received frame. The logs show the CRC bytes in the expected response body, but they do not make a frame valid when additional bytes surround it.

The diagnosis linked the extra zeros to the RS-485 line state at DE/RE switching and to the bus not being properly biased or terminated. An RS-485 receiver needs a defined differential idle state; without it, line transitions around transmit/receive changes can be mistaken for data. Termination controls reflections, while bias establishes the idle state. Correcting the bias polarity and bus wiring resolved the bench response problem.

  1. Inspect the raw receive buffer for the expected RTU address at byte zero and a CRC at the frame end.
  2. If extra 0x00 bytes appear at the boundaries, check pair polarity, the one-point bias arrangement, bus termination, and DE/RE wiring.
  3. Repeat the identical request and compare the full raw buffer; continue only when the returned frame has no stray prefix or suffix bytes and the bridge generates a normal data response.

Changing the TCP function-code list did not remove the extra bytes. Treat a malformed serial frame as a physical-layer problem first.

What does exception 0xE0 mean after the bridge reaches the slave?

A later client response ended in 83 E0 for a function-03 request. The diagnostic identified 0xE0 as a timeout response. It means the bridge did not obtain a usable RTU response before its wait expired; it does not identify a single cause. Check the serial hop rather than assuming the TCP socket is the fault.

Possible cause from the diagnosis Check
Remote server ID mismatch Compare the configured RTU address with the address in the outgoing request and the slave configuration
Physically broken connection or wrong pair polarity Trace the transceiver-to-slave cable and verify the differential pair at both ends
Another bias point on the bus Locate existing bias resistors and retain only one bias point
RS-485 module on wrong UART pins Verify the transceiver against the configured UART pins and DE/RE pin
Slave response exceeds the RTU wait Measure request-to-response time and compare it with the configured RTU timeout

Separate the bench and field evidence when checking the ID. The bench used RTU address ; the final successful field trace used . A timeout at the field unit calls for checking its address and physical bus, not copying the bench mapping. In the example, MB.setTimeout(2000)Change it only after measuring the slave’s response time and confirming that wiring and addressing are correct.

After each correction, repeat one request and inspect the outgoing RTU address, response bytes, and timeout outcome. A successful TCP connection alone cannot separate these serial-side causes.

How do you verify a complete TCP-to-RTU transaction?

Use a single request whose RTU bytes and TCP response can be correlated. In the final successful trace, the bridge sent 14 04 0F CA 00 01 to RTU address , function 04. It received 14 04 02 00 67 F5 19: address 14, function 04, two data bytes 00 67, followed by the RTU CRC F5 19. The TCP response was 00 05 00 00 00 05 01 04 02 00 67.

  1. Send a request from the TCP client to the IP and port reported by the bridge, using the configured TCP unit ID and a function code registered by the bridge.
  2. In the verbose log, confirm dispatch to the intended RTU address, then confirm the transmitted request and a clean response beginning with that address.
  3. Confirm the RTU response has the expected function, data length, and CRC at the end, without leading or trailing bytes.
  4. Confirm the client receives a normal Modbus TCP data response with the expected unit ID and returned data, rather than an exception or timeout.

For the reported field trace, the successful proof was the clean seven-byte RTU response and the TCP payload carrying 00 67; the preceding bench proof was removal of the stray zeros after correcting the bus wiring and bias.

FAQ

How do I connect a client to the example Modbus bridge?

Use the IP printed by the bridge after Wi-Fi connects and TCP port 502. In the reported setup that address was 192.168.0.189.

How do I map a TCP unit ID to an RTU slave?

Configure the bridge mapping for the actual remote RTU address. The bench mapping was TCP unit 1 to RTU address 10; the later field trace targeted RTU address (20).

How do I fix leading and trailing 0x00 bytes in Modbus RTU replies?

Check RS-485 polarity, DE/RE wiring, one-point biasing, and termination. The reported correction biased A+ to 5 V and B− to ground with 680 Ω resistors at one point, and corrected the pair wiring.

How do I prove the TCP-to-RTU bridge works end to end?

Match one TCP request to the log’s RTU transmission, verify a correctly framed RTU response with the expected address and CRC at the end, then confirm a normal TCP data reply with the expected unit ID and data. The final field trace returned RTU data 00 67 through TCP.

Back to blog