Skip to content
Start free evaluation

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 B
headless 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.
  • 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

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.

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.

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.
{"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.

  • tx is the RTU frame sent: address 01, PDU 03 0000 0001, CRC 84 0A (132, 10).
  • rx is the frame received: address 01, PDU 03 02 04B0, CRC BB 30.
  • With timestamp_scope host_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.

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.

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.

  1. Terminal B: {"action":"disconnect"}, then quit. Some commands failed, so the exit code is 1.
  2. Terminal A: {"action":"shutdown"}.
  3. 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.

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.

To connect a PLC or an instrument through a USB-RS232 or RS-485 adapter instead of virtual ports:

  1. 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.
  2. 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.
  3. Find the port name. Plug in the adapter and pick the new name in the serial_ports list. On macOS, use the name that starts with cu., such as /dev/cu.usbserial-....
  4. 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.
  5. 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.