第3章:CAPL脚本入门

好,咱们进入CAPL的世界。说实话,很多工程师一听到「脚本」两个字就头大,觉得那是程序员的事。但CAPL不一样,它专为汽车电子测试而生。我刚开始接触时也觉得别扭,后来发现——这玩意儿其实就是帮我们「监听总线、发消息、做判断」的瑞士军刀。

3.1 CAPL语言概述

CAPL全称是Communication Access Programming Language。说白了,它是Vector公司为CANoe量身定做的一种类C语言。你想想看,我们做ECU测试时,最常干的事是什么?无非就是:

  • 等某个CAN消息来了,触发一个动作
  • 模拟ECU发送特定信号
  • 根据条件改变测试流程

这些事,用CAPL写起来特别顺手。它不像C语言那样需要管理内存,也不像Python那样依赖环境。CAPL是事件驱动的——你只需要告诉它「当XX发生时,做XX」就行。

核心特点:

  • 事件驱动:响应总线事件、时间事件、用户操作
  • 类C语法:学过C语言的,上手极快
  • 内置CAN/LIN/FlexRay/Ethernet库:不用自己造轮子
  • 与CANoe深度集成:直接操作仿真节点、信号、系统变量

我记得有一次,客户要求模拟一个异常场景——ECU在收到特定ID的消息后,必须在50ms内回复。用CAPL写了个on message事件,加个定时器,半小时搞定。要是用其他语言,还得考虑怎么和CAN硬件交互,麻烦得很。

3.2 三大核心事件函数

CAPL里最常用的三个事件函数,我建议你背下来。它们几乎覆盖了90%的测试场景。

3.2.1 on start

这个函数在CANoe仿真启动时自动执行。我习惯用它来做初始化工作:

on start
{
  write("测试开始,版本1.0");
  SetTimer(myTimer, 1000);  // 启动一个1秒定时器
  sysvarSetValue("TestMode", 1);  // 设置系统变量
}

注意,on start只执行一次。如果你需要循环执行某些操作,得配合定时器。

3.2.2 on message

这是CAPL的灵魂。当总线上出现特定CAN消息时,这个函数会被触发。我项目中遇到最多的问题就是——消息过滤不对,导致事件没触发。

on message 0x123
{
  // 收到ID为0x123的消息时执行
  write("收到消息: 0x%x", this.id);
  // 读取信号值
  int speed = this.byte(0);
  if(speed > 100)
  {
    write("警告:车速过高!");
  }
}

我的小技巧:调试时先用 on message * 打印所有消息,确认ID和周期后再精确过滤。我曾经因为看错ID,浪费了一整天排查「为什么事件没触发」。

3.2.3 on key

这个函数监听键盘按键。手动测试时特别有用:

on key 'a'
{
  write("按下A键,发送测试消息");
  message 0x456 msg;
  msg.dlc = 8;
  msg.byte(0) = 0xAA;
  output(msg);
}

on key 's'
{
  write("按下S键,停止测试");
  stop();
}

嗯,这里要注意:按键是区分大小写的。'a'和'A'是两个不同的事件。

3.3 变量声明与基本语法

CAPL的变量声明和C语言很像,但有一些自己的规矩。

3.3.1 变量类型

类型 说明 示例
int 32位有符号整数 int count = 0;
long 64位有符号整数 long timestamp;
float 32位浮点数 float voltage = 12.5;
char 8位字符 char ch = 'A';
byte 8位无符号整数 byte data[8];
message CAN/LIN消息对象 message 0x100 msg;
timer 定时器 timer tCycle;

3.3.2 变量作用域

CAPL的变量作用域分三种:

  • 全局变量:声明在所有函数外面,整个文件都能用
  • 局部变量:声明在函数内部,只在该函数有效
  • 静态变量:用static修饰,值在函数调用间保持
// 全局变量
int g_testCount = 0;

on start
{
  int localVar = 10;  // 局部变量
  static int callCount = 0;  // 静态变量
  callCount++;
  g_testCount++;
  write("第%d次调用", callCount);
}

避坑指南:我曾经在on message里声明了一个大数组作为局部变量,结果每次消息触发都重新分配内存,导致堆栈溢出。后来改成全局变量就没事了。记住——频繁触发的事件函数里,别放大的局部变量。

3.3.3 基本语法要点

  • 每条语句以分号结尾
  • 注释用///* */
  • 条件判断:ifelse ifelse
  • 循环:forwhiledo while
  • 字符串拼接用+,但注意CAPL的字符串操作有限
on message 0x200
{
  int value = this.byte(0);
  
  // 条件判断
  if(value > 100)
  {
    write("值过大: %d", value);
  }
  else if(value > 50)
  {
    write("值适中");
  }
  else
  {
    write("值正常");
  }
  
  // for循环示例
  for(int i = 0; i < 8; i++)
  {
    this.byte(i) = 0xFF;  // 填充所有字节
  }
}

3.4 第一个CAPL脚本

光说不练假把式。咱们写一个完整的CAPL脚本,实现一个简单的功能:监听总线上ID为0x123的消息,提取第一个字节作为车速,如果车速超过80km/h,就发送一条报警消息。

/* 文件名: SpeedMonitor.can */

// 全局变量
int g_alertCount = 0;

on start
{
  write("车速监控脚本启动");
  write("监控ID: 0x123");
}

on message 0x123
{
  // 提取车速(假设第一个字节)
  int speed = this.byte(0);
  
  write("当前车速: %d km/h", speed);
  
  // 超速判断
  if(speed > 80)
  {
    g_alertCount++;
    write("超速报警!第%d次", g_alertCount);
    
    // 构造报警消息
    message 0x456 alertMsg;
    alertMsg.dlc = 8;
    alertMsg.byte(0) = 0xAA;  // 报警标志
    alertMsg.byte(1) = speed; // 当前车速
    alertMsg.byte(2) = 0x01;  // 报警等级
    output(alertMsg);
  }
}

on key 'r'
{
  // 按R键重置计数
  g_alertCount = 0;
  write("报警计数已重置");
}

on key 'q'
{
  // 按Q键退出
  write("停止监控");
  stop();
}

运行步骤:

  1. 在CANoe中新建一个仿真节点
  2. 双击节点,在CAPL编辑器中粘贴上述代码
  3. 编译(F5或点击编译按钮)
  4. 启动仿真(F9)
  5. 用IG模块或另一个节点发送ID为0x123的消息
  6. 观察Write窗口的输出

我第一次写CAPL脚本时,编译报错了好几次。最常见的问题是——忘记声明变量类型,或者把this.byte(0)写成了this.Byte(0)(大小写敏感)。别急,慢慢来,写多了就顺手了。

好了,这一章的内容就到这里。你把这个脚本跑通了,CAPL的基本功就算打牢了。下一章咱们聊聊如何用CAPL做更复杂的测试——比如模拟ECU的故障行为。