How to Develop a Simulation Node
Learning Objectives
After completing this article, you will be able to:
- Understand the design philosophy of simulation nodes and the state machine concept
- Master the methods for implementing ECU behavior models in CAPL
- Learn to use
on startfor variable initialization,on messagefor command response, andon timerfor automatic behavior - Build a complete, interactive simulation node case study
From Requirements to Design: The Simulation Node Mindset
In previous articles, we learned CAPL's basic syntax and event mechanisms. Now, it's time to integrate all this knowledge to build a real ECU behavior model.
What is a Simulation Node?
A Simulation Node is a virtual ECU written in CAPL within CANoe. It's not simply an "echo" (receiving and forwarding whatever comes in), but an entity with business logic: it can receive commands, update internal states, produce automatic behaviors, and report status information.
Think about what a real ECU does:
- Receives commands from other ECUs (e.g., "lock")
- Changes its internal state based on commands (e.g., door lock state)
- Executes automated tasks (e.g., window movement)
- Reports current status to the bus (e.g., "locked")
Our simulation node needs to do all of this.
The Door Module Case Study
Next, we'll build a Door Module (DoorModule) simulation node step by step. This module includes the following features:
Receiving Commands:
LockCmd- Lock commandUnlockCmd- Unlock commandWindowControlCmd- Window control command
Internal States:
- Door lock state (locked/unlocked)
- Window position (0-100%, where 0 means fully up and 100 means fully down)
Automatic Behavior:
- After receiving a window command, automatically execute the movement process (approximately 2 seconds to complete)
- Prevent receiving new commands during movement (simplified anti-pinch logic)
Status Feedback:
- Periodically send
DoorStatusmessage reporting current door lock state and window position
Note: This case study will be used throughout Articles 6, 7, and 8. In Article 7, we'll write test cases for it, and in Article 8, we'll integrate the simulation with testing.
Step 1: Initialization and State Definition
Every simulation node needs to initialize its internal state when measurement starts. This is like an ECU's self-test and initialization process after power-on.
In CAPL, we use the on start event to handle initialization:
variables
{
// Door lock state: 0=unlocked, 1=locked
byte doorLockState = 0;
// Window position: 0-100% (0=fully up, 100=fully down)
byte windowPosition = 0;
// Window movement state: 0=stopped, 1=moving up, 2=moving down
byte windowMovement = 0;
// Timers
msTimer windowTimer; // For smooth window movement
msTimer statusTimer; // For periodic status transmission
// Message declarations
message 0x200 LockCmd; // Lock command from central control
message 0x201 UnlockCmd; // Unlock command from central control
message 0x202 WindowCmd; // Window control command
message 0x300 DoorStatus; // Status message to dashboard
}
on start
{
write("DoorModule: Initialization started");
// Initialize to known state
doorLockState = 0; // Start unlocked
windowPosition = 0; // Windows fully up
windowMovement = 0; // Not moving
// Start periodic status transmission (every 500ms)
setTimer(statusTimer, 500);
write("DoorModule: Initialization complete - Unlocked, Windows up");
}
Let's understand this code:
Variable Declaration Section:
doorLockState,windowPosition,windowMovement— These are the simulation node's "memory," recording current stateswindowTimer— Used to control window movement timing (automatic behavior requires time)- Four
messagevariables — Declare the messages we'll handle and send
on start Event:
- Triggered once when measurement starts
- Sets initial state (unlocked, windows up)
- Starts timer for periodic status message transmission
Important:
on startis the most appropriate place to initialize variables. According to the official documentation, the measurement system is fully initialized at this point, and all CAPL functions can be safely used.
Step 2: Implementing Command Response
Now, we need to respond to commands from other ECUs. This is achieved through the on message event.
Let's first look at the core lock/unlock logic:
// Handle lock command
on message LockCmd
{
// Check if window is moving (safety check)
if (windowMovement != 0)
{
write("DoorModule: Cannot lock - window is moving");
return;
}
// Update internal state
doorLockState = 1;
write("DoorModule: Door locked");
}
// Handle unlock command
on message UnlockCmd
{
// Check if window is moving (safety check)
if (windowMovement != 0)
{
write("DoorModule: Cannot unlock - window is moving");
return;
}
// Update internal state
doorLockState = 0;
write("DoorModule: Door unlocked");
}
Code Explanation:
- Event Filtering:
on message LockCmdonly responds to messages with ID 0x200 - Safety Check: If the window is moving, the operation is prohibited (prevents locking when window is halfway down)
- State Update: Modifies the internal variable
doorLockState - Logging: Uses
write()to output to the Write window for debugging
Next is the window control command:
// Handle window control command
on message WindowCmd
{
byte command;
// Extract command from first byte of message
command = WindowCmd.byte(0);
// Only process if door is unlocked
if (doorLockState == 1)
{
write("DoorModule: Cannot control window - door is locked");
return;
}
// Only process if window is not currently moving
if (windowMovement != 0)
{
write("DoorModule: Cannot control window - already moving");
return;
}
if (command == 1) // Window down
{
windowMovement = 2; // Mark as moving down
setTimer(windowTimer, 20); // Update every 20ms for smooth movement
write("DoorModule: Window moving down");
}
else if (command == 2) // Window up
{
windowMovement = 1; // Mark as moving up
setTimer(windowTimer, 20); // Update every 20ms
write("DoorModule: Window moving up");
}
}
Code Explanation:
- Message Parsing: Uses
this.byte(0)to access the received message data - State Checks:
- The door must be unlocked to control the window (safety consideration)
- The window cannot accept new commands while moving
- Command Execution: Sets the
windowMovementstate and starts the timer based on command type
Tip: The
thiskeyword in anon messageevent represents "the message just received" — a core concept in CAPL's event-driven programming.
Step 3: Implementing Automatic Behavior
Window movement doesn't happen instantly — it takes time. In a real ECU, this is handled by the motor drive circuit. In our simulation node, we use on timer to simulate this process.
// Timer event for window movement
on timer windowTimer
{
byte movementStep = 5; // Move 5% per tick
if (windowMovement == 1) // Moving up
{
// Decrease position
windowPosition = windowPosition - movementStep;
if (windowPosition <= 0)
{
windowPosition = 0;
windowMovement = 0; // Stop moving
write("DoorModule: Window fully up");
}
}
else if (windowMovement == 2) // Moving down
{
// Increase position
windowPosition = windowPosition + movementStep;
if (windowPosition >= 100)
{
windowPosition = 100;
windowMovement = 0; // Stop moving
write("DoorModule: Window fully down");
}
}
// Continue timer if still moving
if (windowMovement != 0)
{
setTimer(windowTimer, 20);
}
}
Code Explanation:
- Smooth Movement: Updates position every 20ms, moving 5% each time
- Boundary Check: Stops when reaching upper limit (0%) or lower limit (100%)
- State Reset: Sets
windowMovementto 0 when movement ends - Cyclic Timer: If still moving, resets the timer to continue the next update
This way, window movement becomes a gradual process rather than an instant jump, matching real ECU behavior.
Step 4: Status Feedback
An ECU not only executes commands but also proactively reports its status. In automotive networks, the instrument cluster, BCM (Body Control Module), and other modules need to know the status of each component.
We use a periodic timer to send status messages:
on timer statusTimer
{
// Pack current state into DoorStatus message
// Byte 0: Door lock state (0=unlocked, 1=locked)
DoorStatus.byte(0) = doorLockState;
// Byte 1: Window position (0-100)
DoorStatus.byte(1) = windowPosition;
// Byte 2: Window movement state (0=stopped, 1=moving up, 2=moving down)
DoorStatus.byte(2) = windowMovement;
// Set ID if not already set
DoorStatus.id = 0x300;
// Send message to bus
output(DoorStatus);
// Reset timer for next transmission
setTimer(statusTimer, 500);
}
Code Explanation:
- Data Packing: Maps internal state variables to message bytes
DoorStatus.byte(0)= door lock stateDoorStatus.byte(1)= window positionDoorStatus.byte(2)= window movement state
- Message Transmission: The
output()function sends the message to the CAN bus - Periodic Update: Resets the timer after each transmission, achieving 500ms transmission intervals
Key Concept:
output()is the core function in CAPL for sending messages to the bus. It accepts amessagetype variable and places it on the specified CAN channel.
Complete Code Listing
Combining all the parts together, here is the complete simulation node:
variables
{
// Internal state variables
byte doorLockState = 0; // 0=unlocked, 1=locked
byte windowPosition = 0; // 0-100% (0=up, 100=down)
byte windowMovement = 0; // 0=stopped, 1=up, 2=down
// Timers
msTimer windowTimer; // For smooth window movement
msTimer statusTimer; // For periodic status transmission
// Message declarations
message 0x200 LockCmd; // Lock command
message 0x201 UnlockCmd; // Unlock command
message 0x202 WindowCmd; // Window control
message 0x300 DoorStatus; // Status message
}
on start
{
write("DoorModule: Initialization started");
// Initialize to known state
doorLockState = 0;
windowPosition = 0;
windowMovement = 0;
// Start periodic status transmission
setTimer(statusTimer, 500);
write("DoorModule: Ready - Unlocked, Windows up");
}
on message LockCmd
{
// Safety check
if (windowMovement != 0)
{
write("DoorModule: Cannot lock - window is moving");
return;
}
doorLockState = 1;
write("DoorModule: Door locked");
}
on message UnlockCmd
{
// Safety check
if (windowMovement != 0)
{
write("DoorModule: Cannot unlock - window is moving");
return;
}
doorLockState = 0;
write("DoorModule: Door unlocked");
}
on message WindowCmd
{
byte command;
command = WindowCmd.byte(0);
// Check door state
if (doorLockState == 1)
{
write("DoorModule: Cannot control window - door is locked");
return;
}
// Check if already moving
if (windowMovement != 0)
{
write("DoorModule: Cannot control window - already moving");
return;
}
if (command == 1) // Window down
{
windowMovement = 2;
setTimer(windowTimer, 20);
write("DoorModule: Window moving down");
}
else if (command == 2) // Window up
{
windowMovement = 1;
setTimer(windowTimer, 20);
write("DoorModule: Window moving up");
}
}
on timer windowTimer
{
byte movementStep = 5;
if (windowMovement == 1) // Moving up
{
windowPosition = windowPosition - movementStep;
if (windowPosition <= 0)
{
windowPosition = 0;
windowMovement = 0;
write("DoorModule: Window fully up");
}
}
else if (windowMovement == 2) // Moving down
{
windowPosition = windowPosition + movementStep;
if (windowPosition >= 100)
{
windowPosition = 100;
windowMovement = 0;
write("DoorModule: Window fully down");
}
}
// Continue if still moving
if (windowMovement != 0)
{
setTimer(windowTimer, 20);
}
}
on timer statusTimer
{
// Pack status into message
DoorStatus.byte(0) = doorLockState;
DoorStatus.byte(1) = windowPosition;
DoorStatus.byte(2) = windowMovement;
DoorStatus.id = 0x300;
// Transmit status
output(DoorStatus);
// Schedule next transmission
setTimer(statusTimer, 500);
}
How to Test This Simulation Node?
Step 1: Create a CANoe Project
- Create a new CANoe project
- Add our DoorModule node in
Simulation Setup(associate the .can file) - Add an Interactive Generator panel for manually sending commands
Screenshot: CANoe Simulation Setup Window
[Screenshot of CANoe Simulation Setup window should be displayed here]
Figure 6.1: DoorModule Simulation Node Configuration
Description: This screenshot shows the steps to add the DoorModule simulation node and Interactive Generator panel in CANoe Simulation Setup.
Step 2: Configure the Interactive Generator
In the Interactive Generator, configure three messages:
| Message ID | Name | Data |
|---|---|---|
| 0x200 | LockCmd | Any (ignored) |
| 0x201 | UnlockCmd | Any (ignored) |
| 0x202 | WindowCmd | Byte 0 = 1 (down) or 2 (up) |
Step 3: Run the Test
- Start measurement
- Observe DoorModule's log output in the Write window
- Use Interactive Generator to send commands:
- Send LockCmd -> Observe door lock state change
- Send WindowCmd (0x01) -> Observe window moving down
- Send WindowCmd (0x02) -> Observe window moving up
Screenshot: CANoe Write Window Output
[Screenshot of CANoe Write window should be displayed here]
Figure 6.2: DoorModule Runtime Log
Description: This screenshot shows the log output of the DoorModule simulation node during runtime, including initialization, command processing, and state changes.
Step 4: Observe Status Messages
Add a Trace window or Data Monitor to observe changes in the 0x300 DoorStatus message:
- Byte 0 should show door lock state (0=unlocked, 1=locked)
- Byte 1 should show window position (0-100)
- Byte 2 should show window movement state (0=stopped, 1=up, 2=down)
Advanced Thinking: How to Make the Simulation More Realistic?
The current simulation node has basic functionality, but it can be further improved:
1. Anti-Pinch Logic
Real window controls have anti-pinch protection — they automatically stop when encountering resistance. In simulation, we can model this logic:
// Enhanced window movement with anti-pinch simulation
on timer windowTimer
{
byte movementStep = 5;
byte resistance = 0; // Simulated resistance (0=none, 1=resistance detected)
// Randomly simulate resistance (10% chance)
// Note: random() returns a value between 0.0 and 1.0
if (random() < 0.1)
{
resistance = 1;
}
if (windowMovement == 1) // Moving up
{
if (resistance == 1)
{
write("DoorModule: Anti-pinch triggered - window stopped");
windowMovement = 0;
return;
}
windowPosition = windowPosition - movementStep;
if (windowPosition <= 0)
{
windowPosition = 0;
windowMovement = 0;
}
}
// ... similar for moving down ...
if (windowMovement != 0)
{
setTimer(windowTimer, 20);
}
}
2. Error Handling and Diagnostics
Add handling for error conditions:
on message WindowCmd
{
byte command;
command = this.byte(0);
// Check for invalid command
if (command > 2)
{
write("DoorModule: ERROR - Invalid window command: %d", command);
return;
}
// ... rest of the code ...
}
3. State Machine Pattern
For more complex logic, you can use the state machine pattern to clearly separate different operational states:
enum DoorState
{
DOOR_UNLOCKED_IDLE,
DOOR_LOCKED_IDLE,
DOOR_WINDOW_MOVING
};
DoorState currentState = DOOR_UNLOCKED_IDLE;
// State transition logic
void transitionTo(DoorState newState)
{
write("DoorModule: State transition %d -> %d", currentState, newState);
currentState = newState;
}
Summary
By building the Door Module simulation node, we learned:
- Simulation node design philosophy: From requirements analysis to state definition
on startinitialization: Setting initial state and starting timerson messagecommand response: Receiving and processing external commandson timerautomatic behavior: Implementing time-dependent business logicoutput()status feedback: Proactively sending status information
This simulation node features:
- Internal state (door lock, window position, movement state)
- Command response (lock, unlock, window control)
- Automatic behavior (smooth movement process)
- Safety checks (prevent operations while moving)
- Status feedback (periodic status message transmission)
Exercises
-
Extension Exercise: Add a
WindowAutoDownfeature to DoorModule — automatically lower the window to 50% after unlocking. Hint: Trigger it inon message UnlockCmd. -
Optimization Exercise: The current window movement is linear. Change it to non-linear movement: move quickly for the first 80% (10%/tick) and slowly for the last 20% (2%/tick), simulating real window characteristics.
-
Debugging Exercise: Intentionally introduce a bug in the code (for example, forget to check the
windowMovementstate), then use breakpoint debugging to locate the problem. Hint: Set breakpoints in CAPL Browser, step through execution, and observe variable value changes. -
Challenge Exercise: Design a lighting control simulation node that implements:
- Receives
LightCmdcommand (on/off) - Supports automatic lighting (turns on automatically when dark)
- Sends
LightStatusstatus feedback - Uses environment variables (
on envVar) to simulate a light sensor
- Receives