数据封装 - 嵌入式通信的数据打包艺术

数据封装 - 嵌入式通信的数据打包艺术

数据封装 ​⏱️ 阅读时长:约 10 分钟 📊 难度等级:初级 🎯 读完你将学会:数据帧结构设计、CRC 校验原理、数据打包解包、序列化处理

要点速览 ​数据封装把原始数据打包成帧,解决边界、类型、完整性问题帧结构包含帧头、长度、命令、数据、CRC、帧尾CRC 校验保证数据完整性,比简单校验和更可靠序列化处理多字节数据的字节序问题如果你想快速了解实现方式,跳到数据打包。

什么是数据封装? ​数据封装就是把原始数据按照特定格式"打包"的过程。就像寄快递,你不能把东西随便一扔,得先装进盒子里,贴上地址标签,写上收件人信息。

在嵌入式通信中,一个完整的数据帧通常包含:

字段作用示例帧头标识数据帧的开始0xAA 0x55长度告诉接收方数据有多大数据字节数命令区分不同类型的数据0x01=读版本数据真正要传输的内容温度值、配置参数校验验证数据是否完整CRC16帧尾标识数据帧的结束0x0D帧结构可视化CRC 校验计算为什么需要数据封装? ​问题:原始数据传输 ​假设你要传输一个温度值 25.6°C,直接发送:

cfloat temperature = 25.6;

uart_send(&temperature, sizeof(float));12这样做有几个问题:

不知道数据边界:接收方怎么知道这 4 个字节是一个 float?不知道数据类型:这 4 个字节是温度还是湿度?不知道数据完整性:传输过程中出错怎么办?不知道数据顺序:大端还是小端?解决:封装成帧 ​把数据封装成帧:

[AA 55][04][01][00 00 CC CD][XX XX][0D]

帧头 长度 命令 温度数据 CRC 帧尾12接收方收到后,可以:

通过帧头 AA 55 知道新帧开始通过长度 04 知道数据有 4 字节通过命令 01 知道这是温度数据通过 CRC 验证数据完整性通过帧尾 0D 确认帧结束帧结构设计 ​基本帧格式 ​+--------+--------+--------+--------+--------+--------+--------+

| Header | Length | Cmd | Data | CRC | Tail |

| 2B | 1B | 1B | NB | 2B | 1B |

+--------+--------+--------+--------+--------+--------+--------+1234定义常量 ​c#define FRAME_HEADER_H 0xAA

#define FRAME_HEADER_L 0x55

#define FRAME_TAIL 0x0D

#define MAX_DATA_LEN 1281234定义命令类型 ​ctypedef enum {

CMD_READ_VERSION = 0x01, // 读版本

CMD_READ_STATUS = 0x02, // 读状态

CMD_SET_CONFIG = 0x03, // 设置配置

CMD_READ_DATA = 0x04, // 读数据

CMD_WRITE_DATA = 0x05, // 写数据

CMD_ACK = 0x80, // 应答

CMD_NACK = 0x81 // 错误应答

} CommandType;123456789定义错误码 ​ctypedef enum {

ERR_NONE = 0x00, // 无错误

ERR_CRC = 0x01, // CRC 错误

ERR_LENGTH = 0x02, // 长度错误

ERR_CMD_UNKNOWN = 0x03, // 未知命令

ERR_PARAM = 0x04 // 参数错误

} ErrorCode;1234567CRC 校验 ​CRC(循环冗余校验)是最常用的校验算法。它通过多项式除法计算出一个校验值,能检测出大多数传输错误。

CRC16 算法 ​cuint16_t calculate_crc16(uint8_t *data, uint16_t len) {

uint16_t crc = 0xFFFF;

for (uint16_t i = 0; i < len; i++) {

crc ^= data[i];

for (uint8_t j = 0; j < 8; j++) {

if (crc & 0x0001) {

crc = (crc >> 1) ^ 0xA001;

} else {

crc >>= 1;

}

}

}

return crc;

}12345678910111213141516CRC 校验的特点 ​特性说明检错能力能检测所有奇数个错误、所有双比特错误计算复杂度O(n),适合嵌入式常用多项式CRC-16/MODBUS: 0xA001为什么用 CRC 而不是简单校验和?

校验和:只能检测奇数个比特错误异或校验:只能检测奇数个比特错误CRC:能检测更多类型的错误,包括突发错误数据打包 ​打包函数 ​cuint16_t pack_frame(uint8_t cmd, uint8_t *data, uint8_t dataLen,

uint8_t *buffer) {

uint16_t index = 0;

// 帧头

buffer[index++] = FRAME_HEADER_H;

buffer[index++] = FRAME_HEADER_L;

// 长度(命令 + 数据)

buffer[index++] = dataLen + 1;

// 命令

buffer[index++] = cmd;

// 数据

if (dataLen > 0 && data != NULL) {

memcpy(&buffer[index], data, dataLen);

index += dataLen;

}

// CRC(从长度字段开始计算)

uint16_t crc = calculate_crc16(&buffer[3], dataLen + 1);

buffer[index++] = crc & 0xFF;

buffer[index++] = (crc >> 8) & 0xFF;

// 帧尾

buffer[index++] = FRAME_TAIL;

return index;

}123456789101112131415161718192021222324252627282930使用示例 ​cuint8_t tempData[4];

serialize_float(25.6, tempData); // 序列化浮点数

uint8_t txBuffer[64];

uint16_t frameLen = pack_frame(CMD_READ_DATA, tempData, 4, txBuffer);

uart_send(txBuffer, frameLen);1234567数据解包 ​解析状态 ​ctypedef enum {

PARSE_IDLE,

PARSE_HEADER_H,

PARSE_HEADER_L,

PARSE_LENGTH,

PARSE_COMMAND,

PARSE_DATA,

PARSE_CRC_L,

PARSE_CRC_H,

PARSE_TAIL

} ParseState;1234567891011解析器结构 ​ctypedef struct {

ParseState state;

uint8_t buffer[MAX_DATA_LEN];

uint8_t dataLen;

uint8_t command;

uint16_t crcReceived;

uint8_t dataIndex;

bool frameReady;

} FrameParser;123456789解析函数 ​cbool parse_byte(FrameParser *parser, uint8_t byte) {

switch (parser->state) {

case PARSE_IDLE:

if (byte == FRAME_HEADER_H) {

parser->state = PARSE_HEADER_H;

}

break;

case PARSE_HEADER_H:

if (byte == FRAME_HEADER_L) {

parser->state = PARSE_HEADER_L;

} else {

parser->state = PARSE_IDLE;

}

break;

case PARSE_HEADER_L:

parser->dataLen = byte - 1; // 减去命令字节

parser->dataIndex = 0;

parser->state = PARSE_LENGTH;

break;

case PARSE_LENGTH:

parser->command = byte;

if (parser->dataLen > 0) {

parser->state = PARSE_DATA;

} else {

parser->state = PARSE_CRC_L;

}

break;

case PARSE_DATA:

parser->buffer[parser->dataIndex++] = byte;

if (parser->dataIndex >= parser->dataLen) {

parser->state = PARSE_CRC_L;

}

break;

case PARSE_CRC_L:

parser->crcReceived = byte;

parser->state = PARSE_CRC_H;

break;

case PARSE_CRC_H:

parser->crcReceived |= (byte << 8);

parser->state = PARSE_TAIL;

break;

case PARSE_TAIL:

parser->state = PARSE_IDLE;

if (byte == FRAME_TAIL) {

// 验证 CRC

uint8_t tempBuffer[MAX_DATA_LEN + 1];

tempBuffer[0] = parser->dataLen + 1;

tempBuffer[1] = parser->command;

memcpy(&tempBuffer[2], parser->buffer, parser->dataLen);

uint16_t crcCalc = calculate_crc16(tempBuffer, parser->dataLen + 2);

if (crcCalc == parser->crcReceived) {

parser->frameReady = true;

return true;

}

}

break;

}

return false;

}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768数据序列化 ​字节序问题 ​多字节数据在传输时,字节序是个大问题:

字节序说明示例(0x12345678)大端序高位在前12 34 56 78小端序低位在前78 56 34 12网络通信通常使用大端序(网络字节序)。

浮点数序列化 ​cvoid serialize_float(float value, uint8_t *buffer) {

uint32_t intValue;

memcpy(&intValue, &value, 4); // 避免直接指针转换

// 大端序

buffer[0] = (intValue >> 24) & 0xFF;

buffer[1] = (intValue >> 16) & 0xFF;

buffer[2] = (intValue >> 8) & 0xFF;

buffer[3] = intValue & 0xFF;

}

float deserialize_float(uint8_t *buffer) {

uint32_t intValue = (buffer[0] << 24) | (buffer[1] << 16) |

(buffer[2] << 8) | buffer[3];

float value;

memcpy(&value, &intValue, 4);

return value;

}123456789101112131415161718结构体序列化 ​c#pragma pack(push, 1) // 1字节对齐

typedef struct {

uint32_t timestamp;

float temperature;

float humidity;

uint16_t pressure;

uint8_t status;

} SensorData;

#pragma pack(pop)

void serialize_sensor_data(SensorData *data, uint8_t *buffer) {

uint16_t index = 0;

// 时间戳(大端序)

buffer[index++] = (data->timestamp >> 24) & 0xFF;

buffer[index++] = (data->timestamp >> 16) & 0xFF;

buffer[index++] = (data->timestamp >> 8) & 0xFF;

buffer[index++] = data->timestamp & 0xFF;

// 温度

serialize_float(data->temperature, &buffer[index]);

index += 4;

// 湿度

serialize_float(data->humidity, &buffer[index]);

index += 4;

// 气压

buffer[index++] = (data->pressure >> 8) & 0xFF;

buffer[index++] = data->pressure & 0xFF;

// 状态

buffer[index++] = data->status;

}123456789101112131415161718192021222324252627282930313233343536关于 #pragma pack

使用 #pragma pack(push, 1) 可以取消结构体的字节对齐,确保结构体紧凑。但要注意:

可能影响 CPU 访问效率某些平台不支持非对齐访问总结 ​数据封装把原始数据打包成帧,解决边界、类型、完整性问题帧结构包含帧头、长度、命令、数据、CRC、帧尾CRC 校验保证数据完整性,比简单校验和更可靠状态机解析逐字节处理接收数据,健壮可靠序列化处理多字节数据的字节序问题相关主题 ​通信协议 - 完整的协议设计串口数据 - 串口数据传输UDP/TCP - 网络数据封装

相关推荐

微信怎么扫码登录别人的账号
在线365bet盘口

微信怎么扫码登录别人的账号

📅 11-02 👁️ 1681
苹果iPhone5参数
365bet365用址

苹果iPhone5参数

📅 10-11 👁️ 4024
如何DIY洗洁精
365bet体育网址

如何DIY洗洁精

📅 09-03 👁️ 221