• 正文
    • 1、协议整体架构
    • 2、协议实现及其应用
    • 3、SACP协议的典型应用场景
  • 相关推荐
申请入驻 产业图谱

一个可用于多设备间的C/C++嵌入式通信协议的设计与实现-SACP协议

02/05 15:55
963
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

素材来源 | GitHub,整理&排版 | 嵌入式应用研究院

SACP(Snapmaker Advanced Communication Protocol)是Snapmaker设备的数据通信协议,用于 控制器(Controller)、PC 端(Host)、HMI(人机界面) 之间的数据传输,从该协议的设计思想上看,可以满足以下几个基本目标:

    可靠性:数据包头部 CRC8 校验 + Checksum 数据完整性验证灵活性:支持 请求/应答机制(SACP_ATTR_REQ / SACP_ATTR_ACK)模块化:封装 数据包结构,可扩展 不同指令集高效性:固定长度头部 + 变长数据部分,节省通信开销

以下为整个协议的实现板块总结:

模块 作用
协议头部 规定数据包的格式
CRC8头部校验 确保头部完整性
Checksum数据校验 确保数据完整性
请求-应答机制 设备间通信
序列号机制 跟踪请求-应答关系
数据包封装-解析 通信可靠性保证

之前我们也讲解过关于MVC框架的实现,基于SACP协议,很容易拓展为一个典型的MVC框架:

嵌入式软件设计之美-以实际项目应用MVC框架与状态模式(上)

嵌入式软件设计之美-以实际项目应用MVC框架与状态模式(下)

以下是整个协议的实现代码的开源仓库:https://github.com/Snapmaker/SnapmakerController-IDEX

1、协议整体架构

unsetunset1.1、SACP协议数据包结构unsetunset

unsetunset1.1.1、核心数据包结构体unsetunset

#pragma pack(1)
typedef struct {
  uint8_t sof_h;           //同步头高字节 0xAA, 一个字节
  uint8_t sof_l;           //同步头低字节 0x55,一个字节
  uint16_t length;         //总长度(头部+数据+校验和),两个字节
  uint8_t version;         //协议版本(当前为0x01),一个字节
  uint8_t recever_id;    //目标设备ID(PC/控制器/HMI),一个字节
  uint8_t crc8;      //头部校验(CRC-8)
  uint8_t sender_id;       //发送设备ID,一个字节
  uint8_t attr;            //数据包属性(请求/应答),一个字节
  uint16_t sequence;       //序列号(用于匹配请求-应答包),两个字节
  uint8_t command_set;     //命令集(功能分类),一个字节
  uint8_t command_id;      //具体命令,一个字节
  uint8_t data[0];         //可变数据参数,数据载荷(length-SACP_HEADER_LEN)
} SACP_struct_t;

该核心数据包结构体中的成员在头文件中预定义的一些宏:

//关于协议、版本号、头部长度的描述
#define SACP_PDU_SOF_H   0xAA
#define SACP_PDU_SOF_L   0x55
#define SACP_VERSION     0x01
#define SACP_HEADER_LEN  (15)
//设备ID标识描述
#define SACP_ID_PC         0
#define SACP_ID_CONTROLLER 1
#define SACP_ID_HMI        2
//数据包大小描述
#define PACK_PARSE_MAX_SIZE 512
#define PACK_PACKET_MAX_SIZE 1024
//请求-应答标识
#define SACP_ATTR_REQ 0
#define SACP_ATTR_ACK 1
    SACP_PDU_SOF_H / SACP_PDU_SOF_L: 同步字,数据包的起始标志 (0xAA55)SACP_VERSION: 协议版本号,当前为 0x01SACP_HEADER_LEN: 头部长度,表示 除去 Payload 的包头部分SACP_ID_PC: PC 端SACP_ID_CONTROLLER: 控制器SACP_ID_HMI: 人机界面(HMI 屏幕)PACK_PARSE_MAX_SIZE: 单次解析最大数据包大小(512 字节)PACK_PACKET_MAX_SIZE: 最大可打包数据长度(1024 字节)SACP_ATTR_REQ: 请求包SACP_ATTR_ACK: 应答包

    • 请求-响应机制:控制器发送请求目标设备解析并返回请求应答

unsetunset1.1.2、核心头部结构unsetunset

该头部结构是作为数据封装的基础信息,即用于package()生成完整的SACP_struct_t

typedef struct {
  uint8_t recever_id;
  uint8_t attribute;
  uint16_t sequence;
  uint8_t command_set;
  uint8_t command_id;
} SACP_head_base_t;

unsetunset1.1.3、数据解析缓存结构unsetunset

typedef struct {
  uint16_t lenght;  //记录当前解析数据长度
  union {
    uint8_t buff[PACK_PARSE_MAX_SIZE]; //数据缓冲区
    SACP_struct_t sacp; //sacp直接映射到SACP_struct_t
  };
} SACP_param_t;

2、协议实现及其应用

unsetunset2.1、数据包的解析unsetunset

解析SACP数据包,校验数据完整性:

ErrCode ProtocolSACP::parse(uint8_t *data, uint16_t len, SACP_param_t &out);

简单的应用:

SACP_param_t parsed_data;
ErrCode result = protocol_sacp.parse(received_data, data_length, parsed_data);
if (result == E_SUCCESS) {
    // 解析成功,即通过parsed_data找到对应协议的字段,分析对应的数据来源并进行进一步的处理
}

unsetunset2.2、数据包的封装unsetunset

封装SACP请求/应答数据包:

uint16_t ProtocolSACP::package(SACP_head_base_t head, uint8_t *in_data, uint16_t length, uint8_t *out_data);

简单的应用:

uint8_t buffer[PACK_PACKET_MAX_SIZE];
SACP_head_base_t head = {SACP_ID_HMI, SACP_ATTR_REQ, protocol_sacp.sequence_pop(), 0x10, 0x02};
uint8_t data_payload[] = {0x01, 0x02, 0x03}; // 示例数据
uint16_t packet_length = protocol_sacp.package(head, data_payload, sizeof(data_payload), buffer);
send_packet(buffer, packet_length); // 发送数据包

完整实现逻辑(头文件):protocol_sacp.h

#ifndef PROTOCOL_ASCP_H
#define PROTOCOL_ASCP_H

#include "../J1/common_type.h"
#include <functional>
// protocol relative macros
#define SACP_PDU_SOF_H   0xAA
#define SACP_PDU_SOF_L   0x55
#define SACP_VERSION     0x01
#define SACP_HEADER_LEN  (15)   // frame_length - length_paylod

#define SACP_ID_PC         0
#define SACP_ID_CONTROLLER 1
#define SACP_ID_HMI        2

#define PACK_PARSE_MAX_SIZE 512
#define PACK_PACKET_MAX_SIZE 1024

#define SACP_ATTR_REQ 0
#define SACP_ATTR_ACK 1

#pragma pack(1)
typedef struct {
  uint8_t sof_h;
  uint8_t sof_l;
  uint16_t length;
  uint8_t version;  // 0x01
  uint8_t recever_id;
  uint8_t crc8;
  uint8_t sender_id;
  uint8_t attr;
  uint16_t sequence;
  uint8_t command_set;
  uint8_t command_id;
  uint8_t data[0];
} SACP_struct_t;

typedef struct {
  uint8_t recever_id;
  uint8_t attribute;
  uint16_t sequence;
  uint8_t command_set;
  uint8_t command_id;
} SACP_head_base_t;

typedef struct {
  uint16_t lenght;  // The total length of data
  union {
    uint8_t buff[PACK_PARSE_MAX_SIZE];
    SACP_struct_t sacp;
  };
} SACP_param_t;
#pragma pack()

class ProtocolSACP {
  public:
    ErrCode parse(uint8_t *data, uint16_t len, SACP_param_t &out);
    // Package the incoming data
    uint16_t package(SACP_head_base_t head, uint8_t *in_data, uint16_t length, uint8_t *out_data);
    uint16_t sequence_pop() {return sequence++;}
  private:
    uint32_t sequence = 0;
};

extern ProtocolSACP protocol_sacp;
#endif

完整实现逻辑(源文件):protocol_sacp.cpp

#include "protocol_sacp.h"
#include <functional>
#include "HAL.h"
#include "../../Marlin/src/core/serial.h"

ProtocolSACP protocol_sacp;

static uint8_t sacp_calc_crc8(uint8_t *buffer, uint16_t len) {
  int crc = 0x00;
  int poly = 0x07;
  for (int i = 0; i < len; i++) {
    for (int j = 0; j < 8; j++) {
      bool bit = ((buffer[i] >> (7 - j) & 1) == 1);
      bool c07 = ((crc >> 7 & 1) == 1);
      crc <<= 1;
      if (c07 ^ bit) {
        crc ^= poly;
      }
    }
  }
  crc &= 0xff;
  return crc;
}

uint16_t calc_checksum(uint8_t *buffer, uint16_t length) {
  uint32_t volatile checksum = 0;

  if (!length || !buffer)
    return 0;

  for (int j = 0; j < (length - 1); j = j + 2)
    checksum += (uint32_t)(buffer[j] << 8 | buffer[j + 1]);

  if (length % 2)
    checksum += buffer[length - 1];

  while (checksum > 0xffff)
    checksum = ((checksum >> 16) & 0xffff) + (checksum & 0xffff);

  checksum = ~checksum;

  return (uint16_t)checksum;
}

ErrCode ProtocolSACP::parse(uint8_t *data, uint16_t len, SACP_param_t &out) {
  uint8_t *parse_buff = out.buff;
  if (parse_buff[0] != SACP_PDU_SOF_H) {
    out.lenght = 0;
  }
  for (uint16_t i = 0; i < len; i++) {
    uint8_t ch = data[i];
    if (out.lenght == 0) {
      if (ch == SACP_PDU_SOF_H) {
        parse_buff[out.lenght++] = ch;
      }
    } else if (out.lenght == 1) {
      if (ch == SACP_PDU_SOF_L) {
        parse_buff[out.lenght++] = ch;
      } else {
        out.lenght = 0;
      }
    } else {
      parse_buff[out.lenght++] = ch;
    }

    if (out.lenght < 7) {
      break;
    }
    else if (out.lenght == 7) {
      if (sacp_calc_crc8(parse_buff, 6) != parse_buff[6]) {
        out.lenght = 0;
      }
    }
    else {
      uint16_t data_len = (parse_buff[3] << 8 | parse_buff[2]);
      uint16_t total_len = data_len + 7;
      if (out.lenght == total_len) {
        uint16_t checksum = calc_checksum(&parse_buff[7], data_len - 2);
        uint16_t checksum1 = (parse_buff[total_len - 1] << 8) | parse_buff[total_len - 2];
        if (checksum == checksum1) {
          out.lenght = 0;
          return E_SUCCESS;
        } else {
          out.lenght = 0;
          return E_PARAM;
        }
      } else if (out.lenght > total_len) {
        out.lenght = 0;
        return E_PARAM;
      }
    }
  }
  return E_IN_PROGRESS;
}


uint16_t ProtocolSACP::package(SACP_head_base_t head, uint8_t *in_data, uint16_t length, uint8_t *out_data) {
  uint16_t data_len = (length + 8); // header 6 byte, checknum 2byte
  SACP_struct_t *out =  (SACP_struct_t *)out_data;
  out->sof_h = SACP_PDU_SOF_H;
  out->sof_l = SACP_PDU_SOF_L;
  out->length = data_len;
  out->version = SACP_VERSION;
  out->recever_id = head.recever_id;
  out->crc8 = sacp_calc_crc8(out_data, 6);
  out->sender_id = SACP_ID_CONTROLLER;
  out->attr = head.attribute;
  out->sequence = head.sequence;
  out->command_set = head.command_set;
  out->command_id = head.command_id;
  for (uint16_t i = 0; i < length; i++) {
    out->data[i] = in_data[i];
  }
  uint16_t checksum = calc_checksum(&out_data[7], data_len - 2);  // - checknum 2 byte
  length = sizeof(SACP_struct_t) + length;
  out_data[length++] = (uint8_t)(checksum & 0x00FF);
  out_data[length++] = (uint8_t)(checksum>>8);
  return length;
}

3、SACP协议的典型应用场景

PC 端 -> 控制器

发送控制指令

控制器 -> HMI

反馈、上报一些信息通过GUI来进行展示

模块间通信

用户操作、数据解析和发送、反馈等

基于该协议的设计和实现,还可将其拓展为典型的MVC架构。

点赞
收藏
评论
分享
加入交流群
举报

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本科毕业于华南理工大学,现美国卡罗尔工商管理硕士研究生在读,曾就职于世界名企伟易达、联发科技等,多年嵌入式产品开发经验,在智能玩具、安防产品、平板电脑、手机开发有丰富的实战开发经验,现任深圳市云之手科技有限公司副总经理、研发总工程师。