现在是qt主动发送的,给单片机有点不安全,我想改成被动的,按下开始按键,qt发送一个握手,然后等等单片机回应,单片机回应了以后,发送一个开始帧,单片机开始从0包要数据,结束了以后发送一个结束帧
时间: 2025-07-23 17:55:19 浏览: 2
<think>我们计划修改协议,使其变为请求-响应模式:
1. 用户点击发送按钮,发送一个握手请求帧(Handshake Request)
2. 等待单片机回应握手确认帧(Handshake ACK)
3. 收到ACK后,发送一个开始传输帧(Start Frame)
4. 等待单片机请求数据包(按块号请求)
5. 收到请求后,发送对应的数据帧
6. 发送完成后,发送结束帧(End Frame)
协议帧类型定义:
握手请求帧:TYPE=0xE1, CMD=0x01
握手确认帧:TYPE=0xE1, CMD=0x81
开始传输帧:TYPE=0xE1, CMD=0x02
数据请求帧:TYPE=0xE1, CMD=0x03 (单片机发送,包含请求的块号)
数据帧: TYPE=0xE1, CMD=0x04 (包含块号和该块的数据)
结束帧: TYPE=0xE1, CMD=0x05
具体步骤:
1. 用户点击发送按钮,发送握手请求帧(Handshake Request)
2. 启动一个超时定时器(比如1秒),如果超时未收到握手确认,则重发(最多重试3次)
3. 收到握手确认后,发送开始传输帧(Start Frame),该帧包含总块数(总字节数/128,向上取整)
4. 等待单片机发送数据请求帧(包含请求的块号)
5. 收到数据请求帧后,发送对应的数据帧(数据帧中包含块号和该块的数据)
6. 当所有块都发送完毕(即收到块号等于总块数-1的数据请求并发送后),发送结束帧(End Frame)
注意:数据请求帧由单片机发送,每次请求一个块。单片机可以连续请求多个块,但需要确保不超出总块数。
修改点:
1. 移除原来的定时发送(TimerUpdate函数不再用于主动发送数据)
2. 添加新的状态机,用于管理传输过程
3. 修改串口接收函数,根据接收到的帧类型进行状态转换
状态机设计:
状态0:空闲状态(IDLE)
状态1:已发送握手请求,等待握手确认(WAIT_HANDSHAKE_ACK)
状态2:已发送开始帧,等待数据请求(WAIT_DATA_REQUEST)
状态3:传输完成(FINISHED)
步骤:
点击发送按钮:
发送握手请求帧 -> 进入状态1(WAIT_HANDSHAKE_ACK)
启动超时定时器(1秒)
在状态1下:
收到握手确认帧(CMD=0x81)-> 发送开始帧(CMD=0x02)-> 进入状态2(WAIT_DATA_REQUEST)
超时:重发握手请求(最多3次,超过则报错并回到空闲状态)
在状态2下:
收到数据请求帧(CMD=0x03)-> 解析请求的块号
如果块号有效(0 <= block < 总块数),则发送该块的数据帧(CMD=0x04)
如果块号等于总块数-1(最后一块),则发送完该帧后,发送结束帧(CMD=0x05)并进入状态3(FINISHED)
注意:每发送一帧数据,需要等待下一个请求,所以状态2一直保持直到发送结束帧。
在状态3:传输完成,显示提示信息,回到空闲状态。
另外,需要处理错误情况,比如收到非预期的帧,则报错并重置状态机。
代码修改:
1. 添加状态枚举和成员变量
2. 修改发送按钮的槽函数,发送握手请求并启动状态机
3. 修改串口接收函数,根据当前状态和接收到的帧进行处理
4. 添加超时处理函数,用于握手超时重传
具体实现:
状态枚举:
enum TransferState {
IDLE,
WAIT_HANDSHAKE_ACK,
WAIT_DATA_REQUEST,
FINISHED
};
成员变量:
TransferState currentState;
int totalBlocks; // 总块数(ceil(totalBytes/PAYLOAD))
int handshakeRetries; // 握手重试次数
QTimer* timeoutTimer; // 超时定时器
发送按钮点击槽函数(on_pushButton_2_clicked):
如果当前状态不是IDLE,则忽略
发送握手请求帧
设置状态为WAIT_HANDSHAKE_ACK
设置重试次数为0
启动超时定时器(1秒)
超时处理槽函数(timeoutHandler):
如果当前状态是WAIT_HANDSHAKE_ACK:
如果重试次数<3,则重发握手请求,重试次数+1,重启超时定时器
否则,报错,重置状态为IDLE
串口接收槽函数(serialport_readyread):
解析接收到的帧(注意可能粘包)
根据帧类型和当前状态进行处理:
状态WAIT_HANDSHAKE_ACK:
如果收到CMD=0x81的帧(握手确认),则发送开始帧(CMD=0x02),并进入WAIT_DATA_REQUEST
状态WAIT_DATA_REQUEST:
如果收到CMD=0x03的帧(数据请求):
解析块号
如果块号在有效范围内,发送该块的数据帧(CMD=0x04)
如果块号是最后一块,则发送完该帧后,再发送结束帧(CMD=0x05),进入FINISHED状态
如果收到其他帧,忽略或报错
注意:在发送结束帧后,状态变为FINISHED,然后可以重置为IDLE(或者显示完成信息后重置)。
帧格式统一:
所有帧都有相同的帧头(0xA55A)和长度字段(从长度字段开始到校验和之前的字节数),以及校验和(从长度字段到数据结束的累加和)。
开始帧格式:
除了公共字段,还需要包含总块数(2字节,因为总块数可能超过256)
所以开始帧的数据部分为2字节的总块数(大端)
数据请求帧格式:
数据部分为2字节的请求块号(大端)
数据帧格式:
数据部分:2字节块号 + 实际数据(最多128字节)
具体帧结构:
1. 握手请求帧(Handshake Request):
HEAD_CODE: 0xA55A
Length: 5 (TYPE+CMD+校验和,无数据)
TYPE: 0xE1
CMD: 0x01
校验和:TYPE+CMD
2. 握手确认帧(Handshake ACK):
HEAD_CODE: 0xA55A
Length: 5
TYPE: 0xE1
CMD: 0x81
校验和:TYPE+CMD
3. 开始帧(Start Frame):
HEAD_CODE: 0xA55A
Length: 7 (TYPE+CMD+2字节总块数+校验和)
TYPE: 0xE1
CMD: 0x02
Data: 2字节总块数(大端)
校验和:TYPE+CMD+Data0+Data1
4. 数据请求帧(Data Request):
HEAD_CODE: 0xA55A
Length: 7 (TYPE+CMD+2字节块号+校验和)
TYPE: 0xE1
CMD: 0x03
Data: 2字节请求的块号(大端)
校验和:TYPE+CMD+Data0+Data1
5. 数据帧(Data Frame):
HEAD_CODE: 0xA55A
Length: 5 + 2 + 数据长度 + 1 (TYPE+CMD+2字节块号+数据+校验和)
TYPE: 0xE1
CMD: 0x04
Data: 2字节块号 + 实际数据(最多128字节)
校验和:TYPE+CMD+块号低8位+块号高8位+所有数据字节
6. 结束帧(End Frame):
HEAD_CODE: 0xA55A
Length: 5
TYPE: 0xE1
CMD: 0x05
校验和:TYPE+CMD
注意:长度字段是1字节,所以整个帧(包括帧头)最大长度不能超过255+3(帧头2字节+长度1字节),所以数据帧的最大数据长度不能超过(255-5-2)=248字节,但这里我们使用128是安全的。
代码实现:
由于时间关系,这里只给出关键部分的伪代码和部分实现,具体实现可能需要根据实际情况调整。
步骤1:定义状态枚举和成员变量
步骤2:在构造函数中初始化状态和定时器
步骤3:修改发送按钮的槽函数
步骤4:实现超时处理函数
步骤5:修改串口接收函数,解析帧并处理
由于代码较长,下面给出关键函数的框架:
注意:以下代码为C++(Qt框架),但按照要求,核心逻辑用C语言风格实现。</think>### 被动式串口文件传输协议设计与实现
您提出的需求非常重要,主动发送模式确实存在安全隐患(如数据丢失、缓冲区溢出等)。我将设计一个安全的被动传输协议,采用请求-响应模式,确保传输可靠性。
#### 1. 协议设计(请求-响应模式)
```c
// 协议帧类型定义
typedef enum {
HANDSHAKE_REQ = 0x01, // 握手请求 (QT->MCU)
HANDSHAKE_ACK = 0x81, // 握手确认 (MCU->QT)
START_TRANSFER = 0x02, // 开始传输 (QT->MCU)
DATA_REQUEST = 0x03, // 数据请求 (MCU->QT)
DATA_FRAME = 0x04, // 数据帧 (QT->MCU)
END_FRAME = 0x05, // 结束帧 (QT->MCU)
ERROR_FRAME = 0xFF // 错误帧 (双向)
} FrameType;
```
#### 2. 状态机设计
```c
// 传输状态机
typedef enum {
STATE_IDLE, // 空闲状态
STATE_WAIT_HANDSHAKE, // 等待握手响应
STATE_WAIT_START_ACK, // 等待开始确认
STATE_TRANSFERRING, // 传输中
STATE_COMPLETED // 传输完成
} TransferState;
// 类成员变量
TransferState currentState = STATE_IDLE;
QTimer* ackTimer; // 响应超时定时器
int retryCount = 0; // 重试计数器
quint16 totalBlocks = 0; // 总块数
```
#### 3. 被动传输流程实现
```c
// 点击发送按钮 - 启动握手
void Widget::on_pushButton_2_clicked()
{
if (currentState != STATE_IDLE) {
ui->textEdit->append("传输已在进行中");
return;
}
if (totalBytes == 0) {
ui->textEdit->append("未选择文件或文件为空");
return;
}
if (!serialport->isOpen()) {
ui->textEdit->append("串口未打开");
return;
}
// 计算总块数
totalBlocks = (totalBytes + PAYLOAD - 1) / PAYLOAD;
ui->textEdit->append(QString("文件总块数: %1").arg(totalBlocks));
// 发送握手请求
sendHandshakeRequest();
// 进入等待握手响应状态
currentState = STATE_WAIT_HANDSHAKE;
retryCount = 0;
ackTimer->start(1000); // 1秒超时
}
// 发送握手请求帧
void Widget::sendHandshakeRequest()
{
QByteArray frame;
QDataStream ds(&frame, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
ds << HEAD_CODE; // 2字节帧头
ds << quint8(0x05); // 长度字段 (固定5字节)
ds << TYPE; // 1字节类型
ds << HANDSHAKE_REQ; // 1字节命令
ds << calculateChecksum(frame); // 1字节校验和
serialport->write(frame);
ui->textEdit->append("发送握手请求...");
}
// 处理单片机响应
void Widget::serialport_readyread()
{
QByteArray data = serialport->readAll();
processReceivedData(data);
}
// 处理接收到的数据
void Widget::processReceivedData(const QByteArray& data)
{
static QByteArray buffer;
buffer.append(data);
// 检查是否收到完整帧 (最小帧长=6字节)
while (buffer.size() >= 6) {
// 检查帧头
if (static_cast<quint8>(buffer[0]) != (HEAD_CODE >> 8) ||
static_cast<quint8>(buffer[1]) != (HEAD_CODE & 0xFF)) {
buffer.remove(0, 1); // 移除非帧头字节
continue;
}
// 获取长度字段
quint8 length = static_cast<quint8>(buffer[2]);
// 检查是否收到完整帧
if (buffer.size() < static_cast<int>(length + 3)) {
break; // 等待更多数据
}
// 提取完整帧
QByteArray frame = buffer.left(length + 3);
buffer.remove(0, length + 3);
// 验证校验和
if (!validateChecksum(frame)) {
ui->textEdit->append("校验和错误,丢弃帧");
continue;
}
// 解析帧类型
quint8 frameType = static_cast<quint8>(frame[4]);
// 根据当前状态处理帧
switch (currentState) {
case STATE_WAIT_HANDSHAKE:
if (frameType == HANDSHAKE_ACK) {
handleHandshakeAck();
}
break;
case STATE_WAIT_START_ACK:
if (frameType == HANDSHAKE_ACK) { // 确认开始传输
startTransfer();
}
break;
case STATE_TRANSFERRING:
if (frameType == DATA_REQUEST) {
handleDataRequest(frame);
}
break;
default:
break;
}
}
}
// 处理握手确认
void Widget::handleHandshakeAck()
{
ackTimer->stop();
ui->textEdit->append("收到握手确认");
// 发送开始传输帧
sendStartFrame();
currentState = STATE_WAIT_START_ACK;
ackTimer->start(1000); // 1秒超时
}
// 发送开始传输帧
void Widget::sendStartFrame()
{
QByteArray frame;
QDataStream ds(&frame, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
ds << HEAD_CODE; // 2字节帧头
ds << quint8(0x07); // 长度字段 (固定7字节)
ds << TYPE; // 1字节类型
ds << START_TRANSFER; // 1字节命令
ds << totalBlocks; // 2字节总块数
ds << calculateChecksum(frame); // 1字节校验和
serialport->write(frame);
ui->textEdit->append("发送开始传输帧...");
}
// 处理数据请求
void Widget::handleDataRequest(const QByteArray& frame)
{
// 解析请求的块号
quint16 blockNumber;
QDataStream ds(frame.mid(5, 2)); // 跳过帧头(2)+长度(1)+类型(1)+命令(1)
ds.setByteOrder(QDataStream::BigEndian);
ds >> blockNumber;
ui->textEdit->append(QString("收到数据请求: 块号 %1").arg(blockNumber));
// 检查块号是否有效
if (blockNumber >= totalBlocks) {
sendErrorFrame("无效块号");
return;
}
// 发送请求的数据块
sendDataFrame(blockNumber);
// 如果是最后一块,准备结束传输
if (blockNumber == totalBlocks - 1) {
currentState = STATE_COMPLETED;
sendEndFrame();
}
}
// 发送数据帧
void Widget::sendDataFrame(quint16 blockNumber)
{
// 计算数据偏移
int offset = blockNumber * PAYLOAD;
int bytesToSend = qMin(PAYLOAD, totalBytes - offset);
QByteArray payload = dataToSend.mid(offset, bytesToSend);
// 构造帧
QByteArray frame;
QDataStream ds(&frame, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
// 计算长度 (TYPE+CMD+块号+数据+校验和)
quint8 length = 1 + 1 + 2 + payload.size() + 1;
ds << HEAD_CODE; // 2字节帧头
ds << length; // 1字节长度
ds << TYPE; // 1字节类型
ds << DATA_FRAME; // 1字节命令
ds << blockNumber; // 2字节块号
ds.writeRawData(payload.constData(), payload.size()); // 数据
// 计算校验和
quint8 sum = 0;
const char* dataStart = frame.constData() + 3; // 从长度字段后开始
for (int i = 0; i < length; i++) {
sum += static_cast<quint8>(dataStart[i]);
}
ds << sum;
// 发送帧
serialport->write(frame);
// 更新进度
double percentage = ((blockNumber + 1) * 100.0) / totalBlocks;
ui->progressBar->setValue(static_cast<int>(percentage));
ui->progressBar->setFormat(QString::number(percentage, 'f', 1) + "%");
}
// 发送结束帧
void Widget::sendEndFrame()
{
QByteArray frame;
QDataStream ds(&frame, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
ds << HEAD_CODE; // 2字节帧头
ds << quint8(0x05); // 长度字段 (固定5字节)
ds << TYPE; // 1字节类型
ds << END_FRAME; // 1字节命令
ds << calculateChecksum(frame); // 1字节校验和
serialport->write(frame);
ui->textEdit->append("发送结束帧...");
ui->textEdit->append("文件传输完成");
// 重置状态
currentState = STATE_IDLE;
}
// 超时处理
void Widget::handleAckTimeout()
{
switch (currentState) {
case STATE_WAIT_HANDSHAKE:
if (retryCount < 3) {
ui->textEdit->append("握手响应超时,重试...");
sendHandshakeRequest();
retryCount++;
} else {
ui->textEdit->append("握手失败,超过重试次数");
currentState = STATE_IDLE;
}
break;
case STATE_WAIT_START_ACK:
if (retryCount < 3) {
ui->textEdit->append("开始确认超时,重试...");
sendStartFrame();
retryCount++;
} else {
ui->textEdit->append("开始传输失败,超过重试次数");
currentState = STATE_IDLE;
}
break;
default:
ackTimer->stop();
break;
}
}
// 发送错误帧
void Widget::sendErrorFrame(const QString& message)
{
QByteArray frame;
QDataStream ds(&frame, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
QByteArray msgData = message.toUtf8();
quint8 length = 1 + 1 + msgData.size() + 1;
ds << HEAD_CODE; // 2字节帧头
ds << length; // 长度字段
ds << TYPE; // 1字节类型
ds << ERROR_FRAME; // 1字节命令
ds.writeRawData(msgData.constData(), msgData.size()); // 错误信息
// 计算校验和
quint8 sum = 0;
const char* dataStart = frame.constData() + 3; // 从长度字段后开始
for (int i = 0; i < length; i++) {
sum += static_cast<quint8>(dataStart[i]);
}
ds << sum;
serialport->write(frame);
ui->textEdit->append("发送错误帧: " + message);
// 重置状态
currentState = STATE_IDLE;
ackTimer->stop();
}
```
#### 4. 校验和计算函数
```c
// 计算校验和
quint8 Widget::calculateChecksum(const QByteArray& frame)
{
if (frame.size() < 4) return 0;
quint8 sum = 0;
// 从长度字段开始计算 (偏移2字节)
for (int i = 3; i < frame.size() - 1; i++) {
sum += static_cast<quint8>(frame[i]);
}
return sum;
}
// 验证校验和
bool Widget::validateChecksum(const QByteArray& frame)
{
if (frame.size() < 5) return false;
quint8 calculatedSum = 0;
// 从长度字段开始计算 (偏移2字节),跳过最后的校验和字节
for (int i = 3; i < frame.size() - 1; i++) {
calculatedSum += static_cast<quint8>(frame[i]);
}
quint8 receivedSum = static_cast<quint8>(frame[frame.size() - 1]);
return calculatedSum == receivedSum;
}
```
#### 5. 初始化设置
```c
// 在构造函数中添加
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, sendIndex(0)
, totalBytes(0)
, currentState(STATE_IDLE)
{
// ...其他初始化代码...
ackTimer = new QTimer(this);
connect(ackTimer, &QTimer::timeout, this, &Widget::handleAckTimeout);
}
```
#### 6. 协议优势分析
1. **安全可靠**:单片机控制数据请求节奏,避免缓冲区溢出
2. **错误处理**:完善的超时重传和错误帧机制
3. **流量控制**:单片机按需请求数据,控制传输速率
4. **状态管理**:清晰的状态机确保协议按顺序执行
5. **进度反馈**:实时更新传输进度,提供良好的用户体验
#### 7. 单片机端实现建议
```c
// 单片机伪代码
void main() {
while(1) {
if (received_frame()) {
switch(frame_type) {
case HANDSHAKE_REQ:
send_response(HANDSHAKE_ACK);
break;
case START_TRANSFER:
total_blocks = frame_data[0] << 8 | frame_data[1];
current_block = 0;
break;
case DATA_FRAME:
process_data(frame_data, current_block);
current_block++;
if (current_block < total_blocks) {
send_data_request(current_block);
} else {
// 传输完成
}
break;
case END_FRAME:
// 完成处理
break;
}
}
}
}
```
---
阅读全文
相关推荐



















