2. CANalyzer CAPL编程入门:CAPL语言基础、事件处理、定时器与消息发送

好,咱们进入正题。CAPL,全称是Communication Access Programming Language。说白了,它就是CANalyzer的“灵魂脚本语言”。你想想看,光靠图形界面拖拖拽拽,很多自动化测试场景根本搞不定。这时候,CAPL就派上用场了。

我个人习惯把CAPL看作一个“事件驱动的微型C语言”。它没有main函数,全靠各种事件来触发执行。这一点,刚接触的朋友可能会觉得别扭。但用熟了,你会发现它特别适合汽车电子这种实时性要求高的场景。

2.1 CAPL语言基础:数据类型、变量与基本语法

先看数据类型。CAPL支持的类型,跟C语言很像,但也有自己的特色。

类型 说明 我常用的场景
int / long 整数,32位 计数器、索引
dword 无符号32位 报文ID、掩码
word 无符号16位 信号值截取
byte 无符号8位 原始字节数据
float 浮点数 物理值计算
char 字符 字符串处理

变量声明也很直接。比如:

int counter = 0;
dword msgId = 0x123;
float temperature = 25.5;
char buffer[64];

嗯,这里要注意。CAPL里变量默认是全局的,除非你在函数内部声明。我在项目中遇到过一个问题:两个事件函数里用了同名变量,结果互相干扰,查了半天。后来我养成了一个习惯——能用局部变量,绝不轻易用全局。

我的小技巧: 全局变量命名加个前缀,比如 g_counter、g_statusFlag。这样一眼就能看出来是全局的,避免踩坑。

2.2 事件处理:on message、on key、on timer

CAPL的核心就是事件。你写代码,其实就是告诉CANalyzer:“当某某事情发生时,你就执行这段代码”。

on message 是最常用的。每当总线上出现一个报文,就会触发。

on message 0x123
{
  write("收到报文 0x123");
  // 这里可以解析信号
}

你也可以用通配符,比如 on message * 捕获所有报文。但说实话,我一般不建议这么干。为什么呢?因为如果报文量很大,比如CAN FD总线,每秒几千帧,你全抓进来,CAPL的执行效率会明显下降。我曾经在一个项目中,就因为用了 on message * 导致界面卡顿,后来改成只监听需要的ID,问题就解决了。

on key 用于键盘交互。调试时特别好用。

on key 'a'
{
  write("你按下了 a 键,开始测试...");
  // 触发某个测试序列
}

我个人习惯用 on key 来做手动触发点。比如按 's' 开始测试,按 'r' 复位。这样调试时不用每次都去点鼠标,效率高很多。

on timer 是定时器事件。这个后面会详细讲,先看个简单例子:

on timer myTimer
{
  write("定时器到期了!");
}

2.3 定时器:msTimer与timer的区别

CAPL里有两种定时器:msTimertimer。区别很简单:

  • msTimer:以毫秒为单位。精度高,适合短周期任务。
  • timer:以秒为单位。适合长周期任务。

看个实际例子:

msTimer cycleTimer;  // 毫秒定时器
timer longTimer;     // 秒定时器

on start
{
  setTimer(cycleTimer, 100);  // 100ms触发一次
  setTimer(longTimer, 5);     // 5秒触发一次
}

on timer cycleTimer
{
  // 每100ms执行一次
  write("周期任务执行中...");
  setTimer(cycleTimer, 100);  // 重新启动
}

on timer longTimer
{
  // 每5秒执行一次
  write("长周期任务执行中...");
  setTimer(longTimer, 5);
}
注意: 定时器是一次性的。触发后必须重新 setTimer,否则它不会再执行。我刚开始学的时候,就忘了重新设置,结果定时器只跑了一次就停了,还以为是代码写错了。

还有一个细节:cancelTimer 可以取消定时器。比如你按了停止键,就应该把正在运行的定时器都取消掉,避免资源浪费。

2.4 消息发送:output函数与报文构造

光收不发,那只是监听。CAPL也能发报文。核心函数就是 output

先构造一个报文对象:

message 0x123 msg;  // 声明一个ID为0x123的报文

on key 's'
{
  msg.dlc = 8;       // 数据长度
  msg.byte(0) = 0xAA;
  msg.byte(1) = 0xBB;
  msg.word(2) = 0x1234;  // 注意:word是从第2字节开始
  output(msg);       // 发送出去
  write("报文已发送");
}

这里有个容易搞混的地方:byte()word() 的索引。 byte(0) 是第一个字节,word(2) 是从第三个字节开始的两个字节。说白了,索引就是起始位置。

我在项目中遇到过一个问题:用 word() 赋值时,没注意字节序。比如我想发0x1234,结果在CANoe里看到的是0x3412。后来才想起来,CAN总线默认是Intel格式(小端序)。如果你用的是Motorola格式(大端序),就得自己手动调一下字节顺序。

核心要点: 发送报文时,一定要确认DBC文件里的信号定义。是Intel还是Motorola?信号起始位在哪?长度多少?这些搞错了,你发的报文就是废的。

另外,output 也可以发送远程帧:

message 0x200.remote req;
output(req);  // 发送远程帧请求

远程帧在诊断测试中很常用。比如你想让某个ECU发送它的当前状态,就可以发一个远程帧过去。

2.5 实战小例子:周期性发送与接收验证

咱们把上面学的串起来,写一个完整的例子。假设我们要:

  1. 每100ms发送一个报文0x100,包含一个递增的计数器。
  2. 同时监听报文0x200,收到后打印出来。
  3. 按 'q' 键停止发送。
msTimer txTimer;
message 0x100 txMsg;
int counter = 0;

on start
{
  setTimer(txTimer, 100);
  write("测试开始,每100ms发送0x100");
}

on timer txTimer
{
  txMsg.byte(0) = counter & 0xFF;
  txMsg.byte(1) = (counter >> 8) & 0xFF;
  output(txMsg);
  counter++;
  setTimer(txTimer, 100);
}

on message 0x200
{
  write("收到0x200,数据: %02x %02x", this.byte(0), this.byte(1));
}

on key 'q'
{
  cancelTimer(txTimer);
  write("已停止发送");
}

这个例子虽然简单,但涵盖了事件、定时器、消息发送和接收。你想想看,实际项目中很多自动化测试脚本,本质上就是这种模式的扩展。比如压力测试,就是高频发送报文,然后监控响应是否超时。

好了,这一章的内容就到这里。CAPL入门其实不难,关键是动手写。我建议你打开CANalyzer,新建一个CAPL模块,把上面的例子敲一遍。遇到问题不要怕,调试信息用 write() 打印出来,慢慢就摸到门道了。