CAN Bus Adapter

The CAN Bus Adapter is a collection of tools that lets you develop and integrate tests and simulations involving communication over a CAN bus within the miniHIL platform.

The set of tools includes:

  • Runtime and model support libraries

    • Easy sending and receiving can frames to the physical can bus from any eTrice actor

    • Supports Remote Transmission Requests (RTR)

    • Supports Extended CAN Frames

  • An DBC-to-eTrice model generator

    • Automatically creates ROOM ActorClasses that support automatic encoding, decoding, packing and unpacking of can frame signals

The CAN generator is part of the gradle build. It is part of the normal assemble process. If needed the generator can also be invoked separately: .\gradlew.bat generateDBCConf. The gradle task generates room models for all files with .dbcconf extension in the model/model-user folder in the miniHilProject as well as in all configured library projects.

CAN HAL

Currently, two separate CAN instances with a pair of physical pins on the MiniHiL board each, are available.

Note The sample point is currently fixed at 75%. If this leads to problems with high CAN-FD bitrates talk to us.

Structure

Structure and dependencies of the can actors

CAN structure

How to use the HAL

  • Connect your DuT to the MiniHiL board. For blue miniHIL boards (v2.3 and higher) have a look at the pinout below. For older boards the following warning is important.

Warning On old (pre V2.3, green) miniHIL boards the pinout of the CAN SUB-D connector is mirrored. I.e. the following pins are swapped: 1 <→ 5, 2 <→ 4, 3 <→ 3, 6 <→ 9, 7 <→ 8.
can pins
canport minihil
  • Import the CAN actor.

import can.api.CANService.*
import can.platform.Stm32CANService.*
  • Create an instance of either AStm32CANService1 or AStm32CANService2 (or both) and connect the ports.

ActorRef can1: AStm32CANService1
ActorRef can2: AStm32CANService2
  • Initialize the CAN actor by sending an open(DCANParams) message to the CAN service ctl port. Make sure both nominal and data bitrate match with the DuT.

DCanParams params = {
    .baudRate_kbs = 40,
    .baudRate_kbs_fd = 125
};
// CANctl bound to ctl port of CAN service actor
CANctl.open(&params);
Important Both baudRate_kbs and baudRate_kbs_fd params must be set to valid values. Otherwise the initialization of the CAN service will fail and it will respond to the open request with a ctl.errorOpen notification message.
  • Wait until you receive the opened() message in response.

  • You are now able to send and receive CAN messages through the CANData port.

   CANMessage msg;
	msg.id = 0x00;
	msg.isExtendedFrame = false;
	//Length has to be a value of {1,2,3,4,5,6,7,8,12,16,20,24,32}
	msg.length = 16;
	msg.data[0] = 0x00;
	msg.data[1] = 0x01;
	//...
	msg.data[15] = 0x0f;
	msg.isRemoteFrame = false;
	msg.isBitRateSwitched = true;
	msg.isFDFrame = true;

   CANdata.send(&msg);

Example Application (HAL only)

This Application will initialize CAN and send one Message per second while waiting for incoming Messages.

RoomModel MiniHilProject {

	import can.api.CANService.*
	import can.platform.Stm32CANService.*
	import etrice.api.timer.PTimer

	ActorClass Application {
		Structure {

			conjugated Port CANctl: PCANCtrl
			conjugated Port CANdata: PCANData

			ActorRef can1: AStm32CANService1
			ActorRef can2: AStm32CANService2

			Binding CANdata and can1.bus
			Binding CANctl and can1.ctl

			SAP timer: PTimer
		}
		Behavior {
			StateMachine {
				State Active {
				}
				State Initializing
				State FatalError
				Transition init0: initial -> Initializing {
					action '''
						//initialize CAN with Nominal Bitrate = 40kb and Data Bitrate = 125kb
						DCanParams params = {
						    .baudRate_kbs = 40,
						    .baudRate_kbs_fd = 125
						};
						CANctl.open(&params);
					'''
				}
				Transition tr0: Initializing -> Active {
					triggers {
						<opened: CANctl>
					}
					action '''
						timer.startTimer(1000);
					'''
				}
				Transition tr1: Initializing -> FatalError {
					triggers {
						<errorOpen: CANctl>
					}
					action '''
						etLogger_logError("Failed to initialize CANServiceTest!");
					'''
				}
				Transition tr2: Active -> Active {
					triggers {
						<received: CANdata>
					}
					action '''
						// received CAN Message
						uint8 * d = transitionData->data;
					'''
				}
				Transition tr3: Active -> Active {
					triggers {
						<errorRecv: CANctl>
					}
					action '''// received incorrect CAN Message'''
				}
				Transition tr4: Active -> Active {
					triggers {
						<errorSend: CANctl>
					}
					action '''// sending CAN Message failed'''
				}
				Transition tr5: Active -> Active {
					triggers {
						<timeout: timer>
					}
					action '''
						// send CAN Message on timer timeout
						CANMessage msg;
						msg.id = 0x00;
						msg.isExtendedFrame = 0;
						//Length has to be a value of {1,2,3,4,5,6,7,8,12,16,20,24,32}
						msg.length = 16;
						msg.data[0] = 0x00;
						msg.data[1] = 0x01;
						//...
						msg.data[15] = 0x0f;
						msg.isRemoteFrame = 0;
						msg.isBitRateSwitched = true;
						msg.isFDFrame = true;

						CANdata.send(&msg);
					'''
				}
			}
		}
	}
}

DBC Generator - Signal-Level CAN Abstraction

While the CAN HAL (described above) works at the raw frame level, the DBC generator provides a signal-level abstraction. It reads a standard .dbc file and automatically generates eTrice ROOM actors that handle encoding, decoding, packing and unpacking of CAN signals.

This means you can send and receive individual signals (e.g. temperature, sw_version) without manually constructing CAN frames.

DBC File Format

A DBC file (.dbc) is a common format for describing CAN bus networks. It defines:

  • Nodes (BU_): ECUs on the bus

  • Messages (BO_): CAN frames with ID, length, and transmitting node

  • Signals (SG_): Data fields within messages with bit position, length, byte order, factor, offset, and receiving nodes

Note
Example DBC file (complete, valid minimal DBC)

The following example is a complete DBC that can be loaded directly into DBC editors/tools. It is equivalent to a file exported with Vector CANdb++.

VERSION ""

NS_ :
	NS_DESC_
	CM_
	BA_DEF_
	BA_
	VAL_
	CAT_DEF_
	CAT_
	FILTER
	BA_DEF_DEF_
	EV_DATA_
	ENVVAR_DATA_
	SGTYPE_
	SGTYPE_VAL_
	BA_DEF_SGTYPE_
	BA_SGTYPE_
	SIG_TYPE_REF_
	VAL_TABLE_
	SIG_GROUP_
	SIG_VALTYPE_
	SIGTYPE_VALTYPE_
	BO_TX_BU_
	BA_DEF_REL_
	BA_REL_
	BA_DEF_DEF_REL_
	BU_SG_REL_
	BU_EV_REL_
	BU_BO_REL_
	SG_MUL_VAL_

BS_:

BU_: testbench ecu

BO_ 256 ecu_tx1: 3 ecu
 SG_ sw_version : 0|8@1+ (1,0) [0|255] "" Vector__XXX
 SG_ temp1 : 16|8@1+ (1,-25) [-25|215] "deg C" testbench

BO_ 250 ecu_tx2: 8 ecu
 SG_ err_bool : 0|1@1+ (1,0) [0|1] "" testbench
 SG_ float_val : 8|8@1- (0.125,0) [-5|26] "floats" testbench

BO_ 100 ecu_rx1: 2 testbench
 SG_ temp_min : 0|8@1+ (1,-25) [-25|215] "deg C" ecu
 SG_ temp_max : 8|8@1+ (1,-25) [-25|215] "deg C" ecu

Signal format 16|8@1+ (1,-25) [-25|215]:

  • 16|8 - start bit 16, length 8 bits

  • @1+ - little-endian (@1), unsigned (+); use @1- for signed

  • (1,-25) - factor 1, offset -25 → physical value = raw * factor + offset

  • [-25|215] - valid range min|max

Place the .dbc file in MiniHilProject/model-user/ (e.g. in a can/ subfolder).

DBC Configuration File (.dbcconf)

The .dbcconf file tells the code generator what to produce from a DBC file. It sits next to the .dbc file (e.g. in model-user/can/).

A complete .dbcconf file structure:

generate Example from "example.dbc" {

    adapter ecu {
        nodes = ecu
    }

    adapter testbench {
        nodes = testbench
    }
    adapter FullBus {
         nodes = ecu, testbench
    }
	portmapper ecu {
		adapter = ecu
        generateContainer {
            canservice = can.platform.Stm32CANService.AStm32CANService1
        }
    }
}

generate block (top level)

The generate block wraps all adapters and portmappers:

generate Example from "example.dbc" {
    // adapters and portmappers go here
}
  • Example - a name you choose. It becomes part of the generated file names (e.g. CANAdapters_Example.room).

  • "example.dbc" - the DBC file to read (path relative to this .dbcconf file).

adapter - selects signals to send and receive over the CAN bus

The adapter block generates adapter ActorClasses in the ROOM model that represent one node or a group of nodes from the DBC file. The nodes property picks one or more ECU nodes from the DBC (BU_: line). The adapters act as proxies for this set of nodes on the actual CAN bus, forwarding messages and signals to and from the bus.

Given this DBC content:

BU_: testbench ecu

BO_ 256 ecu_tx1: 3 ecu        ← sent by node "ecu"
 SG_ temp1 ...
 SG_ sw_version ...

BO_ 250 ecu_tx2: 8 ecu        ← sent by node "ecu"
 SG_ err_bool ...
 SG_ float_val ...

BO_ 100 ecu_rx1: 2 testbench  ← sent by node "testbench"
 SG_ temp_min ...
 SG_ temp_max ...

The generator creates the following DUT adapters:

Adapter TX Messages RX Messages

ecu

ecu_tx1, ecu_tx2

ecu_rx1

ecuPlant

ecu_rx1

ecu_tx1, ecu_tx2

testbench

ecu_rx1

ecu_tx1, ecu_tx2

testbenchPlant

ecu_tx1, ecu_tx2

ecu_rx1

FullBus

(none)

ecu_tx1, ecu_tx2, ecu_rx1

FullBusPlant

ecu_tx1, ecu_tx2, ecu_rx1

(none)

Note

With a FullBus adapter declaration (nodes = ecu, testbench), the adapter declaration represents the complete CAN bus network. In that case, the DUT/environment split can become less meaningful.

When all nodes are included in a single adapter declaration:

  • Plant side adapter forwards all signals/messages from the etrice and test actors to the CAN bus. It cannot be used to receive signals/messages from the bus.

  • non-Plant side adapter forwards signals/messages from CAN bus to etrice and test actors. It cannot be used to send messages to the bus.

Note Although SG_sections can also define receiver nodes, this information is currently ignored by the generator. The generator only checks sender nodes at the message level when determining which direction the corresponding adapter port takes.

portmapper - consolidates all signals into one port

A portmapper takes one adapter and flattens all its signals into a single eTrice protocol. Instead of wiring one port per CAN message, you get one port with all signals as direct messages.

	portmapper ecu {
		adapter = ecu
        generateContainer {
            canservice = can.platform.Stm32CANService.AStm32CANService1
        }
    }
  • adapter = ecu - which adapter to consolidate (must be defined above).

  • generateContainer - generates a ready-to-use container actor (Aecu_SinglePortCANContainerPlant) that internally wires the CAN hardware service, the adapter, and the portmapper together. You only need to instantiate this single actor in your project.

  • canservice = …​ - the platform-specific CAN service actor class to use inside the container. (AStm32CANService1 or AStm32CANService2 for the miniHIL board).

What the portmapper generates:

  • Pecu_SinglePortProtocol - a protocol where each DBC signal is an eTrice message:

    • temp1(int32), float_val(float32), temp_min(int32), etc.

    • setMultipleSignals(DSingleportMultiSignal_ecu) - for buffered multi-signal sends.

  • DSingleportMultiSignal_ecu - a data class with setter operations per signal, used for buffered sends.

  • Aecu_SinglePortCANContainerPlant - the container actor (if generateContainer was specified).

Generated Files

The generator produces ROOM model files in model-gen/can/:

File Description

CANAdapters_<Config>.room

Contains adapter actors. <Config> is the name from generate <Config> from "…​". Contains both a Plant variant (proxies environment nodes) and a non-Plant variant (proxies DUT nodes).

CANSignals_<dbc>.room

Signal data types and per-message multi-signal protocol classes. <dbc> is the DBC file name without extension.

SinglePortAdapter_<Mapper>.room

Single-port adapter and container actors (if generateContainer was specified). <Mapper> is the portmapper name. Contains A<Adapter>_SinglePortCANContainerPlant (proxies environment nodes) and A<Adapter>_SinglePortCANContainer (proxies DUT nodes).

SinglePortSignals_<Mapper>.room

Consolidated single-port protocol (P<Adapter>_SinglePortProtocol) with all signals as messages, plus the DSingleportMultiSignal_<Adapter> data class for buffered sends.

Build Integration

The DBC generator runs as part of the regular miniHIL build.

After changing .dbc or .dbcconf files, run the miniHIL build to regenerate the model files in model-gen/can/.

During generation, all .dbcconf and .dbc files in model and model-user directories are scanned.

Important Ensure model-gen is present in MiniHilProject/modelpath.

Integration in MiniHilProject.room

The generator creates two container variants per portmapper:

  • Plant: an ActorClass that defines the interface (signal ports) that your test actor should implement.

  • non-Plant: the actual adapter container that sits between your test actor and the CAN service, acting as a signal/message proxy.

Container Purpose

Aecu_SinglePortCANContainerPlant

Abstract base class defining the test actor’s interface (port structure).

Aecu_SinglePortCANContainer

The active adapter instance between test actor and CAN service. Handles signal encoding/decoding and message routing.

Set up the test harness

To use the generated signals in your test actor, instantiate the adapter container and wire it to your test actor:

import can.api.CANService.PCANCtrl
import can.SinglePortAdapter_ecu.Aecu_SinglePortCANContainerPlant
import can.SinglePortSignals_ecu.Pecu_SinglePortProtocol

ActorClass Application {
    Structure {
		ActorRef canAdapter: Aecu_SinglePortCANContainer
        ActorRef testcase: AMyTestActor

        Binding testcase.canCtrl and canAdapter.serviceCtrl
        Binding canAdapter.signals and testcase.can
    }
}
ActorClass TestPortProvider {
    Interface {
        conjugated Port canCtrl: PCANCtrl
		conjugated Port can: Pecu_SinglePortProtocol
    }
}

Usage in CaGe Tests

With the generated single-port protocol, signals can be sent directly by name.

Opening the CAN bus

Step OpenCan(int bitrateKbs):
    timeout 200 ms
    action
        ``
            DCanParams canParams = {
                .baudRate_kbs = bitrateKbs,
                .openStr = "",
                .alwaysSendOpenedReply = true
            };
        ``
        canCtrl.open(``&canParams``)
    reaction
        expect canCtrl.opened
;

Direct send (single signal)

Sends immediately one CAN frame per signal:

Step testSend_direct:
    action
        can.temp_min(-5)
        can.temp_max(80)
;

Buffered send (multiple signals)

Set multiple signal values and transmit them together. Useful when signals share the same CAN message:

Step testSend_buffered:
    action
        ``
			DSingleportMultiSignal_ecu multipleSignals;
			DSingleportMultiSignal_ecu_init(&multipleSignals);
			DSingleportMultiSignal_ecu_setTemp_min(&multipleSignals, -10);
			DSingleportMultiSignal_ecu_setTemp_max(&multipleSignals, 85);
        ``
        can.setMultipleSignals(``&multipleSignals``)
;

Receiving CAN signals

To receive signals sent by the DUT using a reaction block:

Step waitForTemperature:
    timeout 500 ms
    reaction
        expect can.temp1 > 20
;

Quick Start

  1. Place your .dbc file in MiniHilProject/model-user/can/.

  2. Create a .dbcconf file next to it defining adapters and portmapper.

  3. Ensure model-gen is present in MiniHilProject/modelpath

  4. Run .\gradlew.bat assemble (or .\gradlew.bat generateDBCConf).

  5. Import generated actors in your MiniHilProject.room.

  6. Wire canAdapter.serviceCtrl and canAdapter.signals to your test actor.

  7. Write CaGe tests using the generated signal-level protocol.

  8. Build and flash to the miniHIL board.

Ingress filtering CAN messages by frame id Advanced

CANFiltering
Flow to enable/disable message filtering

If many actors are connected to the HAL canBus port each actor will receive a copy of the received can message. This might lead to performance issues. But, in many cases each connected actor only reacts to can messages with specific ids. In those cases ingress filtering can be used.

The PCANData Protocol supports message filtering for each connected user port. To enable filtering you have to first stop forwarding of all message by sending a disableForwarding message. Afterwards you may configure which messages your actor should receive using the enableForwardingWithFilter message with DCanFilterConfig payload. NOTE that the DCanFilterConfig only takes a pointer to a uint32 array. To cater for different filter needs (number of valid ids) the array of valid canIDs must be kept within the users actor. The PortClass directly accesses that array and does not create a copy.