#include "widget.h" #include "ui_widget.h" #include <QFileDialog> #include <QDateTime> #include <QTextStream> #include <QSerialPortInfo> #include <QMessageBox> #include <QTimer> #include <QSerialPort> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) , sendIndex(0) , totalBytes(0) , currentState(STATE_IDLE) { ui->setupUi(this); QStringList serialNamePort; serialport = new QSerialPort(this); timerSerial = new QTimer(this); ackTimer = new QTimer(this); // 连接信号和槽 connect(serialport, SIGNAL(readyRead()), this, SLOT(serialport_readyread())); connect(timerSerial, SIGNAL(timeout()), this, SLOT(TimerUpdate())); connect(ackTimer, &QTimer::timeout, this, &Widget::handleAckTimeout); // 搜索一个文件夹中的串口号 foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { serialNamePort << info.portName(); } ui->comboBox->addItems(serialNamePort); } Widget::~Widget() { delete ui; } void Widget::on_openseriButton_clicked() { QSerialPort::BaudRate baudrate = QSerialPort::Baud115200; // 设置波特率115200 QSerialPort::DataBits databits = QSerialPort::Data8; // 数据位8 QSerialPort::StopBits stopbits = QSerialPort::OneStop; // 停止位1 QSerialPort::Parity checkbits = QSerialPort::NoParity; // 无校验 serialport->setPortName(ui->comboBox->currentText()); // 设置串口号 serialport->setBaudRate(baudrate); // 设置波特率 serialport->setDataBits(databits); // 设置数据位 serialport->setStopBits(stopbits); // 设置停止位 serialport->setParity(checkbits); // 设置校验位 if (serialport->open(QIODevice::ReadWrite)) { // 判断是否打开成功 QMessageBox::information(this, "提示", "打开成功"); } else { QMessageBox::critical(this, "提示", "打开失败"); } } void Widget::on_refuseButton_clicked() { QStringList serialNamePort; ui->comboBox->clear(); foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { serialNamePort << info.portName(); } ui->comboBox->addItems(serialNamePort); } void Widget::on_closeButton_2_clicked() { sendIndex=0; timerSerial->stop(); serialport->close(); } void Widget::on_pushButton_clicked() { QFileDialog* f = new QFileDialog(this); f->setWindowTitle("选择数据文件*.bin"); f->setNameFilter("*.bin"); f->setViewMode(QFileDialog::Detail); ui->lineEdit->clear(); ui->textEdit->clear(); QString filePath; if (f->exec() == QDialog::Accepted) filePath = f->selectedFiles()[0]; ui->lineEdit->setText(filePath); ui->textEdit->append("文件路径:" + filePath); // 文件信息 QFileInfo info(filePath); ui->textEdit->append(QString("文件大小:%1 byte").arg(info.size())); ui->textEdit->append(QString("文件名称:%1").arg(info.fileName())); ui->textEdit->append(QString("创建时间:%1").arg(info.created().toString("yyyy-MM-dd hh:mm:ss"))); ui->textEdit->append(QString("修改时间:%1").arg(info.lastModified().toString("yyyy-MM-dd hh:mm:ss"))); // 文件内容 QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { ui->textEdit->append("文件打开失败"); return; } QByteArray fileContent = file.readAll(); file.close(); delete f; // 以十六进制格式显示文件内容 // QString hexContent = fileContent.toHex(' '); // ui->textEdit->append("文件内容(十六进制格式):"); // ui->textEdit->append(hexContent); // 准备发送的数据 dataToSend = fileContent; sendIndex = 0; totalBytes = dataToSend.size(); } const quint16 HEAD_CODE = 0xA55A; const quint8 TYPE = 0xE1; const quint8 CMD = 0x12; // FLASH_WRITE_BUFF_SEG const int PAYLOAD = 128; // 每帧真正写 Flash 的字节数 // void Widget::on_pushButton_2_clicked() // { // // 判断文件是否已选择 // if (totalBytes == 0) { // ui->textEdit->append("未选择文件或文件为空"); // return; // } // // 判断串口是否已打开 // if (!serialport->isOpen()) { // ui->textEdit->append("串口未打开"); // return; // } // timerSerial->start(100); // 每秒发送一次 // } void Widget::TimerUpdate() { if (sendIndex >= totalBytes) { ui->textEdit->append("文件发送完成"); timerSerial->stop(); return; } /*------------------------------------------------- * 1. 计算当前块号(从 0 开始) *------------------------------------------------*/ quint16 block = sendIndex / PAYLOAD; // 第几块 /*------------------------------------------------- * 2. 取本帧要发的 128 字节(最后一帧可能不足 128) *------------------------------------------------*/ int bytesToSend = qMin(PAYLOAD, totalBytes - sendIndex); QByteArray userData = dataToSend.mid(sendIndex, bytesToSend); /*------------------------------------------------- * 3. 构造完整帧 *------------------------------------------------*/ QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); ds << HEAD_CODE; // 2 字节 ds << quint8(5 + userData.size()); // Length = 固定5 + 数据长度 + 校验和 ds << TYPE; // 1 字节 ds << CMD; // 1 字节 ds << block; // 2 字节(Data0+Data1) ds.writeRawData(userData.constData(), userData.size()); // 128 或更少 /*------------------------------------------------- * 4. 计算校验和 * SUM = TYPE + CMD + Data0 + Data1 + ... + DataN *------------------------------------------------*/ quint8 sum = TYPE + CMD; sum += (block & 0xFF); // Data0 sum += ((block >> 8) & 0xFF); // Data1 for (int i = 0; i < userData.size(); ++i) sum += static_cast<quint8>(userData.at(i)); ds << sum; // 1 字节校验和 /*------------------------------------------------- * 5. 真正写到串口 *------------------------------------------------*/ qint64 bytesWritten = serialport->write(frame); if (bytesWritten == -1) { ui->textEdit->append("发送失败: " + serialport->errorString()); timerSerial->stop(); return; } /*------------------------------------------------- * 6. 更新进度 *------------------------------------------------*/ sendIndex += bytesToSend; // 注意:是用户数据长度,不是帧长度 ui->textEdit->append(QString("已发送 %1 / %2 字节") .arg(sendIndex).arg(totalBytes)); double percentage = (sendIndex * 100.0 / totalBytes); ui->progressBar->setValue(static_cast<int>(percentage)); ui->progressBar->setFormat(QString::number(percentage, 'f', 1) + "%"); /*------------------------------------------------- * 7. 可选:等待真正发完(非阻塞) *------------------------------------------------*/ serialport->flush(); // 如之前讨论,可保留或改为 waitForBytesWritten } // 点击发送按钮 - 启动握手 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 << quint8(TYPE); // 1字节类型 ds << quint8(0x11); // 1字节命令 ds << quint8(0x88); // 长度字段 (固定5字节) ds << quint8(0x88); // 长度字段 (固定5字节) 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); ui->textEdit->append("收到数据"); // 检查是否收到完整帧 (最小帧长=6字节) while (buffer.size() >= 4) { // 检查帧头 if (static_cast<quint8>(buffer[0]) != (0x5A) || static_cast<quint8>(buffer[1]) != (0xA5)) { 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]); ui->textEdit->append("收到数据"); // 根据当前状态处理帧 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_TRANSFERRING; ackTimer->start(1000); // 1秒超时 } // 发送开始传输帧 void Widget::sendStartFrame() { QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); ds << HEAD_CODE; // 2字节帧头 ds << quint8(0x05); // 长度字段 (固定7字节) ds << quint8(TYPE); // 1字节类型 ds << quint8(0x11); // 1字节命令 ds << quint8(0x99); // 2字节总块数 ds << quint8(0x99); // 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字节长度 ui->textEdit->append(QString("发送长度 %1").arg(length)); ds << quint8(TYPE); // 1字节类型 ds << quint8(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 << quint8(TYPE); // 1字节类型 ds << quint8(END_FRAME); // 1字节命令 ds << quint8(0x01); // 1字节命令 ds << quint8(0x01); // 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(); } // 计算校验和 quint8 Widget::calculateChecksum(const QByteArray& frame) { if (frame.size() < 4) return 0; quint8 sum = 0; // 从长度字段开始计算 (偏移2字节) for (int i = 3; i < frame.size(); 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; } void Widget::startTransfer() { // 实现代码 // 例如:发送握手请求 // sendHandshakeRequest(); } 这段代码校验是正确的,协议也是正确的,最后的结束帧不是立即发出的,而是,mcu要过最后一帧数据后,又要数据,再发结束帧,比如有0-128包数据(从零开始,128是最后一包),128包已经发完,单片机又要129包,再发结束帧
时间: 2025-07-24 08:24:11 浏览: 12
<think>我们分析一下问题:根据描述,当前的协议设计是,在传输完所有数据块(0到127,共128块)后,单片机还会请求第128块(即129包?),但此时已经没有数据了,所以需要发送结束帧。然而,在代码中,我们是在收到数据请求时判断块号是否有效(即是否在0到totalBlocks-1范围内),如果无效则发送错误帧。但这里的情况是,单片机请求了第128块(块号从0开始,所以128块是第129个请求?),而我们的总块数是128(块号0到127),所以128块是无效的。
但是,根据问题描述,我们期望在发送完最后一包(第127块)后,当单片机再次请求(请求块号128)时,我们发送结束帧。然而,在当前的代码中,当收到块号128时,因为128 >= totalBlocks(128),所以会进入无效块号的处理,发送错误帧。这显然不是我们想要的。
因此,我们需要修改处理逻辑:在收到数据请求时,如果块号等于totalBlocks(即128),则说明单片机在请求最后一包之后的下一个包(实际上没有数据了),此时我们应该发送结束帧,而不是错误帧。
修改建议:
在`handleDataRequest`函数中,当块号无效时,我们检查块号是否等于totalBlocks(即下一个块),如果是,则发送结束帧,否则发送错误帧。
具体修改如下:
在`handleDataRequest`函数中:
原代码:
if (blockNumber >= totalBlocks) {
sendErrorFrame("无效块号");
return;
}
修改为:
if (blockNumber >= totalBlocks) {
// 如果块号等于totalBlocks,说明是最后一包之后的一个请求,发送结束帧
if (blockNumber == totalBlocks) {
// 发送结束帧
sendEndFrame();
currentState = STATE_COMPLETED;
} else {
sendErrorFrame("无效块号");
}
return;
}
但是注意,我们的块号是从0开始的,所以总块数为totalBlocks,那么有效的块号是0到totalBlocks-1。当块号等于totalBlocks时,表示一个不存在的块(即第totalBlocks块),按照协议,此时应该发送结束帧。
另外,在发送结束帧后,我们将状态设置为STATE_COMPLETED,并停止传输。
然而,我们还需要注意,在发送结束帧后,应该停止超时计时器,并将状态重置为IDLE吗?根据描述,发送结束帧后传输完成,所以我们可以将状态重置为IDLE,并停止超时计时器。
但是,在`sendEndFrame`函数中,我们已经将状态设置为STATE_IDLE。所以,在发送结束帧后,状态会被重置。
因此,我们修改`handleDataRequest`函数中的块号检查部分:
同时,我们还需要注意,在发送最后一包(块号totalBlocks-1)后,我们并没有立即发送结束帧,而是等待单片机请求下一包(块号totalBlocks)时再发送结束帧。所以,在发送最后一包数据时,我们不应该发送结束帧,而是等待下一个请求。
所以,修改后的`handleDataRequest`函数中关于块号检查的部分如下:
// 检查块号是否有效
if (blockNumber >= totalBlocks) {
// 如果块号等于totalBlocks,说明是最后一包之后的一个请求,发送结束帧
if (blockNumber == totalBlocks) {
ui->textEdit->append("收到结束请求,发送结束帧");
sendEndFrame();
} else {
sendErrorFrame("无效块号");
}
return;
}
另外,在发送最后一包数据(块号totalBlocks-1)后,我们原本的代码中有一个判断:
if (blockNumber == totalBlocks - 1) {
currentState = STATE_COMPLETED;
sendEndFrame(); // 注意:这里我们原来是在发送最后一包数据后立即发送结束帧,现在要改掉
}
现在,由于我们改为在收到下一个请求(块号totalBlocks)时才发送结束帧,所以我们需要去掉在发送最后一包数据后立即发送结束帧的逻辑。也就是说,在发送最后一包数据后,我们不应该发送结束帧,而是等待单片机的下一个请求。
因此,我们注释掉发送最后一包数据后发送结束帧的代码:
// 如果是最后一块,准备结束传输
// 注意:现在不再在发送数据后立即发送结束帧,而是等待下一个请求(块号等于totalBlocks)时发送结束帧
// 所以,我们只需要在发送最后一包数据后,等待下一个请求即可,不需要做额外操作
// 因此,删除以下代码:
// if (blockNumber == totalBlocks - 1) {
// currentState = STATE_COMPLETED;
// sendEndFrame();
// }
修改后的`handleDataRequest`函数中,发送数据后不再判断是否为最后一包而发送结束帧。
总结修改点:
1. 修改块号无效的判断,当块号等于totalBlocks时,发送结束帧;否则发送错误帧。
2. 去掉在发送最后一包数据后立即发送结束帧的代码。
具体代码修改如下:
在`handleDataRequest`函数中,修改块号检查部分:
// 解析请求的块号
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) {
// 如果块号等于totalBlocks,说明是最后一包之后的一个请求,发送结束帧
if (blockNumber == totalBlocks) {
ui->textEdit->append("收到结束请求,发送结束帧");
sendEndFrame();
} else {
sendErrorFrame(QString("无效块号: %1 (总块数: %2)").arg(blockNumber).arg(totalBlocks));
}
return;
}
// 发送请求的数据块
sendDataFrame(blockNumber);
// 注意:这里去掉了原来的最后一包发送结束帧的逻辑
同时,在`sendEndFrame`函数中,我们设置状态为IDLE,并停止计时器,这些保持不变。
另外,我们还需要注意,在发送结束帧后,单片机可能会回复一个确认,但根据协议,我们可能不需要处理,或者需要处理。但根据问题描述,我们只需要在收到块号等于totalBlocks的请求时发送结束帧即可。
但是,我们也要考虑,如果单片机在收到结束帧后不再发送请求,那么我们的状态已经变为IDLE,所以后续的请求将不会被处理(因为状态不是TRANSFERRING)。所以,如果单片机在收到结束帧后还继续发送请求,我们可能会忽略或者发送错误帧。这需要根据协议来定。
根据描述,发送结束帧后,整个传输就完成了,所以单片机不应该再发送数据请求。如果发送,我们可以在状态为IDLE时忽略或者发送错误帧。但当前代码在状态为TRANSFERRING时才会处理数据请求,所以当状态变为IDLE后,再收到数据请求将不会被处理(因为状态机不处于TRANSFERRING状态)。
因此,我们按照上述修改即可。
但是,我们还需要注意,在发送结束帧后,应该停止超时计时器。因为传输已经结束,不再需要重传。
在`sendEndFrame`函数中,我们已经停止了超时计时器吗?目前没有,所以我们需要在发送结束帧后停止计时器。
修改`sendEndFrame`函数:
void Widget::sendEndFrame()
{
... [原代码]
// 重置状态
currentState = STATE_IDLE;
ackTimer->stop(); // 停止超时计时器
}
另外,在错误处理中,发送错误帧后也应该停止计时器并重置状态。
在`sendErrorFrame`函数中,我们已经做了:
currentState = STATE_IDLE;
ackTimer->stop();
所以,不需要修改。
最后,我们还需要注意,在发送结束帧后,我们可能还需要更新界面显示,比如显示传输完成。
在`sendEndFrame`函数中,我们已经有:
ui->textEdit->append("发送结束帧...");
ui->textEdit->append("文件传输完成");
所以,界面显示已经完成。
但是,我们还需要注意,在发送结束帧后,进度条应该设置为100%。因为最后一包数据发送后,进度条可能还没有到100%。不过,在发送最后一包数据(块号totalBlocks-1)时,进度条已经更新到((totalBlocks-1+1)/totalBlocks *100%)= 100%。所以,进度条已经100%了,不需要再设置。
但是,如果我们在发送结束帧之前,最后一包数据发送后,进度条已经100%了,所以没有问题。
因此,按照上述修改代码即可。
现在,我们重新整理一下修改后的代码部分:
1. 修改`handleDataRequest`函数中的块号检查逻辑。
2. 去掉发送最后一包数据后发送结束帧的代码(即删除原来判断块号为最后一包时发送结束帧的代码)。
3. 在`sendEndFrame`函数中停止超时计时器(如果之前没有的话,加上)。
但是,查看我们之前的`sendEndFrame`函数,并没有停止ackTimer,所以需要加上:
void Widget::sendEndFrame()
{
... [构造帧并发送]
ui->textEdit->append("发送结束帧...");
ui->textEdit->append("文件传输完成");
// 重置状态
currentState = STATE_IDLE;
ackTimer->stop(); // 停止超时重传计时器
}
注意:在`handleDataRequest`函数中,当收到块号等于totalBlocks时,我们调用`sendEndFrame`,然后状态变为IDLE,并停止计时器。
另外,在`handleDataRequest`函数中,我们修改块号检查部分,并去掉最后一包发送结束帧的代码。
下面给出修改后的`handleDataRequest`函数代码(仅修改部分):
void Widget::handleDataRequest(const QByteArray& frame)
{
// 解析请求的块号
quint16 blockNumber;
QDataStream ds(frame.mid(5, 2)); // 跳过帧头(2)+长度(1)+类型(1)+命令(1) -> 共5字节,然后取2字节块号
ds.setByteOrder(QDataStream::BigEndian);
ds >> blockNumber;
ui->textEdit->append(QString("收到数据请求: 块号 %1").arg(blockNumber));
// 检查块号是否有效
if (blockNumber >= totalBlocks) {
// 如果块号等于totalBlocks,说明是最后一包之后的一个请求,发送结束帧
if (blockNumber == totalBlocks) {
ui->textEdit->append("收到结束请求,发送结束帧");
sendEndFrame();
} else {
sendErrorFrame(QString("无效块号: %1 (总块数: %2)").arg(blockNumber).arg(totalBlocks));
}
return;
}
// 发送请求的数据块
sendDataFrame(blockNumber);
// 注意:这里去掉了原来的最后一包发送结束帧的逻辑
}
同时,在`sendEndFrame`函数中,我们加上停止计时器的代码:
void Widget::sendEndFrame()
{
QByteArray frame;
QDataStream ds(&frame, QIODevice::WriteOnly);
ds.setByteOrder(QDataStream::BigEndian);
ds << HEAD_CODE; // 2字节帧头
ds << quint8(0x05); // 长度字段 (固定5字节)
ds << quint8(TYPE); // 1字节类型
ds << quint8(END_FRAME); // 1字节命令
ds << quint8(0x01); // 1字节命令
ds << quint8(0x01); // 1字节命令
ds << calculateChecksum(frame); // 1字节校验和
serialport->write(frame);
ui->textEdit->append("发送结束帧...");
ui->textEdit->append("文件传输完成");
// 重置状态
currentState = STATE_IDLE;
ackTimer->stop(); // 停止超时计时器
}
注意:在`sendEndFrame`函数中,我们构造结束帧的方式可能有问题。根据协议,结束帧应该包含哪些字段?从代码看,我们构造了一个固定5字节(不包含帧头和校验和)的帧,但实际上我们填充了5个字节(从TYPE到两个0x01)。但是,根据协议,结束帧的具体格式需要和单片机约定。这里我们假设结束帧的格式是:
帧头(2字节): 0xA55A
长度(1字节): 0x05 (表示后面有5个字节:TYPE, CMD, 两个字节的数据(这里用0x0101填充),以及校验和)
类型(1字节): TYPE (0xE1)
命令(1字节): END_FRAME (需要定义,比如0x13)
数据(2字节): 0x0101 (这里可能是任意值,但协议需要明确,如果不需要数据,可以去掉,那么长度就要改变)
但是,根据问题描述,协议中并没有明确结束帧的格式,所以需要根据实际情况调整。这里我们按照原代码不变,因为原代码中结束帧就是这样构造的。但要注意,原代码中结束帧的命令是END_FRAME(需要确保定义正确,比如0x13)。
另外,在计算校验和时,原代码中使用了`calculateChecksum`函数,但这个函数计算的是从长度字段之后(不包括长度字段)到校验和之前的所有字节的和。这个函数在结束帧中也是适用的。
所以,结束帧的格式保持不变。
最后,我们还需要注意,在状态机中,当处于STATE_COMPLETED状态时,我们并没有处理。但我们在发送结束帧后直接进入STATE_IDLE,所以不需要STATE_COMPLETED状态了。因此,我们可以删除STATE_COMPLETED状态。
在枚举状态中,我们定义了STATE_COMPLETED,但发送结束帧后我们直接进入IDLE,所以这个状态没有用到,可以删除。
但是,为了代码清晰,我们保留这个状态,或者删除都可以。这里我们选择删除,因为用不到了。
修改状态枚举:
enum TransferState {
STATE_IDLE,
STATE_WAIT_HANDSHAKE,
STATE_WAIT_START_ACK,
STATE_TRANSFERRING
// 删除STATE_COMPLETED
};
同时,在`handleDataRequest`函数中,当发送结束帧后,状态变为IDLE(在`sendEndFrame`函数中设置),所以不需要再设置。
因此,我们删除STATE_COMPLETED状态。
修改头文件widget.h中的枚举:
enum TransferState {
STATE_IDLE,
STATE_WAIT_HANDSHAKE,
STATE_WAIT_START_ACK,
STATE_TRANSFERRING
};
并删除所有STATE_COMPLETED的出现。
这样修改后,代码应该符合要求了。
注意:由于代码较长,以上只列出了需要修改的部分。请根据实际代码进行调整。
如果还有问题,请根据实际运行情况进行调试。
相关问题:
1. 在状态机设计中,如何处理超时重传?
2. 为什么在发送结束帧后需要停止超时计时器?
3. 校验和计算中,为什么从长度字段之后开始计算?
4. 如果单片机在收到结束帧后没有响应,应该如何处理?
请按照要求格式回答。</think>根据您的描述和代码分析,问题在于协议逻辑中结束帧的发送时机不正确。当前代码在发送最后一包数据后立即发送结束帧,但实际协议要求是在单片机请求下一包(超出范围)时才发送结束帧。以下是修改方案:
### 问题原因
1. **协议时序错误**:当前代码在发送最后一包数据(块号`totalBlocks-1`)后立即发送结束帧(`sendEndFrame()`)
2. **不符合握手协议**:实际要求是在单片机请求超出范围的块号(如128块时请求129包)时才发送结束帧
3. **状态机缺陷**:缺少对"超出范围请求"的特殊处理逻辑
### 解决方案
修改`handleDataRequest()`函数,增加对超出范围块号的处理逻辑:
```cpp
void Widget::handleDataRequest(const QByteArray& frame)
{
// ... [解析块号代码不变] ...
ui->textEdit->append(QString("收到数据请求: 块号 %1").arg(blockNumber));
// ===== 修改开始 =====
// 检查块号是否超出范围
if (blockNumber >= totalBlocks) {
// 如果是预期的下一包请求(块号=总块数)
if (blockNumber == totalBlocks) {
ui->textEdit->append("收到结束请求,发送结束帧");
sendEndFrame();
}
// 如果是无效的超范围请求
else {
sendErrorFrame("无效块号: " + QString::number(blockNumber));
}
return;
}
// ===== 修改结束 =====
// 发送请求的数据块
sendDataFrame(blockNumber);
// ===== 删除以下代码 =====
// if (blockNumber == totalBlocks - 1) {
// currentState = STATE_COMPLETED;
// sendEndFrame();
// }
}
```
### 完整协议流程说明
1. **握手阶段**:
```mermaid
sequenceDiagram
PC->>MCU: 握手请求(0x11)
MCU->>PC: 握手确认(ACK)
PC->>MCU: 开始传输(0x99) + 总块数
```
2. **数据传输阶段**:
```mermaid
sequenceDiagram
loop 每块数据传输
MCU->>PC: 数据请求(块号N)
PC->>MCU: 数据帧(块号N+128B数据)
end
```
3. **结束阶段**:
```mermaid
sequenceDiagram
MCU->>PC: 数据请求(块号128) // 超出范围
PC->>MCU: 结束帧(0x13)
```
### 需要同步修改
1. **在`sendEndFrame()`中停止计时器**:
```cpp
void Widget::sendEndFrame()
{
// ... [帧构造代码不变] ...
// 增加停止计时器
ackTimer->stop();
currentState = STATE_IDLE;
}
```
2. **状态枚举精简**(widget.h中):
```cpp
enum TransferState {
STATE_IDLE,
STATE_WAIT_HANDSHAKE,
STATE_WAIT_START_ACK,
STATE_TRANSFERRING
// 删除STATE_COMPLETED状态
};
```
### 修改后协议优势
1. **严格遵循请求-响应模型**:每次发送都响应MCU的明确请求
2. **避免数据冲突**:防止在MCU未准备好时发送结束帧
3. **提高兼容性**:适应不同速度的MCU处理能力
4. **明确传输终止信号**:结束帧作为对非法请求的正式响应
### 注意事项
1. 总块数计算:`totalBlocks = (totalBytes + PAYLOAD - 1) / PAYLOAD`
2. 块号范围:有效块号 0 ~ (totalBlocks-1)
3. 特殊块号:`totalBlocks` 作为结束触发信号
4. 超时处理:保留3次重试机制防止丢包
阅读全文
相关推荐












