如何开发一个仿真节点
先限定模型,再编写事件
本例用一个门锁和车窗模型说明接收事件、状态更新、定时器与状态报文之间的关系。协议和验收步骤是教学设计,不是量产车门协议,也不是防夹手或功能安全实现。示例尚未在 CANoe 编译或执行;应在隔离的仿真配置中对照安装版本帮助验证。
准备一个 Simulation Setup 仿真节点,将 CAN1 映射到预期通道,只使用标准标识符、经典 CAN 数据帧。测试时关闭其他同 ID 发送源,并排除 CAN FD 和扩展帧。此代码不需要 DBC;如果已有数据库,先检查本例的 ID 是否与已有通信冲突。不要将它当作测量分析分支的透明转发器。
定义可检查的报文与状态合同
| CAN1 标准 ID | 方向与内容 | 本例的规则 |
|---|---|---|
| 0x200 | 接收上锁命令;载荷忽略 | 静止时上锁,运动时拒绝 |
| 0x201 | 接收解锁命令;载荷忽略 | 静止时解锁,运动时拒绝 |
| 0x202 | 接收车窗命令;DLC 必须等于 1 | byte 0 为 1:下降;为 2:上升;其他值拒绝 |
| 0x300 | 每次状态定时器到期请求发送;DLC 为 3 | byte 0:门锁;byte 1:位置;byte 2:运动状态 |
门锁状态 0 表示解锁、1 表示上锁。位置 0 表示完全升起、100 表示完全降下。运动状态 0 表示静止、1 表示上升、2 表示下降。门锁和车窗之间的限制只是本例约定:运动时拒绝锁定和解锁,锁定时拒绝车窗命令;它们不能用作真实车辆的安全要求。
测量开始时复位为“解锁、位置 0、静止”。一次有效车窗命令启动运动;运动中到来的新命令被拒绝,不排队、不反向。已经处于目标端点的命令是无操作,不启动定时器。
一个可单独检查的源程序
以下是一份完整的教学源程序。不要再叠加同名的第二个事件处理程序。上锁与解锁事件无需声明接收消息变量;在车窗事件内,实际收到的载荷必须从 this 读取。
variables
{
byte doorLockState;
long windowPosition;
byte windowMovement;
msTimer windowTimer;
msTimer statusTimer;
message CAN1.0x300 doorStatus;
}
on start
{
cancelTimer(windowTimer);
cancelTimer(statusTimer);
doorLockState = 0;
windowPosition = 0;
windowMovement = 0;
doorStatus.RTR = 0;
doorStatus.DLC = 3;
setTimer(statusTimer, 500);
write("DoorModule initialized: unlocked, position 0");
}
on message CAN1.0x200
{
if (this.DIR != Rx || this.RTR != 0) return;
if (windowMovement != 0)
{
write("Lock rejected: window moving");
return;
}
doorLockState = 1;
write("Lock accepted");
}
on message CAN1.0x201
{
if (this.DIR != Rx || this.RTR != 0) return;
if (windowMovement != 0)
{
write("Unlock rejected: window moving");
return;
}
doorLockState = 0;
write("Unlock accepted");
}
on message CAN1.0x202
{
byte command;
if (this.DIR != Rx || this.RTR != 0) return;
if (this.DLC != 1)
{
write("Window rejected: expected exactly one byte");
return;
}
command = this.byte(0);
if (command != 1 && command != 2)
{
write("Window rejected: invalid command %d", command);
return;
}
if (doorLockState != 0 || windowMovement != 0)
{
write("Window rejected: locked or already moving");
return;
}
if ((command == 1 && windowPosition == 100) ||
(command == 2 && windowPosition == 0))
{
write("Window unchanged: already at requested endpoint");
return;
}
if (command == 1) windowMovement = 2;
else windowMovement = 1;
setTimer(windowTimer, 20);
write("Window accepted: movement state %d", windowMovement);
}
on timer windowTimer
{
if (windowMovement == 1)
{
if (windowPosition <= 5) windowPosition = 0;
else windowPosition = windowPosition - 5;
}
else if (windowMovement == 2)
{
if (windowPosition >= 95) windowPosition = 100;
else windowPosition = windowPosition + 5;
}
else return;
if (windowPosition == 0 || windowPosition == 100)
{
windowMovement = 0;
write("Window stopped at %d", windowPosition);
}
else setTimer(windowTimer, 20);
}
on timer statusTimer
{
doorStatus.byte(0) = doorLockState;
doorStatus.byte(1) = windowPosition;
doorStatus.byte(2) = windowMovement;
output(doorStatus);
setTimer(statusTimer, 500);
}
四个容易造成错误结论的地方
1. 消息对象不等于刚收到的帧
声明一个名为 WindowCmd 的消息变量,不代表它会在接收事件中自动变成当前帧。使用 on message 的 this 读取事件数据,并在 byte(0) 之前验证 DLC。空载荷已经到达,但没有有效命令字节;应记录为拒绝,不应写成“没有收到报文”。
2. 先保护边界,再做位置运算
对无符号 byte 先执行 0 - 5,再检查是否小于等于 0,可能在赋值时得到回绕值。旧版这种写法在已到端点后重复发命令时尤其危险。本例使用 long 保存内部位置,在递减前判断是否不大于 5,在递增前判断是否不小于 95;写入一个状态字节前,模型的位置始终保持在 0–100。
3. 运动完成时间不是 2 秒
从 0 到 100,每次增加 5,需要 20 次定时器事件;每次相对重设 20 ms,名义持续时间为 20 × 20 = 400 ms。这是模型参数的推导,不是实测保证。调度延迟和相对重设会影响实际时刻,应保存事件时间戳,不要把 Write 日志在屏幕上的出现时间当作总线时间。
状态报文的重设间隔是 500 ms,比完整运动的名义时长还长。若下降从初始化后很早开始,第一条状态报文就可能显示“位置 100、静止”,看不到运动中的状态。这不等于模型没有运动;需要在调试器观察内部状态,或者在单独实验中缩短状态间隔并明确记录配置变化。
4. 请求发送不等于接收方已经确认
output 没有成功返回码。本例显式声明 CAN1、ID、RTR 和 DLC,并在每次请求发送前写入三字节。仍需在 Trace 中核对实际记录,再验证接收方;不能只凭 Write 的“accepted”认定外部 ECU 已执行。该日志只描述这个仿真节点的内部决定。
在隔离配置中执行验收
先编译这一个 CAPL 节点,解决编译器指出的版本或语法问题,再开始测量。使用独立发送节点或 Interactive Generator 注入经典 CAN 数据帧;界面名称及位置以安装版本为准。上锁和解锁测试可以使用 DLC 0;车窗测试使用 DLC 1。确保命令到达本节点时是接收事件。Trace 同时保留命令和 0x300 状态。
以下是预期检查项,不是已经取得的测试结果。除明确要求连续执行的步骤外,每项从重新开始测量的初始状态进行。
| 输入或操作 | 内部预期 | 应核对的证据 |
|---|---|---|
| 不发命令 | 解锁、位置 0、静止 | 状态载荷应为 00 00 00;核对 CAN1、标准 ID、DLC 3 |
| 初始位置 0,发送 0x202 / 02 | 保持位置 0、静止 | 端点无操作日志;不能出现 251 等回绕值 |
| 发送 0x202 / 01 | 每次运动事件增加 5,到 100 后静止 | 内部事件顺序与时间;最终状态 byte 1 为十进制 100,即 0x64 |
| 到 100 后再次发送 01 | 保持位置 100、静止 | 不启动一次新的运动 |
| 到 100 后发送 02 | 每次减少 5,到 0 后静止 | 单调递减,无负值和回绕 |
| 运动中发反向命令或锁定命令 | 拒绝,保留当前运动 | 拒绝日志与状态;不要只等待下一条周期报文 |
| 静止时发送 0x200,再发 0x202 / 01 | 锁定,车窗命令被拒绝 | 先看到锁状态,再验证车窗位置不变;发送 0x201 后才可恢复控制 |
| 0x202 使用 DLC 0 或 2 | 拒绝 | 不读取不存在的首字节,不更改状态 |
| 0x202 的 byte 0 为 00 或 03 | 拒绝 | 无运动定时器启动 |
| 相同数值 ID 发到 CAN2,或发送 RTR | 不处理为有效命令 | 核对选定通道及帧种类,避免把筛选当作漏收 |
| 停止测量后重新启动 | 回到初始化状态 | 不承接上一次的中间位置或运动状态 |
记录 CANoe 版本、通道映射、发送源、报文行、内部状态以及所有拒绝原因。若观察不符,先根据单一报文流筛选方法确认输入确实到达所观察节点,再检查模型状态,不要立即加入 output(this) 转发所有消息。
后续练习:可复现的故障,比随机故障更适合第一轮测试
若扩展阻力停止模型,应先定义输入、触发位置、停止后的状态与重新启动条件。例如在固定的第 7 次运动事件注入一次阻力,只检查一次可复现路径。禁止新命令并不能检测夹物;不要把当前模型称作防夹手实现。
旧示例中的 random() < 0.1 不符合所据 CAPL 参考。该 API 的签名是 dword random(dword x),返回 0 到 x-1 的整数;即使用 random(10) == 0,也不能承诺每 10 次恰好一次,更不利于复现。完成确定性验收后,才考虑保存随机输入序列作为单独的测试工况。
若要分析 0x300 的周期,先按时间戳观察方法导出同一报文流,再使用间隔分析工具。该统计只能说明所选状态流的时间间隔,不能证明车窗运动或安全功能正确。
函数语义对照本站获授权的 Vector 参考;状态合同、边界反例和验收矩阵是本站分析。参见资料来源与验证边界。