Opto22 provides a Modbus Integration Kit for a SNAP-PAC controller to operate as a modbus master over serial and TCP connections. The subroutines included in the kit require you to configure quite a few parameters to get them working correctly. Each time I go to use the kit in a new strategy I usually have forgotten the nuances of these parameters and spend quite a bit of time trying to figure things out all over again.
In addition to this, the raw data that most of the devices I work with that use modbus require some sort of multiplier to get them into proper units. For example, a lot of devices don’t support floating points, so they return integers that you then need to divide by some power of 10 to get into the correct engineering units. So a value of 77.52 may be stored in an input register as 7752.
Here is the old way of how I would setup a typical modbus chart:
This is functional, and pretty easy to follow, but it takes a bit of time to get it all setup just right, and since I usually have to troubleshoot and debug modbus code in the field, time is important.
My goal is to create a chart with a single configuration section that I can use from strategy to strategy to communicate read and write to any modbus device – serial or TCP.
Since there is no ability to have an array of a “struct” datatype in Opto22 (which would be a totally awesome feature if anyone at Opto is following along!) to place my configuration information in, I’m going to put all my data into a configuration string, and have my modbus chart parse the string. I’ve used this technique of using a string as a record data type in Opto22 Scheduling Subroutine post.
The configuration data I will need to have for each modbus point are as follows:
- A polling interval in seconds
- The communication port
- The modbus device address
- The modbus function type – coil, register, etc.
- The coil/register address
- The type of data we expect – int, float, signed, unsigned, etc.
- A multiplier!
- Where we want to store the data
- A flag noting if the point is writable
So if I put this data into a configuration string it may look like this:
sModbusConfiguration = "1.0,0,1,3,30,0,0.01,MyData,1";
That would have the chart use comm port at index 0, to request holding register 30 from modbus slave address 1 every 1 second as an unsigned 16-bit integer, and multiply the value by .01 and store it in PAC Control variable MyData. Also, since it is writable, any data written to the MyData variable would be divided by .01 and a preset holding register command would be sent back to the device with the written value.
Now since I want to be able to read to more than one point, I will need to make the configuration string an array:
stModbusConfiguration = "1.0,0,1,3,30,0,0.01,MyData,1"; stModbusConfiguration = "1.0,0,1,3,31,0,0.01,MyData2,1";
Now I don’t want to be sending separate requests for each register address when I have several consecutive registers. So to optimize for this common scenario, I want to modify the configuration string to let the chart know that I just want to read the next address on the same slave device. So after defining the first register to read, the next consecutive register will only need the data type, multiplier, storage location, and write flag. I denote an address continuation by starting the line with an asterisk. So changing the above configuration to use this address continuation feature I want to set it up like this:
stModbusConfiguration = "1.0,0,1,3,30,0,0.01,MyData,1"; stModbusConfiguration = "*,0,0.01,MyData2,1";
This will configure the modbus chart to make a single request for these two values every second.
So now that we have the configuration string setup, there are a few other things that will need to be configured: Communication ports, Modbus communication type, and how we want the chart to treat communication errors.
Since I want to be able to communicate with devices on different serial ports and TCP ports I need to be able to create multiple communication handles. These handles need to be indexed for the configuration string, so they will also need to be stored into a pointer array. I also need to let the modbus chart know if we are using ASCII, RTU or TCP for communication. Here is an example of this configuration:
SetCommunicationHandleValue("tcp:192.168.100.50:502", chModbusTCP); SetCommunicationHandleValue("ser:baud=38400,data=8,parity=n,rts-cts=0,stop=1,port=3,timeout=1.0", chModbusRTU); potModbusCommHandles = &chModbusTCP; potModbusCommHandles = &chModbusRTU; //Comm Type: 0=Serial RTU, 1=Serial ASCII, 2=TCP ntModbusCommType = 2; ntModbusCommType = 0; fModbusErrorValue=-32768.0;
In this example I defined two communication handles, one for TCP, and one for serial on port 3. I then assigned these two handles to a pointer table that the modbus chart uses to retrieve the communication handle. I also set the communication protocol type for each comm port by assigning the type to a numeric table using the same respective index as the communication handles pointer table. Lastly, I set a value I want to assign to my variables when there is a communication failure. The modbus chart also includes and error table that will flag a communication failure based on the index of the configuration table.
To assist in building the configuration string table, I use a spreadsheet that generates the configuration string for me:
After the spreadsheet is filled out, copy the yellow section into the user initialization block of the modbus chart. Place the communication handle configuration in this block as well. The rest of the code in the chart will then parse the configuration and perform the reading and writing to the modbus slaves!
Here is a complete configuration for reading power data from a four ABB drives over two different serial ports. After the modbus chart and subroutines are imported into a strategy this is the only code that will need to be setup (The data storage variables need to be created too!):
SetCommunicationHandleValue("ser:2,9600,n,8,1", chModbusAHU1); SetCommunicationHandleValue("ser:3,9600,n,8,1", chModbusAHU2); potModbusCommHandles = &chModbusAHU1; potModbusCommHandles = &chModbusAHU2; ntModbusCommType = 0; ntModbusCommType = 0; fModbusErrorValue=-32768; stModbusConfiguration = "5,1,1,3,106,1,0.1,AHU_2_SA_Fan_Power_kW,0"; stModbusConfiguration = "5,1,3,3,106,1,0.1,AHU_2_RA_Fan_Power_kW,0"; stModbusConfiguration = "5,0,1,3,106,1,0.1,AHU_1_SA_Fan_Power_kW,0"; stModbusConfiguration = "5,0,3,3,106,1,0.1,AHU_1_RA_Fan_Power_kW,0"; stModbusConfiguration = "";
Now if I went to the field and had a register number wrong, or the customer wanted to add a couple more points, it can be done in just a few minutes!
I have provided a sample strategy for download below. It is in 9.3 basic. Along with the needed subroutines from the integration kit, there is also one utility subroutine that is used to help parse the configuration string. Look in the utility folder for the spreadsheet to assist with configuration.
To bring this into an existing strategy:
- Export the modbus chart from the sample strategy (Chart | Export).
- Copy the subroutines to your subroutine folder.
- Open your strategy and include the subs (Configure | Subroutines Included…).
- Import the modbus chart into your strategy (Chart | Import).
- Now setup the pink Modbus User Initialization block in the modbus chart.
Please let me know if this chart works for you or not! I’ve only tested it on the devices I have access to and would like to know which other modbus devices test succesfully or not. Thank you!