3. EtherCAT数据帧结构:帧头、数据报头、数据报内容、帧尾
好,咱们今天来聊聊EtherCAT数据帧的结构。说实话,搞懂这个,你才算真正入了EtherCAT的门。我刚开始接触的时候,看着那一串十六进制数据,头都大了。后来拆开一看,其实就四个部分:帧头、数据报头、数据报内容、帧尾。说白了,就像寄快递——写地址、贴单子、塞东西、封箱。
3.1 帧头(Ethernet Header)
EtherCAT数据帧,本质上还是跑在标准以太网上的。所以它最外面那层,就是标准的以太网帧头。嗯,这里要注意,EtherCAT的以太网帧类型(EtherType)是 0x88A4。这个值很关键,抓包的时候你看到它,就知道这是EtherCAT的报文了。
帧头一共14个字节,结构如下:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 目标MAC地址 | 6 | 从站或主站的MAC |
| 源MAC地址 | 6 | 主站或从站的MAC |
| EtherType | 2 | 固定为0x88A4 |
我个人习惯,抓包时第一眼就看EtherType。如果不是0x88A4,后面就不用看了。有一次我在现场排查问题,客户说EtherCAT通信断了,我一看抓包文件,好家伙,全是0x0800(IPv4)的报文。原来是交换机把EtherCAT帧当普通IP包给丢了。你说气不气人?
3.2 数据报头(Datagram Header)
帧头后面,紧接着就是数据报头。一个EtherCAT帧里可以塞多个数据报(Datagram),每个数据报都有自己的报头。数据报头10个字节,结构如下:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 数据报长度 | 2 | 数据报内容的总长度(字节) |
| 保留位 | 1 | 保留,通常为0 |
| 类型 | 1 | 数据报类型(如APRD、FPWR等) |
| 索引 | 1 | 用于匹配请求和响应 |
| 从站地址 | 4 | 目标从站的地址 |
| 偏移地址 | 1 | 从站内部寄存器或内存的偏移 |
这里有个坑,我必须要说。数据报长度这个字段,它指的是后面数据报内容的长度,不包括报头本身。我曾经见过一个新手,算长度的时候把报头也算进去了,结果从站死活不响应。你想想看,从站收到报文,发现长度对不上,直接就扔了。
3.3 数据报内容(Datagram Payload)
数据报头后面,就是真正的数据了。这部分长度不固定,由前面的数据报长度字段决定。内容可以是过程数据(PDO)、邮箱数据(SDO)、配置数据等等。
举个例子,如果你要读取一个从站的某个寄存器,数据报内容就是空的(读操作不需要数据)。但如果是写操作,数据报内容里就塞着你想要写入的值。
我记得有一次调试伺服驱动器,我需要写一个32位的目标位置值。数据报内容就是4个字节,按小端序排列。比如你要写0x12345678,那在报文里就是 78 56 34 12。这个顺序搞反了,电机就跑到天边去了。嗯,别问我怎么知道的。
3.4 帧尾(FCS)
最后,就是帧尾。它其实就是一个4字节的CRC32校验值,用来保证数据在传输过程中没被篡改。这个由网卡硬件自动计算和校验,咱们做软件的一般不用管它。但如果你抓包发现CRC校验失败,那就要检查一下物理层了——网线是不是太长?接头是不是松了?
帧尾结构很简单:
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| FCS | 4 | CRC32校验值 |
核心要点:一个完整的EtherCAT数据帧 = 14字节帧头 + N个(10字节数据报头 + 数据报内容) + 4字节帧尾。N可以是1到多个,但总长度不能超过以太网最大帧长1518字节。
3.5 实战:用Python解析一个EtherCAT帧
光说不练假把式。咱们写段Python代码,把抓到的原始数据拆开看看。假设我们从Wireshark里导出了一段原始数据:
import struct
# 假设这是从网卡抓到的原始EtherCAT帧(十六进制)
raw_data = bytes.fromhex(
"FFFFFFFFFFFF 001122334455 88A4" # 帧头
"0010 00 01 03 00000001 00" # 数据报头
"0102030405060708090A0B0C0D0E0F10" # 数据报内容(16字节)
"12345678" # 帧尾(FCS)
)
def parse_ethercat_frame(data):
# 解析帧头
dst_mac = data[0:6].hex()
src_mac = data[6:12].hex()
ether_type = struct.unpack('!H', data[12:14])[0]
print(f"目标MAC: {dst_mac}")
print(f"源MAC: {src_mac}")
print(f"EtherType: 0x{ether_type:04X}")
if ether_type != 0x88A4:
print("这不是EtherCAT帧!")
return
# 跳过帧头,开始解析数据报
offset = 14
while offset < len(data) - 4: # 留4字节给FCS
# 解析数据报头
datagram_len = struct.unpack('<H', data[offset:offset+2])[0]
reserved = data[offset+2]
cmd_type = data[offset+3]
index = data[offset+4]
slave_addr = struct.unpack('<I', data[offset+5:offset+9])[0]
offset_addr = data[offset+9]
print(f"\n数据报长度: {datagram_len}")
print(f"命令类型: 0x{cmd_type:02X}")
print(f"从站地址: 0x{slave_addr:08X}")
# 解析数据报内容
payload_start = offset + 10
payload = data[payload_start:payload_start + datagram_len]
print(f"数据内容: {payload.hex()}")
# 移动到下一个数据报
offset = payload_start + datagram_len
# 解析帧尾
fcs = struct.unpack('<I', data[-4:])[0]
print(f"\nFCS校验值: 0x{fcs:08X}")
parse_ethercat_frame(raw_data)
运行这段代码,你会看到每个字段都被拆解出来了。我个人建议,你在调试的时候,把这段代码集成到你的测试工具里。遇到通信问题,直接跑一下,看看数据报头里的命令类型对不对,从站地址对不对。很多时候,问题就出在这些细节上。
小技巧:抓包时,可以在Wireshark里设置过滤条件 eth.type == 0x88A4,这样只显示EtherCAT帧,省得被其他网络流量干扰。
避坑指南:我曾经遇到过一个问题,从站返回的数据报长度和主站期望的不一致。排查了半天,发现是从站的固件版本太旧,处理数据报时多塞了几个字节。所以,如果你发现数据对不上,先检查一下从站的固件版本。
好了,EtherCAT数据帧的结构就讲到这里。说白了,它就是标准以太网帧套了个EtherCAT的壳。你只要记住帧头、数据报头、数据报内容、帧尾这四个部分,再配合抓包工具和代码,基本就能搞定大部分问题了。下一章,咱们聊聊数据报的类型——读、写、读写,还有那些奇奇怪怪的组合。