3. 非周期性数据通信模型:主站与从站之间的邮箱通信机制

好,咱们今天聊聊非周期性数据通信。说白了,就是邮箱通信。

EtherCAT 跑周期数据的时候,那叫一个快,几十微秒一个循环。但有些数据,比如参数配置、固件升级、诊断信息,它们不需要每个周期都传。这时候,邮箱通信就派上用场了。

我个人习惯把邮箱通信想象成「邮局」。主站是发件人,从站是收件人。数据包就是信件,得装进信封,写上地址,然后投递。这个「信封」,就是 EtherCAT 邮箱数据报文的格式。

3.1 邮箱通信的基本模型

先看个整体框架。我画了一张图,帮你理解数据是怎么流动的。

EtherCAT 主站 (发送/接收邮箱数据) 从站 1 邮箱通道 从站 2 邮箱通道 从站 3 邮箱通道 写邮箱命令 读邮箱响应 写邮箱命令 读邮箱响应 写邮箱命令 读邮箱响应 图例 主站 → 从站(写) 从站 → 主站(读) 邮箱通信模型

你看,主站和每个从站之间,都有一条独立的邮箱通道。主站发数据,从站收。从站回数据,主站收。一来一回,完成一次非周期性通信。

这里有个关键点:邮箱通信是双向的,但不同步。主站发完命令,不会傻等。它会在下一个周期或者合适的时机,再去读取从站的响应。这种异步机制,让 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 个字节。我当年第一次看协议栈源码时,就被这个结构搞晕过。后来发现,其实你只需要关注三个字段:长度、类型、数据。地址和通道编号,在大多数场景下都是固定的。

重要提示:邮箱数据长度字段,指的是「数据」部分的长度,不包括头部。如果你算错了,从站会直接丢弃报文。我曾经因为这个 bug 调了整整一个下午。

3.3 邮箱通信的握手流程

通信不是乱来的,得有规矩。邮箱通信的握手流程,我总结为三步:

  1. 主站写邮箱命令:主站把数据写入从站的邮箱寄存器。
  2. 从站处理数据:从站检测到邮箱有数据,开始处理。
  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 会非常快。

我的经验:在项目中,我习惯把 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;
}

这段代码看起来简单,但实际项目中要考虑超时重传、错误处理、多从站并发等问题。我曾经在一个项目中,因为忘记清除读指示位,导致主站一直读到旧数据。排查了半天才发现是这个小问题。

避坑指南:千万不要在主站写邮箱后立即读取响应。从站需要时间处理。我建议至少等待 1 毫秒再轮询。如果从站是低速 MCU,这个时间可能要延长到 5-10 毫秒。

3.6 邮箱通信的性能考量

最后聊点实际的。邮箱通信虽然方便,但性能有限。我总结了几点:

  • 最大数据长度:标准邮箱报文最大 1486 字节(包括头部)。超过这个长度,需要分片传输。
  • 通信延迟:一次邮箱读写,至少需要 2 个 EtherCAT 周期(写 + 读)。如果加上从站处理时间,通常在 100 微秒到几毫秒之间。
  • 并发限制:一个从站同一时间只能处理一个邮箱事务。如果你想同时读写多个从站,主站需要排队处理。

所以,我的建议是:邮箱通信只用于配置和诊断,不要用于实时控制。实时数据走周期通信,非实时数据走邮箱。各司其职,系统才能稳定高效。

好了,邮箱通信的机制就讲到这里。你理解了这些,后面再看 CoE、FoE 这些具体协议,就会轻松很多。


专注资料整理