pythonraspberry-pimodbusrs485raspberry-pi4

How to implement Modbus on Raspberry Pi?


I'm currently engaged in a project where I'm trying to implement Modbus with the Raspberry Pi 4 as master and controlling a number of actuators as slaves. For this purpose I've purchased a special shield for my Pi. I've run a demo test program that confirm that the Pi works with its new shield, but hit a wall afterwards.

Shield user manual - inside the user manual folder.

Master:

## To install dependencies:
## sudo pip3 install modbus-tk
##################################################################################################
import serial
import fcntl
import os
import struct
import termios
import array
#import modbus lib
import modbus_tk
import modbus_tk.defines as cst
import modbus_tk.modbus as modbus
#import modbus_tk.modbus_rtu as modbus_rtu
from modbus_tk import modbus_rtu

# RS485 ioctls define
TIOCGRS485 = 0x542E
TIOCSRS485 = 0x542F
SER_RS485_ENABLED = 0b00000001
SER_RS485_RTS_ON_SEND = 0b00000010
SER_RS485_RTS_AFTER_SEND = 0b00000100
SER_RS485_RX_DURING_TX = 0b00010000
# rs 485 port
ser1 = serial.Serial("/dev/ttySC0",9600)    
ser2 = serial.Serial("/dev/ttySC1",9600)

def rs485_enable():
    buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
    #enable 485 chanel 1
    fcntl.ioctl(ser1, TIOCGRS485, buf)
    buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
    buf[1]  = 0
    buf[2]  = 0
    fcntl.ioctl(ser1, TIOCSRS485, buf)

    #enable 485 chanel 2
    fcntl.ioctl(ser2, TIOCGRS485, buf)
    buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
    buf[1]  = 0
    buf[2]  = 0
    fcntl.ioctl(ser2, TIOCSRS485, buf)
#end of rs485_enable():


if __name__ == '__main__':

    logger = modbus_tk.utils.create_logger("console")

    rs485_enable()

    #set modbus master
    master = modbus_rtu.RtuMaster(
           serial.Serial(port= '/dev/ttySC0',
           baudrate=9600,
           bytesize=8,
           parity='N',
           stopbits=1,
           xonxoff=0)
       )

    master.set_timeout(5.0)
    master.set_verbose(True)
    logger.info("connected")

    logger.info(master.execute(1, cst.READ_HOLDING_REGISTERS, 0, 4))

    #send some queries
    #logger.info(master.execute(1, cst.READ_COILS, 0, 10))
    #logger.info(master.execute(1, cst.READ_DISCRETE_INPUTS, 0, 8))
    #logger.info(master.execute(1, cst.READ_INPUT_REGISTERS, 100, 3))
    #logger.info(master.execute(1, cst.READ_HOLDING_REGISTERS, 100, 12))
    #logger.info(master.execute(1, cst.WRITE_SINGLE_COIL, 7, output_value=1))
    #logger.info(master.execute(1, cst.WRITE_SINGLE_REGISTER, 100, output_value=54))
    #logger.info(master.execute(1, cst.WRITE_MULTIPLE_COILS, 0, output_value=[1, 1, 0, 1, 1, 0, 1, 1]))
    #logger.info(master.execute(1, cst.WRITE_MULTIPLE_REGISTERS, 100, output_value=xrange(12)))

#end of if __name__ == '__main__': 

Slave:

import sys

import serial
import fcntl
import os
import struct
import termios
import array
import time

import modbus_tk
import modbus_tk.defines as cst
import modbus_tk.modbus as modbus
#import modbus_tk.modbus_rtu as modbus_rtu
from modbus_tk import modbus_rtu
# RS485 ioctls
TIOCGRS485 = 0x542E
TIOCSRS485 = 0x542F
SER_RS485_ENABLED = 0b00000001
SER_RS485_RTS_ON_SEND = 0b00000010
SER_RS485_RTS_AFTER_SEND = 0b00000100
SER_RS485_RX_DURING_TX = 0b00010000
# rs 485 port
ser1 = serial.Serial("/dev/ttySC0",9600)    
ser2 = serial.Serial("/dev/ttySC1",9600)

def rs485_enable():
    buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
    #enable 485 chanel 1
    fcntl.ioctl(ser1, TIOCGRS485, buf)
    buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
    buf[1]  = 0
    buf[2]  = 0
    fcntl.ioctl(ser1, TIOCSRS485, buf)

    #enable 485 chanel 2
    fcntl.ioctl(ser2, TIOCGRS485, buf)
    buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
    buf[1]  = 0
    buf[2]  = 0
    fcntl.ioctl(ser2, TIOCSRS485, buf)
#end of def rs485_enable():


if __name__ == '__main__':

    logger = modbus_tk.utils.create_logger("console")

    rs485_enable()

    logger = modbus_tk.utils.create_logger(name="console", record_format="%(message)s")

    #Create the server
    server = modbus_rtu.RtuServer(serial.Serial('/dev/ttySC1'))

    try:
        logger.info("running...")
        logger.info("enter 'quit' for closing the server")

        server.start()

        slave_1 = server.add_slave(1)
        slave_1.add_block('0', cst.HOLDING_REGISTERS, 0, 100)
        while True:
            cmd = sys.stdin.readline()
            args = cmd.split(' ')

            if cmd.find('quit') == 0:
                sys.stdout.write('bye-bye\r\n')
                break

            elif args[0] == 'add_slave':
                slave_id = int(args[1])
                server.add_slave(slave_id)
                sys.stdout.write('done: slave %d added\r\n' % (slave_id))

            elif args[0] == 'add_block':
                slave_id = int(args[1])
                name = args[2]
                block_type = int(args[3])
                starting_address = int(args[4])
                length = int(args[5])
                slave = server.get_slave(slave_id)
                slave.add_block(name, block_type, starting_address, length)
                sys.stdout.write('done: block %s added\r\n' % (name))

            elif args[0] == 'set_values':
                slave_id = int(args[1])
                name = args[2]
                address = int(args[3])
                values = []
                for val in args[4:]:
                    values.append(int(val))
                slave = server.get_slave(slave_id)
                slave.set_values(name, address, values)
                values = slave.get_values(name, address, len(values))
                sys.stdout.write('done: values written: %s\r\n' % (str(values)))

            elif args[0] == 'get_values':
                slave_id = int(args[1])
                name = args[2]
                address = int(args[3])
                length = int(args[4])
                slave = server.get_slave(slave_id)
                values = slave.get_values(name, address, length)
                sys.stdout.write('done: values read: %s\r\n' % (str(values)))

            else:
                sys.stdout.write("unknown command %s\r\n" % (args[0]))
    finally:
        server.stop()

The actuators I'm planning on using are Linak LA36. I believe these are the functions I'll be using:

enter image description here

From page 21-22 of this document.

The wall is rather simply getting started with Modbus. I've studied the actuator's technical documents to determine what to send, but I'm lost in regards to writing the program. I had hoped to perhaps be able to modify the demo program to suit my needs, but was unable to understand the code uses there.

Searching on the internet, I attempted to find a tutorial or description of what the different variables and functions did, to better understand, but couldn't find anything like that. I did find where the demo code originated, but wasn't able to find/understand anything there that could help me.

I've seen that there are programs that should enable Modbus with the Raspberry Pi (like PyModbus), but I'm uncertain whether my circumstances are different, having a special shield, and whether such programs would work for my setup?

So, finally, I'm here hoping for some help. Advice, instructions, examples, at this point anything is welcomed to perhaps get me further. It could also be that using the demo code as a foundation is a mistake, and someone could point me in a different direction?

I'm quite willing to try different things, and any help is appreciated.

Thank you in advance.

Update:

I've since then looked around for other options and stumbled across minimalmodbus which I'm trying to use. With the RS485 shield still in it's demo configuration...

RS485 shield

...I've been trying to execute some code I found from minimalmodbus in a Python interpretor:

>>> import minimalmodbus
>>> instr = minimalmodbus.Instrument('/dev/ttySC0', 1)
>>> instr
minimalmodbus.Instrument<id=0xb7437b2c, address=1, close_port_after_each_call=False, debug=False, serial=Serial<id=0xb7437b6c, open=True>(port='/dev/ttySC0', baudrate=19200, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False)>
>>> instr.read_register(24, 1)
5.0
>>> instr.write_register(24, 450, 1)
>>> instr.read_register(24, 1)

Where I've changed '/dev/ttyUSB0' (in the original code) to '/dev/ttySC0'. Now I'm stuck on:

>>> instr
    minimalmodbus.Instrument<id=0xb7437b2c, address=1, close_port_after_each_call=False, debug=False, serial=Serial<id=0xb7437b6c, open=True>(port='/dev/ttySC0', baudrate=19200, bytesize=8, parity='N', stopbits=1, timeout=0.05, xonxoff=False, rtscts=False, dsrdtr=False)>

Which gives SyntaxError: invalid syntax highlighting minimalmodbus.


Solution

  • I think you need to start with simple things and build on top of them. In your comments, you state you want to use minimalmodbus, that's fine but let's start with the demo code and try to make work with your actuators first. Later you can come back to other libraries like minimalmodbus or pymodbus.

    Before we go into the code I think you should understand what Modbus is. Essentially Modbus uses a serial port (it could be over RS485 or over a more conventional RS232 or over TTL levels, but that's just the physical layer, the electrical levels used to convey the information; you already have you RS485 port on your hat and your actuator works over RS485 too, so nothing to be concerned about on that front, as long as you have wired the bus correctly, A to A and B to B).

    So, what else needs Modbus other than a serial port? Modbus works in a master-slave configuration. That means there is only one Master (your Raspberry Pi computer in your case) and one or many slaves (your actuators). According to what we said in the previous paragraph your Modbus runs on a two-wire (with RS485 levels) bus. In this configuration, contrary to the more general RS232 standard where you have three wires: RX, TX, and GND, you cannot have a full-duplex communication (only the master or one of all the slaves can talk onto the bus, while all other devices listen, similarly to a walkie-talkie radio link). To stretch the analogy a bit further, the same way you need a PTT (push-to-talk) button on your WT radio, you need a signal for either the master or any of the slaves on your Modbus to PTT when they want to talk. Some RS485 transceivers have this function implemented by hardware; on your hat, without examining the circuit in detail and looking at the demo code, it seems the direction control on the bus is implemented in software with the rs485_enable() function.. EDIT: Looking at the hardware in detail I have to correct myself: your RS485 hat is actually doing the direction control with via hardware with its SPI to double-UART SC16IS752. This chip was designed to be backward compatible with older UARTS that used the flow control RTS signal as direction control (the PTT function we mentioned before). That's why you need the rs485_enable().

    Enough theory, now with the practical part. There is one important thing you seem to have missed. On the manual you linked, on page 21 you have this paragraph:

    Before integration into a MODBUS system a few parameters of the actuator have to be checked and eventually changed. This preparation is done by use of the BusLink PC tool (the tool is described in details later) and guarantees that the actuator is able to execute basic functionality. Further fine-tuning may be required to fulfill system-or application requirements.

    Then, if you go back to the table on page 12 you'll see that what they call the addressing (which is generally referred to as slave ID) defaults to 247 (un-assigned). So the first thing you need to do is use this Buslink PC tool to set the address on the actuators to any number from 1 to 246 (if you plan to connect more than one actuator on your bus you'll have to set a different number for each actuator). See page 28 for more details.

    After you successfully finish that configuration you should be able to run the demo master code. For instance if you want to move the actuator 10 mm you can try:

    ## To install dependencies:
    ## sudo pip3 install modbus-tk
    ##################################################################################################
    import serial
    import fcntl
    import os
    import struct
    import termios
    import array
    #import modbus lib
    import modbus_tk
    import modbus_tk.defines as cst
    import modbus_tk.modbus as modbus
    #import modbus_tk.modbus_rtu as modbus_rtu
    from modbus_tk import modbus_rtu
    
    # RS485 ioctls define
    TIOCGRS485 = 0x542E
    TIOCSRS485 = 0x542F
    SER_RS485_ENABLED = 0b00000001
    SER_RS485_RTS_ON_SEND = 0b00000010
    SER_RS485_RTS_AFTER_SEND = 0b00000100
    SER_RS485_RX_DURING_TX = 0b00010000
    # rs 485 port
    ser1 = serial.Serial("/dev/ttySC0",19200)    
    ser2 = serial.Serial("/dev/ttySC1",9600)
    
    def rs485_enable():
        buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
        #enable 485 chanel 1
        fcntl.ioctl(ser1, TIOCGRS485, buf)
        buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
        buf[1]  = 0
        buf[2]  = 0
        fcntl.ioctl(ser1, TIOCSRS485, buf)
    
        #enable 485 chanel 2
        fcntl.ioctl(ser2, TIOCGRS485, buf)
        buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
        buf[1]  = 0
        buf[2]  = 0
        fcntl.ioctl(ser2, TIOCSRS485, buf)
    #end of rs485_enable():
    
    
    if __name__ == '__main__':
    
        logger = modbus_tk.utils.create_logger("console")
    
        rs485_enable()
    
        #set modbus master
        master = modbus_rtu.RtuMaster(
               serial.Serial(port= '/dev/ttySC0',
               baudrate=9600,
               bytesize=8,
               parity='N',
               stopbits=1,
               xonxoff=0)
           )
    
        master.set_timeout(5.0)
        master.set_verbose(True)
        logger.info("connected")
    
        logger.info(master.execute(1, cst.WRITE_SINGLE_REGISTER, 1, output_value=100))  #Write target position 10mm (1/10mm*100)
        logger.info(master.execute(1, cst.WRITE_SINGLE_REGISTER, 2, output_value=1))  #Move actuator
    

    Note that I've only changed the last two lines of the demo code. The first 1, right before cst.WRITE_SINGLE_REGISTER must be the same address slave that you set up with the BusLink PC Tool. The number right after (1 in the first line and 2 in the second) is the register number you need to write according to page 22 of the manual. Lastly, output_value is the value you need to write on each register. On register number 1 you need to write the target position where you want to move the actuator from its reference (measured in multiples of 0.1mm) and just a 1 on the second (see again table on page 22 of the manual, steps 2 and 3).

    You can complete the sequence with step 4 by reading input register 3 and 5. Note that to read input registers the function code is cst.READ_INPUT_REGISTERS.

    Give it a go and see if you can make it work. After you do it we can take a look at minimalmodbus.

    EDIT: Knowing a bit better how your hardware works (see edit above), it's now clear you can use any Modbus library you like, you just have to keep the demo code above #end of rs485_enable(): and call rs485_enable() somewhere before you start sending data.

    For minimalmodbus you can try something like this:

    import serial
    import fcntl
    import os
    import struct
    import termios
    import array
    #Remove modbus-tk imports and add minimalmodbus
    import minimalmodbus
    
    
    # only /dev/ttySC0 will be used
    # RS485 ioctls define
    TIOCGRS485 = 0x542E
    TIOCSRS485 = 0x542F
    SER_RS485_ENABLED = 0b00000001
    SER_RS485_RTS_ON_SEND = 0b00000010
    SER_RS485_RTS_AFTER_SEND = 0b00000100
    SER_RS485_RX_DURING_TX = 0b00010000
    # rs 485 port
    ser1 = serial.Serial("/dev/ttySC0",19200)    
    
    
    def rs485_enable():
        buf = array.array('i', [0] * 8) # flags, delaytx, delayrx, padding
        #enable 485 chanel 1
        fcntl.ioctl(ser1, TIOCGRS485, buf)
        buf[0] |=  SER_RS485_ENABLED|SER_RS485_RTS_AFTER_SEND
        buf[1]  = 0
        buf[2]  = 0
        fcntl.ioctl(ser1, TIOCSRS485, buf)
    
    #end of rs485_enable():
    
    
    if __name__ == '__main__':
    
        actuator = minimalmodbus.Instrument('/dev/ttySC0', 1) # port name, slave address (in decimal), change according to actuator address
    
        rs485_enable()   #you need to keep this for your hat to work
    
        #minimalmodbus setup
        actuator.serial.port               # this is the serial port name
        actuator.serial.baudrate = 19200   # Baud rate
        actuator.serial.bytesize = 8
        actuator.serial.parity   = serial.PARITY_NONE
        actuator.serial.stopbits = 1
        actuator.serial.timeout  = 0.05   # seconds
    
        actuator.address     # this is the slave (actuator) address number
        actuator.mode = minimalmodbus.MODE_RTU   # rtu mode
    
        #write registers
        actuator.write_register(1, 100)  #write target distance to move
        actuator.write_register(2, 1)    #Move!