Based on contributions by Wouter.v.Velzen.
Modbus-RTU lets you control a conveyor beltâs frequency inverter directly from a PC (or, in principle, from the Modbus port on a Doosan or Universal Robot control box). This guide connects a conveyorâs Mitsubishi FR-D720S inverter to a PC over RS-485 and drives it from a Python script.
What you need
- Conveyor belt with a Mitsubishi FR-D720S-025SC-EC inverter
- USB-to-RS-485 converter (e.g. the Waveshare USB to RS485)
- An Ethernet cable you can cut and wire yourself
- Python 3, with the
minimalmodbusandpyserialpackages installed (pip install minimalmodbus pyserial)
Background reading: Modbus RTU, RS-485.
Steps
1. Physical setup
The inverter should already be wired so it has power and can drive the motor â see How to wire a conveyor beltâs electrical box (frequency inverter, E-stop, direction control) if you still need to do that.
On the bottom left of the inverter is an RJ-45 Ethernet connector. It doesnât carry Ethernet â it gives you the Modbus-RTU link.
Cut the other end of the Ethernet cable and wire it into the RS-485-to-USB adapter:
| Ethernet wire | USB adapter |
|---|---|
| 3 + 5 | A+ |
| 4 + 6 | Bâ |
| 1 | GND |
(Wire coloring is not always the same for every Ethernet cable â check with a multimeter if unsure.)
2. Configure the inverter for Modbus
Change the following parameters on the inverter to switch it from manual control to Modbus (NET) control:
| Setting | Name | Value | Description |
|---|---|---|---|
| 79 | Operation mode selection | 0 | Allow NET mode on power-up; 0 lets you switch modes |
| 340 | Communication startup mode | 1 | Boot in NET (Modbus) mode automatically |
| 549 | Protocol selection | 1 | Modbus RTU (default is the Mitsubishi protocol) |
| 117 | Station number | 1 | Slave address (change if you have multiple devices on the same bus) |
| 118 | Communication speed | 192 | 19200 baud (other options: 48 = 4800, 96 = 9600, 384 = 38400) |
| 119 | Stop bit / data length | 10 | 8 data bits, 1 stop bit |
| 120 | Parity check | 2 | Even parity (Modbus standard) |
| 122 | Comm. check time interval | 9999 | Disables the communication timeout, so the drive wonât fault on a lack of traffic |
| 123 | Waiting time setting | 9999 | Use the protocol default |
| 124 | CR/LF selection | 0 | Not used by Modbus, leave at 0 |
| 1 | Maximum frequency | (see motor) | Leave as-is unless you know your motorâs rated frequency |
| 2 | Minimum frequency | 0.0 Hz | |
| 7 | Acceleration time | 2.0 s | Increase/decrease to change ramp speed |
| 8 | Deceleration time | 2.0 s | Increase/decrease to change ramp speed |
| 37 | Rotation speed setting | (see motor) | Rated motor speed in rpm â leave as-is unless known |
| 77 | Parameter write selection | 2 | Allows parameter writes while the drive is running (handy while testing) |
After changing these settings, power-cycle the inverter (off for at least 30 seconds) before continuing.
3. Find your serial port
Plug in the USB-to-RS485 adapter and find its port name:
- Linux/macOS: usually
/dev/ttyUSB0(check withls /dev/ttyUSB*) - Windows: check Device Manager â Ports (COM & LPT) for the assigned
COMxport
The script below uses
termios/ttyfor non-blocking keyboard input, which are Unix-only modules. It will run on Linux and macOS as-is; on Windows, either run it inside WSL, or replace theKeyListenerclass with an equivalent usingmsvcrt.
4. Run the control script
This script runs the conveyor forward for a few seconds, then in reverse, repeating in cycles, while tracking estimated revolutions per cycle. Press p to pause/resume and q to quit cleanly. Set PORT to your adapterâs port name and MAX_FREQ_HZ to match parameter Pr.1 on your inverter before running it.
"""
Mitsubishi FR-D720S Modbus RTU control example - looping/repeatability version.
Each cycle:
1. Run forward at SPEED_PCT % of MAX_FREQ_HZ for RUN_TIME_S seconds
2. Stop and ramp down
3. Run reverse at SPEED_PCT % for RUN_TIME_S seconds
4. Stop and ramp down
5. Print per-cycle and cumulative revolution totals
Keyboard control (Linux/macOS terminal):
p pause / resume (motor is stopped while paused)
q quit cleanly (motor stopped, final stats printed)
Ctrl-C also exits cleanly.
Robustness: every Modbus call is wrapped in _with_retry() to survive the
EMI bursts coming out of the inverter's switching stage. See the
Troubleshooting section for shielding / ferrite / cable routing tips.
FR-D700 Modbus holding register map:
40002 Inverter RESET (writing here reboots the drive!)
40009 Run command bit0 STOP, bit1 STF, bit2 STR
40014 Set frequency (RAM) 0.01 Hz
40201 Output frequency 0.01 Hz
40208 Output speed (rpm) needs Pr.37 = rated rpm @ 50 Hz
"""
import sys
import time
import select
import termios
import tty
import minimalmodbus
import serial
# ---------------------------------------------------------------------------
# User configuration
# ---------------------------------------------------------------------------
PORT = "/dev/ttyUSB0"
SLAVE_ADDR = 1
BAUDRATE = 19200
PARITY = serial.PARITY_EVEN
BYTESIZE = 8
STOPBITS = 1
TIMEOUT_S = 0.4
MAX_FREQ_HZ = 50.0 # must match Pr.1 (Maximum frequency) on the inverter
SPEED_PCT = 10
RUN_TIME_S = 5.0
POLL_PERIOD_S = 0.2
INTER_TX_S = 0.02
MAX_RETRIES = 3
MOTOR_POLES = 4
# ---------------------------------------------------------------------------
# Register addresses (minimalmodbus uses 0-based: 4xxxx - 40001)
# ---------------------------------------------------------------------------
REG_RESET = 40002 - 40001
REG_CONTROL = 40009 - 40001
REG_SET_FREQ = 40014 - 40001
REG_OUT_FREQ = 40201 - 40001
REG_OUT_RPM = 40208 - 40001
CTRL_STOP = 0x0001
CTRL_STF = 0x0002
CTRL_STR = 0x0004
# ---------------------------------------------------------------------------
# Non-blocking keyboard input
# ---------------------------------------------------------------------------
class KeyListener:
"""Put stdin in cbreak mode so we can poll for single keystrokes."""
def __enter__(self):
self.fd = sys.stdin.fileno()
self.old = termios.tcgetattr(self.fd)
tty.setcbreak(self.fd)
return self
def __exit__(self, *exc):
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old)
def get_key(self):
"""Return a single character if one is waiting, else ''."""
dr, _, _ = select.select([sys.stdin], [], [], 0)
if dr:
return sys.stdin.read(1)
return ''
# ---------------------------------------------------------------------------
# Robust Modbus wrapper
# ---------------------------------------------------------------------------
class CommStats:
ok = 0
retried = 0
failed = 0
def _with_retry(inv, func, *args, **kwargs):
last_exc = None
for attempt in range(1, MAX_RETRIES + 1):
try:
result = func(*args, **kwargs)
CommStats.ok += 1
time.sleep(INTER_TX_S)
return result
except (minimalmodbus.NoResponseError,
minimalmodbus.InvalidResponseError,
minimalmodbus.LocalEchoError) as e:
last_exc = e
CommStats.retried += 1
try:
inv.serial.reset_input_buffer()
inv.serial.reset_output_buffer()
except Exception:
pass
time.sleep(0.05 * attempt)
CommStats.failed += 1
raise last_exc
# ---------------------------------------------------------------------------
def open_inverter() -> minimalmodbus.Instrument:
inv = minimalmodbus.Instrument(PORT, SLAVE_ADDR, mode=minimalmodbus.MODE_RTU)
inv.serial.baudrate = BAUDRATE
inv.serial.parity = PARITY
inv.serial.bytesize = BYTESIZE
inv.serial.stopbits = STOPBITS
inv.serial.timeout = TIMEOUT_S
inv.clear_buffers_before_each_transaction = True
time.sleep(0.2)
return inv
def write_freq(inv, hz: float) -> None:
value = int(round(hz * 100))
_with_retry(inv, inv.write_register, REG_SET_FREQ, value, functioncode=6)
def set_control(inv, bits: int) -> None:
_with_retry(inv, inv.write_register, REG_CONTROL, bits, functioncode=6)
def read_output_freq(inv) -> float:
raw = _with_retry(inv, inv.read_register, REG_OUT_FREQ, functioncode=3)
return raw / 100.0
def read_output_rpm(inv) -> int:
return _with_retry(inv, inv.read_register, REG_OUT_RPM, functioncode=3)
# ---------------------------------------------------------------------------
# Control flow helpers
# ---------------------------------------------------------------------------
class QuitRequested(Exception):
pass
def handle_keys(keys: KeyListener, inv) -> None:
"""
Check stdin for 'p' (pause) or 'q' (quit).
If paused, block here until 'p' (resume) or 'q' (quit).
Motor is stopped on entry to pause, but the set-frequency is left intact.
"""
k = keys.get_key()
if not k:
return
if k.lower() == 'q':
raise QuitRequested()
if k.lower() == 'p':
print("\n[PAUSED] motor stopped. Press 'p' to resume, 'q' to quit.")
set_control(inv, CTRL_STOP)
while True:
k2 = keys.get_key()
if k2.lower() == 'p':
print("[RESUMED]")
return
if k2.lower() == 'q':
raise QuitRequested()
time.sleep(0.05)
def wait_until_stopped(inv, keys: KeyListener, t_prev_ref: list,
revs_accum_ref: list) -> None:
"""Spin until output frequency is essentially zero."""
while True:
time.sleep(POLL_PERIOD_S)
handle_keys(keys, inv)
t_now = time.monotonic()
dt = t_now - t_prev_ref[0]
t_prev_ref[0] = t_now
try:
f_out = read_output_freq(inv)
except Exception:
continue
revs_accum_ref[0] += (f_out / (MOTOR_POLES / 2.0)) * dt
if f_out < 0.05:
return
def run_direction(inv, keys: KeyListener, direction_bits: int,
label: str, cycle: int) -> float:
target_hz = MAX_FREQ_HZ * SPEED_PCT / 100.0
print(f"\n--- cycle {cycle}: {label} at {SPEED_PCT}% ({target_hz:.2f} Hz) ---")
set_control(inv, direction_bits)
revs = [0.0] # boxed so wait_until_stopped can mutate it
t_start = time.monotonic()
t_prev = [t_start]
while True:
time.sleep(POLL_PERIOD_S)
handle_keys(keys, inv)
t_now = time.monotonic()
dt = t_now - t_prev[0]
t_prev[0] = t_now
try:
f_out = read_output_freq(inv)
except Exception as e:
print(f" [warn] read_output_freq failed: {e}")
continue
revs[0] += (f_out / (MOTOR_POLES / 2.0)) * dt
try:
rpm = read_output_rpm(inv)
except Exception:
rpm = -1
print(f" t={t_now - t_start:5.2f}s f_out={f_out:5.2f} Hz "
f"rpm(reg)={rpm:5d} est. revs={revs[0]:7.2f}")
if t_now - t_start >= RUN_TIME_S:
break
set_control(inv, CTRL_STOP)
print(" -> STOP, waiting for ramp-down")
wait_until_stopped(inv, keys, t_prev, revs)
print(f" cycle {cycle} {label}: revolutions = {revs[0]:.2f}")
return revs[0]
# ---------------------------------------------------------------------------
def main() -> None:
inv = open_inverter()
print(f"Connected to inverter at {PORT}, slave {SLAVE_ADDR}")
print("Keys: p = pause/resume, q = quit")
set_control(inv, CTRL_STOP)
time.sleep(0.2)
target_hz = MAX_FREQ_HZ * SPEED_PCT / 100.0
write_freq(inv, target_hz)
cycle = 0
totals = {"fwd": 0.0, "rev": 0.0}
per_cycle = [] # list of (cycle, fwd, rev, net)
with KeyListener() as keys:
try:
while True:
cycle += 1
fwd = run_direction(inv, keys, CTRL_STF, "FORWARD", cycle)
time.sleep(1.0)
rev = run_direction(inv, keys, CTRL_STR, "REVERSE", cycle)
totals["fwd"] += fwd
totals["rev"] += rev
per_cycle.append((cycle, fwd, rev, fwd - rev))
print(f"\n=== cycle {cycle} summary ===")
print(f" fwd={fwd:7.2f} rev={rev:7.2f} net={fwd - rev:+7.2f}")
print(f" cumulative: fwd={totals['fwd']:8.2f} "
f"rev={totals['rev']:8.2f} "
f"net={totals['fwd'] - totals['rev']:+8.2f}")
print(f" comms: ok={CommStats.ok} retried={CommStats.retried} "
f"failed={CommStats.failed}")
time.sleep(1.0)
except (QuitRequested, KeyboardInterrupt):
print("\n[QUIT] stopping motor...")
finally:
try:
set_control(inv, CTRL_STOP)
except Exception:
pass
# Final report
print("\n=============== FINAL REPORT ===============")
print(f"Cycles completed: {len(per_cycle)}")
print(f"{'cyc':>4} {'fwd':>9} {'rev':>9} {'net':>9}")
for c, f, r, n in per_cycle:
print(f"{c:>4} {f:>9.2f} {r:>9.2f} {n:>+9.2f}")
if per_cycle:
fwds = [f for _, f, _, _ in per_cycle]
revs = [r for _, _, r, _ in per_cycle]
nets = [n for _, _, _, n in per_cycle]
def stats(name, xs):
mean = sum(xs) / len(xs)
spread = max(xs) - min(xs)
# population stddev
var = sum((x - mean) ** 2 for x in xs) / len(xs)
sd = var ** 0.5
print(f" {name}: mean={mean:8.2f} min={min(xs):8.2f} "
f"max={max(xs):8.2f} spread={spread:6.2f} sd={sd:6.3f}")
print("\nRepeatability:")
stats("fwd", fwds)
stats("rev", revs)
stats("net", nets)
print(f"\nComms totals: ok={CommStats.ok} "
f"retried={CommStats.retried} failed={CommStats.failed}")
if __name__ == "__main__":
main()
Register reference
| Register | Function | Units | R/W |
|---|---|---|---|
| 40201 | Output frequency | 0.01 Hz | R |
| 40202 | Output current | 0.01 A | R |
| 40203 | Output voltage | 0.1 V | R |
| 40205 | Frequency setting | 0.01 Hz | R |
| 40208 | Output speed / converter output voltage (DC-bus) | see note | R |
| 40002 | Inverter reset | bool | W |
| 40009 | Run command (STF/STR/STOP bits) | 0 â 2 | W |
| 40014 | Set frequency (RAM) | 0.01 Hz | W |
| 40015 | Set frequency (EEPROM) â limited write cycles, donât poll this one | 0.01 Hz | W |
Check: the source material used register 40208 for both âoutput speed (rpm)â and âconverter DC-bus voltageâ in different places â these canât both be right. The script above reads it as rpm (needs Pr.37 set to the motorâs rated rpm). Verify register 40208âs actual function against the FR-D700 series manual for your firmware before trusting the rpm readout.
Datasheet: FR-D700 series manual
Troubleshooting
- Modbus errors when the motor starts/stops: the inverterâs switching stage generates EMI that can corrupt RS-485 traffic right as the motor ramps up or down. The script retries each Modbus call up to
MAX_RETRIEStimes and resets the serial buffers between retries â this is expected behaviour, not a wiring fault, as long asCommStats.failedstays low. - No response at all: double-check the A+/Bâ wiring, that parameter 549 is set to Modbus RTU (not the Mitsubishi protocol), and that the baud rate/parity in the script matches parameters 118â120.
- Script exits with a
termiosimport error: youâre on Windows â see the note in step 3.
Related
- How to wire a conveyor beltâs electrical box (frequency inverter, E-stop, direction control) â wiring the inverterâs power and motor connections
- How to calculate motor power, RPM and torque for a conveyor belt â sizing the motor/inverter for your conveyor
Rewritten and consolidated (Sept 2026) from the original student how-toâs: How to control a conveyor belt using MODBUS-RTU.


