CAPL Script

核心交互

学习目标

完成本篇后,你将能够:

  • 理解 CAPL 程序的生命周期和各阶段的作用
  • 掌握 on message 事件的用法和 this 关键字
  • 使用 output() 函数发送 CAN 消息
  • 理解消息选择器和符号化编程
  • 编写一个完整的响应逻辑:当收到特定信号时自动发送响应消息

在第三篇中,我们学习了 CAPL 的语法基础。现在,是时候让你的 CAPL 程序真正"活"起来了。

想象一下:你刚刚写好了第一个 on key 'a' 事件程序,按下 'a' 键,Write 窗口出现了 "Hello CAPL!"。但接下来你可能会想:我的程序怎么和 CAN 总线对话?

这就是本篇要解决的问题。我们将学习 CAPL 与总线世界对话的"语言"——事件驱动机制。


CAPL 程序的生命周期

在 CAPL 的世界里,程序不是从 main() 函数开始,而是由事件驱动的。但即便如此,CAPL 程序也有自己的生命周期(Lifecycle)。

💡 类比:就像人从出生到死亡有不同阶段一样,CAPL 程序从启动到停止也有关键时刻。

四个关键生命周期事件

CAPL 程序的生命周期由四个核心事件控制:

事件 触发时机 典型用途
on preStart 测量初始化阶段(启动前) 早期初始化、记录开始(不能使用 output())
on start 程序启动时刻 变量初始化、配置设置、启动定时器
on preStop 停止测量被请求时 清理资源、保存状态、发送关闭消息
on stopMeasurement 测量结束时 最终清理、生成报告

实践示例

让我们看一个典型的生命周期程序:

variables {
    msTimer heartbeatTimer;  // Timer variable declaration
}

on preStart
{
    write("=== Program starting: prepare environment ===");
}

on start
{
    write("=== Program started: initialize variables ===");

    int engineSpeed = 0;
    message 0x100 EngineDataMsg;

    // Set periodic timer
    setTimer(heartbeatTimer, 1000);  // Send heartbeat every second
}

on preStop
{
    write("=== Stop requested: cleanup resources ===");
    cancelTimer(heartbeatTimer);  // Cancel timer
}

on stopMeasurement
{
    write("=== Program ended: generate final report ===");
}

📝 重要说明on start 是最常用的生命周期事件,通常在这里初始化变量、设置定时器等。


事件驱动模型(上):接收与响应

CAPL 是事件驱动(Event-Driven)的语言。这意味着程序平时处于"休眠"状态,只有当特定事件发生时才会"醒来"执行代码。

最核心的事件是 on message——当有 CAN 消息被接收时触发。

on message 基础

on message *
{
    // * 表示接收所有消息
    write("收到消息:ID=0x%X", this.ID);
}

这段代码的意思是:无论收到什么消息,都打印其 ID

使用 this 关键字访问消息内容

on message 事件中,this 关键字代表刚刚收到的这条消息。你可以通过 this 访问消息的所有属性:

on message *
{
    // Access basic message properties
    write("Message ID: 0x%X", this.ID);        // Message identifier
    write("Data length: %d", this.DLC);        // Data length code
    write("Channel: %d", this.CAN);            // CAN channel number

    // Access message data (byte by byte)
    write("Data: %02X %02X %02X %02X",
          this.byte(0), this.byte(1),
          this.byte(2), this.byte(3));

    // Use DIR selector to check if received or sent
    // dir can be 'rx' (received) or 'tx' (transmitted)
    if (this.dir == rx) {
        write("This is a received message");
    }
}

📝 重要说明:理解 this 的含义是关键——它指向当前事件的"主体",即刚收到的消息对象。

消息选择器:精确过滤消息

如果你只想处理特定的消息,可以使用消息选择器(Message Selector):

方式 1:按 ID 过滤

// Process only messages with ID 0x100
on message 0x100
{
    write("Received EngineData message");
}

方式 2:按名称过滤(需要 DBC 数据库)

// If EngineData message (ID=0x100) is defined in database
on message EngineData
{
    // Directly access signals using signal names
    write("Engine Speed: %d RPM", this.EngineSpeed);
    write("Engine Temp: %d C", this.EngineTemp);
}

方式 3:按通道过滤

// Process only messages from CAN1 channel
on message CAN1.*
{
    write("Message from CAN1 channel");
}

💡 实用技巧:从简单开始(使用 on message * 观察所有消息),然后逐步细化过滤条件。

实战:消息接收监听器

让我们编写一个完整的消息监听器:

variables {
    // Statistics counters
    int totalMessages = 0;
    int engineDataCount = 0;
}

on message *
{
    totalMessages++;
    write("[%d] Received message: 0x%X", totalMessages, this.ID);
}

on message 0x100  // EngineData message
{
    engineDataCount++;
    write("  -> This is EngineData message (#%d)", engineDataCount);

    // Check data
    if (this.byte(0) > 0x80) {
        write("  -> Warning: Engine speed too high!");
    }
}

运行这个程序,你会看到所有消息的流动,包括特定消息的详细分析。

[!SCREENSHOT]
位置:CAPL Browser > Write 窗口
内容:显示消息接收日志
标注:圈出 "收到消息" 和 "EngineData 消息" 的输出行


事件驱动模型(下):发送与控制

学会了"听"(接收),现在我们来学习"说"(发送)。

output() 函数:发送消息

output() 是 CAPL 中发送消息的核心函数:

void output(message msg);

重要提示:避免递归发送

// ❌ Wrong: do not use output(this) in on message
on message * {
    output(this);  // This causes infinite recursion!
}

// ✅ Correct: check message direction
on message 0x100 {
    if (this.dir == rx) {  // Only process received messages
        // Process message without resending
    }
}

📝 重要说明:在 on message * 中直接使用 output(this) 会导致递归发送,因为每条发送的消息又会触发 on message 事件。

基础发送示例

on key 's'  // Press 's' to send message
{
    // Step 1: Declare message variable
    message 0x200 HeartbeatMsg;

    // Step 2: Set message data
    HeartbeatMsg.byte(0) = 0xAA;  // Heartbeat signature
    HeartbeatMsg.byte(1) = 0x55;  // Fixed data

    // Step 3: Send message
    output(HeartbeatMsg);

    write("Heartbeat message sent");
}

按一下 's' 键,一条消息就被发送到 CAN 总线上。

📝 重要说明message 是 CAPL 的特殊数据类型,用于表示 CAN 帧。

定时器事件:实现周期性发送

output() 只能发送一次消息。要实现周期性发送(比如每秒发送一次心跳),我们需要定时器(Timer)。

Timer 类型对比

特性 timer msTimer
时间单位 毫秒
最大时间 596.52 小时 596.52 小时
常用场景 长时间定时 快速响应任务
语法 timer myTimer; msTimer myTimer;

CAPL 支持两种定时器:

  • timer - 基于秒
  • msTimer - 基于毫秒(更常用)

周期性发送完整示例

variables {
    msTimer heartbeatTimer;  // Declare millisecond timer
    message 0x200 HeartbeatMsg;
    int heartbeatCount = 0;
}

on start
{
    write("Starting periodic heartbeat transmission...");
    setTimer(heartbeatTimer, 1000);  // Trigger first time after 1 second
}

on timer heartbeatTimer
{
    heartbeatCount++;

    // Prepare heartbeat message
    HeartbeatMsg.byte(0) = 0xAA;      // Heartbeat signature
    HeartbeatMsg.byte(1) = heartbeatCount & 0xFF;  // Counter

    // Send message
    output(HeartbeatMsg);

    write("Heartbeat #%d sent", heartbeatCount);

    // Re-arm timer to create loop
    setTimer(heartbeatTimer, 1000);
}

这段代码的效果:

  1. 程序启动后 1 秒,触发第一次 on timer
  2. 发送心跳消息
  3. 重新设置定时器,再过 1 秒再次触发
  4. 如此循环,形成每秒一次的心跳

💡 类比:定时器就像闹钟——响了之后,你需要重新设置它,它才会再次响起。

取消定时器

on key 'x'  // Press 'x' key to stop heartbeat
{
    cancelTimer(heartbeatTimer);
    write("Heartbeat stopped");
}

组合使用:响应式发送

最实用的模式是:接收消息 → 处理逻辑 → 发送响应

variables {
    message 0x100 EngineData;   // Received message
    message 0x200 WarningLight; // Response message
}

on message 0x100  // Receive EngineData
{
    // Read engine speed value (assume it's in the first two bytes)
    int engineSpeed = this.byte(0) * 256 + this.byte(1);

    write("Received engine speed: %d RPM", engineSpeed);

    // Logic check: engine speed too high
    if (engineSpeed > 6000) {
        write("Engine speed too high! Sending warning light signal");

        // Prepare warning message
        WarningLight.byte(0) = 0x01;  // Turn on warning light

        // Send response message
        output(WarningLight);
    }
}

📝 重要说明:这是一个典型的响应式系统——根据输入(消息)产生输出(消息),是 CAPL 仿真节点的核心逻辑。


与数据库(DBC)集成——从"数字"到"语义"

到目前为止,我们都在使用数字(如 0x100)和字节(如 this.byte(0))来操作消息。虽然这是底层的做法,但不够直观。

DBC 数据库让我们可以用语义化的名称来编程。

DBC 数据库术语对照

术语 英文 说明
DBC Database CAN CAN 数据库文件格式
消息 Message CAN 帧,包含 ID 和数据
信号 Signal 消息中的特定数据字段
编码 Encoding 信号的数据表示方式(原始值/物理值)

为什么需要 DBC?

假设没有数据库:

on message 0x100
{
    // What do these numbers mean?
    int rpm = this.byte(0) * 256 + this.byte(1);
    int temp = this.byte(2);
}

有了数据库后:

on message EngineData
{
    // Directly use meaningful names
    write("Engine Speed: %d", this.EngineSpeed);
    write("Engine Temp: %d", this.EngineTemp);
}

哪种更清晰?显然是第二种。

在 CANoe 中创建简单 DBC

[!SCREENSHOT]
位置:CANoe > Database Manager
内容:显示创建新数据库的界面
标注:圈出 "Create New Database" 按钮

我们创建一个最简单的 DBC:

  1. 在 Database Manager 中创建新数据库

  2. 添加一条消息:

    • 名称:EngineData
    • ID:0x100
    • DLC:8 字节
  3. 添加两个信号:

    • EngineSpeed:长度 16 位,位置 0-15
    • EngineTemp:长度 8 位,位置 16-23

📝 重要说明:DBC 文件定义了消息和信号的结构(名称、ID、长度、位置等),让 CAPL 可以进行符号化编程

符号化编程实践

有了 DBC 后,CAPL 代码变得直观易懂:

on message EngineData  // Use message name directly
{
    // Use signal names directly, no need to parse bytes manually
    int rpm = this.EngineSpeed;
    int temp = this.EngineTemp;

    write("Engine speed: %d RPM", rpm);
    write("Engine temp: %d C", temp);

    // Logic check
    if (temp > 110) {
        write("Engine overheating!");

        // Send warning message (also use symbolic name)
        WarningLight.LampStatus = 1;
        output(WarningLight);
    }
}

getMessageID() 函数:动态获取消息 ID

有时你需要根据消息名动态获取 ID:

on start
{
    dword engineDataId;

    // Get message ID from database
    engineDataId = getMessageID("EngineData");

    if (engineDataId != -1) {
        write("EngineData message ID is: 0x%X", engineDataId);
    } else {
        write("Error: EngineData message not found");
    }
}

提示:在多数据库环境中,需要指定数据库名:getMessageID("DatabaseName.EngineData")

这在需要动态处理消息时很有用。

📝 重要说明:符号化编程是 CAPL 进阶技能——它让代码更易读、更易维护。建议始终使用 DBC 数据库进行开发。


综合案例:智能转速监控节点

现在我们来整合所有知识点,编写一个完整的智能监控节点

📝 编程范式说明:本案例使用原始编程(无需DBC数据库),使用直接的CAN ID和字节访问。这是理解CAPL底层工作原理的最好方式。

需求定义

我们要编写一个 ECU 仿真节点,它的功能是:

  1. 接收来自发动机的 EngineData 消息(ID=0x100,包含转速和温度)
  2. 当转速超过 6000 RPM 时,自动发送 WarningLight 消息(ID=0x200)点亮警告灯
  3. 当温度超过 110°C 时,发送 WarningLight 消息并记录日志
  4. 每秒发送一次心跳消息

第一步:定义变量

variables {
    // Message variables (using raw CAN IDs)
    message 0x100 engineMsg;      // Received message
    message 0x200 warningMsg;     // Sent message
    message 0x200 heartbeatMsg;   // Heartbeat message (reuses same ID)

    // Timer
    msTimer heartbeatTimer;

    // Statistics variables
    int totalEngineMsgs = 0;
    int warningTriggered = 0;
    int heartbeatCount = 0;

    // Constants for thresholds
    const int RPM_THRESHOLD = 6000;
    const int TEMP_THRESHOLD = 110;
    const int TEMP_OFFSET = 40;  // Temperature offset: raw + 40 = actual degC
}

第二步:初始化

on start
{
    write("=== Smart Engine Monitor Node Started (Raw Mode) ===");
    write("Using raw CAN IDs - no DBC database required");

    // Initialize heartbeat message
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = 0xAA;  // Heartbeat signature

    // Start periodic heartbeat
    setTimer(heartbeatTimer, 1000);

    write("Initialization complete");
}

第三步:接收消息并处理

on message 0x100  // EngineData message
{
    // Local variables for extracted values
    int rawRpm;
    int actualRpm;
    int rawTemp;
    int actualTemp;

    totalEngineMsgs++;

    // Extract RPM value (bytes 0-1, 16-bit, scale 0.25)
    rawRpm = this.word(0);
    actualRpm = rawRpm / 4;

    // Extract Temperature value (byte 2, offset -40)
    rawTemp = this.byte(2);
    actualTemp = rawTemp - TEMP_OFFSET;

    write("[%d] Received EngineData (0x100): RPM=%d (raw=%d), Temp=%d degC (raw=%d)",
          totalEngineMsgs, actualRpm, rawRpm, actualTemp, rawTemp);

    // Logic check 1: Engine speed too high
    if (actualRpm > RPM_THRESHOLD) {
        warningTriggered++;
        write("*** WARNING #%d: Engine speed too high (%d RPM)! ***",
              warningTriggered, actualRpm);

        // Send warning light message
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;  // LampStatus = ON
        output(warningMsg);

        write("WarningLight (0x200) sent: LampStatus=1 (ON)");
    }

    // Logic check 2: Engine temperature too high
    if (actualTemp > TEMP_THRESHOLD) {
        warningTriggered++;
        write("*** WARNING #%d: Engine temperature too high (%d degC)! ***",
              warningTriggered, actualTemp);

        // Send warning light message
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;  // LampStatus = ON
        output(warningMsg);

        write("WarningLight (0x200) sent: LampStatus=1 (ON)");
    }

    // Normal state
    if (actualRpm <= RPM_THRESHOLD && actualTemp <= TEMP_THRESHOLD) {
        write("Status: Engine operating normally");
    }
}

第四步:周期性心跳

on timer heartbeatTimer
{
    heartbeatCount++;

    // Update heartbeat data
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = heartbeatCount & 0xFF;

    // Send heartbeat message
    output(heartbeatMsg);

    write("Heartbeat #%d sent on 0x200 (data: 0x%02X)",
          heartbeatCount, heartbeatCount & 0xFF);

    // Re-arm timer
    setTimer(heartbeatTimer, 1000);
}

第五步:清理资源

📝 重要说明:所有CAPL代码中的注释应使用英文以避免字符编码问题。

on preStop
{
    write("=== Stopping monitor node ===");
    cancelTimer(heartbeatTimer);

    write("Total engine messages processed: %d", totalEngineMsgs);
    write("Total warnings triggered: %d", warningTriggered);
}

on stopMeasurement
{
    write("=== Monitor node stopped ===");
}

完整代码一览

variables {
    message 0x100 engineMsg;
    message 0x200 warningMsg;
    message 0x200 heartbeatMsg;
    msTimer heartbeatTimer;
    int totalEngineMsgs = 0;
    int warningTriggered = 0;
    int heartbeatCount = 0;
    const int RPM_THRESHOLD = 6000;
    const int TEMP_THRESHOLD = 110;
    const int TEMP_OFFSET = 40;
}

on start
{
    write("=== Smart Engine Monitor Node Started (Raw Mode) ===");
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = 0xAA;
    setTimer(heartbeatTimer, 1000);
    write("Initialization complete");
}

on message 0x100
{
    int rawRpm = this.word(0);
    int actualRpm = rawRpm / 4;
    int rawTemp = this.byte(2);
    int actualTemp = rawTemp - TEMP_OFFSET;

    totalEngineMsgs++;
    write("[%d] Received EngineData: RPM=%d, Temp=%d degC",
          totalEngineMsgs, actualRpm, actualTemp);

    if (actualRpm > RPM_THRESHOLD) {
        warningTriggered++;
        write("Warning #%d: Engine speed too high (%d RPM)!", warningTriggered, actualRpm);
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;
        output(warningMsg);
    }

    if (actualTemp > TEMP_THRESHOLD) {
        warningTriggered++;
        write("Warning #%d: Engine temperature too high (%d C)!", warningTriggered, actualTemp);
        warningMsg.dlc = 1;
        warningMsg.byte(0) = 1;
        output(warningMsg);
    }

    if (actualRpm <= RPM_THRESHOLD && actualTemp <= TEMP_THRESHOLD) {
        write("Status: Engine normal");
    }
}

on timer heartbeatTimer
{
    heartbeatCount++;
    heartbeatMsg.dlc = 1;
    heartbeatMsg.byte(0) = heartbeatCount & 0xFF;
    output(heartbeatMsg);
    write("Heartbeat #%d", heartbeatCount);
    setTimer(heartbeatTimer, 1000);
}

on preStop
{
    write("=== Stopping monitor node ===");
    cancelTimer(heartbeatTimer);
    write("Total engine messages processed: %d", totalEngineMsgs);
    write("Total warnings triggered: %d", warningTriggered);
}

on stopMeasurement
{
    write("=== Monitor node stopped ===");
}

这个程序整合了我们学到的所有核心概念:

  • 生命周期事件:初始化和清理
  • 消息接收on messagethis 关键字
  • 消息发送output() 函数
  • 周期性任务:定时器和 on timer
  • 符号化编程:使用 DBC 中的消息和信号名
  • 业务逻辑:条件判断和状态监控

📝 重要说明:本示例使用符号化编程(基于DBC数据库),这是实际项目中的推荐做法。使用符号化编程前,需要先在CANoe中创建包含EngineDataWarningLight消息的DBC数据库。

如果你的环境没有DBC数据库,可以参考原始编程版本(无需数据库):

  • 消息声明:message 0x100 engineMsg(替代message EngineData
  • 数据访问:this.word(0) / 4(替代this.EngineSpeed
  • 警告设置:warningMsg.byte(0) = 1(替代warningMsg.LampStatus = 1

两种编程方式功能完全一致,只是风格不同。符号化编程更易读,原始编程更独立。

💡 实战建议:尝试修改这个程序,比如调整转速/温度阈值,添加新的警告条件,或者增加更多监控功能。

[!SCREENSHOT]
位置:CANoe > Measurement Setup > Write 窗口
内容:显示智能监控节点的运行日志
标注:圈出 "收到发动机数据"、"警告" 和 "心跳" 的输出行


本篇总结

在 CAPL 的世界里,事件驱动是核心机制。我们学习了:

  • 生命周期管理on start 用于初始化,on preStop 用于清理
  • 消息接收on message 事件和 this 关键字访问消息内容
  • 消息选择器:使用 *ID名称通道 过滤消息
  • 消息发送output() 函数是 CAPL 与总线对话的"嘴巴"
  • 定时器setTimer() + on timer 实现周期性任务
  • 符号化编程:DBC 数据库让代码从"数字"变成"语义"

这些是 CAPL 编程的基石——掌握了它们,你就能构建复杂的 ECU 仿真节点。

课后练习

  1. 基础练习:编写一个程序,接收所有消息并打印其 ID、数据长度和前 4 个字节的数据。

  2. 定时器练习:创建一个 msTimer,实现每 500 毫秒发送一次递增计数器的功能。

  3. 逻辑练习:修改本篇的综合案例,添加一个新的判断:当转速低于 800 RPM 时,认为是"怠速状态",发送一条 IdleStatus 消息。

  4. 进阶练习(有 DBC 环境):创建一个包含 DoorStatus 消息的 DBC(包含 DoorOpen 信号),编写 CAPL 程序:当收到 DoorOpen=1 时,延迟 2 秒后自动发送 Alarm 消息。


📝 预告:下一篇文章我们将学习如何调试 CAPL 程序。调试是每个程序员的必备技能——我们将从最简单的 write() 开始,逐步学习断点、观察窗口等强大工具。