CAPL Script

如何开发一个仿真节点

先限定模型,再编写事件

本例用一个门锁和车窗模型说明接收事件、状态更新、定时器与状态报文之间的关系。协议和验收步骤是教学设计,不是量产车门协议,也不是防夹手或功能安全实现。示例尚未在 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 参考;状态合同、边界反例和验收矩阵是本站分析。参见资料来源与验证边界。