4. CAPL测试脚本基础
CAPL(Communication Access Programming Language)是CANoe内置的类C事件驱动脚本语言。它不像普通程序那样从main函数顺序执行,而是由总线事件、定时器、键盘或系统事件触发对应的事件处理程序(event handler)。理解这一点,是写出可靠ECU通信一致性测试脚本的前提:测试用例的“自动化执行入口”本质上就是一组被精心编排的事件处理块。
本节围绕四个核心能力展开:报文收发、信号读写、定时器、事件处理,并最终把它们组合成一个可复用的测试用例入口结构。
一、CAPL程序的基本骨架
一个CAPL节点(Network Node)通常包含变量声明区、事件处理区和自定义函数区。变量分为全局变量、局部变量和系统变量引用。测试脚本中常用的声明如下:
variables
{
msTimer tWait; // 毫秒定时器
int gStep = 0; // 测试步骤计数器
int gResult = 0; // 0=未判定 1=通过 2=失败
message 0x123 txMsg; // 待发送报文
}
on start
{
write("测试节点已启动");
gStep = 0;
setTimer(tWait, 100); // 启动后100ms进入第一步
}
其中 on start 是CAPL节点加载并开始测量后自动触发的事件,非常适合作为测试用例的统一入口。把初始化、首条激励发送、首个定时器启动都放在这里,测试流程就有了确定的起点。
二、报文收发:控制总线上的帧
发送报文用 output(),接收报文用 on message 事件。发送前需要给报文对象赋值,包括ID、DLC和数据字节。例如构造一条周期为20ms、数据为 00 11 22 33 44 55 66 77 的报文:
on start
{
txMsg.id = 0x123;
txMsg.dlc = 8;
txMsg.byte(0) = 0x00;
txMsg.byte(1) = 0x11;
txMsg.byte(2) = 0x22;
txMsg.byte(3) = 0x33;
txMsg.byte(4) = 0x44;
txMsg.byte(5) = 0x55;
txMsg.byte(6) = 0x66;
txMsg.byte(7) = 0x77;
output(txMsg); // 单次发送
}
on message 0x200
{
// 收到被测ECU的响应帧
if (this.byte(0) == 0xAA)
{
gResult = 1; // 判定通过
}
else
{
gResult = 2; // 判定失败
}
}
注意 this 关键字代表当前触发事件的报文对象,可直接读取其字节、DLC和时间戳。若需要周期发送,可用 setTimer 配合定时器反复调用 output(),而不是依赖 output 的循环——CAPL没有阻塞式循环等待,必须用事件驱动方式组织时序。
三、信号读写:从报文到物理值
直接操作字节不利于表达物理含义。CAPL支持通过数据库(DBC)中定义的信号名读写:
on start
{
txMsg.EngineSpeed = 1500; // 写入信号(物理值)
output(txMsg);
}
on message 0x200
{
float v;
v = this.VehicleSpeed; // 读取信号物理值
if (v > 100.0)
{
write("车速超限: %f", v);
}
}
信号读写依赖DBC中已定义的报文与信号映射。若信号未在数据库中定义,则只能按字节操作。测试脚本中建议优先使用信号名,这样脚本可读性高,且当DBC更新时无需改动字节偏移。
四、定时器与事件处理:构建时序
CAPL提供 msTimer(毫秒)和 timer(秒级,精度较低)两类定时器。测试用例中常用 setTimer 启动、cancelTimer 取消,并在 on timer 中处理超时逻辑。下面是一个典型的“发送激励—等待响应—超时判定”结构:
on start
{
gStep = 1;
output(txMsg); // 发送激励
setTimer(tWait, 200); // 200ms内等待响应
}
on message 0x200
{
if (gStep == 1)
{
cancelTimer(tWait); // 收到响应,取消超时
gResult = 1;
gStep = 2;
write("响应正常,用例通过");
}
}
on timer tWait
{
if (gStep == 1)
{
gResult = 2;
write("超时未收到响应,用例失败");
}
}
这段代码体现了CAPL测试脚本的核心模式:事件驱动 + 定时器超时保护。没有定时器,脚本可能在等待响应时永久挂起;没有事件处理,脚本无法对总线上的异步响应做出反应。
五、把测试用例封装成可复用入口
当多个测试用例需要顺序执行时,推荐用一个步骤计数器 gStep 驱动状态机,每个步骤对应一次激励发送和一次响应等待。下面给出一个可直接参考的测试用例入口模板,包含两个用例:用例1检查0x200响应,用例2检查0x201响应。
variables
{
msTimer tWait;
int gStep = 0;
int gPass = 0;
int gFail = 0;
message 0x123 txMsg;
}
on start
{
gStep = 1;
gPass = 0;
gFail = 0;
txMsg.dlc = 8;
txMsg.byte(0) = 0x01;
output(txMsg);
setTimer(tWait, 200);
}
on message 0x200
{
if (gStep == 1)
{
cancelTimer(tWait);
gPass++;
write("用例1通过");
gStep = 2;
txMsg.byte(0) = 0x02;
output(txMsg);
setTimer(tWait, 200);
}
}
on message 0x201
{
if (gStep == 2)
{
cancelTimer(tWait);
gPass++;
write("用例2通过");
gStep = 3;
write("全部用例完成:通过=%d 失败=%d", gPass, gFail);
}
}
on timer tWait
{
if (gStep == 1 || gStep == 2)
{
gFail++;
write("用例%d超时失败", gStep);
gStep = 3;
}
}
这个模板的关键点有三处:第一,on start 只负责初始化并触发第一个用例;第二,每个 on message 处理块用 gStep 判断当前处于哪个用例,避免不同用例的响应互相干扰;第三,on timer 统一处理超时,保证任何一步等待都不会无限挂起。实际项目中,可以把每个用例的激励构造、期望值判断抽成独立函数,由 gStep 调度,从而在保持入口清晰的同时支持几十条用例。
on start 和每个事件处理块中加入 write() 输出当前步骤与关键变量,配合CANoe的Write窗口可以快速定位是激励未发出、响应未收到,还是判定条件写错。while 或 delay 做阻塞等待。总线事件和定时器事件都在同一个事件循环中调度,阻塞会直接导致响应事件无法被处理,测试必然超时失败。