May 07, 2019 / by Sergey Kapustin

Generate Modbus Reader and Writer for RuiDeng DPS

While working on a steering mechanism for my robot, I wanted to record the current draw of the motors. The data would give me a rough idea about the friction in the mechanism so as to decide whether I need a different motor controller or different ideas. How can I go about collecting the data?

To feed the motors during testing, I use RuiDeng DPS power supply, which can provide the data over Modbus. There is an app for Windows and Android to read it, but I couldn’t find anything suitable for MacOS. Given that Modbus is a standard protocol for connecting industrial devices, would it be a ridiculous idea to develop a generic library to use it with this power supply and other devices? It turned out that somebody developed such a library.

Perhaps the best implementation comes from SILA Embedded Solutions. The library is small and clean, and it supports different platforms, which is a huge plus. But I needed a bit more. Different applications that I have in mind would require me to duplicate the code that uses the library. That’s how Bus Adapter project came about. The software generates a C++ interface for a program to interact with a Modbus-compliant device or for building my own.

This post describes how I used Bus Adapter to get the data from the power supply.

On YouTube

Model

The first step is to create a model of the Modubs device. A model describes registers that the device supports. It may also contain other attributes that help to generate source code.

The table below represents power supply’s registers, which will be used to create its model.

Function Description Bytes Dec Places Unit Read/Write Address
U-SET Voltage setting 2 2 V R/W x0000
I-SET Current setting 2 2 A R/W x0001
UOUT Output voltage display value 2 2 V R x0002
IOUT Output current display value 2 2 A R x0003
POWER Output power display value 2 1 or 2 W R x0004
UIN Input voltage display value 2 2 V R x0005
LOCK Key lock 2 0 - R/W x0006
PROTECT Protection status 2 0 - R x0007
CV/CC Constant Voltage/Current status 2 0 - R x0008
ONOFF Switch output state 2 0 - R/W x0009
B-LED Back light brightness level 2 0 - R/W x000A
MODEL Product model 2 0 - R x000B
VERSION Firmware version 2 0 - R x000C
EXTRACT-M Shortcut to bring data set 2 0 - R/W x0023
U-SET Voltage setting 2 2 V R/W x0050
I-SET Current setting 2 2 A R/W x0051
S-OVP Over-voltage protection value 2 2 V R/W x0052
S-OCP Over-current protection value 2 2 A R/W x0053
S-OPP Over-power protection value 2 1 W R/W x0054
B-LED Back light brightness level 2 0 - R/W x0055
M-PRE Memory preset number 2 0 - R/W x0056
S-INI Power output switch 2 2 - R/W x0057

Bus Adapter’s code generator accepts a JSON-formatted file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
'namespace': [ 'rdps' ],
'register_sets':  [
  {
    'registers': [
      { 'name':'uset',    'dec_places':'2', 'doc':'Voltage setting (2dp)' },
      { 'name':'iset',    'dec_places':'2', 'doc':'Current setting (2dp)' },
      { 'name':'uout',    'dec_places':'2', 'doc':'Output voltage display value (2dp)' },
      { 'name':'iout',    'dec_places':'2', 'doc':'Output current display value (2dp)' },
      { 'name':'power',   'doc':'Output power display value (1-2dp)' },
      { 'name':'uin',     'doc':'Input voltage display value (2dp)' },
      { 'name':'lock',    'doc':'Key lock' },
      { 'name':'protect', 'doc':'Protection status' },
      { 'name':'cvcc',    'doc':'Constant voltage/current status' },
      { 'name':'onoff',   'doc':'Switch output state'},
      { 'name':'bled',    'doc':'Backlight brightness level'},
      { 'name':'model',   'doc':'Product model'},
      { 'name':'version', 'doc':'Firmware version'}
    ]
  },
  {
    'start_rid':'35',
    'registers': [
      { 'name':'extractm',  'doc':'Preset ID'}
    ]
  },
  {
    'start_rid':'80',
    'repeat':'10',
    'registers_type':'hold',
    'registers': [
      { 'name':'uset', 'doc':'Voltage setting (2dp)' },
      { 'name':'iset', 'doc':'Current setting (2dp)' },
      { 'name':'sovp', 'doc':'Over-voltage protection (2dp)' },
      { 'name':'socp', 'doc':'Over-current protection (2dp)' },
      { 'name':'sopp', 'doc':'Over-power protection (1dp)' },
      { 'name':'bled', 'doc':'Backlight brightness levels' },
      { 'name':'mpre', 'doc':'Memory preset number' },
      { 'name':'sini', 'doc':'Power output switch (2dp)' }
    ]
  }
]
}

To automate assignment of register IDs, the registers are split into groups. The registers within a group are contiguous. The first register has an ID of start_id attribute. IDs of the following registers are incremented every word, which takes 2 bytes.

In case of the power supply, the first group starts with register 0, named uset. The second group starts with register 35, named extractm. The third group starts with register 80, which is uset preset.

Power supply’s document indicates that there are ten consecutive instances of the third group. Attribute repeat specifies that. The code generator will now add an additional parameter to the interface to distinguish between the instances.

Decimal points are useful for displaying the values. Floating-point types are not used for data storage or by Modbus protocol.

Build Files

The build uses cmake modules from cmake-helpers project. The modules implement common procedures for cross-platform development. To build reader and writer programs for the power supply, I use the following project structure:

1
2
3
4
5
6
7
8
9
make.sh
CMakeLists.txt
config
  |-- serial.cfg
model
  |-- dps.mdl
src
  |-- x86
        |-- CMakeLists.txt

Root CMakeLists.txt contains instructions to set up module path and to include the module main_project:

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.7)
project(dps)

set(CMAKE_MODULE_PATH $ENV{CMAKEHELPERS_HOME}/cmake/Modules)
set(ROOT_SOURCE_DIR ${PROJECT_SOURCE_DIR})

include(main_project)

CMakeLists.txt at src/x86 configures some definitions and the main dependency, Bus Adapter:

1
2
3
4
5
add_definitions(-DMB_MASTER_RTU_ENABLED=1)
add_definitions(-DMB_MASTER_TOTAL_SLAVE_NUM=1)
add_definitions(-DMB_MASTER_TIMEOUT_MS_RESPOND=1000)

setup_dep(boltabus $ENV{BOLTABUS_HOME} SUB_DIR "./")

Build

Once the build is set up and the model is complete, I start the build process using a shell script:

1
./make.sh -x -m ${PWD}/model/dps.mdl

The script reads command line arguments, exports paths and invokes cmake.

Run

At this point, the build is done and I store serial link parameters to a configuration file to avoid typing them on every run:

1
2
3
4
5
6
7
8
9
device=/dev/tty.wchusbserialfd1460
baud=9600
data_bits=8
stop_bits=1
parity=0

# Modbus server IDs
local_sid=20
remote_sids=1

After starting up the power supply, I request it to load preset number 2, which on my unit has output voltage set at 12, and maximum amps at 5:

1
$ ./build-x86/Release/bin/dps-writer -c config/serial.cfg -l -g 1 --extractm 2

At debug log level, dps-writer outputs Modbus request/response messages and other information. Note, the source code uses portable logger (aka boltalog) project.

1
2
3
4
5
2019-05-07T14:31:02.764 I [1235788]: 12,/dev/tty.wchusbserialfd1460,9600,8,0
2019-05-07T14:31:02.771 I [1235788]: 3,1
2019-05-07T14:31:02.777 D [1235792]: 4,8,01:06:00:23:00:02:F9:C1
2019-05-07T14:31:03.131 D [1235792]: 5,8,01:06:00:23:00:02:F9:C1
2019-05-07T14:31:03.131 I [1235788]: 17,1

Next, I turn the power on:

1
2
3
4
5
6
7
$ ./build-x86/Release/bin/dps-writer -c config/serial.cfg -l -g 1 --onoff 1

2019-05-07T14:31:16.102 I [1235880]: 12,/dev/tty.wchusbserialfd1460,9600,8,0
2019-05-07T14:31:16.108 I [1235880]: 3,1
2019-05-07T14:31:16.114 D [1235883]: 4,8,01:06:00:09:00:01:98:08
2019-05-07T14:31:16.448 D [1235883]: 5,8,01:06:00:09:00:01:98:08
2019-05-07T14:31:16.449 I [1235880]: 17,1

Now, I can start recording the required information using dps-reader:

1
2
$ ./build-x86/Release/bin/dps-reader -c config/serial.cfg -l -g 2 --poll_count 100
--poll_delay 1000 --uset --iset --iout --uout

The program logs the preset values as well as voltage and current being supplied:

1
2
3
4
5
2019-05-07T12:07:12.994 I [160741]: 18,sid:1,uset:12.02,iset:5,uout:12.01,iout:0.38
2019-05-07T12:07:14.350 I [160741]: 18,sid:1,uset:12.02,iset:5,uout:12.01,iout:0.38
2019-05-07T12:07:15.708 I [160741]: 18,sid:1,uset:12.02,iset:5,uout:12.01,iout:0.38
2019-05-07T12:07:17.065 I [160741]: 18,sid:1,uset:12.02,iset:5,uout:12.01,iout:0.67
2019-05-07T12:07:18.423 I [160741]: 18,sid:1,uset:12.02,iset:5,uout:12.01,iout:1.01

Conclusion

The goal is achieved. I can read and store the data from the power supply. The best thing, it only took me a short time to set up.

References

You may also like

January 09, 2018

Custom Protocol for Serial Link Communication

After spending your time on learning advanced “hello world” tutorial of some third-party library, and trying to code something more useful, you hit your first problem. Scavenging the internet for help on seemingly simple issues burns more of your precious time with little return. You think - there has to be a better way to move your project along.