RTU and ASCII communication
This tutorial uses a pair of PDComm virtual serial ports instead of a real cable. The two ports behave as if joined by a null-modem cable. You attach the headless simulator as an RTU server to one port and the C++ example console as an RTU client to the other. The last section explains how to do the same with a real PLC and a USB-RS232 adapter.
terminal A terminal V terminal Bheadless RTU server ── modbus-a ══ pdcomm-vserial pair ══ modbus-b ── C++ RTU client- Connect an RTU server and client with a serial configuration (
transport,port,baud,parity,stop_bits). - Try serial-only functions, broadcasts and an address nobody answers.
- Look at the actual RTU and ASCII bytes with
trace. - Corrupt one character and watch the CRC error appear as a server diagnostics counter and a client timeout.
What you need
Section titled “What you need”- The PDComm Modbus SDK and the C++ console you built in the quickstart
- The PDComm virtual serial tool
pdcomm-vserial. Tools covers installing it and other setups. - Three terminals, all started in the same working folder
1. Create a virtual port pair
Section titled “1. Create a virtual port pair”In terminal V, run:
pdcomm-vserial pair --link-a "$PWD/modbus-a" --link-b "$PWD/modbus-b"{"ports":[{"backend":"pty","link":".../modbus-a","node":"a","path":"/dev/ttys011"},{"backend":"pty","link":".../modbus-b","node":"b","path":"/dev/ttys016"}],"ready":true}When ready is true, the working folder contains the links modbus-a and modbus-b, which point to pseudo terminals created by the operating system (path). pair uses its standard input as a command channel, so keep terminal V open; closing its input removes the virtual ports.
2. Open the RTU server
Section titled “2. Open the RTU server”In terminal A, start the headless simulator and open an RTU server on modbus-a. Replace the port path with the absolute path of your working folder.
"$MODBUS_SDK/bin/pdcomm-modbus-headless"{"action":"configure","config":{"transport":"rtu","role":"server","address":"127.0.0.1:0","port":"/absolute/path/modbus-a","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"connect"}{"action":"data_set","table":"holding_registers","address":0,"values":[1200,1185,230,50]}When connect returns "connected":true, the server has opened the port and waits for requests. A serial configuration still needs address for the format, but doesn’t use it.
3. Send requests from the RTU client
Section titled “3. Send requests from the RTU client”In terminal B, start the C++ console and list the serial ports the operating system knows:
./build/modbus-tutorial/pdcomm-modbus-tutorial{"action":"serial_ports"}serial_ports returns real devices such as USB adapters (/dev/cu.usbserial-... on macOS, COM3 on Windows). The virtual pair’s links aren’t listed, so enter the path yourself:
{"action":"configure","config":{"transport":"rtu","role":"client","address":"127.0.0.1:0","port":"/absolute/path/modbus-b","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"connect"}{"action":"request","pdu":"0300000004"}{"action":"request","pdu":"0600010064"}{"action":"request","pdu":"0800001234"}{"action":"request","pdu":"11"}{"ok":true,"result":{"response":[3,8,4,176,4,161,0,230,0,50],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[6,0,1,0,100],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[8,0,0,18,52],"source":"wire","status":"success"}}{"ok":true,"result":{"response":[17,2,1,255],"source":"wire","status":"success"}}- You read the 1200, 1185, 230 and 50 the server set (
0x04B0,0x04A1,0x00E6,0x0032). - 08 (Diagnostics) and 17 (Report Server ID) are serial-line-only functions that you can’t use over TCP.
4. Look at the bytes
Section titled “4. Look at the bytes”{"action":"trace_clear"}{"action":"request","pdu":"0300000001"}{"action":"trace"}{"ok":true,"result":{"entries":[ {"bytes":[1,3,0,0,0,1,132,10],"direction":"tx",...,"timestamp_scope":"host_io_batch","transport":"rtu",...}, {"bytes":[1,3,2,4,176,187,48],"direction":"rx",...,"timestamp_scope":"host_io_batch","transport":"rtu",...}, {"request":[3,0,0,0,1],"response":[3,2,4,176],"sequence":15,"source":"wire","status":"success","unit":1}]}}The lines are wrapped for reading; the real output is one line.
txis the RTU frame sent: address01, PDU03 0000 0001, CRC84 0A(132, 10).rxis the frame received: address01, PDU03 02 04B0, CRCBB 30.- With
timestamp_scopehost_io_batch, the time is when the PC finished a read, not a UART bit time. Don’t use it to measure t1.5 or t3.5.
5. Send a broadcast and address a missing unit
Section titled “5. Send a broadcast and address a missing unit”{"action":"request","unit":0,"pdu":"0600020007"}{"action":"request","pdu":"0300020001"}{"action":"request","unit":0,"pdu":"0300000001"}{"action":"request","unit":2,"pdu":"0300000001"}| Request | Result | Notes |
|---|---|---|
| Unit 0, write 7 to register 2 | {"response":null,"source":"wire","status":"no_response"} |
A broadcast gets no response. The client waits turnaround_ms (100 ms by default), then finishes. |
| Read register 2 | [3,2,0,7] |
The broadcast was applied. |
| Read from unit 0 | Command error: {"error":"InvalidConfig","ok":false} |
Only writes (05, 06, 15, 16, 21, 22) can be broadcast. |
| Read from unit 2 | Command error: {"error":"Timeout","ok":false} after about 1 s |
No server has address 2, so nobody answers. |
6. Corrupt one character
Section titled “6. Corrupt one character”Enter this command in terminal V. It flips the lowest bit of the next character node a (the server side) receives:
{"command":"inject","node":"a","fault":{"xor":1}}After {"ok":true}, send a request in terminal B:
{"action":"request","pdu":"0300000001"}The client fails with Timeout: the server dropped the request because of the wrong CRC and didn’t answer. Check the diagnostics counters in terminal A:
{"action":"diagnostics"}{"ok":true,"result":{"counters":{"bus_messages":8,"busy":0,"communication_errors":1,...},...}}communication_errors is 1. Send the request again and it succeeds. CRC errors caused by noise or a parity mismatch on a real line show up the same way: as a timeout on the client and as a communication error count on the server.
7. Switch to ASCII
Section titled “7. Switch to ASCII”Disconnect both sides, change only transport to ascii, and connect again, starting with the server in terminal A:
{"action":"disconnect"}{"action":"configure","config":{"transport":"ascii","role":"server","address":"127.0.0.1:0","port":"/absolute/path/modbus-a","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"connect"}Then the client in terminal B:
{"action":"disconnect"}{"action":"configure","config":{"transport":"ascii","role":"client","address":"127.0.0.1:0","port":"/absolute/path/modbus-b","unit":1,"baud":19200,"parity":"even","stop_bits":1,"timeout_ms":1000}}{"action":"connect"}{"action":"trace_clear"}{"action":"request","pdu":"0300000002"}{"action":"trace"}{"ok":true,"result":{"response":[3,4,4,176,0,100],"source":"wire","status":"success"}}The tx bytes [58,48,49,48,51,...,70,65,13,10] in trace are the characters :010300000002FA plus CR LF. The response can arrive as two rx entries: that’s how the operating system batched the characters, and PDComm assembles everything from : to LF into one frame. ASCII mode uses 7 data bits automatically.
8. Clean up
Section titled “8. Clean up”- Terminal B:
{"action":"disconnect"}, thenquit. Some commands failed, so the exit code is 1. - Terminal A:
{"action":"shutdown"}. - Terminal V:
{"command":"stop"}. The virtual ports and links disappear.
connect fails when another program already has the port open. Never open the same port for a server and a client at once.
Common errors
Section titled “Common errors”| Output | Cause and fix |
|---|---|
Invalid endpoint configuration |
parity is none with stop_bits 1. Use 2 stop bits or a parity. A unit outside 1–247 gives the same error. |
connect fails with Io(Other, "No such file or directory") |
The port path doesn’t exist. For virtual ports, check that terminal V is still running and that the path is absolute. |
connect fails with InvalidConfig |
port is empty. |
| Every request times out | Check that both sides use the same transport, baud, parity, stop_bits and unit address, and that the server is connected. |
Serial communication problems explains the causes in detail.
Connect a real device
Section titled “Connect a real device”To connect a PLC or an instrument through a USB-RS232 or RS-485 adapter instead of virtual ports:
- Check the device settings. Find the transmission mode (RTU or ASCII), speed, parity, stop bits and unit address in the device manual. If the device uses 8N1, change it to 8E1 (recommended) or 8N2, write the settings to the device and restart its communication module if needed.
- Wire it. On RS-232, transmit and receive must cross exactly once along the whole path (2↔3, 3↔2, 5↔5 null modem between two 9-pin DTE connectors). On RS-485, connect D0/D1 polarity, termination and the common line as the device instructions say. An RS-232 adapter can’t connect directly to an RS-485 port.
- Find the port name. Plug in the adapter and pick the new name in the
serial_portslist. On macOS, use the name that starts withcu., such as/dev/cu.usbserial-.... - Test with reads first. Send the first requests with function 03 or 04 to read-only addresses from the device manual. Write only to addresses the manual says are safe, and restore their original values afterwards.
- If nothing answers, check speed, parity, unit address and transmission mode again, then follow Serial communication problems.
Configure serial ports covers details such as USB adapter latency and parity handling of the macOS FTDI driver.