SDO Segmented 传输详解
概述
当数据 > 4 字节 时,必须使用 Segmented(分段)传输。每段 CAN 帧最多携带 7 字节有效数据(字节 1-7)。
典型场景:
- 读取制造商设备名称(0x1008,字符串)
- 读取固件版本号(0x100A,字符串)
- 写入大量配置参数
- 固件更新(通常使用 Block 传输,但 Segmented 也可以)
Segmented Upload 流程
三阶段
┌──────────────┐
│ 1. Initiate │ Client 发送 Upload Request
│ Upload │ Server 响应第一段数据
├──────────────┤
│ 2. Segment │ Client 发送 "继续" 命令
│ Transfer │ Server 发送下一段
│ (循环) │ Toggle 位翻转
├──────────────┤
│ 3. End │ Server 发送最后一段
│ Transfer │ 标记 "结束" 位
└──────────────┘阶段 1:Initiate Upload
Request(Client -> Server):
COB-ID: 0x600 + node_id
DLC: 8
Byte 0: 0x40
- CCS = 001 (Upload)
Byte 1-2: Index(小端)
Byte 3: Sub-index
Byte 4-7: 0x00Response(Server -> Client):
COB-ID: 0x580 + node_id
DLC: 8
Byte 0: 0x00 | (s << 1) | (e << 0)
- SCS = 000 (Segmented Response)
- s=1, e=0 = 第一段,非最后一段
Byte 1-2: Index(小端)
Byte 3: Sub-index
Byte 4-7: 数据段 0(最多 4 字节,因为 Initiate 响应没有 Toggle 位)等等,让我更正。Initiate Upload Response 的格式实际上与 Expedited 类似:
Byte 0: 0x40 | (n << 2) | (s << 1) | (e << 0)
- SCS = 011
- e=0 = Segmented 模式
- s=1 = 有数据
- n = 未传输字节数阶段 2:Segment Transfer(循环)
Request(Client -> Server):
COB-ID: 0x600 + node_id
DLC: 8
Byte 0: 0x60 | (c << 4) | (toggle << 1)
- CCS = 010 (Segment)
- c=0 = 继续传输
- toggle = 0 或 1(交替翻转)
Byte 1-7: 0x00(填充)Response(Server -> Client):
COB-ID: 0x580 + node_id
DLC: 8
Byte 0: 0x00 | (toggle << 1) | (e << 0)
- SCS = 000 (Segment)
- toggle = 与 Client Request 匹配
- e=0 = 非最后一段
Byte 1-7: 数据段 N阶段 3:End Transfer
Response(最后一段,Server -> Client):
COB-ID: 0x580 + node_id
DLC: 8
Byte 0: 0x00 | (toggle << 1) | (e << 0)
- e=1 = 最后一段
Byte 1-(7-n): 数据(最后一段可能不满 7 字节)
Byte (8-n)-7: 0x00(填充,n 字节)Toggle 位机制
Toggle 位是 Segmented 传输的核心机制,用于检测丢包和失步:
- 初始值:0
- 每发送/接收一段后翻转(0 -> 1 -> 0 -> 1…)
- 如果收到的 Toggle 位与预期不匹配 -> Abort 传输
Client Server
│ │
│── Initiate Upload ──────>│
│<── Segment 0 (t=0) ─────│
│ │
│── Segment (t=0) ────────>│
│<── Segment 1 (t=1) ─────│
│ │
│── Segment (t=1) ────────>│
│<── Segment 2 (t=0) ─────│
│ │
│── Segment (t=0) ────────>│
│<── Segment 3 (t=1, e=1)─│ <-- 最后一段
│ │失步示例:
如果 Client 发送 t=0 的 Segment 命令,但 Server 回复 t=0(期望收到 t=1),说明中间丢失了一段。Client 应发送 Abort。
完整示例:读取制造商设备名称(0x1008sub0)
假设设备名称为 "RobotJoint-6DOF"(16 字节,含终止符 \0)。
时序图
Client Server (Node 3)
│ │
│ [603] 40 08 10 00 00 00 00 00 │
│─── Initiate Upload ────────────────>│
│ │ 查找 0x1008sub0
│ │ 数据: "RobotJoint-6DOF\0"
│ │ 16 字节,需 Segmented
│ │
│ [583] 00 08 10 00 52 6F 62 6F │
│<── Initiate Response (t=0) ─────────│
│ 数据: "Robo" (4 bytes) │
│ │
│ [603] 60 00 00 00 00 00 00 00 │
│─── Segment Request (t=0) ──────────>│
│ │
│ [583] 02 74 4A 6F 69 6E 74 2D │
│<── Segment (t=1) ───────────────────│
│ 数据: "tJoint-" (7 bytes) │
│ │
│ [603] 62 00 00 00 00 00 00 00 │
│─── Segment Request (t=1) ──────────>│
│ │
│ [583] 03 36 44 4F 46 00 00 00 │
│<── Segment (t=0, e=1) ──────────────│
│ 数据: "6DOF\0" (5 bytes) │
│ n=2 (2 bytes padding) │
│ │
│ 完整字符串: "RobotJoint-6DOF\0" │逐帧解析:
帧 1:Initiate Upload Request
603 [8] 40 08 10 00 00 00 00 00
│ │ │ │ └─ 填充
│ │ │ └──── 子索引 0
│ │ └──────── 索引 0x1008
│ └─────────── CCS=1 (Upload)
└────────────── 命令字节帧 2:Initiate Upload Response
583 [8] 00 08 10 00 52 6F 62 6F
│ │ │ │ │ │ │ └─ 'o' (0x6F)
│ │ │ │ │ │ └──── 'b' (0x62)
│ │ │ │ │ └─────── 'o' (0x6F)
│ │ │ │ └────────── 'R' (0x52)
│ │ │ └───────────── 子索引 0
│ │ └───────────────── 索引 0x1008
│ └───────────────────── SCS=000, s=0, e=0 (Segmented)
└──────────────────────── 命令字节 0x00字节 0 = 0x00:s=0, e=0 表示这是 Segmented 传输的第一段(Initiate Response 的 s/e 位在某些实现中为 0)。
帧 3:Segment Request (t=0)
603 [8] 60 00 00 00 00 00 00 00
│ └─ CCS=010 (Segment), c=0 (继续), t=0帧 4:Segment Response (t=1)
583 [8] 02 74 4A 6F 69 6E 74 2D
│ │ │ │ │ │ │ └─ '-' (0x2D)
│ │ │ │ │ │ └──── 't' (0x74)
│ │ │ │ │ └────── 'n' (0x6E)
│ │ │ │ └──────── 'i' (0x69)
│ │ │ └────────── 'o' (0x6F)
│ │ └──────────── 'J' (0x4A)
│ └────────────── 't' (0x74)
└───────────────── 0x02 = t=1, e=0字节 0 = 0x02:0000 0010 = toggle=1, e=0(非最后一段)
帧 5:Segment Request (t=1)
603 [8] 62 00 00 00 00 00 00 00
│ └─ CCS=010 (Segment), c=0 (继续), t=1帧 6:Segment Response (t=0, e=1) – 最后一段
583 [8] 03 36 44 4F 46 00 00 00
│ │ │ │ │ └─ 填充
│ │ │ │ └──── 填充
│ │ │ └─────── 'F' (0x46)
│ │ └────────── 'O' (0x4F)
│ └──────────── '6' (0x36)
└───────────────── 0x03 = t=0, e=1 (最后一段, n=2)字节 0 = 0x03:0000 0011 = toggle=0, e=1, n=2(2 字节填充)
最后一段有 5 字节有效数据:0x36 0x44 0x4F 0x46 0x00 = “6DOF\0”
Segmented Download 流程
Segmented Download 与 Upload 类似,但方向相反:
Client Server
│ │
│ [603] 20 XX XX XX 00 00 00 00 │ XX = 0 (Segmented)
│─── Initiate Download ──────────────>│
│ │
│ [583] 60 XX XX XX 00 00 00 00 │
│<── Initiate Response ───────────────│
│ │
│ [603] 00|t D1 D2 D3 D4 D5 D6 D7 │ 段 0 (t=0)
│─── Segment ────────────────────────>│
│ │
│ [583] 20 XX XX XX 00 00 00 00 │
│<── Segment Response (t=0) ─────────│
│ │
│ [603] 01|t D1 D2 ... Dk 00 ... │ 最后段 (t=1, e=1)
│─── Segment (e=1) ─────────────────>│
│ │
│ [583] 60 XX XX XX 00 00 00 00 │
│<── Download Complete ──────────────│Segment Download Request 字节 0:
Bit 7-5: 000 = CCS (Segment)
Bit 4: c=0 (继续) / c=1 (最后一段)
Bit 3: 0 (保留)
Bit 2-0: n (未传输字节数,仅最后一段)实际上,Segment 命令的字节 0 格式为:
Download Segment:
Bit 7-5: 000 (CCS)
Bit 4: c (0=继续, 1=最后一段)
Bit 3: 0
Bit 2: n (最后一段才有意义)
Bit 1: toggle
Bit 0: 0
Upload Segment:
Bit 7-5: 000 (SCS)
Bit 4-3: 00
Bit 2: n (最后一段)
Bit 1: toggle
Bit 0: e (1=最后一段)让我重新表述:
Client Segment Download 命令
Byte 0: (c << 4) | (n << 0) | (toggle << 1)更准确地说:
c=1, e=1(最后一段): 字节 0 =0x10 | (n << 2) | (toggle << 1)c=0(继续): 字节 0 =0x00 | (toggle << 1)
标准格式:
Upload request: 0x40 + index + sub
Upload response: SCS + index + sub + data
Download request: CCS + index + sub + data
Download response: 0x60 + index + sub + 0x00对于 Segmented:
Client -> Server (Segment):
字节 0:
Bit 7-5: CCS = 010 (Segment transfer)
Bit 4: c=0 (more segments) / c=1 (last segment)
Bit 3-1: n (last segment only, bytes not in use)
Bit 0: toggle
Wait, this is getting complex. Let me use the correct standard format.让我使用正确的 CiA 301 规范格式:
正确的 Segmented 命令格式
Client Segment Upload (Requesting more data from server):
CCS = 011 (3)
Byte 0 = 0x60 | (toggle << 1)Server Segment Upload Response:
SCS = 000 (0)
Byte 0 = (n << 2) | (toggle << 1) | eClient Segment Download:
CCS = 000 (0)
Byte 0 = (c << 5) | (n << 2) | (toggle << 1)Server Segment Download Response:
SCS = 010 (2)
Byte 0 = 0x20 | (toggle << 1)命令行实操
读取软件版本号(> 4 字节字符串)
# 启动监听
candump vcan0 &
# 发送 Initiate Upload 请求(0x100Asub0,软件版本)
cansend vcan0 603#400A100000000000
# 观察 Initiate Response(可能包含前 4 字节)
# vcan0 583 [8] 00 0A 10 00 32 2E 31 2E
# 数据: "2.1." (4 bytes)
# 发送 Segment Request (t=0)
cansend vcan0 603#6000000000000000
# 观察下一段
# vcan0 583 [8] 02 30 00 00 00 00 00 00
# 数据: "0\0" (2 bytes, 最后一段)
# 完整版本号: "2.1.0\0"代码实现
Segmented Upload(客户端)
int sdo_upload_segmented(int sock, uint8_t node_id,
uint16_t index, uint8_t sub_index,
uint8_t *buf, size_t buf_size, size_t *out_len) {
struct can_frame req, resp;
uint8_t toggle = 0;
size_t total = 0;
canid_t req_cobid = 0x600 + node_id;
canid_t resp_cobid = 0x580 + node_id;
/* 1. Initiate Upload */
memset(&req, 0, sizeof(req));
req.can_id = req_cobid;
req.can_dlc = 8;
req.data[0] = 0x40;
req.data[1] = index & 0xFF;
req.data[2] = (index >> 8) & 0xFF;
req.data[3] = sub_index;
write(sock, &req, sizeof(req));
if (read(sock, &resp, sizeof(resp)) < 0) {
return -1;
}
/* 检查是否为 Abort */
if (resp.data[0] == 0x80) {
uint32_t abort_code = resp.data[4] | (resp.data[5] << 8) |
(resp.data[6] << 16) | (resp.data[7] << 24);
fprintf(stderr, "SDO Abort: 0x%08X\n", abort_code);
return -1;
}
/* 检查是否为 Expedited */
if (resp.data[0] & 0x03 == 0x03) {
/* Expedited 响应 */
int n = (resp.data[0] >> 2) & 0x07;
*out_len = 4 - n;
memcpy(buf, &resp.data[4], *out_len);
return 0;
}
/* 2. Segmented Transfer */
for (;;) {
/* 从 Initiate Response 中提取第一段数据 */
int data_len = 4; /* Initiate Response 有 4 字节数据 */
if (total + data_len > buf_size) {
fprintf(stderr, "Buffer too small\n");
return -1;
}
memcpy(buf + total, &resp.data[4], data_len);
total += data_len;
/* 检查是否结束 */
if (resp.data[0] & 0x01) { /* e=1 */
int n = (resp.data[0] >> 2) & 0x07;
total -= n; /* 减去填充字节 */
*out_len = total;
return 0;
}
/* 发送 Segment Request */
memset(&req, 0, sizeof(req));
req.can_id = req_cobid;
req.can_dlc = 8;
req.data[0] = 0x60 | (toggle << 1);
write(sock, &req, sizeof(req));
if (read(sock, &resp, sizeof(resp)) < 0) {
return -1;
}
/* 验证 Toggle 位 */
if ((resp.data[0] >> 1 & 0x01) != toggle) {
fprintf(stderr, "Toggle bit mismatch\n");
return -1;
}
/* 准备下一次 Toggle */
toggle = 1 - toggle;
}
}总结
| 维度 | 说明 |
|---|---|
| 数据量 | > 4 字节 |
| 每段数据 | 最多 7 字节 |
| 阶段 | Initiate -> Segment (循环) -> End |
| Toggle 位 | 0 -> 1 -> 0 -> 1… 交替翻转 |
| 最后一段标记 | e=1 + n(填充字节数) |
| Abort 条件 | Toggle 位不匹配、超时、Abort 帧 |
支付宝
微信