邮箱网关 (Mailbox Gateway)
邮箱网关实现 ETG.8200 标准,允许外部诊断工具通过 UDP/IP 访问 EtherCAT 主站和从站的对象字典。
C SDK 提供两种实现:
- DLL 内置网关 — 直接复用主站当前对象字典与从站邮箱栈,适合常规部署
- SDK 自带 UDP 服务 (
master/gateway.h) — 用户提供回调,适合定制路由 / 鉴权 / 限速
邮箱网关服务符合 ETG.8200 标准规范,使用标准 UDP 端口 0x88A4 (34980),帧结构完全符合 Table 1 要求。
服务控制
DarraGateway* gateway_create(uint16_t master_index);
void gateway_destroy(DarraGateway* gw);
int gateway_set_port(DarraGateway* gw, uint16_t port);
uint16_t gateway_get_port(const DarraGateway* gw);
int gateway_start(DarraGateway* gw);
void gateway_stop(DarraGateway* gw);
bool gateway_is_running(const DarraGateway* gw);
gateway_create()创建网关实例,默认端口0x88A4 + master_index,返回句柄(NULL=分配失败)gateway_set_port()必须在启动前调用,返回 0 成功、-1 已在运行gateway_start()启动后台 UDP 监听线程,返回 0 成功、-1 端口绑定失败、-2 已在运行gateway_destroy()销毁实例(如果服务仍在运行,内部自动 stop)
运行多个主站实例时,端口按 master_index 自动偏移:实例 0=34980、实例 1=34981、实例 N=34980+N。被占用时启动阶段顺移最多 10 个槽位。
端口策略
默认端口 0x88A4 (34980)
实例 0: 34980
实例 1: 34981
实例 N: 34980 + N
被占用时启动阶段顺移最多 10 个槽位,全部失败由系统分配。gateway_get_port() 启动后返回实际绑定端口。
协议回调
typedef int (*DarraGatewayMasterOdCallback)(uint8_t mbx_type,
const uint8_t* request, int request_len,
uint8_t* response_buf, int response_buf_size, void* user);
typedef int (*DarraGatewaySlaveCallback)(uint16_t slave_addr, uint8_t mbx_type,
const uint8_t* request, int request_len,
uint8_t* response_buf, int response_buf_size, void* user);
void gateway_set_master_od_callback(DarraGateway* gw,
DarraGatewayMasterOdCallback cb, void* user);
void gateway_set_slave_callback(DarraGateway* gw,
DarraGatewaySlaveCallback cb, void* user);
- 主站对象字典回调: 收到
Address == 0x0000的请求时调用 - 从站邮箱回调: 收到
Address != 0x0000的请求时调用 - 回调返回写入响应缓冲的字节数;0=无响应,负值=丢弃
统计信息
typedef struct {
uint64_t rxPackets; /* 收到的 UDP 包数 */
uint64_t txPackets; /* 发送的响应包数 */
uint64_t rxBytes; /* 收到的字节数 */
uint64_t txBytes; /* 发送的字节数 */
uint64_t masterOdRequests; /* 路由到主站 OD 的请求数 */
uint64_t slaveRequests; /* 路由到从站邮箱的请求数 */
uint64_t dropMalformed; /* 帧格式错误丢弃数 */
uint64_t dropTimeout; /* 处理超时丢弃数 */
} DarraGatewayStats;
void gateway_get_statistics(const DarraGateway* gw, DarraGatewayStats* out_stats);
void gateway_reset_statistics(DarraGateway* gw);
get_statistics 单次原子复制;reset_statistics 清零所有计数器,不影响运行状态。
统计字段在所有 SDK 中名字与语义完全一致。诊断脚本可在不同语言间无缝互译。
支持的协议
| 协议 | 类型字节 | 说明 |
|---|---|---|
| CoE | 0x03 | SDO 读写,支持主站(ETG.1510)和从站对象字典 |
| SoE | 0x04 | IDN 参数读写(ETG.1020 标准) |
| FoE | 0x05 | 文件传输(当前仅支持单包) |
| VoE | 0x0F | 厂商特定协议透传 |
地址路由规则:
- Address
0x0000→ 主站对象字典(仅 CoE) - Address = 从站站地址 → 透传到对应从站(CoE / SoE / FoE / VoE)
帧结构
EtherCAT Header (2B): Length(11bit) + Type=0x05
Mailbox Header (6B): Length(2) + Address(2) + 通道+优先级(1) + 类型+计数器(1)
Data (NB): 协议负载
EtherCAT Header:
- Length: Mailbox Header + Data 的总长度
- Data Type: 固定为 0x05 (Mailbox communication)
Mailbox Header:
- Length: 邮箱数据部分长度
- Address: 0x0000=主站, 其他=从站站地址
- Type: 0x03=CoE, 0x04=SoE, 0x05=FoE, 0x0F=VoE
- Cnt: 邮箱计数器 (1-7 循环)
完整示例
自己启动案例
#include "master/gateway.h"
static int on_master_od(uint8_t mbx_type, const uint8_t* req, int req_len,
uint8_t* resp, int resp_cap, void* user)
{
/* 从主站 OD 取数据, 写入 resp, 返回字节数 */
return build_master_od_response(mbx_type, req, req_len, resp, resp_cap);
}
static int on_slave_mbx(uint16_t addr, uint8_t mbx_type,
const uint8_t* req, int req_len,
uint8_t* resp, int resp_cap, void* user)
{
/* 转发到对应从站邮箱 */
return forward_to_slave(addr, mbx_type, req, req_len, resp, resp_cap);
}
int main(void) {
DarraGateway* gw = gateway_create(0);
gateway_set_port(gw, 0x88A4);
gateway_set_master_od_callback(gw, on_master_od, NULL);
gateway_set_slave_callback(gw, on_slave_mbx, NULL);
if (gateway_start(gw) != 0) {
fprintf(stderr, "网关启动失败\n");
gateway_destroy(gw);
return 1;
}
/* ... 主循环 ... */
DarraGatewayStats s;
gateway_get_statistics(gw, &s);
printf("rx=%llu tx=%llu master_od=%llu drop=%llu\n",
(unsigned long long)s.rxPackets, (unsigned long long)s.txPackets,
(unsigned long long)s.masterOdRequests,
(unsigned long long)(s.dropMalformed + s.dropTimeout));
gateway_stop(gw);
gateway_destroy(gw);
return 0;
}
外部代码案例 (C 客户端)
#include <winsock2.h>
/* ETG.8200 帧: EtherCAT Header (2) + Mailbox Header (6) + Data */
static int build_frame(uint8_t* frame, uint16_t address, uint8_t mbx_type,
const uint8_t* mbx_data, uint16_t mbx_len)
{
uint16_t ecat_len = 6 + mbx_len;
/* EtherCAT Header: Length[10:0] | DataType[15:12]=0x05 */
*(uint16_t*)(frame + 0) = (uint16_t)((0x05 << 12) | (ecat_len & 0x07FF));
*(uint16_t*)(frame + 2) = mbx_len;
*(uint16_t*)(frame + 4) = address;
frame[6] = 0x00; /* Channel + Priority */
frame[7] = (uint8_t)(mbx_type << 4); /* Type[7:4] */
memcpy(frame + 8, mbx_data, mbx_len);
return 8 + mbx_len;
}
/* CoE SDO Upload: address=0x0000 主站 OD, 其他=从站 */
int coe_sdo_read(SOCKET sock, uint16_t address, uint16_t index, uint8_t subindex,
uint8_t* resp, int resp_cap)
{
uint8_t co_data[8] = {0};
co_data[1] = 0x20; /* CoE Type: SDO Request */
co_data[2] = 0x40; /* SDO Upload (Read) */
*(uint16_t*)(co_data + 3) = index;
co_data[5] = subindex;
uint8_t frame[64];
int n = build_frame(frame, address, 0x03 /* CoE */, co_data, sizeof(co_data));
/* sendto + recvfrom 略 */
return n;
}
错误处理
| 协议 | 错误类型 | 错误码 |
|---|---|---|
| CoE | 对象不存在 | 0x06020000 |
| CoE | 不支持的访问 | 0x06010000 |
| CoE | 写保护 | 0x06010002 |
| CoE | 命令未实现 | 0x05040001 |
| SoE | IDN 不存在 | 0x1009 |
| SoE | 写入失败 | 0x7002 |
| FoE | 文件未找到 | 0x00008002 |
| FoE | 非法文件名 | 0x00008003 |
| VoE | 无响应 | 0x0003 |
完整错误码列表参考 错误处理。
注意事项
默认网关是强透传的,允许网络访问主站和从站的全部对象字典。建议:
- 务必在受控网络中使用,或通过 IP 白名单 / VPN 隧道 限制访问
- 配合操作系统防火墙规则,仅放行可信来源 IP
- 生产环境中谨慎开启,避免暴露到公网
邮箱网关运行在独立线程,对主循环性能影响极小。但大量同步 SDO/邮箱操作仍可能影响总体延迟——请合理限制并发与速率。
限制
已实现功能:
- CoE 完整透传 / SoE IDN 参数访问 / FoE 单包文件传输 / VoE 厂商协议透传
- 主站对象字典访问 + 从站邮箱透传
- 错误处理和错误码映射
待实现功能:
- FoE 分段传输(大文件支持)
- Address 映射表 (+0x8000 虚拟映射)
- 多播/广播请求
- 会话状态管理
异步邮箱事务 (slave/mbx_registry.h)
slave/mbx_registry.h 提供一组非阻塞邮箱事务 API:调用方提交一笔邮箱事务(CoE/SoE/FoE/AoE/VoE/FSoE 任意协议帧)后立即拿到 ticket,随后异步等待 / 取消 / 取结果。适合需要并发提交多笔邮箱操作、或不希望阻塞当前线程的场景。
邮箱网关(上文)是 ETG.8200 UDP 路由服务,面向外部诊断工具。异步邮箱事务面向应用程序自身发起的邮箱通信,二者无关。
事务状态枚举
typedef enum {
EC_MBX_STATUS_PENDING = 0, /* 待处理 / 未发起 */
EC_MBX_STATUS_SUCCESS = 1, /* 成功 */
EC_MBX_STATUS_TIMEOUT = -1, /* 超时 */
EC_MBX_STATUS_CANCELLED = -2, /* 被取消 */
EC_MBX_STATUS_MBX_ERROR = -3, /* 对端返回 mailbox Error 帧 */
EC_MBX_STATUS_PROTO_MISMATCH = -4, /* 响应协议类型不匹配 */
EC_MBX_STATUS_COUNTER_FAIL = -5, /* 响应 counter 不匹配 */
EC_MBX_STATUS_WKC_FAIL = -6, /* 底层 wkc<=0 */
EC_MBX_STATUS_NO_MEMORY = -7, /* 缓冲申请失败 */
EC_MBX_STATUS_INVALID_ARG = -8, /* 参数错误 */
EC_MBX_STATUS_NOT_IMPL = -9 /* 未实现 (运行时库未导出) */
} ec_mbx_status_t;
事务结构
/* 自然对齐 (x64), 总大小 72 字节 — 切勿 #pragma pack(1) */
typedef struct {
/* ---- 输入字段 (调用者填) ---- */
uint16_t slave; /* 从站索引 (1-based) */
uint8_t protocol_type; /* MBXPROT_* (AOE=0x01/EOE=0x02/COE=0x03/FOE=0x04/SOE=0x05/VOE=0x0F) */
uint8_t reserved0;
uint32_t timeout_us; /* 总超时 (微秒); 0=默认 */
volatile int cancel_flag; /* 调用方置 1 请求取消 */
void* request_frame; /* 请求帧缓冲 (含 6 字节 MbxHeader) */
uint16_t request_len; /* 请求有效长度 */
uint16_t reserved1;
void* response_frame; /* 响应帧缓冲 (调用者分配) */
uint16_t response_cap; /* 响应可写容量 */
uint16_t response_len; /* 输出: 实际响应长度 */
/* ---- 输出字段 ---- */
ec_mbx_status_t status; /* 最终状态 */
uint32_t error_code; /* 协议特定错误码 */
uint8_t response_counter; /* 实际收到的响应 counter */
uint8_t reserved2[3];
uint64_t submit_time_us; /* 提交时刻 */
uint64_t complete_time_us; /* 完成时刻 */
} ec_mbx_transaction_t;
统计结构
/* 自然对齐 (x64), 总大小 72 字节 — 切勿 #pragma pack(1) */
typedef struct {
uint64_t total_sent; /* 发送事务总数 */
uint64_t total_received; /* 成功接收响应总数 */
uint64_t total_timeout; /* 超时总数 */
uint64_t total_mbx_error; /* 邮箱错误帧总数 */
uint64_t total_proto_error; /* 协议不匹配总数 */
uint64_t total_cancelled; /* 取消总数 */
uint32_t last_error_code; /* 最后协议错误码 */
uint64_t last_error_time_us; /* 最后错误时间 (微秒) */
uint64_t total_latency_us; /* 累计 latency; 平均 = total_latency_us / total_received */
} ec_mbx_stats_t;
API
/* 查询 per-slave/per-protocol 邮箱统计 (protocol_type=0 累加所有协议) */
int ec_mbx_get_stats(struct dll* dll, uint16_t master_index, uint16_t slave,
uint8_t protocol_type, ec_mbx_stats_t* out_stats);
/* 重置邮箱统计 (protocol_type=0 清空所有协议) */
void ec_mbx_reset_stats(struct dll* dll, uint16_t master_index, uint16_t slave,
uint8_t protocol_type);
/* 异步投递邮箱事务, 输出 ticket */
int ec_mbx_submit_async(struct dll* dll, uint16_t master_index,
ec_mbx_transaction_t* txn, uint32_t* out_ticket);
/* 等待异步邮箱事务完成 */
int ec_mbx_wait(struct dll* dll, uint16_t master_index, uint32_t ticket, int timeout_ms);
/* 取消异步邮箱事务 */
int ec_mbx_cancel(struct dll* dll, uint16_t master_index, uint32_t ticket);
/* 拉取异步邮箱事务完整结果快照 */
int ec_mbx_get_result(struct dll* dll, uint16_t master_index, uint32_t ticket,
ec_mbx_transaction_t* out_txn);
所有函数首参为 dll_t*。DLL 未导出对应入口(老版 DLL)时安全返回 EC_MBX_STATUS_NOT_IMPL,不崩溃。
ec_mbx_transaction_t / ec_mbx_stats_t 必须按 x64 自然对齐(各 72 字节),绝不能 #pragma pack(1)——SDK 头文件已正确声明,直接 #include "slave/mbx_registry.h" 即可,不要自行重定义。
示例:
#include "slave/mbx_registry.h"
uint8_t req[64], resp[256];
/* ... 在 req 中构造 6 字节 MbxHeader + CoE SDO 请求 ... */
ec_mbx_transaction_t txn = {0};
txn.slave = 1;
txn.protocol_type = MBXPROT_COE; /* 0x03 */
txn.timeout_us = 1000000; /* 1s */
txn.request_frame = req;
txn.request_len = 14;
txn.response_frame = resp;
txn.response_cap = sizeof(resp);
uint32_t ticket = 0;
if (ec_mbx_submit_async(&dll, master, &txn, &ticket) == EC_MBX_STATUS_SUCCESS) {
if (ec_mbx_wait(&dll, master, ticket, 2000) == EC_MBX_STATUS_SUCCESS) {
ec_mbx_transaction_t result = {0};
ec_mbx_get_result(&dll, master, ticket, &result);
printf("响应 %u 字节, status=%d\n", result.response_len, result.status);
}
}
/* 查询邮箱统计 */
ec_mbx_stats_t st = {0};
if (ec_mbx_get_stats(&dll, master, 1, 0, &st) == 0) {
printf("发送=%llu 接收=%llu 超时=%llu\n",
(unsigned long long)st.total_sent,
(unsigned long long)st.total_received,
(unsigned long long)st.total_timeout);
}