背景
有时候在与下位机通信时会选择串口,所以就需要使用到QT中的QSerialPort类。在接收下位机返回的指令时,经常会出现数据包分包和粘包的现象。
数据包格式
一般在与下位机通信时的协议都是具有一定的格式,也就是利用这个格式来解决数据分包和粘包的问题。
- 一般数据格式如下
帧头 | 功能码 | 数据长度 | 数据内容 | 检验位 |
---|---|---|---|---|
1 byte | 1 byte | 2 byte | n byte | 1 byte |
- 解释
(1)帧头:定位到一帧数据的起始位置。可以定特殊一点的,不会经常出现的,如:0xfa、0xfb
(2)功能:这条协议表示什么功能。如:0x01表示控制灯光、0x02表示查询状态
(3)数据长度:数据内容的大小。
(4)数据内容:真正使用到的数据。如:0x01表示开灯成功,0x02表示关灯成功,这样UI就可以根据这个进行变化。
(5)检验位:检验这一帧数据是否正确。如:异或校验、和校验。
注意:可能有的协议还会有索引位、帧尾之类的
解决方法
- 基本流程
(1)将每次接收到的数据保存到一个buffer中
(2)定位到帧头索引,若索引不为0,则说明前面这一部分的数据无用,可以舍弃
(3)获取数据长度字节,得到数据内容的字节大小
(4)提取到一条完整的指令,如上面的格式则指令长度为 n + 5
(5)检查校验位是否正确
(6)从buffer中移除该指令
- 代码实现
char getXorCheck(const QByteArray &data)
{
char crc = 0x00;
for (const auto &ch : data) {
crc ^= ch;
}
return crc;
}
void slot_receiveSerialPortData()
{
QByteArray data = m_serialPort->readAll();
// 串口通信数据会分包或粘包,所以需要将收到的数据缓存起来,再从中解析出相应的指令
m_buffer.append(data);
// 先找到帧头的位置
int headerIndex = m_buffer.indexOf(char(0xfa));
if (-1 == headerIndex) {
m_buffer.clear(); // 找不到帧头,则将所有缓存数据清空
} else {
if (headerIndex > 0) {
// 如果帧头不在第一个,则说明前面的数据是没用的,直接舍弃
m_buffer.remove(0, headerIndex);
}
// 数据长度位的索引为2,3 所以size需要>=4
if (m_buffer.size() >= 4) { // 有包含数据长度位
// 取出完整的一条指令
int dataLen = QString(m_buffer.mid(2, 2).toHex()).toInt(nullptr, 16); // 数据内容长度
int cmdLen = dataLen + 5; // 完整指令数据长度
if (m_buffer.size() >= cmdLen) {
QByteArray cmd = m_buffer.left(cmdLen); // 指令数据
// 异或校验不包含帧头
// 检查校验位
char pbCrc = getXorCheck(cmd.mid(1, dataLen + 3));
char pcCrc = cmd.at(cmdLen - 1);
if (pbCrc == pcCrc) {
// 解析指令
// 按功能单元区分
char funcNum = cmd.at(1);
if (char(0x01) == funcNum) {
}
} else {
// 失败 错误处理
}
m_buffer.remove(0, cmdLen); // 将取出的指令数据从缓存中移除
}
}
}
}