跳到主要内容

FSoE 功能安全

FSoE (Functional Safety over EtherCAT) 安全通信接口,在标准 PDO 通道上叠加安全协议层,实现 SIL3/PLe 等级的功能安全通信。

通过 slave.GetFSoE() 获取 FSoE& 引用。

FSoE 设备检测

slave.GetFSoE() 在从站初始化时自动检测。检测过程:

  1. CoE 前置条件 — 从站必须支持 CoE 邮箱协议
  2. 0xF980:01 检测 — 尝试 SDO 读取设备级 FSoE 安全地址,成功则确认支持
  3. 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 周期时序

每个 PDO 周期,系统自动执行:

  1. 周期开始 — 刷新所有 FSoE 从站的安全输入缓冲区、检查状态变化并触发事件
  2. DataExchange 回调 — 从站级别回调,读取输入(已是最新数据),修改输出
  3. ProcessDataCyclicSync 回调 — 主站级别回调(普通 PDO 读写)
  4. 周期结束 — 提交所有 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) — 看门狗超时(毫秒),默认 100
  • connectionId (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);
PDO 偏移自动计算

自动偏移依赖 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_tCRC 错误计数
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
字段类型说明
StateFSoEState当前 FSoE 状态
LastErrorFSoEError最后的错误代码
ErrorCountuint32_t总错误计数
FramesSentuint32_t发送帧数
FramesReceiveduint32_t接收有效帧数
CrcErrorsuint32_tCRC 错误计数
WatchdogErrorsuint32_t看门狗超时计数
WatchdogExpiredbool看门狗是否过期
InFailsafebool是否处于失效安全模式
IsInitializedbool连接是否已初始化
FSoEConnectionConfig
字段类型说明
ConnectionIduint16_t唯一连接标识符
SafetyAddressuint16_tFSoE 从站安全地址(拨码开关设定)
WatchdogTimeMsuint32_t看门狗超时(毫秒),默认 100
SafeInputSizeuint16_t安全输入数据大小(字节),由设备决定
SafeOutputSizeuint16_t安全输出数据大小(字节),由设备决定
PdoInputOffsetuint32_tIOmap 中输入偏移(通常用 slave.Ioffset())
PdoOutputOffsetuint32_tIOmap 中输出偏移(通常用 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 状态转换。自动验证状态转换有效性。

参数:

返回值:

  • 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);
};

回调类型定义:

回调类型说明
OnStateChangedstd::function<void(FSoEState, FSoEState)>状态变化
OnErrorstd::function<void(FSoEError)>错误
OnFailsafestd::function<void(FSoEFailsafeReason)>失效安全触发
OnSafeDataUpdatedstd::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

返回值:

  • booltrue 表示可用,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 分配建议
  • 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 计算规则。