How to control a conveyor belt over Modbus-RTU from Python (Mitsubishi FR-D720S inverter)

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 minimalmodbus and pyserial packages 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 with ls /dev/ttyUSB*)
  • Windows: check Device Manager → Ports (COM & LPT) for the assigned COMx port

:warning: The script below uses termios/tty for 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 the KeyListener class with an equivalent using msvcrt.

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

:warning: 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_RETRIES times and resets the serial buffers between retries — this is expected behaviour, not a wiring fault, as long as CommStats.failed stays 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 termios import error: you’re on Windows — see the note in step 3.

Related


Rewritten and consolidated (Sept 2026) from the original student how-to’s: How to control a conveyor belt using MODBUS-RTU.