3. SDO通信机制与报文格式
SDO(Service Data Object)是 CoE 中用于访问对象字典“单个条目”的通信方式,与 PDO 的周期性过程数据不同,SDO 是请求—应答式的非周期通信,适合读写参数、配置、诊断信息等。理解 SDO,关键是抓住两条主线:一是访问流程(初始化、分段、完整访问),二是报文格式(命令字、索引、子索引、数据区)。把这两条主线打通,就能自己实现一个 SDO 读取工具。
一、SDO 的两种访问方式
对象字典条目的数据长度决定了 SDO 采用哪种访问方式。若数据长度不超过 4 字节,可用快速 SDO(Expedited SDO),数据直接塞进报文的数据区,一次请求—应答即可完成;若数据长度超过 4 字节,则用分段 SDO(Segmented SDO),先交换长度信息,再按段传输,每段最多 7 字节有效数据。此外还有完整访问(Complete Access),它允许一次访问某个对象下的全部子索引,常用于读取整个数组或记录型对象。
二、SDO 报文结构
一个 SDO 报文由 8 字节 CANopen 数据帧承载,结构如下:
| 字节 | 字段 | 说明 |
|---|---|---|
| 0 | 命令字(CCS/SCS) | 区分上传/下载、快速/分段、请求/应答 |
| 1–2 | 索引(Index) | 对象字典主索引,小端 |
| 3 | 子索引(SubIndex) | 对象下的子项编号 |
| 4–7 | 数据区 | 快速访问时存放数据或长度信息 |
命令字是 SDO 的“语义核心”。以客户端(主站)发起为例,上传(读)请求命令字为 0x40;下载(写)请求则根据数据长度使用 0x23(4 字节)、0x27(3 字节)、0x2B(2 字节)、0x2F(1 字节)。服务器(从站)应答时,命令字会带上“快速/分段”和“数据长度”标志,例如上传应答 0x43 表示快速访问且数据 4 字节,0x4B 表示 2 字节,0x4F 表示 1 字节。分段传输时,命令字中的 bit0 表示“还有后续段”,bit4 表示“切换位(toggle)”,用于保证段序正确。
三、上传(读取)流程
以读取 0x1018:01(厂商 ID,4 字节)为例,主站发送上传请求:
请求: 40 18 10 01 00 00 00 00
↑ ↑ ↑
| | └─ 子索引 01
| └─ 索引 0x1018(小端 18 10)
└─ 命令字 0x40(上传请求)
应答: 43 18 10 01 78 56 34 12
↑ ↑
| └─ 数据区:0x12345678(厂商 ID)
└─ 命令字 0x43(快速上传应答,4 字节)
若对象数据超过 4 字节,应答命令字会变为 0x41(分段上传初始化应答),数据区给出总长度;随后主站发送段请求 0x60,从站以 0x00/0x10 等命令字返回每段 7 字节数据,直到最后一段命令字 bit0 为 0 表示结束。
四、下载(写入)流程
写入 0x1018:01 时,主站发送下载请求,命令字按数据长度选择:
请求: 23 18 10 01 78 56 34 12
↑ ↑
| └─ 待写入数据
└─ 命令字 0x23(下载请求,4 字节)
应答: 60 18 10 01 00 00 00 00
↑
└─ 命令字 0x60(下载应答,成功)
若写入失败,应答命令字为 0x80,数据区返回 4 字节中止码(Abort Code),例如 0x06010002 表示“试图写只读对象”,0x06070010 表示“数据类型不匹配”。中止码是排障时最重要的线索。
五、完整访问(Complete Access)
完整访问通过命令字中的 bit4(CA 位)标识。上传时命令字为 0x40 | 0x10 = 0x50,下载时为 0x23 | 0x10 = 0x33。它一次读取对象下所有子索引,数据按子索引顺序拼接,长度由对象定义决定。完整访问适合批量读取 PDO 映射、同步管理器配置等结构化对象,能显著减少报文往返。
六、实现一个 SDO 读取工具
下面给出一个基于 Python 的 SDO 读取工具核心逻辑,假设底层已有 EtherCAT 主站库可发送/接收 CANopen 帧。工具接收索引、子索引,自动判断快速或分段访问,并解析应答。
def sdo_upload(index, subindex):
# 构造上传请求:命令字 0x40 + 索引小端 + 子索引
req = bytes([0x40, index & 0xFF, (index >> 8) & 0xFF, subindex, 0, 0, 0, 0])
resp = send_sdo(req) # 底层发送并等待应答
cmd = resp[0]
if cmd == 0x43: # 快速上传,4 字节
return int.from_bytes(resp[4:8], 'little')
elif cmd == 0x4B: # 快速上传,2 字节
return int.from_bytes(resp[4:6], 'little')
elif cmd == 0x4F: # 快速上传,1 字节
return resp[4]
elif cmd == 0x41: # 分段上传初始化
total = int.from_bytes(resp[4:8], 'little')
data = b''
toggle = 0
while True:
seg_req = bytes([0x60 | (toggle << 4), 0, 0, 0, 0, 0, 0, 0])
seg = send_sdo(seg_req)
data += seg[1:8] # 每段 7 字节有效数据
if not (seg[0] & 0x01): # bit0=0 表示最后一段
break
toggle ^= 1
return int.from_bytes(data[:total], 'little')
elif cmd == 0x80: # 中止
abort = int.from_bytes(resp[4:8], 'little')
raise Exception(f"SDO Abort: 0x{abort:08X}")
else:
raise Exception(f"未知命令字: 0x{cmd:02X}")
使用示例:读取 0x1018:01 厂商 ID,调用 sdo_upload(0x1018, 0x01),若返回 0x12345678,说明读取成功。若抛出中止码 0x06010002,则说明该对象只读,需检查是否误用了下载操作。
七、关键判断与排障要点
- 命令字不匹配:请求与应答命令字必须成对出现,若收到
0x80应立即解析中止码。 - 长度判断:快速访问数据区有效字节数由命令字 bit2–3 决定,解析时不能固定取 4 字节。
- 分段 toggle:每段 toggle 位必须交替,若从站返回 toggle 错误,说明段序丢失,需重传。
- 完整访问:CA 位置 1 后,数据区长度可能超过 7 字节,需按对象定义的总长度循环接收。
- 字节序:索引、子索引、数据均按小端解析,跨平台实现时务必统一。
掌握以上流程与报文格式后,SDO 读取工具的核心就是“构造请求—发送—解析命令字—按长度取数据—处理分段与中止”。实际工程中,建议把命令字、索引、子索引、数据区封装成结构体,并记录每次请求的原始报文,便于对照从站手册排查问题。