跳到主要内容

FoE (File over EtherCAT)

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

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

属性

属性类型访问说明
DefaultTimeoutMsint读写默认超时时间(毫秒),默认 5000(5秒)
DefaultPassworduint读写默认密码,默认 0

文件传输

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) — 启用 CRC32 校验(自动验证数据完整性)

返回值:

  • 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) — 启用 CRC32 校验(自动附加校验信息)

返回值:

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

进度事件

ProgressChanged

public event EventHandler<FoEProgressEventArgs>? ProgressChanged;

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

相关结构:

  • 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 进度钩子。启用后,Download / Upload 操作会通过 ProgressChanged 事件自动报告传输进度。

参数:

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

返回值:

  • bool — 成功返回 true

DisableProgressHook()

public bool DisableProgressHook()

禁用 FoE 进度钩子,停止进度报告。

返回值:

  • bool — 成功返回 true

EstimatePacketCount(int fileSize, int mailboxSize = 512)

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

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

参数:

  • 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 : uint
{
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 = FoEInstance.GetErrorDescription(FoEErrorCode.NotFound);
Console.WriteLine($"下载失败: {desc}");
}

完整示例

固件更新

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}%");

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 / UploadAsynccancellationToken 参数取消,触发时 Task<T> 抛出 OperationCanceledException

固件升级注意

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

示例 — 下载带取消:

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 写不会被中断
  • EnableProgressHook 必须在 Upload / Download 之前调用, 否则第一轮进度回调会丢失
  • 部分 SDK 版本可能不支持 BUSY 进度回调, 此时日志会记录"BUSY 进度信息将不可用", 其他功能不受影响

默认参数

属性类型说明
DefaultTimeoutMsintFoE 默认超时 (毫秒), 默认 5000
DefaultTimeoutUsintFoE 默认超时 (微秒), 派生自 DefaultTimeoutMs * 1000. 设置时反向计算 ms
DefaultPassworduintFoE 默认密码, 默认 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;
slave.FoE.Download("firmware.bin", out _); // 使用默认值

取消传输

Cancel()

public bool Cancel()

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

返回值:

  • booltrue=请求成功; 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 入口会自动清零, 仅在异常复位或外部手动操作时调用.

返回值:

  • booltrue=清除成功

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; } // = 100.0 * Done / Entire
}

示例:

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