4. CAPL函数与定时器:自定义函数、定时器(msTimer, timer)的创建与使用

各位好,今天我们聊一个非常核心的话题——CAPL里的函数和定时器。

说实话,这两个东西在HIL测试里太常用了。你想想看,没有函数,你的代码就是一团乱麻;没有定时器,很多时序相关的测试根本没法做。我在项目里见过不少新手,上来就写一大段代码,结果调试的时候自己都看不懂。嗯,这就是没用好函数。

4.1 自定义函数:让代码更清爽

先说说自定义函数。CAPL里的函数,说白了就是把一段重复用的逻辑包起来。你给它起个名字,传几个参数,它帮你干活,最后可能还给你返回个结果。

我个人习惯,只要一段代码出现两次以上,我就会考虑把它写成函数。这不是什么高深技巧,纯粹是为了少写重复代码,少出bug。

函数的基本结构

// 无返回值函数
void SetRelayState(int relayId, int state)
{
    // 设置继电器状态
    sysSetVariableInt("Relay_Module", "Relay_State", relayId, state);
    write("继电器 %d 状态设置为 %d", relayId, state);
}

// 有返回值函数
int CalculateChecksum(byte data[], int length)
{
    int i;
    int checksum = 0;
    
    for(i = 0; i < length; i++)
    {
        checksum += data[i];
    }
    
    return checksum & 0xFF;
}

你看,第一个函数只是执行一个操作,不返回东西。第二个函数算完校验和,把结果返回给调用者。这两种形式,基本覆盖了90%的场景。

我的小建议

函数名最好能让人一眼看懂它是干嘛的。比如 SetRelayState,一看就知道是设置继电器状态。别用 Func1DoSomething 这种名字,三个月后你自己都看不懂。

4.2 函数的参数传递

CAPL的参数传递,有个地方容易踩坑——数组参数。

// 正确写法:数组作为参数
void ProcessMessage(byte buffer[], int size)
{
    int i;
    for(i = 0; i < size; i++)
    {
        buffer[i] = buffer[i] ^ 0xFF;  // 取反操作
    }
}

// 调用方式
byte txData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
ProcessMessage(txData, 8);

我曾经犯过一个错:在函数里修改了数组,以为只是局部修改,结果发现原始数组也被改了。CAPL里数组参数默认是传引用的,不是传值。这一点要记住。

注意

CAPL不支持函数重载。你不能写两个同名但参数不同的函数。如果你需要类似功能,可以用默认参数值,或者干脆换个函数名。

4.3 定时器:msTimer 和 timer

定时器是CAPL里另一个重要工具。HIL测试里,很多场景需要「等一会儿再干某事」,或者「每隔一段时间干某事」。这时候就要用定时器。

CAPL提供了两种定时器:

类型 精度 最小单位 典型用途
msTimer 毫秒级 1 ms 周期性发送报文、延时操作
timer 秒级 1 s 长时间等待、超时判断

说白了,msTimer 适合精细控制,timer 适合粗粒度操作。我在项目里,90%的情况用 msTimer,因为HIL测试对时间精度要求通常比较高。

4.4 定时器的创建与使用

先看一个完整的例子:

// 声明定时器变量
msTimer periodicTimer;
timer  timeoutTimer;

// 定时器到期时触发的函数
on timer periodicTimer
{
    // 每100ms发送一次CAN报文
    message 0x100 msg;
    msg.dlc = 8;
    msg.byte(0) = 0xAA;
    msg.byte(1) = 0xBB;
    output(msg);
    
    // 重新启动定时器,实现周期性
    setTimer(periodicTimer, 100);
}

on timer timeoutTimer
{
    write("超时!5秒内没有收到响应");
    // 执行超时处理逻辑
    SetRelayState(1, 0);  // 关闭继电器
}

// 在某个事件中启动定时器
on key 's'
{
    // 启动周期定时器,每100ms触发一次
    setTimer(periodicTimer, 100);
    write("周期发送已启动");
}

on key 't'
{
    // 启动超时定时器,5秒后触发
    setTimer(timeoutTimer, 5);
    write("超时定时器已启动,等待5秒...");
}

这个例子展示了两个关键点:

  • setTimer() 用来启动定时器,第一个参数是定时器变量,第二个参数是时间值
  • on timer 后面跟定时器变量名,定义到期后执行什么
  • 要实现周期性,需要在 on timer 里再次调用 setTimer()

重要提醒

定时器变量必须在全局范围声明。你不能在函数内部声明定时器。这是CAPL的语法限制,我刚开始也踩过这个坑。

4.5 定时器的取消与重置

有时候,定时器启动了,但条件变了,我们想取消它。CAPL提供了 cancelTimer() 函数。

// 取消定时器
on key 'c'
{
    cancelTimer(periodicTimer);
    cancelTimer(timeoutTimer);
    write("所有定时器已取消");
}

// 重置定时器(其实就是取消再重新设置)
on key 'r'
{
    cancelTimer(timeoutTimer);
    setTimer(timeoutTimer, 5);
    write("超时定时器已重置");
}

我曾经遇到一个场景:测试ECU的休眠唤醒。发送唤醒报文后启动一个5秒超时定时器,如果ECU在5秒内回复了,就取消定时器;如果没回复,定时器到期就报失败。这个逻辑用定时器实现非常清晰。

4.6 定时器的高级用法

再说一个实用技巧——多个定时器配合使用。

msTimer sendTimer;
msTimer checkTimer;

on timer sendTimer
{
    // 发送测试报文
    message 0x200 testMsg;
    testMsg.byte(0) = 0x01;
    output(testMsg);
    
    // 启动检查定时器,50ms后检查响应
    setTimer(checkTimer, 50);
}

on timer checkTimer
{
    // 检查是否收到响应
    if(g_ResponseReceived == 0)
    {
        write("警告:未收到ECU响应");
        // 记录故障
    }
    else
    {
        write("ECU响应正常");
        g_ResponseReceived = 0;  // 重置标志
    }
}

on message 0x300
{
    // 收到ECU的响应报文
    g_ResponseReceived = 1;
}

这个模式在HIL测试里很常见:先发一个请求,然后等一段时间检查响应。如果没响应,就报错。你想想看,这不就是测试自动化里最基本的「请求-响应」模式吗?

实用技巧

我建议把定时器的时间值定义成常量,而不是直接写数字。比如:

const int SEND_INTERVAL_MS = 100;
const int RESPONSE_TIMEOUT_MS = 50;

setTimer(sendTimer, SEND_INTERVAL_MS);
setTimer(checkTimer, RESPONSE_TIMEOUT_MS);

这样改时间的时候,只改一处就行,不会漏掉。

4.7 常见坑与避坑指南

最后,分享几个我踩过的坑:

  • 定时器重入问题:如果一个定时器还没到期,你又调了一次 setTimer(),它会重新计时。这不是bug,是特性。但如果你没意识到这一点,可能会出问题。
  • 定时器精度:msTimer 说是毫秒级,但实际精度受系统负载影响。我在Windows下跑CANoe,实测精度大概在1-2ms左右。如果你需要微秒级控制,得用硬件定时器。
  • 函数递归:CAPL支持函数递归调用,但递归深度有限制。我试过超过10层递归就报错了。所以,能用循环解决的问题,别用递归。

好了,关于CAPL函数和定时器,今天就聊这么多。这些东西看着简单,但用好了,你的测试脚本会变得非常清晰、高效。下一章我们聊聊事件处理,那也是CAPL里一个很有意思的话题。