跳到主要内容

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 — 失效安全,从站输出安全值
自动状态推进

Initialize() 建立连接后,中间状态(Session → Connection → Parameter)由 DLL 自动推进;每个 PDO 周期调用 ProcessCycle() 驱动数据交换。

模板族 BindSafeIO<TIn,TOut>() 当前未实现

C++ FSoE当前不提供 BindSafeIO<TIn,TOut>() / BindSafeInput<TIn>() / BindSafeOutput<TOut>() / BindMdpSafeIO<>() 等基于结构体类型推断的模板方法 (这些是 C# SDK 的能力,C++ 尚未落地)。下方"最简示例"及"简化绑定"章节中所有 fsoe.BindSafeIO<...>(...) 形式的代码反映的是规划中的目标 API,不是当前可调用接口。

C++ 当前真实可用路径:fsoe.Initialize(...) 建连接 → PDO 周期回调里 fsoe.ReadSafeInput(size) / fsoe.WriteSafeOutput(data, size) + fsoe.ProcessCycle()。 多连接走 SafetyManager非模板 BindSafeInput() / BindSafeOutput() / BindSafeIO() 方法(参数为 slaveIndex / 字节大小,非类型推断)。

推荐方式

使用 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);  // 带 FSoE 的安全 IO 从站
auto& fsoe = slave.GetFSoE();

// 1. 建立 FSoE 连接 (connectionId=0 自动分配, safetyAddress 为硬件拨码, 看门狗 100ms)
if (!fsoe.Initialize(/*connectionId*/ 0, /*safetyAddress*/ 0x0100, /*watchdogMs*/ 100)) {
printf("FSoE 初始化失败\n");
return;
}

// 2. 订阅错误事件 (OnError 是真实成员回调)
fsoe.OnError = [](FSoEError error) {
printf("FSoE 错误: 0x%04X\n", static_cast<int>(error));
};

// 3. 在 PDO 周期回调里读写安全数据并推进状态机
master.Events().SetPDOCallbackSync([&](uint16_t mi) {
auto inBytes = fsoe.ReadSafeInput(sizeof(SafeDigitalInput));
if (inBytes.size() == sizeof(SafeDigitalInput)) {
SafeDigitalInput input;
std::memcpy(&input, inBytes.data(), sizeof(input));

SafeDigitalOutput output{};
bool ch0 = (input.InputBits & 0x01) != 0;
output.OutputBits = ch0 ? 0x01 : 0x00;

fsoe.WriteSafeOutput(reinterpret_cast<const uint8_t*>(&output), sizeof(output));
}
fsoe.ProcessCycle(); // 驱动 FSoE 状态机推进
});
PDO 周期时序

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

  1. 周期开始 — 刷新所有 FSoE 从站的安全输入缓冲区、检查状态变化并触发事件
  2. DataExchange 回调 — 从站级别回调,读取输入(已是最新数据),修改输出
  3. ProcessDataCyclicSync 回调 — 主站级别回调(普通 PDO 读写)
  4. 周期结束 — 提交所有 FSoE 从站的安全输出缓冲区到 DLL

FSoE 与普通 PDO 的区别

FSoE 安全数据不能像普通 PDO 那样直接用零拷贝指针映射。原因在于协议层的差异:

使用体验对比

尽管底层机制不同,Initialize() + 周期内 ReadSafeInput() / WriteSafeOutput() / ProcessCycle() 的使用体验已经接近普通 PDO 的零拷贝读写。

普通 PDO

IOmap 中的数据就是用户数据,结构体直接叠加到内存指针即可。

FSoE PDO

IOmap 中存放的是 FSoE 协议帧,用户的安全数据被包裹在帧内部,且需要经过校验。

SDK 的 FSoE 协议引擎负责:

  • CRC 校验 — 验证输入帧完整性、计算输出帧 CRC
  • 状态机管理 — Session → Connection → Parameter → Data 自动推进
  • 看门狗 — 监控通信超时,超时自动进入 Failsafe
  • 序列号 — 检测帧丢失和重复

绕过 DLL 直接读写 IOmap 会破坏安全协议完整性,因此 FSoE 必须通过独立缓冲区访问。

简化绑定

当前 C++ SDK 未实现 BindSafeIO 模板族

下面的 BindSafeIO<TIn,TOut>() / BindSafeInput<TIn>() / BindSafeOutput<TOut>() / BindMdpSafeIO<>() / BindMdpSafeInput<>() / BindMdpSafeOutput<>() / BindMdpDriveAxis<>() 目前只是 C# SDK 的能力,C++ FSoE 类尚未提供这些模板方法slave.GetFSoE() 返回的 FSoE 实例上没有这些成员,也没有 SetConnectionId())。本节及随后的示例代码反映的是 规划中的目标 API,不是当前可调用的接口。

C++ 当前可用的真实路径:

  1. fsoe.Initialize(connectionId, safetyAddress, watchdogMs)Initialize(FSoEConnectionConfig) 建立连接;
  2. 在 PDO 周期回调里 fsoe.ReadSafeInput(size) / fsoe.WriteSafeOutput(data, size) 读写安全数据,并调用 fsoe.ProcessCycle()
  3. 多连接场景用 SafetyManager 类的(非模板BindSafeInput() / BindSafeOutput() / BindSafeIO() / BindMdpDriveAxis() 方法(签名见下方 FSoE 管理器,参数为 slaveIndex/safetyAddress/字节大小,不是模板类型推断)。

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 连接
DxSafeMdpConfig 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 管理器

SafetyManager 类用于管理多个 FSoE 连接(C++ SDK 中类名为 SafetyManager,另有面向连接的封装 FSoEConnectionManager;不存在名为 FSoEManager 的类):

using namespace darra;

SafetyManager 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();

SafetyManager 完整方法

  • AddConnection(slaveIndex) (FSoE&) — 添加连接
  • ConnectionCount() (size_t) — 连接数量
  • GetConnection(index) (FSoE&) — 获取指定连接
  • InitializeAll() (bool) — 初始化所有连接
  • CloseAll() (void) — 关闭所有连接
  • AllInDataState() (bool) — 所有连接是否在 Data 状态
  • AnyInFailsafe() (bool) — 是否有连接处于失效安全
  • ProcessCycle() (void) — 周期处理
  • GetDiagnostics() (std::vector<SafeModuleDiag>) — 全局诊断
  • FindByAddress(safetyAddress) (FSoE*) — 按安全地址查找
  • FindByConnectionId(id) (FSoE*) — 按连接 ID 查找
  • FindConnectionByAddress(addr) (FSoE*) — 按安全地址查找(别名)
  • GetStatusSummary() (std::string) — 状态摘要字符串
  • WriteOutputFrame(connIdx, data) (bool) — 写入原始输出帧
  • ReadInputFrame(connIdx) (std::vector<uint8_t>) — 读取原始输入帧
  • BindSafeIO(slaveIndex, inOff, inSize, outOff, outSize, addr, wdMs) (bool) — 绑定安全输入输出(非模板,按字节大小)
  • BindSafeInput(slaveIndex, addr, inSize, wdMs) (bool) — 绑定安全输入
  • BindSafeOutput(slaveIndex, addr, outSize, wdMs) (bool) — 绑定安全输出
  • BindMdpSafeInput(slaveIndex, addr, inSize, moduleNo, wdMs) (bool) — MDP 绑定安全输入
  • BindMdpSafeOutput(slaveIndex, addr, outSize, moduleNo, wdMs) (bool) — MDP 绑定安全输出
  • BindMdpDriveAxis(slaveIndex, axisIndex, addr, wdMs) (bool) — MDP 驱动轴安全绑定
FSoEConnectionMode 枚举
enum class FSoEConnectionMode {
Single, // 单连接模式 - 所有模块共用一个 FSoE 连接
Multiple // 多连接模式 - 每个模块独立 FSoE 连接
};
SafeModuleProfile 枚举
enum class SafeModuleProfile : uint16_t {
DigitalInput = 190, // FSoE Digital Input
DigitalInOut = 195, // FSoE Digital In-/Output
DigitalOutput = 290, // FSoE Digital Output
DriveConnection = 790, // FSoE MDP Drive Connection
Master = 6900 // FSoE Master
};

完整示例

本节示例使用未实现的模板绑定 API

下列示例中的 BindSafeInput<>() / BindSafeOutput<>() / BindSafeIO<>() / BindMdpSafeIO<>()规划中的目标 API,C++ FSoE 类当前未实现。 真实可调用路径见上文 简化绑定 的 danger 提示与 FSoE 管理器 章节: Initialize() + ReadSafeInput() / WriteSafeOutput() / ProcessCycle(),多连接用 SafetyManager 的非模板 BindSafeInput() / BindSafeOutput() / BindSafeIO()。 示例中的 OnError / OnFailsafe / OnStateChanged / SetFailsafeOutput 是真实成员,可直接用。

数字量安全 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:
// 静态版: 需显式传入 dll_t&
static bool IsConnectionIdAvailable(dll_t& dll, uint16_t connId);
// 成员版: 用当前实例持有的 DLL
bool IsConnectionIdAvailable(uint16_t connId) const;
};

检测指定 ConnID 是否可用(未被任何活动连接占用)。静态版需传入 dll_t&(如 master.Dll());成员版直接用从站实例持有的 DLL。跨从站全局生效。

参数:

  • dll (dll_t&) — DLL 上下文(仅静态版需要)
  • connId (uint16_t) — 待校验的 Connection ID

返回值:

  • booltrue 表示可用,false 表示已被占用或校验失败

示例:

auto& fsoe = master.GetSlave(1).GetFSoE();

// 查找第一个可用 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";

// 用分配到的 ConnID 初始化 FSoE 连接
fsoe.Initialize(allocId, /*safetyAddress*/ 0x0100, /*watchdogMs*/ 100);
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
virtual uint16_t Compute(const uint8_t* data, size_t length) const = 0;
/// vector 重载
uint16_t Compute(const std::vector<uint8_t>& data) const;
};

CRC-16 抽象接口,对齐 C# IFSoECrc16。允许应用提供自己的实现来 mock / 替换默认 CCITT-FALSE。 接口本身只有 Compute();带 ConnectionID 种子的计算是 FSoECrc16 类的静态方法 ComputeWithConnectionId(),不是接口虚函数。

FSoECrc16 类 (默认 CCITT-FALSE)

class FSoECrc16 : public IFSoECrc16 {
public:
/// 默认构造 = CCITT-FALSE: poly=0x1021, init=0xFFFF, xorOut=0x0000
/// connectionId 用 std::optional, std::nullopt = 不注入 ConnID 前缀
FSoECrc16(uint16_t polynomial = 0x1021,
uint16_t initial = 0xFFFF,
uint16_t xorOut = 0x0000,
std::optional<uint16_t> connectionId = std::nullopt);

/// 全局共享的 CCITT-FALSE 单例 (无 ConnectionID 种子)
static const FSoECrc16& CcittFalse();

/// 工厂: 携带 ConnectionID 的 CRC-16 实例
static FSoECrc16 CreateWithConnectionId(uint16_t connectionId);

/// 静态便捷方法: 等价 CreateWithConnectionId(connId).Compute(data, length)
static uint16_t ComputeWithConnectionId(const uint8_t* data, size_t length,
uint16_t connectionId);

uint16_t Compute(const uint8_t* data, size_t length) const override;
};

参数对应 ETG.5120 §5.2 规定的 CRC-16/CCITT-FALSE:多项式 0x1021、初值 0xFFFF、不反转输入输出、不 xor-out。构造时传入 connectionIdstd::optional)后,Compute() 会把它作为数据前缀注入运算(FSoE 标准做法);也可直接用静态 ComputeWithConnectionId(data, len, connId)

示例:

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 标准用法) — 静态方法
uint16_t safeCrc = FSoECrc16::ComputeWithConnectionId(
payload.data(), payload.size(), 0x0042);

// 或构造带 ConnID 的实例后直接 Compute()
auto crc2 = FSoECrc16::CreateWithConnectionId(0x0042);
uint16_t safeCrc2 = crc2.Compute(payload.data(), payload.size());

FSoEPdoFrame 与 FSoEPdoLayout

FSoEPdoFrame / FSoEPdoLayout 用于在用户态拼装 / 解析 FSoE PDO 帧(CommandByte + 数据段 + CRC 段 + ConnectionID)。对齐 C# FSoEPdoFrame / FSoEPdoLayout

struct FSoEPdoLayout {
uint16_t CommandOffset = 0; // Command 字节偏移
uint16_t SafeDataOffset = 1; // SafeData 起始偏移
uint16_t SafeDataLength = 2; // SafeData 总长度
uint16_t CrcSegmentCount = 1; // CRC 段数 = ceil(SafeDataLength*8/16)
std::vector<uint16_t> CrcOffsets; // 各段 CRC 偏移 (大小 = CrcSegmentCount)
uint16_t ConnIdOffset = 0; // ConnID 字节偏移
uint16_t TotalLength = 0; // 帧总长度
uint16_t PaddingBytes = 0; // 末段奇数字节填充 (0/1)

void Validate() const; // 校验布局自洽性, 非法抛 std::invalid_argument
static uint16_t ComputeSegmentCount(uint16_t safeDataBits); // 计算理论 CRC 段数
};

class FSoEPdoFrame {
public:
FSoECommand Command = FSoECommand::Reset; // FSoE Command (枚举, 非 uint8_t)
uint16_t ConnectionId = 0; // ConnectionID
std::vector<uint8_t> SafeData; // 安全数据段
std::vector<uint16_t> CrcSegments; // 每段 CRC-16 (按 layout 切分)

/// 序列化为字节流 (传 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;
/// 用 ConnID 内部构造 CCITT-FALSE 后校验
bool ValidateCrcSegments(uint16_t connectionId) const;
};

示例 (拼装一帧再发出去):

using namespace darra::ethercat;

FSoEPdoLayout layout;
layout.SafeDataLength = 6;
layout.CrcSegmentCount = FSoEPdoLayout::ComputeSegmentCount(6 * 8); // 3 段
layout.CrcOffsets = {3, 6, 9}; // 各段 CRC 偏移 (按实际帧布局填)
layout.TotalLength = 13; // 须自洽, 否则 Validate() 抛异常

FSoEPdoFrame f;
f.Command = FSoECommand::Data; // FSoE Data Command (枚举)
f.SafeData = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
f.ConnectionId = 0x0042;

// 让 CRC 引擎按 layout 切片重算所有 CRC 段
auto crc = FSoECrc16::CreateWithConnectionId(f.ConnectionId);
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 计算规则。