CANopen

Overview

This sample application shows how the CANopenNode CANopen protocol stack can be used in Zephyr.

CANopen is an internationally standardized (EN 50325-4, CiA 301) communication protocol and device specification for embedded systems used in automation. CANopenNode is a 3rd party, open-source CANopen protocol stack.

Apart from the CANopen protocol stack integration, this sample also demonstrates the use of non-volatile storage for the CANopen object dictionary and optionally program download over CANopen.

Requirements

  • A board with CAN bus and flash support

  • Host PC with CAN bus support

Building and Running

Building and Running for TWR-KE18F

The NXP TWR-KE18F board is equipped with an onboard CAN transceiver. This board supports CANopen LED indicators (red and green LEDs). The sample can be built and executed for the TWR-KE18F as follows:

west build -b twr_ke18f samples/subsys/canbus/canopen
west flash

Pressing the button labelled SW3 will increment the button press counter object at index 0x2102 in the object dictionary.

Building and Running for FRDM-K64F

The NXP FRDM-K64F board does not come with an onboard CAN transceiver. In order to use the CAN bus on the FRDM-K64F board, an external CAN bus tranceiver must be connected to PTB18 (CAN0_TX) and PTB19 (CAN0_RX). This board supports CANopen LED indicators (red and green LEDs)

The sample can be built and executed for the FRDM-K64F as follows:

west build -b frdm_k64f samples/subsys/canbus/canopen
west flash

Pressing the button labelled SW3 will increment the button press counter object at index 0x2102 in the object dictionary.

Testing CANopen Communication

CANopen communication between the host PC and Zephyr can be established using any CANopen compliant application on the host PC. The examples here uses CANopen for Python for communicating between the host PC and Zephyr. First, install python-canopen along with the python-can backend as follows:

pip3 install --user canopen python-can

Next, configure python-can to use your CAN adapter through its configuration file. On GNU/Linux, the configuration looks similar to this:

cat << EOF > ~/.canrc
[default]
interface = socketcan
channel = can0
bitrate = 125000
EOF

Please refer to the python-can documentation for further details and instructions.

Finally, bring up the CAN interface on the test PC. On GNU/Linux, this can be done as follows:

sudo ip link set can0 type can bitrate 125000 restart-ms 100
sudo ip link set up can0

To better understand the communication taking place in the following examples, you can monitor the CAN traffic from the host PC. On GNU/Linux, this can be accomplished using candump from the can-utils package as follows:

candump can0

NMT State Changes

Changing the Network Management (NMT) state of the node can be accomplished using the following Python code:

import canopen
import os
import time

ZEPHYR_BASE = os.environ['ZEPHYR_BASE']
EDS = os.path.join(ZEPHYR_BASE, 'samples', 'subsys', 'canbus', 'canopen',
                   'objdict', 'objdict.eds')
NODEID = 10

network = canopen.Network()

network.connect()

node = network.add_node(NODEID, EDS)

# Green indicator LED will flash slowly
node.nmt.state = 'STOPPED'
time.sleep(5)

# Green indicator LED will flash faster
node.nmt.state = 'PRE-OPERATIONAL'
time.sleep(5)

# Green indicator LED will be steady on
node.nmt.state = 'OPERATIONAL'
time.sleep(5)

# Node will reset communication
node.nmt.state = 'RESET COMMUNICATION'
node.nmt.wait_for_heartbeat()

# Node will reset
node.nmt.state = 'RESET'
node.nmt.wait_for_heartbeat()

network.disconnect()

Running the above Python code will update the NMT state of the node which is reflected on the indicator LEDs (if present).

SDO Upload

Reading a Service Data Object (SDO) at a given index of the CANopen object dictionary (here index 0x1008, the manufacturer device name) can be accomplished using the following Python code:

import canopen
import os

ZEPHYR_BASE = os.environ['ZEPHYR_BASE']
EDS = os.path.join(ZEPHYR_BASE, 'samples', 'subsys', 'canbus', 'canopen',
                   'objdict', 'objdict.eds')
NODEID = 10

network = canopen.Network()

network.connect()

node = network.add_node(NODEID, EDS)
name = node.sdo['Manufacturer device name']

print("Device name: '{}'".format(name.raw))

network.disconnect()

Running the above Python code should produce the following output:

Device name: 'Zephyr RTOS/CANopenNode'

SDO Download

Writing to a Service Data Object (SDO) at a given index of the CANopen object dictionary (here index 0x1017, the producer heartbeat time) can be accomplished using the following Python code:

import canopen
import os

ZEPHYR_BASE = os.environ['ZEPHYR_BASE']
EDS = os.path.join(ZEPHYR_BASE, 'samples', 'subsys', 'canbus', 'canopen',
                   'objdict', 'objdict.eds')
NODEID = 10

network = canopen.Network()

network.connect()

node = network.add_node(NODEID, EDS)
heartbeat = node.sdo['Producer heartbeat time']
reboots = node.sdo['Power-on counter']

# Set heartbeat interval without saving to non-volatile storage
print("Initial heartbeat time: {} ms".format(heartbeat.raw))
print("Power-on counter: {}".format(reboots.raw))
heartbeat.raw = 5000
print("Updated heartbeat time: {} ms".format(heartbeat.raw))

# Reset and read heartbeat interval again
node.nmt.state = 'RESET'
node.nmt.wait_for_heartbeat()
print("heartbeat time after reset: {} ms".format(heartbeat.raw))
print("Power-on counter: {}".format(reboots.raw))

# Set interval and store it to non-volatile storage
heartbeat.raw = 2000
print("Updated heartbeat time: {} ms".format(heartbeat.raw))
node.store()

# Reset and read heartbeat interval again
node.nmt.state = 'RESET'
node.nmt.wait_for_heartbeat()
print("heartbeat time after store and reset: {} ms".format(heartbeat.raw))
print("Power-on counter: {}".format(reboots.raw))

# Restore default values, reset and read again
node.restore()
node.nmt.state = 'RESET'
node.nmt.wait_for_heartbeat()
print("heartbeat time after restore and reset: {} ms".format(heartbeat.raw))
print("Power-on counter: {}".format(reboots.raw))

network.disconnect()

Running the above Python code should produce the following output:

Initial heartbeat time: 1000 ms
Power-on counter: 1
Updated heartbeat time: 5000 ms
heartbeat time after reset: 1000 ms
Power-on counter: 2
Updated heartbeat time: 2000 ms
heartbeat time after store and reset: 2000 ms
Power-on counter: 3
heartbeat time after restore and reset: 1000 ms
Power-on counter: 4

Note that the power-on counter value may be different.

PDO Mapping

Transmit Process Data Object (PDO) mapping for data at a given index of the CANopen object dictionary (here index 0x2102, the button press counter) can be accomplished using the following Python code:

import canopen
import os

ZEPHYR_BASE = os.environ['ZEPHYR_BASE']
EDS = os.path.join(ZEPHYR_BASE, 'samples', 'subsys', 'canbus', 'canopen',
                   'objdict', 'objdict.eds')
NODEID = 10

network = canopen.Network()

network.connect()

node = network.add_node(NODEID, EDS)
button = node.sdo['Button press counter']

# Read current TPDO mapping
node.tpdo.read()

# Enter pre-operational state to map TPDO
node.nmt.state = 'PRE-OPERATIONAL'

# Map TPDO 1 to transmit the button press counter on changes
node.tpdo[1].clear()
node.tpdo[1].add_variable('Button press counter')
node.tpdo[1].trans_type = 254
node.tpdo[1].enabled = True

# Save TPDO mapping
node.tpdo.save()
node.nmt.state = 'OPERATIONAL'

# Reset button press counter
button.raw = 0

print("Press the button 10 times")
while True:
    node.tpdo[1].wait_for_reception()
    print("Button press counter: {}".format(node.tpdo['Button press counter'].phys))
    if node.tpdo['Button press counter'].phys >= 10:
        break

network.disconnect()

Running the above Python code should produce the following output:

Press the button 10 times
Button press counter: 0
Button press counter: 1
Button press counter: 2
Button press counter: 3
Button press counter: 4
Button press counter: 5
Button press counter: 6
Button press counter: 7
Button press counter: 8
Button press counter: 9
Button press counter: 10

Testing CANopen Program Download

Building and Running for FRDM-K64F

The sample can be rebuilt with MCUboot and program download support for the FRDM-K64F as follows:

  1. Build and flash MCUboot by following the instructions in the MCUboot documentation page.

  2. Rebuild the CANopen sample with MCUboot support:

    west build -b frdm_k64f samples/subsys/canbus/canopen -- -DCONFIG_BOOTLOADER_MCUBOOT=y
    
  3. Sign the newly rebuilt CANopen sample binary (using either the demonstration-only RSA key from MCUboot or any other key used when building MCUboot itself):

    west sign -t imgtool --bin --no-hex -- --key mcuboot/root-rsa-2048.pem \
            --version 1.0.0
    
  4. Flash the newly signed CANopen sample binary using west:

    west flash --skip-rebuild --bin-file zephyr/zephyr.signed.bin
    
  5. Confirm the newly flashed firmware image using west:

    west flash --skip-rebuild --runner canopen --confirm-only
    
  6. Finally, resign the CANopen sample binary with a new version number and perform a program download over CANopen:

    west sign -t imgtool --bin --no-hex  -- --key mcuboot/root-rsa-2048.pem \
            --version 1.0.1
    west flash --skip-rebuild --bin-file zephyr/zephyr.signed.bin \
            --runner canopen
    

Modifying the Object Dictionary

The CANopen object dictionary used in this sample application can be found under samples/subsys/canbus/canopen/objdict in the Zephyr tree. The object dictionary can be modified using any object dictionary editor supporting CANopenNode object dictionary code generation.

A popular choice is the EDS editor from the libedssharp project. With that, the samples/subsys/canbus/canopen/objdict/objdicts.xml project file can be opened and modified, and new implementation files (samples/subsys/canbus/canopen/objdict/CO_OD.h and samples/subsys/canbus/canopen/objdict/CO_OD.c) can be generated. The EDS editor can also export an updated Electronic Data Sheet (EDS) file (samples/subsys/canbus/canopen/objdict/objdicts.eds).