A framework to create custom, realtime, modbus drivers using the Mesa PktUART component.
There are several existing ways to control Modbus devices with LinuxCNC, and those should also be considered. They include, but are not limited to:
There are also several drivers for specific VFDs, for example
hy-vfd - HuangYang VFD
hy-gt-vfd - HuangYang GT VFD
gs2-vfd - Automation Direct GS2 VFD
vfdb-vfd - Delta VFD-B VFD
wj200-vfd - Hitachi wj200 VFD
For most applications (especially the "hy" (HuangYang) VFDs which do not actually conform to true modbus) it is likely to be less work to use these.
Reasons to use this framework:
1) It is realtime, commands and reads happen deterministically. This does not mean that they are fast, but that they happen at the same rate all of the time, independent of other activity on the PC. At 9600bps a typical transaction takes 18 servo-thread cycles, and the channels are serviced consecutively. At higher bit-rates things are better, but each read or write will always take at least three cycles, per register. To ameliorate this a little, writes to registers are only performed when necessary. Reads happen in turn, taking as long as they take.
2) It uses ports on the Mesa card. If you already have a Mesa card then this means that no extra hardware is needed. A typical application would be to re-purpose the smart-serial port on the 7i96 as a modbus port to control a VFD. If you do not have a Mesa card then this is obviously no advantage at all.
1. Quick Start
Install the linuxcnc-uspace-dev or linuxcnc-dev package (depending on the LinuxCNC version installed).
Create a new definition file similar to analogue.mod and relayboard.mod (the annotated mesa_uart.mod.sample file should help here)
Run the command:
./modcompile my_file.mod
and the "modcompile" script will compile and install the module. As some realtime implementations of LinuxCNC realtime require that modules be kernel modules, and kernel modules have no access to files, the mesa_modbus framework compiles-in the modbus register structure and HAL pins into an immutable, loadable, module. If you need to change the register assignments or add/remove pins/registers then the module must be recompiled.
Load and activate the module with HAL commands such as
loadrt my_file ports=hm2_7i69.0.pktuart.0
addf my_file servo-thread
2. Modbus
Rather than document modbus here, the main details can be found at: https://en.wikipedia.org/wiki/Modbus
The mesa_modbus system supports modbus commands 1,2,3,5,6,15,16.
3. HAL Interface
Modbus write commands will create HAL input pins, that take values from HAL and pass them to the Modbus device.
Modbus read commands will read data from the device and pass that to HAL output pins for use by other components.
HAL_BIT, HAL_U32 and HAL_S32 pins will present the (16 bit) Modbus registers exactly as the bits are delivered from the device and will send them to the device as 16 bit values according to standard C type conversion standards. HAL_FLOAT values are treated differently. When a float pin is specified then two extra HAL pins are created with the same name but suffixed with -scale and offset. The value from the modbus register will be interpreted as a sighed-16 bit number. It will then be multiplied by the scale value and then the offset will be added (i.e. the offset is in engineering units, and the scale converts 16 bit register values to engineering units).
The reverse transformation is performed when writing to the device.
All Modbus registers are 16 bits. It is possible that in some applications this might be used to represent encoder counts or some other number that will wrap-around. To circumvent this problem in the case of the HAL_S32 pin type the registers will be promoted to signed 64-bit internally then multiplied by the "scale" pin value and presented as a floating point value on a HAL pin with the suffix "scaled". For example mymodule.0.counts-0-scaled.
In addition to the pins configured in the definition file, each module will create the following pins for each instance of the driver:
modname.address (default 0x01)
modname.baudrate (default 9600)
modname.parity (default 0 (no parity) options are 1 for odd and 2 for even.)
modname.txdelay (default 20 bit lengths. Generally should be larger than Rx Delay)
modname.rxdelay (default 15)
modname.drive_delay (default 0)
modname.update-hz (default 0)
modname.fault - indicates a fault with the device or comms
modname.last-error - indicates the error code that set the fault output.
These can be redefined at any time and will take effect the next time that a modbus packet is assembled.
modname.update-hz is provided to slow down the transaction rate for modbus devices that become unstable if polled too frequently. If you see fault 11 then try setting this to 0.1Hz or even 1Hz. If set to zero the system runs as fast as it can.
The fault codes returned in "last error" are
Code | Fault |
---|---|
1 |
Illegal Function |
2 |
Illegal Data Address |
3 |
Illegal Data Value |
4 |
Server Device Failure |
5 |
Acknowledge |
6 |
Server Device Busy |
7 |
Negative Acknowledge |
8 |
Memory Parity Error |
9 |
Gateway Path Unavailable |
10 |
Gateway Failed to Respond |
11 |
Comm Timeout |
Each module exports a single HAL function to be attached to a realtime thread. The function name is just the module name, with no distinction made between read and write cycles.
All modules created by the framework require a hostmot2 pktuart instance to be given to the "ports" modparam on the "loadrt" file. See the example in the [Quick Start] section.
4. Configuration File
A Mesa_Modbus configuration file is actually a C header file and must conform to C syntax rules. An example file is included here:
/* The format of the channel descriptors is: {TYPE, FUNC, ADDR, COUNT, pin_name} TYPE is one of HAL_BIT, HAL_FLOAT, HAL_S32, HAL_U32 FUNC = 1, 2, 3, 4, 5, 6, 15, 16 - Modbus commands COUNT = number of coils/registers to read */ #define MAX_MSG_LEN 16 // may be increased if necessary to max 251 static const hm2_modbus_chan_descriptor_t channels[] = { /* {TYPE, FUNC, ADDR, COUNT, pin_name} */ // Create 8 HAL bit pins coil-00 .. -07 supplying the values of coils at 0x0000 {HAL_BIT, 1, 0x0000, 8, "coil"}, // Create 8 HAL bit pins input-00 .. -07 supplying the values of inputs at 0x0000 {HAL_BIT, 2, 0x0000, 8, "input"}, // Create a HAL pin to set the coil at address 0x0010 {HAL_BIT, 5, 0x0010, 1, "coil-0"}, // Create 8 HAL pins to set the coils at 0x0020 {HAL_BIT, 15, 0x0020, 8, "more_coils"}, // Create a scaled floating point pin calculated from input register 0x0100 {HAL_FLOAT, 4, 0x0100, 1, "float"}, // Create 4 unsigned integer HAL pins from the holding registers at 0x0200-0x203 {HAL_S32, 3, 0x0003, 4, "holding"}, // Create a single signed int HAL pin to control the register at 0x0300 {HAL_S32, 6, 0x0300, 1, "relay-3"}, // Create 7 scaled FP HAL pins to control holfing registers at 0x400-0x406 {HAL_FLOAT, 16, 0x0300, 1, "more_floats"}, };
Typically the comments would not be included in a config file.
MAX_MSG_LEN can be included as a #define if required, but will default to 16 bytes if this is omitted. The Modbus protocol forces a hard max limit of 251 bytes, but that would imply setting thousands of bits or hundreds of registers in a single transaction.
An optional DEBUG parameter may be defined. This will default to RTAPI_MSG_ERR (1) which means that only error messages will be shown. include the line
#define DEBUG 3
To see verbose data from the driver which can be useful for debugging. Be aware that this is a lot of data, and it should be turned back to 1 when the driver is working.
The text static const hm2_modbus_chan_descriptor_t channels[] = {
must be left unchanged, and the concluding };
is also very
important.
Between the start and end delimiters defined above there should be as many descriptors as necessary for the device being controlled. For a simple device (such as a single channel ADC) there might be only one line. For such a simple device the following minimal description file would suffice
static const hm2_modbus_chan_descriptor_t channels[] = { /* {TYPE, FUNC, ADDR, COUNT, pin_name} */ {HAL_FLOAT, 3, 0x0000, 1, "volts"}, };
The valid HAL pin types supported are HAL_BIT, HAL_FLOAT, HAL_U32 and HAL_S32.
The supported Modbus command types are:
Description | Code |
---|---|
Read Coils |
1 |
Read Discrete Inputs |
2 |
Read Multiple Holding Registers |
3 |
Read Input Registers |
4 |
Write Single Coil |
5 |
Write Single Holding Register |
6 |
Write Multiple Coils |
15 |
Write Multiple Holding Registers |
16 |
The Modbus address can be given in Hexadecimal, decimal (or even octal) as can the modbus command. Typically the modbus commands are given in decimal and the addresses in hex.
If the number in the "count" column is >1 and if the command given
supports multiple reads/writes then a numbered sequence of HAL pins will
be created using the root name from the definition with an appended 2
digit suffix, eg volts-03
. For commands that do not support multiple
values (5, 6) the count column is silently ignored (but must be numeric
and not omitted)
5. Compiling
A simple script modcompile is provided that will compile and install a new HAL module based on the mesa_modbus.c file and the pin definition file. The sample definition files use the .mod prefix but this is not necessary except in the special case of the modcompile all command, which will compile and install all .mod files in the current directory.
sudo modcompile my_file.mod
or
sudo modcompile all
"modcompile" is provided by the "linuxcnc-dev" package.
sudo apt-get install linuxcnc-uspace-dev
or
sudo apt-get install linuxcnc-dev
if using RTAI kernel realtime.
Alternatively the package should be installable with the Synaptic package manager.
6. Hardware Connection
The Mesa serial ports have separate pins for Tx and Tx pairs. For RS422 Modbus RTU communications these should be connected at the Mesa card Tx+ to Rx+ and Tx- to Rx-.
Note that there are differing naming standards for Modbus pins. Typically Rx+ and TX+ will connect to the B- pin on the modbus device and Rx- and Tx- will connect to the A+ pin. (ie, +/- will appear reversed.
6.1. Ad-hoc Modbus device access
For experimentation and one-off configuration it is possible to send / receive data through the FPGA serial port using the mesaflash utility in a script. A sample script follows.
#! /bin/bash # First setup the DDR and Alt Source regs for the 7I96 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x1100=0x1F800 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x1104=0x1C3FF mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x1200=0x1F800 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x1204=0x1C3FF # Next set the baud rate DDS's for 9600 baud mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6300=0x65 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6700=0x65 # setup the TX and RX mode registers mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6400=0x00000A20 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6800=0x3FC0140C # Reset the TX and RX UARTS mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6400=0x80010000 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6800=0x80010000 # load two 8-byte modbus commands: # 01 05 00 00 5A 00 F7 6A and 01 01 00 00 00 01 FD CA mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6100=0x00000501 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6100=0x6AF7005A mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6100=0x00000101 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6100=0xCAFD0100 # Command the TX UART to send the two 8 byte packets mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6200=0x08 mesaflash --device 7i96 --addr 10.10.10.10 --wpo 0x6200=0x08 sleep 1 # display TX Mode mesaflash --device 7i96 --addr 10.10.10.10 --rpo 0x6400 # display the RX mode reg, RX count, and the data mesaflash --device 7i96 --addr 10.10.10.10 --rpo 0x6800 mesaflash --device 7i96 --addr 10.10.10.10 --rpo 0x6600 mesaflash --device 7i96 --addr 10.10.10.10 --rpo 0x6500 mesaflash --device 7i96 --addr 10.10.10.10 --rpo 0x6500