CAPL (Communication Access Programming Language) 是一种专门用于嵌入式系统和汽车电子测试领域的编程语言,特别是在 CAN (Controller Area Network) 总线和汽车网络通信系统中被广泛使用。它由 Vector 公司开发,主要用于编写与汽车控制单元 (ECU) 进行通信的测试脚本。
一、初识 CAPL 脚本语言
1.CAPL 的基本特点
- 简易语法:CAPL 的语法较为简洁,类似于 C 语言,因此对于有编程背景的人来说,学习起来相对容易。
- 事件驱动:CAPL 脚本通常是事件驱动的,即它会响应特定的事件(如接收到 CAN 消息、定时器超时等)来触发相关的代码执行,没有 main 函数。
- 强大的功能库:CAPL 提供了一些用于网络通信的内建函数,能够方便地进行消息的发送、接收、信号的监控、定时器管理等。
2.CAPL 脚本的基本结构
CAPL 脚本一般包括以下几个主要部分:
- 事件 (Event):CAPL 脚本最常见的元素是事件,事件可以是定时器、信号接收、CAN 消息的接收等。事件的执行通常是由某个触发条件来启动。
- 函数 (Function):CAPL 允许你创建函数,以便在多个地方复用代码。函数的参数可以是消息对象、信号值或任何你需要的变量。
- 变量 (Variable):CAPL 变量可以用于存储数据,变量的类型可以是整型、浮动类型、字符串、结构体等。
- 定时器 (Timer):CAPL 提供了定时器功能,可以设置延时后执行某些操作。
3.CAPL 脚本的应用场景
- CAN 总线仿真:通过 CAPL 脚本模拟不同的 CAN 总线节点,并验证通信协议是否正确。
- 自动化测试:对车辆电子控制单元 (ECU) 进行功能测试,模拟不同的车载设备和网络环境。
- 故障模拟:模拟 CAN 总线中的异常或故障情景,检测系统如何应对。
- 实时数据监控与分析:通过 CAPL 脚本实时分析 CAN 总线传输的数据包,并做出响应。
4.CAPL 脚本示例(浏览即可)
// 定义一个接收消息的事件
on message CanMessage
{// 如果接收到的消息ID是 0x100if (this.id == 0x100){// 打印接收到的消息内容output("Received message with ID 0x100");// 发送一个回复消息output("Sending response...");message CanResponse;CanResponse.id = 0x200;CanResponse.data[0] = 0x01;CanResponse.data[1] = 0x02;output("Response sent.");output(CanResponse);}
}// 设置一个定时器事件,每5秒触发一次
variables
{msTimer t1;
}on start
{setTimer(t1, 5000); // 设置定时器,5秒后触发
}on timer t1
{output("Timer expired!");setTimer(t1, 5000); // 重新设置定时器,每5秒触发一次
}
通过示例可以大致看看 capl 脚本构成是什么样子,可以去对应一下第二节中所说的基本结构,接下来开始正式学习 capl 脚本
二、CAPL 脚本语言基本数据类型
1.新建文件
在学习语言之前,先来看看怎么创建该文件。打开 CANoe 软件新建一个项目,在Simulation Setup 组件中:
上图 1 最右侧的 CAN1 是 CAN 总线,按照图 2 的方式,在 CAN 总线上右击点击 Insert Network Node,添加一个 ECU 节点,这个节点便是我们的仿真节点,用来做模拟真实 ECU 在 CAN 总线上的报文信息发送与接收。点击新建的 ECU 节点上的✏️图标,选择一个文件地址,然后在文件名栏写入新建的和该 ECU 绑定的 capl 脚本文件名称,最后点击确定,进入 CAPL 脚本文件编辑界面。
新建文件后原始界面会出现以下默认模板:
/*@!Encoding:936*/
//这一行是一个注释,指定了文件的编码方式。
//在这个案例中,文件的编码是 936,这是简体中文字符集的编码方式(GBK)。includes
{//用于引入其他必要的头文件或库文件。//可以通过在 {} 中列出需要的文件路径,来包含其他CAPL脚本或外部库文件。//这些文件可以提供函数、变量和其他CAPL文件中的定义。
}variables
{//用于声明和定义变量。可以在这个块中定义脚本中需要使用的所有变量,//比如整数、字符串、数组等。
}
2.基本数据类型
1.整数类型:int,long,short,unsigned
2.浮点类型:float,double
3.字符类型:char
4.字符串类型:string,char[ ]( CAPL不直接定义 string
类型,通常通过 char[]
或 string
类型处理字符数组来实现。 )
5.布尔类型:bool
6.枚举类型:enum
7.时间类型:time
8.指针类型:int*等
下面是一个简单的示例,通过触发键盘上的键然后执行一些操作(建议实操一下)
/*@!Encoding:936*/
includes
{}variables
{int a=20;int b=0366;float c=1.23;float d=23E10;char e ='e';char f[25] = "good job!";
}on key 'c'
{write("%d",a);write("%d",b);write("%f",c);write("%f",d);write("%c",e);write("%s",f);
}
3.结构体类型
在 CAPL 中,结构体通常与 message
关键字配合使用,用来定义消息格式。结构体字段可以是基本数据类型(如 int
, float
, char[]
)。可以通过 message
类型来表示结构体字段,然后直接用于发送和接收 CAN 消息。CAPL 中的结构体定义通常是在 message
中完成,便于与 CAN 总线通信相关的数据结构配合使用。
下面是一个简单的示例,通过触发键盘上的 a 键然后执行一些操作(建议实操)
/*@!Encoding:936*/
includes
{}variables
{}on key 'a'
{struct Person{int age;long hight;char name[50];};struct Person p1 = {18,180,"张三"};struct Person p2 = {20,182,"李四"};write("姓名%s,年龄:%d,身高:%d",p1.name,p1.age,p1.hight);write("姓名%s,年龄:%d,身高:%d",p2.name,p2.age,p2.hight);
}
4.message 类型(重点)
在 CAPL 中,message
类型是一个非常重要的概念,用于定义 CAN 总线上的消息格式。CAPL 是为汽车电子系统中的 CAN 总线通信设计的,它通过定义 message
类型来描述消息的结构、字段和信号。CAPL 中的 message
类型不仅定义了数据字段的类型,还通常用于接收和发送消息。
报文选择器
关键字 | 描述 | 数据类型 | 访问权限 |
CAN | CAN 报文传输的逻辑通道,有效值范围:1-32 | word | —— |
MsgChannel | CAN 报文传输的逻辑通道,有效值范围:1-32 | word | —— |
ID | CAN 报文标识符 | dword | —— |
Name | 报文在 dbc 文件中的名称 | char[ ] | readonly |
DIR | 报文传输方向 | byte | —— |
RTR | 远程帧标志位 | byte | —— |
DLC | 报文的数据长度,有效值范围:0-15 | byte | —— |
Byte(x) | 数据字节位有效值范围:0-63(8-63 字节仅适用于 CAN-FD) | byte | —— |
选中 output 然后按 F1,就会出现官方帮助文档,会有不同协议的 output 函数解释
5.定时器类型
在 CAPL 中,定时器是用于实现时间延迟或周期性任务的机制。
CAPL 中的定时器主要有以下几种类型:
1️⃣setTimer
定时器
setTimer
用于设置一个定时器,它会在指定的时间后触发一次。定时器会一直运行,直到触发或被取消。
setTimer(TimerName, TimeInterval);//TimerName: 定时器的名称,可以是任何合法的标识符。
//TimeInterval: 触发时间间隔,单位为毫秒(ms)。例如,1000 表示 1 秒。
2️⃣cancelTimer
cancelTimer
用于取消已设置的定时器。它会停止定时器的计时,不再触发定时器相关的事件。
cancelTimer(TimerName);//TimerName: 要取消的定时器名称。
如何使用定时器实现超时处理示例:
variables
{message MyMessage 0x01; // 假设这是要发送的消息int counter = 0; // 用于计数
}on start
{setTimer(MyTimer, 1000); // 启动定时器,每1秒触发一次
}on timer MyTimer
{counter++; // 增加计数output("Timer triggered, count: %d", counter);if (counter >= 5){cancelTimer(MyTimer); // 如果计数达到5次,则取消定时器output("Timer stopped after 5 triggers.");}else{setTimer(MyTimer, 1000); // 每隔1秒重新启动定时器}
}
6.CAPL 和 C语言的区别
CAPL 语言和 C语言在设计之处有很多相似的地方,但是为了更好的熟悉 CAPL,这里着重说明一下两者比较明显的区别
1️⃣函数定义
CAPL:CAPL 中的函数可以在 functions 部分定义,并且通常没有 return 类型,除非需要返回值,通常在函数中简化了对事件和定时器的处理
C语言:C语言中的函数通常需要明确的返回类型,包含返回值/参数等
2️⃣事件处理
CAPL: CAPL 脚本中处理事件的方式与 C 语言的传统方式不同,CAPL 是事件驱动的,可以定义对消息、定时器等的响应,语法非常简洁。
C语言: CAPL 脚本中处理事件的方式与 C 语言的传统方式不同,CAPL 是事件驱动的,可以定义对消息、定时器等的响应,语法非常简洁。
3️⃣定时器
CAPL:CAPL 中的定时器非常简便,可以直接通过变量定义并使用。
timers
{TimerName = 1000; // 1000ms
}
C语言: C 语言没有直接的定时器类型,通常使用 sleep
、usleep
或操作系统提供的定时器功能来模拟定时器。
4️⃣消息发送和接收
CAPL: CAPL 提供了内置的函数来简化 CAN 总线消息的发送和接收。使用 output
来发送消息,on message
来接收消息。
on message CanMessage
{output("Message received!");
}// 发送消息
message CanMessage msg;
msg.Signal = 100;
output(msg);
C语言: C 语言没有专门的函数用于处理 CAN 消息,通常需要使用外部库来实现。发送和接收的实现会依赖于 CAN 总线的驱动和库。
// 伪代码:发送CAN消息
sendMessage(CanMessage);// 伪代码:接收CAN消息
receiveMessage(&CanMessage);
5️⃣类型安全
CAPL: CAPL 是一种较为简化的语言,它的类型系统较为宽松,允许某些类型的隐式转换。例如,CAPL中某些变量类型可以直接赋值,而不需要强制类型转换。
int a = 5;
float b = 2.5;
a = b; // 这种隐式转换是允许的
C语言: C 语言中对于类型转换要求严格,需要显式的类型转换。
6️⃣结构体与数组
CAPL: CAPL 中的结构体和数组语法较为简化,特别是在事件和消息处理时可以直接使用消息的信号作为结构体成员。
message MyMessage
{int speed;float temperature;
}MyMessage msg;
msg.speed = 100;
msg.temperature = 36.6;
C语言: C 语言提供强大的结构体和数组支持,需要手动定义并管理。
struct MyMessage {int speed;float temperature;
};struct MyMessage msg;
msg.speed = 100;
msg.temperature = 36.6;
7️⃣内存管理
CAPL: CAPL 是为特定应用(如CAN总线通信)设计的语言,并不需要开发者手动管理内存。内存管理通常由工具环境处理。
C语言: C 语言提供了对内存的直接控制,使用 malloc
、free
进行内存分配和释放。开发者需要显式地管理内存。
三、发送报文
这一节具体来解析怎么发送一帧报文
/*@!Encoding:936*/
includes
{}variables
{message 0x720 msg_tx; // 定义接收消息,ID 为 0x720
}on key 'b'
{msg_tx.dlc = 3; // 设置数据长度码 (DLC) 为 3msg_tx.ID = 0x720; // 设置消息 ID 为 0x720msg_tx.byte(0) = 0x02; // 设置消息的第一个字节为 0x02msg_tx.byte(1) = 0x10; // 设置消息的第二个字节为 0x10msg_tx.byte(2) = 0x01; // 设置消息的第三个字节为 0x01// 发出报文write("Sending message with ID 0x666, Data: 0x11 0x22");output(msg_tx);
}
具体代码如上,呈现的结果如下图(Rx 需要在工程代码中实现)
代码量不多,过程也很简单,首先先定义一个 message 类型的变量,名为 msg_tx,接着在按键 b 事件中进行结果内容实现:
msg_tx.dlc = 3;
:DLC
(数据长度码)表示消息的数据部分的字节数。dlc = 3
表示该消息的数据部分有 3 个字节。
msg_tx.ID = 0x720;
:设置消息的 ID 为 0x720
,这个 ID 在 CAN 总线上用于标识消息。每个消息在 CAN 总线上都有一个唯一的 ID。
msg_tx.byte(0) = 0x02;
:设置消息的第一个字节为 0x02
。
msg_tx.byte(1) = 0x10;
:设置消息的第二个字节为 0x10
。
msg_tx.byte(2) = 0x01;
:设置消息的第三个字节为 0x01
。
output(msg_tx);
:output
是 CAPL 中用于发送消息的函数,将 msg_tx
消息发送到 CAN 总线,并且在 Trace 窗口中显示发送的消息。
最后用 write 函数将结果打印在终端。
也可以周期性发送:
variables
{message 0x720 msg_tx; // 定义接收消息,ID 为 0x720msTimer myTimer; // 定义定时器,用于周期性触发
}on start
{setTimer(myTimer, 10); // 启动定时器,10 毫秒后第一次触发
}on timer myTimer
{// 设置消息内容msg_tx.dlc = 3; // 数据长度为 3msg_tx.ID = 0x720; // 设置消息 ID 为 0x720msg_tx.byte(0) = 0x02; // 设置第一个字节为 0x02msg_tx.byte(1) = 0x10; // 设置第二个字节为 0x10msg_tx.byte(2) = 0x01; // 设置第三个字节为 0x01// 发送消息output(msg_tx); // 发送消息 msg_tx// 重新设置定时器,确保每 10 毫秒发送一次setTimer(myTimer, 10); // 10 毫秒后再次触发定时器
}