Bus Adapter
Bus Adapter allows to generate cross-platform C++ library and x86 reader/writer programs for interacting with third-party Modbus devices or for building your own.
Unlike using bare bones Modbus libraries directly, the software generates a thin, strongly-typed portable interface that decouples caller’s code from the details of data encoding, pointer arithmetic and other error-prone logic.
Features
- Generates portable API and reader/writer programs from a model
- Enables configuration of data and code footprint
- Uses static memory allocation
- Supports configurable, portable logging
- Allows signed/unsigned 16-, 32-, 64-bit integer and double data types
- Complies with Modbus RTU standard
- Provides serial I/O device drivers
Platforms
- x86
- MacOS, Linux
- STM32
- STM32F103
- AVR
- ATmega168, ATmega328p, ATmega1280, ATmega2560
Project Example
Documentation
Example Project for Experimentation
The project includes example model and source code for experimentation and unit testing.
Example firmware, built for STM32 / AVR, acts as Modbus slave and polls a time-of-flight sensor (TOF) for range measurements. The slave stores retrieved range value using generated Modbus API. A reader program, acting as Modbus master, queries the slave for the recorded range value.
1
2
3
4
5
6
7
8
9
10
11
+-----------------------+ +------------------------+
| Linux / MacOS, Master | | STM32 / AVR, Slave |
| | | |
| +---------------+ | | +------------------+ | +------------+
| | Reader +---+------+->| TOF poller task +--+----->| TOF sensor |
| +---------------+ | | +------------------+ | +------------+
| | | |
| +---------------+ | | +------------------+ |
| | Writer +---+------+->| Operation params | |
| +---------------+ | | +------------------+ |
+-----------------------+ +------------------------+
Dependencies
MacOS
Linux
MacOS or Linux
- Cmake
- C++ Boost
- Doxygen
- Graphviz
- Socat
- Google Test - included
- ctpp - included
- spdlog - included
- cmake-helpers - included
- utility - included
- devices - included
- boltalog - included
- boltabus - included
For STM32 target
- ARM GNU toolchain
- export ARMTOOLS_HOME=_SOME_DIRECTORY_
- Download and extract the archive to $ARMTOOLS_HOME
- FreeRTOS - included
- libopencm3 - included
- stm32-cmake - included
- st-link
firmware
- Example installation instructions are here
For AVR target
Build
Bus Adapter can be built as a library to integrate with a custom program/firmware. Another option is to just build reader/writer. To demonstrate both options, the project includes an example model that generates the API and the programs that use that API.
Note, in order for some unit tests to execute successfully, example model must be generated. To do that, set ENABLE_EXAMPLE option.
To build example for x86 / STM32 / AVR, from the project’s root directory execute:
1
$ ./make.sh -x -s -a
Parameters:
- -x - build code generator, generate APIs and reader/writer programs. Must execute at least once
- -s - build STM32 firmware
- -a - build AVR firmware
Model
A model descrbes input/output registers and coils that some Modbus device supports. In addition to bus specifics, the model provides indications as to how to generate the source code. Text file format uses JSON notation and defines the following attributes.
namespace
Boltabus code genearator uses namespace array to enclose the generated code into specified C++ namespace. For example if model contains this:
1
'namespace': [ 'example', 'modbus' ],
code generator would output this:
1
2
3
4
5
6
7
8
9
namespace example
{
namespace modbus
{
... code ...
} // namespace modbus
} // namespace example
msb
The parameter msb indicates if the numbers are transmitted using most-significant (MSB) or least-significant byte (LSB) order. By default, LSB order is used.
1
'msb':'false',
register_sets
register_sets defines an array containing logically grouped registers or coils.
A group must contain registers or coils of the same type: coil, contact, input, hold. Default type, if parameter is not specified, is “hold”. Multiple groups having the same type can be specified.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'register_sets': [
{
'registers_type':'coil',
...
},
{
'registers_type':'contact',
...
},
{
'registers_type':'input',
...
},
{
'registers_type':'hold',
...
},
{
'registers_type':'hold',
...
}
]
}
start_rid
start_rid is a register or coil number that starts a given group. Default is 0. In the example below, the first holding register has an ID of 4. The code generator will calculate register IDs for the subsequent registers in the group using this first ID.
1
2
3
4
5
6
7
{
'registers_type':'hold',
'start_rid':'4',
'registers': [
...
]
}
repeat
repeat parameter indicates how many times a group should be repeated in code, i.e. how many instances should exist. For example, a power supply may have multiple value combinations of voltage/current. Instead of creating unique names for each parameter, the model would use parameter repeat to generate an API that accepts a group/”instance” identifier.
In the following example model defines one group to be repeated 2 times. Client C++ code will need to specify parameter group_id as 0 or 1 when getting/setting values:
1
2
3
4
5
6
7
8
{
'start_rid':'0',
'repeat':'2',
'registers': [
{ 'name':'voltage' },
{ 'name':'current' }
]
}
C++ code example. Note, function signature includes starting Register ID. This is to prevent potential name clash with another repeated group that would define similar register names:
1
2
uint16_t v1 = adapter->voltage_0(slave_id, 0);
uint16_t v2 = adapter->voltage_0(slave_id, 1);
registers
registers is an group of registers/coils that are contiguous to each other. Code generator needs non-contiguous registers to be defined in different groups. This is required for calculating register/coil IDs automatically and thus minimizing error-prone manual specification. It is especially desired when registers may be defined using non-standard size of a Modbus register, which is two bytes (one word).
For example, given the model below, registers would have the following IDs:
- r1 - 4 (because start_rid is 4)
- r2 - 5 (because start_rid + 1 previous 2-byte register)
- r3 - 9 (because start_rid + 1 starting 2-byte register + 4 previous “2-byte registers”). 4 previous registers is because parameter bytes of r2 is 8 (not the default 2), and 8 / 2 = 4
1
2
3
4
5
6
'start_rid':'4',
'registers': [
{ 'name':'r1', 'bytes':'2' },
{ 'name':'r2', 'bytes':'8' },
{ 'name':'r3', 'bytes':'2' }
]
Because code generator calculates IDs, caller’s C++ code turns out simpler:
1
2
3
uint16_t r1 = adapter->r1(slave_id);
uint64_t r2 = adapter->r2(slave_id);
uint16_t r3 = adapter->r3(slave_id);
register specification
Register specification describes a Modbus register or coil using the following attributes:
- name - register name. The name is used as part of getter/setter in the generated interface
- dec_places - a number of decimal places in a value. Code generator uses double data type in the interface to represent the value. On wire, the value is represented using an integer. A penalty for using this on embedded platform is larger code size
- signed - type of integer: 1 - signed, 0 - unsigned. Default, if not specified, is unsigned
- bytes - size in bytes of the value for input/hold register types. Valid values:
- 2 - represented by uint16_t/int16_t. Default, if not specified, is uint16_t; if signed is 1, int16_t
- 4 - represented by uint32_t/int32_t
- 8 - represented by uint64_t/int64_t
- For coil/contact register type, the data type s uint8_t of 1 byte
- doc - value description. Code generator outputs this text into getter/setter comments
Given this model:
1
2
3
4
'registers': [
{ 'name':'r2', 'dec_places':'2', 'bytes':'8', 'doc':'Analog register 2' }
{ 'name':'r3' }
]}
code generator outputs the following API. Note val data types of r2 and r3:
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
/**
* Analog register 2. Getter.
*
* @param sid - server ID
* @param rc - if not nullptr, it will be set to 0 when the operation
* completes successfully, and to -1 if the operation fails
* @param info - if not nullptr, the structure is updated with the information
* about the register
* @return the value
*/
double r2(uint16_t sid, int* rc = nullptr, btr::bus::ReadRegInfo* info = nullptr);
/**
* Analog register 2. Setter.
*
* @param sid - server ID
* @param val - the value to set
* @param rc - if not nullptr, it will be set to 0 when the operation completes
* successfully, and to -1 if the operation fails
*/
void set_r2(uint16_t sid, double val, int* rc = nullptr);
/* ... */
uint16_t r3(uint16_t sid, int* rc = nullptr, btr::bus::ReadRegInfo* info = nullptr);
/* ... */
void set_r3(uint16_t sid, uint16_t val, int* rc = nullptr);
Logging
This project uses Portable Logger library for logging. The logging API is generated from a model in boltabus/model/logger.mdl.
Logging functions can be disabled during the build by setting variable BTR_LOG_ENABLED to 0. This will reduce binary size and speed up program execution.
Code Configuration
The project uses different pre-processor directives to control what will be compiled into a program. On embedded platform, they also indicate which devices to use for Modbus and logging.
CMakeLists in src/x86, src/stm32, src/avr contain directives for the example programs.