CAN Bus Simulation with CANopen 
|
The miniHIL CANOpen abstraction described on this page is currently only available on request and not part of the default miniHIL release. Contact us for more information. |
Motivation
In embedded development, it is often necessary to simulate CAN bus nodes, especially when testing larger systems where real hardware is not yet available or is difficult to access. This How-To demonstrates how to set up a basic CAN bus simulation using miniHIL infrastructure and a CANopen communication model.
Pre-requisites
-
miniHIL platform ready
-
CAN bus wiring established (physical connection)
-
A connected Device Under Test DUT that is connected with the simulated CAN bus
Basic Concepts
CAN
The Controller Area Network (CAN) is a robust vehicle bus standard designed to allow microcontrollers and devices to communicate with each other without a host computer. It uses message-based communication and supports multi-master configurations.
CANopen
CANopen is a higher-layer protocol based on CAN, mainly designed for embedded networking applications. It defines communication protocols (PDO, SDO, NMT) and a device description (Object Dictionary).
Basic CANopen concepts:
-
NMT (Network Management): Manages the state of the nodes in the network (Operational, Pre-operational, Stopped).
-
PDO (Process Data Object): Used for real-time data exchange between nodes. PDOs can be transmitted without confirmation.
-
SDO (Service Data Object): Used for configuration and accessing the object dictionary. SDOs require confirmation and are used for non-time-critical data exchange.
-
Object Dictionary: A data structure that defines the parameters and data types of a device. It contains all the information about the device’s capabilities and configuration.
Setting up a CAN Simulation
You will need two actors:
-
AStm32CANService1 for the CAN bus simulation
-
Simulated CANopen node (e.g., GenericCANNode.room)
A basic setup is show below:
ActorClass ACanSimulation {
Interface {
Port deviceCtrl: PGenericCANNodeControl
}
Structure {
ActorRef can: AStm32CANService1
ActorRef canStarter: ACanStarter
ActorRef deviceSim: AGenericCanNode
Binding canStarter.canCtrl and can.ctl
Binding deviceSim.canBus and can.bus
Binding canStarter.device and deviceSim.fct
Binding deviceSim.simCtrl and deviceCtrl
}
}
For this, you need an actor that starts the simulation, which could look like this:
ActorClass ACanStarter {
Interface {
conjugated Port canCtrl: PCANCtrl
conjugated Port deviceSim: PGenericCANNodeControl
}
Structure {
external Port canCtrl
external Port deviceSim
}
Behavior {
StateMachine {
State state0
Transition init0: initial -> state0 {
action '''
DCanParams params;
DCanParams_init(¶ms);
params.baudRate_kbs = 125;
params.baudRate_kbs_fd = 125;
canCtrl.open(¶ms);'''
}
State finished
Transition tr0: state0 -> finished {
triggers {
<opened: canCtrl>
}
action '''
deviceSim.boot(10);
'''
}
}
}
}
The ACanStarter actor is responsible for opening the CAN bus and starting the simulation. It uses the PCANCtrl port to control the CAN bus and the PGenericCANNodeControl port to boot the simulation.
The Simulated CANopen Node
Each node typically consists of:
-
Heartbeat Producer (sending regular heartbeat messages)
-
CANopen Handler (processing NMT messages)
-
SDO Server (handling SDO requests)
-
PDO Server (sending/receiving PDO messages)
Example Node Structure (AGenericCanNode)
RoomModel actors.GenericCANNode {
import etrice.api.types.*
import actors.canopen.base.ASDOServer
import actors.canopen.base.ACanOpenHeartbeatProducerService
import actors.canopen.base.ACanOpenHandler
import actors.canopen.base.APDOServer
import actors.canopen.base.PCanOpenHeartbeatProducerCtrl
import actors.canopen.base.PCanOpenCtrl
import actors.canopen.base.PCanOpenSDOServerConfig
import actors.canopen.base.PCanOpenSDOEvt
import actors.canopen.base.PPDOServerCtrl
import actors.ECMotor.PECMotorMovementSimCtrl
import etrice.api.logger.PLogger
import can.api.CANService.PCANData
import actors.canopen.base.etMemory
import actors.canopen.base.CanOpenOD
ActorClass AGenericCANNode {
Interface {
conjugated Port canBus[4]: PCANData
Port fct: PGenericCANNodeControl
}
Structure {
usercode1 '''
// Start of generated header file
#include "GenericCANNodeOD.h"
#include <string.h>
#include "canopen/CanOpen.h"
#include "canopen/od/CanOpenOD.h"
#include "base/etMemory_VariableSize.h"
'''
ActorRef sdoServer: ASDOServer
ActorRef heartbeatProducer: ACanOpenHeartbeatProducerService
ActorRef canOpenHandler: ACanOpenHandler
ActorRef pdoServer: APDOServer
Binding canOpenHandler.status and heartbeatProducer.statusIn
Binding canOpenHandler.status and sdoServer.status
Binding canOpenHandler.status and pdoServer.status
conjugated Port heatbeatCtrl: PCanOpenHeartbeatProducerCtrl
conjugated Port handlerCtrl: PCanOpenCtrl
conjugated Port sdoServerCtrl: PCanOpenSDOServerConfig
conjugated Port pdoServerCtrl: PPDOServerCtrl
SAP logger: PLogger
Binding heatbeatCtrl and heartbeatProducer.fct
Binding sdoServerCtrl and sdoServer.cfg
Binding pdoServerCtrl and pdoServer.ctrl
Binding handlerCtrl and canOpenHandler.fct
Binding canBus and canOpenHandler.canBus
Binding canBus and sdoServer.canBus
Binding canBus and heartbeatProducer.canBus
Binding canBus and pdoServer.canBus
external Port fct
// Attributes
Attribute odMemory: etMemory ref
Attribute od: CanOpenOD
Attribute initSuccessful: boolean = "false"
Attribute canOpenNodeId: uint8 = "0"
Attribute enableLogging: boolean = "true"
}
Behavior {
StateMachine {
State UnConfigured
State Configured
Transition init0: initial -> UnConfigured
Transition tr0: UnConfigured -> Configured {
triggers {
<boot: fct>
}
action '''
canOpenNodeId = transitionData;
odMemory = genericCANNodeId_getOdMemory();
if (genericCANNodeId_initOD(&od, odMemory, canOpenNodeId)) {
initSuccessful = true;
}
if (initSuccessful) {
DSDOServerConfig cfg;
cfg.canOpenNodeID = transitionData;
cfg.canOpenObjectDirectory = &od;
sdoServerCtrl.configure(&cfg);
pdoServerCtrl.setNodeID(canOpenNodeId);
pdoServerCtrl.setOd(&od);
CanOpenOD_entry* entry = CanOpenOD_getEntry(&od, 0x1008);
uint8_t* deviceNamePtr = (uint8_t*)CanOpenOD_getValueFromEntry(entry, 0);
snprintf((char*)deviceNamePtr, 150, "GenericNode %d", canOpenNodeId);
entry = CanOpenOD_getEntry(&od, 0x1000);
uint32_t* deviceTypePtr = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0);
*deviceTypePtr = 12345;
entry = CanOpenOD_getEntry(&od, 0x1018);
uint32_t* identityObject_1 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 1);
uint32_t* identityObject_2 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 2);
uint32_t* identityObject_3 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 3);
*identityObject_1 = 11;
*identityObject_2 = 12;
*identityObject_3 = 13;
entry = CanOpenOD_getEntry(&od, 0x3201);
uint32_t* genericObject_0x01 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x01);
uint32_t* genericObject_0x02 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x02);
uint32_t* genericObject_0x03 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x03);
uint32_t* genericObject_0x04 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x04);
uint32_t* genericObject_0x05 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x05);
uint32_t* genericObject_0x06 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x06);
uint32_t* genericObject_0x07 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x07);
uint32_t* genericObject_0x08 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x08);
uint32_t* genericObject_0x09 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x09);
uint32_t* genericObject_0x0A = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x0A);
uint32_t* genericObject_0x0B = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x0B);
uint32_t* genericObject_0x0C = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x0C);
uint32_t* genericObject_0x0D = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x0D);
uint32_t* genericObject_0x0E = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x0E);
uint32_t* genericObject_0x0F = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x0F);
uint32_t* genericObject_0x10 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x10);
uint32_t* genericObject_0x11 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x11);
uint32_t* genericObject_0x12 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x12);
uint32_t* genericObject_0x13 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x13);
uint32_t* genericObject_0x14 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x14);
uint32_t* genericObject_0x15 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x15);
uint32_t* genericObject_0x16 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x16);
uint32_t* genericObject_0x17 = (uint32_t*)CanOpenOD_getValueFromEntry(entry, 0x17);
*genericObject_0x01 = 0x01;
*genericObject_0x02 = 0x02;
*genericObject_0x03 = 0x03;
*genericObject_0x04 = 0x04;
*genericObject_0x05 = 0x05;
*genericObject_0x06 = 0x06;
*genericObject_0x07 = 0x07;
*genericObject_0x08 = 0x08;
*genericObject_0x09 = 0x09;
*genericObject_0x0A = 0x0A;
*genericObject_0x0B = 0x0B;
*genericObject_0x0C = 0x0C;
*genericObject_0x0D = 0x0D;
*genericObject_0x0E = 0x0E;
*genericObject_0x0F = 0x0F;
*genericObject_0x10 = 0x10;
*genericObject_0x11 = 0x11;
*genericObject_0x12 = 0x12;
*genericObject_0x13 = 0x13;
*genericObject_0x14 = 0x14;
*genericObject_0x15 = 0x15;
*genericObject_0x16 = 0x16;
*genericObject_0x17 = 0x17;
}
heatbeatCtrl.setNodeID(transitionData);
heatbeatCtrl.enable(1000);
handlerCtrl.start(transitionData);'''
}
}
}
}
ProtocolClass PGenericCANNodeControl {
incoming {
Message boot(uint8)
}
outgoing {
}
}
}
Setting up the Object Dictionary
Each simulated node has an Object Dictionary (OD) containing:
-
Static information (Device Name, Vendor ID, etc.)
-
Configuration parameters
-
Application-specific process data
A simple example for setting up an Object Dictionary can be found in GenericCANNodeOD.c in the genericCANNodeId_initOD function.
CanOpenOD_VAR deviceType = (CanOpenOD_VAR) memory->alloc(memory, sizeof(CanOpenOD_VAR));
deviceType->attributes.data.readable = 1;
deviceType->attributes.data.writable = 0;
deviceType->dataLength_bytes = 4;
deviceType->dataPtr = memory->alloc(memory, 4);
...
od->entries[0].odIndex = 0x1000;
od->entries[0].type = CanOpenOD_entryType_VAR;
od->entries[0].entry = (void*)deviceType;
In GenericCanNodeOD.c:
-
genericCANNodeId_getOdMemory(): returns a pointer to the memory manager for the Object Dictionary. -
genericCANNodeId_initOD(): initializes the Object Dictionary with the given node ID and memory manager. -
You can add entries such as device type, identity objects, hardware/software version, etc.
|
GenericCANNodeOD.c must define the expected OD entries used in the node. You can create it manually or generate it based on an EDS file. |
The content of individual entries in the Object Dictionary can be initialized during the boot transition, as shown in GenericCANNode.room.
To adapt the OD for your specific CANopen node:
-
Change the device-specific fields like device name, hardware/software version
-
Add or remove variables, arrays, or records as required.
-
Optionally map PDOs if real-time data exchange is needed.
Mapping PDOs
To map an entry to a PDO:
-
Use
canOpenPDO_addPDOToOD()to create and register a new PDO (Usually inGenericCANNodeOD.cin thegenericCANNodeId_initODfunction).:
CanOpenPdoOdEntries addedPDOEntries;
uint8_t success = 0;
// === TPDO 1A00: Maps 0x2031:00 === ------> buttons
if (canOpenPDO_addPDOToOD(memory, od, 0x190, CanOpenPdoType_TPDO, 1, &addedPDOEntries) == 0) {
CanOpenPDOMapping map = { .index = 0x1000, .subIndex = 0, .length = 1 };
success |= CanOpenOD_setValueInEntry(addedPDOEntries.mappingEntry, 1, &map, 4, 1, od); // Note: valueLength = 4 always here
if (success != 0) return 0;
} else {
return 0;
}
Important notes when mapping PDOs:
-
When mapping from a record, the first subindex starts at 1, not 0.
-
The last parameter
valueLengthinCanOpenOD_setValueInEntry()must always be set to 4 (when mapping PDOs) because it refers to the size of theCanOpenPDOMappingstruct, not the size of the actual data (e.g. 1 byte for a uint8).
Running the Simulation
Once everything is wired and configured:
-
Start the simulation
-
The node will enter "Pre-Operational state"
-
It will respond to NMT, SDO and LSS commands as per CANopen
Triggering PDOs
After setting up the mapping, PDOs must be transmitted either cyclically based on an event. For cyclic PDOs, you can configure a timer-based trigger (e.g. using the APDOServer actor). For event-driven PDOs, you may need to call the pdoServer.triggerPDO() message manually when your data changes.
Extending the Simulation
You can add more simulated nodes by:
-
Instantiating additional "GenericCanOpenNode" actors
-
Setting up individual OD content if needed