活动介绍

#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次重试机制防止丢包
阅读全文

相关推荐

大家在看

recommend-type

NAND FLASH 控制器源码(verilog)

这是NAND FLASH 控制器的verilog源码,很有参考价值! 这是NAND FLASH 控制器的verilog源码,很有参考价值!
recommend-type

实体消歧系列文章.rar

实体消歧系列文章.rar
recommend-type

matlab飞行轨迹代码-msa-toolkit:这是在MATLAB中开发的用于模拟火箭6自由度动力学的代码

matlab飞行模拟代码msa-工具包 MSA 工具包是存储任务分析团队实施的代码的存储库。 它由几个文件夹组成,将在下面的段落中简要介绍。 模拟器 这是在MATLAB中开发的用于模拟6自由度火箭动力学的代码。 该模拟器可预测 3D 轨迹、远地点、作用在火箭上的力以及各种其他空气动力学数据。 数据 包含当前飞行数据、火箭几何形状和模拟参数的文件夹。 通用功能 在该文件夹中,存储了工具包代码中使用的常用函数。 autoMatricesProtub 此代码允许使用 Missile DATCOM 自动计算火箭空气动力学系数,适用于不同的气闸配置。 空气动力学优化 此代码实现了火箭的空气动力学优化。 优化变量是鳍弦和高度、鳍形状、卵形长度和卵形形状。 代码使用遗传算法达到目的。 远地点分析 当结构质量已知且具有一定程度的不确定性时,此代码使用不同的电机执行主要的远地点分析,以选择最好的电机。 敏感性分析 该代码实现了对火箭上升阶段的敏感性分析。 有两种类型的分析可用:确定性和随机性。 在确定性分析中,可以改变空气动力学系数的标称值和火箭的结构质量。 变化的相对幅度由用户设置,并且对于分析中考虑
recommend-type

qt打包程序(自定义打包界面及功能)

1 BasePack项目是安装包界面,用静态编译的qt创建的项目 2 静态编译的环境是vs2017+32位的编译器编译而成 3 PackQtEx项目是打包界面,用的也是vs2017+32位编译器创建的 4 打包用的压缩库用的是32位的静态7z库 5 安装包用的解压缩库用的也是32位的静态7z库 6 没有选择vs2017+64位编译器的原因是,没法用64位的去静态编译qt库,我没试成功。 7 打包界面界面不是静态编译的qt创建的,为了使用相同的32位7z库,所以也选择了32位的vs2017编译器创建项目。
recommend-type

易语言WinSock模块应用

易语言WinSock模块应用源码,WinSock模块应用,启动,停止,监听,发送,接收,断开连接,取服务器端口,取服务器IP,取客户IP,取客户端口,异步选择,检查连接状态,连接,断开,关闭,创建,发送数据,接收数据,取本机名,取本机IP组,窗口1消息处理,客户进入,客户离开,数据到达

最新推荐

recommend-type

汽车电子领域CAN总线通信:DBC与Excel文件互转工具解析 v2.0

内容概要:本文介绍了CAN总线及其通信技术在汽车电子和工业自动化领域的应用,重点讲解了DBC文件与Excel文件之间的高效互转方法。DBC文件作为一种描述CAN通信中消息和信号的规范文件,在汽车电子领域至关重要。文中提到的工具不仅能从DBC文件中提取信息并导入到Excel文件中,还能将Excel文件的数据按DBC格式转换,支持传统CAN、CANFD及J1939协议,极大提高了转换效率和数据准确性。 适合人群:从事汽车电子、工业自动化相关工作的工程师和技术人员。 使用场景及目标:适用于需要频繁处理DBC文件与Excel文件转换的场合,如汽车开发、维护和故障诊断等,旨在提升工作效率和数据准确性。 其他说明:随着汽车电子和工业自动化技术的发展,掌握这些工具对于提高工作效率非常重要。
recommend-type

基于CAN通讯的rh850u2a16芯片Bootloader与OTA固件刷写系统 - Bootloader 必备版

基于CAN通讯的RH850U2A16芯片Bootloader及OTA刷写系统的开发过程及其功能特性。主要内容涵盖:1) CAN通讯Bootloader的设计,实现了远程固件升级;2) 配套CAPL上位机程序,便于用户操作;3) UDS服务刷写,支持多种OTA更新模式;4) Flash驱动可在RAM中运行,提升系统性能;5) 支持Boot与App互访数据,增强数据交互能力;6) 实现App有效标记检查与跳转,保障系统稳定性;7) 基于AUTOSAR标准架构,提供完整的配置工程;8) 开发了串口控制台程序,辅助调试;9) 使用量产级代码并采用GHS编译器,确保生产环境中的稳定运行。 适合人群:嵌入式系统开发者、汽车电子工程师、对CAN通讯和Bootloader感兴趣的工程师。 使用场景及目标:适用于需要远程固件升级和高可靠性的汽车电子控制系统。主要目标是提高系统的稳定性和可靠性,满足工业标准和生产需求。 其他说明:文中提到的技术和方法不仅展示了Bootloader的具体实现细节,还强调了系统设计的完整性和标准化,为后续开发和维护提供了坚实的基础。
recommend-type

springbooot+vue基于Java的宠物救助管理系统的设计与实现毕业论文.doc

springbooot+vue基于Java的宠物救助管理系统的设计与实现毕业论文.doc
recommend-type

Axure手机移动端+后台端常用元件库

Axure手机移动端+后台端常用元件库
recommend-type

基于COMSOL的锂枝晶模型研究:涵盖单枝晶到多枝晶的五种生长方式及物理场特性

利用COMSOL软件对锂枝晶模型的研究,涵盖了单枝晶定向生长、多枝晶定向生长、多枝晶随机生长、无序生长随机形核以及雪花枝晶五种生长方式。每种生长方式分别涉及相场、浓度场和电场三种物理场的交互作用,尤其强调了单枝晶定向生长的相关文献支持。通过具体的代码示例展示了如何在COMSOL中构建和模拟这些模型,帮助研究人员更好地理解锂枝晶的生成机制及其对电池性能的影响。 适合人群:从事电池研究的专业人士、材料科学家、物理学家及相关领域的研究生。 使用场景及目标:适用于希望深入了解锂枝晶生长机理及其物理场特性的科研工作者,旨在为他们提供理论依据和技术手段,促进相关领域的进一步研究。 其他说明:文中提供了详细的物理场分析和代码示例,有助于读者实际操作和验证研究成果。同时,还引用了单枝晶定向生长的相关文献,增加了研究的可信度和深度。
recommend-type

年轻时代音乐吧二站:四万音乐与图片资料库

根据提供的信息,我们可以梳理出以下知识点: ### 知识点一:年轻时代音乐吧二站修正版 从标题“年轻时代音乐吧二站修正版”可以推断,这是一个与音乐相关的网站或平台。因为提到了“二站”,这可能意味着该平台是某个项目或服务的第二代版本,表明在此之前的版本已经存在,并在此次发布中进行了改进或修正。 #### 描述与知识点关联 描述中提到的“近四万音乐数据库”,透露了该音乐平台拥有一个庞大的音乐库,覆盖了大约四万首歌曲。对于音乐爱好者而言,这表明用户可以访问和欣赏到广泛和多样的音乐资源。该数据库的规模对于音乐流媒体平台来说是一个关键的竞争力指标。 同时,还提到了“图片数据库(另附带近500张专辑图片)”,这暗示该平台不仅提供音乐播放,还包括了视觉元素,如专辑封面、艺人照片等。这不仅增强了用户体验,还可能是为了推广音乐或艺人而提供相关视觉资料。 ### 知识点二:下载 影音娱乐 源代码 源码 资料 #### 下载 “下载”是指从互联网或其他网络连接的计算机中获取文件的过程。在这个背景下,可能意味着用户可以通过某种方式从“年轻时代音乐吧二站修正版”平台下载音乐、图片等资源。提供下载服务需要具备相应的服务器存储空间和带宽资源,以及相应的版权许可。 #### 影音娱乐 “影音娱乐”是指以音频和视频为主要形式的娱乐内容。在这里,显然指的是音乐吧平台提供的音乐播放服务,结合上述的图片数据库,该平台可能还支持视频内容或直播功能,为用户提供丰富的视听享受。 #### 源代码 提到“源代码”和“源码”,很可能意味着“年轻时代音乐吧二站修正版”可能是开源的,或者是该平台允许用户下载其应用程序的源代码。在开源的情况下,开发者社区可以查看、修改和分发源代码,促进更多人参与到平台的建设和改进中。 #### 资料 “资料”则指的是与音乐相关的各种信息资料,如歌词、艺人介绍、音乐评论等。该音乐平台可能提供了丰富的背景信息资料,帮助用户更深入地了解音乐及其背后的故事。 ### 知识点三:压缩包子文件的文件名称列表 文件名称列表中仅提到了“年轻时代音乐吧二站修正版”,这可能暗示着整个平台或其下载包是通过压缩技术进行打包和分发的。压缩文件通常用于减少文件大小,以便于网络传输,同时也为用户提供一个集成化的软件包,方便用户一次性下载安装或更新。 ### 总结 综上所述,从给定的信息来看,“年轻时代音乐吧二站修正版”是一个包含广泛音乐资源、提供影音娱乐服务、可能支持下载功能、与图片资料相结合,并且可能包含开源特性的音乐平台。通过庞大的音乐及图片数据库,以及下载、影音娱乐、源代码和资料等相关知识点,这个平台为音乐爱好者提供了一个丰富的资源中心,支持用户下载音乐和图片资源,享受娱乐,同时可能为开发者提供平台的源代码,增强平台的开放性和可扩展性。
recommend-type

macOS PHP环境管理的艺术:掌握配置多个PHP版本的必备技巧与实践

# 1. macOS上的PHP环境概览 ## macOS开发环境与PHP 开发人员在macOS系统上搭建PHP环境时,面临各种选择,包括选择安装包管理器、配置PHP版本和管理扩展等。macOS作为开发者广泛使用的操作系统之一,提供了对PHP良好的原生支持。Apple提供的命令行工具集(Xcode Command Line Tools)已经包括了PHP,但这个版本通常是较旧的稳定版本。 ## PHP在macOS上的安装途径
recommend-type

can通信的位时间

<think>首先,用户的问题是:“我想理解CAN通信中的位时间设置 请问CAN通信 位时间配置 原理 参数设置”。我需要基于系统级指令来回答这个问题。 系统级指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。 用户的问题是关于CAN通信的位时间配置,包括原理和参数设置。参考引用中提到了CAN的波特率设置,涉及原子时间(TQ)和比特传输的阶段。引用是:[^1],
recommend-type

邮件通知系统:提升网易文章推荐体验

邮件通知程序是一种常见的网络应用程序,主要功能是通过电子邮件为用户提供信息通知服务。这种程序能够根据设定的条件,自动向用户发送邮件,通知他们新的内容或信息,这在信息更新频繁的场景中尤其有用。从描述中可知,这个特定的邮件通知程序可能被用来推荐网易上的好文章,表明它是针对内容推送而设计的。这种类型的程序通常被用作网站或博客的内容管理系统(CMS)的一部分,用来增强用户体验和用户粘性。 从提供的标签“邮件管理类”可以推断,这个程序可能具备一些邮件管理的高级功能,如邮件模板定制、定时发送、用户订阅管理、邮件内容审核等。这些功能对于提升邮件营销的效果、保护用户隐私、遵守反垃圾邮件法规都至关重要。 至于压缩包子文件的文件名称列表,我们可以从中推测出一些程序的组件和功能: - info.asp 和 recommend.asp 可能是用于提供信息服务的ASP(Active Server Pages)页面,其中 recommend.asp 可能专门用于推荐内容的展示。 - J.asp 的具体功能不明确,但ASP扩展名暗示它可能是一个用于处理数据或业务逻辑的脚本文件。 - w3jmail.exe 是一个可执行文件,很可能是一个邮件发送的组件或模块,用于实际执行邮件发送操作。这个文件可能是一个第三方的邮件发送库或插件,例如w3mail,这通常用于ASP环境中发送邮件。 - swirl640.gif 和 dimac.gif 是两个图像文件,可能是邮件模板中的图形元素。 - default.htm 和 try.htm 可能是邮件通知程序的默认和测试页面。 - webcrea.jpg 和 email.jpg 是两个图片文件,可能是邮件模板设计时使用的素材或示例。 邮件通知程序的核心知识点包括: 1. 邮件系统架构:邮件通知程序通常需要后端服务器和数据库来支持。服务器用于处理邮件发送逻辑,数据库用于存储用户信息、订阅信息以及邮件模板等内容。 2. SMTP 协议:邮件通知程序需要支持简单邮件传输协议(SMTP)以与邮件服务器通信,发送邮件到用户指定的邮箱。 3. ASP 编程:由于提及了ASP页面,这表明开发邮件通知程序可能用到 ASP 技术。ASP 允许在服务器端执行脚本以生成动态网页内容。 4. 邮件内容设计:设计吸引人的邮件内容对于提高用户互动和兴趣至关重要。邮件模板通常包括文本、图片、链接,以及可能的个性化元素。 5. 用户订阅管理:邮件通知程序需要提供用户订阅和退订的功能,以便用户可以控制他们接收到的信息类型和数量。 6. 邮件发送策略:为了遵守反垃圾邮件法律并提高邮件送达率,邮件通知程序需要实现合理的发送策略,例如定时发送、避免过度发送、邮件列表管理等。 7. 安全性和隐私保护:发送邮件时需要确保邮件内容的安全性和用户隐私,避免敏感信息泄露,并且遵守相关的数据保护法律和规范。 8. 性能优化:邮件通知程序需要有效地处理大量用户的邮件发送请求,保证邮件发送的高效性,并且优化系统性能以应对高峰时段。 9. 用户体验:良好的用户体验设计能够增加用户的互动和满意度,包括清晰的订阅界面、灵活的邮件设置选项、易于理解的通知内容等。 10. 反馈机制:用户对邮件的反馈,如打开率、点击率和退订率,是衡量邮件通知程序效果的重要指标。有效的反馈机制可以帮助改进邮件内容和发送策略。 通过了解这些知识点,我们可以对邮件通知程序的设计、开发和运作有更全面的认识。
recommend-type

【macOS PHP开发环境搭建新手必备】:使用brew一步到位安装nginx、mysql和多版本php的终极指南

# 1. macOS PHP开发环境概述 ## macOS下PHP开发的重要性 在macOS上设置PHP开发环境是开发高效Web应用程序的关键。macOS为开发者提供了一个稳定且用户友好的操作系统环境,结合了Unix的强大功能和苹果的直观界面设计。由于其Linux类似的核心,macOS常被视为Web开发的理想平台,特别是在搭