3. CAPL编程基础与测试脚本骨架
CAPL(Communication Access Programming Language)是CANoe平台内嵌的类C事件驱动脚本语言。它不像普通C程序那样从main函数顺序执行,而是由总线事件、定时器事件和测量系统事件触发回调函数。理解这一机制,是写出可靠自动化测试脚本的前提。本节围绕事件驱动、变量作用域、定时器、消息收发和测试用例骨架五个要点展开,并给出可直接套用的脚本骨架。
一、事件驱动机制:脚本何时被执行
CAPL脚本由若干事件处理程序(event handler)组成,每个处理程序以关键字开头,例如 on start、on message、on timer、on key、on signal。CANoe在测量运行期间监听这些事件,一旦条件满足就调用对应函数体。脚本中未放入任何事件处理程序的语句不会被执行,这是初学者最常见的错误。
常见事件类型及其触发时机如下:
on start:测量开始时执行一次,用于初始化变量、启动定时器、设置初始报文。on message <Msg>:总线上收到指定报文时触发,用于响应式逻辑。on timer <T>:定时器到期时触发,用于周期发送或超时判断。on signal <Sig>:指定信号值更新时触发,适合做信号级监控。on key 'x':按下键盘按键时触发,便于手动干预调试。
需要特别注意:on message 与 on signal 的触发依赖总线实际收到报文,若节点未发送或通道配置错误,处理程序永远不会被调用。因此调试时应先用Write窗口打印确认事件是否触发。
二、变量作用域:三类变量的选择
CAPL变量按作用域分为全局变量、局部变量和系统变量。全局变量在 variables 块中声明,整个脚本可见,生命周期贯穿整个测量;局部变量在函数或事件处理程序内部声明,仅在该块内有效;系统变量(System Variable)由CANoe数据库定义,可通过 @sysvar::命名空间::变量名 访问,用于面板、测试模块和脚本之间共享数据。
选择原则:跨事件处理程序共享的状态(如计数器、当前测试步骤)用全局变量;临时计算用局部变量;需要与Panel或Test Module交互的数据用系统变量。全局变量在测量重启时会被重新初始化,若需保持状态应写入系统变量或使用 write/read 持久化。
三、定时器:周期发送与超时控制
CAPL定时器通过 msTimer 或 timer 声明,使用 setTimer() 启动、cancelTimer() 取消。定时器到期后触发对应的 on timer 处理程序。典型用法有两种:一是周期发送报文,在 on timer 中发送后再次 setTimer 实现循环;二是超时监控,发送请求后启动定时器,若在超时前收到响应则取消定时器,否则判定超时。
variables
{
msTimer tSend;
msTimer tTimeout;
int gRespFlag = 0;
}
on start
{
setTimer(tSend, 100); // 100ms后首次发送
}
on timer tSend
{
message 0x123 msg;
msg.dlc = 8;
msg.byte(0) = 0x01;
output(msg);
setTimer(tSend, 100); // 重新装载,形成100ms周期
}
on message 0x456
{
gRespFlag = 1;
cancelTimer(tTimeout); // 收到响应,取消超时
}
on timer tTimeout
{
write("响应超时,测试失败");
}
四、消息收发:报文与信号操作
发送报文使用 output(),接收报文通过 on message 处理。CAPL中报文对象可直接访问 dlc、byte(i)、id 等属性。若使用数据库(DBC)定义,还可通过信号名直接读写,例如 msg.EngineSpeed = 1500;,CAPL会自动完成信号打包与字节序处理。信号级操作比手工拼字节更安全,推荐优先使用。
接收侧若需读取信号,可在 on message 中直接访问 this.EngineSpeed 或对应报文变量。注意信号更新与报文接收是同一时刻,若需做滤波或去抖,应在处理程序中自行实现计数逻辑。
五、测试用例结构:从脚本到可判定结果
自动化测试脚本的核心是“激励—等待—判定”。一个完整的测试用例通常包含:初始化条件、发送激励、等待响应或超时、比较实际值与期望值、输出判定结果。CAPL中可用 testWaitForMessage()、testWaitForSignal() 等测试函数简化等待逻辑,并用 testStepPass()/testStepFail() 输出结果到Test Report。
下面给出一个可直接套用的测试用例骨架,以“验证0x123请求后0x456在200ms内响应”为例:
testcase TC_ResponseCheck()
{
message 0x123 req;
message 0x456 resp;
int result;
// 1. 初始化
req.dlc = 8;
req.byte(0) = 0x01;
// 2. 发送激励
output(req);
// 3. 等待响应,超时200ms
result = testWaitForMessage(0x456, 200);
// 4. 判定
if (result == 1)
{
testStepPass("收到0x456响应");
}
else
{
testStepFail("200ms内未收到0x456响应");
}
}
该骨架中,testWaitForMessage 返回1表示在超时前收到报文,返回0表示超时。实际项目中还应校验响应报文的信号值是否在期望范围内,例如 resp.byte(1) == 0x02,只有报文和内容都正确才判定通过。
on start 中统一初始化全局变量和定时器,避免在多个测试用例中重复初始化导致状态污染。测试用例之间应通过系统变量或全局变量传递上下文,而不是依赖隐式顺序。六、事件驱动与测试用例的配合关系
事件驱动机制与测试用例结构并非对立,而是互补。测试用例负责“主动发起激励并判定”,事件处理程序负责“被动响应总线变化”。在复杂场景中,常用做法是:测试用例发送请求后,由 on message 处理程序记录响应状态到全局变量,测试用例再通过轮询或等待函数读取该变量完成判定。这样既保留了事件驱动的实时性,又让测试逻辑保持线性可读。
上图展示了CAPL脚本中事件源、共享状态与测试用例三者的关系:左侧事件源触发处理程序,中间通过全局变量、系统变量和定时器维护共享状态,右侧测试用例按“初始化—激励—等待—判定—输出”五步执行,并通过全局变量与事件处理程序交换数据,最终形成闭环。实际开发时,建议先画出这样的关系图,再逐块填充代码,可显著减少事件遗漏和状态竞争问题。
on message 中直接调用 testStepPass 等测试函数,可能导致测试报告步骤与用例结构错乱。正确做法是在事件处理程序中只更新状态变量,由测试用例统一判定和输出。