C++ tutorial
In the second tutorial we will write a simple CANopen master and slave using the C++ CANopen application library. The applications will play a form of PDO ping-pong, where each receives a value via PDO and sends it back via another PDO. Trivial, to be sure, but it demonstrates almost everything you need to build a full CANopen application.
We will build up the application incrementally, showing the lines you need to add at each step in the unified diff format. But you can also just download the final master.cpp and slave.cpp.
Like the previous tutorial, we will use the virtual CAN interface on Linux, although the code also shows how to use an IXXAT CAN controller on Windows.
CAN channels and timers
Let’s start simple by just opening a CAN channel. Create the following file:
tutorial.cpp
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <lely/ev/loop.hpp>
#if _WIN32
#include <lely/io2/win32/ixxat.hpp>
#include <lely/io2/win32/poll.hpp>
#elif defined(__linux__)
#include <lely/io2/linux/can.hpp>
#include <lely/io2/posix/poll.hpp>
#else
#error This file requires Windows or Linux.
#endif
#include <lely/io2/sys/io.hpp>
#if _WIN32
#include <thread>
#endif
using namespace lely;
int
main() {
// Initialize the I/O library. This is required on Windows, but a no-op on
// Linux (for now).
io::IoGuard io_guard;
#if _WIN32
// Load vcinpl2.dll (or vcinpl.dll if CAN FD is disabled).
io::IxxatGuard ixxat_guard;
#endif
// Create an I/O context to synchronize I/O services during shutdown.
io::Context ctx;
// Create an platform-specific I/O polling instance to monitor the CAN bus, as
// well as timers and signals.
io::Poll poll(ctx);
// Create a polling event loop and pass it the platform-independent polling
// interface. If no tasks are pending, the event loop will poll for I/O
// events.
ev::Loop loop(poll.get_poll());
// I/O devices only need access to the executor interface of the event loop.
auto exec = loop.get_executor();
#if _WIN32
// Create an IXXAT CAN controller and channel. The VCI requires us to
// explicitly specify the bitrate and restart the controller.
io::IxxatController ctrl(0, 0, io::CanBusFlag::NONE, 125000);
ctrl.restart();
io::IxxatChannel chan(ctx, exec);
#elif defined(__linux__)
// Create a virtual SocketCAN CAN controller and channel, and do not modify
// the current CAN bus state or bitrate.
io::CanController ctrl("vcan0");
io::CanChannel chan(poll, exec);
#endif
chan.open(ctrl);
#if _WIN32
// Create two worker threads to ensure the blocking canChannelReadMessage()
// and canChannelSendMessage() used by the IXXAT CAN channel do not hold up
// the event loop.
std::thread workers[] = {std::thread([&]() { loop.run(); }),
std::thread([&]() { loop.run(); })};
#endif
// Run the event loop until no tasks remain (or the I/O context is shut down).
loop.run();
#if _WIN32
// Wait for the worker threads to finish.
for (auto& worker : workers) worker.join();
#endif
return 0;
}
Open a terminal and build tutorial.cpp
with
g++ -std=c++14 -Wall -Wextra -pedantic -g -O2 \
$(pkg-config --cflags liblely-io2) \
tutorial.cpp -o tutorial \
$(pkg-config --libs liblely-io2)
The pkg-config
lines expand into the compiler options needed to find and link
to the I/O library and its dependencies.
If you test the program by running
./tutorial
it should exit immediately without showing anything. Our main()
function
creates an event loop, which runs until no tasks remain. But since we haven’t
submitted any tasks yet, it stops right away.
If, instead, you see the following error:
terminate called after throwing an instance of 'std::system_error'
what(): CanController: No such device
Aborted (core dumped)
you need to create the vcan0
interface (click
here for
instructions). And if you see
terminate called after throwing an instance of 'std::system_error'
what(): CanController: Operation not permitted
Aborted (core dumped)
the transmit queue length is most likely too small. Increase it with
sudo ip link set vcan0 down
sudo ip link set vcan0 txqueuelen 1000
sudo ip link set vcan0 up
To ensure proper blocking behavior when sending CAN frames with SocketCAN, the
I/O library sets the socket send buffer to its smallest possible value, while
increasing the transmit queue length of the network interface to at least 128
(see section 3.4 in https://rtime.felk.cvut.cz/can/socketcan-qdisc-final.pdf
for why this is necessary). However, changing the transmit queue length requires
the CAP_NET_ADMIN
capability, which our application does not have. Hence the
“Operation not permitted” error.
A CANopen device not only requires access to a CAN channel, but also to a system timer. So let’s create one:
tutorial.cpp
Clean shutdown
Right now, our application still terminates immediately. But once we turn it
into a CANopen device, it will read and process CAN frames indefinitely. The
only way to terminate it then will be by killing the process. It would be much
nicer, however, to be able to perform a clean shutdown. We can do this by
creating a signal handler, which starts the shutdown process when the user
presses Ctrl+C
or the process receives the termination signal:
tutorial.cpp
The signal handler invokes ctx.shutdown()
. This function cancels all pending
read and write operations submitted to the CAN channel as well as any wait
operation submitted to the timer. And since new I/O operations are aborted
before they start, it is only a matter of time before all remaining tasks on the
event loop have been executed and it stops.
Since we have registered a wait operation with the signal handler, our application no longer terminates immediately, but waits for the user to stop it.
The slave
Now that the I/O services have been created, it’s time to turn our program into an CANopen device. This is the point where the master and slave implementations diverge. Let’s start with the slave.
Just like the previous tutorial, we need an electronic data sheet (EDS) or device configuration file (DCF). We will load this file at runtime to create the object dictionary.
It is also possible to use the DCF-to-C tool to generate a device description in C at compile time. This is useful for embedded devices which lack the resources to parse a text file at runtime. But it does require the user to recompile their program every time the EDS/DCF changes.
Download cpp-slave.eds.
This is a simple device description containing two manufacturer-specific
objects: 4000 and 4001, both of type UNSIGNED32
. The first object is mapped to
a Receive-PDO with CAN-ID $NODEID+0x200
. The second to a Transmit-PDO with
CAN-ID $NODEID+0x180
. Both PDOs are synchronous, so the value of object 4001
is sent by the TPDO after the reception of a SYNC message from the master, while
object 4000 is updated by the RPDO after the SYNC. The EDS also contains objects
for heartbeat consumption (1016) and production (1017).
For the first version of the slave we will simply create an instance of the
BasicSlave
class, with node-ID 2. Copy tutorial.cpp
to slave.cpp
and add
the following lines:
slave.cpp
To build the slave, we need to link against the C++ CANopen application library (liblely-coapp):
g++ -std=c++14 -Wall -Wextra -pedantic -g -O2 \
$(pkg-config --cflags liblely-coapp) \
slave.cpp -o slave \
$(pkg-config --libs liblely-coapp)
This library depends on the I/O library, so we don’t need to mention it explicitly.
Running
./slave
shows the following output:
NMT: entering reset application state
NMT: entering reset communication state
NMT: running as slave
NMT: entering pre-operational state
NMT: entering operational state
The slave goes operational by itself and then waits for SYNC messages from the master.
If you happened to run
candump vcan0
in another terminal before starting the slave, you would see a single CAN frame:
vcan0 702 [1] 00
This is the boot-up message for node 2. No other frames are sent, because heartbeat production is disabled by default (as is heartbeat consumption).
It is possible create multiple virtual slave devices in the same process by
adding another canopen::BasicSlave
instance. Note that you need to create a
separate io::Timer
and io::CanChannel
instance for each node. The
io::Context
, io::Poll
, ev::Loop
and io::CanController
instances can be
shared between multiple nodes.
Generating a master DCF
Like the slave, the master needs an EDS or DCF. But unlike the slave, for which we can usually obtain the EDS/DCF from the vendor, we have to create the master DCF ourselves. It is possible to do this by hand, but it is error-prone and a lot of work. Especially if we want to use the “remote PDO mapping” feature (see below). Fortunately, we have a tool for this: dcfgen.
Create the following file:
cpp-tutorial.yml
This is a very simple configuration file, but it’s all dcfgen
needs. Many more
features are supported of course. Download
cpp-tutorial.yml for an
example showing all the supported options.
Create master.dcf
by running
dcfgen -r cpp-tutorial.yml
The -r
flag enables “remote PDO mapping”.
The master
The first version of the master is very similar to the slave. We just create an
instance of AsyncMaster
(with node-ID 1) instead of BasicSlave
:
master.cpp
Some options supported by dcfgen
may cause it to generate a master.bin
file
in addition to master.dcf
. This file contains SDO write requests in the
“concise DCF” format, and is used by the master to configure the slave during
the boot-up process. If such a file is generated, specify its path as the fourth
argument to the AsyncMaster
constructor. This tutorial does not produce such a
file, so we pass an empty string instead.
The fact that this is a master is determined by the least significant bit in
object 1F80 (NMT startup) in master.dcf
. This bit is automatically set by
dcfgen
. Because the slave maps two objects to PDOs, so does the master. But
we’ll get to that later.
Open a new terminal and build the master:
g++ -std=c++14 -Wall -Wextra -pedantic -g -O2 \
$(pkg-config --cflags liblely-coapp) \
master.cpp -o master \
$(pkg-config --libs liblely-coapp)
Running
./master
shows almost the same output as the slave:
NMT: entering reset application state
NMT: entering reset communication state
NMT: running as master
NMT: entering pre-operational state
NMT: entering operational state
But if you run
candump vcan0
before starting the master, the output is a little different:
vcan0 701 [1] 00
vcan0 000 [2] 82 00
Like the previous tutorial, the first two lines show the boot-up message from
the master, followed by the NMT “reset communication” command (82
) to all
nodes (00
). If you still have the slave running in another terminal, you can
see that it received the reset command:
NMT: entering reset communication state
NMT: running as slave
NMT: entering pre-operational state
NMT: entering operational state
while candump
shows the boot-up message:
vcan0 702 [1] 00
The EDS of the master contains an entry for node 2 in object 1F81 (NMT slave assignment), which means it will try to boot the slave. In this case, the boot-up procedure consists of two SDO upload (= read) requests:
vcan0 602 [8] 40 00 10 00 00 00 00 00
vcan0 582 [8] 43 00 10 00 00 00 00 00
vcan0 602 [8] 40 18 10 01 00 00 00 00
vcan0 582 [8] 43 18 10 01 60 03 00 00
The first request checks the device type (object 1000), the second the vendor-ID
(object 1018 sub-index 1) of the slave. Since both values (0 and 0x360) match
the expected values in the EDS of the master (objects 1F84 and 1F85, sub-index
2), the master issues the NMT “start remote node” command (01
) to node 2:
vcan0 000 [2] 01 02
Both the heartbeat producer (object 1017) and SYNC producer (object 1005 and
1006) of the master are configured to send a message every second. Since the
SYNC message triggers the transmission of two PDOs, four frames appear in the
output of candump
every second:
vcan0 701 [1] 05
vcan0 080 [0]
vcan0 202 [4] 00 00 00 00
vcan0 182 [4] 00 00 00 00
This first line shows that the master is operational (NMT state 05
), the
second is the SYNC message (CAN-ID 080
), the third is the TPDO sent by the
master and the fourth the TPDO sent by the slave (see below).
CANopen events
In our application we would like to be notified of and respond to CANopen
events, such as a slave being booted or a SYNC message or PDO being
sent/received. In the C++ CANopen application library, this can be achieved by
overriding the corresponding method, such as OnBoot()
or OnSync()
. We could
do this by creating our own MyMaster
class, which inherits from AsyncMaster
and overrides the event callbacks we’re interested in. However, that means we
have a single method responsible for handling an event for all slaves. When we
have an application with several different types of slaves, this quickly becomes
unwieldy. Instead, we create a driver class, each instance of which is
responsible for a single slave.
The following code shows how to implement a driver and respond to a boot-up event:
master.cpp
Running the master while the slave is running in another terminal shows an extra line of output:
slave 2 booted sucessfully
If the slave is not running, nothing extra is shown. Also not an error, since the behavior prescribed by CiA 302-2 (Network management) is to wait indefinitely for optional slaves.
Configuring a slave
To create our driver, we could have inherited from BasicDriver
. However, that
would require all our event callbacks to be asynchronous or, at the very least,
non-blocking. In the case of OnBoot()
, where we simply print a log message,
that’s not a problem. But if we need to perform an SDO request and wait for the
result, we have to be careful that we don’t hold up the event loop. We could
solve this with futures (a bit like the promises chaining in JavaScript), but
it’s much more convenient and readable to write (pseudo-)blocking code. This is
why our driver inherits from FiberDriver
. FiberDriver
ensures that every
event callback runs inside a
fiber (or stackful
coroutine). Whenever we issue a blocking request, such as an SDO, we simply
suspend the fiber until the request completes.
Instead of FiberDriver
, we could also inherit from LoopDriver
. This class
creates a dedicated event loop for each driver, running in a separate thread.
Blocking requests are handled by running the event loop again while waiting for
the request to complete. For the most part, the result is indistinguishable from
using fibers. But the nested event loops can lead to
priority inversion and even
deadlocks if you’re not careful.
SDOs are commonly used to configure a slave before it becomes operational. In the following code, we show how to use them to configure heartbeat monitoring between the master and slave:
master.cpp
The OnConfig()
event callback follows the boot process flowchart in CiA 302-2.
The OnDeconfig()
callback is Lely-specific. We have added it to allow graceful
shutdown behavior. In this case by disabling heartbeat monitoring, but it can
also be used to put a slave in a known safe state before terminating the master.
Instead of writing code to configure the heartbeat producer and consumer, we
could also use the heartbeat_producer
and heartbeat_consumer
options of
dcfgen
. This would result in a master.bin
file containing the same SDO
requests (in the “concise DCF” format) that we now wrote by hand. The master
would execute these SDO requests right before calling OnConfig()
. We omitted
that feature in this tutorial so we could show you how to do it by hand. But
for a real application it would be better to do as much as possible with
dcfgen
before overriding OnConfig()
.
candump
shows two extra SDO requests, download (= write) requests in this
case, during the boot-up process of the slave:
vcan0 602 [8] 23 16 10 01 D0 07 01 00
vcan0 582 [8] 60 16 10 01 00 00 00 00
vcan0 602 [8] 2B 17 10 00 E8 03 00 00
vcan0 582 [8] 60 17 10 00 00 00 00 00
The first configures the heartbeat consumer (object 1016 sub-index 1) of the
slave, the second the heartbeat producer (object 1017). Similarly, after
terminating the master with Ctrl+C
, two more SDO requests appear, which
disable heartbeat monitoring:
vcan0 602 [8] 2B 17 10 00 00 00 00 00
vcan0 582 [8] 60 17 10 00 00 00 00 00
vcan0 602 [8] 23 16 10 01 00 00 00 00
vcan0 582 [8] 60 16 10 01 00 00 00 00
PDO ping-pong
Like the slave, the master has two manufacturer-specific objects of type
UNSIGNED32
: 2000 and 2200, mapped to an RPDO and TPDO, respectively. The
CAN-ID of the RPDO (182
) is equal to that of the TPDO of the slave, and the
CAN-ID of the TPDO (202
) is equal to the RPDO of the slave. So the value of
object 4001 on the slave is copied into a PDO with CAN-ID 182
, sent by the
slave and received by the master after a SYNC message, and copied into object
2000 on the master. Similary, object 2200 on the master is sent to object 4000
on the slave via a PDO with CAN-ID 202
(sent by the master).
Sending a PDO and processing a received PDO only happen after a SYNC (in the case of synchronous PDOs). This means that it takes two SYNC periods for a change on one of the devices to be communicated to the other. For example, if we change object 4001 on the slave, the value is sent after the next SYNC, it is recevied by the master and held in a buffer until the SYNC after that, when it is processed and copied to object 2000 on the master.
To play PDO ping-pong, we change the slave to copy the value of object 4000 to
object 4001 every time we receive a PDO. We have to create our own slave class to
be able to override the OnWrite()
event callback, which is invoked when an
object in the object dictionary is modified:
slave.cpp
Alternatively, we could override the OnSync()
method, which is invoked after
any received PDOs have been processed.
In the master we do something similar, except that we increment the value before copying it. Otherwise we would never see any change. Also, we don’t directly access the local object dictionary. The fact that object 4000 and 4001 on the slave are mapped to object 2200 and 2000 on the master is an implementation detail we’d rather ignore. Instead, we want to use the object indices of the slave in the code of the master.
This does not seem to be of great benefit in this particular example. However, most CANopen slaves follow a particular device profile. In that case, being able to refer to official, standardized object indices, instead of whatever object they happen to be mapped to on the master, greatly simplifies writing generic drivers.
This “remote PDO mapping” is a Lely-specific feature, implemented in the C++
CANopen application library. It requires the user (or rather dcfgen
) to copy
the PDO mapping on the slave (objects 1600 and 1A00) to the master (object 5A00
and 5E00, respectively), as well as specifying the origin of the PDOs (objects
5800 and 5C00).
Add the OnRpdoWrite()
event callback to the master:
master.cpp
We also reset the values of objects 4000 and 4001 in OnConfig()
, so we start
with a clean slate each time.
When running both applications, candump
shows
vcan0 080 [0]
vcan0 202 [4] 00 00 00 00
vcan0 182 [4] 00 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 00 00 00 00
vcan0 182 [4] 00 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 01 00 00 00
vcan0 182 [4] 00 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 01 00 00 00
vcan0 182 [4] 00 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 01 00 00 00
vcan0 182 [4] 01 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 01 00 00 00
vcan0 182 [4] 01 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 02 00 00 00
vcan0 182 [4] 01 00 00 00
...
vcan0 080 [0]
vcan0 202 [4] 02 00 00 00
vcan0 182 [4] 01 00 00 00
where we’ve filtered out the heartbeat messages.
Object 2200 on the master (sent via PDO 202
) and 4001 on the slave (sent via
PDO 182
) start out at 0. The master receives the 0 from object 4001 after the
second SYNC, when it increments the value of object 2200. So only on the third
SYNC do we see a non-zero value. This value is processed by the slave after the
fourth SYNC and sent back to the master after the fifth. All in all, it takes
four SYNC periods for an object to be incremented.
This concludes the C++ tutorial. There are many more CANopen events that may need to be handled in a non-trivial application, but they all follow the same pattern shown here.