Add synchronous and streaming MotorService APIs backed by the PLC Modbus TCP runtime and protocol driver. Extend AUBO JSON commands and isolate vendor libstdc++ paths while keeping build-tree tests runnable.
1023 lines
46 KiB
Markdown
1023 lines
46 KiB
Markdown
# MotorService 与 Modbus TCP PLC 电机控制
|
||
|
||
本文说明当前 `MotorService`、CMVR PLC v1 寄存器协议和 Siemens
|
||
S7-1215C DC/DC/DC 侧的对接方法。本文只描述当前源码已经存在的接口和
|
||
协议;寄存器表中标为“预留”或“当前未发送”的内容,不代表上层已经开放。
|
||
|
||
## 1. 当前范围
|
||
|
||
当前实现是一条刻意保持简单的单轴控制链:
|
||
|
||
```text
|
||
gRPC MotorService
|
||
|
|
||
v
|
||
MotorManager
|
||
|
|
||
v
|
||
AbstractMotor
|
||
|
|
||
v
|
||
CmvrPlcMotorProtocol
|
||
|
|
||
v
|
||
ModbusTcpMotorBusRuntime
|
||
|
|
||
v
|
||
libmodbus -> PLC MB_SERVER -> 驱动器/电机
|
||
```
|
||
|
||
各层职责如下:
|
||
|
||
- `MotorService`:解析 `MotorManager` ID 和电机选择器,执行单电机互斥、
|
||
同步等待、gRPC 取消检测、流式 latest-wins 和软件急停抢占。
|
||
- `MotorManager`:按 `motor_id` 或 `joint_name` 返回 `AbstractMotor`。
|
||
- `AbstractMotor`:提供统一的置零、Profile、Cyclic、使能和 Quick Stop
|
||
接口。
|
||
- `CmvrPlcMotorProtocol`:执行关节限位检查,把 SI 单位转换为定点整数,
|
||
并映射成 CMVR PLC v1 命令。
|
||
- `ModbusTcpMotorBusRuntime`:管理一个 PLC 连接、握手、心跳、重连、每轴
|
||
命令序列、payload/commit 写入和 ACK 轮询。
|
||
- PLC:必须原子接收命令、执行驱动器控制、发布状态,并独立执行通信和
|
||
周期指令 watchdog。
|
||
|
||
当前服务按“一个 RPC 或一个流独占一个电机”仲裁,不是多轴同步控制器。
|
||
同一个 `MotorManager` 中不同电机可以分别被不同调用者控制。需要多轴同一扫描
|
||
周期锁存时,应在后续版本增加 manager/runtime 级批量 commit,不应依赖客户端
|
||
逐轴调用。
|
||
|
||
runtime 的 `start()` 启动连接 supervisor,而不是要求 PLC 当时已经在线。首次
|
||
同步握手失败时 MotorManager 仍完成注册,runtime 保持
|
||
`connected=false`,并从 `reconnect_min_ms` 起按上限退避持续重试。离线期间
|
||
状态 RPC 返回 `UNAVAILABLE`,运动命令在写 mailbox 前安全失败;PLC 后续上线
|
||
并完成完整身份/session 握手后无需重启 `cmvr_es`。
|
||
|
||
## 2. 实际 gRPC 接口
|
||
|
||
服务定义位于:
|
||
|
||
- `protos/cmvr/api/motor_service.proto`
|
||
- `protos/cmvr/api/motor_command.proto`
|
||
|
||
`MotorTarget.header.device_id` 是 `MotorManager` 的设备 ID,不是单个电机
|
||
的 ID。`MotorTarget.selector` 必须且只能设置一个:
|
||
|
||
```text
|
||
motor_id uint32,当前服务要求不大于 255
|
||
joint_name 非空字符串
|
||
```
|
||
|
||
### 2.1 Unary RPC
|
||
|
||
| RPC | 当前语义 | 是否阻塞 |
|
||
| --- | --- | --- |
|
||
| `setZero` | 调用 `calibrateZeroQ()`。PLC 后端发送 `SetZero`,等待 PLC 进入成功终态后返回 | 是,PLC 后端安全命令上限当前为 5 s |
|
||
| `moveToZero` | 复用 Profile Position,把目标位置设为 `0 rad`;不是 `setZero`,也不发送寄存器图中的 `MoveToZero` opcode | 是,直到到位、取消或超时 |
|
||
| `profilePosition` | 设置 Profile Position 的目标位置、最大速度和加速度 | 是,直到到位、取消或超时 |
|
||
| `profileVelocity` | 设置 Profile Velocity 的目标速度和加速度 | 是,直到实际速度稳定进入容差 |
|
||
| `emergencyStop` | 抢占当前阻塞命令/流,设置服务内软件急停锁存并调用 `quickStop()` | 是,等待 PLC Quick Stop 成功终态 |
|
||
| `getStatus` | 读取模式、位置、速度、到位标志和 MotorService 仲裁状态 | 同步快照读取 |
|
||
| `setEnabled` | `enabled=true` 调用 `torqueOn()`,成功后解除服务内软件急停锁存;`false` 调用 `torqueOff()` | 是,等待 PLC 成功终态 |
|
||
|
||
`moveToZero`、`profilePosition` 和 `profileVelocity` 使用
|
||
`MotorWaitOptions`:
|
||
|
||
| 字段 | `0` 时默认值 | 服务端范围/语义 |
|
||
| --- | ---: | --- |
|
||
| `timeout_ms` | 30000 ms | 1 ms 至 600000 ms |
|
||
| `poll_period_ms` | 10 ms | 1 ms 至 1000 ms |
|
||
| `position_tolerance_rad` | 0.001 rad | 仅正值覆盖默认值 |
|
||
| `velocity_tolerance_rad_s` | 0.01 rad/s | 仅正值覆盖默认值 |
|
||
| `settle_sample_count` | 3 | 1 至 1000 个连续样本 |
|
||
|
||
`setZero` 只有在 PLC 返回 `Completed + ZeroValid` 且 `zero_epoch` 相比命令前
|
||
变化时才成功。如果 RPC 返回失败、取消、断链或超时,而 commit 可能已经写出,
|
||
session/zero epoch 的最终结果是不确定的;禁止直接盲重试,应先通过 PLC/HMI
|
||
或维护诊断确认当前 session、`zero_epoch` 和实际零位状态。
|
||
|
||
Profile Position 只有在 PLC/驱动器报告 `target_reached`,同时实际位置和
|
||
速度连续满足容差时才返回成功。Profile Velocity 在实际速度连续满足目标速度
|
||
容差时返回成功;RPC 返回后速度命令仍然有效。正常停车应再发送目标速度为
|
||
`0 rad/s` 的 `profileVelocity`,不能把 RPC 返回理解为“速度控制已经结束”。
|
||
|
||
Profile Position/Velocity 的 PLC payload 使用 600000 ms
|
||
`command_timeout_ms` 上限,与 MotorService 允许的最长等待一致。RPC 使用更短
|
||
超时或被取消时,服务端仍会主动发送 Quick Stop;PLC 通信 watchdog 仍是上位机
|
||
失联时的权威保护。该 600000 ms 是 PLC 执行上限,不会把 RPC 默认等待从
|
||
30000 ms 延长。
|
||
|
||
成功 ACK 的 Profile Position/Velocity 会绑定到提交时的
|
||
`connection_epoch`。只要 PLC 断线重连使 epoch 改变,该 Profile 的位置、速度
|
||
和到位反馈就全部失效:`getQ/getQd` 返回 NaN,`reachedTargetQ` 返回 false。
|
||
即使新 session 的安全停止速度恰好也是目标 `0 rad/s`,也不能把数值相等误认为
|
||
旧命令成功。成功的新 Profile 会重新绑定当前 epoch;成功的首个 Cyclic
|
||
sample、Quick Stop 或 Disable 会清除该绑定。Enable/SetZero 本身不恢复旧
|
||
Profile 反馈的可信性。
|
||
|
||
当前 `getStatus` 会依次调用模式、位置、速度和到位读取。对于 Modbus TCP 后端,
|
||
这些读取不是一次原子寄存器快照,字段可能来自相邻的 PLC 扫描周期。
|
||
它只暴露 `AbstractMotor` 的通用状态和 MotorService 仲裁状态,不包含
|
||
`plc_boot_id`、session、`fault_code`、`zero_epoch` 或 PLC result code。
|
||
结果不确定或需要故障恢复时,应从 PLC/HMI 或维护诊断层读取这些原始字段,
|
||
不能只依赖本 RPC。
|
||
|
||
### 2.2 双向流 RPC
|
||
|
||
当前有两个双向流:
|
||
|
||
```proto
|
||
rpc streamCyclicPosition(stream CyclicPositionRequest)
|
||
returns (stream CyclicControlResponse);
|
||
|
||
rpc streamCyclicVelocity(stream CyclicVelocityRequest)
|
||
returns (stream CyclicControlResponse);
|
||
```
|
||
|
||
首帧必须是 `open`:
|
||
|
||
```text
|
||
open.target 选择一个 MotorManager 中的单个电机
|
||
open.watchdog_timeout_ms gRPC 层输入 watchdog
|
||
```
|
||
|
||
watchdog 为 `0` 时默认 500 ms,非零值被限制到 20 ms 至 60000 ms。打开
|
||
成功后,服务端先返回:
|
||
|
||
```text
|
||
phase = CYCLIC_STREAM_OPENED
|
||
sequence = 0
|
||
status = 当前电机状态
|
||
```
|
||
|
||
`CYCLIC_STREAM_OPENED` 只表示 MotorService 已解析目标、取得该电机的独占
|
||
控制权并选择了 Cyclic 模式。CMVR PLC 后端采用惰性打开:收到第一个合法
|
||
setpoint 时才依次发送 `OpenCyclic*` 和对应的 `Cyclic*Sample`。因此客户端
|
||
必须等到该 setpoint 的 `CYCLIC_STREAM_APPLIED`,才能确认 PLC 已 ACK 打开
|
||
命令并锁存了首个样本。
|
||
|
||
每次 `OpenCyclicPosition/Velocity` 都创建新的轴级 stream epoch。PLC 必须在
|
||
Open 的同一个原子状态事务中清零 `last_applied_cyclic_sequence`、旧样本去重
|
||
状态和 cyclic watchdog,再设置正确的 CSP/CSV mode 与 `StreamActive=1`。
|
||
runtime 只有确认这些 Open 后置条件才接受 ACK,Protocol 随后才从样本序列 `1`
|
||
重新开始。Quick Stop、Disable 或断链关闭旧 epoch;重开后的首个 `1` 必须
|
||
重新应用,不能被旧流的样本 `1` 去重。
|
||
|
||
每次 gRPC cyclic 流首帧触发的 `setMode` 都建立一个新的上层
|
||
`cyclic_generation`,即使模式值与上一条流相同。该 generation 绑定当时的
|
||
`connection_epoch`。活动流遇到重连后会永久锁存失败:同一流的当前和后续
|
||
setpoint 都返回失败,不发送新的 `OpenCyclic*` 或 `Cyclic*Sample`;服务端
|
||
终止并 Quick Stop 该流。客户端必须创建新的 gRPC 流,由新首帧显式建立新的
|
||
generation 后才允许重新 Open。禁止把断链前或断链期间 pending 的 latest
|
||
setpoint 自动应用到新 PLC session。
|
||
|
||
后续每帧只能是 `setpoint`:
|
||
|
||
- 周期位置:严格递增且非零的 `sequence`、`target_position_rad`,以及可选
|
||
`target_velocity_rad_s`。
|
||
- 周期速度:严格递增且非零的 `sequence`、`target_velocity_rad_s`。
|
||
|
||
序列号允许跳号,但不能为 `0`、重复或倒退。服务端 reader 只保留一个尚未
|
||
处理的最新帧;新帧覆盖旧帧时,`dropped_setpoints` 累计增加。这是低延迟
|
||
latest-wins 语义,不适合必须逐点无损执行的离线轨迹。
|
||
|
||
每次 `AbstractMotor` 接受 setpoint 后返回:
|
||
|
||
```text
|
||
phase = CYCLIC_STREAM_APPLIED
|
||
sequence = 已应用的客户端序列
|
||
dropped_setpoints = 本流累计覆盖数
|
||
```
|
||
|
||
对于 CMVR PLC 后端,`AbstractMotor` 返回成功前,runtime 已经看到相同
|
||
command sequence 的 PLC ACK;周期样本还要求
|
||
`last_applied_cyclic_sequence` 严格等于本次样本序列。对于其他电机后端,
|
||
`APPLIED` 只保证对应后端的同步调用返回 `true`。
|
||
为避免每个周期样本额外触发多次现场总线状态读取,`APPLIED` 响应刻意不携带
|
||
`status`。`OPENED` 和终止响应仍携带完整状态;需要连续遥测的客户端应以独立、
|
||
较低频率调用 `getStatus`,不能把 `APPLIED` 当作状态采样接口。
|
||
|
||
客户端正常结束请求流后,服务端先同步调用 `quickStop()`;确认后返回
|
||
`CYCLIC_STREAM_STOPPED` 并释放电机所有权。当前协议没有显式 `close`
|
||
请求帧;客户端 half-close 就是结束信号。若 Quick Stop 未被后端确认,
|
||
服务端改发 `CYCLIC_STREAM_FAILED` 并以 `INTERNAL` 结束 RPC,不会把失败
|
||
误报成 `STOPPED`。
|
||
|
||
出现以下情况时,服务端会 Quick Stop:
|
||
|
||
- gRPC context 被取消或 deadline 到期;
|
||
- 输入在 watchdog 窗口内没有新帧;
|
||
- 序列号或 payload 非法;
|
||
- PLC/后端拒绝 setpoint;
|
||
- 客户端停止读取响应;
|
||
- `emergencyStop` 增加抢占 generation。
|
||
|
||
服务端流使用同步 `Write()`。慢客户端可能阻塞反馈写入,reader 此时仍会
|
||
覆盖 pending setpoint,但上层 watchdog 检查也可能被延迟。因此:
|
||
|
||
1. 客户端必须并发、持续读取反馈;
|
||
2. gRPC watchdog 只是第一层保护;
|
||
3. PLC 侧通信 watchdog 和周期样本 watchdog 才是断网、进程卡死时的
|
||
权威保护。
|
||
|
||
### 2.3 当前没有开放的能力
|
||
|
||
当前 `MotorService` 没有以下 RPC:
|
||
|
||
- Profile Torque;
|
||
- Cyclic Torque 流;
|
||
- Clear Fault;
|
||
- 普通 Stop/Halt;
|
||
- 多轴原子控制。
|
||
|
||
寄存器块预留了 `target_torque`,但 `CmvrPlcMotorProtocol` 当前明确拒绝
|
||
周期力矩。不要因为寄存器存在就让 PLC 项目把该功能标记为已经可用。
|
||
|
||
### 2.4 仲裁和 gRPC 状态
|
||
|
||
同一电机已有阻塞命令或流时,新控制调用返回
|
||
`RESOURCE_EXHAUSTED`。软件急停锁存期间,运动命令返回
|
||
`FAILED_PRECONDITION`;只有成功执行 `setEnabled(enabled=true)` 才清除
|
||
该服务内锁存。
|
||
|
||
任何取消、超时、异常或周期流结束后的 Quick Stop 如果未被后端确认成功,
|
||
MotorService 也会按 fail-closed 原则进入同一软件急停锁存。此时 RPC 的
|
||
`INTERNAL` 表示“安全状态尚未确认”,不能继续发送运动命令;应先排查 PLC/
|
||
驱动器状态,再通过成功的 `setEnabled(enabled=true)` 显式恢复。
|
||
显式 `emergencyStop` 会在调用后端前先锁存;若最终 Quick Stop 未确认,它
|
||
返回 `FAILED_PRECONDITION`,但仍保持锁存。两种返回码都不能解释为已经安全
|
||
停机。
|
||
|
||
常见 gRPC 状态包括:
|
||
|
||
- `INVALID_ARGUMENT`:选择器、数值、首帧或序列号非法;
|
||
- `NOT_FOUND`:MotorManager 或电机不存在;
|
||
- `RESOURCE_EXHAUSTED`:同一电机已有 owner;
|
||
- `FAILED_PRECONDITION`:软件急停锁存或后端拒绝命令;
|
||
- `DEADLINE_EXCEEDED`:同步等待或流 watchdog 超时;
|
||
- `ABORTED`:被 `emergencyStop` 抢占;
|
||
- `CANCELLED`:客户端取消或停止读取。
|
||
- `UNAVAILABLE`:后端断链,或状态因故障/watchdog 返回非 finite;
|
||
- `INTERNAL`:服务异常,或命令失败后的安全清理/Quick Stop 也失败。
|
||
|
||
## 3. MotorManager 配置
|
||
|
||
仓库已提供单 PLC、两轴配置示例
|
||
`cmvr-es/config/devices/motor/plc_motors.pb.txt`,内容如下:
|
||
|
||
```textproto
|
||
motor {
|
||
id: "plc_motors"
|
||
|
||
motor_groups {
|
||
id: "plc_axis_group"
|
||
bus_type: MOTOR_BUS_MODBUS_TCP
|
||
vendor: MOTOR_VENDOR_PLC_GENERIC
|
||
protocol: MOTOR_PROTOCOL_CMVR_PLC_V1
|
||
|
||
modbus_tcp {
|
||
host: "192.168.0.10"
|
||
port: 502
|
||
unit_id: 1
|
||
|
||
connect_timeout_ms: 500
|
||
io_timeout_ms: 100
|
||
heartbeat_period_ms: 100
|
||
communication_watchdog_ms: 1000
|
||
cyclic_watchdog_ms: 500
|
||
status_poll_period_ms: 20
|
||
reconnect_min_ms: 100
|
||
reconnect_max_ms: 2000
|
||
command_ack_timeout_ms: 500
|
||
|
||
protocol_major: 1
|
||
protocol_minor: 0
|
||
|
||
axes { motor_id: 1 axis_index: 0 }
|
||
axes { motor_id: 2 axis_index: 1 }
|
||
}
|
||
|
||
joint_limits {
|
||
enable: true
|
||
source: JOINT_LIMIT_SOURCE_CUSTOM
|
||
joints {
|
||
joint_name: "PLC_AXIS_1"
|
||
q_lb: -3.141592653589793
|
||
q_ub: 3.141592653589793
|
||
qd: 1.0
|
||
qdd: 2.0
|
||
}
|
||
joints {
|
||
joint_name: "PLC_AXIS_2"
|
||
q_lb: -1.5707963267948966
|
||
q_ub: 1.5707963267948966
|
||
qd: 0.5
|
||
qdd: 1.0
|
||
}
|
||
}
|
||
|
||
motors {
|
||
motors { id: 1 joint_name: "PLC_AXIS_1" }
|
||
motors { id: 2 joint_name: "PLC_AXIS_2" }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
字段行为:
|
||
|
||
- `host` 必须是 IPv4 字面量(例如 `192.168.0.10`),不做 DNS 解析;这样
|
||
TCP 建连和 `stop()` 不会被无界的名称解析阻塞。
|
||
- `port=0` 时 runtime 使用 502;`unit_id=0` 时使用 1。
|
||
- `connect_timeout_ms=0` 时使用 500 ms;该值为 TCP 建连的独立硬超时。
|
||
- `io_timeout_ms=0` 时使用 100 ms,并同时用于 libmodbus response timeout
|
||
和 byte timeout。
|
||
- 心跳、通信 watchdog、周期 watchdog、状态轮询、重连和 ACK timeout
|
||
为 `0` 时,当前 runtime 分别使用
|
||
100、500、500、20、100、2000、500 ms;上面的实体样例将通信 watchdog
|
||
显式放宽为 1000 ms。
|
||
- `communication_watchdog_ms` 必须同时不少于
|
||
`2 * heartbeat_period_ms` 和
|
||
`3 * io_timeout_ms + 2 * heartbeat_period_ms`;
|
||
`command_ack_timeout_ms`、`cyclic_watchdog_ms` 必须不少于
|
||
`3 * io_timeout_ms + status_poll_period_ms`,因为一次可靠状态读取包含
|
||
sequence-before、完整状态块、sequence-after 三次 FC3;
|
||
`cyclic_watchdog_ms` 必须在 20 ms 至 60000 ms 内;
|
||
`reconnect_max_ms` 不得小于 `reconnect_min_ms`,否则初始化失败。
|
||
- `protocol_major=0` 时使用当前 major `1`。握手严格检查 major;
|
||
PLC 的 minor 版本不得低于配置要求。
|
||
- `axis_index` 是 PLC 轴块索引,必须小于 PLC 发布的 `axis_count`,且同一
|
||
group 内不能重复。
|
||
- `encoder_counts_per_rev` 和 `gear_ratio` 对 CMVR PLC v1 不生效,因为 PLC
|
||
协议交换的是 SI 定点值,而不是编码器 count。
|
||
|
||
还需要在 DeviceManager 配置中注册该 MotorSystem:
|
||
|
||
```textproto
|
||
device_manager {
|
||
# 只有未被 RobotArm 选择且仍希望初始化全部电机时才需要 true。
|
||
init_all_motors_when_no_active_joints: true
|
||
|
||
devices {
|
||
id: "plc_motors"
|
||
type: DEVICE_TYPE_MOTOR_SYSTEM
|
||
config_file: "devices/motor/plc_motors.pb.txt"
|
||
enable: true
|
||
}
|
||
}
|
||
```
|
||
|
||
`id` 必须与 `motor.id` 以及 gRPC
|
||
`MotorTarget.header.device_id` 一致。安装产物读取
|
||
`output/bin/config/`;修改源码树配置后需再次执行 `cmake --install build`。
|
||
|
||
## 4. CMVR PLC v1 Holding Register
|
||
|
||
### 4.1 地址和 32 位编码
|
||
|
||
本文和 C++ runtime 中的地址都是 **零基 Modbus PDU Holding Register
|
||
地址**。地址 `0` 是第一个 Holding Register,PLC/HMI 文档若使用 `40001`
|
||
风格显示,则通常对应本文地址 `0`。不要把 `40001` 直接传给
|
||
`modbus_read_registers()`。
|
||
|
||
每个寄存器是 16 位。所有 32 位有符号或无符号值均使用:
|
||
|
||
```text
|
||
register[offset] = bits 31..16(高 WORD)
|
||
register[offset + 1] = bits 15..0 (低 WORD)
|
||
```
|
||
|
||
libmodbus 负责单个 16 位寄存器在线路上的字节序;PLC 应显式按“高 WORD
|
||
在前、低 WORD 在后”组合 32 位值。TIA 中建议用移位和 OR 的辅助 FC,
|
||
不要依赖 `AT` 视图、`BLKMOV` 或 CPU 内部字节布局偶然得到相同结果。
|
||
|
||
### 4.2 全局区
|
||
|
||
全局区从地址 `0` 开始,共 32 个寄存器:
|
||
|
||
| 地址 | 长度 | 类型 | 名称 | 方向与说明 |
|
||
| ---: | ---: | --- | --- | --- |
|
||
| 0 | 1 | `UINT16` | `magic_cm` | PLC -> CMVR,固定 `16#434D` |
|
||
| 1 | 1 | `UINT16` | `magic_vr` | PLC -> CMVR,固定 `16#5652` |
|
||
| 2 | 1 | `UINT16` | `protocol_major` | PLC -> CMVR,当前为 1 |
|
||
| 3 | 1 | `UINT16` | `protocol_minor` | PLC -> CMVR,当前为 0 |
|
||
| 4 | 1 | `UINT16` | `axis_count` | PLC -> CMVR,可用轴块数量 |
|
||
| 5 | 1 | `UINT16` | `plc_global_state` | PLC -> CMVR,全局状态 |
|
||
| 6..7 | 2 | `UINT32` | `plc_boot_id` | PLC -> CMVR,每次 PLC 程序运行实例重启必须改变 |
|
||
| 8..9 | 2 | `UINT32` | `cmvr_session_id` | CMVR -> PLC,每次成功握手/重连生成新的非零随机会话 |
|
||
| 10..11 | 2 | `UINT32` | `cmvr_heartbeat` | CMVR -> PLC,周期递增 |
|
||
| 12..13 | 2 | `UINT32` | `plc_heartbeat` | PLC -> CMVR,PLC 自己的周期计数 |
|
||
| 14..15 | 2 | `UINT32` | `communication_watchdog_ms` | CMVR -> PLC,PLC 侧通信 watchdog |
|
||
| 16 | 1 | `UINT16` | `global_error` | PLC -> CMVR,全局错误 |
|
||
| 17 | 1 | `UINT16` | `owner_state` | PLC -> CMVR,当前 owner/握手状态 |
|
||
| 18..19 | 2 | `UINT32` | `owner_session_id` | PLC -> CMVR,当前 Accepted/Rejected 决策对应的候选 session |
|
||
| 20..31 | 12 | - | reserved | 写 0,PLC 忽略 |
|
||
|
||
当前 runtime 在握手初始快照中检查 magic、版本、`axis_count` 和非零
|
||
`plc_boot_id`,先写通信 watchdog,最后以新 session + 首个 heartbeat 作为
|
||
候选 owner 的发布动作。握手的
|
||
每次轮询以及 owner 接受后的最终完整快照都必须保持相同的 magic、版本、
|
||
`axis_count` 和 boot ID;其中任一变化或 boot ID 变为 0 都会立即断线重连。
|
||
只有
|
||
`owner_session_id` 等于本次 session,且 `owner_state=Accepted`,连接才
|
||
进入可发命令状态。后续心跳周期同时检查 `plc_boot_id`、owner/session 以及
|
||
`plc_heartbeat` 是否持续推进;PLC 心跳在通信 watchdog 窗口内不变化会主动
|
||
断线并重连。`owner_state=Rejected` 也只有在 `owner_session_id` 回显本次
|
||
session 时才表示本次握手被拒;旧 session 遗留的 Rejected 状态会被忽略并继续
|
||
轮询。
|
||
|
||
PLC 检测到新 session 后,发布顺序必须是:先把 `owner_state` 改成
|
||
`Accepting`(此时不能先改 `owner_session_id`),然后安全停止旧 owner、清除
|
||
mailbox/旧 stream,再写候选 `owner_session_id`,最后发布 `Accepted`。拒绝时
|
||
同样先处于 `Accepting`/`None`,写完对应 session 后最后发布 `Rejected`。
|
||
否则新的 session ID 可能与残留的旧 `Accepted` 短暂组合,令 CMVR 过早发命令。
|
||
|
||
### 4.3 每轴布局
|
||
|
||
轴 `i` 的基地址:
|
||
|
||
```text
|
||
B(i) = 100 + 128 * i
|
||
```
|
||
|
||
每轴占 128 个 Holding Registers:
|
||
|
||
```text
|
||
B + 0 .. B + 63 控制区
|
||
B + 64 .. B + 127 状态区
|
||
```
|
||
|
||
#### 控制区
|
||
|
||
| 相对地址 | 长度 | 类型 | 名称 | 当前说明 |
|
||
| ---: | ---: | --- | --- | --- |
|
||
| 0..1 | 2 | `UINT32` | `payload_sequence` | 本次轴命令序列 |
|
||
| 2 | 1 | `UINT16` | `command_code` | 见命令码表 |
|
||
| 3 | 1 | `UINT16` | `command_flags` | 当前写 0 |
|
||
| 4..5 | 2 | `INT32` | `target_position` | micro-rad |
|
||
| 6..7 | 2 | `INT32` | `target_velocity` | micro-rad/s |
|
||
| 8..9 | 2 | `INT32` | `acceleration` | micro-rad/s^2 |
|
||
| 10..11 | 2 | `INT32` | `target_torque` | 预留,mN·m |
|
||
| 12..13 | 2 | `INT32` | `position_tolerance` | 预留,micro-rad |
|
||
| 14..15 | 2 | `INT32` | `velocity_tolerance` | 预留,micro-rad/s |
|
||
| 16..17 | 2 | `UINT32` | `command_timeout_ms` | 命令完成时限;Profile 当前写 600000 ms |
|
||
| 18..19 | 2 | `UINT32` | `stream_watchdog_ms` | PLC 侧周期样本 watchdog |
|
||
| 20..21 | 2 | `UINT32` | `cyclic_sample_sequence` | 周期样本序列 |
|
||
| 22..23 | 2 | `UINT32` | `client_monotonic_time_ms` | CMVR steady-clock 低 32 位 |
|
||
| 24..25 | 2 | `UINT32` | `expected_zero_epoch` | `SetZero` 写入命令前读到的 zero epoch |
|
||
| 26 | 1 | `UINT16` | `disconnect_action` | 预留;当前驱动写 0 |
|
||
| 27..28 | 2 | `UINT32` | `command_session_id` | 必须等于当前已接受 session |
|
||
| 29..59 | 31 | - | reserved | 写 0 |
|
||
| 60..61 | 2 | `UINT32` | `payload_sequence_mirror` | 必须等于 payload sequence |
|
||
| 62..63 | 2 | `UINT32` | `commit_sequence` | 单独、最后写入 |
|
||
|
||
#### 状态区
|
||
|
||
下表地址相对于 `S = B + 64`:
|
||
|
||
| 相对地址 | 长度 | 类型 | 名称 | 说明 |
|
||
| ---: | ---: | --- | --- | --- |
|
||
| 0..1 | 2 | `UINT32` | `ack_sequence` | PLC 已解析的 command sequence |
|
||
| 2..3 | 2 | `UINT32` | `active_sequence` | 当前执行中的 command sequence |
|
||
| 4 | 1 | `UINT16` | `command_state` | 命令状态 |
|
||
| 5 | 1 | `UINT16` | `result_code` | 结果码 |
|
||
| 6 | 1 | `UINT16` | `axis_state` | PLC/驱动器轴状态 |
|
||
| 7 | 1 | `UINT16` | `current_mode` | `cmvr.msgs.RunMode` 数值 |
|
||
| 8..9 | 2 | `INT32` | `actual_position` | micro-rad |
|
||
| 10..11 | 2 | `INT32` | `actual_velocity` | micro-rad/s |
|
||
| 12..13 | 2 | `INT32` | `actual_torque` | mN·m |
|
||
| 14..15 | 2 | `INT32` | `target_position` | PLC 当前目标,micro-rad |
|
||
| 16..17 | 2 | `INT32` | `target_velocity` | PLC 当前目标,micro-rad/s |
|
||
| 18 | 1 | bit field | `status_flags` | 见状态位表 |
|
||
| 19 | 1 | `UINT16` | `drive_statusword` | 原始驱动器状态字 |
|
||
| 20..21 | 2 | `UINT32` | `fault_code` | PLC/驱动器故障码 |
|
||
| 22..23 | 2 | `UINT32` | `zero_epoch` | 置零版本 |
|
||
| 24..25 | 2 | `UINT32` | `last_applied_cyclic_sequence` | 最后实际锁存的周期样本 |
|
||
| 26..27 | 2 | `UINT32` | `state_sequence` | 状态 seqlock;非零偶数才是稳定版本 |
|
||
| 28..29 | 2 | `UINT32` | `plc_monotonic_time_ms` | PLC 单调时间低 32 位 |
|
||
| 30..31 | 2 | `UINT32` | `heartbeat_age_ms` | PLC 计算的 CMVR 心跳年龄 |
|
||
| 32..33 | 2 | `UINT32` | `ack_session_id` | `ack_sequence` 所属 session |
|
||
| 34..61 | 28 | - | reserved | PLC 写 0 |
|
||
| 62..63 | 2 | `UINT32` | `state_sequence_mirror` | 稳定版本镜像 |
|
||
|
||
PLC 状态区使用 seqlock 发布。稳定版本从非零偶数 `2` 开始,每次完整更新增加
|
||
`2`;版本回绕到 `0` 时跳到 `2`。发布顺序必须是:
|
||
|
||
1. 更新任何状态字段前,先把 `state_sequence` 写成下一奇数,
|
||
`state_sequence_mirror` 保持上一个稳定偶数;
|
||
2. 写完 ACK、状态、实际值、flags、heartbeat age 等所有字段;
|
||
3. 先把 `state_sequence_mirror` 写成新的非零偶数;
|
||
4. 最后把 `state_sequence` 写成相同偶数。
|
||
|
||
runtime 在同一 socket 互斥区内执行三段读取:先单独读取
|
||
`state_sequence`,再读取完整 64-word 状态块,最后再次单独读取
|
||
`state_sequence`。只有 before、after、块内 sequence 和 mirror 四者相等,
|
||
且为非零偶数时才接受完整块;失败最多重试 3 次。这样不要求 Siemens
|
||
`MB_SERVER` 对 64-word FC3 做原子内存快照,也允许不同的完整状态读取之间版本
|
||
持续推进。
|
||
|
||
为保证活性,PLC 不得在每个高速控制扫描都无条件翻转 Holding 状态版本。建议
|
||
驱动控制状态先写内部 shadow,再由较低频的对外发布任务在字段有意义变化时复制
|
||
到 Holding 状态区,并让每个稳定偶数版本至少覆盖三次连续 FC3 的时间窗口;
|
||
也可以使用双缓冲后按上述 seqlock 顺序发布。否则 PLC 每次 FC3 之间都推进版本,
|
||
runtime 的 3 次有限重试会按设计失败,而不是返回可能撕裂的状态。
|
||
|
||
### 4.4 命令码
|
||
|
||
| 值 | 名称 | 当前上层使用 |
|
||
| ---: | --- | --- |
|
||
| 0 | `Nop` | 否 |
|
||
| 1 | `SetZero` | `setZero` |
|
||
| 2 | `MoveToZero` | 预留;当前 `moveToZero` 发送 `ProfilePosition(target=0)` |
|
||
| 3 | `ProfilePosition` | `moveToZero`、`profilePosition` |
|
||
| 4 | `ProfileVelocity` | `profileVelocity` |
|
||
| 5 | `OpenCyclicPosition` | 第一个周期位置样本前自动发送 |
|
||
| 6 | `CyclicPositionSample` | 周期位置样本 |
|
||
| 7 | `OpenCyclicVelocity` | 第一个周期速度样本前自动发送 |
|
||
| 8 | `CyclicVelocitySample` | 周期速度样本 |
|
||
| 9 | `CloseCyclicStream` | 预留;当前流结束使用 `QuickStop` |
|
||
| 10 | `QuickStop` | `emergencyStop`、流结束和上层超时 |
|
||
| 11 | `Enable` | `setEnabled(true)` |
|
||
| 12 | `Disable` | `setEnabled(false)` |
|
||
|
||
PLC 必须区分:
|
||
|
||
- `SetZero`:按已确认的项目语义设置当前位置基准,不得擅自解释为运动回原点;
|
||
- `ProfilePosition(target=0)`:运动到已经建立的零位;
|
||
- Homing/寻找原点:当前 gRPC 和寄存器协议没有独立开放。
|
||
|
||
### 4.5 命令状态、结果和状态位
|
||
|
||
`command_state` 定义:
|
||
|
||
```text
|
||
0 Idle 1 Received 2 Validating
|
||
3 Accepted 4 Running 5 TargetReached
|
||
6 Completed 7 Rejected 8 Failed
|
||
9 TimedOut 10 QuickStopped 11 CommunicationLost
|
||
```
|
||
|
||
runtime 会先拒绝未知状态,再按命令检查成功后置条件:
|
||
|
||
- `SetZero`:`Completed`、`ZeroValid=1`,且 `zero_epoch` 相比命令前变化;
|
||
- `Enable`:`Completed` 且 `Enabled=1`;
|
||
- `Disable`:`Completed` 且 `Enabled=0`;
|
||
- `QuickStop`:`QuickStopped` 或 `Completed`,且实际速度已接近 0;
|
||
- Profile:`Completed` 或 `TargetReached`;
|
||
- 非终态 ACK 只接受 `Accepted`、`Running`、`TargetReached`、`Completed`。
|
||
|
||
`Rejected`、`Failed`、`TimedOut`、`CommunicationLost` 始终视为失败。
|
||
|
||
`result_code` 定义:
|
||
|
||
```text
|
||
0 Ok 1 InvalidCommand
|
||
2 InvalidParameter 3 AxisNotReady
|
||
4 AxisBusy 5 NotEnabled
|
||
6 PositionLimit 7 VelocityLimit
|
||
8 AccelerationLimit 9 ZeroNotValid
|
||
10 DriveFault 11 CommandTimeout
|
||
12 SequenceError 13 SessionMismatch
|
||
14 CommunicationWatchdog
|
||
15 CyclicWatchdog 16 Unsupported
|
||
17 InternalError
|
||
```
|
||
|
||
`status_flags`:
|
||
|
||
| bit | 名称 |
|
||
| ---: | --- |
|
||
| 0 | Enabled |
|
||
| 1 | Moving |
|
||
| 2 | TargetReached |
|
||
| 3 | Fault |
|
||
| 4 | QuickStopActive |
|
||
| 5 | CommunicationWatchdogExpired |
|
||
| 6 | CyclicWatchdogExpired |
|
||
| 7 | ZeroValid |
|
||
| 8 | StreamActive |
|
||
| 9 | CommandBusy |
|
||
|
||
`CmvrPlcMotorProtocol` 把 `Fault`、`CommunicationWatchdogExpired` 和
|
||
`CyclicWatchdogExpired` 都作为致命反馈状态;失败/未知 `command_state` 或非
|
||
`Ok result_code` 同样无效。此时 `getQ/getQd` 返回 NaN,`reachedTargetQ`
|
||
返回 false,MotorService 不得把残留的有限位置/速度误判为到位或成功状态。
|
||
|
||
### 4.6 缩放和范围
|
||
|
||
CMVR PLC v1 使用固定缩放:
|
||
|
||
| 量 | gRPC/C++ 单位 | 寄存器值 |
|
||
| --- | --- | --- |
|
||
| 位置 | rad | `round(rad * 1,000,000)`,micro-rad |
|
||
| 速度 | rad/s | `round(rad/s * 1,000,000)`,micro-rad/s |
|
||
| 加速度 | rad/s^2 | `round(rad/s^2 * 1,000,000)`,micro-rad/s^2 |
|
||
| 力矩 | N·m | 预留为 `round(N·m * 1,000)`,mN·m |
|
||
|
||
前三者当前由 `CmvrPlcMotorProtocol` 实际使用,并在转换前检查 finite 和
|
||
`INT32` 范围。缩放为 1,000,000 时,理论可表示范围约为
|
||
`[-2147.483648, 2147.483647]` 个对应 SI 单位。PLC 仍必须再次执行软件限位、
|
||
驱动器限位和状态检查,不能只依赖上位机检查。
|
||
|
||
Modbus PLC 电机初始化强制要求每个轴都有有限、有效的 `q_lb`、`q_ub`、`qd`
|
||
和 `qdd`:`q_ub > q_lb`,且 `qd`、`qdd` 均大于 0。缺失、NaN、无穷或非法
|
||
限位会令电机初始化失败,不能以“未配置限位”的方式继续带轴运行。
|
||
|
||
## 5. payload-first、commit-last 和 ACK
|
||
|
||
### 5.1 CMVR 写入顺序
|
||
|
||
每轴的 `command_sequence` 独立递增并跳过 `0`。一次命令严格执行:
|
||
|
||
1. 仅 `SetZero` 先可靠读取 fresh `zero_epoch`;其他命令不做冗余 baseline
|
||
状态读取;
|
||
2. 在本地构造 62 个寄存器的完整 payload;
|
||
3. 同时写入 `payload_sequence` 和 `payload_sequence_mirror`;
|
||
4. 使用一次 Holding Register 批量写,把 `B+0 .. B+61` 写入 PLC;
|
||
5. 再使用第二次写,把同一序列写入 `B+62 .. B+63`;
|
||
6. 轮询状态区,直到 `ack_sequence` 等于本次 command sequence;
|
||
7. 检查 `command_state` 和 `result_code`;
|
||
8. 周期样本还要检查 `last_applied_cyclic_sequence`。
|
||
|
||
PLC 只允许在以下条件全部满足时消费 payload:
|
||
|
||
```text
|
||
commit_sequence != last_processed_commit
|
||
payload_sequence == payload_sequence_mirror
|
||
payload_sequence == commit_sequence
|
||
command_session_id == owner_session_id
|
||
当前 cmvr_session_id 是已取得 owner 的有效 session
|
||
```
|
||
|
||
对于 `SetZero`,PLC 还必须要求 `expected_zero_epoch` 等于执行前的当前
|
||
`zero_epoch`;不匹配时以 `Rejected + SequenceError` ACK,避免陈旧或重复的
|
||
置零事务改变新的零位基准。
|
||
|
||
PLC 应先把完整 payload 复制到内部命令快照,再更新
|
||
`last_processed_commit`。不要一边读取 Holding Register,一边执行驱动器动作。
|
||
|
||
无论接受还是拒绝,PLC 都应把 `ack_sequence` 和 `ack_session_id` 更新为
|
||
本次命令的序列和 session,同时填写 `command_state` 和 `result_code`。
|
||
runtime 只有在两者都精确匹配时才接受 ACK,旧连接残留的相同序列不会被误认。
|
||
否则 CMVR 只能得到模糊的 ACK timeout。
|
||
|
||
如果 commit 已写成功但 ACK 读取失败,电机是否已经执行是不确定的。
|
||
CMVR runtime 不会自动重放该运动命令。特别是 `SetZero`、非零速度和使能命令,
|
||
调用方不得在未知结果下盲目重试;应先读取 PLC 状态、boot/session、zero epoch
|
||
和实际轴状态。
|
||
|
||
### 5.2 boot/session 防重放
|
||
|
||
PLC 必须把全局 session 和每轴 command sequence 共同作为命令命名空间:
|
||
|
||
- `cmvr_session_id` 在每次成功握手/重连时重新生成非零随机值。检测到新
|
||
session 时,PLC 必须先把 `owner_state` 置为 `Accepting`,并且这一步必须
|
||
早于改写 `owner_session_id`;随后安全停止旧 owner 的轴、清除旧的
|
||
stream-active 状态,并清空各轴旧 mailbox 的 commit/payload 接收状态。
|
||
清理全部完成后再写入新的 `owner_session_id`,最后一步才把
|
||
`owner_state` 发布为 `Accepted`。这样 runtime 不会把“新 session ID +
|
||
旧 Accepted”误认为新 owner 已就绪。拒绝候选 session 时也必须先保持
|
||
`Accepting`、写入对应 `owner_session_id`,最后一步发布 `Rejected`。
|
||
CMVR 只有看到该
|
||
握手确认后才把连接标记为可用。CMVR
|
||
会把每轴 command sequence 从 `1` 重新开始,PLC 必须在新 session
|
||
命名空间内接受该序列。runtime 至少保证相邻两次连接的 session 不相同,
|
||
避免紧邻重连立即复用旧 mailbox/ACK 命名空间。
|
||
- 每个轴应保存“当前 session 下最后处理的 commit sequence”。相同 commit
|
||
只返回原 ACK,不能再次执行。
|
||
- PLC 启动时必须生成新的非零 `plc_boot_id`,清除 owner 和旧 commit 接受
|
||
状态,并要求看到新的有效 session/heartbeat 后才接受命令。若 Holding DB
|
||
设置为 retentive,也不能让上次启动遗留的 commit 自动执行。
|
||
- CMVR 周期检查 `plc_boot_id`。boot ID 变化会断开连接;重连成功后
|
||
`connection_epoch` 改变。Protocol 会锁存并拒绝旧 cyclic generation,
|
||
不会自动重新 `OpenCyclic*`。只有客户端新建 gRPC 流并通过首帧建立新
|
||
generation 后才能恢复。
|
||
- Profile 和 Cyclic 命令把 Protocol 已绑定的 `connection_epoch` 作为
|
||
`expected_connection_epoch` 传给 runtime。runtime 在调用入口、等待每轴
|
||
队列之后,以及持有 I/O 锁准备分配 sequence/写 commit 前都要求
|
||
invocation/current/expected 三者严格一致。因此即使重连恰好发生在
|
||
Protocol 读取 epoch 与 runtime 写 mailbox 之间,旧命令也只会失败,不会
|
||
写入新 session。Quick Stop/Disable 不绑定旧 expected epoch,它们作为新调用
|
||
只清理当前 session。
|
||
- command sequence 是 32 位并会回绕。PLC 应使用 session 加序列的状态机,
|
||
明确处理回绕;不能简单把“任何不相等的值”永远视为新命令。
|
||
|
||
TCP 自身有序可靠,但不能替代上述应用层规则:PLC DB 可能保留旧值,PLC 和
|
||
CMVR 也可能独立重启。
|
||
|
||
## 6. S7-1215C DC/DC/DC 与 TIA Portal
|
||
|
||
### 6.1 MB_SERVER 数据块
|
||
|
||
建议创建一个专用、非 retentive 的协议数据块,例如:
|
||
|
||
```scl
|
||
HoldingRegister : ARRAY[0 .. 100 + 128 * AXIS_COUNT - 1] OF WORD;
|
||
```
|
||
|
||
数组下标与本文零基 PDU 地址一致。根据使用的 TIA Portal 和 CPU firmware,
|
||
`MB_HOLD_REG` 对数据块访问方式可能有要求;若编译器不允许优化 DB 的
|
||
VARIANT/指针映射,应关闭该协议 DB 的 optimized block access。不要把命令
|
||
状态机、驱动器实例 DB 与外部可写 Holding Register 直接重叠。
|
||
|
||
在 OB1 或固定周期 OB 中每个扫描周期调用一个 `MB_SERVER` 实例。典型参数
|
||
包括:
|
||
|
||
```text
|
||
DISCONNECT = FALSE
|
||
CONNECT_ID = 项目内唯一连接 ID
|
||
IP_PORT = 502
|
||
MB_HOLD_REG = 协议 HoldingRegister 数组
|
||
NDR/DR/ERROR/STATUS = 诊断输出
|
||
```
|
||
|
||
不同 TIA Portal 版本的块接口和 VARIANT 写法可能略有差异,应以当前工程中
|
||
插入的 `MB_SERVER` 指令帮助为准。一个 server 实例使用自己的 instance DB;
|
||
连接 ID 和 TCP 端口不得与其他 OUC/Modbus 实例冲突。
|
||
|
||
`MB_SERVER` 只负责 Modbus TCP 搬运。另建 PLC FB 完成:
|
||
|
||
1. 初始化 magic、协议版本、axis count 和 boot ID;
|
||
2. 监视 session 与 heartbeat;
|
||
3. 对每轴执行 payload/commit 原子接收;
|
||
4. 做范围、状态、使能、零位和 command timeout 校验;
|
||
5. 调用 Technology Object、PROFINET 驱动器 telegram 或项目已有驱动器 FB;
|
||
6. 更新 ACK、命令状态、结果码、实际值和状态位;
|
||
7. 执行通信/周期 watchdog 和安全降级。
|
||
|
||
### 6.2 32 位辅助函数
|
||
|
||
PLC 侧应显式实现以下等价逻辑:
|
||
|
||
```text
|
||
DecodeUDInt(high, low) =
|
||
SHL(WORD_TO_DWORD(high), 16) OR WORD_TO_DWORD(low)
|
||
|
||
EncodeHigh(value) = DWORD_TO_WORD(SHR(value, 16))
|
||
EncodeLow(value) = DWORD_TO_WORD(value AND 16#0000_FFFF)
|
||
```
|
||
|
||
有符号值先按 DWORD 原样组合,再解释为 DINT。负值使用二进制补码;不要分别
|
||
对高、低 WORD 做有符号运算。
|
||
|
||
### 6.3 驱动器动作映射
|
||
|
||
映射必须由实际驱动器/Technology Object 语义决定:
|
||
|
||
- `SetZero` 只执行已确认的“当前位置建立零位”动作,不得自动替换成会运动的
|
||
Homing。
|
||
- “回 0 位”当前收到的是 `ProfilePosition(target=0)`。
|
||
- Profile Position/Velocity 的轨迹生成在 PLC/驱动器侧完成;Modbus TCP
|
||
不是驱动器位置环或电流环。
|
||
- Cyclic Position/Velocity 是 CMVR 到 PLC 的软实时 setpoint 更新。PLC
|
||
在本地扫描周期锁存最新样本,再由 PLC/驱动器的确定性周期执行。
|
||
- 稳态周期样本通常需要 5 次 Modbus 事务(payload、commit,以及
|
||
guard/full/guard 三次 ACK 快照读取);恰逢心跳到期时增加 1 次。首个样本
|
||
还需要先完成一次惰性的 Open。这个事务模型不承诺固定控制频率,也不是硬
|
||
实时链路。
|
||
- Quick Stop 的减速度、抱闸时序和重力轴保持策略必须在 PLC/驱动器中配置。
|
||
- Enable/Disable 必须检查故障、STO、抱闸和轴 ready 状态;不能只翻转一个
|
||
普通布尔位。
|
||
|
||
PLC 必须在执行前再次检查位置、速度、加速度、驱动器状态和项目级互锁,并把
|
||
拒绝原因写入 `result_code`。
|
||
|
||
当前测试只在 x86 loopback fake PLC 上验证功能和协议一致性,尚未给出
|
||
S7-1215C 实机可持续频率。投产前必须在目标 TIA Portal 程序、真实 PLC 扫描
|
||
周期和现场交换网络下阶梯增加 CSP/CSV 发送频率,记录 ACK 延迟的
|
||
P50/P99/最大值、`dropped_setpoints`、Modbus 异常与 watchdog 触发次数。
|
||
`cyclic_watchdog_ms` 应依据实测最坏延迟并保留工程余量设置;在完成这项台架
|
||
测试前,不能宣称支持某个固定 Hz。
|
||
|
||
CMVR 中的 `QuickStop` 和 `Disable` 走 safety-priority 通道:它们会增加该轴
|
||
的取消 generation,令正在等待的普通命令失败,并绕过普通轴命令互斥锁。安全
|
||
命令不先读取状态,也不在 mailbox 前插入心跳写;拿到 socket 后首先发送安全
|
||
payload/commit。它仍与单个 Modbus socket 的一次事务互斥,不会把两条报文
|
||
交错写入;普通命令在 commit 前会再次检查 generation,避免急停完成后补发
|
||
旧运动命令。
|
||
|
||
同一轴的 safety 命令使用独立 safety mutex 串行。每个 safety 调用在等待该锁
|
||
之前就提升普通命令的取消 generation,因此抢占不会被前一个 Quick Stop 阻塞;
|
||
但后来的 safety 调用不会取消前一个 safety 调用的 ACK 等待,多个并发
|
||
Quick Stop/Disable 都能得到各自确定的执行结果。
|
||
|
||
### 6.4 心跳、watchdog 和断链
|
||
|
||
CMVR 默认每 100 ms 写一次 `cmvr_session_id + cmvr_heartbeat`。本仓库实体
|
||
样例要求 PLC 在 1000 ms 通信 watchdog 内看到 heartbeat **发生变化**
|
||
(字段为 0 时 runtime 默认 500 ms)。PLC 应使用自己的
|
||
单调时间测量“最后一次变化”的年龄,不能只检查 TCP socket 仍连接,也不能把
|
||
重复读到同一个 counter 当作有效心跳。
|
||
|
||
命令提交不会为每个周期样本强制写 heartbeat;runtime 记录最后一次成功写入
|
||
时刻,仅在 `heartbeat_period_ms` 已到期时由当前命令顺带补写。supervisor
|
||
worker 仍按周期写心跳并检查 PLC boot/session/heartbeat,因此高频 setpoint
|
||
既不会产生一倍额外 Modbus 写流量,也不会饿死通信 watchdog。
|
||
|
||
推荐 PLC 状态机:
|
||
|
||
```text
|
||
无 owner
|
||
-> 收到有效非零 session 且 heartbeat 开始变化
|
||
-> owner active
|
||
-> 接受该 session 的 commit
|
||
|
||
owner active
|
||
-> heartbeat age 超过 communication_watchdog_ms
|
||
-> 对所有 owner 轴执行受控停止/Quick Stop
|
||
-> 设置 CommunicationWatchdogExpired
|
||
-> command_state = CommunicationLost
|
||
-> 释放 owner
|
||
```
|
||
|
||
周期流还需要每轴独立 watchdog。只有新的
|
||
`Cyclic*Sample.cyclic_sample_sequence` 才刷新它;普通 CMVR 心跳不能让旧的
|
||
非零速度无限保持。周期 watchdog 超时后应停止该轴、清除 `StreamActive`,
|
||
设置 `CyclicWatchdogExpired`,并要求重新 `OpenCyclic*`。网络恢复后不得
|
||
自动恢复断链前的速度或 setpoint;旧 gRPC 流必须失败关闭,由客户端新建流。
|
||
|
||
CMVR runtime 遇到 Modbus 读写错误会关闭 socket,并按
|
||
`reconnect_min_ms` 到 `reconnect_max_ms` 指数退避重连。它不会自动重放上一
|
||
条运动命令。PLC 侧安全动作必须在没有 CMVR 参与的情况下独立完成。
|
||
|
||
## 7. 安全边界
|
||
|
||
标准 **S7-1215C DC/DC/DC 不是 failsafe PLC**。`MB_SERVER`、普通 OB/FB、
|
||
普通数字输出以及本服务的 `emergencyStop` 都只是功能性控制,不能提供
|
||
安全等级的急停、STO 或防护门联锁。
|
||
|
||
实际设备至少应按风险评估使用:
|
||
|
||
- 硬接线急停回路;
|
||
- 合规的安全继电器,或 F-CPU + F-I/O;
|
||
- 驱动器 STO 双通道或经认证的安全功能;
|
||
- 接触器/抱闸反馈和必要的 EDM;
|
||
- 与机械负载、重力轴和制动距离匹配的安全设计。
|
||
|
||
软件 `emergencyStop` 和 Modbus Quick Stop 可以作为操作层的快速停止,但
|
||
不能替代硬接线安全回路。标准 CPU 程序卡死、以太网交换机故障、普通输出粘连
|
||
或软件错误时,硬件安全链仍必须独立切断危险能量。
|
||
|
||
## 8. 构建、依赖和测试
|
||
|
||
### 8.1 x86-64
|
||
|
||
仓库已有 x86-64 的 libmodbus 3.1.11:
|
||
|
||
```text
|
||
dependency/x86/third_party/modbus/3.1.11/include/modbus
|
||
dependency/x86/third_party/modbus/3.1.11/lib/libmodbus.so
|
||
dependency/x86/third_party/modbus/3.1.11/lib/libmodbus.so.5
|
||
```
|
||
|
||
`request.txt` 已包含 `third_party/modbus/3.1.11`。标准构建流程:
|
||
|
||
```bash
|
||
cmake -S . -B build -DBUILD_TESTING=ON
|
||
cmake --build build -j"$(nproc)"
|
||
ctest --test-dir build --output-on-failure
|
||
cmake --install build
|
||
ldd -r output/bin/cmvr_es | grep -E 'modbus|not found'
|
||
```
|
||
|
||
只验证本次 MotorService/PLC 电机链路时,可执行:
|
||
|
||
```bash
|
||
cmake --build build --target \
|
||
modbus_tcp_motor_bus_runtime_test \
|
||
grpc_motor_service_test \
|
||
grpc_motor_service_modbus_e2e_test \
|
||
-j4
|
||
ctest --test-dir build \
|
||
-R '^(modbus_tcp_motor_bus_runtime_test|grpc_motor_service_test|grpc_motor_service_modbus_e2e_test)$' \
|
||
--output-on-failure
|
||
```
|
||
|
||
当前这 3 个 CTest 目标共包含 63 个 GoogleTest 用例:Modbus runtime 25 个、
|
||
MotorService 36 个、gRPC–Modbus 端到端 2 个。
|
||
|
||
测试分别覆盖:
|
||
|
||
- Modbus runtime 的命令、离线启动后上线、重连/stop-start session 隔离、
|
||
stale owner 决策、握手身份漂移、三段 seqlock 撕裂重试、旧 cyclic
|
||
generation 跨 epoch 的 fail-closed 锁存、Protocol-to-runtime
|
||
epoch TOCTOU 拒绝、Profile 反馈 session 绑定、CSP/CSV 样本 ACK、冗余
|
||
状态/心跳事务抑制、并发 safety 串行、故障反馈拒绝、非法轴/样本,以及
|
||
仓库真实样例配置的解析和 runtime 初始化;
|
||
- MotorService 的同步 Profile Position/Velocity 等待、取消/超时、急停竞争、
|
||
异常边界、CSP/CSV 流、流背压和停止失败;
|
||
- 单进程真实链路
|
||
gRPC stub → DeviceManager → MotorManager → `AbstractMotor` →
|
||
CMVR PLC protocol → Modbus TCP fake PLC,包括使能、Profile
|
||
Position/Velocity、CSP、half-close Quick Stop、急停锁存、活动 cyclic 流
|
||
跨重连失败关闭、新流恢复,以及断线重连不重放。
|
||
|
||
fake PLC 用例会在 loopback 地址启动本地 server,运行环境必须允许本地 TCP
|
||
bind/listen。
|
||
|
||
运行安装产物:
|
||
|
||
```bash
|
||
./output/bin/cmvr_es
|
||
```
|
||
|
||
默认 gRPC 端口由
|
||
`cmvr-es/config/tasks/grpc_server_task/grpc_server_task.pb.txt` 配置,当前为
|
||
`50052`。启动前应先用禁能或脱载轴验证 PLC 寄存器和方向。
|
||
|
||
### 8.2 启用配置和 gRPC 调用
|
||
|
||
首次联调前:
|
||
|
||
1. 把 `cmvr-es/config/devices/motor/plc_motors.pb.txt` 中的 `host`、轴映射
|
||
和关节限位改成现场值;
|
||
2. 完成 PLC watchdog、驱动器 Quick Stop 和硬件安全链检查;
|
||
3. 将 `cmvr-es/config/manager/device_manager.pb.txt` 中 `plc_motors` 的
|
||
`enable` 改为 `true`;
|
||
4. 重新执行 `cmake --install build`,再启动 `./output/bin/cmvr_es`。
|
||
|
||
启用 reflection 后,可先确认服务和状态:
|
||
|
||
```bash
|
||
grpcurl -plaintext 127.0.0.1:50052 list cmvr.api.MotorService
|
||
|
||
grpcurl -plaintext \
|
||
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"}}' \
|
||
127.0.0.1:50052 cmvr.api.MotorService/getStatus
|
||
```
|
||
|
||
在轴已安全脱载、PLC/驱动器允许使能后,显式使能并执行一个同步位置命令:
|
||
|
||
```bash
|
||
grpcurl -plaintext \
|
||
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"},"enabled":true}' \
|
||
127.0.0.1:50052 cmvr.api.MotorService/setEnabled
|
||
|
||
grpcurl -plaintext \
|
||
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"},"targetPositionRad":0.1,"maxVelocityRadS":0.2,"accelerationRadS2":0.5,"wait":{"timeoutMs":30000}}' \
|
||
127.0.0.1:50052 cmvr.api.MotorService/profilePosition
|
||
```
|
||
|
||
软件 Quick Stop:
|
||
|
||
```bash
|
||
grpcurl -plaintext \
|
||
-d '{"target":{"header":{"deviceId":"plc_motors"},"jointName":"PLC_AXIS_1"}}' \
|
||
127.0.0.1:50052 cmvr.api.MotorService/emergencyStop
|
||
```
|
||
|
||
双向周期流应使用生成的 gRPC client stub,并发写 setpoint、持续读反馈;不要
|
||
用只发送一次 JSON 的 unary 调用方式模拟。客户端必须先收到 `OPENED`,发送
|
||
首个样本,再等待相同 sequence 的 `APPLIED`。正常退出时 half-close 写端并
|
||
继续读取,直到收到 `STOPPED` 和最终 OK status。
|
||
|
||
当前服务默认监听 `0.0.0.0:50052`,使用 insecure gRPC,任何可达客户端都能
|
||
发控制命令。现场至少应绑定可信控制网接口或回环地址并配置防火墙;不得直接
|
||
暴露到办公网或公网。若需要跨不可信网络访问,应在进入设备前增加认证、TLS
|
||
和工业安全网关。
|
||
|
||
### 8.3 ARM 当前缺失
|
||
|
||
`dependency/arm/third_party/` 当前没有 libmodbus。现有
|
||
`libmodbus.so.5.1.0` 是 x86-64 ELF,不能复制到 ARM 设备使用。
|
||
|
||
ARM 支持前需要:
|
||
|
||
1. 为目标 ARM ABI 编译 libmodbus 3.1.11;
|
||
2. 按相同布局放入
|
||
`dependency/arm/third_party/modbus/3.1.11/{include,lib}`;
|
||
3. 确认 ARM toolchain/顶层 CMake 选择 `dependency/arm`。当前顶层
|
||
`CMakeLists.txt` 仍把 `ARCH` 设为 `x86`;
|
||
4. 在目标设备执行 `file`、`readelf -h` 和 `ldd -r` 验证架构、SONAME 和
|
||
运行时依赖;
|
||
5. 重新执行无硬件测试和 PLC 台架测试。
|
||
|
||
在这些步骤完成前,ARM 构建应视为不支持 Modbus PLC 电机后端。
|
||
|
||
### 8.4 PLC 台架检查
|
||
|
||
建议按以下顺序验证:
|
||
|
||
1. PLC 上电后检查 magic、版本、axis count、boot ID。
|
||
2. 只连接 Modbus,确认 session、CMVR heartbeat 和 PLC heartbeat。
|
||
3. 禁能状态验证错误参数、重复 commit、旧 session 和 ACK/result。
|
||
4. 验证 `SetZero` 的项目语义,确认没有意外运动。
|
||
5. 低速、低加速度验证 Profile Position 和 Profile Velocity。
|
||
6. 验证周期流正常结束、gRPC watchdog 和 PLC cyclic watchdog。
|
||
7. 分别拔网线、停止 `cmvr_es`、重启交换机、重启 PLC,确认不会恢复旧速度。
|
||
8. 验证 `emergencyStop` 后必须显式 enable 才能再次运动。
|
||
9. 在硬件安全回路测试合格后,才允许带载运行。
|
||
|
||
## 9. 当前已知约束
|
||
|
||
- MotorService 是单轴 API,没有多轴同扫描周期 commit。
|
||
- `DeviceManager`/`MotorManager` 拓扑在 gRPC 服务运行期间必须保持不变;
|
||
当前管理器只支持启动期注册,不支持在仍有 RPC 或流持有电机时热移除、
|
||
热替换同 ID 的 MotorManager。需要换配置时,应先停止 gRPC 服务和设备,
|
||
再重建运行时。
|
||
- Profile Torque、Cyclic Torque、Clear Fault 和独立 Homing 未开放。
|
||
- gRPC 流的同步 `Write()` 可能受慢客户端背压;PLC watchdog 必须独立。
|
||
- `getStatus` 不是一次原子 Modbus 快照。
|
||
- Modbus TCP 不提供认证、加密或安全完整性。控制网络应隔离,并在需要时通过
|
||
防火墙/VPN/工业安全网关限制访问。
|
||
- Modbus TCP 的普通软件停止不具备功能安全等级。
|