I recently needed a Modbus RTU (Remote Terminal Unit) device to test some software against. Modbus is a serial communication command/response or master/slave protocol for connecting a master control device to various types of slave industrial devices known as Remote Terminal Units, using either point-to-point RS-232 asynchronous serial links, or point-to-multipoint RS-485 asynchronous serial buses.
After some research, I determined the shortest path to a working RTU device would be to make some very minor changes to the Linux demo RTU application included in the FreeMODBUS Modbus RTU library (hereafter FreeMODBUS). I ran the modified RTU demo app on a Raspberry Pi Zero running my own MuntsOS Embedded Linux, configured as a USB Serial Gadget device.
The FreeMODBUS architecture has a well-defined API (Application Programming Interface) between protocol handling code and user application code. The protocol handling code calls four service handler callback functions, one for each of the four Modbus data element types:
eMBRegDiscreteCB
handles requests for reading from Modbus discrete inputs.eMBRegCoilsCB
handles requests for reading from and write to Modbus coil outputs.eMBRegInputCB
handles requests for reading from Modbus (analog) input registers.eMBRegHoldingCB
handles requests for reading from and writing to Modbus (analog output) holding registers.
The Linux demo RTU application runs the protocol handling code in a separate Posix thread spawned by the C main program. The C main program source file also contains implementations for the service handler callback functions.
This Make with Ada project illustrates how to implement the service handler callback functions in Ada, instead of C. It allowed me to quickly and easily leverage an enormous amount of Ada embedded systems code, from both my Linux Simple I/O Library (hereafter libsimpleio) and MuntsOS Embedded Linux (hereafter MuntsOS) repositories.
MuntsOS USB Serial GadgetWhen a Linux microcomputer is configured as a USB Serial Gadget device, its USB port is configured for a very special form of serial port emulation. When you plug the device computer (e.g. a Raspberry Pi Zero) into a host computer (e.g. a Linux or Windows PC), the host computer will see its end of the USB cable as an ordinary USB serial port, named something like /dev/ttyACM0
(Linux) or com9:
(Windows) and the device computer will see its end of the cable as serial port device /dev/ttyGS0
.
Often the host computer (Master) can supply power to the device computer (RTU) via the USB cable, too.
A Modbus RTU program running on the device computer opens serial port device /dev/ttyGS0
and then listens for commands. A Modbus master program running on the host computer opens serial port e.g./dev/ttyACM0
to issue commands to and receive responses from the RTU program running on the device computer.
This MuntsOS application note (page 6) describes how to set up a Raspberry Pi Zero as a USB Serial Gadget device. Note that for this Make with Ada project, the OPTIONS
word in config.txt
must be changed to 0x7AC
instead of 0x72D
to prevent mgetty
(the serial console login manager in MuntsOS) from acquiring /dev/ttyGS0
.
Although the examples in this Make with Ada project use a Raspberry Pi Zero Wireless for the RTU, because of its low cost and low power consumption, other Linux microcomputers such as the BeagleBone Black, PocketBeagle, and other Raspberry Pi models can be used as well. Example #1 has been tested on a Raspberry Pi 4 Mode B.
Modbus RTU Framework for AdaSource code for the RTU framework is stored in the MuntsOS code repository, in the directory examples/ada/freemodbus/
. There you will find a make
include file, some patch files, and a C main program.
The make
include file freemodbus.mk
defines rules that download the FreeMODBUS RTU library source code distribution tarball, unpack it, patch the source tree, and compile all of the C code required to build an RTU application. freemodbus.mk
must be included by each RTU project Makefile
.
The RTU application C main program has been adapted from the FreeMODBUS Linux example. It has been modified to call adainit()
to elaborate the Ada packages and then call daemon()
to run as a background process.
Also in examples/ada/freemodbus/
are two Ada package specifications:
- FreeMODBUS is a parent package that defines a few constants.
- FreeMODBUS.Services is a package specification for the callback functions that the FreeMODBUS library code will call to execute commands from a Modbus master device.
Each RTU project must contain two files customized for the particular project: Makefile
and freemodbus-services.adb
.
Providing Ada service callbacks to the FreeMODBUS C library is accomplished by using some special features of the gnatbind
and gnatlink
programs.
The following RTU project make
target extracted from the Makefile
for Example #1 shows how to combine a C main program, Ada packages, and libsimpleio.so
into a working program:
rtu-raspberrypi-gpio: modbus_rtu_mk_freemodbus
mkdir -p $(ADA_OBJ)
$(GNATMAKE) $(GNATMAKEFLAGS) -c freemodbus-services.adb -cargs $(GNATMAKECFLAGS)
$(GNATBIND) -n $(ADA_OBJ)/*.ali
$(GNATLINK) $(ADA_OBJ)/logging.ali $(MODBUS_SRC)/obj/*.o -o $@ -lpthread -lsimpleio
$(GNATSTRIP) $@
First the subordinate target modbus_rtu_mk_freemodbus
(defined in freemodbus.mk
) cross-compiles all of the FreeMODBUS C code as well as the C main program.
Next gnatmake
cross-compiles freemodbus-services.adb
and its dependencies.
gnatbind -n
indicates there will be no Ada main program. Instead, gnatbind
will append a subprogram called adainit()
to one of the .ali
files in the object directory. It is not obvious to me how gnatbind
selects which particular .ali
file, which will have to be explicitly provided to gnatlink
for the next step of the build process.
Finally gnatlink
links all of the objects and libraries into a single program file and strip
reduces the size of the program file by removing unnecessary debug information.
As part of this Make With Ada project, and in order to validate the RTU device framework, I also needed to write some Modbus master device support code in Ada. I accomplished this by adding the following six packages to libsimpleio (links are to the package specification files):
- libModbus is a straightforward Ada thin binding to the libmodbus Modbus master device library for Linux.
- Modbus is the parent package for Ada Modbus objects (i.e. classes). It defines a type
Bus
for encapsulating the Modbus physical and transport layers. - Modbus.DiscreteInputs defines a type
PinSubclass
that implementsGPIO.PinInterface
(part of libsimpleio) for Modbus discrete inputs. - Modbus.Coils defines a type
PinSubclass
that implementsGPIO.PinInterface
for Modbus coil outputs. - Modbus.InputRegisters defines a type
RegisterClass
(and a corresponding access typeRegister
) for encapsulating Modbus 16-bit (analog) input registers. - Modbus.HoldingRegisters defines a type
RegisterClass
(and a corresponding access typeRegister
) for encapsulating Modbus 16-bit (analog output) holding registers.
The above packages fully support the four Modbus standard data element types, each of which has its own distinct address space. The libmodbus library also supports an additional semi-standard data element type, a 32-bit floating point value occupying two adjacent 16-bit (analog) input registers or two adjacent 16-bit (analog output) holding registers. I implemented support for 32-bit floating point registers by adding the following two additional generic packages to libsimpleio:
- Modbus.InputShortFloat must be instantiated with a floating point type (e.g.
Voltage.Volts
from libsimpleio) and defines a typeInputClass
that encapsulates 32-bit floating point registers in the (analog) input register address space. - Modbus.OutputShortFloat must be instantiated with a floating point type and defines a type
OutputClass
than encapsulates 32-bit floating point registers in the (analog output) holding register address space.
This RTU example project maps Raspberry Pi GPIO pins to Modbus discrete inputs and coils. All GPIO pins are mapped into both the discrete input and coil address spaces. Writing to a coil will configure the corresponding GPIO pin as an output. Reading from a discrete input will configure the corresponding GPIO pin as an input. (Do not try to use the same GPIO pin for both!)
This Modbus master test program reads from a button connected to a Modbus discrete input and writes to an LED connected to a Modbus coil output.
I used a Raspberry Pi 4 running Raspbian for the Modbus master device, compiling the test program with the Debian native GNAT toolchain.
I used a Raspberry Pi Zero breadboard setup for the RTU hardware. The button is connected to GPIO 19 and the LED is connected to GPIO 26. The Raspberry Pi Zero Wireless is running MuntsOS. I compiled the RTU program with the GNAT cross-toolchain for MuntsOS.
This RTU example project maps the I/O resources of the Automation pHAT (3 analog inputs, 3 digital inputs, 3 digital outputs, and a SPDT relay) to Modbus input registers, discrete inputs, and coils.
This Modbus master test program toggles the relay once a second and then displays the states of the digital inputs and outputs as well as the analog input voltages.
I used a Raspberry Pi 4 running Raspbian for the Modbus master and a Raspberry Pi Zero Wireless running MuntsOS with an Automation pHAT for the RTU.
This RTU example project maps the I/O resources of the ADC DAC Pi Zero (two 3.3V analog inputs and two 2.048V analog outputs) to Modbus input and holding registers.
This Modbus master test program displays the analog voltages at IN1
, IN2
, O1
, and O2
once a second and then sets output O1
to match input IN1
and O2
to match IN2
. Exception handlers prevent setting the outputs above 2.048V.
I used a Raspberry Pi 4 running Raspbian for the Modbus master and a Raspberry Pi Zero Wireless running MuntsOS with an ADC DAC Pi Zero for the RTU.
Comments