FSoE 功能安全
FSoE (Functional Safety over EtherCAT) 安全通信接口,在标准 PDO 通道上叠加安全协议层,实现 SIL3/PLe 等级的功能安全通信。
通过 slave.GetFSoE() 获取 FSoE& 引用。
slave.GetFSoE() 在从站初始化时自动检测。检测过程:
- CoE 前置条件 — 从站必须支持 CoE 邮箱协议
- 0xF980:01 检测 — 尝试 SDO 读取设备级 FSoE 安全地址,成功则确认支持
- 0x9001:02 检测 — 若上步失败,尝试读取 MDP 连接参数(适用于多模块安全设备)
仅检查 CoE 支持是不够的,因为很多非安全设备(如普通伺服驱动器)也支持 CoE。FSoE 设备必须实现特定的安全对象索引才能被正确识别。
快速开始
FSoE 工作流程
FSoE 在 EtherCAT PDO 通道上建立独立的安全连接。主站负责协议管理,从站负责安全逻辑。
状态机流程:
- Reset — 初始状态,等待主站发起会话
- Session — 会话建立,交换连接 ID
- Connection — 连接建立,验证安全地址
- Parameter — 参数下载阶段(SRA CRC 校验)
- Data — 正常安全数据交换
- Failsafe — 失效安全,从站输出安全值
BindSafeIO 一行完成初始化 + 启动数据交换,中间状态(Session → Connection → Parameter)由 DLL 自动推进。
使用 Darra EtherCAT Master Tools 的代码导出功能,可一键生成安全数据结构体和绑定代码。
最简示例(数字量安全 IO)
// 安全数据结构体(通过 Darra 配置工具一键导出)
#pragma pack(push, 1)
struct SafeDigitalInput // EL19xx: 2 字节
{
uint8_t InputBits; // 安全输入状态位(每位对应一个通道)
uint8_t DiagBits; // 输入诊断状态
};
struct SafeDigitalOutput // EL29xx: 1 字节
{
uint8_t OutputBits; // 安全输出控制位
};
#pragma pack(pop)
auto& slave = master.GetSlave(1); // EL1904 安全输入 + EL2904 安全输出
auto& fsoe = slave.GetFSoE();
// 1. 一行绑定 + exchange 回调:自动推断数据大小、启动数据交换
// exchange 每个 PDO 周期自动调用
fsoe.BindSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0100, // safetyAddress
[](const SafeDigitalInput& input, SafeDigitalOutput& output) {
bool ch0 = (input.InputBits & 0x01) != 0;
output.OutputBits = ch0 ? 0x01 : 0x00;
});
// 2. 订阅事件(自动触发,无需手动调用)
fsoe.OnError = [](FSoEError error) {
printf("FSoE 错误: 0x%04X\n", static_cast<int>(error));
};
每个 PDO 周期,系统自动执行:
- 周期开始 — 刷新所有 FSoE 从站的安全输入缓冲区、检查状态变化并触发事件
- DataExchange 回调 — 从站级别回调,读取输入(已是最新数据),修改输出
- ProcessDataCyclicSync 回调 — 主站级别回调(普通 PDO 读写)
- 周期结束 — 提交所有 FSoE 从站的安全输出缓冲区到 DLL
FSoE 与普通 PDO 的区别
FSoE 安全数据不能像普通 PDO 那样直接用零拷贝指针映射。原因在于协议层的差异:
尽管底层机制不同,BindSafeIO + DataExchange 回调的使用体验已经非常接近普通 PDO。
普通 PDO
IOmap 中的数据就是用户数据,结构体直接叠加到内存指针即可。
FSoE PDO
IOmap 中存放的是 FSoE 协议帧,用户的安全数据被包裹在帧内部,且需要经过校验。
SDK 的 FSoE 协议引擎负责:
- CRC 校验 — 验证输入帧完整性、计算输出帧 CRC
- 状态机管理 — Session → Connection → Parameter → Data 自动推进
- 看门狗 — 监控通信超时,超时自动进入 Failsafe
- 序列号 — 检测帧丢失和重复
绕过 DLL 直接读写 IOmap 会破坏安全协议完整性,因此 FSoE 必须通过独立缓冲区访问。
简化绑定
BindSafeIO<TIn, TOut>()
template<typename TIn, typename TOut>
bool BindSafeIO(uint16_t safetyAddress,
std::function<void(const TIn&, TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100, uint16_t connectionId = 0);
一行完成 FSoE 初始化 + 启动数据交换。自动从结构体推断 SafeInputSize / SafeOutputSize,使用从站的 Ioffset() / Ooffset() 作为 PDO 偏移,connectionId 为 0 时自动分配。
exchange 回调每个 PDO 周期自动调用,以 const 引用传入安全输入、可写引用传入安全输出。
参数:
safetyAddress(uint16_t) — 从站安全地址(硬件拨码)exchange(std::function) — 数据交换回调(可选)watchdogMs(uint32_t) — 看门狗超时(毫秒),默认 100connectionId(uint16_t) — 连接 ID,0 则自动分配
返回值:
bool— 成功返回true
示例:
fsoe.BindSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0100,
[](const SafeDigitalInput& input, SafeDigitalOutput& output) {
output.OutputBits = input.InputBits;
});
BindSafeInput<TIn>()
template<typename TIn>
bool BindSafeInput(uint16_t safetyAddress,
std::function<void(const TIn&)> exchange = nullptr,
uint32_t watchdogMs = 100, uint16_t connectionId = 0);
仅绑定安全输入(SafeOutputSize = 0),适用于纯安全输入设备(如 EL1904)。
BindSafeOutput<TOut>()
template<typename TOut>
bool BindSafeOutput(uint16_t safetyAddress,
std::function<void(TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100, uint16_t connectionId = 0);
仅绑定安全输出(SafeInputSize = 0),适用于纯安全输出设备(如 EL2904)。
BindMdpSafeIO<TIn, TOut>()
// 自动偏移(从 DENI PDO 配置自动计算)
template<typename TIn, typename TOut>
bool BindMdpSafeIO(uint16_t safetyAddress,
std::function<void(const TIn&, TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100);
// 显式偏移(用户指定模块 PDO 偏移)
template<typename TIn, typename TOut>
bool BindMdpSafeIO(uint16_t safetyAddress,
uint32_t pdoInputOffset, uint32_t pdoOutputOffset,
std::function<void(const TIn&, TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100);
MDP 多连接绑定。每次调用添加一个独立 FSoE 连接,自动从结构体推断安全数据大小。所有连接在首个 PDO 周期自动启动,无需手动调用。
自动偏移版本通过 slave.GetMDP()->GetModulePdoLayout() 从 DENI 的 PDO Assignment/Mapping 配置自动计算每个模块在 IOmap 中的偏移,按调用顺序依次分配(对应槽位顺序)。
示例:
// 自动偏移 + exchange 回调(推荐)
fsoe.BindMdpSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0100,
[](const SafeDigitalInput& input, SafeDigitalOutput& output) {
output.OutputBits = input.InputBits;
});
fsoe.BindMdpSafeIO<SafeDigitalInput, SafeDigitalOutput>(0x0200);
// 显式偏移 — 用户指定模块 PDO 偏移
fsoe.BindMdpSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0100, slave.Ioffset(), slave.Ooffset());
fsoe.BindMdpSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0200, slave.Ioffset() + 7, slave.Ooffset() + 6);
自动偏移依赖 slave.GetMDP()->GetModulePdoLayout(),该方法通过 CoE SDORead 读取 PDO Assignment (0x1C12/0x1C13) 和 PDO Mapping 条目,累加各 Entry 的 BitLen 计算模块大小,再按槽位顺序累积为字节偏移(相对于 slave.Ioffset()/slave.Ooffset())。
需要从站已完成 DENI 配置(ConfigMap 后),不依赖 ESI 文件。如果 MDP 模块未检测到或 CoE 不可用,自动偏移将回退到从站基础偏移,此时应使用显式偏移版本。
BindMdpSafeInput<TIn>() / BindMdpSafeOutput<TOut>()
template<typename TIn>
bool BindMdpSafeInput(uint16_t safetyAddress,
std::function<void(const TIn&)> exchange = nullptr,
uint32_t watchdogMs = 100);
template<typename TOut>
bool BindMdpSafeOutput(uint16_t safetyAddress,
std::function<void(TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100);
MDP 多连接绑定(仅安全输入 / 仅安全输出),自动偏移。
BindMdpDriveAxis<TIn, TOut>()
// 自动偏移
template<typename TIn, typename TOut>
bool BindMdpDriveAxis(int axisNumber, uint16_t safetyAddress,
std::function<void(const TIn&, TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100);
// 显式偏移
template<typename TIn, typename TOut>
bool BindMdpDriveAxis(int axisNumber, uint16_t safetyAddress,
uint32_t pdoInputOffset, uint32_t pdoOutputOffset,
std::function<void(const TIn&, TOut&)> exchange = nullptr,
uint32_t watchdogMs = 100);
MDP 驱动轴安全连接绑定。每轴一个独立 FSoE 连接。首个 PDO 周期自动启动。
示例:
fsoe.BindMdpDriveAxis<SafeDriveInput, SafeDriveOutput>(
0, 0x0100, // axisNumber, safetyAddress
[](const SafeDriveInput& input, SafeDriveOutput& output) {
if ((input.SafetyStatusWord & 0x0100) != 0)
output.SafetyControlWord |= 0x0080; // 确认故障
});
fsoe.BindMdpDriveAxis<SafeDriveInput, SafeDriveOutput>(1, 0x0200);
能力检测
auto& fsoe = master.GetSlave(1).GetFSoE();
if (!fsoe.IsCapable()) {
printf("从站不支持 FSoE\n");
return;
}
属性
| 属性 | 类型 | 说明 |
|---|---|---|
| IsInitialized() | bool | 是否已初始化 |
| State() | FSoEState | 当前 FSoE 状态 |
| InFailsafe() | bool | 是否处于失效安全模式 |
| WatchdogExpired() | bool | 看门狗是否过期 |
| LastError() | FSoEError | 最后的错误代码 |
| ConnectionStatus() | FSoEConnectionStatus | 连接状态详情(通信统计) |
| SafetyAddress() | uint16_t | 安全地址(缓存值) |
| ConnectionId() | uint16_t | 连接 ID(缓存值) |
| SafeInputSize() | int | 安全输入数据大小(字节) |
| CrcErrorCount() | uint32_t | CRC 错误计数 |
FSoEState 枚举
enum class FSoEState : int {
Reset = 0x100, // 初始/重置状态
Session = 0x101, // 会话建立
Connection = 0x102, // 连接建立
Parameter = 0x103, // 参数下载
Data = 0x104, // 数据交换(正常工作)
Failsafe = 0x105 // 失效安全
};
FSoEError 枚举
enum class FSoEError : int {
None = 0x0000, // 无错误
WrongCommand = 0x0001, // 错误的命令
UnknownCommand = 0x0002, // 未知命令
WrongConnectionId = 0x0003, // 连接ID不匹配
CrcError = 0x0004, // CRC校验失败
Watchdog = 0x0005, // 看门狗超时
WrongAddress = 0x0006, // 错误的FSoE地址
WrongData = 0x0007, // 无效数据
CommParamLength = 0x0008, // 通信参数长度错误
CommParam = 0x0009, // 通信参数错误
AppParamLength = 0x000A, // 应用参数长度错误
AppParam = 0x000B, // 应用参数错误
UnexpectedSession = 0x000C, // 意外的会话命令
FailsafeData = 0x000D, // 收到失效安全数据
NotInitialized = 0x0100, // FSoE未初始化(内部错误)
MaxConnections = 0x0101, // 达到最大连接数(内部错误)
InvalidStateTransition = 0x0102 // 无效状态转换(内部错误)
};
FSoEConnectionStatus
| 字段 | 类型 | 说明 |
|---|---|---|
| State | FSoEState | 当前 FSoE 状态 |
| LastError | FSoEError | 最后的错误代码 |
| ErrorCount | uint32_t | 总错误计数 |
| FramesSent | uint32_t | 发送帧数 |
| FramesReceived | uint32_t | 接收有效帧数 |
| CrcErrors | uint32_t | CRC 错误计数 |
| WatchdogErrors | uint32_t | 看门狗超时计数 |
| WatchdogExpired | bool | 看门狗是否过期 |
| InFailsafe | bool | 是否处于失效安全模式 |
| IsInitialized | bool | 连接是否已初始化 |
FSoEConnectionConfig
| 字段 | 类型 | 说明 |
|---|---|---|
| ConnectionId | uint16_t | 唯一连接标识符 |
| SafetyAddress | uint16_t | FSoE 从站安全地址(拨码开关设定) |
| WatchdogTimeMs | uint32_t | 看门狗超时(毫秒),默认 100 |
| SafeInputSize | uint16_t | 安全输入数据大小(字节),由设备决定 |
| SafeOutputSize | uint16_t | 安全输出数据大小(字节),由设备决定 |
| PdoInputOffset | uint32_t | IOmap 中输入偏移(通常用 slave.Ioffset()) |
| PdoOutputOffset | uint32_t | IOmap 中输出偏移(通常用 slave.Ooffset()) |
FSoEFailsafeReason 枚举
enum class FSoEFailsafeReason {
WatchdogTimeout, // 看门狗超时
CrcError, // CRC错误
CommunicationError, // 通信错误
ApplicationRequest, // 应用请求
SlaveRequest, // 从站请求
MasterRequest, // 主站请求
RecoveryToData // 恢复到数据模式
};
安全数据结构
使用 Darra EtherCAT Master Tools 的代码导出功能,可一键生成安全数据结构体和绑定代码。 导出结果包含正确的结构体大小、字段布局和 FSoE 绑定调用,避免手动定义出错。
结构体定义
安全数据结构体示例:
#pragma pack(push, 1)
// EL19xx 数字量安全输入(2 字节安全数据)
struct SafeDigitalInput {
uint8_t InputBits; // 安全输入状态位(每位对应一个通道)
uint8_t DiagBits; // 输入诊断状态
};
// EL29xx 数字量安全输出(1 字节安全数据)
struct SafeDigitalOutput {
uint8_t OutputBits; // 安全输出控制位
};
// ETG.6100 安全驱动输入(10 字节安全数据)
struct SafeDriveInput {
uint16_t SafetyStatusWord; // 安全状态字
int32_t SafeActualPosition; // 安全实际位置
int32_t SafeActualVelocity; // 安全实际速度
};
// ETG.6100 安全驱动输出(2 字节安全数据)
struct SafeDriveOutput {
uint16_t SafetyControlWord; // 安全控制字
};
// 安全编码器输入(6 字节安全数据)
struct SafeEncoderInput {
uint32_t SafePosition; // 安全位置值
uint16_t StatusWord; // 状态字
};
#pragma pack(pop)
SafeInputSize / SafeOutputSize 必须与 sizeof(T) 一致,否则读写会失败。
状态控制
RequestState()
bool RequestState(FSoEState targetState) const;
请求 FSoE 状态转换。自动验证状态转换有效性。
参数:
targetState(FSoEState) — 目标状态
返回值:
bool— 成功返回true
EnterFailsafe()
bool EnterFailsafe() const;
主动进入失效安全模式。从站将输出安全值(由 SetFailsafeOutput 预设)。
Reset()
bool Reset() const;
重置 FSoE 连接到初始状态,重新开始状态机。
Close()
void Close();
关闭 FSoE 连接,释放资源。
连接管理
Initialize()
bool Initialize(uint16_t connectionId, uint16_t safetyAddress,
uint32_t watchdogMs = 10);
bool Initialize(const FSoEConnectionConfig& config);
初始化 FSoE 连接。
参数:
connectionId— 连接 ID(唯一标识)safetyAddress— 从站安全地址(硬件拨码)watchdogMs— 看门狗超时(毫秒)
参数和配置
DownloadParameters()
std::optional<uint32_t> DownloadParameters(const uint8_t* data, uint32_t size) const;
下载安全参数。返回 SRA CRC32 校验值。
仅在 Parameter 状态下可调用。参数内容由安全配置工具生成。
参数:
data(const uint8_t*) — 参数数据size(uint32_t) — 数据大小
返回值:
std::optional<uint32_t>— 成功返回 SRA CRC32,失败返回std::nullopt
SetFailsafeOutput()
bool SetFailsafeOutput(const uint8_t* data, uint32_t size) const;
预设失效安全时的输出值。当 FSoE 进入 Failsafe 状态时,从站自动切换到此值。
示例:
// 失效安全时所有输出关闭
uint8_t failsafe[] = {0x00};
fsoe.SetFailsafeOutput(failsafe, sizeof(failsafe));
// 或使用结构体
SafeDriveOutput fs{};
fs.SafetyControlWord |= 0x0001; // 请求 STO(安全扭矩关闭)
fsoe.SetFailsafeOutput(reinterpret_cast<const uint8_t*>(&fs), sizeof(fs));
安全 IO 数据
ReadSafeInput()
std::vector<uint8_t> ReadSafeInput(int size) const;
读取安全输入数据。
WriteSafeOutput()
bool WriteSafeOutput(const uint8_t* data, int size) const;
写入安全输出数据。
示例:
auto& fsoe = master.GetSlave(1).GetFSoE();
// 读取安全输入
auto input = fsoe.ReadSafeInput(2);
if (!input.empty()) {
bool ch0 = (input[0] & 0x01) != 0;
printf("安全输入 CH0: %s\n", ch0 ? "高" : "低");
}
// 写入安全输出
uint8_t output[] = {0x01};
fsoe.WriteSafeOutput(output, sizeof(output));
高级状态查询
auto& fsoe = master.GetSlave(1).GetFSoE();
// FSoE 协议状态
FSoEState state = fsoe.State();
// 是否处于失效安全模式
bool failsafe = fsoe.InFailsafe();
// 看门狗是否超时
bool wd_expired = fsoe.WatchdogExpired();
// CRC 错误计数
uint32_t crc_errors = fsoe.CrcErrorCount();
// 最后的错误代码
FSoEError err = fsoe.LastError();
// 获取完整连接状态
FSoEConnectionStatus cs = fsoe.ConnectionStatus();
printf("状态=%d, CRC错误=%u, 失效安全=%s\n",
static_cast<int>(cs.State), cs.CrcErrors,
cs.InFailsafe ? "是" : "否");
// 获取旧版状态结构体
auto status = fsoe.GetStatus(); // std::optional<FSoEStatus>
if (status) {
printf("状态: %d\n", status->State);
printf("失效安全: %s\n", status->FailsafeActive ? "是" : "否");
printf("CRC 错误: %u\n", status->CrcErrors);
printf("看门狗错误: %u\n", status->WatchdogErrors);
}
诊断
ClearError()
void ClearError() const;
清除 FSoE 错误。清除后可尝试重新建立连接。
GetFailsafeReason()
FSoEFailsafeReason GetFailsafeReason() const;
获取失效安全触发原因。根据最后的错误代码推断原因。
周期处理
在 PDO 回调中调用:
fsoe.ProcessCycle(); // 统一周期处理
事件回调
auto& fsoe = master.GetSlave(1).GetFSoE();
// 状态变化回调
fsoe.OnStateChanged = [](FSoEState oldState, FSoEState newState) {
printf("FSoE 状态: %d -> %d\n",
static_cast<int>(oldState), static_cast<int>(newState));
};
// 错误回调
fsoe.OnError = [](FSoEError error) {
printf("FSoE 错误: 0x%04X\n", static_cast<int>(error));
};
// 失效安全回调
fsoe.OnFailsafe = [](FSoEFailsafeReason reason) {
printf("FSoE 失效安全触发\n");
};
// 安全数据更新回调
fsoe.OnSafeDataUpdated = [](const uint8_t* data, int size) {
printf("安全数据更新: %d 字节\n", size);
};
回调类型定义:
| 回调 | 类型 | 说明 |
|---|---|---|
| OnStateChanged | std::function<void(FSoEState, FSoEState)> | 状态变化 |
| OnError | std::function<void(FSoEError)> | 错误 |
| OnFailsafe | std::function<void(FSoEFailsafeReason)> | 失效安全触发 |
| OnSafeDataUpdated | std::function<void(const uint8_t*, int)> | 安全数据更新 |
MDP 多连接模式
MDP 设备(安全 IO 耦合器、多轴驱动器等)包含多个安全模块。多连接模式为每个模块创建独立的 FSoE 连接,各连接有独立的状态机、看门狗和安全数据缓冲区。
- 单连接 (
BindSafeIO) — 简单设备、单轴驱动器 - 多连接 (
BindMdpSafeIO) — 模块需要独立监控/恢复时(如多轴驱动器各轴独立安全连接)
对于支持 MDP(模块化设备配置文件)的从站,可以管理多个独立的 FSoE 连接:
auto& fsoe = master.GetSlave(1).GetFSoE();
// 初始化 MDP 连接
FSoEMdpConfig cfg{};
cfg.ConnectionId = 1;
cfg.SafetyAddress = 0x0100;
cfg.WatchdogTimeMs = 100;
cfg.SafeInputSize = 2;
cfg.SafeOutputSize = 1;
cfg.PdoInputOffset = 0;
cfg.PdoOutputOffset = 0;
fsoe.MdpInitConnection(0, cfg);
// 获取 MDP 连接状态
int state = fsoe.MdpGetState(0);
// 获取 MDP 连接完整状态
auto mdpStatus = fsoe.MdpGetStatus(0); // std::optional<FSoEConnectionStatus>
if (mdpStatus) {
printf("MDP连接0: 状态=%d, CRC错误=%u\n",
static_cast<int>(mdpStatus->State), mdpStatus->CrcErrors);
}
// 读取/写入 MDP 安全 IO
auto mdp_input = fsoe.MdpReadSafeInput(0, 2);
uint8_t mdp_out[] = {0x01};
fsoe.MdpWriteSafeOutput(0, mdp_out, sizeof(mdp_out));
// MDP 请求状态转换
fsoe.MdpRequestState(0, FSoEState::Data);
// MDP 下载安全参数
uint8_t params[] = {0x01, 0x02};
auto crc = fsoe.MdpDownloadParameters(0, params, sizeof(params));
// MDP 设置失效安全输出
uint8_t fs_out[] = {0x00};
fsoe.MdpSetFailsafeOutput(0, fs_out, sizeof(fs_out));
// MDP 检查看门狗
bool wd_ok = fsoe.MdpCheckWatchdog(0);
// MDP 获取最后错误 / 清除错误
FSoEError mdpErr = fsoe.MdpGetLastError(0);
fsoe.MdpClearError(0);
// 重置 MDP 连接
fsoe.MdpReset(0);
// 关闭 MDP 连接
fsoe.MdpCloseConnection(0);
MDP 检测与查询
auto& fsoe = master.GetSlave(1).GetFSoE();
// 检测从站支持的 FSoE 连接数
uint16_t connCount = fsoe.MdpDetectConnections();
printf("FSoE 连接数: %u\n", connCount);
// 获取从站的 FSoE 连接数量
uint16_t slaveConnCount = fsoe.MdpGetSlaveConnectionCount();
// 读取设备级安全地址 (0xF980:01)
auto devAddr = fsoe.MdpGetDeviceAddress();
if (devAddr) printf("设备安全地址: 0x%04X\n", *devAddr);
// 读取模块通信参数
auto commParam = fsoe.MdpGetModuleCommParam(0);
// 读取模块诊断数据
auto diag = fsoe.MdpGetModuleDiagnosis(0);
if (diag) {
printf("模块0: 状态=%d, 失效安全=%s\n",
static_cast<int>(diag->State), diag->InFailsafe ? "是" : "否");
}
FSoE 管理器
FSoEManager 类用于管理多个 FSoE 连接:
using namespace darra;
FSoEManager mgr(dll, master.MasterNumber());
// 添加连接
auto& conn1 = mgr.AddConnection(1);
auto& conn2 = mgr.AddConnection(2);
// 初始化所有连接
mgr.InitializeAll();
// 全局安全状态检查
if (mgr.AllInDataState()) {
printf("所有连接处于 Data 状态\n");
}
// 是否有连接处于失效安全
if (mgr.AnyInFailsafe()) {
printf("有连接处于失效安全状态\n");
}
// 周期处理 (在 PDO 回调中调用)
mgr.ProcessCycle();
// 获取诊断
auto diags = mgr.GetDiagnostics();
// 关闭所有连接
mgr.CloseAll();
FSoEManager 完整方法
AddConnection(slaveIndex)(FSoE&) — 添加连接ConnectionCount()(size_t) — 连接数量GetConnection(index)(FSoE&) — 获取指定连接InitializeAll()(bool) — 初始化所有连接CloseAll()(void) — 关闭所有连接AllInDataState()(bool) — 所有连接是否在 Data 状态AnyInFailsafe()(bool) — 是否有连接处于失效安全ProcessCycle()(void) — 周期处理GetDiagnostics()(std::vector<FSoEModuleDiag>) — 全局诊断FindByAddress(safetyAddress)(FSoE*) — 按安全地址查找FindByConnectionId(id)(FSoE*) — 按连接 ID 查找FindConnectionByAddress(addr)(FSoE*) — 按安全地址查找(别名)GetStatusSummary()(std::string) — 状态摘要字符串WriteOutputFrame(connIdx, data)(bool) — 写入原始输出帧ReadInputFrame(connIdx)(std::vector<uint8_t>) — 读取原始输入帧BindSafeIO(...)(bool) — 绑定安全输入输出BindSafeInput(...)(bool) — 绑定安全输入BindSafeOutput(...)(bool) — 绑定安全输出BindMdpSafeInput(...)(bool) — MDP 绑定安全输入BindMdpSafeOutput(...)(bool) — MDP 绑定安全输出BindMdpDriveAxis(...)(bool) — MDP 驱动轴安全绑定
FSoEConnectionMode 枚举
enum class FSoEConnectionMode {
Single, // 单连接模式 - 所有模块共用一个 FSoE 连接
Multiple // 多连接模式 - 每个模块独立 FSoE 连接
};
FSoEModuleProfile 枚举
enum class FSoEModuleProfile : uint16_t {
DigitalInput = 190, // FSoE 数字量输入
DigitalInOut = 195, // FSoE 数字量输入/输出
DigitalOutput = 290, // FSoE 数字量输出
DriveConnection = 790, // FSoE 驱动连接 (CiA402)
Master = 6900 // FSoE 主站模块
};
完整示例
数字量安全 IO
#include "ethercat.hpp"
using namespace darra;
int main() {
EtherCATMaster master(dll);
master.SetNetwork("\\Device\\NPF_{...}")
.SetENI("config.deni")
.Build();
auto& safeInput = master.GetSlave(1); // EL1904 安全输入
auto& safeOutput = master.GetSlave(2); // EL2904 安全输出
// 一行绑定 + exchange 回调
SafeDigitalInput lastInput{};
safeInput.GetFSoE().BindSafeInput<SafeDigitalInput>(
0x0100,
[&lastInput](const SafeDigitalInput& input) {
lastInput = input; // 捕获最新输入
});
safeOutput.GetFSoE().BindSafeOutput<SafeDigitalOutput>(
0x0200,
[&lastInput](SafeDigitalOutput& output) {
output.OutputBits = lastInput.InputBits; // 输入直通输出
});
// 事件(自动触发)
safeInput.GetFSoE().OnFailsafe = [](FSoEFailsafeReason reason) {
printf("安全输入 失效安全!\n");
};
safeOutput.GetFSoE().OnError = [](FSoEError error) {
printf("安全输出 错误: 0x%04X\n", static_cast<int>(error));
};
// 预设失效安全输出
uint8_t failsafe[] = {0x00};
safeOutput.GetFSoE().SetFailsafeOutput(failsafe, sizeof(failsafe));
master.SetState(EcState::OP);
master.Start();
getchar();
return 0;
}
安全驱动器
auto& drive = master.GetSlave(1); // 带 FSoE 的伺服驱动器
// 一行绑定 + exchange 回调
drive.GetFSoE().BindSafeIO<SafeDriveInput, SafeDriveOutput>(
0x0100,
[](const SafeDriveInput& driveIn, SafeDriveOutput& driveOut) {
if ((driveIn.SafetyStatusWord & 0x0100) != 0) // Bit8: 安全故障
{
bool sto = (driveIn.SafetyStatusWord & 0x0001) != 0;
printf("安全故障: STO=%d, 位置=%d\n", sto, driveIn.SafeActualPosition);
driveOut.SafetyControlWord |= 0x0080; // 确认故障
}
else
{
driveOut.SafetyControlWord &= ~0x0001; // 清除 STO 请求
driveOut.SafetyControlWord &= ~0x0080; // 清除故障确认
}
});
drive.GetFSoE().OnFailsafe = [](FSoEFailsafeReason reason) {
printf("驱动器安全停止!\n");
};
多连接 IO(MDP 模式)
每个模块有独立 FSoE 连接,可独立监控状态和错误。PDO 偏移从 DENI PDO 配置自动计算:
#pragma pack(push, 1)
struct SafeDigitalInput {
uint8_t InputBits;
uint8_t DiagBits;
};
struct SafeDigitalOutput {
uint8_t OutputBits;
};
#pragma pack(pop)
auto& coupler = master.GetSlave(1); // 安全 IO 耦合器,2 个安全模块
// 绑定 2 个独立连接 + exchange 回调(自动计算 PDO 偏移)
coupler.GetFSoE().BindMdpSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0100,
[](const SafeDigitalInput& input, SafeDigitalOutput& output) {
output.OutputBits = input.InputBits;
});
coupler.GetFSoE().BindMdpSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0200,
[](const SafeDigitalInput& input, SafeDigitalOutput& output) {
output.OutputBits = input.InputBits;
});
// 首个 PDO 周期自动启动所有连接
// 状态变化回调
coupler.GetFSoE().OnStateChanged = [](FSoEState oldState, FSoEState newState) {
printf("FSoE: %d -> %d\n",
static_cast<int>(oldState), static_cast<int>(newState));
};
多轴安全驱动器(MDP 模式)
#pragma pack(push, 1)
struct SafeDriveInput {
uint16_t SafetyStatusWord;
int32_t SafeActualPosition;
int32_t SafeActualVelocity;
};
struct SafeDriveOutput {
uint16_t SafetyControlWord;
};
#pragma pack(pop)
auto& drive = master.GetSlave(1); // 双轴伺服驱动器
// 共用 exchange 逻辑
auto driveExchange = [](const SafeDriveInput& input, SafeDriveOutput& output) {
if ((input.SafetyStatusWord & 0x0100) != 0) // 安全故障
output.SafetyControlWord |= 0x0080; // 确认故障
else
output.SafetyControlWord &= ~0x0001; // 清除 STO
};
drive.GetFSoE().BindMdpDriveAxis<SafeDriveInput, SafeDriveOutput>(
0, 0x0100, driveExchange);
drive.GetFSoE().BindMdpDriveAxis<SafeDriveInput, SafeDriveOutput>(
1, 0x0200, driveExchange);
// 首个 PDO 周期自动启动
// 每轴独立监控
drive.GetFSoE().OnFailsafe = [](FSoEFailsafeReason reason) {
printf("失效安全触发!\n");
};
FSoE ConnID 校验 (ETG.5120 §5.2.3)
FSoE 协议为每个安全连接分配唯一的 16 位 Connection ID (ConnID),用于区分同一总线上的多条安全连接。主站在创建新连接前必须校验 ConnID 是否可用,避免与已存在的连接冲突导致 CRC 或状态机错误。
IsConnectionIdAvailable()
class FSoE {
public:
static bool IsConnectionIdAvailable(uint16_t conn_id);
};
检测指定 ConnID 是否可用(未被任何活动连接占用)。这是一个静态方法,跨从站全局生效。
参数:
conn_id(uint16_t) — 待校验的 Connection ID
返回值:
bool—true表示可用,false表示已被占用
示例:
// 查找第一个可用 ConnID(从 1 开始)
uint16_t allocId = 0;
for (uint16_t id = 1; id < 0x1000; ++id) {
if (FSoE::IsConnectionIdAvailable(id)) {
allocId = id;
break;
}
}
if (allocId == 0) {
std::cerr << "无可用 ConnID\n";
return -1;
}
std::cout << "分配 ConnID: 0x" << std::hex << allocId << "\n";
// 创建 FSoE 连接
auto& fsoe = master.GetSlave(1).GetFSoE();
fsoe.SetConnectionId(allocId);
fsoe.BindSafeIO<SafeDigitalInput, SafeDigitalOutput>(
0x0100, [](const auto& in, auto& out) { /* ... */ });
- ConnID 0x0000 保留,不应使用
- 同一 FSoE 主站下所有连接的 ConnID 必须唯一
- 推荐从 0x0001 开始线性分配,或使用 ConnID == SafetyAddress 的简化策略
- SDK 内部已维护 ConnID 注册表,
IsConnectionIdAvailable()直接查询该表
ETG.5120 §5.2.3 FSoE Connection ID 管理规则。
FSoE CRC-16 与 PDO 帧 (ETG.5120 §5.2)
底层 FSoE 协议引擎已自动校验 / 计算 CRC,用户无需直接接触;下面这组 API 仅在以下场景下使用:
- 自研 FSoE 主站 / 从站 stack 时,做单元测试比对参考实现
- 解析 PCAP 抓包,离线还原帧字段并复算 CRC
- 与第三方安全 PLC 互通时排查"真实 CRC 与从站期望 CRC 不一致"的问题
IFSoECrc16 接口
class IFSoECrc16 {
public:
virtual ~IFSoECrc16() = default;
/// 计算缓冲区的 CRC-16(不依赖 ConnectionID)
virtual uint16_t Compute(const uint8_t* data, size_t length) const = 0;
/// 计算带 ConnectionID 种子的 CRC-16(FSoE 标准用法)
virtual uint16_t ComputeWithConnectionId(const uint8_t* data, size_t length,
uint16_t connectionId) const = 0;
};
CRC-16 抽象接口,对齐 C# IFSoECrc16。允许应用提供自己的实现来 mock / 替换默认 CCITT-FALSE。
FSoECrc16 类 (默认 CCITT-FALSE)
class FSoECrc16 : public IFSoECrc16 {
public:
/// 默认构造 = CCITT-FALSE: poly=0x1021, init=0xFFFF, xorOut=0x0000, connId=0
FSoECrc16(uint16_t polynomial = 0x1021,
uint16_t initialValue = 0xFFFF,
uint16_t xorOutput = 0x0000,
uint16_t connectionId = 0);
/// 全局共享的 CCITT-FALSE 单例 (无 ConnectionID 种子)
static const FSoECrc16& CcittFalse();
/// 工厂: 携带 ConnectionID 的 CRC-16 实例
static FSoECrc16 CreateWithConnectionId(uint16_t connectionId);
uint16_t Compute(const uint8_t* data, size_t length) const override;
uint16_t ComputeWithConnectionId(const uint8_t* data, size_t length,
uint16_t connectionId) const override;
};
参数对应 ETG.5120 §5.2 规定的 CRC-16/CCITT-FALSE:多项式 0x1021、初值 0xFFFF、不反转输入输出、不 xor-out。connectionId 非 0 时 CRC 会把它作为初始种子注入运算(FSoE 标准做法),等价于 C# FSoECrc16.CreateWithConnectionId(...)。
示例:
using darra::ethercat::FSoECrc16;
using darra::ethercat::IFSoECrc16;
// 用全局单例做最简单的 CCITT-FALSE 校验
const IFSoECrc16& crc = FSoECrc16::CcittFalse();
uint16_t v = crc.Compute(payload.data(), payload.size());
// 给特定连接算 CRC (FSoE 标准用法)
auto crc2 = FSoECrc16::CreateWithConnectionId(0x0042);
uint16_t safeCrc = crc2.ComputeWithConnectionId(payload.data(), payload.size(), 0x0042);
FSoEPdoFrame 与 FSoEPdoLayout
FSoEPdoFrame / FSoEPdoLayout 用于在用户态拼装 / 解析 FSoE PDO 帧(CommandByte + 数据段 + CRC 段 + ConnectionID)。对齐 C# FSoEPdoFrame / FSoEPdoLayout。
struct FSoEPdoLayout {
uint16_t safe_data_size; // 安全数据字节数
/// 计算理论上的 CRC 段数量
int GetCrcSegmentCount() const noexcept;
};
class FSoEPdoFrame {
public:
uint8_t command; // FSoE Command byte
std::vector<uint8_t> safe_data; // 安全数据段
std::vector<uint16_t> crc_segments; // 每段 CRC-16 (按 layout 切分)
uint16_t connection_id; // ConnectionID
/// 序列化为字节流 (传 IFSoECrc16* 自动重算 CRC, nullptr 直接用现有值)
std::vector<uint8_t> ToBytes(const FSoEPdoLayout& layout,
const IFSoECrc16* crc = nullptr);
/// 从原始字节流构建 FSoEPdoFrame (传 IFSoECrc16* 顺手做 CRC 校验)
static FSoEPdoFrame FromBytes(const uint8_t* data, size_t data_size,
const FSoEPdoLayout& layout,
const IFSoECrc16* crc = nullptr);
/// 用给定 CRC 校验所有 CRC 段
bool ValidateCrcSegments(const IFSoECrc16& crc) const;
};
示例 (拼装一帧再发出去):
using namespace darra::ethercat;
FSoEPdoLayout layout{ /*safe_data_size*/ 6 };
FSoEPdoFrame f;
f.command = 0x05; // FSoE Data Command
f.safe_data = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
f.connection_id = 0x0042;
// 让 CRC 引擎按 layout 切片重算所有 CRC 段
auto crc = FSoECrc16::CreateWithConnectionId(f.connection_id);
auto bytes = f.ToBytes(layout, &crc);
// ... 把 bytes 写入 PDO 输出区 ...
示例 (从 PCAP 解析并校验):
const IFSoECrc16& crcSpec = FSoECrc16::CcittFalse();
auto frame = FSoEPdoFrame::FromBytes(buf, len, layout, &crcSpec);
if (!frame.ValidateCrcSegments(crcSpec)) {
printf("FSoE CRC 校验失败, 帧不合法\n");
}
正常运行时 BindSafeIO / BindMdpSafeIO 已经把 FSoEPdoFrame 隐藏在协议引擎里。手动 ToBytes 后写 IOmap 会绕过状态机和看门狗,会让从站跳到 Failsafe。这两个类只用于离线分析与单元测试。
ETG.5120 §5.2 FSoE 帧格式; ETG.5120 §5.2.5 CRC 计算规则。