2. CAPL语言基础:数据类型、变量声明、事件函数

各位同学好,我是老张。今天咱们聊聊CAPL语言最基础的东西——数据类型、变量声明,还有那几个你天天都会碰到的事件函数。说实话,我刚开始学CAPL的时候,觉得这东西跟C语言差不多,结果一上手就踩了不少坑。嗯,咱们慢慢来,把这些基础打扎实了,后面写脚本才能顺手。

2.1 CAPL的数据类型——别小看这些基础类型

CAPL的数据类型,说白了就是告诉编译器「我要存什么数据」。跟C语言很像,但有些细节不一样。我最早做HIL测试时,就因为搞混了数据类型,导致总线报文解析出来全是乱码,排查了半天才发现是类型用错了。

咱们先看最常用的几种:

类型 关键字 位宽 取值范围 我常用的场景
整数型 int 32位 -2^31 ~ 2^31-1 计数器、索引
无符号整数 dword 32位 0 ~ 2^32-1 报文ID、掩码
长整数 long 64位 -2^63 ~ 2^63-1 时间戳、大数值
字节型 byte 8位 0 ~ 255 单字节信号
浮点型 float 32位 ±1.18e-38 ~ ±3.4e38 模拟量、电压值
字符型 char 8位 ASCII字符 字符串处理

重点提醒:CAPL里没有unsigned int这种写法。你想用无符号整数,就用dword。我见过不少新手用int去存报文ID,结果ID超过0x7FFFFFFF就变成负数了,那解析出来的数据肯定不对。

还有一个特殊类型——message。这是CAPL独有的,专门用来表示CAN/LIN/FlexRay报文。你声明一个message变量,就能直接操作它的ID、DLC、数据字节。举个例子:

message 0x100 msg;  // 声明一个ID为0x100的CAN报文
msg.dlc = 8;        // 设置数据长度
msg.byte(0) = 0xAA; // 给第一个字节赋值
output(msg);        // 发送到总线上

我个人习惯把message变量声明成全局的,这样在多个事件函数里都能用。但要注意,别在on start里声明局部message,否则出了函数就没了。

2.2 变量声明——全局还是局部?这是个问题

变量声明这块,其实就两个原则:作用域生命周期。全局变量在整个脚本运行期间都存在,局部变量只在当前函数或事件块里有效。

我一般这样划分:

  • 全局变量:放在文件最前面,用variables关键字包裹。适合存那些需要跨函数共享的数据,比如测试状态、累计计数。
  • 局部变量:在事件函数或自定义函数内部声明。适合临时计算、循环索引。

看个例子:

variables
{
  int g_testCount = 0;      // 全局变量,记录测试次数
  dword g_lastMsgId = 0;    // 全局变量,记录上一条报文ID
}

on start
{
  int localVar = 10;        // 局部变量,只在on start里有效
  g_testCount = 0;          // 初始化全局变量
  write("测试开始,初始计数:%d", g_testCount);
}

on message 0x200
{
  int localCount = 0;       // 每次收到报文,这个变量都会重新初始化为0
  localCount++;
  g_testCount++;            // 全局变量会一直累加
  write("局部计数:%d,全局计数:%d", localCount, g_testCount);
}

我的小技巧:全局变量命名时加个前缀,比如g_,这样一眼就能看出来是全局的。我曾经在一个3000行的脚本里找bug,就是因为全局变量和局部变量重名了,那个排查过程……唉,不提了。

另外,CAPL不支持动态内存分配,所以别想着用malloc。所有变量在编译时就已经确定好了大小。你想想看,这其实也挺好,至少不会出现内存泄漏的问题。

2.3 事件函数——CAPL的灵魂所在

事件函数,说白了就是「当某个事情发生时,自动执行的代码块」。CAPL是事件驱动的,你不需要写main函数,也不需要轮询。你只需要告诉系统:当XX发生时,执行YY

咱们重点讲三个最常用的事件函数:on starton stopon message

2.3.1 on start——一切从这里开始

on start在测量开始时执行一次。我通常用它来做初始化工作:

  • 清空全局变量
  • 打开日志文件
  • 发送初始化报文
  • 设置定时器
on start
{
  write("=== HIL测试开始 ===");
  g_testPass = 0;
  g_testFail = 0;
  
  // 设置一个1秒的循环定时器
  setTimer(cycleTimer, 1000);
  
  // 发送唤醒报文
  message 0x100 wakeMsg;
  wakeMsg.byte(0) = 0x01;
  output(wakeMsg);
  
  write("初始化完成,等待报文...");
}

注意一点:on start里不要做耗时操作,比如延时等待。因为整个脚本的启动流程是串行的,你在这里卡住了,后面的初始化就进行不下去。我记得有一次,我在on start里加了个wait(5000),结果CANoe界面卡了5秒才出来,被项目经理骂了一顿。

2.3.2 on stop——善始善终

on stop在测量停止时执行。它的主要用途是收尾工作:

  • 关闭文件句柄
  • 输出测试统计结果
  • 恢复ECU到安全状态
on stop
{
  write("=== HIL测试结束 ===");
  write("通过:%d,失败:%d", g_testPass, g_testFail);
  
  // 发送休眠报文
  message 0x100 sleepMsg;
  sleepMsg.byte(0) = 0x00;
  output(sleepMsg);
  
  // 关闭日志
  fileClose(g_logHandle);
}

避坑指南:我曾经在on stop里调用output()发送报文,结果发现报文根本没发出去。为什么?因为on stop触发时,CAN硬件可能已经停止工作了。所以,如果你需要在停止时发送报文,最好在on preStop里处理,那个事件触发得更早。

2.3.3 on message——最核心的事件

on message是CAPL里最常用的事件函数,没有之一。只要总线上有报文过来,它就会触发。你可以指定监听某个ID,也可以监听所有报文。

基本语法:

// 监听特定ID
on message 0x200
{
  write("收到ID为0x200的报文");
  write("第一个字节:0x%02x", this.byte(0));
}

// 监听所有报文
on message *
{
  write("收到报文,ID:0x%x", this.id);
}

// 监听带掩码的ID范围
on message 0x100-0x1FF
{
  write("收到ID在0x100到0x1FF之间的报文");
}

这里有个关键点——this关键字。在on message事件里,this代表当前收到的报文对象。你可以通过this.id获取ID,this.dlc获取数据长度,this.byte(index)获取某个字节。

我实际项目中的一个例子:

on message 0x3E0  // 发动机转速报文
{
  // 解析转速信号,假设转速在byte0和byte1,大端模式
  int rpm;
  rpm = (this.byte(0) << 8) | this.byte(1);
  
  // 判断是否超速
  if (rpm > 6000)
  {
    write("警告:发动机超速!当前转速:%d rpm", rpm);
    g_overSpeedCount++;
  }
  
  // 记录到测试报告中
  testReportAddData("EngineSpeed", rpm);
}

性能建议:如果总线报文很多(比如CAN FD,每秒几千帧),on message *里不要做复杂计算。否则CPU占用率会飙升,导致丢帧。我一般只在on message *里做简单的过滤,然后把数据交给定时器去处理。

2.4 综合示例——一个简单的报文监控脚本

咱们把今天学的知识串起来,写一个实用的脚本:监控CAN总线上的报文,统计每种ID出现的次数,并在测试结束时输出报告。

variables
{
  // 用结构体数组存储报文统计信息
  struct MsgStat
  {
    dword id;
    int count;
  } g_msgStats[100];
  
  int g_msgIndex;
}

on start
{
  int i;
  g_msgIndex = 0;
  
  // 初始化统计数组
  for (i = 0; i < 100; i++)
  {
    g_msgStats[i].id = 0;
    g_msgStats[i].count = 0;
  }
  
  write("报文监控脚本已启动");
}

on message *
{
  int i;
  int found = 0;
  
  // 查找是否已有该ID的统计
  for (i = 0; i < g_msgIndex; i++)
  {
    if (g_msgStats[i].id == this.id)
    {
      g_msgStats[i].count++;
      found = 1;
      break;
    }
  }
  
  // 如果是新ID,添加到统计列表
  if (found == 0 && g_msgIndex < 100)
  {
    g_msgStats[g_msgIndex].id = this.id;
    g_msgStats[g_msgIndex].count = 1;
    g_msgIndex++;
  }
}

on stop
{
  int i;
  
  write("===== 报文统计报告 =====");
  write("共检测到 %d 种报文ID", g_msgIndex);
  
  for (i = 0; i < g_msgIndex; i++)
  {
    write("ID: 0x%X, 出现次数: %d", 
          g_msgStats[i].id, g_msgStats[i].count);
  }
  
  write("===== 报告结束 =====");
}

这个脚本虽然简单,但涵盖了今天讲的所有知识点:全局变量声明、结构体使用、on start初始化、on message处理、on stop收尾。你可以在自己的CANoe项目里试试,看看能不能跑起来。

好了,今天的内容就到这里。数据类型、变量声明、事件函数,这三样东西是CAPL的基石。你把这章吃透了,后面学定时器、诊断、系统变量什么的,就会轻松很多。下节课咱们聊CAPL的流程控制——if else、for循环、while循环,这些东西写测试逻辑时天天用。

有什么问题,欢迎在课程群里交流。我是老张,咱们下节课见。