素材来源 | 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架构。