Chapter 159: CANopen Protocol Basics
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamentals of the CANopen protocol and its role in industrial automation and embedded systems.
- Describe the core CANopen concepts: Object Dictionary (OD), Service Data Objects (SDOs), and Process Data Objects (PDOs).
- Explain the purpose and function of key CANopen communication objects: Network Management (NMT), Synchronization (SYNC), Emergency (EMCY), PDOs, SDOs, and Heartbeat.
- Understand the structure of CANopen Communication Object Identifiers (COB-IDs) and their relation to standard 11-bit CAN identifiers.
- Configure the ESP32’s TWAI peripheral (or an external CAN controller) for basic CANopen communication.
- Conceptually understand how to implement the transmission and reception of basic CANopen messages like Heartbeat, SDOs, and PDOs.
- Recognize differences in implementing CANopen solutions across various ESP32 variants.
- Identify common issues and troubleshooting techniques when working with ESP32 and CANopen.
Introduction
In previous chapters, we’ve explored direct CAN communication using the ESP32’s TWAI peripheral, including its application in automotive protocols like OBD-II and J1939. While these protocols are tailored for vehicle diagnostics and networking, the industrial automation world often relies on a different, highly versatile higher-layer protocol built upon CAN: CANopen.
CANopen, standardized by CAN in Automation (CiA), primarily as CiA 301 (application layer and communication profile), provides a framework for creating interoperable and configurable distributed control systems. It’s widely used in diverse fields such as factory automation, robotics, medical devices, motion control, maritime electronics, and building automation. CANopen defines not just how messages are sent, but also how devices describe themselves, their data, and their behavior through a standardized structure called the Object Dictionary.
The ESP32, with its robust processing capabilities, integrated peripherals (including TWAI for many variants), and networking features, can serve as a powerful node in a CANopen network, whether as a simple I/O device, a sensor/actuator controller, or even a more complex control unit. This chapter will introduce the basic principles of the CANopen protocol, laying the groundwork for developing CANopen-compliant applications with the ESP32. Due to the extensive nature of the full CANopen specification, we will focus on the foundational concepts and illustrative examples of basic message handling.
Theory
CANopen Architecture (CiA 301 and Device Profiles)
CANopen is more than just a set of message definitions; it’s a comprehensive communication profile that specifies:
- Application Layer (CiA 301): Defines the core communication mechanisms, including:
- The structure and access methods for the Object Dictionary.
- Protocols for SDOs (Service Data Objects) and PDOs (Process Data Objects).
- Network Management (NMT) services.
- Special function objects like SYNC, EMCY, TIME, and Heartbeat.
- Device Profiles (CiA 4xx series, etc.): Standardize the Object Dictionary entries and behavior for specific classes of devices. This ensures interoperability between devices from different manufacturers. Examples include:
CiA 401
: Generic I/O modules.CiA 402
: Drives and motion controllers.CiA 404
: Measuring devices and closed-loop controllers.CiA 406
: Encoders.
A CANopen network typically consists of one or more NMT master devices and several NMT slave devices.
The Object Dictionary (OD)
The Object Dictionary (OD) is the heart of every CANopen device. It is essentially a structured table containing all data relevant to that device, including:
- Configuration parameters: Baud rate, Node-ID, PDO mapping, SDO server parameters.
- Application parameters: Device-specific settings, calibration data.
- Process data: Real-time sensor inputs, actuator outputs.
- Communication parameters: Definitions for PDOs, SDOs, EMCY, etc.
Structure of an OD Entry: Each entry in the OD is addressed by a 16-bit Index and, if the entry represents a more complex data structure like an array or record, an 8-bit Sub-Index.
| Field | Description |
| Index | 16-bit address of the object (e.g., 0x1000 for Device Type). |
| Sub-Index | 8-bit address for elements within an array or record (0 for simple objects). |
| Object Code | Defines if the object is a simple variable, array, or record. |
| Name | A descriptive name (optional, for documentation). |
| Data Type | Standardized data type (e.g., BOOLEAN, INTEGER16, UNSIGNED32, VISIBLE_STRING, DOMAIN). |
| Access Rights | RO (Read-Only), WO (Write-Only), RW (Read-Write), CONST. |
| Value | The actual data stored in the object. |
Standardized OD Index Ranges:
0x1000 - 0x1FFF
: Communication Profile Area (e.g., device type, error register, SDO/PDO communication parameters, NMT parameters). These are largely defined by CiA 301.0x2000 - 0x5FFF
: Manufacturer-Specific Profile Area (defined by the device vendor).0x6000 - 0x9FFF
: Standardized Device Profile Area (defined by device profiles like CiA 401, 402).
Communication Object Identifiers (COB-IDs)
CANopen primarily uses 11-bit CAN identifiers. These are referred to as Communication Object Identifiers (COB-IDs). A default COB-ID allocation scheme is defined, which combines a Function Code (4 bits) and the device’s Node-ID (7 bits).
Default COB-ID Structure: Function Code (bits 10-7) | Node-ID (bits 6-0)
- Node-ID: A unique 7-bit address (1-127) for each device on the CANopen network. Node-ID 0 is reserved for NMT master messages to all nodes or for unconfigured devices.
Pre-defined Default COB-IDs (CiA 301):
Service / Object | Function Code (Hex) | COB-ID Formula (Decimal) | CAN ID Range (Hex) |
---|---|---|---|
NMT Control | 0x0 | 0 | 0x000 (Broadcast) |
SYNC | 0x1 | 128 | 0x080 (Broadcast) |
EMCY | 0x1 | 128 + Node-ID | 0x081 – 0x0FF |
TIME Stamp | 0x2 | 256 | 0x100 (Broadcast) |
TPDO1 | 0x3 | 384 + Node-ID | 0x181 – 0x1FF |
RPDO1 | 0x4 | 512 + Node-ID | 0x201 – 0x27F |
TPDO2 | 0x5 | 640 + Node-ID | 0x281 – 0x2FF |
RPDO2 | 0x6 | 768 + Node-ID | 0x301 – 0x37F |
TPDO3 | 0x7 | 896 + Node-ID | 0x381 – 0x3FF |
RPDO3 | 0x8 | 1024 + Node-ID | 0x401 – 0x47F |
TPDO4 | 0x9 | 1152 + Node-ID | 0x481 – 0x4FF |
RPDO4 | 0xA | 1280 + Node-ID | 0x501 – 0x57F |
SDO (Server TX / Client RX) | 0xB | 1408 + Node-ID | 0x581 – 0x5FF |
SDO (Server RX / Client TX) | 0xC | 1536 + Node-ID | 0x601 – 0x67F |
Heartbeat / NMT Error Control | 0xE | 1792 + Node-ID | 0x701 – 0x77F |
LSS | N/A | N/A | 0x7E4, 0x7E5 |
Note: “Node-ID group” refers to how the function code bits are combined with the Node-ID. For example, EMCY uses function code 0001_2
(binary), so its COB-ID is 0x080 + Node-ID
. The COB-IDs for SDOs and PDOs can be reconfigured via the Object Dictionary.
Key CANopen Services and Protocols
- Network Management (NMT):
- Controls the communication state of CANopen slave devices.
- An NMT Master sends commands to NMT Slaves.
- NMT States:
- Initialization: Device performs self-test and basic initialization. Transitions to Pre-Operational.
- Pre-Operational: SDO communication is possible for configuration. PDO communication is disabled. Device can send Heartbeat/Boot-up message.
- Operational: Full communication, including PDOs, is active.
- Stopped: Device only listens for NMT commands. No SDO/PDO communication (except Heartbeat).
- NMT Commands (COB-ID
0x000
):- Data:
[Command Specifier (CS) | Node-ID (0 for all)]
CS=1
: Start Remote Node (Operational)CS=2
: Stop Remote Node (Stopped)CS=128 (0x80)
: Enter Pre-OperationalCS=129 (0x81)
: Reset Node (re-initializes application)CS=130 (0x82)
: Reset Communication (re-initializes CANopen communication parameters)
- Data:
- Boot-up Message: After initialization, a device sends one NMT Heartbeat message with its Node-ID and state 0 (Boot-up event) using COB-ID
0x700 + Node-ID
.
graph TD subgraph CANopen NMT Slave State Machine direction TB A[Initializing] -- "Initialization<br>Finished" --> B(Pre-Operational); B -- "NMT: Start Node<br><b>(CS=1)</b>" --> C(Operational); C -- "NMT: Enter Pre-Operational<br><b>(CS=128)</b>" --> B; C -- "NMT: Stop Node<br><b>(CS=2)</b>" --> D(Stopped); B -- "NMT: Stop Node<br><b>(CS=2)</b>" --> D; D -- "NMT: Start Node<br><b>(CS=1)</b>" --> C; D -- "NMT: Enter Pre-Operational<br><b>(CS=128)</b>" --> B; B -- "NMT: Reset Node<br><b>(CS=129)</b>" --> A; C -- "NMT: Reset Node<br><b>(CS=129)</b>" --> A; D -- "NMT: Reset Node<br><b>(CS=129)</b>" --> A; B -- "NMT: Reset Communication<br><b>(CS=130)</b>" --> A; C -- "NMT: Reset Communication<br><b>(CS=130)</b>" --> A; D -- "NMT: Reset Communication<br><b>(CS=130)</b>" --> A; end %% Styling classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; class A startNode; class B,C,D processNode;
- Service Data Objects (SDOs):
- Provide confirmed access (read or write) to any entry in a device’s Object Dictionary.
- Operate on a client-server model:
- SDO Client: Initiates the SDO transfer to access an OD entry on an SDO server.
- SDO Server: Responds to SDO requests from a client, providing access to its own OD. Each device typically has one SDO server.
- Used for device configuration, diagnostics, and accessing non-time-critical parameters.
- SDO Protocol (simplified):
- Initiate Download (Client writes to Server’s OD):
- Client sends: COB-ID
0x600 + ServerNodeID
. Data:[ccs(1 byte) | Index(2 bytes) | SubIndex(1 byte) | Data(0-4 bytes for expedited)]
. ccs
(Client Command Specifier) indicates expedited/segmented, size.
- Client sends: COB-ID
- Initiate Upload (Client reads from Server’s OD):
- Client sends: COB-ID
0x600 + ServerNodeID
. Data:[ccs(1 byte) | Index(2 bytes) | SubIndex(1 byte) | 0000 (4 dummy bytes)]
.
- Client sends: COB-ID
- Server Response:
- Server sends: COB-ID
0x580 + ServerNodeID
. Data:[scs(1 byte) | Index(2 bytes) | SubIndex(1 byte) | Data(0-4 bytes for expedited)]
or SDO Abort code. scs
(Server Command Specifier).
- Server sends: COB-ID
- Segmented Transfer: For data > 4 bytes, a sequence of request/response segments is used.
- Initiate Download (Client writes to Server’s OD):
- Each device has at least one default SDO channel. Additional SDO channels can be configured.
sequenceDiagram actor Client participant Server %% SDO Upload (Client Reads from Server OD) rect rgb(240, 248, 255) note over Client, Server: SDO Upload (Read) Client->>Server: Initiate Upload Request<br/>COB-ID: 0x600 + NodeID_Srv<br/>Data: [0x40, Index, SubIndex, 0s] Server-->>Client: Initiate Upload Response<br/>COB-ID: 0x580 + NodeID_Srv<br/>Data: [scs, Index, SubIndex, Value] end %% SDO Download (Client Writes to Server OD) rect rgb(248, 255, 240) note over Client, Server: SDO Download (Write) Client->>Server: Initiate Download Request<br/>COB-ID: 0x600 + NodeID_Srv<br/>Data: [ccs, Index, SubIndex, Value] Server-->>Client: Initiate Download Response<br/>COB-ID: 0x580 + NodeID_Srv<br/>Data: [0x60, Index, SubIndex, 0s] end %% SDO Abort rect rgb(255, 248, 248) note over Client, Server: SDO Abort (e.g., Object not found) Client->>Server: Initiate Upload Request (for non-existent object) Server-->>Client: Abort Transfer<br/>COB-ID: 0x580 + NodeID_Srv<br/>Data: [0x80, Index, SubIndex, Abort Code] end
- Process Data Objects (PDOs):
- Used for real-time, unconfirmed transfer of process data (e.g., sensor values, control signals).
- High speed, low overhead. No protocol handshake like SDOs.
- Producer-Consumer Model: One device produces a PDO, and one or more devices consume it.
- PDO Mapping: The contents of a PDO (up to 8 data bytes) are defined by mapping entries from the producer’s (for TPDO) or consumer’s (for RPDO) Object Dictionary into the PDO. This mapping is configurable via SDOs (OD indices
0x1A00-0x1A03
for TPDOs,0x1600-0x1603
for RPDOs). - TPDO (Transmit PDO): Sends data from the local device.
- RPDO (Receive PDO): Receives data for the local device.
- Transmission Types (configurable in OD
0x1800-0x1803
for TPDOs,0x1400-0x1403
for RPDOs):- Synchronous (Acyclic/Cyclic): Transmitted after receiving a SYNC object. Can be every SYNC, or every Nth SYNC.
- Asynchronous (Event-Driven): Transmitted when the process data changes (event timer can limit rate) or on a timer.
- RTR-only: (Less common for PDOs, not recommended for high bus load).
- Default COB-IDs are based on Node-ID and PDO number (1-4), but are configurable.
graph TD subgraph "PDO Communication Model" Producer("<b>Producer Node</b><br><i>(e.g., Sensor Array)</i><br>Maps OD values to TPDO1") -- "TPDO1 (COB-ID 0x18A)" --> CAN_BUS(("CAN Bus")); CAN_BUS -- "RPDO1 (Listens to 0x18A)" --> Consumer1("<b>Consumer Node 1</b><br><i>(e.g., Motor Controller)</i><br>Maps RPDO1 to its OD"); CAN_BUS -- "RPDO2 (Listens to 0x18A)" --> Consumer2("<b>Consumer Node 2</b><br><i>(e.g., Display Unit)</i><br>Maps RPDO2 to its OD"); CAN_BUS -- "Message Ignored" --> OtherNode("<b>Other Node</b><br><i>(Not configured to consume this PDO)</i>"); end %% Styling classDef producer fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF; classDef consumer fill:#D1FAE5,stroke:#059669,stroke-width:1.5px,color:#065F46; classDef other fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; class Producer producer; class Consumer1,Consumer2 consumer; class OtherNode other;
- Synchronization Object (SYNC):
- A broadcast message (COB-ID
0x080
) sent by a SYNC producer (often the NMT master). - Used to synchronize actions across multiple devices, especially for synchronous PDO transmission or sampling of inputs.
- Typically contains no data bytes, but can have a counter if configured.
- The SYNC cycle period is configured in OD entry
0x1006
on the SYNC producer.
- A broadcast message (COB-ID
- Emergency Object (EMCY):
- Broadcast by a device when it detects a critical internal error.
- High priority to ensure it’s transmitted promptly.
- COB-ID:
0x080 + Node-ID
of the device reporting the error. - Data (8 bytes):
- Bytes 0-1: Emergency Error Code (EEC – e.g.,
0x1000
for generic error). - Byte 2: Error Register (from OD entry
0x1001
). - Bytes 3-7: Manufacturer-specific error information.
- Bytes 0-1: Emergency Error Code (EEC – e.g.,
- The error history is often stored in OD entries
0x1003
.
- Heartbeat Protocol:
- Allows devices to monitor the NMT state of other devices on the network.
- Each device (Heartbeat Producer) periodically broadcasts a Heartbeat message.
- COB-ID:
0x700 + Node-ID
of the producer. - Data (1 byte): The NMT state of the producer (e.g.,
0x00
=Boot-up,0x04
=Stopped,0x05
=Operational,0x7F
=Pre-Operational). - The Heartbeat producer time is configured in OD entry
0x1017
. - Consumers can monitor these heartbeats and detect if a node fails (Heartbeat Consumer Time in OD
0x1016
). - Replaces the older Node Guarding protocol (which used RTR frames and was less efficient).
ESP32 TWAI Configuration for CANopen
- Baud Rate: Common CANopen rates are 1 Mbps, 800 kbps, 500 kbps, 250 kbps, 125 kbps, etc. Configuration must match the network.
- 11-bit Identifiers: CANopen primarily uses 11-bit standard CAN identifiers. Transmitted messages should not have the
TWAI_MSG_FLAG_EXTD
flag set. - Acceptance Filter: Configure the TWAI acceptance filter to accept relevant COB-IDs. This is crucial for efficient operation.
- A device needs to accept:
- NMT messages (COB-ID
0x000
). - SYNC messages (COB-ID
0x080
) if it’s synchronous. - Its own SDO RX COB-ID (e.g.,
0x600 + Node-ID
). - Its configured RPDO COB-IDs.
- Heartbeat messages it needs to consume.
- EMCY messages.
- NMT messages (COB-ID
- Multiple filter entries might be needed, or a wider filter with software post-filtering.
- A device needs to accept:
Practical Examples (Conceptual)
Disclaimer: Implementing a full CANopen stack (including a complete Object Dictionary manager, SDO server/client state machines, PDO mapping/linking, NMT slave/master logic) is a complex task, often involving specialized libraries or significant development effort. The examples below are highly simplified to illustrate basic CAN frame interactions relevant to CANopen COBs using the ESP32’s TWAI driver. They do not represent a compliant CANopen device.
Hardware Setup
- ESP32 Development Board (e.g., ESP32, ESP32-S2, ESP32-S3, ESP32-C6, ESP32-H2).
- CAN Transceiver Module (e.g., MCP2551, TJA1050 based).
- Wiring: ESP32 TWAI_TX/RX to transceiver, transceiver to CAN bus. Common ground. Power.
- CANopen Network/Analyzer: Another CANopen device or a CAN analyzer tool capable of interpreting CANopen messages (e.g., PCAN-View with CANopen add-in, BusMaster with CANopen configuration, Wireshark with CANdissector).
- Termination: Proper 120-Ohm termination on the CAN bus.
Project Setup (VS Code, ESP-IDF)
- New ESP-IDF project (e.g.,
esp32_canopen_basics
). - Configure TWAI GPIOs in
menuconfig
orsdkconfig.defaults
.
Example 1: Transmitting a CANopen Heartbeat Message
This example shows an ESP32 acting as a simple Heartbeat producer.
main/main.c
:
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/twai.h"
#include "esp_log.h"
static const char *TAG = "CANOPEN_NODE";
// Default TWAI GPIOs - configure via menuconfig
#define TWAI_TX_GPIO CONFIG_TWAI_TX_GPIO
#define TWAI_RX_GPIO CONFIG_TWAI_RX_GPIO
// CANopen Defines
#define MY_NODE_ID 0x0A // Example Node-ID: 10
#define CANOPEN_BAUD_RATE_KBPS 125 // Example: 125 kbps
// NMT States (simplified)
#define NMT_STATE_BOOTUP 0x00
#define NMT_STATE_STOPPED 0x04
#define NMT_STATE_OPERATIONAL 0x05
#define NMT_STATE_PREOPERATIONAL 0x7F
uint8_t current_nmt_state = NMT_STATE_BOOTUP; // Initial state
esp_err_t twai_init_canopen(void) {
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO, TWAI_RX_GPIO, TWAI_MODE_NORMAL);
twai_timing_config_t t_config;
switch (CANOPEN_BAUD_RATE_KBPS) {
case 1000: t_config = TWAI_TIMING_CONFIG_1MBITS(); break;
case 800: t_config = TWAI_TIMING_CONFIG_800KBITS(); break;
case 500: t_config = TWAI_TIMING_CONFIG_500KBITS(); break;
case 250: t_config = TWAI_TIMING_CONFIG_250KBITS(); break;
case 125: t_config = TWAI_TIMING_CONFIG_125KBITS(); break;
default:
ESP_LOGE(TAG, "Unsupported CANopen baud rate: %d kbps", CANOPEN_BAUD_RATE_KBPS);
return ESP_FAIL;
}
// Filter: Accept NMT (0x000), SYNC (0x080), and our SDO_RX (0x600 + MY_NODE_ID)
// This is a very basic filter setup. A real device needs more.
// For simplicity, we'll use accept all and filter in software for these examples.
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
ESP_LOGE(TAG, "Failed to install TWAI driver");
return ESP_FAIL;
}
ESP_LOGI(TAG, "TWAI driver installed for CANopen at %d kbps", CANOPEN_BAUD_RATE_KBPS);
if (twai_start() != ESP_OK) {
ESP_LOGE(TAG, "Failed to start TWAI driver");
return ESP_FAIL;
}
ESP_LOGI(TAG, "TWAI driver started");
return ESP_OK;
}
esp_err_t send_canopen_heartbeat(uint8_t node_id, uint8_t nmt_state) {
twai_message_t message;
message.identifier = 0x700 + node_id; // Heartbeat COB-ID
message.flags = TWAI_MSG_FLAG_NONE; // Standard 11-bit ID
message.data_length_code = 1;
message.data[0] = nmt_state;
// Pad unused data bytes (CAN hardware often does this, but good practice)
for (int i = 1; i < 8; ++i) message.data[i] = 0x00;
ESP_LOGI(TAG, "Sending Heartbeat: ID=0x%03lX, NodeID=0x%02X, State=0x%02X",
message.identifier, node_id, nmt_state);
if (twai_transmit(&message, pdMS_TO_TICKS(100)) == ESP_OK) { // Short timeout
// ESP_LOGD(TAG, "Heartbeat message queued for transmission");
return ESP_OK;
} else {
ESP_LOGE(TAG, "Failed to queue Heartbeat message");
return ESP_FAIL;
}
}
void canopen_heartbeat_task(void *pvParameters) {
if (twai_init_canopen() != ESP_OK) {
ESP_LOGE(TAG, "CANopen TWAI initialization failed. Task stopping.");
vTaskDelete(NULL);
return;
}
// Send initial Boot-up message
current_nmt_state = NMT_STATE_BOOTUP;
send_canopen_heartbeat(MY_NODE_ID, current_nmt_state);
// Simulate transition to Pre-Operational then Operational after some time
vTaskDelay(pdMS_TO_TICKS(1000)); // Simulate boot process
current_nmt_state = NMT_STATE_PREOPERATIONAL;
ESP_LOGI(TAG, "Node %d transitioned to PRE-OPERATIONAL", MY_NODE_ID);
vTaskDelay(pdMS_TO_TICKS(2000)); // Simulate configuration
current_nmt_state = NMT_STATE_OPERATIONAL;
ESP_LOGI(TAG, "Node %d transitioned to OPERATIONAL", MY_NODE_ID);
// Heartbeat period (e.g., 1000 ms, configure via OD 0x1017 in a real device)
const TickType_t heartbeat_period_ms = pdMS_TO_TICKS(1000);
while (1) {
send_canopen_heartbeat(MY_NODE_ID, current_nmt_state);
vTaskDelay(heartbeat_period_ms);
}
}
void app_main(void) {
ESP_LOGI(TAG, "Starting CANopen Basic Node Application (Heartbeat Producer)");
xTaskCreate(canopen_heartbeat_task, "canopen_hb_task", 4096, NULL, 5, NULL);
// Add other tasks for SDO/PDO handling, NMT slave logic etc.
}
Build Instructions:
- Save as
main/main.c
. - Ensure TWAI GPIOs are configured.
idf.py build
.
Run/Flash/Observe:
idf.py -p /dev/ttyUSB0 flash monitor
.- Connect to a CAN bus with a CANopen analyzer tool.
- You should see Heartbeat messages (COB-ID
0x70A
ifMY_NODE_ID
is 10) being transmitted periodically. The data byte should change from0x00
(Boot-up) to0x7F
(Pre-Operational) and then to0x05
(Operational).
Example 2: Basic NMT Slave Logic and SDO RX (Conceptual Parsing)
This example outlines how a slave might react to NMT commands and parse an incoming SDO request.
// Add this task and helper functions to main.c
// COB-IDs
#define COB_ID_NMT_CONTROL 0x000
#define COB_ID_SDO_RX_BASE 0x600 // Client to Server (this device is server)
#define COB_ID_SDO_TX_BASE 0x580 // Server to Client
// NMT Command Specifiers (CS)
#define NMT_CS_START_NODE 1
#define NMT_CS_STOP_NODE 2
#define NMT_CS_ENTER_PREOP 128
#define NMT_CS_RESET_NODE 129
#define NMT_CS_RESET_COMM 130
// SDO Client Command Specifiers (ccs for request from client)
// For SDO Download (client writes to server)
#define SDO_CCS_DOWNLOAD_INITIATE_EXPEDITED_SIZE_INDICATED 0x23 // n=0-3 bytes of data
#define SDO_CCS_DOWNLOAD_INITIATE_EXPEDITED_NO_SIZE 0x2F // n=0 bytes of data
#define SDO_CCS_DOWNLOAD_INITIATE_NORMAL 0x21 // Segmented, size in bytes 4-7
// For SDO Upload (client reads from server)
#define SDO_CCS_UPLOAD_INITIATE 0x40
// Simplified Object Dictionary Entry (conceptual)
typedef struct {
uint16_t index;
uint8_t sub_index;
uint32_t value; // Using uint32_t for simplicity
uint8_t size_bytes; // Size of the value in bytes
char access_type[3]; // "RO", "RW", "WO"
} od_entry_t;
// Very basic OD
od_entry_t object_dictionary[] = {
{0x1000, 0x00, 0x12345678, 4, "RO"}, // Device Type
{0x1008, 0x00, 0x45535033, 4, "RO"}, // Manufacturer Device Name (ASCII "ESP3")
{0x1018, 0x01, MY_NODE_ID, 4, "RO"}, // Identity Object, Vendor ID (using NodeID for example)
{0x2000, 0x00, 100, 2, "RW"}, // Manufacturer-specific parameter
};
const int od_size = sizeof(object_dictionary) / sizeof(od_entry_t);
// Function to find an OD entry
od_entry_t* find_od_entry(uint16_t index, uint8_t sub_index) {
for (int i = 0; i < od_size; ++i) {
if (object_dictionary[i].index == index && object_dictionary[i].sub_index == sub_index) {
return &object_dictionary[i];
}
}
return NULL;
}
// Function to send an SDO Abort message
void send_sdo_abort(uint8_t client_node_id_for_tx_cobid, uint16_t index, uint8_t sub_index, uint32_t abort_code) {
twai_message_t sdo_response;
sdo_response.identifier = COB_ID_SDO_TX_BASE + MY_NODE_ID; // We are the server
sdo_response.flags = TWAI_MSG_FLAG_NONE;
sdo_response.data_length_code = 8;
sdo_response.data[0] = 0x80; // SCS: Abort SDO Transfer
sdo_response.data[1] = index & 0xFF;
sdo_response.data[2] = (index >> 8) & 0xFF;
sdo_response.data[3] = sub_index;
sdo_response.data[4] = abort_code & 0xFF;
sdo_response.data[5] = (abort_code >> 8) & 0xFF;
sdo_response.data[6] = (abort_code >> 16) & 0xFF;
sdo_response.data[7] = (abort_code >> 24) & 0xFF;
ESP_LOGW(TAG, "Sending SDO Abort: Idx=0x%04X, Sub=0x%02X, Code=0x%08lX", index, sub_index, abort_code);
twai_transmit(&sdo_response, pdMS_TO_TICKS(100));
}
// Function to send an SDO Upload (Read) Expedited Response
void send_sdo_upload_exp_response(uint16_t index, uint8_t sub_index, uint32_t value, uint8_t num_bytes) {
twai_message_t sdo_response;
sdo_response.identifier = COB_ID_SDO_TX_BASE + MY_NODE_ID;
sdo_response.flags = TWAI_MSG_FLAG_NONE;
sdo_response.data_length_code = 8; // Always 8 for SDO response frame
// SCS for expedited upload response: 0100_nes_b
// n = number of bytes in data[4-7] that are NOT data (4 - actual data bytes)
// e = 1 (expedited)
// s = 1 (size indicated)
uint8_t scs = 0x40; // Base for upload response
if (num_bytes <= 4) {
scs |= (1 << 1); // e = 1 (expedited)
scs |= (1 << 0); // s = 1 (size indicated)
scs |= ((4 - num_bytes) & 0x03) << 2; // n bits
} else { // Should not happen for expedited, but as a fallback
send_sdo_abort(0, index, sub_index, 0x06070010); // Data type mismatch / length too high for SDO
return;
}
sdo_response.data[0] = scs;
sdo_response.data[1] = index & 0xFF;
sdo_response.data[2] = (index >> 8) & 0xFF;
sdo_response.data[3] = sub_index;
// Fill data bytes (LSB first for multi-byte values)
for(int i=0; i<4; ++i) sdo_response.data[4+i] = 0; // Clear padding area
for (int i = 0; i < num_bytes; ++i) {
sdo_response.data[4 + i] = (value >> (i * 8)) & 0xFF;
}
ESP_LOGI(TAG, "Sending SDO Upload Response: Idx=0x%04X, Sub=0x%02X, Value=0x%08lX (%d bytes)", index, sub_index, value, num_bytes);
twai_transmit(&sdo_response, pdMS_TO_TICKS(100));
}
void canopen_main_logic_task(void *pvParameters) {
// Assuming TWAI is initialized by heartbeat_task or here
if (current_nmt_state == NMT_STATE_BOOTUP) { // If heartbeat task hasn't run yet
if (twai_init_canopen() != ESP_OK) {
ESP_LOGE(TAG, "CANopen TWAI initialization failed. Task stopping.");
vTaskDelete(NULL);
return;
}
// Send initial Boot-up message
current_nmt_state = NMT_STATE_BOOTUP;
send_canopen_heartbeat(MY_NODE_ID, current_nmt_state);
vTaskDelay(pdMS_TO_TICKS(100)); // Short delay
current_nmt_state = NMT_STATE_PREOPERATIONAL; // Transition after boot
ESP_LOGI(TAG, "Node %d (Logic Task) set to PRE-OPERATIONAL", MY_NODE_ID);
}
ESP_LOGI(TAG, "CANopen Main Logic Task Started. NodeID=0x%02X", MY_NODE_ID);
twai_message_t rx_message;
while (1) {
if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) == ESP_OK) { // Poll with short timeout
// NMT Message Handling
if (rx_message.identifier == COB_ID_NMT_CONTROL && rx_message.data_length_code == 2) {
uint8_t nmt_cs = rx_message.data[0];
uint8_t nmt_node_id = rx_message.data[1];
if (nmt_node_id == MY_NODE_ID || nmt_node_id == 0) { // Addressed to us or all
ESP_LOGI(TAG, "NMT Command RX: CS=0x%02X for NodeID=0x%02X", nmt_cs, nmt_node_id);
switch (nmt_cs) {
case NMT_CS_START_NODE:
current_nmt_state = NMT_STATE_OPERATIONAL;
ESP_LOGI(TAG, "NMT: Switched to OPERATIONAL");
break;
case NMT_CS_STOP_NODE:
current_nmt_state = NMT_STATE_STOPPED;
ESP_LOGI(TAG, "NMT: Switched to STOPPED");
break;
case NMT_CS_ENTER_PREOP:
current_nmt_state = NMT_STATE_PREOPERATIONAL;
ESP_LOGI(TAG, "NMT: Switched to PRE-OPERATIONAL");
break;
case NMT_CS_RESET_NODE:
ESP_LOGI(TAG, "NMT: Reset Node command received. Rebooting...");
esp_restart();
break;
case NMT_CS_RESET_COMM:
ESP_LOGI(TAG, "NMT: Reset Communication command received. Re-init CANopen params...");
// Re-initialize communication profile parameters from OD
current_nmt_state = NMT_STATE_PREOPERATIONAL; // Typically back to pre-op
break;
}
}
}
// SDO Request Handling (Client to this Server Node)
else if (rx_message.identifier == (COB_ID_SDO_RX_BASE + MY_NODE_ID)) {
if (current_nmt_state == NMT_STATE_PREOPERATIONAL || current_nmt_state == NMT_STATE_OPERATIONAL) {
uint8_t ccs = rx_message.data[0] & 0xE0; // Extract command specifier bits
uint16_t index = (rx_message.data[2] << 8) | rx_message.data[1];
uint8_t sub_index = rx_message.data[3];
ESP_LOGI(TAG, "SDO Request RX: CCS=0x%02X, Idx=0x%04X, Sub=0x%02X", rx_message.data[0], index, sub_index);
if (ccs == SDO_CCS_UPLOAD_INITIATE) { // Client wants to read (upload from server)
ESP_LOGI(TAG, "SDO Upload (Read) initiated by client for 0x%04X:%02X", index, sub_index);
od_entry_t* entry = find_od_entry(index, sub_index);
if (entry && (strcmp(entry->access_type, "RO") == 0 || strcmp(entry->access_type, "RW") == 0)) {
if (entry->size_bytes <= 4) { // Expedited
send_sdo_upload_exp_response(index, sub_index, entry->value, entry->size_bytes);
} else {
ESP_LOGW(TAG, "SDO Upload: Segmented transfer not implemented in this example.");
send_sdo_abort(0, index, sub_index, 0x05040001); // SDO protocol timeout (conceptual for not implemented)
}
} else {
ESP_LOGW(TAG, "SDO Upload: Object 0x%04X:%02X not found or not readable.", index, sub_index);
send_sdo_abort(0, index, sub_index, 0x06020000); // Object does not exist
}
} else if ((rx_message.data[0] & 0xE0) == 0x20 ) { // Client wants to write (download to server)
// CCS bits: 001_nes_b (n=bytes valid, e=expedited, s=size indicated)
// Example: SDO_CCS_DOWNLOAD_INITIATE_EXPEDITED_SIZE_INDICATED (0x23)
bool expedited = (rx_message.data[0] >> 1) & 0x01;
bool size_indicated = rx_message.data[0] & 0x01;
uint8_t num_data_bytes_in_req = 0;
uint32_t received_value = 0;
if (expedited && size_indicated) {
num_data_bytes_in_req = 4 - ((rx_message.data[0] >> 2) & 0x03);
for(int i=0; i<num_data_bytes_in_req; ++i) {
received_value |= ((uint32_t)rx_message.data[4+i] << (i*8));
}
ESP_LOGI(TAG, "SDO Download (Write) initiated by client for 0x%04X:%02X, Value=0x%08lX (%d bytes)",
index, sub_index, received_value, num_data_bytes_in_req);
// TODO: Implement actual write to OD and send SDO download response
// For now, just log. A real implementation would check access rights, data type, etc.
// Then send SDO Download Response: COB_ID_SDO_TX_BASE + MY_NODE_ID, data[0]=0x60 (scs)
// send_sdo_download_response(index, sub_index);
} else {
ESP_LOGW(TAG, "SDO Download: Non-expedited or no-size-indicated not implemented.");
send_sdo_abort(0, index, sub_index, 0x05040001); // SDO protocol timeout
}
} else {
ESP_LOGW(TAG, "SDO: Unsupported client command specifier: 0x%02X", rx_message.data[0]);
send_sdo_abort(0, index, sub_index, 0x05040005); // Invalid or unknown command specifier
}
} else {
ESP_LOGW(TAG, "SDO Request received but node not in Pre-Op or Operational state.");
// SDOs are typically only processed in Pre-Op or Operational
}
}
// Add PDO, SYNC, EMCY handling here if needed
}
// Other logic for the node can run here, e.g., reading sensors for PDOs
// The heartbeat is sent by its own task in this example structure.
vTaskDelay(pdMS_TO_TICKS(10)); // Yield for other tasks
}
}
// In app_main, after creating heartbeat_task:
// xTaskCreate(canopen_main_logic_task, "canopen_logic_task", 4096, NULL, 5, NULL);
Note: This SDO handling is extremely basic. A real SDO server needs to handle segmented transfers, various command specifiers for both upload and download, data type checking, and proper OD access rights.
Variant Notes
- ESP32, ESP32-S2, ESP32-S3, ESP32-C6, ESP32-H2:
- These variants possess a built-in TWAI controller compliant with CAN 2.0B, suitable for CANopen’s 11-bit identifier scheme. The ESP-IDF TWAI driver (
driver/twai.h
) is used. - Sufficient processing power and memory for implementing a CANopen slave stack, including an Object Dictionary.
- These variants possess a built-in TWAI controller compliant with CAN 2.0B, suitable for CANopen’s 11-bit identifier scheme. The ESP-IDF TWAI driver (
- ESP32-C3:
- Lacks a built-in TWAI/CAN controller. Requires an external CAN controller IC (e.g., MCP2515) connected via SPI.
- The ESP32-C3 acts as an SPI master. The TWAI driver examples are not directly applicable. A driver for the specific external CAN IC is needed.
- The higher-level CANopen protocol logic (OD management, SDO/PDO state machines) would still run on the ESP32-C3, but CAN frame I/O goes through the external IC.
- General:
- A robust CAN transceiver is always required.
- For complex CANopen devices (e.g., NMT master, extensive OD, multiple PDOs/SDOs), consider the ESP32’s RAM and flash capacity, especially if using a full third-party CANopen stack.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Baud Rate Mismatch | No communication at all on the bus. Frequent error frames visible on a CAN analyzer. | Ensure every single node on the CANopen network is configured to the exact same baud rate (e.g., 125 kbps, 250 kbps). Check configuration files, firmware settings, and any physical switches. |
Node-ID Conflict | Erratic behavior, Heartbeat conflicts, nodes stop responding, SDOs go to the wrong device. | Verify every device on the network has a unique Node-ID between 1 and 127. Check hardware (DIP switches) and software (OD entry 0x2F00:02 in some legacy systems, or configure via LSS). |
Incorrect COB-ID Usage | Messages are ignored by target nodes. Services like SDO or PDO do not work. Unexpected cross-talk. | Double-check the COB-ID calculations. Remember the default formulas (e.g., Heartbeat is 0x700 + NodeID). Verify if COB-IDs have been reconfigured from their defaults in the Object Dictionary. |
Improper NMT State Handling | Device sends PDOs while in Pre-Operational state. Device does not start/stop when commanded by the NMT Master. | Strictly enforce the NMT state machine rules. PDO communication is only allowed in the Operational state. SDOs are allowed in Pre-Operational and Operational. Ensure your device correctly processes NMT commands (COB-ID 0x000). |
Faulty PDO Mapping | PDO data received is nonsensical or incorrect. Data appears in the wrong byte positions. | Use an SDO client to read the PDO mapping parameter objects (0x1A00+ for TPDOs, 0x1600+ for RPDOs). Verify that the mapped OD entries (index, sub-index, and data length) are correct and match the producer’s and consumer’s expectations. |
Missing or Incorrect Bus Termination | Intermittent errors, especially on longer bus lines or at higher speeds. “Ghost” frames or reflections. | A CAN bus segment requires exactly two 120-Ohm termination resistors, one at each physical end of the main bus line. Measure resistance between CAN_H and CAN_L (should be ~60 Ohms). |
SDO Access Failures (Aborts) | An SDO tool reports an “Abort Code” when trying to read/write an OD entry. | Decode the SDO abort code. Common codes include: 0x06020000: Object does not exist. 0x06010002: Write to a read-only object. 0x06090011: Sub-index does not exist. This points directly to an error in your Object Dictionary definition or access logic. |
Exercises
- Exercise 1 (Easy): Parse Received Heartbeat
- Modify the
canopen_main_logic_task
from Example 2. - When a CAN message is received, check if its COB-ID falls within the Heartbeat range (
0x701
to0x77F
). - If it’s a Heartbeat, extract the producing Node-ID from the COB-ID and the NMT state from the first data byte. Log this information.
- Modify the
- Exercise 2 (Medium): Implement Basic SDO Server Download Response
- Extend Example 2’s SDO handling. When an “SDO Initiate Download Request (expedited)” is received for a writable OD entry (e.g.,
0x2000, 0x00
):- Verify access rights (
RW
). - Conceptually update the value in your
object_dictionary
array. - Send an “SDO Download Response” (COB-ID
0x580 + MY_NODE_ID
, data[0x60, Index_LSB, Index_MSB, SubIndex, 0,0,0,0]
).
- Verify access rights (
- Test by sending an SDO write request from a CANopen tool.
- Extend Example 2’s SDO handling. When an “SDO Initiate Download Request (expedited)” is received for a writable OD entry (e.g.,
- Exercise 3 (Conceptual): Design PDO Mapping for a Sensor
- Imagine your ESP32 is connected to a temperature sensor and a humidity sensor. You want to transmit these two values in a single TPDO.
- Define two conceptual OD entries for Temperature (e.g.,
0x2001:00
,INTEGER16
) and Humidity (e.g.,0x2002:00
,UNSIGNED8
). - Describe the OD entries needed to configure TPDO1 (communication parameter
0x1800
and mapping parameter0x1A00
) to:- Transmit these two values.
- Use a specific COB-ID (e.g.,
0x180 + MY_NODE_ID
). - Be event-driven (transmitted when data changes).
- You don’t need to write C code for the ESP32, but detail the OD index, sub-index, and values you would set for
0x1800
(COB-ID, transmission type) and0x1A00
(number of mapped objects, mapping entries for0x2001:00
and0x2002:00
).
Summary
- CANopen is a standardized higher-layer protocol based on CAN, widely used in industrial automation and embedded systems.
- The Object Dictionary (OD) is central to CANopen, defining all device parameters and data.
- Service Data Objects (SDOs) provide confirmed, peer-to-peer access to OD entries for configuration and non-time-critical data.
- Process Data Objects (PDOs) offer efficient, unconfirmed transfer of real-time process data, with contents defined by OD mapping.
- Network Management (NMT) controls device states (Initializing, Pre-Operational, Operational, Stopped).
- Other key services include SYNC (synchronization), EMCY (emergency messages), and Heartbeat (node state monitoring).
- CANopen primarily uses 11-bit CAN identifiers (COB-IDs), often derived from a function code and a unique 7-bit Node-ID (1-127).
- ESP32 variants with built-in TWAI can directly interface with CANopen networks; others (like ESP32-C3) require an external CAN controller.
- Implementing a full CANopen device requires careful handling of the OD, state machines for NMT/SDO/PDO, and adherence to CiA specifications.
Further Reading
- CiA 301: CANopen application layer and communication profile: The core CANopen specification. (Available from CAN in Automation (CiA))
- CiA 303-1: CANopen – Cabling and connector pin assignment
- CiA 305: CANopen layer setting services (LSS) and protocols
- Device Profile Specifications (e.g., CiA 401, CiA 402): For specific device types.
- “Embedded Networking with CAN and CANopen” by Olaf Pfeiffer, Andrew Ayre, and Christian Keydel: A comprehensive book on CAN and CANopen.
- CANopen Stack Implementations: Explore open-source stacks like CanFestival or commercial stacks for deeper understanding of implementation details.
- ESP-IDF TWAI Programming Guide: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/peripherals/twai.html (Select your target ESP32 variant).