核心交互
学习目标
完成本篇后,你将能够:
- 理解 CAPL 程序的生命周期和各阶段的作用
- 掌握
on message事件的用法和this关键字 - 使用
output()函数发送 CAN 消息 - 理解消息选择器和符号化编程
- 编写一个完整的响应逻辑:当收到特定信号时自动发送响应消息
在第三篇中,我们学习了 CAPL 的语法基础。现在,是时候让你的 CAPL 程序真正"活"起来了。
想象一下:你刚刚写好了第一个 on key 'a' 事件程序,按下 'a' 键,Write 窗口出现了 "Hello CAPL!"。但接下来你可能会想:我的程序怎么和 CAN 总线对话?
这就是本篇要解决的问题。我们将学习 CAPL 与总线世界对话的"语言"——事件驱动机制。
CAPL 程序的生命周期
在 CAPL 的世界里,程序不是从 main() 函数开始,而是由事件驱动的。但即便如此,CAPL 程序也有自己的生命周期(Lifecycle)。
💡 类比:就像人从出生到死亡有不同阶段一样,CAPL 程序从启动到停止也有关键时刻。
四个关键生命周期事件
CAPL 程序的生命周期由四个核心事件控制:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
on preStart |
测量初始化阶段(启动前) | 早期初始化、记录开始(不能使用 output()) |
on start |
程序启动时刻 | 变量初始化、配置设置、启动定时器 |
on preStop |
停止测量被请求时 | 清理资源、保存状态、发送关闭消息 |
on stopMeasurement |
测量结束时 | 最终清理、生成报告 |
实践示例
让我们看一个典型的生命周期程序:
variables {
msTimer heartbeatTimer; // Timer variable declaration
}
on preStart
{
write("=== Program starting: prepare environment ===");
}
on start
{
write("=== Program started: initialize variables ===");
int engineSpeed = 0;
message 0x100 EngineDataMsg;
// Set periodic timer
setTimer(heartbeatTimer, 1000); // Send heartbeat every second
}
on preStop
{
write("=== Stop requested: cleanup resources ===");
cancelTimer(heartbeatTimer); // Cancel timer
}
on stopMeasurement
{
write("=== Program ended: generate final report ===");
}
📝 重要说明:
on start是最常用的生命周期事件,通常在这里初始化变量、设置定时器等。
事件驱动模型(上):接收与响应
CAPL 是事件驱动(Event-Driven)的语言。这意味着程序平时处于"休眠"状态,只有当特定事件发生时才会"醒来"执行代码。
最核心的事件是 on message——当有 CAN 消息被接收时触发。
on message 基础
on message *
{
// * 表示接收所有消息
write("收到消息:ID=0x%X", this.ID);
}
这段代码的意思是:无论收到什么消息,都打印其 ID。
使用 this 关键字访问消息内容
在 on message 事件中,this 关键字代表刚刚收到的这条消息。你可以通过 this 访问消息的所有属性:
on message *
{
// Access basic message properties
write("Message ID: 0x%X", this.ID); // Message identifier
write("Data length: %d", this.DLC); // Data length code
write("Channel: %d", this.CAN); // CAN channel number
// Access message data (byte by byte)
write("Data: %02X %02X %02X %02X",
this.byte(0), this.byte(1),
this.byte(2), this.byte(3));
// Use DIR selector to check if received or sent
// dir can be 'rx' (received) or 'tx' (transmitted)
if (this.dir == rx) {
write("This is a received message");
}
}
📝 重要说明:理解
this的含义是关键——它指向当前事件的"主体",即刚收到的消息对象。
消息选择器:精确过滤消息
如果你只想处理特定的消息,可以使用消息选择器(Message Selector):
方式 1:按 ID 过滤
// Process only messages with ID 0x100
on message 0x100
{
write("Received EngineData message");
}
方式 2:按名称过滤(需要 DBC 数据库)
// If EngineData message (ID=0x100) is defined in database
on message EngineData
{
// Directly access signals using signal names
write("Engine Speed: %d RPM", this.EngineSpeed);
write("Engine Temp: %d C", this.EngineTemp);
}
方式 3:按通道过滤
// Process only messages from CAN1 channel
on message CAN1.*
{
write("Message from CAN1 channel");
}
💡 实用技巧:从简单开始(使用
on message *观察所有消息),然后逐步细化过滤条件。
实战:消息接收监听器
让我们编写一个完整的消息监听器:
variables {
// Statistics counters
int totalMessages = 0;
int engineDataCount = 0;
}
on message *
{
totalMessages++;
write("[%d] Received message: 0x%X", totalMessages, this.ID);
}
on message 0x100 // EngineData message
{
engineDataCount++;
write(" -> This is EngineData message (#%d)", engineDataCount);
// Check data
if (this.byte(0) > 0x80) {
write(" -> Warning: Engine speed too high!");
}
}
运行这个程序,你会看到所有消息的流动,包括特定消息的详细分析。
[!SCREENSHOT]
位置:CAPL Browser > Write 窗口
内容:显示消息接收日志
标注:圈出 "收到消息" 和 "EngineData 消息" 的输出行
事件驱动模型(下):发送与控制
学会了"听"(接收),现在我们来学习"说"(发送)。
output() 函数:发送消息
output() 是 CAPL 中发送消息的核心函数:
void output(message msg);
重要提示:避免递归发送
// ❌ Wrong: do not use output(this) in on message
on message * {
output(this); // This causes infinite recursion!
}
// ✅ Correct: check message direction
on message 0x100 {
if (this.dir == rx) { // Only process received messages
// Process message without resending
}
}
📝 重要说明:在
on message *中直接使用output(this)会导致递归发送,因为每条发送的消息又会触发on message事件。
基础发送示例
on key 's' // Press 's' to send message
{
// Step 1: Declare message variable
message 0x200 HeartbeatMsg;
// Step 2: Set message data
HeartbeatMsg.byte(0) = 0xAA; // Heartbeat signature
HeartbeatMsg.byte(1) = 0x55; // Fixed data
// Step 3: Send message
output(HeartbeatMsg);
write("Heartbeat message sent");
}
按一下 's' 键,一条消息就被发送到 CAN 总线上。
📝 重要说明:
message是 CAPL 的特殊数据类型,用于表示 CAN 帧。
定时器事件:实现周期性发送
output() 只能发送一次消息。要实现周期性发送(比如每秒发送一次心跳),我们需要定时器(Timer)。
Timer 类型对比
| 特性 | timer |
msTimer |
|---|---|---|
| 时间单位 | 秒 | 毫秒 |
| 最大时间 | 596.52 小时 | 596.52 小时 |
| 常用场景 | 长时间定时 | 快速响应任务 |
| 语法 | timer myTimer; |
msTimer myTimer; |
CAPL 支持两种定时器:
timer- 基于秒msTimer- 基于毫秒(更常用)
周期性发送完整示例
variables {
msTimer heartbeatTimer; // Declare millisecond timer
message 0x200 HeartbeatMsg;
int heartbeatCount = 0;
}
on start
{
write("Starting periodic heartbeat transmission...");
setTimer(heartbeatTimer, 1000); // Trigger first time after 1 second
}
on timer heartbeatTimer
{
heartbeatCount++;
// Prepare heartbeat message
HeartbeatMsg.byte(0) = 0xAA; // Heartbeat signature
HeartbeatMsg.byte(1) = heartbeatCount & 0xFF; // Counter
// Send message
output(HeartbeatMsg);
write("Heartbeat #%d sent", heartbeatCount);
// Re-arm timer to create loop
setTimer(heartbeatTimer, 1000);
}
这段代码的效果:
- 程序启动后 1 秒,触发第一次
on timer - 发送心跳消息
- 重新设置定时器,再过 1 秒再次触发
- 如此循环,形成每秒一次的心跳
💡 类比:定时器就像闹钟——响了之后,你需要重新设置它,它才会再次响起。
取消定时器
on key 'x' // Press 'x' key to stop heartbeat
{
cancelTimer(heartbeatTimer);
write("Heartbeat stopped");
}
组合使用:响应式发送
最实用的模式是:接收消息 → 处理逻辑 → 发送响应。
variables {
message 0x100 EngineData; // Received message
message 0x200 WarningLight; // Response message
}
on message 0x100 // Receive EngineData
{
// Read engine speed value (assume it's in the first two bytes)
int engineSpeed = this.byte(0) * 256 + this.byte(1);
write("Received engine speed: %d RPM", engineSpeed);
// Logic check: engine speed too high
if (engineSpeed > 6000) {
write("Engine speed too high! Sending warning light signal");
// Prepare warning message
WarningLight.byte(0) = 0x01; // Turn on warning light
// Send response message
output(WarningLight);
}
}
📝 重要说明:这是一个典型的响应式系统——根据输入(消息)产生输出(消息),是 CAPL 仿真节点的核心逻辑。
与数据库(DBC)集成——从"数字"到"语义"
到目前为止,我们都在使用数字(如 0x100)和字节(如 this.byte(0))来操作消息。虽然这是底层的做法,但不够直观。
DBC 数据库让我们可以用语义化的名称来编程。
DBC 数据库术语对照
| 术语 | 英文 | 说明 |
|---|---|---|
| DBC | Database CAN | CAN 数据库文件格式 |
| 消息 | Message | CAN 帧,包含 ID 和数据 |
| 信号 | Signal | 消息中的特定数据字段 |
| 编码 | Encoding | 信号的数据表示方式(原始值/物理值) |
为什么需要 DBC?
假设没有数据库:
on message 0x100
{
// What do these numbers mean?
int rpm = this.byte(0) * 256 + this.byte(1);
int temp = this.byte(2);
}
有了数据库后:
on message EngineData
{
// Directly use meaningful names
write("Engine Speed: %d", this.EngineSpeed);
write("Engine Temp: %d", this.EngineTemp);
}
哪种更清晰?显然是第二种。
在 CANoe 中创建简单 DBC
[!SCREENSHOT]
位置:CANoe > Database Manager
内容:显示创建新数据库的界面
标注:圈出 "Create New Database" 按钮
我们创建一个最简单的 DBC:
-
在 Database Manager 中创建新数据库
-
添加一条消息:
- 名称:
EngineData - ID:
0x100 - DLC:
8字节
- 名称:
-
添加两个信号:
EngineSpeed:长度 16 位,位置 0-15EngineTemp:长度 8 位,位置 16-23
📝 重要说明:DBC 文件定义了消息和信号的结构(名称、ID、长度、位置等),让 CAPL 可以进行符号化编程。
符号化编程实践
有了 DBC 后,CAPL 代码变得直观易懂:
on message EngineData // Use message name directly
{
// Use signal names directly, no need to parse bytes manually
int rpm = this.EngineSpeed;
int temp = this.EngineTemp;
write("Engine speed: %d RPM", rpm);
write("Engine temp: %d C", temp);
// Logic check
if (temp > 110) {
write("Engine overheating!");
// Send warning message (also use symbolic name)
WarningLight.LampStatus = 1;
output(WarningLight);
}
}
getMessageID() 函数:动态获取消息 ID
有时你需要根据消息名动态获取 ID:
on start
{
dword engineDataId;
// Get message ID from database
engineDataId = getMessageID("EngineData");
if (engineDataId != -1) {
write("EngineData message ID is: 0x%X", engineDataId);
} else {
write("Error: EngineData message not found");
}
}
提示:在多数据库环境中,需要指定数据库名:
getMessageID("DatabaseName.EngineData")
这在需要动态处理消息时很有用。
📝 重要说明:符号化编程是 CAPL 进阶技能——它让代码更易读、更易维护。建议始终使用 DBC 数据库进行开发。
综合案例:智能转速监控节点
现在我们来整合所有知识点,编写一个完整的智能监控节点。
📝 编程范式说明:本案例使用原始编程(无需DBC数据库),使用直接的CAN ID和字节访问。这是理解CAPL底层工作原理的最好方式。
需求定义
我们要编写一个 ECU 仿真节点,它的功能是:
- 接收来自发动机的
EngineData消息(ID=0x100,包含转速和温度) - 当转速超过 6000 RPM 时,自动发送
WarningLight消息(ID=0x200)点亮警告灯 - 当温度超过 110°C 时,发送
WarningLight消息并记录日志 - 每秒发送一次心跳消息
第一步:定义变量
variables {
// Message variables (using raw CAN IDs)
message 0x100 engineMsg; // Received message
message 0x200 warningMsg; // Sent message
message 0x200 heartbeatMsg; // Heartbeat message (reuses same ID)
// Timer
msTimer heartbeatTimer;
// Statistics variables
int totalEngineMsgs = 0;
int warningTriggered = 0;
int heartbeatCount = 0;
// Constants for thresholds
const int RPM_THRESHOLD = 6000;
const int TEMP_THRESHOLD = 110;
const int TEMP_OFFSET = 40; // Temperature offset: raw + 40 = actual degC
}
第二步:初始化
on start
{
write("=== Smart Engine Monitor Node Started (Raw Mode) ===");
write("Using raw CAN IDs - no DBC database required");
// Initialize heartbeat message
heartbeatMsg.dlc = 1;
heartbeatMsg.byte(0) = 0xAA; // Heartbeat signature
// Start periodic heartbeat
setTimer(heartbeatTimer, 1000);
write("Initialization complete");
}
第三步:接收消息并处理
on message 0x100 // EngineData message
{
// Local variables for extracted values
int rawRpm;
int actualRpm;
int rawTemp;
int actualTemp;
totalEngineMsgs++;
// Extract RPM value (bytes 0-1, 16-bit, scale 0.25)
rawRpm = this.word(0);
actualRpm = rawRpm / 4;
// Extract Temperature value (byte 2, offset -40)
rawTemp = this.byte(2);
actualTemp = rawTemp - TEMP_OFFSET;
write("[%d] Received EngineData (0x100): RPM=%d (raw=%d), Temp=%d degC (raw=%d)",
totalEngineMsgs, actualRpm, rawRpm, actualTemp, rawTemp);
// Logic check 1: Engine speed too high
if (actualRpm > RPM_THRESHOLD) {
warningTriggered++;
write("*** WARNING #%d: Engine speed too high (%d RPM)! ***",
warningTriggered, actualRpm);
// Send warning light message
warningMsg.dlc = 1;
warningMsg.byte(0) = 1; // LampStatus = ON
output(warningMsg);
write("WarningLight (0x200) sent: LampStatus=1 (ON)");
}
// Logic check 2: Engine temperature too high
if (actualTemp > TEMP_THRESHOLD) {
warningTriggered++;
write("*** WARNING #%d: Engine temperature too high (%d degC)! ***",
warningTriggered, actualTemp);
// Send warning light message
warningMsg.dlc = 1;
warningMsg.byte(0) = 1; // LampStatus = ON
output(warningMsg);
write("WarningLight (0x200) sent: LampStatus=1 (ON)");
}
// Normal state
if (actualRpm <= RPM_THRESHOLD && actualTemp <= TEMP_THRESHOLD) {
write("Status: Engine operating normally");
}
}
第四步:周期性心跳
on timer heartbeatTimer
{
heartbeatCount++;
// Update heartbeat data
heartbeatMsg.dlc = 1;
heartbeatMsg.byte(0) = heartbeatCount & 0xFF;
// Send heartbeat message
output(heartbeatMsg);
write("Heartbeat #%d sent on 0x200 (data: 0x%02X)",
heartbeatCount, heartbeatCount & 0xFF);
// Re-arm timer
setTimer(heartbeatTimer, 1000);
}
第五步:清理资源
📝 重要说明:所有CAPL代码中的注释应使用英文以避免字符编码问题。
on preStop
{
write("=== Stopping monitor node ===");
cancelTimer(heartbeatTimer);
write("Total engine messages processed: %d", totalEngineMsgs);
write("Total warnings triggered: %d", warningTriggered);
}
on stopMeasurement
{
write("=== Monitor node stopped ===");
}
完整代码一览
variables {
message 0x100 engineMsg;
message 0x200 warningMsg;
message 0x200 heartbeatMsg;
msTimer heartbeatTimer;
int totalEngineMsgs = 0;
int warningTriggered = 0;
int heartbeatCount = 0;
const int RPM_THRESHOLD = 6000;
const int TEMP_THRESHOLD = 110;
const int TEMP_OFFSET = 40;
}
on start
{
write("=== Smart Engine Monitor Node Started (Raw Mode) ===");
heartbeatMsg.dlc = 1;
heartbeatMsg.byte(0) = 0xAA;
setTimer(heartbeatTimer, 1000);
write("Initialization complete");
}
on message 0x100
{
int rawRpm = this.word(0);
int actualRpm = rawRpm / 4;
int rawTemp = this.byte(2);
int actualTemp = rawTemp - TEMP_OFFSET;
totalEngineMsgs++;
write("[%d] Received EngineData: RPM=%d, Temp=%d degC",
totalEngineMsgs, actualRpm, actualTemp);
if (actualRpm > RPM_THRESHOLD) {
warningTriggered++;
write("Warning #%d: Engine speed too high (%d RPM)!", warningTriggered, actualRpm);
warningMsg.dlc = 1;
warningMsg.byte(0) = 1;
output(warningMsg);
}
if (actualTemp > TEMP_THRESHOLD) {
warningTriggered++;
write("Warning #%d: Engine temperature too high (%d C)!", warningTriggered, actualTemp);
warningMsg.dlc = 1;
warningMsg.byte(0) = 1;
output(warningMsg);
}
if (actualRpm <= RPM_THRESHOLD && actualTemp <= TEMP_THRESHOLD) {
write("Status: Engine normal");
}
}
on timer heartbeatTimer
{
heartbeatCount++;
heartbeatMsg.dlc = 1;
heartbeatMsg.byte(0) = heartbeatCount & 0xFF;
output(heartbeatMsg);
write("Heartbeat #%d", heartbeatCount);
setTimer(heartbeatTimer, 1000);
}
on preStop
{
write("=== Stopping monitor node ===");
cancelTimer(heartbeatTimer);
write("Total engine messages processed: %d", totalEngineMsgs);
write("Total warnings triggered: %d", warningTriggered);
}
on stopMeasurement
{
write("=== Monitor node stopped ===");
}
这个程序整合了我们学到的所有核心概念:
- 生命周期事件:初始化和清理
- 消息接收:
on message和this关键字 - 消息发送:
output()函数 - 周期性任务:定时器和
on timer - 符号化编程:使用 DBC 中的消息和信号名
- 业务逻辑:条件判断和状态监控
📝 重要说明:本示例使用符号化编程(基于DBC数据库),这是实际项目中的推荐做法。使用符号化编程前,需要先在CANoe中创建包含
EngineData和WarningLight消息的DBC数据库。如果你的环境没有DBC数据库,可以参考原始编程版本(无需数据库):
- 消息声明:
message 0x100 engineMsg(替代message EngineData)- 数据访问:
this.word(0) / 4(替代this.EngineSpeed)- 警告设置:
warningMsg.byte(0) = 1(替代warningMsg.LampStatus = 1)两种编程方式功能完全一致,只是风格不同。符号化编程更易读,原始编程更独立。
💡 实战建议:尝试修改这个程序,比如调整转速/温度阈值,添加新的警告条件,或者增加更多监控功能。
[!SCREENSHOT]
位置:CANoe > Measurement Setup > Write 窗口
内容:显示智能监控节点的运行日志
标注:圈出 "收到发动机数据"、"警告" 和 "心跳" 的输出行
本篇总结
在 CAPL 的世界里,事件驱动是核心机制。我们学习了:
- 生命周期管理:
on start用于初始化,on preStop用于清理 - 消息接收:
on message事件和this关键字访问消息内容 - 消息选择器:使用
*、ID、名称或通道过滤消息 - 消息发送:
output()函数是 CAPL 与总线对话的"嘴巴" - 定时器:
setTimer()+on timer实现周期性任务 - 符号化编程:DBC 数据库让代码从"数字"变成"语义"
这些是 CAPL 编程的基石——掌握了它们,你就能构建复杂的 ECU 仿真节点。
课后练习
-
基础练习:编写一个程序,接收所有消息并打印其 ID、数据长度和前 4 个字节的数据。
-
定时器练习:创建一个
msTimer,实现每 500 毫秒发送一次递增计数器的功能。 -
逻辑练习:修改本篇的综合案例,添加一个新的判断:当转速低于 800 RPM 时,认为是"怠速状态",发送一条
IdleStatus消息。 -
进阶练习(有 DBC 环境):创建一个包含
DoorStatus消息的 DBC(包含DoorOpen信号),编写 CAPL 程序:当收到DoorOpen=1时,延迟 2 秒后自动发送Alarm消息。
📝 预告:下一篇文章我们将学习如何调试 CAPL 程序。调试是每个程序员的必备技能——我们将从最简单的
write()开始,逐步学习断点、观察窗口等强大工具。