4. CAPL测试脚本基础

CAPL(Communication Access Programming Language)是CANoe内置的类C事件驱动脚本语言。它不像普通程序那样从main函数顺序执行,而是由总线事件、定时器、键盘或系统事件触发对应的事件处理程序(event handler)。理解这一点,是写出可靠ECU通信一致性测试脚本的前提:测试用例的“自动化执行入口”本质上就是一组被精心编排的事件处理块。

本节围绕四个核心能力展开:报文收发、信号读写、定时器、事件处理,并最终把它们组合成一个可复用的测试用例入口结构。

一、CAPL程序的基本骨架

一个CAPL节点(Network Node)通常包含变量声明区、事件处理区和自定义函数区。变量分为全局变量、局部变量和系统变量引用。测试脚本中常用的声明如下:

variables
{
  msTimer tWait;          // 毫秒定时器
  int     gStep = 0;      // 测试步骤计数器
  int     gResult = 0;    // 0=未判定 1=通过 2=失败
  message 0x123 txMsg;    // 待发送报文
}

on start
{
  write("测试节点已启动");
  gStep = 0;
  setTimer(tWait, 100);   // 启动后100ms进入第一步
}

其中 on start 是CAPL节点加载并开始测量后自动触发的事件,非常适合作为测试用例的统一入口。把初始化、首条激励发送、首个定时器启动都放在这里,测试流程就有了确定的起点。

二、报文收发:控制总线上的帧

发送报文用 output(),接收报文用 on message 事件。发送前需要给报文对象赋值,包括ID、DLC和数据字节。例如构造一条周期为20ms、数据为 00 11 22 33 44 55 66 77 的报文:

on start
{
  txMsg.id  = 0x123;
  txMsg.dlc = 8;
  txMsg.byte(0) = 0x00;
  txMsg.byte(1) = 0x11;
  txMsg.byte(2) = 0x22;
  txMsg.byte(3) = 0x33;
  txMsg.byte(4) = 0x44;
  txMsg.byte(5) = 0x55;
  txMsg.byte(6) = 0x66;
  txMsg.byte(7) = 0x77;
  output(txMsg);          // 单次发送
}

on message 0x200
{
  // 收到被测ECU的响应帧
  if (this.byte(0) == 0xAA)
  {
    gResult = 1;          // 判定通过
  }
  else
  {
    gResult = 2;          // 判定失败
  }
}

注意 this 关键字代表当前触发事件的报文对象,可直接读取其字节、DLC和时间戳。若需要周期发送,可用 setTimer 配合定时器反复调用 output(),而不是依赖 output 的循环——CAPL没有阻塞式循环等待,必须用事件驱动方式组织时序。

三、信号读写:从报文到物理值

直接操作字节不利于表达物理含义。CAPL支持通过数据库(DBC)中定义的信号名读写:

on start
{
  txMsg.EngineSpeed = 1500;   // 写入信号(物理值)
  output(txMsg);
}

on message 0x200
{
  float v;
  v = this.VehicleSpeed;      // 读取信号物理值
  if (v > 100.0)
  {
    write("车速超限: %f", v);
  }
}

信号读写依赖DBC中已定义的报文与信号映射。若信号未在数据库中定义,则只能按字节操作。测试脚本中建议优先使用信号名,这样脚本可读性高,且当DBC更新时无需改动字节偏移。

四、定时器与事件处理:构建时序

CAPL提供 msTimer(毫秒)和 timer(秒级,精度较低)两类定时器。测试用例中常用 setTimer 启动、cancelTimer 取消,并在 on timer 中处理超时逻辑。下面是一个典型的“发送激励—等待响应—超时判定”结构:

on start
{
  gStep = 1;
  output(txMsg);              // 发送激励
  setTimer(tWait, 200);       // 200ms内等待响应
}

on message 0x200
{
  if (gStep == 1)
  {
    cancelTimer(tWait);       // 收到响应,取消超时
    gResult = 1;
    gStep = 2;
    write("响应正常,用例通过");
  }
}

on timer tWait
{
  if (gStep == 1)
  {
    gResult = 2;
    write("超时未收到响应,用例失败");
  }
}

这段代码体现了CAPL测试脚本的核心模式:事件驱动 + 定时器超时保护。没有定时器,脚本可能在等待响应时永久挂起;没有事件处理,脚本无法对总线上的异步响应做出反应。

CAPL测试用例事件驱动执行模型 on start(统一入口) 初始化变量 / 构造报文 / 启动定时器 output(txMsg) 发送激励报文 / 写入信号 setTimer(tWait, 200) 启动超时保护定时器 等待总线事件 on message / on timer 竞争 on message 0x200 cancelTimer(tWait) gResult = 1(通过) on timer tWait gStep 仍为 1 gResult = 2(失败) 收到响应 超时未响应

五、把测试用例封装成可复用入口

当多个测试用例需要顺序执行时,推荐用一个步骤计数器 gStep 驱动状态机,每个步骤对应一次激励发送和一次响应等待。下面给出一个可直接参考的测试用例入口模板,包含两个用例:用例1检查0x200响应,用例2检查0x201响应。

variables
{
  msTimer tWait;
  int gStep = 0;
  int gPass = 0;
  int gFail = 0;
  message 0x123 txMsg;
}

on start
{
  gStep = 1;
  gPass = 0;
  gFail = 0;
  txMsg.dlc = 8;
  txMsg.byte(0) = 0x01;
  output(txMsg);
  setTimer(tWait, 200);
}

on message 0x200
{
  if (gStep == 1)
  {
    cancelTimer(tWait);
    gPass++;
    write("用例1通过");
    gStep = 2;
    txMsg.byte(0) = 0x02;
    output(txMsg);
    setTimer(tWait, 200);
  }
}

on message 0x201
{
  if (gStep == 2)
  {
    cancelTimer(tWait);
    gPass++;
    write("用例2通过");
    gStep = 3;
    write("全部用例完成:通过=%d 失败=%d", gPass, gFail);
  }
}

on timer tWait
{
  if (gStep == 1 || gStep == 2)
  {
    gFail++;
    write("用例%d超时失败", gStep);
    gStep = 3;
  }
}

这个模板的关键点有三处:第一,on start 只负责初始化并触发第一个用例;第二,每个 on message 处理块用 gStep 判断当前处于哪个用例,避免不同用例的响应互相干扰;第三,on timer 统一处理超时,保证任何一步等待都不会无限挂起。实际项目中,可以把每个用例的激励构造、期望值判断抽成独立函数,由 gStep 调度,从而在保持入口清晰的同时支持几十条用例。

调试建议:在 on start 和每个事件处理块中加入 write() 输出当前步骤与关键变量,配合CANoe的Write窗口可以快速定位是激励未发出、响应未收到,还是判定条件写错。
CAPL中不要使用 while 或 delay 做阻塞等待。总线事件和定时器事件都在同一个事件循环中调度,阻塞会直接导致响应事件无法被处理,测试必然超时失败。