3. 非周期性数据通信模型:主站与从站之间的邮箱通信机制
好,咱们今天聊聊非周期性数据通信。说白了,就是邮箱通信。
EtherCAT 跑周期数据的时候,那叫一个快,几十微秒一个循环。但有些数据,比如参数配置、固件升级、诊断信息,它们不需要每个周期都传。这时候,邮箱通信就派上用场了。
我个人习惯把邮箱通信想象成「邮局」。主站是发件人,从站是收件人。数据包就是信件,得装进信封,写上地址,然后投递。这个「信封」,就是 EtherCAT 邮箱数据报文的格式。
3.1 邮箱通信的基本模型
先看个整体框架。我画了一张图,帮你理解数据是怎么流动的。
你看,主站和每个从站之间,都有一条独立的邮箱通道。主站发数据,从站收。从站回数据,主站收。一来一回,完成一次非周期性通信。
这里有个关键点:邮箱通信是双向的,但不同步。主站发完命令,不会傻等。它会在下一个周期或者合适的时机,再去读取从站的响应。这种异步机制,让 EtherCAT 在跑周期数据的同时,还能优雅地处理非周期数据。
3.2 邮箱数据报文结构
嗯,咱们得看看这个「信封」长什么样。EtherCAT 邮箱报文有固定的格式,我直接给你拆开看。
| 字节偏移 | 长度(字节) | 字段名称 | 说明 |
|---|---|---|---|
| 0 | 2 | 长度 | 邮箱数据的总长度(不包括头部) |
| 2 | 2 | 地址 | 目标从站地址或主站地址 |
| 4 | 1 | 通道编号 | 标识邮箱通道(通常为 0) |
| 5 | 1 | 优先级 | 0 = 紧急, 1 = 高, 2 = 中, 3 = 低 |
| 6 | 2 | 类型 | 邮箱协议类型(CoE, FoE, SoE, AoE 等) |
| 8 | N | 数据 | 实际的邮箱协议数据 |
这个头部一共 8 个字节。我当年第一次看协议栈源码时,就被这个结构搞晕过。后来发现,其实你只需要关注三个字段:长度、类型、数据。地址和通道编号,在大多数场景下都是固定的。
3.3 邮箱通信的握手流程
通信不是乱来的,得有规矩。邮箱通信的握手流程,我总结为三步:
- 主站写邮箱命令:主站把数据写入从站的邮箱寄存器。
- 从站处理数据:从站检测到邮箱有数据,开始处理。
- 主站读邮箱响应:主站轮询从站的邮箱状态,读取响应数据。
你想想看,这个过程像不像你发邮件?你写邮件(主站写),对方看到后回复(从站处理),你再打开邮箱看回复(主站读)。
但这里有个坑:主站怎么知道从站处理完了?
答案是:状态位。每个从站的邮箱都有两个关键状态位:
- 写指示位:主站写入数据后置位,告诉从站「有你的信」。
- 读指示位:从站写入响应后置位,告诉主站「回信写好了」。
主站通过轮询这些状态位,来判断什么时候可以读取响应。嗯,这里要注意:轮询间隔不能太短,否则会浪费总线带宽。我一般建议至少间隔 100 微秒以上。
3.4 邮箱协议类型
邮箱里装的内容,可以是不同协议。EtherCAT 定义了多种邮箱协议,我列个表给你看:
| 协议类型 | 类型值 | 用途 | 典型场景 |
|---|---|---|---|
| CoE | 0x01 | CANopen over EtherCAT | SDO 参数读写、PDO 映射配置 |
| FoE | 0x02 | File over EtherCAT | 固件升级、配置文件下载 |
| SoE | 0x03 | Servo over EtherCAT | 伺服驱动器参数配置 |
| AoE | 0x04 | ADS over EtherCAT | Beckhoff 专用协议 |
| VoE | 0x05 | Vendor over EtherCAT | 厂商自定义协议 |
最常用的是 CoE。说白了,它就是 CANopen 协议跑在 EtherCAT 邮箱上。你如果做过 CANopen,上手 CoE 会非常快。
ec_coe_write() 和 ec_coe_read()。这样上层应用不用关心邮箱细节,直接传 SDO 索引和子索引就行。代码复用率很高。
3.5 邮箱通信的代码示例
光说不练假把式。我给你看一段伪代码,模拟主站通过邮箱写一个 SDO 的过程。
// 主站发送邮箱数据到从站
int ec_mailbox_write(uint16_t slave_addr, uint8_t *data, uint16_t len) {
// 1. 检查从站邮箱是否空闲
if (check_mailbox_busy(slave_addr)) {
return -1; // 邮箱忙,稍后重试
}
// 2. 构造邮箱头部
mailbox_header_t header;
header.length = len;
header.address = slave_addr;
header.channel = 0;
header.priority = 2; // 中优先级
header.type = 0x01; // CoE 协议
// 3. 写入邮箱数据寄存器
write_mailbox_reg(slave_addr, &header, sizeof(header));
write_mailbox_reg(slave_addr, data, len);
// 4. 置位写指示位
set_write_indicator(slave_addr, 1);
return 0;
}
// 主站读取从站邮箱响应
int ec_mailbox_read(uint16_t slave_addr, uint8_t *buffer, uint16_t *len) {
// 1. 检查读指示位
if (!get_read_indicator(slave_addr)) {
return -1; // 从站还没准备好响应
}
// 2. 读取邮箱头部
mailbox_header_t header;
read_mailbox_reg(slave_addr, &header, sizeof(header));
// 3. 读取邮箱数据
*len = header.length;
read_mailbox_reg(slave_addr, buffer, *len);
// 4. 清除读指示位
clear_read_indicator(slave_addr);
return 0;
}
这段代码看起来简单,但实际项目中要考虑超时重传、错误处理、多从站并发等问题。我曾经在一个项目中,因为忘记清除读指示位,导致主站一直读到旧数据。排查了半天才发现是这个小问题。
3.6 邮箱通信的性能考量
最后聊点实际的。邮箱通信虽然方便,但性能有限。我总结了几点:
- 最大数据长度:标准邮箱报文最大 1486 字节(包括头部)。超过这个长度,需要分片传输。
- 通信延迟:一次邮箱读写,至少需要 2 个 EtherCAT 周期(写 + 读)。如果加上从站处理时间,通常在 100 微秒到几毫秒之间。
- 并发限制:一个从站同一时间只能处理一个邮箱事务。如果你想同时读写多个从站,主站需要排队处理。
所以,我的建议是:邮箱通信只用于配置和诊断,不要用于实时控制。实时数据走周期通信,非实时数据走邮箱。各司其职,系统才能稳定高效。
好了,邮箱通信的机制就讲到这里。你理解了这些,后面再看 CoE、FoE 这些具体协议,就会轻松很多。