4、CAPL脚本入门:CAPL语言基础、事件函数(on message, on key)、简单路由逻辑实现

好,咱们进入第四讲。前面几章我们把CANoe的界面、工程配置、网关路由的基础概念都过了一遍。从这章开始,要动真格的了——写代码。

别紧张,CAPL其实没那么可怕。它不像C语言那么底层,也不像Python那么自由。它就是个专门为汽车总线测试设计的脚本语言。说白了,就是让CANoe按照你的想法去干活。

4.1 CAPL语言基础:先认识几个关键点

我第一次接触CAPL的时候,觉得它长得挺像C语言。确实,语法上有很多相似之处。但有几个地方,你得特别注意。

4.1.1 变量声明

CAPL里声明变量,跟C语言一样,要指定类型。常用的有这些:

类型 说明 示例
int 32位有符号整数 int counter = 0;
dword 32位无符号整数 dword msgId = 0x123;
word 16位无符号整数 word length = 8;
byte 8位无符号整数 byte dataByte = 0xFF;
float 浮点数 float voltage = 12.5;
char 字符 char ch = 'A';

嗯,这里要注意:CAPL里没有全局变量和局部变量的说法,只有全局变量事件函数内的变量。全局变量要声明在variables块里。

关键点:所有全局变量,必须写在 variables { } 这个大括号里。这是CAPL的规矩。

variables
{
  int g_counter = 0;
  dword g_lastMsgId = 0;
  byte g_routingTable[10];
}

4.1.2 数组和结构体

数组用法跟C一样。结构体稍微有点不同,你得先定义再使用。

// 定义结构体
struct CanMessage
{
  dword id;
  byte dlc;
  byte data[8];
};

// 声明结构体变量
struct CanMessage g_rxMsg;

我个人习惯把结构体定义放在variables块之前,这样清晰一些。

4.2 事件函数:CAPL的灵魂

CAPL是事件驱动的。什么意思呢?就是平时它闲着,只有特定事件发生了,它才会执行对应的代码。这些事件函数,就是CAPL的入口。

4.2.1 on message:最常用的事件

on message 是CAPL里最核心的事件函数。当CAN总线上有报文过来时,它就会被触发。

on message 0x123
{
  // 当收到ID为0x123的报文时,执行这里的代码
  write("收到报文: 0x%x", this.id);
}

这里有个关键字 this,它代表当前收到的报文对象。通过 this.idthis.dlcthis.byte(0) 可以访问报文的各个字段。

小技巧:如果你想让所有报文都触发,可以用 on message *。但要注意,这样性能开销会大一些。我在项目中一般只在调试阶段用 *,正式代码里都会指定具体的ID。

4.2.2 on key:键盘交互

这个函数用来响应键盘按键。调试的时候特别方便。

on key 'a'
{
  write("你按下了 'a' 键");
}

on key 'F1'
{
  write("你按下了 F1 功能键");
}

我曾经在测试网关路由时,就用 on key 来手动触发不同的路由策略。按 '1' 走路径A,按 '2' 走路径B,调试效率高了不少。

4.2.3 其他常用事件

事件函数 触发时机
on start 测量开始时执行一次
on preStart 测量开始前执行
on stopMeasurement 测量停止时执行
on timer 定时器到期时执行
on errorFrame 检测到错误帧时执行

4.3 简单路由逻辑实现:从理论到代码

好了,基础语法和事件函数都认识了。咱们来写点真正有用的东西——一个简单的网关路由逻辑。

假设场景是这样的:

  • CAN1总线上有报文0x123,需要转发到CAN2总线
  • 转发时,报文ID要改成0x456
  • 数据内容保持不变

这个需求很典型吧?我在实际项目中遇到过无数次。下面就是实现代码:

variables
{
  // 定义两个CAN通道的句柄
  message 0x123 g_msgFromCan1;
  message 0x456 g_msgToCan2;
}

on message 0x123
{
  // 收到CAN1上的报文
  if (this.can == 1)  // 确认来自CAN1
  {
    // 复制数据到目标报文
    g_msgToCan2.dlc = this.dlc;
    for (int i = 0; i < this.dlc; i++)
    {
      g_msgToCan2.byte(i) = this.byte(i);
    }
    
    // 发送到CAN2
    output(g_msgToCan2);
    
    write("路由成功: 0x%x (CAN1) -> 0x%x (CAN2)", this.id, g_msgToCan2.id);
  }
}

注意:这里有个容易踩的坑。如果你在 on message 里直接修改 this.id 然后 output(this),是不行的。因为 this 是只读的。你必须创建一个新的报文对象来发送。

3.3.1 用键盘控制路由开关

调试的时候,我们经常需要临时开启或关闭路由。用 on key 来实现就很简单:

variables
{
  int g_routingEnabled = 1;  // 1:开启, 0:关闭
}

on key 'r'
{
  g_routingEnabled = !g_routingEnabled;
  if (g_routingEnabled)
  {
    write("路由已开启");
  }
  else
  {
    write("路由已关闭");
  }
}

on message 0x123
{
  if (g_routingEnabled)
  {
    // 执行路由逻辑
    // ... 代码同上
  }
}

你看,这样在测试时按一下 'r' 键,就能控制路由的启停。我曾经在实车测试时,就用这个方法来模拟网关的故障模式,省了不少事。

4.4 本章小结

这一章我们学了CAPL的变量声明、事件函数,还写了一个简单的路由逻辑。你想想看,其实核心就三点:

  1. 变量声明:全局变量放 variables 块里
  2. 事件函数on message 处理报文,on key 处理键盘
  3. 路由实现:收到报文 -> 创建新报文 -> 复制数据 -> 发送

下一章,我们会把这个简单的路由逻辑扩展成更复杂的场景,比如多ID路由、数据映射、信号级别的路由。嗯,到时候你会看到,CAPL的威力远不止于此。

好,今天就到这里。动手试试吧,在CANoe里建个新工程,写个 on message 看看效果。相信我,亲手跑通一次,比看十遍教程都管用。