SDO Client/Server 实现

架构概述

SDO 通信采用典型的 Client/Server 架构:

┌──────────────┐                      ┌──────────────┐
│  SDO Client   │                      │  SDO Server   │
│  (主站/PC)    │                      │  (设备/节点)   │
├──────────────┤                      ├──────────────┤
│ 命令行工具    │                      │ 对象字典       │
│ 配置工具      │                      │ 服务循环       │
│ 上位机软件    │                      │ 数据访问层     │
└──────┬───────┘                      └──────┬───────┘
       │                                     │
       │  CAN Frame (COB-ID 0x600+N)         │
       │──────────────────────────────────> │
       │                                     │
       │  CAN Frame (COB-ID 0x580+N)         │
       │◀────────────────────────────────── │
       │                                     │
       └────────── CAN Bus (vcan0) ──────────┘

职责分离:

  • Client:发起请求、等待响应、解析数据、处理超时
  • Server:监听请求、查找对象字典、读取/写入数据、返回响应

SDO Client 实现

状态机

                    ┌─────────┐
                    │  IDLE   │
                    └────┬────┘
                         │
            send_request │
                         ▼
              ┌──────────────────┐
              │  WAIT_RESPONSE   │◀── 500ms 超时
              └────────┬─────────┘
                       │
              ┌────────┼────────┐
              │        │        │
        success│  timeout│  abort│
              ▼        ▼       ▼
         ┌────────┐┌────────┐┌────────┐
         │COMPLETE││ ERROR  ││ ERROR  │
         └────────┘└────────┘└────────┘

核心数据结构

typedef struct {
    int       sock;          /* CAN socket */
    uint8_t   node_id;       /* 目标节点 ID */
    uint32_t  timeout_ms;    /* 超时时间(默认 500ms) */
    canid_t   req_cobid;     /* 请求 COB-ID (0x600 + node_id) */
    canid_t   resp_cobid;    /* 响应 COB-ID (0x580 + node_id) */
} sdo_client_t;

typedef enum {
    SDO_STATE_IDLE,
    SDO_STATE_WAIT_RESPONSE,
    SDO_STATE_COMPLETE,
    SDO_STATE_ERROR
} sdo_state_t;

typedef struct {
    sdo_state_t state;
    uint8_t     data[256];    /* 接收缓冲区 */
    size_t      data_len;     /* 接收到的数据长度 */
    uint32_t    abort_code;   /* Abort 码(如果有) */
} sdo_transfer_t;

关键函数

1. 初始化

int sdo_client_init(sdo_client_t *client, int sock, uint8_t node_id) {
    client->sock = sock;
    client->node_id = node_id;
    client->timeout_ms = 500;
    client->req_cobid = 0x600 + node_id;
    client->resp_cobid = 0x580 + node_id;
    return 0;
}

2. Expedited Upload

int sdo_client_upload_expedited(sdo_client_t *client,
                                uint16_t index, uint8_t sub_index,
                                uint8_t *data, size_t *data_len) {
    struct can_frame req, resp;
    fd_set fds;
    struct timeval tv;

    /* 构建请求 */
    memset(&req, 0, sizeof(req));
    req.can_id = client->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;

    /* 发送 */
    if (write(client->sock, &req, sizeof(req)) < 0) {
        return -1;
    }

    /* 等待响应(带超时) */
    FD_ZERO(&fds);
    FD_SET(client->sock, &fds);
    tv.tv_sec = client->timeout_ms / 1000;
    tv.tv_usec = (client->timeout_ms % 1000) * 1000;

    if (select(client->sock + 1, &fds, NULL, NULL, &tv) <= 0) {
        fprintf(stderr, "SDO timeout\n");
        return -1;
    }

    /* 接收 */
    if (read(client->sock, &resp, sizeof(resp)) < 0) {
        return -1;
    }

    /* 验证 COB-ID */
    if (resp.can_id != client->resp_cobid) {
        return -1;
    }

    /* 检查 Abort */
    if (resp.data[0] == 0x80) {
        client->abort_code = resp.data[4] | (resp.data[5] << 8) |
                             (resp.data[6] << 16) | (resp.data[7] << 24);
        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 模式 */
    return -2;
}

3. Expedited Download

int sdo_client_download_expedited(sdo_client_t *client,
                                  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 = client->req_cobid;
    req.can_dlc = 8;
    req.data[0] = 0x20 | (n << 1) | 0x03;  /* CCS=0, e=1, s=1 */
    req.data[1] = index & 0xFF;
    req.data[2] = (index >> 8) & 0xFF;
    req.data[3] = sub_index;
    memcpy(&req.data[4], data, data_len);

    if (write(client->sock, &req, sizeof(req)) < 0) {
        return -1;
    }

    /* 等待并验证响应 */
    /* ... (与 Upload 类似的超时处理) ... */

    if (resp.data[0] != 0x60) {
        return -1;  /* 非预期响应 */
    }

    return 0;
}

超时机制

int sdo_wait_response(int sock, uint32_t timeout_ms) {
    fd_set fds;
    struct timeval tv;

    FD_ZERO(&fds);
    FD_SET(sock, &fds);
    tv.tv_sec = timeout_ms / 1000;
    tv.tv_usec = (timeout_ms % 1000) * 1000;

    int ret = select(sock + 1, &fds, NULL, NULL, &tv);
    if (ret < 0) {
        return SDO_ERROR_SELECT;
    }
    if (ret == 0) {
        return SDO_ERROR_TIMEOUT;
    }
    return 0;  /* 数据可读 */
}

SDO Server 实现

架构

┌───────────────────────────────────────────┐
│             SDO Server                    │
├───────────────────────────────────────────┤
│                                           │
│  ┌─────────┐    ┌──────────┐             │
│  │ CAN     │───>│ Request   │             │
│  │ Socket  │    │ Parser   │             │
│  └─────────┘    └────┬─────┘             │
│                      │                    │
│               ┌──────┴──────┐             │
│               │ OD Lookup   │             │
│               └──────┬──────┘             │
│                      │                    │
│  ┌─────────┐    ┌────┴─────┐             │
│  │ CAN     │<───│ Response  │             │
│  │ Socket  │    │ Builder  │             │
│  └─────────┘    └──────────┘             │
│                                           │
│  ┌──────────────────────────────┐         │
│  │   Object Dictionary (OD)     │         │
│  │   0x1000: Device Type        │         │
│  │   0x1001: Error Register     │         │
│  │   0x1008: Device Name        │         │
│  │   ...                        │         │
│  └──────────────────────────────┘         │
└───────────────────────────────────────────┘

核心数据结构

typedef struct {
    int       sock;
    uint8_t   node_id;
    canid_t   req_cobid;   /* 0x600 + node_id */
    canid_t   resp_cobid;  /* 0x580 + node_id */
    od_t     *od;          /* 对象字典指针 */
} sdo_server_t;

请求处理循环

int sdo_server_run(sdo_server_t *server) {
    struct can_frame req, resp;

    while (1) {
        /* 1. 接收请求 */
        if (read(server->sock, &req, sizeof(req)) < 0) {
            continue;
        }

        /* 2. 验证 COB-ID */
        if (req.can_id != server->req_cobid) {
            continue;  /* 不是发给我的 */
        }

        /* 3. 解析命令 */
        uint8_t ccs = req.data[0] >> 5;

        switch (ccs) {
        case 1:  /* Initiate Upload */
            handle_upload_initiate(server, &req, &resp);
            break;
        case 0:  /* Initiate Download / Segment */
            if (req.data[0] == 0x40) {
                handle_upload_initiate(server, &req, &resp);
            } else {
                handle_download_initiate(server, &req, &resp);
            }
            break;
        case 3:  /* Segment Upload */
            handle_segment_upload(server, &req, &resp);
            break;
        default:
            send_abort(server, &req, 0x05040001);
            break;
        }

        /* 4. 发送响应 */
        write(server->sock, &resp, sizeof(resp));
    }
}

对象字典查找

od_entry_t *od_find(od_t *od, uint16_t index, uint8_t sub_index) {
    for (int i = 0; i < od->entry_count; i++) {
        if (od->entries[i].index == index) {
            if (od->entries[i].max_sub == 0) {
                /* VAR 类型,sub_index 必须为 0 */
                if (sub_index == 0) {
                    return &od->entries[i];
                }
            } else {
                /* ARRAY/RECORD 类型 */
                if (sub_index <= od->entries[i].max_sub) {
                    return &od->entries[i];
                }
            }
        }
    }
    return NULL;  /* 未找到 */
}

Upload 处理

static void handle_upload_initiate(sdo_server_t *server,
                                   struct can_frame *req,
                                   struct can_frame *resp) {
    uint16_t index = req->data[1] | (req->data[2] << 8);
    uint8_t sub_index = req->data[3];

    /* 查找对象字典 */
    od_entry_t *entry = od_find(server->od, index, sub_index);
    if (!entry) {
        send_abort(server, req, 0x06020000);  /* 对象不存在 */
        return;
    }

    /* 检查访问权限 */
    if (entry->access == OD_ACCESS_WO) {
        send_abort(server, req, 0x06010002);  /* 只写对象 */
        return;
    }

    /* 获取数据 */
    uint8_t *data = entry->data;
    size_t data_len = entry->data_size;

    if (data_len <= 4) {
        /* Expedited 传输 */
        resp->can_id = server->resp_cobid;
        resp->can_dlc = 8;
        int n = 4 - data_len;
        resp->data[0] = 0x40 | (n << 2) | 0x03;  /* SCS=011, e=1, s=1 */
        resp->data[1] = index & 0xFF;
        resp->data[2] = (index >> 8) & 0xFF;
        resp->data[3] = sub_index;
        memcpy(&resp->data[4], data, data_len);
    } else {
        /* Segmented 传输 */
        /* 实现分段发送逻辑 */
        /* ... */
    }
}

Abort 发送

static void send_abort(sdo_server_t *server,
                       struct can_frame *req,
                       uint32_t abort_code) {
    uint16_t index = req->data[1] | (req->data[2] << 8);
    uint8_t sub_index = req->data[3];

    struct can_frame resp;
    memset(&resp, 0, sizeof(resp));
    resp.can_id = server->resp_cobid;
    resp.can_dlc = 8;
    resp.data[0] = 0x80;
    resp.data[1] = index & 0xFF;
    resp.data[2] = (index >> 8) & 0xFF;
    resp.data[3] = sub_index;
    resp.data[4] = abort_code & 0xFF;
    resp.data[5] = (abort_code >> 8) & 0xFF;
    resp.data[6] = (abort_code >> 16) & 0xFF;
    resp.data[7] = (abort_code >> 24) & 0xFF;

    write(server->sock, &resp, sizeof(resp));
}

集成演示

模块 2 的示例 7(examples/07-sdo-demo/)演示了完整的 SDO Client/Server 交互:

  1. Server 启动:创建模拟对象字典,监听 vcan0
  2. Client 启动:连接到同一 vcan0
  3. 自动执行 5 步演示:
    • Expedited Upload 0x1000(设备类型)
    • Expedited Upload 0x1001(错误寄存器)
    • Expedited Download 0x1017(心跳周期)
    • Segmented Upload 0x1008(设备名称)
    • Segmented Upload 0x100A(软件版本)
# 终端 1:启动 Server
./sdo_server vcan0 3

# 终端 2:运行 Client
./sdo_client upload-exp vcan0 3 0x1000 0

错误处理最佳实践

场景处理方式
超时返回错误码,不重试(由上层决定是否重试)
Abort 响应记录 abort_code,返回错误
Toggle 位不匹配发送 Abort,终止传输
COB-ID 不匹配忽略帧(可能发给其他节点)
对象不存在返回 0x06020000 Abort
访问权限错误返回 0x06010002 Abort

总结

组件关键特性
Client状态机、超时机制、Expedited/Segmented 支持
Server请求解析、OD 查找、权限检查、Abort 处理
通信COB-ID 计算、帧格式解析、大小端处理
错误超时、Abort、Toggle 位校验
如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕
Cygnus Yang 支付宝支付宝
Cygnus Yang 微信微信