CAPL 有 *.can
和 *.cin
两种文件格式;一般.can 文件中写testcase
及MainTest()
,而.cin文件中写供.can调用的接口方法和变量等,其结构类似于 C 语言中的.c
和.h
文件
一个大致的结构如下:
// Test.cin includes{} variables{int x =6; // 全局变量,若要使用该变量,引入Test.cin 文件即可 }void printVar(int val) {write("Print variable val: %d", val); // 作用类似于 C语言的 printf 函数 }// TC.can includes{#include "Test.cin" } variables{} testcase Test(){printVar(x); } void MainTest( ){Test(); }
在打开 CAPL Browser 编辑界面时,默认的组成部分有:Include、Variable、System、Value Objects,其中 Include 为需要包含的已存在的头文件,一般不配置;Variable为申明与定义全局变量,需要定义的变量包括需要发送的信号以及其数据类型。
0. CAPL中的数据类型有:
byte
(1字节),word
(2字节),dword
(4字节)
有符号整型:int
(2字节),long
(4字节)
浮点数:float
(8字节),double
(16字节)
CAN消息类型:Message
;
定时器类型:timer
(单位为s),msTimer
(单位为ms);
单个字符:char
(1字节)。
除了界面基础的信息外,在 CAPL 脚本中,我们大量使用官方定的的一些接口,这些接口通常需要查看 Help 文档或者是 CAPL 的手册,下面是梳理的一些常用接口。
1、定时器
CAPL中的定时器的使用相当频繁,比如测试时需要向定时发送某条CAN报文时就需要用定时器;定时器的声明:
msTimer myTimer1;//声明了一个ms定时器,定时单位是毫秒
timer myTimer2;//声明了一个以秒为单位的定时器;
设置定时器:
setTimer(myTimer1,400);//设置定时器myTimer1为一个400ms定时器;
setTimerCyclic(myTimer2,2);//设置定时器myTimer2为一个2s为周期的循环定时器;
设置定时器定时事件,即当定时器计时时间到时将要执行的操作:
on timer myTimer1{
.......
}
2、信息的操作和发送
//CAN消息发送:
message 0x7ff Msg;//声明一个message,ID=0x7ff
Msg.dlc=8;//设置其DLC=8;
Msg.id=0x100;//更改其ID=0x100;
Msg.byte(0)=55;//设置数据场的第一个字节为55
output(Msg);//发送Msg
//CANFD消息发送:
msg1.FDF=1;
msg1.BRS=1;
msg1.dlc=8;
Msg.id=0x100;//更改其ID=0x100;
msg1.byte(0)=0x44;
msg1.byte(10)=0x10;
msg1.byte(11)=0x11;
output(Msg);//发送Msg
3、节点上下线操作
节点是在dbc
中定义的,如VCU
,BMS
,MCU
等,有时需要将它们离线,离线后不再向总线上发送报文,在线时可以向总线上发送报文。
节点上线:
void testSetEcuOnline(dbNode aNode);
void testSetEcuOnline(char aNodeName[]);
节点下线:
void testSetEcuOffline(dbNode aNode);
void testSetEcuOffline(char aNodeName[]);
4、检查错误帧
进行CAN通讯的测试时,检查错误帧是很常见的,要用CAPL脚本实现自动检测错误帧也不困难,它的核心就是调用错误检查函数ChkStart_ErrorFrameOccured()
,该函数一旦被调用,CANoe 就会从此函数被调用时开始持续检测总线上有没有出现错误帧。
下面是一个小的例子:
dword chechId;
dword numCheckEvents;
checkId=ChkStart_ErrorFrameOccured();//开始检测错误帧
TestAddCondition(checkId);//添加检测条件,如果出现了错误帧,则输出报告中会记录下来
TestWaitForTimeout(5000);//持续检测5s
checkControl_Stop(checkId);//停止检测错误帧
numCheckEvents=ChkQuery_NumEvents(checkId);//对5s内的检测情况进行获取,若函数返回0则没有出现错误帧
if(numCheckEvents>0)
TestStepFail("Error Frames Occured");
5、添加事件信号
这种事件信号相当于信号量机制,一般使用在需要等待某个或者是多个条件满足时进行下一步操作。
具体做法是:在一个位置添加需要等待的事件,程序中的其他地方,如果某个事件发生了(如周期超界等),提供该事件的供应,则等待的程序段获得了该事件,继续执行下面的操作。主要使用的函数有以下几个:
//供应text事件
long TestSupplyTextEvent( char aText[] );
//添加text事件
long TestJoinTextEvent(char[]aText);
//等待text事件,有一个出现则程序执行下一步
long TestWaitForAnyJoinedEvent(dword aTimeout);
//等待text事件,所有等待事件都出现则程序执行下一步
long TestWaitForAllJoinedEvents(dword aTimeout);
以下是一个例子:
TestJoinTextEvent("Test finished");
TestJoinTextEvent("Error Frame Occured");
TestWaitForAnyJoinedEvents(20000);
或者:
TestWaitForAllJoinedEvents(20000);
在系统事件on errorFrame中:
on errorFrame
{
TestSupplyTextEvent("Error Frame occured");
}
在系统的on message 中:
on message 0x400
{
TestSupplyTextEvent("Test Finished")
}
6、回调函数
CAPL 中也有类似于C
语言中的回调函数的机制,如检测报文周期和错误帧的函数中就可以使用,当周期超界或者总线出现错误帧就会自动调用回调函数执行一些操作;如:
ErrChkId=ChkStart_ErrorFramesOccured(Callback_ErrorFrameOccured");//检查错误帧,如果发现错误帧就调用回调函数
回调函数设计如下:
void Callback_errorFrameOccured(dword chk_id)
{
float t;
t=timeNow()/100000.0;//记录出现错误帧的时间
testStep("ErrorFrameTimeStamp","%.6f s",t);//打印该事件戳
TestSupplyTextEvent("ErrorFrameOccured");//供应Text事件
}
7、监视总线的情况,这一般会用在查看一段时间内,总线上有没有出现通讯异常的情况。需要使用函数ChkStart_NodeBabbling( )
. 如,检测一段时间内总线有没有出现停止通讯的情况:
CheckId=ChkStart_NodeBabbling(CAN::PT_MCU,0);//立即开始检查总线状态
testWaitForTimeout(2000);//延时2s
ChkControl_Stop(CheckId);//停止检测
QueryNumberEvents=ChkQuery_NumEvents(CheckId);//如果在2s内总线停止通讯,则QueryNumberEvents!=0
8、关于获取关键时间点
(1)CANoe中获取定时器当前计时值的函数为:timerToElapse();
该函数原型如下:
long timerToElapse(timer);
long timerToElpase(msTimer);
(2)获取等待某个事件的时间,需要使用函数TestGetLastWaitElapsedTimeNS()
,其原型如下:
float TestGetLastWaitElapsedTimeNS();
(3)获取当前的仿真时间点:
float timeNowFloat();
(4)等待指定报文:
long TestWaitForMessage(dbMessage aMessage,dword aTimeout);
long TestWaitForMessage(dword aMessageId,dword aTimeout);
若在aTimeout
时间内等到了指定ID
的报文,函数返回1,否则返回0;
(5)获取报文的数据,等到了报文之后,如果想知道报文的具体内容可以使用函数:
message msg;
long result;
result=TestGetWaitEventMsgData(msg);
.....处理msg.....
9、多总线测试
设置总线背景,一般都总线测试都会有两路及以上的 CAN,这时若要通过 CAPL 脚本获取某个 CAN 通道上的报文时,就需要先设置好总线背景,即将总线设置为值监听某一路的 CAN 通道。下面是一个例子:
void BusContextConfiguration(char yBus[])
{
yBusContext=GetBusNameContext(yBus);//这里的yBusContext为全局变量
SetBusContext(yBusContext);
}
//使用:
BusContextConfiguration("CAN1");//将总线监听设为CAN1
此时等待某一路的 CAN 报文可是这样实现:
res=testWaitForMessage(CAN1::NM_IPU,600);//等待CAN1上的名称为NM_IPU的报文,等待事件为600ms
10、诊断报文的发送和接收
request_A.SendRequest(); //诊断请求
TestWaitForDiagResponse(request_A, 5000); //诊断接收
11、将诊断请求 / 响应写入报告
TestReportWriteDiagObject (diagRequest req);
TestReportWriteDiagObject (diagResponse resp);
TestReportWriteDiagResponse (diagRequest req);
12、获取诊断请求 / 响应的原始数据
long diagGetPrimitiveByte( diagRequest request, DWORD bytePos);
long diagGetPrimitiveByte( diagResponse response, DWORD bytePos);
13、获取诊断请求 / 响应的参数
long diagGetParameter (diagResponse obj, char parameterName[], double output[1])
long diagGetParameter (diagRequest obj, char parameterName[], double output [1])
double diagGetParameter (diagResponse obj, char parameterName[])
double DiagGetParameter (diagRequest obj, char parameterName[])
long diagGetParameter (diagResponse obj, long mode, char parameterName[], double output[1])
long DiagGetParameter (diagRequest obj, long mode, char parameterName[], double output [1])
double diagGetParameter (diagResponse obj, long mode, char parameterName[])
double diagGetParameter (diagRequest obj, long mode, char parameterName[])
14. 最后分享最近刚使用 CAPL 脚本的一些注意点以及一个示例:
第一 CAPL 的局部变量是静态局部变量。经过使用发现,在variables{ }
之外,事件或者函数内部定义的局部变量是静态局部变量,其值不会因为退出本事件或者函数,而变为初始值。所以如果真的需要一个局部变量,在每次退出之前,重新使用赋值语句赋为初始值。
第二建议使用系统变量或者环境变量,这样可以跨不同的capl脚本操作,比如检测某个环境变量或者系统变量的变化,来执行一些动作。
on sysvar sysvar::Engine::EngineStateSwitch
{
$EngineState::OnOff = @this;
if(@this)
$EngineState::EngineSpeed = @sysvar::Engine::EngineSpeedEntry;
else
$EngineState::EngineSpeed = 0;
}
第三个就是以太网转 CAN 的 Capl 脚本示例:
/*@!Encoding:1252*/
variables{ // // Constants // const WORD kPort = 23; // UDP port number for instance const WORD kRxBufferSize = 1500; const WORD kTxBufferSize = 1500; // // Structure of UDP payload // _align(1) struct CANData{ BYTE dlc;BYTE flags; // Bit 7 - Frame type (0 = standard, 1 = extended) // Bit 6 - RTR bit ('1' = RTR bit is set) DWORD canId; BYTE canData[8]; }; // // Global variables // UdpSocket gSocket; CHAR gRxBuffer[kRxBufferSize]; CHAR gTxBuffer[kTxBufferSize]; DWORD gOwnAddress; DWORD gModuleAddress= 0xFFFFFFFF; // default is the broadcast address 255.255.255.255 and the TCP/IP stack will build the Network broadcast address
} Measurement start handler//
on start{DWORD addresses[1]; // get own IP address of the Windows TCP/IP stack IpGetAdapterAddress( 1, addresses, elcount(addresses) ); gOwnAddress = addresses[0]; // open UDP socket gSocket = UdpSocket::Open( 0, kPort ); if (gSocket.GetLastSocketError() != 0){write( "<%BASE_FILE_NAME%> Open UDP socket failed, result %d. Measurement stopped!", gSocket.GetLastSocketError());stop();return; }if (gSocket.ReceiveFrom( gRxBuffer, elcount(gRxBuffer) ) != 0){ if (gSocket.GetLastSocketError() != 997){ // ignore pending IO operation write( "<%BASE_FILE_NAME%> UDPReceive failed, result %d. Measurement stopped!", gSocket.GetLastSocketError() ); stop(); return; } }
} On receive UDP data handler using CAPL Callback //
void OnUdpReceiveFrom( dword socket, long result, dword address, dword port, char buffer[], dword size){DWORD dataOffset;struct CANData canData;message * anMsg; if (address == gOwnAddress) return; // ignore own broadcasts // // Store IP address of module to reach // if (gModuleAddress == 0){gModuleAddress = address;} // // Handle received data // dataOffset = 0;while (dataOffset + __size_of(struct CANData) <= size){ memcpy( canData, buffer, dataOffset ); canMsg.id = (canData.canId & 0x1FFFFFFF) | ((canData.flags & 0x80) ? 0x80000000 : 0); canMsg.dlc = canData.dlc & 0x0f; canMsg.rtr = ((canData.flags & 0x40) ? 1 : 0); canMsg.byte(0) = canData.canData[0]; canMsg.byte(1) = canData.canData[1]; canMsg.byte(2) = canData.canData[2]; canMsg.byte(3) = canData.canData[3]; canMsg.byte(4) = canData.canData[4]; canMsg.byte(5) = canData.canData[5]; canMsg.byte(6) = canData.canData[6]; canMsg.byte(7) = canData.canData[7]; output( canMsg ); dataOffset += __size_of(struct CANData); }
// // Receive more data // if (gSocket.ReceiveFrom( gRxBuffer, elcount(gRxBuffer) ) != 0){ if (gSocket.GetLastSocketError() != 997){ // ignore pending IO operation write( "<%BASE_FILE_NAME%> UDPReceive failed, result %d. Measurement stopped!", gSocket.GetLastSocketError() ); stop(); return; } }
} Handler for CAN messages//
on message *{ int i; struct CANData canData; if ((this.dir == RX) && (gModuleAddress != 0)){ canData.canId = this.id & 0x1FFFFFFF; canData.flags = ((this.id & 0x80000000) ? 0x80 : 0x00) | ((this.rtr == 1) ? 0x40 : 0x00); canData.dlc = this.dlc; for( i = 0; i < 8; i++ ){ canData.canData[i] = (i < this.dlc) ? this.byte(i) : 0; } memcpy( gTxBuffer, canData ); gSocket.SendTo( gModuleAddress, kPort, gTxBuffer, __size_of(struct CANData) ); } else if (gModuleAddress == 0){ write( "<%BASE_FILE_NAME%> Tx not possible. Module to reach must send packets first." ); //Server simulation }
}
反向应用,以下是一个简单的CAN转以太网(CAN to Ethernet)的CAPL(Communication Access Programming Language)脚本示例。这个脚本将监听CAN总线上的消息,并将其转发到指定的以太网目标地址和端口。
variables
{int port = 1234; // 以太网端口号char ip[] = "192.168.0.100"; // 目标IP地址
}on start
{// 初始化以太网连接setSocketParameter("IP", ip);setSocketParameter("Port", port);openSocket();
}on canMessage CAN1
{// 检查是否成功接收到CAN消息if (this.canId == 0x100) // 假设我们只转发ID为0x100的CAN消息{// 构造以太网消息char ethMsg[8];for (int i = 0; i < 8; i++){ethMsg[i] = this.byte(i);}// 发送以太网消息writeSocket(ethMsg, 8);}
}on key 'q' // 按下'q'键时关闭以太网连接
{closeSocket();write("Socket closed.");
}
在这个脚本中,我们定义了一个以太网的目标 IP 地址和端口号。脚本启动时,会初始化以太网连接。当接收到 CAN 消息时,如果消息的 ID 是 0x100,则会将消息内容转发到以太网。按下 ‘q’ 键时,会关闭以太网连接。
请根据实际需求调整IP地址、端口号和CAN消息ID。此外,确保你的网络环境允许CAPL脚本与目标IP地址进行通信。