Modbus on Raspberry PLCs with Python
Implementation for Raspberry based PLCs (Python)
Installation
1. librpiplc implements useful functions to use Raspberry based Industrial Shields PLCs for industrial environments. Follow the installation steps from here.
2. python3-librpiplc provides a Python wrapper for the librpiplc C library, enabling Python applications to interface with the GPIOs of Raspberry Pi based Industrial Shields PLCs. Follow the installation steps from here.
3. pymodbus is a Python library which offers client and server implementation for Modbus protocol. Check the documentation here. Install it executing the command: pip install pymodbus.
Slave
The following examples are Modbus slave implementations with both RTU and TCP:
Implementation Guide
1. Device configuration: The code begins by defining constants such as the Modbus slave address and the version and model of the RPI PLC. It also includes a configuration dictionary specifying the number of digital outputs, analog outputs, analog inputs, and digital inputs for the specific PLC model being used, providing essential parameters for device setup and interaction. This example uses version V4 of RPI PLC 21+, change these parameters according to your PLC.
# Define the slave address SLAVE_ADDR = 1 # Define your version and model VERSION = "RPIPLC_V4" MODEL = "RPIPLC_21" # Define the configuration for the PLC models (Define your PLC pinouts here) PLC_CONFIG = { 'RPIPLC_21': {'Q': 5, 'A': 3, 'AI': 6, 'I': 7} }
# Define the slave address SLAVE_ADDR = 1 # Define your version and model VERSION = "RPIPLC_V4" MODEL = "RPIPLC_21" # Define the configuration for the PLC models (Define your PLC pinouts here) PLC_CONFIG = { 'RPIPLC_21': {'Q': 5, 'A': 3, 'AI': 6, 'I': 7} }
2. Initializing the PLC: An initialization function is defined to configure the PLC hardware and set the pin modes according to the specified model. This step ensures that the PLC is properly configured and ready to interact with the Modbus server, establishing a foundational setup for subsequent communication tasks.
def init(): # Initialize the RPI PLC rpiplc.init(VERSION, MODEL) # Set pin modes for digital and analog inputs and outputs for i in range(PLC_CONFIG[MODEL]['Q']): pin_name = "Q0.{}".format(i) rpiplc.pin_mode(pin_name, rpiplc.OUTPUT) for i in range(PLC_CONFIG[MODEL]['A']): pin_name = "A0.{}".format(i + 5) rpiplc.pin_mode(pin_name, rpiplc.OUTPUT) for i in range(PLC_CONFIG[MODEL]['I']): pin_name = "I0.{}".format(i) rpiplc.pin_mode(pin_name, rpiplc.INPUT) for i in range(PLC_CONFIG[MODEL]['AI']): pin_name = "I0.{}".format(i + 7) rpiplc.pin_mode(pin_name, rpiplc.INPUT)
def init(): # Initialize the RPI PLC rpiplc.init(VERSION, MODEL) # Set pin modes for digital and analog inputs and outputs for i in range(PLC_CONFIG[MODEL]['Q']): pin_name = "Q0.{}".format(i) rpiplc.pin_mode(pin_name, rpiplc.OUTPUT) for i in range(PLC_CONFIG[MODEL]['A']): pin_name = "A0.{}".format(i + 5) rpiplc.pin_mode(pin_name, rpiplc.OUTPUT) for i in range(PLC_CONFIG[MODEL]['I']): pin_name = "I0.{}".format(i) rpiplc.pin_mode(pin_name, rpiplc.INPUT) for i in range(PLC_CONFIG[MODEL]['AI']): pin_name = "I0.{}".format(i + 7) rpiplc.pin_mode(pin_name, rpiplc.INPUT)
3. Running the server: The script sets up a Modbus server by defining data blocks for discrete inputs, coils, holding registers, and input registers. It creates a Modbus slave context with these data blocks and starts the Modbus TCP server in a separate thread. This step enables the server to listen for incoming Modbus requests and respond accordingly, facilitating communication with external Modbus clients.
def run_server(): # Define data blocks with 100 registers di = ModbusSequentialDataBlock(0, [0] * 100) co = ModbusSequentialDataBlock(0, [0] * 100) hr = ModbusSequentialDataBlock(0, [0] * 100) ir = ModbusSequentialDataBlock(0, [0] * 100) # Create a Modbus slave context store = ModbusSlaveContext(di=di, co=co, hr=hr, ir=ir) context = ModbusServerContext(slaves={SLAVE_ADDR: store}, single=False) # Start the Modbus TCP server in a separate thread server_thread = Thread(target=start_modbus_server, args=(context,), daemon=True) server_thread.start() # Main loop to update PLC data based on Modbus register values try: while True: update_plc_datastore(store) time.sleep(0.1) except KeyboardInterrupt: print("Server stopped")
def run_server(): # Define data blocks with 100 registers di = ModbusSequentialDataBlock(0, [0] * 100) co = ModbusSequentialDataBlock(0, [0] * 100) hr = ModbusSequentialDataBlock(0, [0] * 100) ir = ModbusSequentialDataBlock(0, [0] * 100) # Create a Modbus slave context store = ModbusSlaveContext(di=di, co=co, hr=hr, ir=ir) context = ModbusServerContext(slaves={SLAVE_ADDR: store}, single=False) # Start the Modbus TCP server in a separate thread server_thread = Thread(target=start_modbus_server, args=(context,), daemon=True) server_thread.start() # Main loop to update PLC data based on Modbus register values try: while True: update_plc_datastore(store) time.sleep(0.1) except KeyboardInterrupt: print("Server stopped")
4. Start Modbus Server: The start_modbus_server() function is responsible for initializing and starting the Modbus server. Depending on the protocol (TCP or RTU), this function sets up the server with the appropriate communication parameters and starts it, allowing it to handle Modbus requests from clients. The context parameter represents the Modbus server context, containing the data blocks for discrete inputs, coils, holding registers, and input registers. It manages and responds to Modbus requests.
- For Modbus TCP, the server needs to be configured with the address, which includes the IP address and the port used:
# Start Modbus Server TCP def start_modbus_server(context): StartTcpServer(context=context, address=("10.10.10.20", 502))
# Start Modbus Server TCP def start_modbus_server(context): StartTcpServer(context=context, address=("10.10.10.20", 502))
- For Modbus RTU, the server needs to be configured with the serial communication parameters, including the serial port, baud rate, data bits, parity, and stop bits:
# Start Modbus Server RTU def start_modbus_server(context): StartSerialServer(context=context, port='/dev/ttySC0', baudrate=38400, bytesize=8, parity='E', stopbits=1, )
# Start Modbus Server RTU def start_modbus_server(context): StartSerialServer(context=context, port='/dev/ttySC0', baudrate=38400, bytesize=8, parity='E', stopbits=1, )
5. Updating PLC datastore: An update function is implemented to synchronize the PLC's state with the Modbus register values. This function reads the values of coils and holding registers from the Modbus store and writes them to the corresponding PLC digital and analog outputs, respectively. It also reads the values of digital and analog inputs from the PLC and updates the Modbus store accordingly, ensuring bidirectional communication between the PLC and Modbus clients.
def update_plc_datastore(store): # Update digital outputs coils = store.getValues(1, 0, PLC_CONFIG[MODEL]['Q']) for i in range(PLC_CONFIG[MODEL]['Q']): pin_name = "Q0.{}".format(i) rpiplc.digital_write(pin_name, coils[i]) # Update analog outputs registers = store.getValues(3, 0, PLC_CONFIG[MODEL]['A']) for i in range(PLC_CONFIG[MODEL]['A']): pin_name = "A0.{}".format(i + 5) rpiplc.analog_write(pin_name, registers[i]) # Update digital inputs di_values = [] for i in range(PLC_CONFIG[MODEL]['I']): pin_name = "I0.{}".format(i) value = rpiplc.digital_read(pin_name) di_values.append(value) store.setValues(2, 0, di_values) # Update analog inputs ai_values = [] for i in range(PLC_CONFIG[MODEL]['AI']): pin_name = "I0.{}".format(i + 7) value = rpiplc.analog_read(pin_name) ai_values.append(value) store.setValues(4, 0, ai_values)
def update_plc_datastore(store): # Update digital outputs coils = store.getValues(1, 0, PLC_CONFIG[MODEL]['Q']) for i in range(PLC_CONFIG[MODEL]['Q']): pin_name = "Q0.{}".format(i) rpiplc.digital_write(pin_name, coils[i]) # Update analog outputs registers = store.getValues(3, 0, PLC_CONFIG[MODEL]['A']) for i in range(PLC_CONFIG[MODEL]['A']): pin_name = "A0.{}".format(i + 5) rpiplc.analog_write(pin_name, registers[i]) # Update digital inputs di_values = [] for i in range(PLC_CONFIG[MODEL]['I']): pin_name = "I0.{}".format(i) value = rpiplc.digital_read(pin_name) di_values.append(value) store.setValues(2, 0, di_values) # Update analog inputs ai_values = [] for i in range(PLC_CONFIG[MODEL]['AI']): pin_name = "I0.{}".format(i + 7) value = rpiplc.analog_read(pin_name) ai_values.append(value) store.setValues(4, 0, ai_values)
Master
The following code modules are written in Python and use the pymodbus library to provide functions for Modbus RTU and TCP master devices:
API Reference
This module encapsulates the functionality required to connect to a Modbus server and perform read and write operations on coils and registers. Below is a detailed description of the class methods, including their syntax, parameters, and example codes.
To be able to use these functions, first initialize the Modbus client by specifying the connection method, whether TCP or RTU, and the corresponding parameters.
- For TCP: modbus_client = ModbusTCP(host='10.10.10.20', port=502)
- For RTU: modbus_client = ModbusRTU(port='/dev/ttySC0', baudrate=38400, bytesize=8, parity='E', stopbits=1)
If the parameters are not specified, the default values are used:
- For TCP, the default values are host='10.10.10.20' and port=502.
- For RTU, the default values are port='/dev/ttySC0', baudrate=38400, bytesize=8, parity='E', and stopbits=1.
Description:
Reads the status of coils starting from a specific address (FC=01).
Syntax:
read_coil(address, quantity, slave_id)
Parameters:
- address: The starting address of the coils.
- quantity: The number of coils to read.
- slave_id: The address of the slave device.
Example code:
# Read coils from the Modbus server if modbus_client.connect(): modbus_client.read_coil(address=1, quantity=10, slave_id=1) modbus_client.close()
Description:
Reads the status of discrete inputs starting from a specific address (FC=02).
Syntax:
read_discrete_input(address, quantity, slave_id)
Parameters:
- slave_id: The address of the slave device.
- address: The starting address of the discrete inputs.
- quantity: The number of discrete inputs to read.
Example code:
# Read discrete inputs from the Modbus server if modbus_client.connect(): modbus_client.read_discrete_input(address=1, quantity=10, slave_id=1) modbus_client.close()
Description:
Reads the values of holding registers starting from a specific address (FC=03).
Syntax:
read_holding_register(address, quantity, slave_id)
Parameters:
- address: The starting address of the holding registers.
- quantity: The number of holding registers to read.
- slave_id: The address of the slave device.
Example code:
# Read holding registers from the Modbus server if modbus_client.connect(): modbus_client.read_holding_register(address=1, quantity=10, slave_id=1) modbus_client.close()
Description:
Reads the values of input registers starting from a specific address (FC=04).
Syntax:
read_input_register(address, quantity, slave_id)
Parameters:
- address: The starting address of the input registers.
- quantity: The number of input registers to read.
- slave_id: The address of the slave device.
Example code:
# Read input registers from the Modbus server if modbus_client.connect(): modbus_client.read_input_register(address=1, quantity=10, slave_id=1) modbus_client.close()
Description:
Writes a single binary value to a specific coil address (FC=05).
Syntax:
write_coil(address, value, slave_id)
Parameters:
- address: The address of the coil.
- value: The value to write to the coil.
- slave_id: The address of the slave device.
Example code:
# Write a single coil to the Modbus server if modbus_client.connect(): modbus_client.write_coil(address=1, value=True, slave_id=1) modbus_client.close()
Description:
Writes a single value to a specific holding register address (FC=06).
Syntax:
write_register(address, value, slave_id)
Parameters:
- address: The address of the holding register.
- value: The value to write to the holding register.
- slave_id: The address of the slave device.
Example code:
# Write a single register to the Modbus server if modbus_client.connect(): modbus_client.write_register(address=1, value=123, slave_id=1) modbus_client.close()
Description:
Writes multiple binary values to consecutive coil addresses (FC=15).
Syntax:
write_multiple_coils(address, value, slave_id)
Parameters:
- address: The starting address of the coils.
- value: The values to write to the coils.
- slave_id: The address of the slave device.
Example code:
# Write multiple coils to the Modbus server if modbus_client.connect(): modbus_client.write_multiple_coils(address=1, value=[True, False, True], slave_id=1) modbus_client.close()
Description:
Writes multiple values to consecutive holding register addresses (FC=16).
Syntax:
write_multiple_registers(address, value, slave_id)
Parameters:
- address: The starting address of the holding registers.
- value: The values to write to the holding registers.
- slave_id: The address of the slave device.
Example code:
# Write multiple registers to the Modbus server if modbus_client.connect(): modbus_client.write_multiple_registers(address=1, value=[123, 456, 789], slave_id=1) modbus_client.close()
There are no comments for now.