跳到主要内容

FoE (File over EtherCAT)

FoE 协议用于 EtherCAT 从站的文件传输,支持固件更新、配置文件上传下载等场景。

通过 slave.FoE 访问。从站不支持 FoE 时为 null。​

属性​

类别属性类型读写说明
默认参数DefaultTimeoutMsint读写默认超时时间(毫秒),默认 5000(5秒)
DefaultTimeoutUsint读写默认超时(微秒),等于 DefaultTimeoutMs × 1000;设置时反向换算回毫秒
DefaultPassworduint读写默认密码,默认 0
错误LastErrorCodeFoEErrorCode?只读最近一次 FoE 操作的错误码(FoEErrorCode 枚举),成功或未开始为 null

文件传输​

Download(string filename, uint? password = null, int? timeoutMs = null, bool enableCrc = false)​

public byte[]? Download(string filename, uint? password = null, int? timeoutMs = null, bool enableCrc = false)

从从站设备下载(读取)文件。

参数:

  • filename (string) — 要下载的文件名
  • password (uint?) — FoE 密码(不指定则使用 DefaultPassword)
  • timeoutMs (int?) — 超时时间(毫秒,不指定则使用 DefaultTimeoutMs)
  • enableCrc (bool) — 启用 CRC 校验(自动验证数据完整性)

返回值:

  • byte[]? — 文件内容字节数组,失败返回 null

示例:

// 基本下载
byte[]? firmware = slave.FoE.Download("firmware.bin");

// 启用 CRC 校验
byte[]? config = slave.FoE.Download("config.xml", enableCrc: true);

Upload(string filename, byte[] fileData, uint? password = null, int? timeoutMs = null, bool enableCrc = false)​

public bool Upload(string filename, byte[] fileData, uint? password = null, int? timeoutMs = null, bool enableCrc = false)

上传(写入)文件到从站设备。

参数:

  • filename (string) — 要上传的文件名
  • fileData (byte[]) — 要写入的文件数据
  • password (uint?) — FoE 密码(不指定则使用 DefaultPassword)
  • timeoutMs (int?) — 超时时间(毫秒,不指定则使用 DefaultTimeoutMs)
  • enableCrc (bool) — 启用 CRC 校验(自动附加校验信息)

返回值:

  • bool — 成功返回 true

示例:

// 基本上传
byte[] newFirmware = File.ReadAllBytes("new_firmware.bin");
bool success = slave.FoE.Upload("firmware.bin", newFirmware);

// 启用 CRC 校验
bool ok = slave.FoE.Upload("firmware.bin", newFirmware, enableCrc: true);

EnsureBootState()​

public bool EnsureBootState()

确保从站处于 Bootstrap(Boot)状态。固件烧写等 bootloader 区域传输必须在 Boot 状态下进行,否则从站返回 BootstrapOnly;普通配置文件用 PreOp 即可。

返回值:

  • bool — 已在 Boot 或切换成功返回 true;切换失败返回 false 并置 LastErrorCode = BootstrapOnly

进度事件​

ProgressChanged​

public event EventHandler<FoEProgressEventArgs>? ProgressChanged;

FoE 传输进度事件。订阅后,Download / Upload 操作会自动触发进度报告。

FoEProgressEventArgs​

相关结构:

  • Slave (ushort) — 从站编号
  • PacketNumber (int) — 数据包序号
  • DataSize (int) — 数据大小(字节)
  • ProgressPercent (int) — 估算的进度百分比 (0-100)

示例:

slave.FoE.ProgressChanged += (sender, e) =>
{
Console.Write($"\r传输进度: {e.ProgressPercent}% (包 {e.PacketNumber})");
};

byte[] firmware = File.ReadAllBytes("new_firmware.bin");
bool success = slave.FoE.Upload("firmware.bin", firmware, enableCrc: true);
Console.WriteLine(success ? "\n上传完成!" : "\n上传失败!");

传输进度​

EnableProgressHook(int estimatedTotalPackets = 0)​

public bool EnableProgressHook(int estimatedTotalPackets = 0)

启用 FoE 进度钩子并设置总包数估算。通常只需订阅 ProgressChanged:Download / Upload 入口会自动注册钩子并覆盖估算值(Upload 自动按 EstimatePacketCount 估算,Download 传 0)。

参数:

  • estimatedTotalPackets (int) — 估算的总数据包数(用于计算进度百分比,0 = 不估算)

返回值:

  • bool — 成功返回 true

DisableProgressHook()​

public bool DisableProgressHook()

禁用 FoE 进度钩子,停止进度报告。实例级操作,只在自身是主站 hook 属主时摘除 master 级钩子。

返回值:

  • bool — 成功返回 true

ClearMasterProgressHook(ushort masterIndex)​

public static bool ClearMasterProgressHook(ushort masterIndex)

无条件释放某主站上的进度/BUSY 钩子(master 级单例槽位的显式释放入口)。与 DisableProgressHook 的差异:后者是实例级、只在自身是属主时生效。主站关闭、从站热插拔或更换从站前调用本方法清掉残留注册。

参数:

  • masterIndex (ushort) — 主站索引

返回值:

  • bool — 成功返回 true

EstimatePacketCount(int fileSize, int mailboxSize = 512)​

public int EstimatePacketCount(int fileSize, int mailboxSize = 512)

估算文件传输所需的数据包数量。可用于 EnableProgressHook 的 estimatedTotalPackets 参数。

参数:

  • fileSize (int) — 文件大小(字节)
  • mailboxSize (int) — 邮箱大小(字节,默认 512)

返回值:

  • int — 估算的数据包数量

示例:

// 启用进度回调
slave.FoE.ProgressChanged += (sender, e) =>
{
Console.Write($"\r传输进度: {e.ProgressPercent}% (包 {e.PacketNumber}, {e.DataSize} 字节)");
};

byte[] firmware = File.ReadAllBytes("new_firmware.bin");
int packets = slave.FoE.EstimatePacketCount(firmware.Length);
slave.FoE.EnableProgressHook(packets);

bool success = slave.FoE.Upload("firmware.bin", firmware);
slave.FoE.DisableProgressHook();

Console.WriteLine(success ? "\n上传完成!" : "\n上传失败!");

错误处理​

FoEInstance.GetErrorDescription(FoEErrorCode errorCode)​

public static string GetErrorDescription(FoEErrorCode errorCode)

获取 FoE 错误码的中文描述文本。

参数:

  • errorCode (FoEErrorCode) — FoE 协议错误码

返回值:

  • string — 中文描述文本

相关结构:

public enum FoEErrorCode : ushort
{
NotDefined = 0x8000, // 未定义错误
NotFound = 0x8001, // 文件未找到
AccessDenied = 0x8002, // 访问被拒绝
DiskFull = 0x8003, // 磁盘已满
Illegal = 0x8004, // 非法操作
PacketNumberWrong = 0x8005, // 数据包序号错误
AlreadyExists = 0x8006, // 文件已存在
NoUser = 0x8007, // 无此用户
BootstrapOnly = 0x8008, // 仅 Bootstrap 模式可用
NotBootstrap = 0x8009, // 不在 Bootstrap 模式
NoRights = 0x800A, // 权限不足
ProgramError = 0x800B // 程序错误
}

示例:

byte[]? data = slave.FoE.Download("firmware.bin");
if (data == null)
{
string desc = DarraEtherCAT.Slave.FoEInstance.GetErrorDescription(DarraEtherCAT.Slave.FoEInstance.FoEErrorCode.NotFound);
Console.WriteLine($"下载失败: {desc}");
}

LastErrorCode​

public FoEErrorCode? LastErrorCode { get; }

最近一次 FoE 操作的错误码 (ETG.1000.6 Table 92)。Download / Upload 入口会先清零,成功返回 null;失败时把 DLL 经 fileSize 返回的负值错误码映射进此属性,配合返回值 (null / false) 判断具体失败原因。

示例:

byte[]? data = slave.FoE.Download("firmware.bin");
if (data == null && slave.FoE.LastErrorCode is DarraEtherCAT.Slave.FoEInstance.FoEErrorCode err)
{
Console.WriteLine($"下载失败: {DarraEtherCAT.Slave.FoEInstance.GetErrorDescription(err)}");
}

完整示例​

固件更新​

if (slave.FoE != null)
{
// 备份当前固件
byte[]? backup = slave.FoE.Download("firmware.bin");
if (backup != null)
File.WriteAllBytes("firmware_backup.bin", backup);

// 上传新固件(带进度和 CRC 校验)
slave.FoE.ProgressChanged += (s, e) =>
Console.Write($"\r更新进度: {e.ProgressPercent}%");

slave.FoE.EnsureBootState(); // 固件烧写需 Boot(Bootstrap)状态

byte[] newFirmware = File.ReadAllBytes("new_firmware.bin");
bool ok = slave.FoE.Upload("firmware.bin", newFirmware, enableCrc: true);
Console.WriteLine(ok ? "\n固件更新成功" : "\n固件更新失败");
}

配置文件传输​

if (slave.FoE != null)
{
slave.FoE.DefaultPassword = 0x12345678;
slave.FoE.DefaultTimeoutMs = 10000; // 10秒

// 读取配置
byte[]? config = slave.FoE.Download("config.xml");
if (config != null)
{
string xmlContent = System.Text.Encoding.UTF8.GetString(config);
Console.WriteLine($"配置内容:\n{xmlContent}");
}

// 写入新配置
string newConfig = "<Config><Param1>100</Param1></Config>";
byte[] configBytes = System.Text.Encoding.UTF8.GetBytes(newConfig);
slave.FoE.Upload("config.xml", configBytes);
}

取消传输与 BUSY 钩子​

固件升级或大体积配置传输常常持续数十秒甚至几分钟。两个实用能力:

  • 中途取消 — 允许用户按"取消"终止当前传输, 避免一直等到超时
  • BUSY 钩子 — 从站在擦写 Flash / 烧录固件期间会周期返回 FoE BUSY 帧 (ETG.1000.6 §5.10 Table 93), 上位机借此显示"xx% 烧写中"

取消传输 (CancellationToken)​

通过 DownloadAsync / UploadAsync 的 cancellationToken 参数取消,触发时 Task<T> 抛出 OperationCanceledException。

签名:

public Task<byte[]?> DownloadAsync(string filename, uint? password = null, int? timeoutMs = null,
bool enableCrc = false, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
public Task<bool> UploadAsync(string filename, byte[] fileData, uint? password = null, int? timeoutMs = null,
bool enableCrc = false, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
固件升级注意

固件烧写命令可能已部分发送至从站, 取消后需重新下发完整固件。

示例 — 下载带取消:

var cts = new CancellationTokenSource();

// 启动异步下载 (非阻塞)
var progress = new Progress<int>(p => Console.Write($"\r进度: {p}%"));
var task = slave.FoE.DownloadAsync(
"firmware.bin",
progress: progress,
cancellationToken: cts.Token);

// 用户按 "取消" 按钮:
// cts.Cancel();

try
{
byte[]? data = await task;
Console.WriteLine(data != null ? "\n下载完成" : "\n下载失败");
}
catch (OperationCanceledException)
{
Console.WriteLine("\n用户取消传输");
}

示例 — 上传 (固件升级) 带取消:

var cts = new CancellationTokenSource();
byte[] firmware = File.ReadAllBytes("fw.bin");

var progress = new Progress<int>(p => Console.Write($"\r烧写: {p}%"));
var task = slave.FoE.UploadAsync(
"firmware.bin",
firmware,
enableCrc: true,
progress: progress,
cancellationToken: cts.Token);

// 用户中途取消:
// cts.Cancel();

try
{
bool ok = await task;
Console.WriteLine(ok ? "\n固件烧写成功" : "\n固件烧写失败");
}
catch (OperationCanceledException)
{
Console.WriteLine("\n已取消; 固件可能已部分写入, 需重新升级");
}

BUSY 钩子 (烧写进度)​

从站在处理耗时 Flash 操作时会按 ETG.1000.6 Table 93 周期回 BUSY 帧, 主站 SDK 解析后通过同一个 ProgressChanged 事件上报, BUSY 相关字段在 FoEProgressEventArgs 扩展属性中:

  • BusyDone (int) — 从站报告的已完成字节数
  • BusyEntire (int) — 从站报告的总字节数
  • BusyText (string) — 从站自报告的状态文本 (如 "Erasing flash..." / "Programming sector 3/8")

当 BusyEntire > 0 时可认为当前进度来自 BUSY 帧 (否则是 DATA 阶段的包计数进度)。EnableProgressHook() 同时注册了 DATA 与 BUSY 两类回调, 无需额外 API。

示例 — 固件升级进度条 (含 BUSY 烧写显示):

if (slave.FoE == null) return;

slave.FoE.ProgressChanged += (sender, e) =>
{
if (e.BusyEntire > 0)
{
// BUSY 阶段: 从站正在擦写 Flash
double pct = 100.0 * e.BusyDone / e.BusyEntire;
Console.Write(
$"\r[烧写中] {pct:F1}% ({e.BusyDone}/{e.BusyEntire}) {e.BusyText} ");
}
else
{
// DATA 阶段: 主站在推送文件数据
Console.Write($"\r[传输中] {e.ProgressPercent}% (包 {e.PacketNumber}, {e.DataSize}B) ");
}
};

byte[] firmware = File.ReadAllBytes("fw.bin");
int packets = slave.FoE.EstimatePacketCount(firmware.Length);
slave.FoE.EnableProgressHook(packets);

try
{
bool ok = slave.FoE.Upload("firmware.bin", firmware, enableCrc: true);
Console.WriteLine(ok ? "\n烧写完成" : "\n烧写失败");
}
finally
{
slave.FoE.DisableProgressHook();
}

取消 + BUSY 组合 (推荐)​

固件升级的实战组合: 异步调用 + 进度条 + BUSY 钩子 + 用户取消。用户看到真实烧写进度, 中途长时间卡在某个扇区时可主动取消避免无限等待。

var cts = new CancellationTokenSource();

slave.FoE.ProgressChanged += (s, e) =>
{
if (e.BusyEntire > 0)
Console.Write($"\r烧写 {100.0 * e.BusyDone / e.BusyEntire:F1}% {e.BusyText} ");
else
Console.Write($"\r传输 {e.ProgressPercent}% ");
};

int packets = slave.FoE.EstimatePacketCount(firmware.Length);
slave.FoE.EnableProgressHook(packets);

var task = slave.FoE.UploadAsync(
"firmware.bin", firmware, enableCrc: true,
cancellationToken: cts.Token);

// 超时兜底: 单扇区卡 > 30 秒自动取消
_ = Task.Delay(TimeSpan.FromSeconds(30), cts.Token).ContinueWith(_ =>
{
if (!task.IsCompleted) cts.Cancel();
}, TaskScheduler.Default);

try { await task; }
catch (OperationCanceledException) { Console.WriteLine("\n已取消"); }
finally { slave.FoE.DisableProgressHook(); }

注意事项​

  • BUSY 帧由从站决定是否推送, 部分低端从站不实现 BUSY 阶段, 上位机会"安静"几秒直到 FoE ACK 到达
  • 取消标志仅影响 下一次 DLL 主循环迭代, 从站当前正在执行的 Flash 写不会被中断
  • 已订阅 ProgressChanged 时,Download / Upload 入口会自动注册进度钩子;手动 EnableProgressHook 需在传输开始前调用,否则第一轮进度回调会丢失
  • DLL 未导出 FOESetBusyHook 时不支持 BUSY 进度回调,此时日志会记录「BUSY 进度信息将不可用」,其他功能不受影响

默认参数​

类别属性类型读写说明
默认参数DefaultTimeoutMsint读写FoE 默认超时 (毫秒), 默认 5000
DefaultTimeoutUsint读写FoE 默认超时 (微秒), 派生自 DefaultTimeoutMs * 1000. 设置时反向计算 ms
DefaultPassworduint读写FoE 默认密码, 默认 0. Download / Upload 不指定 password 时回退使用
public int DefaultTimeoutMs { get; set; } = 5000;
public int DefaultTimeoutUs { get; set; } // = DefaultTimeoutMs * 1000
public uint DefaultPassword { get; set; } = 0;

示例:

slave.FoE.DefaultTimeoutMs = 10000;
slave.FoE.DefaultPassword = 0xCAFE0001;
byte[]? data = slave.FoE.Download("firmware.bin"); // 使用默认超时与默认密码

取消传输​

Cancel()​

public bool Cancel()

请求取消当前 FoE 下载/上传事务。 从任意线程调用 (与 CancellationToken 等效但不依赖 async). 内部置位 DLL 取消标志, 下一次主循环迭代后 Download / Upload 提前返回。

返回值:

  • bool — true=请求成功; false=DLL 未导出 FOERequestCancel 或调用失败
注意

固件烧写中途中断可能导致从站 Flash 部分写入, 需重新上传完整固件。

示例:

var task = Task.Run(() => slave.FoE.Upload("firmware.bin", data));
await Task.Delay(2000);
slave.FoE.Cancel(); // 任意线程, 不需要 cancellationToken

ClearCancel()​

public bool ClearCancel()

清除取消标志, 允许重新开始新的 FoE 事务。 Download / Upload 入口会自动清零, 仅在异常复位或外部手动操作时调用。

返回值:

  • bool — true=清除成功

BUSY 帧事件​

BusyReceived​

public event EventHandler<FoEBusyEventArgs>? BusyReceived;

从站返回 BUSY 帧时触发 (固件 Flash 烧写期间周期上报). 与 ProgressChanged.BusyDone/BusyEntire/BusyText 同源, 二选一即可。 首次订阅时自动注册 BUSY Hook, 最后一个订阅者取消后自动解注册 (除非 ProgressChanged 仍持有 Hook).

备注

回调线程是 DLL worker, UI 应 Invoke 切回主线程。

相关结构:

public sealed class FoEBusyEventArgs : EventArgs
{
public ushort SlaveIndex { get; init; }
public ushort Done { get; init; } // 已完成量
public ushort Entire { get; init; } // 总量
public string Text { get; init; } // 从站文本
public int RetryIndex { get; init; } // 重试序号
public double Percent { get; } // = Entire > 0 ? 100.0 * Done / Entire : 0
}

示例:

slave.FoE.BusyReceived += (s, e) =>
{
Console.WriteLine($"[BUSY] {e.Done}/{e.Entire} ({e.Percent:F1}%) {e.Text}");
};

byte[] fw = File.ReadAllBytes("firmware.bin");
slave.FoE.Upload("firmware.bin", fw);