CAPL Script

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 start for variable initialization, on message for command response, and on timer for 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 command
  • UnlockCmd - Unlock command
  • WindowControlCmd - 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 DoorStatus message 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 states
  • windowTimer — Used to control window movement timing (automatic behavior requires time)
  • Four message variables — 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 start is 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:

  1. Event Filtering: on message LockCmd only responds to messages with ID 0x200
  2. Safety Check: If the window is moving, the operation is prohibited (prevents locking when window is halfway down)
  3. State Update: Modifies the internal variable doorLockState
  4. 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:

  1. Message Parsing: Uses this.byte(0) to access the received message data
  2. State Checks:
    • The door must be unlocked to control the window (safety consideration)
    • The window cannot accept new commands while moving
  3. Command Execution: Sets the windowMovement state and starts the timer based on command type

Tip: The this keyword in an on message event 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:

  1. Smooth Movement: Updates position every 20ms, moving 5% each time
  2. Boundary Check: Stops when reaching upper limit (0%) or lower limit (100%)
  3. State Reset: Sets windowMovement to 0 when movement ends
  4. 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:

  1. Data Packing: Maps internal state variables to message bytes
    • DoorStatus.byte(0) = door lock state
    • DoorStatus.byte(1) = window position
    • DoorStatus.byte(2) = window movement state
  2. Message Transmission: The output() function sends the message to the CAN bus
  3. 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 a message type 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

  1. Create a new CANoe project
  2. Add our DoorModule node in Simulation Setup (associate the .can file)
  3. 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

  1. Start measurement
  2. Observe DoorModule's log output in the Write window
  3. 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 start initialization: Setting initial state and starting timers
  • on message command response: Receiving and processing external commands
  • on timer automatic behavior: Implementing time-dependent business logic
  • output() 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

  1. Extension Exercise: Add a WindowAutoDown feature to DoorModule — automatically lower the window to 50% after unlocking. Hint: Trigger it in on message UnlockCmd.

  2. 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.

  3. Debugging Exercise: Intentionally introduce a bug in the code (for example, forget to check the windowMovement state), then use breakpoint debugging to locate the problem. Hint: Set breakpoints in CAPL Browser, step through execution, and observe variable value changes.

  4. Challenge Exercise: Design a lighting control simulation node that implements:

    • Receives LightCmd command (on/off)
    • Supports automatic lighting (turns on automatically when dark)
    • Sends LightStatus status feedback
    • Uses environment variables (on envVar) to simulate a light sensor