SDO Expedited 传输详解

概述

Expedited(快速)传输是 SDO 中最简单的数据传输方式,适用于 ≤ 4 字节 的数据。它的特点是:

  • 一次请求 + 一次响应
  • 数据直接嵌入 CAN 帧(字节 4-7)
  • 无需分段和确认机制

适用场景:

  • 读取设备类型(0x1000,4 字节)
  • 读取错误寄存器(0x1001,1 字节)
  • 写入配置参数(如心跳周期 0x1017,2 字节)

Expedited Upload 流程

1. 请求帧(Client -> Server)

COB-ID: 0x600 + node_id
DLC: 8

Byte 0: 0x40 | (n << 2) | 0x03
  - Bit 7-5: CCS = 001 (Upload)
  - Bit 4-2: n = 未传输字节数(0-3)
  - Bit 1-0: s=1, e=1 (标记为 Expedited 且有数据)

Byte 1-2: Index(小端模式)
Byte 3: Sub-index
Byte 4-7: 0x00(填充)

简化的请求字节 0:

对于 Expedited Upload,通常直接写 0x40,由服务端决定是否使用 Expedited 模式。

2. 响应帧(Server -> Client)

COB-ID: 0x580 + node_id
DLC: 8

Byte 0: 0x40 | (n << 2) | 0x03
  - Bit 7-5: SCS = 011 (Upload Response)
  - Bit 4-2: n = 未传输字节数
  - Bit 1-0: s=1, e=1

Byte 1-2: Index(小端模式)
Byte 3: Sub-index
Byte 4-(7-n): 数据
Byte (8-n)-7: 0x00(填充)

3. 逐字节解析示例

场景:读取节点 3 的设备类型(0x1000sub0)

请求:

candump vcan0
  vcan0  603  [8]  40 00 10 00 00 00 00 00
         │    │   │  │  │  │  └─ 填充字节 3
         │    │   │  │  │  └──── 填充字节 2
         │    │   │  │  └─────── 填充字节 1
         │    │   │  └────────── 填充字节 0
         │    │   └───────────── 子索引 0x00
         │    └───────────────── 索引 0x1000(小端:00 10)
         └────────────────────── CCS=001, Expedited 标记

响应:

candump vcan0
  vcan0  583  [8]  4B 00 10 00 92 01 01 00
         │    │   │  │  │  │  │  └─ 数据字节 3 (0x00)
         │    │   │  │  │  │  └──── 数据字节 2 (0x01)
         │    │   │  │  │  └─────── 数据字节 1 (0x01)
         │    │   │  │  └────────── 数据字节 0 (0x92)
         │    │   │  └───────────── 子索引 0x00
         │    │   └───────────────── 索引 0x1000
         │    └───────────────────── SCS=011, n=00, s=1, e=1
         └────────────────────────── COB-ID 0x583

解析字节 0: 0x4B = 0100 1011
  Bit 7-5: 011 = SCS (Upload Response)
  Bit 4-2: 00 = n (未传输字节数 = 0,即 4 字节数据)
  Bit 1:   1  = s (有数据)
  Bit 0:   1  = e (Expedited 模式)

数据: 0x00 0x01 0x01 0x92 (小端模式)
     = 0x00010192 (大端读取值)

含义: 设备类型 = 0x00010192
  - 0x92 = 146 (设备子类型)
  - 0x01 = 运动控制设备
  - 0x01 = 设备符合 CiA 402 行规
  - 0x00 = 保留

Expedited Download 流程

1. 请求帧(Client -> Server)

COB-ID: 0x600 + node_id
DLC: 8

Byte 0: 0x20 | (n << 1) | 0x03
  - Bit 7-5: CCS = 000 (Download)
  - Bit 4-2: n = 未传输字节数
  - Bit 1-0: s=1, e=1

Byte 1-2: Index(小端模式)
Byte 3: Sub-index
Byte 4-(7-n): 数据
Byte (8-n)-7: 0x00(填充)

2. 响应帧(Server -> Client)

COB-ID: 0x580 + node_id
DLC: 8

Byte 0: 0x60
  - Bit 7-5: SCS = 010 (Download Response)

Byte 1-2: Index(小端模式)
Byte 3: Sub-index
Byte 4-7: 0x00(填充)

3. 逐字节解析示例

场景:写入节点 3 的心跳周期(0x1017sub0)为 1000ms

请求:

cansend vcan0 603#2B171000E8030000
  vcan0  603  [8]  2B 17 10 00 E8 03 00 00
         │    │   │  │  │  │  └─ 填充字节 1
         │    │   │  │  │  └──── 填充字节 0
         │    │   │  │  └───────── 数据字节 1 (0x03)
         │    │   │  └──────────── 数据字节 0 (0xE8)
         │    │   └──────────────── 子索引 0x00
         │    └──────────────────── 索引 0x1017
         └───────────────────────── CCS=000, n=01 (2字节未传输), s=1, e=1

解析字节 0: 0x2B = 0010 1011
  Bit 7-5: 000 = CCS (Download)
  Bit 4-2: 010 = n = 2 (未传输字节数 = 2,即实际传输 2 字节)
  Bit 1-0: 11 = s=1, e=1 (Expedited)

数据: 0xE8 0x03 (小端模式)
     = 0x03E8 = 1000 (十进制)

响应:

vcan0  583  [8]  60 17 10 00 00 00 00 00
         │    │   │  │  └─ 填充
         │    │   │  └───── 子索引 0x00
         │    │   └───────── 索引 0x1017
         │    └───────────── SCS=010 (Download Response)
         └───────────────── COB-ID 0x583

命令行实操

前置条件

# 创建虚拟 CAN 设备
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0

# 启动监听(另一个终端)
candump vcan0

读取设备类型(0x1000sub0)

# 发送 Expedited Upload 请求
cansend vcan0 603#4000100000000000

# 预期响应
# vcan0  583  [8]  4B 00 10 00 92 01 01 00

读取错误寄存器(0x1001sub0)

# 1 字节数据
cansend vcan0 603#4001100000000000

# 预期响应
# vcan0  583  [8]  4F 01 10 00 00 00 00 00
# 0x4F = 0100 1111: SCS=011, n=11 (3字节未传输), s=1, e=1
# 数据: 0x00 (无错误)

写入心跳周期(0x1017sub0)

# 2 字节数据: 1000ms = 0x03E8
cansend vcan0 603#2B171000E8030000

# 预期响应
# vcan0  583  [8]  60 17 10 00 00 00 00 00

读取制造商设备名称(0x1008sub0)

这是一个 VISIBLE_STRING 类型,长度 > 4 字节,不能使用 Expedited 传输。必须使用 Segmented 传输。

代码实现

Expedited Upload(客户端)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/can.h>
#include <linux/can/raw.h>

int sdo_upload_expedited(int sock, const char *ifname, uint8_t node_id,
                         uint16_t index, uint8_t sub_index,
                         uint8_t *data, size_t *data_len) {
    struct can_frame req, resp;
    struct ifreq ifr;
    struct sockaddr_can addr;
    canid_t req_cobid = 0x600 + node_id;
    canid_t resp_cobid = 0x580 + node_id;

    /* 创建 CAN socket 并绑定(省略,见 can_socket.h) */

    /* 构建请求帧 */
    memset(&req, 0, sizeof(req));
    req.can_id = req_cobid;
    req.can_dlc = 8;
    req.data[0] = 0x40;  /* CCS=1 (Upload) */
    req.data[1] = index & 0xFF;         /* 索引低字节 */
    req.data[2] = (index >> 8) & 0xFF;  /* 索引高字节 */
    req.data[3] = sub_index;

    /* 发送请求 */
    write(sock, &req, sizeof(req));

    /* 接收响应 */
    read(sock, &resp, sizeof(resp));

    /* 验证响应 */
    if (resp.can_id != resp_cobid) {
        fprintf(stderr, "Wrong COB-ID: expected 0x%03X, got 0x%03X\n",
                resp_cobid, resp.can_id);
        return -1;
    }

    if (resp.data[0] == 0x80) {
        /* Abort 响应 */
        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) {  /* e=1, s=1 */
        int n = (resp.data[0] >> 2) & 0x07;  /* 未传输字节数 */
        *data_len = 4 - n;
        memcpy(data, &resp.data[4], *data_len);
    }

    return 0;
}

Expedited Download(客户端)

int sdo_download_expedited(int sock, const char *ifname, uint8_t node_id,
                           uint16_t index, uint8_t sub_index,
                           const uint8_t *data, size_t data_len) {
    struct can_frame req, resp;
    int n = 4 - data_len;  /* 未传输字节数 */

    /* 构建请求帧 */
    memset(&req, 0, sizeof(req));
    req.can_id = 0x600 + node_id;
    req.can_dlc = 8;
    req.data[0] = 0x20 | (n << 1) | 0x03;  /* CCS=0, Expedited, n */
    req.data[1] = index & 0xFF;
    req.data[2] = (index >> 8) & 0xFF;
    req.data[3] = sub_index;
    memcpy(&req.data[4], data, data_len);

    /* 发送请求 */
    write(sock, &req, sizeof(req));

    /* 接收响应 */
    read(sock, &resp, sizeof(resp));

    /* 验证响应 */
    if (resp.data[0] != 0x60) {
        fprintf(stderr, "SDO Download failed, cmd=0x%02X\n", resp.data[0]);
        return -1;
    }

    return 0;
}

常见问题

Q: Expedited 传输的字节 0 中 n 位如何计算?

n 表示 未传输的字节数(即填充字节数)。对于 4 字节数据,n=0;对于 1 字节数据,n=3。

实际数据长度n 值字节 0(Upload)字节 0(Download)
1 字节30x4F0x2F
2 字节20x4B0x2B
3 字节10x470x27
4 字节00x430x23

Q: 服务端可以不使用 Expedited 模式吗?

是的。即使客户端发送 Expedited Upload 请求(0x40),服务端也可以选择使用 Segmented 模式响应。这通常发生在数据类型 > 4 字节时。

Q: 为什么小端模式很重要?

CANOpen 协议规定多字节数据使用小端模式传输。如果解析时使用大端模式,数据会被错误解读。例如 0x03E8(1000ms)在小端模式下传输为 E8 03,如果按大端读取会得到 0xE803(59395)。

总结

维度说明
数据量≤ 4 字节
通信1 请求 + 1 响应
请求命令Upload=0x40, Download=0x20|n|s|e
响应命令Upload=0x4B|n|s|e, Download=0x60
Abort0x80 + 4 字节错误码
数据编码小端模式
如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
Cygnus Yang 支付宝支付宝
Cygnus Yang 微信微信