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: 0x00

Response(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) | e

Client 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 帧
如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
Cygnus Yang 支付宝支付宝
Cygnus Yang 微信微信