Quickstart

This quickstart example describes in detail the steps to build a CANopen node. The source files are included in the directory example/quickstart/app and should be followed during reading this article. In this example, we will create a CANopen Clock. While this clock node is not intended for serious applications, the example illustrates the key principles of development using the CANopen Stack.

Functional Specification

The CANopen clock will only run if the device is switched to operational mode. The object dictionary will be updated every second with the new time. In operational mode, the node will transmit a process data object (TPDO) whenever the object dictionary entry “second” (2100h:03) is changed. The service data object server (SDOS) will allow access to the device information contained within the object dictionary.

Object Dictionary

To keep the software as simple as possible, we will use a static object dictionary. In this case, the object dictionary is an array of object entries, declared as a constant array of object entries of type CO_OBJ. The object dictionary is declared in the file clock_spec.c:

  :
/* define the static object dictionary */
const CO_OBJ ClockOD[] = {
        :
    /* here is your object dictionary */
        :
    CO_OBJ_DIR_ENDMARK  /* mark end of used objects */
};
/* set number of object entries */
const uint16_t ClockODLen = sizeof(ClockOD)/sizeof(CO_OBJ);
  :

Mandatory Object Entries

The object dictionary must hold at least the following mandatory object entries:

Index:sub Type Access Value Description
1000h:00 UNSIGNED32 Const 0 Device Type
1001h:00 UNSIGNED8 Read-only 0 Error Register
1005h:00 UNSIGNED32 Const 0x80 COB-ID SYNC
1017h:00 UNSIGNED16 Const 0 Heartbeat Producer
1018h:00 UNSIGNED8 Const 4 Identity Object
1018h:01 UNSIGNED32 Const 0 - Vendor ID
1018h:02 UNSIGNED32 Const 0 - Product code
1018h:03 UNSIGNED32 Const 0 - Revision number
1018h:04 UNSIGNED32 Const 0 - Serial number

Note: For complex object dictionary entries (e.g. 1018h), subindex 0 holds the highest subindex in this object.

Note: The values shown for 1018h are only dummy values. Vendor IDs are managed by CiA, and each registered company is assigned a globally unique value.

These mandatory entries are added with the following lines of code:

  :
    {CO_KEY(0x1000, 0, CO_UNSIGNED32|CO_OBJ_D__R_), 0, (uintptr_t)0},
    {CO_KEY(0x1001, 0, CO_UNSIGNED8 |CO_OBJ____R_), 0, (uintptr_t)&Obj1001_00_08},
    {CO_KEY(0x1005, 0, CO_UNSIGNED32|CO_OBJ_D__R_), 0, (uintptr_t)0x80},
    {CO_KEY(0x1017, 0, CO_UNSIGNED16|CO_OBJ_D__R_), 0, (uintptr_t)0},

    {CO_KEY(0x1018, 0, CO_UNSIGNED8 |CO_OBJ_D__R_), 0, (uintptr_t)4},
    {CO_KEY(0x1018, 1, CO_UNSIGNED32|CO_OBJ_D__R_), 0, (uintptr_t)0},
    {CO_KEY(0x1018, 2, CO_UNSIGNED32|CO_OBJ_D__R_), 0, (uintptr_t)0},
    {CO_KEY(0x1018, 3, CO_UNSIGNED32|CO_OBJ_D__R_), 0, (uintptr_t)0},
    {CO_KEY(0x1018, 4, CO_UNSIGNED32|CO_OBJ_D__R_), 0, (uintptr_t)0},
  :

Attention: The CANopen Stack relies on a binary search algorithm to ensure that object dictionary entries are found quickly. Because of this, you must keep the index / subindex of all entries in the object dictionary sorted in ascending order.

Most of these entries are constant, and their values can be stored directly inside the object dictionary. This is not the case for the error register object 1001h. This entry is read-only, but can be changed by the node. We need to declare a global variable to contain the runtime value of the entry:

uint8_t Obj1001_00_08 = 0;

A pointer to this variable is stored in the corresponding object dictionary entry. This entry must be marked as CO_OBJ____R_ instead of CO_OBJ_D__R_ to let the stack know that this entry contains a pointer instead of a direct value.

SDO Server

The settings for the SDO server are defined in CiA301 and must contain the following object dictionary entries:

Index:sub Type Access Value Description
1200h:00 UNSIGNED8 Const 2 Communication Object SDO Server
1200h:01 UNSIGNED32 Const 600h + node ID - SDO Server Request COBID
1200h:02 UNSIGNED32 Const 580h + node ID - SDO Server Response COBID

Note: The predefined COBIDs are dependent on the actual node ID. For this reason, the CANopen stack allows you to specify entries whose value depends on the current node ID at runtime. This behavior is specified using the CO_OBJ__N___ flag.

The following lines add the SDO server entries to the object dictionary:

  :
    {CO_KEY(0x1200, 0, CO_UNSIGNED8 |CO_OBJ_D__R_), 0, (uintptr_t)2},
    {CO_KEY(0x1200, 1, CO_UNSIGNED32|CO_OBJ_DN_R_), 0, CO_COBID_SDO_REQUEST()},
    {CO_KEY(0x1200, 2, CO_UNSIGNED32|CO_OBJ_DN_R_), 0, CO_COBID_SDO_RESPONSE()},
  :

Application Object Entries

Next, we need to add some application-specific object entries to support the clock-functionality of the example:

Index:sub Type Access Value Description
2100h:00 UNSIGNED8 Const 3 Clock Object
2100h:01 UNSIGNED32 Read Only 0 - Hour
2100h:02 UNSIGNED8 Read Only 0 - Minute
2100h:03 UNSIGNED8 Read Only 0 - Second

Note: These entries are placed within the manufacturer-specific area (from 2000h up to 5FFFh) and can be chosen freely (see CiA301). Entries outside of this range cannot be chosen freely, and should conform to the various CiA standards and profiles (e.g. CiA301 for communication profile area, CiA401 for generic IO modules, etc).

These entries are created using the following lines of code:

  :
    {CO_KEY(0x2100, 0, CO_UNSIGNED8 |CO_OBJ_D__R_), 0, (uintptr_t)3},
    {CO_KEY(0x2100, 1, CO_UNSIGNED32|CO_OBJ___PR_), 0, (uintptr_t)&Obj2100_01_20},
    {CO_KEY(0x2100, 2, CO_UNSIGNED8 |CO_OBJ___PR_), 0, (uintptr_t)&Obj2100_02_08},
    {CO_KEY(0x2100, 3, CO_UNSIGNED8 |CO_OBJ___PR_), CO_TASYNC, (uintptr_t)&Obj2100_03_08},
  :

Note: The type CO_TASYNC for the object entry 2100h:03 indicates the transmission of all PDOs, which includes this object. We use this mechanism to achieve the PDO transmission on each write access to the second.

We also need to create three global variables to hold the runtime values of the clock object:

uint32_t Obj2100_01_20 = 0;
uint8_t  Obj2100_02_08 = 0;
uint8_t  Obj2100_03_08 = 0;

TPDO Communication

The communication settings for the TPDO must contain the following object entries:

Index:sub Type Access Value Description
1800h:00 UNSIGNED8 Const 2 Communication Object TPDO #0
1800h:01 UNSIGNED32 Const 40000180h - PDO transmission COBID (no RTR)
1800h:02 UNSIGNED8 Const 254 - PDO transmission type

Note: The CANopen stack does not support remote CAN frames as they are no longer recommended for new devices. The use of RTR frames in CANopen devices has been deprecated for many years now. Bit 30 in 1800h:01 indicates that remote transfers are not allowed for this PDO. The CAN identifier 180h + node-ID is the recommended value from the pre-defined connection set.

See the following lines in the object dictionary:

  :
    {CO_KEY(0x1800, 0, CO_UNSIGNED8 |CO_OBJ_D__R_), 0, (uintptr_t)2},
    {CO_KEY(0x1800, 1, CO_UNSIGNED32|CO_OBJ_DN_R_), 0, CO_COBID_TPDO_DEFAULT(0)},
    {CO_KEY(0x1800, 2, CO_UNSIGNED8 |CO_OBJ_D__R_), 0, (uintptr_t)254},
  :

TPDO Data Mapping

The mapping settings for the TPDO must contain the following object entries:

Index:sub Type Access Value Description
1A00h:00 UNSIGNED8 Const 3 Mapping Object TPDO #0
1A00h:01 UNSIGNED32 Const 21000120h - map: 32-bit clock hour
1A00h:02 UNSIGNED32 Const 21000208h - map: 8-bit clock minute
1A00h:03 UNSIGNED32 Const 21000308h - map: 8-bit clock second

How we get these values is explained in section configuration of PDO mapping. This way of defining the payload for PDOs is part of the CiA301 standard and leads us to the following lines in the object dictionary:

  :
    {CO_KEY(0x1A00, 0, CO_UNSIGNED8 |CO_OBJ_D__R_), 0, (uintptr_t)3},
    {CO_KEY(0x1A00, 1, CO_UNSIGNED32|CO_OBJ_D__R_), 0, CO_LINK(0x2100, 0x01, 32)},
    {CO_KEY(0x1A00, 2, CO_UNSIGNED32|CO_OBJ_D__R_), 0, CO_LINK(0x2100, 0x02,  8)},
    {CO_KEY(0x1A00, 3, CO_UNSIGNED32|CO_OBJ_D__R_), 0, CO_LINK(0x2100, 0x03,  8)},
  :

Node Specification

The main settings of the node are configured inside the CO_NODE_SPEC struct. This struct will not be modified at runtime, so it can be declared as a constant, reducing RAM usage. The CANopen node needs some memory buffers for dynamic operations. These two buffers are statically allocated, and a pointer is stored inside the CO_NODE_SPEC struct. Refer to the file clock_spec.c for more detail:

#include "co_core.h"
    :
/* Define some default values for our CANopen node: */
#define APP_NODE_ID       1u                  /* CANopen node ID             */
#define APP_BAUDRATE      250000u             /* CAN baudrate                */
#define APP_TMR_N         16u                 /* Number of software timers   */
#define APP_TICKS_PER_SEC 1000u               /* Timer clock frequency in Hz */
#define APP_OBJ_N         128u                /* Object dictionary max size  */
    :
/* Each software timer needs some memory for managing
 * the lists and states of the timed action events.
 */
CO_TMR_MEM TmrMem[APP_TMR_N];

/* Each SDO server needs memory for the segmented or
 * block transfer requests.
 */
uint8_t SdoSrvMem[CO_SDOS_N * CO_SDO_BUF_BYTE];
    :
/* Collect all node specification settings in a single
 * structure for initializing the node easily.
 */
CO_NODE_SPEC AppSpec = {
    APP_NODE_ID,             /* default Node-Id                */
    APP_BAUDRATE,            /* default Baudrate               */
    (CO_OBJ *)&ClockOD[0],   /* pointer to object dictionary   */
    APP_OBJ_N,               /* object dictionary max length   */
    NULL,                    /* no EMCY info fields (unused)   */
    &TmrMem[0],              /* pointer to timer memory blocks */
    APP_TMR_N,               /* number of timer memory blocks  */
    APP_TICKS_PER_SEC,       /* timer clock frequency in Hz    */
    (CO_IF_DRV *)&AppDriver, /* select drivers for application */
    &SdoSrvMem[0]            /* SDO Transfer Buffer Memory     */
};

Warning: Never manipulate these variables directly. Doing so will cause problems.

Application Start

The application code is implemented in the file clock_app.c. This file is responsible for the CANopen Stack startup as well as the application-specific clock function. Let’s start with the CANopen Stack startup:

#include "co_core.h"

/* get external node specification */
extern const CO_NODE_SPEC AppSpec;

/* Allocate a global CANopen node object */
CO_NODE Clk;

/* main entry function */
void main(int argc, char *argv[])
{
    uint32_t ticks;

    /* Initialize your hardware layer and the CANopen stack.
     * Stop execution if an error is detected.
     */
    /* BSPInit(); */
    CONodeInit(&Clk, (CO_NODE_SPEC *)&AppSpec);
    if (CONodeGetErr(&Clk) != CO_ERR_NONE) {
        while(1);
    }

    /* Use CANopen software timer to create a cyclic function
     * call to the callback function 'AppClock()' with a period
     * of 1s (equal: 1000ms).
     */
    ticks = COTmrGetTicks(&Clk.Tmr, 1000, CO_TMR_UNIT_1MS);
    COTmrCreate(&Clk.Tmr, 0, ticks, AppClock, &Clk);

    /* Start the CANopen node and set it automatically to
     * NMT mode: 'OPERATIONAL'.
     */
    CONodeStart(&Clk);
    CONmtSetMode(&Clk.Nmt, CO_OPERATIONAL);

    /* In the background loop we process received CAN frames
     * and executes elapsed action callback functions.
     */
    while (1) {
        CONodeProcess(&Clk);
        COTmrProcess(&Clk.Tmr);
    }
}

Application Callback

The timer callback function AppClock() includes the main functionality of the clock node:

/* timer callback function */
static void AppClock(void *p_arg)
{
    CO_NODE  *node;
    CO_OBJ   *od_sec;
    CO_OBJ   *od_min;
    CO_OBJ   *od_hr;
    uint8_t   second;
    uint8_t   minute;
    uint32_t  hour;

    /* For flexible usage (not needed, but nice to show), we use the argument
     * as reference to the CANopen node object. If no node is given, we ignore
     * the function call by returning immediatelly.
     */
    node = (CO_NODE *)p_arg;
    if (node == 0) {
        return;
    }

    /* Main functionality: when we are in operational mode, we get the current
     * clock values out of the object dictionary, increment the seconds and
     * update all clock values in the object dictionary.
     */
    if (CONmtGetMode(&node->Nmt) == CO_OPERATIONAL) {

        od_sec = CODictFind(&node->Dict, CO_DEV(0x2100, 3));
        od_min = CODictFind(&node->Dict, CO_DEV(0x2100, 2));
        od_hr  = CODictFind(&node->Dict, CO_DEV(0x2100, 1));

        COObjRdValue(od_sec, node, (void *)&second, sizeof(second), 0);
        COObjRdValue(od_min, node, (void *)&minute, sizeof(minute), 0);
        COObjRdValue(od_hr , node, (void *)&hour  , sizeof(hour),   0);

        second++;
        if (second >= 60) {
            second = 0;
            minute++;
        }
        if (minute >= 60) {
            minute = 0;
            hour++;
        }

        COObjWrValue(od_hr , node, (void *)&hour  , sizeof(hour),   0);
        COObjWrValue(od_min, node, (void *)&minute, sizeof(minute), 0);
        COObjWrValue(od_sec, node, (void *)&second, sizeof(second), 0);
    }
}

Note: The last write access with COObjWrValue() triggers the transmission of the PDO, because the corresponding object entry is defined as type CO_TASYNC.

Hardware Connection

Next, you will need to add drivers to your project to interface the stack with your hardware, as shown in clock_spec.c:

   :
                                              /* select application drivers: */
#include "co_can_dummy.h"                     /* CAN driver                  */
#include "co_timer_dummy.h"                   /* Timer driver                */
#include "co_nvm_dummy.h"                     /* NVM driver                  */
   :
/* Select the drivers for your application. For possible
 * selections, see the directory /drivers. In this example
 * we select the driver templates. You may fill them with
 * your specific hardware functionality.
 */
const CO_IF_DRV AppDriver = {
    &DummyCanDriver,
    &DummyTimerDriver,
    &DummyNvmDriver
};
   :

When you write your device driver, you will need to set up a hardware timer interrupt within your low-level layer - the Board Support Package (BSP) - and configure a periodic interrupt source with a frequency of APP_TICKS_PER_SEC. The timer interrupt service handler should look something like this:

#include "co_core.h"
   :
/* get external CANopen node */
extern CO_NODE Clk;
   :
void App_TmrIsrHandler(void)
{
    /* collect elapsed timed actions */
    COTmrService(&Clk.Tmr);
}