3. CAPL语言基础与测试脚本结构

CAPL(Communication Access Programming Language)是Vector为CANoe量身打造的类C事件驱动脚本语言。它不像通用编程语言那样从main函数顺序执行,而是围绕"事件"组织代码:总线报文到达、定时器超时、按键触发、测量开始或结束,都会调用你预先写好的事件处理函数。理解CAPL的变量、数据类型、运算符、流程控制、函数定义以及事件驱动机制与程序生命周期,是写出可靠自动化测试脚本的前提。

变量与数据类型

CAPL的变量按作用域分为全局变量和局部变量。全局变量在variables块中声明,整个测量期间持续存在,可被多个事件处理函数共享;局部变量在函数或事件块内部声明,只在本次调用中有效。CAPL是静态类型语言,声明时必须给出类型,常用类型如下:

  • int:16位有符号整数,范围约 -32768~32767,适合计数器、状态码。
  • long:32位有符号整数,适合报文ID、时间戳、累加量。
  • dword:32位无符号整数,常用于原始报文数据。
  • float/double:浮点数,用于电压、温度、物理量计算。
  • byte:8位无符号整数,适合单字节数据。
  • char:字符类型,配合字符串使用。
  • message:报文对象,如message 0x123 msg1;声明一条ID为0x123的报文。
  • timer:定时器对象,用于周期或延时触发。
  • msTimer:毫秒级定时器,最常用。

数组声明形如int buf[8];,字符串用char name[32];。CAPL还支持结构体struct,可把多个相关字段打包,例如把一条诊断请求的SID、子功能、数据长度组合成一个结构体,便于在多个函数间传递。

运算符与流程控制

CAPL运算符与C基本一致:算术运算符+ - * / %,关系运算符== != > < >= <=,逻辑运算符&& || !,位运算符& | ^ ~ << >>,以及赋值和复合赋值= += -=。处理报文数据时位运算尤其重要,例如从数据字节中提取某几位:value = (data[0] >> 4) & 0x0F;。

流程控制包括if/else、switch/case、while、for、do...while。CAPL没有goto,但支持break和continue。在测试脚本中,switch常用于根据报文ID或诊断服务ID分派处理逻辑,for常用于遍历数组或循环发送多帧报文。需要注意:CAPL事件处理函数中应避免长时间阻塞循环,否则会拖慢整个测量系统的实时响应。

函数定义

CAPL函数用void或具体返回类型定义,例如:

int Add(int a, int b)
{
  return a + b;
}

void SendTestFrame(long id, byte data0)
{
  message *msg;
  msg.id = id;
  msg.dlc = 8;
  msg.byte(0) = data0;
  output(msg);
}

函数可以带参数和返回值,支持递归调用。CAPL还提供大量内置函数,如output()发送报文、write()输出日志、setTimer()启动定时器、cancelTimer()取消定时器、testWaitForMessage()等待报文、testStep()记录测试步骤。自定义函数应尽量短小、职责单一,把"构造报文""发送并等待响应""校验结果"拆开,便于复用和排错。

事件驱动机制与程序生命周期

CAPL程序的生命周期由CANoe测量控制:测量开始(Start)时,CAPL程序被加载并初始化,on start事件块执行;测量运行期间,各种事件按发生顺序触发对应处理函数;测量停止(Stop)时,on stop事件块执行,程序结束。常见事件块包括:

  • on start:测量启动时执行一次,适合初始化变量、启动定时器、发送首帧。
  • on stop:测量停止时执行一次,适合保存结果、释放资源。
  • on message 0x123:收到指定ID报文时触发,是总线测试最核心的事件。
  • on timer myTimer:定时器超时触发,用于周期发送或超时判断。
  • on key 'a':按下键盘按键触发,便于手动干预测试。
  • on signal UpdateSignal:信号值变化时触发,适合基于信号层的测试。

下面这张图展示了CAPL程序从测量开始到结束的事件驱动执行路径,以及全局变量、事件处理函数与内置函数之间的调用关系。

CAPL 程序生命周期与事件驱动执行路径 测量开始 Start 加载 CAPL 程序 on start 事件块 初始化变量 / 启动定时器 测量运行中 事件循环等待触发 事件触发源 on message 0x123 ← 总线报文到达 on timer myTimer ← 定时器超时 on key 'a' ← 按键触发 on signal UpdateSignal ← 信号变化 事件处理函数 读取/修改全局变量 调用自定义函数 调用内置函数 output() / write() setTimer() / cancelTimer() 全局变量区 variables 跨事件共享状态 / 计数器 / 标志位 测量停止 Stop on stop 事件块 → 保存结果 / 释放资源 程序结束 触发

一个可运行的测试脚本骨架

把上述要素组合起来,下面是一个典型的CAPL测试脚本结构,用于周期发送报文并统计接收次数:

variables
{
  msTimer tSend;
  int rxCount = 0;
  int txCount = 0;
}

on start
{
  write("测试开始");
  setTimer(tSend, 100);
}

on timer tSend
{
  message 0x123 msg;
  msg.dlc = 8;
  msg.byte(0) = txCount & 0xFF;
  output(msg);
  txCount++;
  setTimer(tSend, 100);
}

on message 0x456
{
  rxCount++;
  if (rxCount >= 10)
  {
    cancelTimer(tSend);
    write("已收到10帧响应,停止发送");
  }
}

on stop
{
  write("发送帧数=%d,接收帧数=%d", txCount, rxCount);
}

这段脚本中,variables块声明全局定时器和计数器;on start启动100ms周期定时器;on timer每100ms发送一帧0x123并递增发送计数;on message 0x456收到响应后累加接收计数,达到10帧时取消定时器;on stop输出统计结果。整个脚本没有main函数,执行完全由事件驱动。

编写CAPL脚本时,建议把"发送激励""等待响应""结果判定"拆成独立函数,事件块只做调度。这样既符合事件驱动模型,也便于在Test Module或Test Sequence中复用。
CAPL事件处理函数中不要写死循环或长时间延时。若需要等待,应使用testWaitForMessage()、testWaitForTimeout()等测试函数,或通过定时器分步推进,否则会阻塞CANoe的实时事件处理,导致报文丢失或测量异常。