CAPL Script

Debugging CAPL

Learning Objectives

After completing this article, you will be able to:

  • Master the core debugging methods for CAPL programs
  • Use the write() function proficiently for log-based debugging
  • Set up and use breakpoints in CAPL Browser
  • Apply a systematic error troubleshooting process to locate problems
  • Establish good debugging habits to improve development efficiency

Debugging: From Guessing to Seeing

Have you ever encountered this scenario: your program compiles successfully, but its runtime behavior is completely different from what you expected? You start questioning everything, including your own code.

This is perfectly normal. But here is a secret: good programmers never need to guess.

The core philosophy of debugging is simple: let evidence speak. When a program has problems, you should not just stare at the code in frustration. Instead, use tools to "see" - see the values of variables, the program execution flow, and message transmission.

CAPL provides three debugging "weapons":

  1. write() - The simplest but most commonly used debugging tool
  2. Breakpoints - Pause the program and inspect the execution state
  3. Watch Window - Monitor variable changes in real-time

Let us learn each of these weapons in detail.

Important Note: Debugging is a systematic way of thinking. Develop the habit of using tools to verify hypotheses, rather than relying on intuition and guessing.


Weapon One: Write Window - Your Most Reliable Debugging Partner

The write() function is the most fundamental debugging tool in CAPL. It serves as the programmer's "eyes", helping you see the internal running state of your program.

write() Function Basics

The write() function outputs text messages to the Write Window, with a format similar to C language's printf.

void write(char format[], ...);

Basic Usage

Let us start with a simple example:

on key 'h'
{
    // Print simple text
    write("Hello World!");

    // Print variable value
    int engineSpeed = 3000;
    write("Engine Speed: %d RPM", engineSpeed);

    // Print complex data
    float fuelLevel = 0.75;
    write("Fuel Level: %.1f%%", fuelLevel * 100);
}

Press the 'h' key, and the Write Window will display:

Hello World!
Engine Speed: 3000 RPM
Fuel Level: 75.0%

Practical Debugging Patterns

In actual development, here are the three most common patterns for using write():

Pattern 1: Tracking Program Execution Flow

on start
{
    write("Program started: initializing variables");
    initializeVariables();
    write("Program started: variables initialized");
}

void initializeVariables()
{
    write("Initialize function called");
    // Initialization code...
}

By inserting write() at key locations, you can confirm whether the code executes as expected.

Pattern 2: Outputting Message Content

on message EngineData
{
    // Use write to output received message content
    write("Received EngineData message");
    write("  ID: 0x%X", this.ID);
    write("  Length: %d bytes", this.DLC);
    write("  Data: %02X %02X %02X %02X",
          this.byte(0), this.byte(1),
          this.byte(2), this.byte(3));
}

This helps you confirm whether a message was received and what its contents are.

Pattern 3: Outputting Signal Values

on message EngineData
{
    // Verify signal values
    write("Engine Speed: %d", this.EngineSpeed);
    write("Engine Temp: %d", this.EngineTemp);

    // Add conditional output
    if (this.EngineTemp > 110) {
        write("Warning: Engine temperature too high!");
    }
}

Format Specifier Quick Reference

Specifier Meaning Example
%d Decimal integer 123
%u Unsigned decimal integer 456
%X Hexadecimal (uppercase) 0x7B
%x Hexadecimal (lowercase) 0x7b
%I64X 64-bit hexadecimal (uppercase) 0x1A2B3C4D5E6F
%f Floating point 3.141593
%.2f Floating point (2 decimal places) 3.14
%s String -
%I64u 64-bit unsigned integer 12345678901234

Practical Tip: Develop the habit of adding write() statements at key locations. The output should be detailed enough to be useful, but not so excessive that it becomes noise - find the right balance.


Weapon Two: Breakpoints - Making Time Stand Still

write() is powerful, but it has a limitation: it can only tell you "what happened in the past", not "what is happening right now".

This is when you need Breakpoints.

What Is a Breakpoint?

A breakpoint is a marker in your program. When execution reaches a breakpoint, the program pauses, allowing you to inspect the current state: variable values, memory contents, call stack, and more.

Important Note: A breakpoint is like freezing time, allowing you to carefully observe the program's state at that precise moment.

Setting Breakpoints

Setting breakpoints in CAPL Browser is straightforward:

Method 1: Click to the Left of the Line Number

  1. Open a .can file
  2. Find the line of code where you want to set a breakpoint
  3. Click the gray area to the left of the line number
  4. A red dot appears, indicating the breakpoint is set

Method 2: Keyboard Shortcut

  1. Place the cursor on the target line
  2. Press F9
  3. The breakpoint marker appears

Method 3: Right-Click Menu

  1. Right-click on the target line
  2. Select Toggle Breakpoint

[!SCREENSHOT]
Location: CAPL Browser code editor
Content: Setting a breakpoint to the left of a line number
Annotation: Red box highlighting the breakpoint marker (red dot)

Breakpoint Window Management

CAPL Browser provides a dedicated Breakpoint Window to manage all breakpoints:

  1. To open: Windows -> Breakpoint Window
  2. Features:
    • View all set breakpoints
    • Enable/disable individual breakpoints
    • Delete unnecessary breakpoints
    • Quickly jump to breakpoint locations

[!SCREENSHOT]
Location: CAPL Browser > Breakpoint Window
Content: Displaying the breakpoint list
Annotation: Highlight the Enabled column and double-click jump functionality

Using Breakpoints for Debugging

After setting breakpoints, how do you use them for debugging?

Step 1: Run the Program

  1. Set breakpoints in your code
  2. Start CANoe measurement
  3. The program will pause at breakpoints

Step 2: Inspect the State

When the program pauses, you can inspect:

Variable Values

  • Hover your mouse over a variable to see its current value
  • Or use the Watch Window to view values

Call Stack

  • View the function call sequence
  • Find the call chain where the problem occurs

Memory Contents

  • View message object contents
  • Check array and structure values

Step 3: Step Through Execution

After pausing at a breakpoint, you can:

  • Step Over (F10): Execute the next line of code without entering function internals
  • Step Into (F11): Execute the next line; if it is a function call, enter the function
  • Step Out (Shift+F11): Return from the current function to its caller

Step 4: Continue Execution

When finished debugging:

  • Continue (F5): Resume program execution until the next breakpoint
  • Stop: Stop debugging

Conditional Breakpoints

Sometimes you only want to pause under specific conditions, such as when a variable equals a particular value:

  1. Right-click on the breakpoint
  2. Select Break Test/Simulation
  3. Set the condition (e.g., speed > 3000)

This way, the program only pauses when the condition is met, greatly improving debugging efficiency.

Tip: Conditional breakpoints are primarily used in Test Modules. In regular simulation nodes, it is more common to use if statements combined with write() to achieve similar results.

Hands-On: Using Breakpoints to Find Logic Errors

Let us learn how to use breakpoints through a practical example.

Scenario: You have code that checks engine speed:

on message EngineData
{
    if (this.EngineSpeed > 3000) {
        this.WarningLight = 1;  // Turn on warning light
        output(WarningMessage);
    }
}

But you discover that the warning light turns on even when the engine speed is 2500 RPM. This is strange.

Debugging with Breakpoints:

  1. Set a breakpoint on the line if (this.EngineSpeed > 3000)
  2. Start measurement
  3. When an EngineData message is received, the program pauses
  4. Observe the actual value of this.EngineSpeed
  5. You might discover the value is actually 3200 (hexadecimal 0x7D0)
  6. Check the database configuration and find the unit was set incorrectly (should be RPM but is actually a raw value)

The breakpoint lets you see the "truth" directly.


Weapon Three: Watch Window - Real-Time Variable Monitoring

Breakpoints are powerful, but sometimes you need to continuously monitor a variable's changes without having the program pause at a breakpoint every time.

This is where the Watch Window comes in.

Opening the Watch Window

  1. While in debug mode (program paused at a breakpoint)
  2. Select Windows -> Watch Window from the menu

Or use the keyboard shortcut Alt+2.

Adding Variables to Watch

Method 1: Drag and Drop

  1. Select a variable name in the code
  2. Drag it to the Watch Window
  3. The variable is added to the monitoring list

Method 2: Type Directly

  1. Double-click in the Watch Window
  2. Type the variable name
  3. Press Enter to confirm

Method 3: Right-Click

  1. Right-click on a variable
  2. Select Add Watch

Watch Window Features

The Watch Window provides the following information:

Column Meaning
Name Variable name
Value Current value
Type Data type
Address Memory address (advanced feature)

[!SCREENSHOT]
Location: CAPL Browser > Watch Window
Content: Displaying the variable monitoring list
Annotation: Highlight the Value column showing real-time variable values

Hands-On: Monitoring a Timer

variables {
    timer sendTimer;
}

on start
{
    setTimer(sendTimer, 100);  // 100ms timer
}

on timer sendTimer
{
    // Set breakpoint here to observe sendTimer state
    output(HeartbeatMessage);
    setTimer(sendTimer, 100);
}

Set a breakpoint in on timer sendTimer, then view the sendTimer values in the Watch Window. You can observe:

  • sendTimer.active - Whether the timer is active
  • sendTimer.time - Remaining time

Watching Complex Data Structures

The Watch Window can also display complex data structures:

message Objects

EngineData.ID = 0x100
EngineData.DLC = 8
EngineData.EngineSpeed = 3000

Arrays

messageArray[0].ID = 0x100
messageArray[1].ID = 0x200

This allows you to drill down into the internals of data structures.

Practical Tip: For variables you frequently need to observe, create a "debugging variable list" and add them all to the Watch Window at once to improve debugging efficiency.


Common Error Patterns and Troubleshooting Workflow

Experienced programmers know that errors often follow patterns. Mastering common error patterns helps you locate problems quickly.

Error Pattern 1: Compile Errors

Symptoms

The program fails to compile, and the Output window displays error messages.

Common Causes and Solutions

1. Syntax Errors

// ❌ Wrong
on messgae EngineData  // Typo: messgae should be message

// ✅ Correct
on message EngineData

2. Undeclared Variables

// ❌ Wrong: using an undeclared variable
speed = 3000;

// ✅ Correct: declare first
int speed;
speed = 3000;

3. Type Mismatch

// ❌ Wrong: dword cannot be directly assigned a string
dword id = "EngineData";

// ✅ Correct: use a message object
message EngineData msg;

Troubleshooting Workflow

  1. Check the Output Window: Read the compile error messages carefully
  2. Locate the Error Line: Error messages typically indicate the file and line number
  3. Check Spelling: Especially keywords and variable names
  4. Verify Types: Ensure assignment types match
  5. Review Context: Sometimes the error line is not the actual problem; check the lines before and after

Practical Tip: Start fixing from the first error. Sometimes one error triggers a cascade of subsequent errors. After fixing the first one, recompile to see if it resolves multiple issues.

Error Pattern 2: Runtime Logic Errors

Symptoms

The program compiles successfully, but its runtime behavior does not match expectations.

Common Causes

1. Incorrect Conditional Logic

// ❌ Wrong: using the wrong comparison operator
if (speed > 3000)  // Should be speed >= 3000
{
    // Processing logic
}

// ✅ Correct
if (speed >= 3000)
{
    // Processing logic
}

2. Variable Scope Errors

// ❌ Wrong: declaring inside event handler and expecting value to persist
on key 'a'
{
    int counter;  // Redeclared each time, value lost
    counter++;
    write("Counter: %d", counter);
}

// ✅ Correct: declare in variables block
variables {
    int counter = 0;
}

on key 'a'
{
    counter++;
    write("Counter: %d", counter);
}

3. Message Handling Errors

// ❌ Wrong: not accessing message data correctly
on message EngineData
{
    byte data = this.byte(5);  // Might be out of bounds
}

// ✅ Correct: check DLC first
on message EngineData
{
    if (this.DLC >= 6) {
        byte data = this.byte(5);
    }
}

Troubleshooting Workflow

  1. Use write() to Output Intermediate Results

    on message EngineData
    {
        write("Starting message processing");
        write("Speed value: %d", this.EngineSpeed);
    
        if (this.EngineSpeed > 3000) {
            write("Entered conditional branch");
            // Processing logic
        }
    
        write("Processing complete");
    }
    
  2. Use Breakpoints to Pause Execution

    • Set breakpoints at key locations
    • Observe actual variable values
    • Verify whether logic branches are triggered
  3. Use Watch Window to Monitor Variables

    • Add relevant variables to the Watch Window
    • View real-time value changes
    • Confirm data flow direction

Important Note: Logic errors are the hardest to troubleshoot because they involve the program's "way of thinking". Systematically use write() and breakpoints to progressively narrow down the problem scope.

Error Pattern 3: Bus Communication Errors

Symptoms

The program logic is correct, but messages on the bus are not being sent or received as expected.

Common Causes

1. Node Not Properly Associated

// Problem: CAPL program not bound to simulation node
// Solution: Ensure the CAPL node is added in Simulation Setup

2. Database Binding Errors

// Problem: Using messages or signals that don't exist in the database
// Solution: Check the .dbc file, confirm message and signal names are correct

3. Channel Configuration Errors

// Problem: Message sent to the wrong channel
// Solution: Check actual channel configuration in Output window

Troubleshooting Workflow

  1. Verify Node Status

    • Check if the node is activated in Simulation Setup
    • Confirm the CAPL program compiled successfully
  2. Check Database Configuration

    // Use write to output message information
    on start
    {
        write("Checking database configuration");
        write("EngineData message ID: 0x%X", getMessageID("EngineData"));
    }
    
  3. Monitor Bus Activity

    • View messages on the bus in CANoe's Trace window
    • Confirm whether messages are actually being sent
  4. Verify Channel Settings

    • Check channel mapping in CANoe configuration
    • View channel settings in CANoe's Hardware -> Configuration
    • Confirm simulation nodes are correctly bound to target channels

Important Note: Bus communication problems typically involve multiple components (CAPL program, simulation nodes, database, hardware configuration). Use a layered troubleshooting approach, verifying step by step from the application layer to the transport layer.


Debugging in Practice: A Complete Case Study

Let us apply everything we have learned through a complete debugging case study.

Case Background

In Article 4, we wrote code to check engine speed:

on message EngineData
{
    // 当发动机速度大于 3000 RPM 时,发送警告消息
    if (this.EngineSpeed > 3000) {
        WarningMessage.WarningCode = 1;
        output(WarningMessage);
    }
}

But testing revealed that warning messages were being sent even when the engine speed was only 2500 RPM.

Debugging Process

Step 1: Add Debug Output

First, we use write() to output intermediate results:

on message EngineData
{
    // Output received raw data
    write("Received EngineData message");
    write("  Message ID: 0x%X", this.ID);
    write("  Message length: %d", this.DLC);

    // Output signal values
    write("  Engine speed: %d", this.EngineSpeed);

    // Output conditional check results
    if (this.EngineSpeed > 3000) {
        write("  Condition check: speed > 3000 is TRUE");
        write("  Preparing to send warning message");
        WarningMessage.WarningCode = 1;
        output(WarningMessage);
        write("  Warning message sent");
    }
    else {
        write("  Condition check: speed > 3000 is FALSE");
    }
}

After starting measurement, the Write Window shows:

Received EngineData message
  Message ID: 0x100
  Message length: 8
  Engine speed: 2500
  Condition check: speed > 3000 is FALSE

Strange - the condition check shows "FALSE", but warning messages are still being sent. This indicates the problem is not in the conditional logic, but in the message sending logic.

Step 2: Set Breakpoints for Deeper Investigation

Set a breakpoint on the line output(WarningMessage);. When the program pauses:

  1. Check the Watch Window

    • Confirm the contents of WarningMessage
    • Check the value of WarningCode
  2. Step Through Execution

    • Use F10 to execute line by line
    • Observe changes at each step

The breakpoint reveals the truth: the program is also sending WarningMessage somewhere else.

Step 3: Search for All Output Statements

Use Ctrl+F to search for output(WarningMessage) throughout the .can file:

// Found another place that also sends warning message
on message OverheatData
{
    if (this.Temp > 100) {
        WarningMessage.WarningCode = 2;  // Overheat warning
        output(WarningMessage);
    }
}

It turns out another message, OverheatData, also sends WarningMessage. The engine temperature was too high (possibly a sensor fault), causing the warning message to be sent.

Step 4: Fix and Verify

The fix is straightforward: differentiate between warning types by using separate message objects:

on message EngineData
{
    if (this.EngineSpeed > 3000) {
        SpeedWarning.WarningCode = 1;
        output(SpeedWarning);  // Use dedicated speed warning message
    }
}

on message OverheatData
{
    if (this.Temp > 100) {
        TempWarning.WarningCode = 2;  // Use dedicated temperature warning message
        output(TempWarning);
    }
}

Step 5: Add Better Logging

To make future debugging easier, we improve the log output:

on message EngineData
{
    write("[EngineData] Received message, speed: %d RPM", this.EngineSpeed);

    if (this.EngineSpeed > 3000) {
        write("[EngineData] Speed exceeded limit, sending warning");
        SpeedWarning.WarningCode = 1;
        output(SpeedWarning);
        write("[EngineData] Warning sent");
    }
}

Debugging Summary

This case study demonstrates the complete debugging workflow:

  1. Symptom Identification: Warning messages sent when they should not be
  2. Log Tracing: Use write() to output intermediate results
  3. Breakpoint Investigation: Use breakpoints to pause the program and inspect state
  4. Search and Analysis: Find all possible causes
  5. Fix and Verify: Modify code and verify results
  6. Improve Prevention: Add better logging to prepare for future debugging

Important Note: Debugging does not end when you find the bug. You need to understand why the bug occurred and take measures to prevent it from happening again.


Debugging Best Practices

1. Defensive Programming

Consider debugging needs while writing code:

// Good practice: add parameter validation
void sendMessage(message msg)
{
    // Validate parameters
    if (msg.DLC == 0) {
        write("Error: message length is 0");
        return;
    }

    // Output debug information
    write("Sending message ID: 0x%X", msg.ID);
    output(msg);
}

2. Establish Logging Standards

Create a unified log format for your team:

// Format: [Component] Action: Details
write("[EngineMonitor] Message processing: speed %d RPM", speed);
write("[EngineMonitor] Error: speed sensor timeout");
write("[EngineMonitor] Status: timer reset complete");

Benefits:

  • Quickly identify log sources
  • Easy to search and filter
  • All team members can understand

3. Use Meaningful Variable Names

// Bad naming
int x = 3000;
if (x > 2500) {
    output(m);
}

// Good naming
int engineSpeedThreshold = 3000;
int currentEngineSpeed = getEngineSpeed();

if (currentEngineSpeed > engineSpeedThreshold) {
    output(speedWarningMessage);
}

4. Clean Up Debug Code Promptly

Before releasing the project, clean up or disable debug output:

// Use conditional compilation
#ifdef DEBUG
    write("Debug info: speed %d", speed);
#endif

// Or use global switch
variables {
    dword g_debugLevel = 0;  // 0=off, 1=basic info, 2=detailed info
}

void debugLog(char text[], dword level)
{
    if (level <= g_debugLevel) {
        write(text);
    }
}

5. Learn to Use Your Tools

  • Output Window: Compile errors and write() output
  • Trace Window: Actual messages on the bus
  • Breakpoint Window: Manage all breakpoints
  • Watch Window: Monitor variables

Practical Tip: Treat debugging as a "scientific method": form a hypothesis, design an experiment (debug), collect evidence (observe), and verify or refute the hypothesis.


Summary

Debugging is an essential core skill for every programmer. In CAPL development, you have three powerful weapons:

  • write() function: The simplest and most commonly used debugging tool, suitable for outputting intermediate results and tracking program flow
  • Breakpoints: Let you pause program execution and inspect current state - a powerful tool for locating logic errors
  • Watch Window: Monitor variable changes in real-time, especially useful for tracking data flow

Common error types and troubleshooting methods:

  1. Compile errors: Check the Output window, start fixing from the first error
  2. Runtime logic errors: Use write() and breakpoints to systematically verify hypotheses
  3. Bus communication errors: Use layered troubleshooting, verifying step by step from application layer to transport layer

Remember: good programmers do not rely on guessing; they use tools to see. Develop systematic debugging habits to improve your development efficiency.


Exercises

Exercise 1: write() Basics

Write a CAPL program that uses the write() function to output the following information:

  • Current time (using the timeNow() function)
  • A timer's status
  • The number of CAN messages received

Exercise 2: Breakpoint Debugging

Modify the code from Exercise 1 by intentionally introducing a logic error (e.g., an incorrect conditional check), then use breakpoints to locate and fix it.

Exercise 3: Comprehensive Debugging

Create a CAPL program containing the following errors:

  1. A variable scope error
  2. A message handling logic error
  3. An array bounds error

Then use the methods learned in this article to troubleshoot and fix each one.

Exercise 4: Establish Debugging Standards

Design a logging format standard for your team that includes:

  • Log level definitions (info, warning, error)
  • Unified log prefix format
  • Principles for when to output logs