The target ESP32 receives Modbus TCP queries through a W5500, then polls up to 56 Modbus ASCII servers over a UART and selector MUX; the serial connection is 3.3 V TTL full duplex, not RS-485. The practical design is a TCP-server/serial-client bridge, with a shared register bank and a serial transaction coordinated with each MUX selection.
Where does a request travel through the ESP32 bridge?
A PLC sends a TCP query to the ESP32 through the W5500. The TCP server handles that request and exposes data from the register bank. Separately, the serial client polls the field devices and updates that bank. The serial path is UART TX/RX, through the selected MUX channel, to the chosen station; the reply returns over that channel and UART before the application updates the registers.
The TCP and serial sides use different Modbus transports. Modbus TCP requests do not directly become serial ASCII frames through a wire-level pass-through: application logic must associate the TCP-accessible values with the results collected by serial polling. The library approach was attractive for this setup because the application needed distinct TCP-side handling by server ID and register memory accessible from both sides.
| Path | Role | Decision to verify |
|---|---|---|
| PLC to W5500 to ESP32 | TCP server | Confirm the TCP handler reads the intended register bank and returns the required data. |
| ESP32 UART to MUX to station | Serial client | Select the correct MUX channel before transmitting and hold it through the response or timeout. |
| Station reply to ESP32 register bank | Application update | Decode words with the correct data type before the TCP side serves them. |
The evidence describes a future 56-station installation and a successful single-station test at address 16. Treat those as different validation scopes: a working UART exchange does not by itself prove MUX sequencing, shared-register behavior, or a full multi-station scan deadline.
Which serial implementation fits the ASCII requirement?
Three approaches were considered: carry on with the existing RTU-oriented library, bypass its checks for a special RTU server ID, or convert between ASCII wire frames and the library’s internal binary message form. The tested direction was the translator approach. It keeps the established client/server and application framework while isolating ASCII framing and LRC handling at the serial protocol boundary.
| Approach | What it changes | Trade-off in this application |
|---|---|---|
| Port internal protocol code or combine another TCP and serial library | Replace or split existing application components. | Remains a fallback if the library cannot represent required behavior, but gives up the desired shared application framework. |
| Special-case RTU server ID 58 | Interpret the ASCII colon as an RTU address and bypass normal CRC validation through a special callback. | Would reserve ID 58, lose message-content checking in that path, and require application-side construction and decoding. |
| Translate ASCII frames at the serial boundary | Convert received ASCII to the library’s binary message form and outgoing binary messages to ASCII. | Preserves most core handling and the normal request/callback model; it still requires correct ASCII state, LRC, timeout, and framing implementation. |
Use a translator-capable branch or release only after checking its status: the initial ASCII attempt compiled but was explicitly untested, and the receive timeout and LRC defects were corrected through later changes and hardware testing. Do not treat an early experimental revision as equivalent to the tested revision. If the application must send arbitrary plain-text strings rather than standard Modbus ASCII messages, the translator design discussed here is not a fit; that use case motivated the earlier bypass concept.
Why can the ASCII translator reuse the RTU client?
RTU and TCP share a binary-style inner message representation, while ASCII puts that message into printable hexadecimal characters, adds a leading colon, an LRC, and a CRLF terminator. The library proposal therefore translated at send and receive boundaries instead of rewriting the entire request queue and application layer. On reception, the ASCII frame is checked and converted into the binary form; before transmission, a binary message is encoded as ASCII.
This boundary matters because an ASCII frame cannot be validated as an RTU frame: ASCII uses an LRC, whereas RTU uses a CRC. The observed library error text used the phrase Invalid ASCII CRC, but the reported defect was in LRC calculation and validation. Diagnose the algorithm and wire bytes, not just the label.
Protocol mode belongs to the client or server instance, not to each queued message. The tested code exposed useModbusASCII(), useModbusRTU(), and isModbusASCII(). Switching mode with requests already queued can make those requests fail because they are handled under the instance’s current mode. Select the mode before starting traffic, and drain or discard queued work before any runtime mode change.
Which physical and frame details must match?
Verify the electrical connection before diagnosing frame parsing. The tested station interface was direct 3.3 V asynchronous UART TX/RX, with no RS-485 physical layer. The UART was configured with SERIAL_8N1; the baud rate and pin assignments were installation values, not supplied constants. Match the actual station’s serial settings and MUX wiring rather than inferring them from an RS-485 example.
The tested FC03 request addressed station 16 and read two holding registers starting at address 0. The captured wire transaction was:
:100300000002EB\r\n
:10030400040000E5\r\n
Each pair of hexadecimal characters represents one binary byte. The request fields are station , function , starting address , quantity , and LRC . The response contains station , function , byte count , data words and , and LRC .
The target application normally reads 10 holding registers from address 0; the logic-analyzer transaction above reads only two. Confirm both quantities in hardware, since a two-register timing and decoding test does not establish the timing or byte count for a 10-register poll. The station emitted an additional CRLF in one observed response. Record whether the device repeats that terminator and confirm the selected ASCII state machine accepts it without contaminating the next transaction.
How should the ESP32 ASCII client be configured?
Configure the UART and handlers, select ASCII mode, set the desired timeout, and only then start the client. The tested timeout sequence called setTimeout(100) after useModbusASCII(); that ordering produced an observed timeout of about 110 ms before the later timeout corrections. Later branch changes also added an optional timeout argument to the ASCII-mode call, defaulting to one second. Use only the signature present in the branch or release you build.
Serial2.begin(BAUDRATE, SERIAL_8N1, RXPIN, TXPIN);
MB.onDataHandler(&handleData);
MB.onErrorHandler(&handleError);
MB.useModbusASCII();
MB.setTimeout(100);
MB.begin();
Replace the symbolic baud and pin values with the board and station settings. The mode call must precede queued requests. If returning the instance to RTU mode, use useModbusRTU() before adding RTU requests, and check the state with isModbusASCII() where the built revision provides it. Do not mix queued work from the two modes.
- Wire and initialize the UART for the actual TTL serial interface, then confirm TX and RX at the station connector.
- Register data and error handlers before starting the client.
- Select ASCII mode and configure its timeout using the API sequence supported by the code revision.
- Start the client with
begin(), then issue one known FC03 request to a single station. - Check transmitted and received bytes on a logic analyzer before enabling MUX scans or TCP consumers.
How can the LRC and register decoding be checked?
For the tested request, sum the binary data bytes before the LRC: . The two’s-complement LRC is , making the eight-bit sum including the LRC zero. For the response, ; its LRC is . These calculations use the binary byte values represented by the ASCII hex pairs, not a sum of the characters’ ASCII codes.
The first hardware trace exposed an off-by-one LRC calculation: the implementation inverted the sum but omitted the plus-one step for the two’s complement. The transmitted LRC was instead of . After the transmit calculation was corrected, the station replied, but the client rejected the response until the receive-side LRC check was corrected too. Validate both directions independently; a correct query does not prove the response check is correct.
After framing and LRC pass, validate payload interpretation. The first displayed result appeared as zero because the application handler interpreted register data as floating-point values. Changing the handler to int16_t produced register 0 = 4 and register 1 = 0, matching the captured response. A valid frame only establishes that the words arrived; the application must still decode them using the device’s actual register representation and expected data type.
How should MUX selection and request queuing be coordinated?
The MUX makes the serial path a single selected route. Change its address lines for the intended station, transmit that station’s request, and keep the selection unchanged until its response or timeout completes. Do not switch to another channel while a response may still be arriving. For this reason the initial design considered blocking synchronous requests: the application could select the route, wait for the reply, then advance.
The later tested polling loop used nonblocking requests and issued a new request only when no request was open. Its essential policy was to track outstanding work, decrement the count on either data or error, and issue the next request only after completion. With a MUX, preserve the same single-transaction invariant and make route selection part of the serialized transaction sequence. Do not enqueue requests for several stations while changing selector lines independently; a queued request may transmit after the application has moved the MUX.
A larger client queue can hold a burst of requests, and the client can send the next queued request after a response. That can reduce application-side gaps when requests target a fixed route, but does not remove the MUX ordering constraint. If queueing a burst, size capacity for the burst plus any additional requests expected, and do not change a shared MUX route until the transaction using it has completed.
The data and error handlers execute on the client core. Keep them short: copy the received values into application-owned storage and return. If the main loop processes that storage concurrently, protect shared values with a mutex, as required by the described callback execution model.
Which timeout and scheduler settings control a missing reply?
The ASCII receive state machine has an inter-character timeout: if another byte does not arrive within the configured interval, the partial message is faulty. An early revision used a one-second default and did not reset its timeout reference after every successfully read byte. The corrected logic refreshed that reference as bytes arrived. Later code made the ASCII timeout configurable through the mode call, with one second as the default; the tested sequence using setTimeout(100) after switching modes gave about 110 ms before the fix.
| Condition or setting | Observed behavior | Engineering action |
|---|---|---|
| ASCII default in the tested branch | One-second timeout unless changed. | Set a value suitable for the station response interval and absent-device scan policy. |
setTimeout(100) after useModbusASCII()
|
About 110 ms was observed in the test; the extra interval was attributed to timeout-check timing. | Verify the built revision’s timeout behavior against a deliberately disconnected station. |
| No response and a receive loop that does not yield | The ESP32 task watchdog reset the device because the client task occupied a core while waiting. | Use the corrected yielding behavior and run a long disconnected-device test. |
| Reduced task delay | The measured inter-request gap fell from about 8 ms to about 1.4 ms, with short testing showing no reset. | Test under slow and missing station responses; reduced scheduler yield increases watchdog risk. |
A timeout is not the same as a scan deadline. Each absent station can consume a timeout before the client advances; with multiple unavailable stations, timeout accumulation can dominate the scan. Select the timeout from the allowed fault-recovery time and measured worst-case station response, then verify both normal and disconnected cases. Do not remove scheduler yields solely to gain throughput unless the watchdog remains stable under the slowest response path.
Can 56 stations meet a 500 or 1000 ms scan cycle?
The observed timings provide two useful but installation-specific bounds. Initially, the request/response took about 4 ms and the measured idle gap before the next query was about 8 ms. Later, after a delay was moved so it ran less often, the inter-request gap measured about 1.4 ms and the full query/response cycle was mostly 5–6 ms. The second measurement came from short testing of one station; it is not a guaranteed per-device time under a 10-register payload, MUX switching, processing, or timeouts.
| Timing basis | 56-station arithmetic | Interpretation |
|---|---|---|
| Observed 5–6 ms full cycle after delay change | Leaves nominal time within either target, but this was measured on a short single-station test and must be repeated for the real payload and all channels. |
These calculations assume one request per station, no missed responses, no extra MUX-switching or data-processing cost, and that the cited cycle time applies to every station. The target application reads 10 registers; the demonstrated trace read two. Measure the actual 10-register transaction at the configured UART rate and include MUX settling or switching time if present in the hardware. One 100 ms timeout adds approximately that much to a scan for each timed-out station, so a few removed devices can erase the nominal margin.
Queue-full errors appeared when requests were issued faster than responses completed. The control condition is not an arbitrary delay between loop iterations; it is the number of outstanding requests. For a sequential MUX scan, allow one open request and start the next only after data or error completion. For a fixed-route burst, size the queue for the intended burst and keep the handler fast.
What should a hardware acceptance test prove?
Test the exact board, UART, MUX wiring, firmware revision, and station configuration intended for deployment. The logic analyzer should show the expected selector state and wire frame for each station, not merely a successful decoded callback. Keep terminal logging disabled during throughput measurements because the reported maximum-rate test disabled it; then separately enable diagnostics to inspect error handling.
- On one station, capture the FC03 request and response. Check the leading colon, hexadecimal byte sequence, LRC, and CRLF ending against a known transaction.
- Repeat with the application’s 10-register request, checking byte count and payload length as well as LRC.
- Confirm the handler stores the response as the intended integer or other device-specific type, then confirm the TCP server returns the same register values to the PLC.
- Attach multiple MUX channels and verify that each request is sent only while its intended station is selected; check that no late response is assigned to the next station.
- Disconnect one station. Confirm a bounded timeout, error callback, advancement to the next selected station, and no task-watchdog reset.
- Run all 56 stations for longer than a brief bench check. Record total scan period, per-station response times, missed replies, queue errors, and watchdog behavior at both the required normal cycle and with a missing device.
Accept the faster delay configuration only after the long run remains stable with a slow or disconnected station; then verify on the logic analyzer that each request follows the correct MUX selection and that the 56-station scan stays inside the required cycle.
What should engineers check about ESP32 Modbus ASCII polling?
Can I use an RTU client to poll a Modbus ASCII station?
Only with an ASCII-capable implementation that converts frames at the serial boundary. Set useModbusASCII() on the client before queuing requests; ordinary RTU framing and CRC validation do not match ASCII frames.
Does a Modbus ASCII connection require RS-485?
No. The tested station used 3.3 V full-duplex asynchronous UART TX/RX through a MUX, not RS-485. Match the electrical interface and UART settings of the actual device.
Can I set the ASCII timeout to 100 ms?
The test called setTimeout(100) after useModbusASCII() and observed about 110 ms before the timeout fix. Verify the behavior in the built revision and test a disconnected station.
Measure the actual 10-register request, MUX overhead, and timeouts across all stations.
How do I verify a MUXed ASCII station is the one that replied?
Capture the selector lines and UART transaction together on a logic analyzer. Confirm the route remains selected through the reply or timeout, then verify the station address and LRC in the received frame before accepting the decoded registers.