多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Qt的STM32 IAP上位机开发实战:从Bootloader设计到固件传输

基于Qt的STM32 IAP上位机开发实战:从Bootloader设计到固件传输 1. 项目整体设计与方案选型1.1 为什么使用Qt开发IAP升级上位机在嵌入式开发中固件升级一直是个绕不开的环节。STM32的IAPIn-Application Programming在应用编程升级方式说白了就是通过Bootloader程序接收外部传来的固件数据写入内部Flash然后跳转到新程序运行。这套机制在量产设备的远程维护、现场功能更新、Bug修复场景里几乎是标配能力。而上位机负责的就是“把固件文件可靠地送进设备里”这件事。我在选型时对比过几套方案MFC、C# WinForms、Python脚本、Qt。MFC太老旧界面开发效率低字体缩放、DPI适配都是坑C#做串口工具倒是顺手但跨平台能力基本为零以后想移植到Linux工控机上就得重写Python做原型验证效率高但发布时需要打包解释器启动速度和界面响应都不如原生方案而且客户机器上不一定有Python环境。最终选了Qt核心原因有三点。第一Qt的C框架性能足够串口收发、文件解析、进度刷新这类I/O密集操作完全hold得住不用像C#那样担心GC停顿。第二跨平台特性真的很实用同一套代码在Windows上编译一版给现场工程师用在Linux工控机上再编译一版给产线用逻辑完全不用改。第三Qt的信号槽机制天然适合串口异步通信串口数据到了就发个信号界面收到信号就刷新进度条这种事件驱动的模型写起来非常顺手不用自己维护回调函数的地狱。QSerialPort模块是Qt官方提供的串口库跨平台支持Windows、Linux、macOS封装了底层串口API用法清晰是开发上位机串口通信的核心组件。1.2 IAP升级方案的整体架构与通信协议设计IAP升级方案的架构分为两端STM32端是Bootloader加载程序负责接收数据并写入Flash上位机端是Qt应用负责读取固件文件、分包发送、确认应答。通信方式选择上我用了串口而非网络。原因很现实串口在任何嵌入式设备上都是标配资源不需要额外配网调试时用一个USB转TTL模块就能工作。网络升级虽然传输速度快但需要在设备端接入以太网或WiFi模块成本和复杂度都高出一个量级。对于中小型设备串口IAP是最稳妥、最实用的方案。通信协议设计上我走的是自定义协议。帧格式定义为帧头0xAA 0x55固定2字节用于同步命令字1字节区分握手、擦除、写入、校验、跳转长度2字节小端模式表示数据区长度数据区可变长度存放固件数据、地址信息等CRC162字节校验整帧数据防止传输错误协议规格表如下字段长度说明帧头2字节0xAA 0x55用于帧同步命令字1字节0x01握手、0x02擦除、0x03写入、0x04校验、0x05跳转长度2字节数据区长度小端数据区可变地址、数据、错误码等CRC162字节整帧CRC校验这里有一个关键的选型决策为什么不直接用Ymodem协议Ymodem是很成熟的老牌协议很多Bootloader例程都在用但它的帧格式和流程控制是固定的扩展性不足。比如我想在升级前先握手确认设备型号和Bootloader版本或者在擦除Flash前给设备发送一个升压命令Ymodem就不太好处理。自定义协议虽然要多写一些代码但对流程的控制力强得多可以根据项目需求随意扩展。这个取舍我认为在实战项目中更值得。2. 第一步STM32端Bootloader设计与实现2.1 内存分区规划与中断向量表重映射Bootloader设计的第一步是内存分区。我使用的是STM32F103系列Flash大小512KB分区方案如下Bootloader区0x08000000 ~ 0x0800FFFF64KBAPP区0x08010000 ~ 0x0807FFFF448KBBootloader区放的是设备上电后最先执行的程序负责判断是否有升级请求如果有就进入等待接收固件的模式如果没有就跳转到APP区运行。APP区是业务程序所在区域也就是我们日常开发时用Keil或STM32CubeIDE编译运行的代码。分区设计有个关键点APP区的起始地址必须是扇区对齐的。ST的F1系列每个Flash扇区大小不一样前4个扇区是16KB后面的扇区是64KB。为了保证擦除操作简单我把APP区起始地址定在了0x08010000正好是一个大扇区的起始位置。如果APP区的起始地址不对齐擦除时就会误擦掉Bootloader代码导致设备变砖。给APP工程配置时需要修改两个地方。一是IROM起始地址改为0x08010000大小改为0x70000448KB二是在系统初始化时重映射中断向量表把NVIC向量表偏移设置为APP区的偏移量。STM32的中断向量表默认在Flash起始地址0x08000000如果APP程序不重映射向量表中断响应就会等到跳到Bootloader去响应这肯定是不对的。F1系列是在SystemInit函数后面加上SCB-VTOR 0x08010000这行代码。Bootloader接收完固件后跳转流程是这样的关闭全局中断将MSP主堆栈指针设置为APP区的堆栈地址然后跳转到复位向量执行APP程序。如果这一步漏掉什么细节设备跳转后直接死机或卡在启动阶段。核心代码大概长这样typedef void (*pFunction)(void); void JumpToApp(void) { uint32_t app_addr 0x08010000; uint32_t app_sp *(volatile uint32_t *)app_addr; pFunction app_reset (pFunction)(*(volatile uint32_t *)(app_addr 4)); __disable_irq(); SCB-VTOR app_addr; __set_MSP(app_sp); app_reset(); }这段代码里有个容易忽略的坑app_reset取的是复位中断向量不是app_addr本身。ARM Cortex-M架构规定Flash起始4字节存放主堆栈指针紧接着4字节存放复位向量地址。所以跳转时必须先从app_addr处读取SP值再从app_addr4处读取复位地址顺序不能反。2.2 握手、擦除、写入的命令实现Bootloader的接收端是串口中断。每当收到一个完整帧就根据帧头、命令字、长度、数据、CRC校验依次解析。校验通过后分发到对应的处理函数。握手命令的实现相对简单。上位机发送0x01命令帧Bootloader收到后回复一个包含设备型号、Bootloader版本、Flash大小的应答帧。这个握手动作表面上看起来只是一个“打招呼”实际上是整个升级流程的第一道防护线。如果上位机没有收到正确应答就说明设备此时不在Bootloader模式盲目发数据只会白费等超时。我实际开发中见过不少同事跳过握手直接发固件结果设备没有进入升级模式白搞半天。擦除命令要小心处理。Flash擦除是不可逆的但一次只能擦除整页如果中途断电就是半擦状态。STM32F103的Flash编程手册规定擦除操作执行期间CPU不能从同一块Flash取指所以要确保擦除函数本身放在RAM中运行或者从系统存储区运行。实际操作中我一般一次擦除一个扇区在发送下一帧数据前完成擦除避免一次性擦除整个APP区导致长时间等待也避免中途故障导致Bootloader失去后续数据时闪存处于全空状态用户数据就彻底没了。实际测试中我直接擦除整个APP区速度约为1.5秒完成448KB。这个等待是可以接受的。写入命令的接收处理最核心。上位机发送的数据帧里包含了目标地址和固件数据Bootloader用一个接收缓冲区接收数据再逐字节写入Flash。写Flash时必须按照半字操作也就是2字节的宽度否则会触发Flash编程错误。如果需要写入奇数个字节最后一字节要特殊处理拼上一个0xFF再做半字写入。写入流程中最容易出问题的点是数据对齐和写入时序。我曾经遇到一种情况上位机每包发送256字节数据但最后一包可能只有150字节如果代码里没有对最后不足2字节的情况做处理就会导致写Flash时越界或者触发总线错误。后来我加了一个强制2字节对齐的封装问题才彻底解决。每个扇区全部写完数据后要执行一次Flash锁定操作锁上Flash防止意外写入。跳到APP前还应该做一次整体校验。这部分我在后面会详细说明。3. 第二步Qt上位机框架搭建与拖拽文件处理3.1 串口通信模块的封装与数据收发处理Qt上位机的核心模块是串口通信。我使用QSerialPort类来封装串口通信这是Qt官方提供跨平台串口通信类使用方式非常简单。首先在.pro文件中添加串口模块QT serialport然后创建串口管理器类核心信号槽逻辑如下class SerialManager : public QObject { Q_OBJECT public: explicit SerialManager(QObject *parent nullptr) { m_serial new QSerialPort(this); connect(m_serial, QSerialPort::readyRead, this, SerialManager::onReadyRead); } bool openPort(const QString portName, qint32 baudRate) { m_serial-setPortName(portName); m_serial-setBaudRate(baudRate); m_serial-setDataBits(QSerialPort::Data8); m_serial-setStopBits(QSerialPort::OneStop); m_serial-setParity(QSerialPort::NoParity); m_serial-setFlowControl(QSerialPort::NoFlowControl); return m_serial-open(QIODevice::ReadWrite); } private slots: void onReadyRead() { QByteArray data m_serial-readAll(); // 这里将数据追加到接收缓冲区并解析出完整帧 } };这里有个重要的经验点串口readyRead信号触发时不一定代表正好收到了一整帧数据。串口通信是字节流的没有帧的边界概念可能一个信号里只到了半帧数据也可能一个信号里到了好几帧数据。所以上位机上必须自己维护一个接收缓冲区收到数据先追加进去再尝试从缓冲区里解析完整帧。我写过一个tryParseFrame函数循环查找帧头和帧尾每找到一包完整帧就提取出来处理然后从缓冲区中移除已经处理过的部分。如果不用这种模式数据很容易乱导致解析失败。串口配置方面STM32升级时我使用的是115200-8-N-1也就是波特率115200、8位数据位、无校验位、1位停止位。波特率在这个场景下是一个指标权衡9600太慢传大固件效率太低115200是最稳妥的高速档位再往上的460800、921600虽然更快但对线材质量、USB转TTL模块的电平质量要求更高偶尔会出现数据错乱。实际测试中115200传256KB固件大约需要1分钟算是可以接受的。我还发现一个关于波特率选择的小规律如果现场使用是劣质杜邦线或者线长超过20cm建议波特率不要超过115200否则偶尔CRC校验不过会让你怀疑人生。如果位线非常短且质量好可以用256000跑一版速度能提升一倍。3.2 拖拽文件处理技巧从拖入到拿到文件路径拖拽文件处理是Qt上位机里一个很小但非常实用的技巧。做过上位机的人都有这种体会每次要刷固件都得点“浏览”按钮在弹出的文件对话框里一层层翻目录找文件重复操作多了真的很烦。我见过一些做量产测试工具的同事把文件路径写死在配置文件里要换固件版本就得改配置文件再重启软件效率低不说还容易改错。拖拽操作就完全解决了这个问题把bin文件从资源管理器里直接拖到软件窗口上路径自动进输入框一气呵成。Qt实现拖拽其实很简单核心是三步。第一步在主窗口构造函数里开启接收拖拽事件setAcceptDrops(true);第二步重写dragEnterEvent。这个事件在拖拽过程中会不断触发用来判断拖进来的东西是否被接受。我在这里判断了MIME类型只接受文件类型的数据是文件就接受事件否则忽略void MainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event-mimeData()-hasUrls()) { event-acceptProposedAction(); } else { event-ignore(); } }第三步重写dropEvent。这个事件在松开鼠标的那一刻触发这时候才能安全地拿到文件路径void MainWindow::dropEvent(QDropEvent *event) { const QMimeData *mimeData event-mimeData(); if (!mimeData-hasUrls()) return; QListQUrl urlList mimeData-urls(); if (urlList.isEmpty()) return; QString filePath urlList.first().toLocalFile(); QFileInfo fileInfo(filePath); if (!fileInfo.exists()) return; if (fileInfo.suffix().toLower() ! bin) { QMessageBox::warning(this, 提示, 请选择bin格式的固件文件); return; } ui-filePathEdit-setText(filePath); }这里有几个容易忽略的细节。第一一次可能拖入多个文件程序里要判断只取第一个否则用户不小心拖了好几个文件进来逻辑就乱了。第二文件后缀要过滤我只接受.bin格式。第三Qt5中QMimeData返回的是QUrl需要通过toLocalFile()转换成本机文件路径直接拿url()转字符串会得出类似file:///C:/xxx.bin的格式用不顺手。还有一个界面交互上的优化拖拽文件进入窗口的时候最好给用户一个视觉反馈表示“这里是可释放的”。我是通读dragEnterEvent里给控件设置一个边框高亮的效果比如用一个widget包住文件路径的显示区域拖入时设置一个蓝色高亮背景松开后恢复。这个细节虽然很小但非常提升使用体验现场使用的人一眼就知道固件文件拖对了。3.3 界面布局与文件加载界面布局我是这样安排的顶部是串口配置区下拉框选择串口号、波特率按钮是“打开串口”中间是文件选择区左侧是文件路径显示右侧是“浏览”和“开始升级”按钮底部是一个大进度条和日志输出区。这个布局参考了市面上一部分串口工具软件的做法用户上手成本低也适合工控触摸屏上操作。文件加载逻辑上我使用QFile读入固件文件读完先做文件长度校验。升级前还需要检查固件大小是否超过APP分区容量超过就警告并中止。这个校验不能省因为STM32的上位机端如果不知道Flash剩余空间写入时会直接触发Flash写错误导致升级中断设备进入异常状态。另一个文件相关的经验如果用户选择的是.hex文件直接读取会包含地址信息和校验字节不能当作纯二进制数据写入Flash。我在文件加载这里做了后缀判断只有.bin文件才直接原样加载.hex会先调用解析函数转换为纯二进制数据再加载。后面第4章我会详细讲解这个转换逻辑。4. 第三步固件文件解析与分包策略4.1 bin与hex固件文件的差异及处理在IAP升级场景里上位机收到的固件文件最常见有两种bin格式和hex格式。bin格式是纯二进制数据不带任何地址信息就是“程序在Flash里的原始内容”。Flash地址是多少bin中的第一个字节就放在那个地址上。上位机读取bin文件之后只需要把文件内容原封不动地发送给Bootloader写入到约定的APP起始地址即可流程最简单。hex文件是Intel HEX格式是文本文件每一行以:开头由“长度、地址、记录类型、数据、校验和”组成。常见的记录类型包括数据记录00、EOF标记01、扩展线性地址记录04等。hex文件里每一行都带有地址信息所以才能支持在Flash的不同位置分散加载数据。如果需要把hex转换成bin就必须解析每一行记录根据地址信息把数据填入一个大的缓冲区。实际开发中有一个值得注意的点如果Bootloader设计的APP起始地址是0x08010000而hex文件里记录的地址也是0x08010000那么解析出来填充到缓冲区后数据刚好与Flash地址对齐。但有些开发工具生成的hex文件地址起点是0x08000000与Bootloader约定的APP区起点不一致。这种情况就需要上位机在解析hex时检测并自动调整偏移否则写入位置错位程序跑不起来。我的实现方案是这样将hex解析后的数据放到QByteArray中初始化长度为256KB全部填充为0xFF。遍历hex每一行记录如果记录类型是数据记录就计算地址与起始地址的差值把数据写入QByteArray的对应位置。处理完所有数据记录后再根据真实的固件总长度截取有效数据作为bin文件内容。这个方案可以兼容地址不连续或分散链接的固件。4.2 分包发送的算法设计与流量控制Bootloader的接收缓冲区是有限的不可能一帧把整个固件都发过去一般一包数据限制在256字节或512字节。我设计的分包发送算法是这样的单包大小256字节帧格式帧头命令字长度2字节地址4字节数据256字节CRC16发送顺序第一包从APP起始地址开始地址递增每一包都带上本包地址分包数量 固件总长度除以单包大小有余数则向上取整。例如固件大小是51200字节单包256字节分包数就是 (51200 255) / 256 200 包。发送时的流量控制走的是简单应答机制。上位机发送一包数据后等待Bootloader的确认帧ACK或错误帧收到ACK后再发下一包。这里引入一个超时机制发送后500ms内没收到任何响应就重发当前包重发三次仍没响应就中止升级。如果不用确认机制、直接连续发送所有数据包等于闭着眼睛开车。串口通信过程中线缆可能接触不良导致串口缓冲区数据丢失接收端解析出错整包都丢了。有了ACK机制后发送端能实时感知接收端的状态。关于这个ACK等待时间我实际调试中发现50ms就足够STM32完成一包数据的Flash编程因为STM32F103在72MHz主频下写256字节Flash的时间很短瓶颈并不在这里。但我还是设置了500ms的超时多留出一些余量防止Bootloader端同时处理别的事情导致的延迟。实际优化中上位机可以做一个简单的流水线重叠处理发送数据包的同时后台读取串口等待对应ACK。这个方案将整体升级时间缩短了一些。发送端用一个独立的QTimer作为超时控制每次发送数据包后重启定时器收到ACK后停止定时器。这里有个很常见的坑定时器必须与发送数据一一对应不能用“收到ACK就停”这种粗粒度逻辑否则重发后ACK对应的是旧包还是新包就分不清了编出的接收状态会混乱。5. 第四步升级流程状态机设计与异常处理5.1 上位机状态机设计升级流程不是简单的“点一下按钮数据发完就完事”的逻辑。Bootloader和上位机之间要经过握手、擦除、发送、校验、跳转这5个阶段每个阶段都有语义上的状态迁移所以我使用状态机来管理整个升级流程。状态定义StateIdle空闲状态等待用户打开文件、点击升级StateHandshake已发送握手命令等待设备应答StateErase已发送擦除命令等待擦除完成确认StateSending正在分包发送固件数据StateVerify已发送校验命令等待校验结果StateJump已发送跳转命令等待设备重启状态机用switch语句实现每个状态对应一个处理函数。关键逻辑集中在串口数据的处理里收到数据帧后根据当前状态判断这个响应属于哪个命令的应答然后进行相应的处理。比如在StateHandshake状态收到应答帧后检查应答帧中的设备型号是否与预期一致一致就切换到StateErase不一致就提示“设备型号不匹配”。在StateSending状态收到ACK帧就发送下一包数据收到错误帧就重发当前包重试超限就中止升级并提示失败原因。我在UI界面上设置了一个状态指示灯用不同颜色表示当前状态灰色空闲、蓝色握手、橙色擦除、绿色发送、紫色校验、深绿跳转。这个设计最初是因为调试时发现状态不明导致问题定位困难后来引入颜色指示后用户一眼就能看出卡在哪个环节这对现场排查问题帮助很大。5.2 失败重传机制与边界条件处理每个状态收不到响应或收到错误响应时都必须有对应的处理策略。我整理了一个超时时间表阶段单次超时最大重试中止条件握手1000ms3次3次超时则中止并提示擦除5000ms2次超时中止并提示数据发送500ms3次超时中止并提示校验2000ms3次校验失败则中止跳转1000ms1次超时提示但可忽略这里有个很重要的边界情况最后一包数据长度不足256字节。型号是256字节单包分包的如果固件总长度正好是2的倍数就不用特殊处理如果不是最后一包数据长度会小于256字节。这个长度放在帧头长度字段中在上位机计算时用totalSize - sentSize来取余。处理起来逻辑不复杂但少写了这个判断最后一包就会发送缓冲区里残留的旧数据直接导致写入失败。还有一个常见但容易被忽视的情况固件文件长度为零、文件长度小于一包数据、文件长度大于APP区剩余Flash空间。这三种情况必须在点击升级按钮前就拦截而不是在传输过程中报错。我写了一个validateFileAndFlash()函数专门做升级前检查逻辑包括bool MainWindow::validateFileAndFlash(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::critical(this, 错误, 无法打开固件文件); return false; } qint64 fileSize file.size(); if (fileSize 0) { QMessageBox::critical(this, 错误, 固件文件为空); return false; } if (fileSize APP_FLASH_SIZE) // APP_FLASH_SIZE 448 * 1024 { QMessageBox::critical(this, 错误, 固件文件超过APP区Flash容量); return false; } return true; }在等待握手响应、数据ACK时界面上的“开始升级”按钮应当置灰防止用户在升级过程中重复点击。我在状态机中加了一个busy标志状态机进入任何非StateIdle状态时置位退出时清零。这个小细节可以避免很多不必要的麻烦。6. 第五步完整调试流程与常见问题排查6.1 上位机与Bootloader联调的关键步骤联调前的准备工作一定要做足。首先需要用USB转TTL模块连接电脑和STM32开发板接线方式是TXD接RXD、RXD接TXD、GND接GND。这里反复确认容易踩坑的地方交叉接线。很多人第一次用USB转TTL会把TXD接TXDRXD接RXD结果怎么发都收不到数据还以为是代码的问题。这个低级错误占了我见到联调问题的一半以上。其次通讯前先确认Bootloader已烧录到单片机的0x08000000地址APP程序已烧录到0x08010000地址。Bootloader和APP是两份独立的工程烧录时要分别指定对应地址。联调的时候我推荐的顺序是先用串口调试助手验证再用Qt上位机软件验证。串口助手的好处是能手动控制发送内容方便逐字节检查协议格式。我会先用串口助手发送握手命令0xAA 0x55 0x01 0x00 0x00 0xXX 0xXX查看Bootloader是否回复了正确应答。如果握手通了再开始Qt上位机的完整升级流程。调试过程中最有效的工具是日志输出。我在Qt代码里大量加入了qDebug()输出包括发送的每一帧内容、接收的每一帧内容、状态切换的事件。这些输出在调试阶段价值巨大比如定位“为什么升级响应超时”这类问题翻日志看一眼就能知道上一帧到底是什么时候发的有没有收到ACL发出去的CRC对不对。刚把日志输出都删掉等发布版本时再注释掉。6.2 高频异常场景的排查方法与解决实录联调中遇到过几次让人印象深刻的“疑难杂症”我梳理成一个排错对照表按出现概率排序供参考问题1串口打开失败现象程序报错“无法打开串口COM3”返回QSerialPort::PermissionError。排查方向串口是否被其他软件占用比如串口调试助手没有释放COM口USB转TTL模块是否被系统识别可以去设备管理器里查看是否存在感叹号设备权限问题在Linux下尤其明显当前用户可能没有/dev/ttyUSB0的读写权限。问题2连接不上没有应答现象发握手命令STM32没回复。排查方向这个是我见过最多的问题九成是接线错误。第一查明是否交叉接线第二查明两边的GND是否连在一起串口通信电位要共地否则电平就无法判断第三查明波特率是否一致第四量一下TXD脚有没有波形输出如果MCU没正常复位TXD脚上会一直是空闲电平。问题3发了几包数据之后卡住ACK一直等不到现象前几包数据正常然后上位机超时重发几次后中止。排查方向重点检查Bootloader接收缓冲区大小。如果接收缓冲区只有512字节而上位机一次性发了好几个数据包缓冲区就溢出了后面的数据包直接被丢弃。解决方式是上位机严格遵守“发一包等ACK”的模式不要尝试连续发送。另外检查Bootloader中是否开启了USART_RXNEIE中断而且中断里处理的耗时不能过长。我在Bootloader的串口回调里只做接收和缓冲把Flash写入放在主循环中处理避免中断长时间占用导致丢字节。问题4固件写完校验不通过现象升级完成校验时报错。排查方向先检查最后一包的长度是否处理正确。我遇到过一种情况固件长度正好是256的倍数但是协议里长度字段算错了导致最后一包多发了256字节到Flash外面。然后检查地址偏移是否算错特别是hex文件里起始地址不统一的情况。最后检查STM32 Flash在写入时是否做了2字节对齐如果最后一包只有一个字节数据没做对齐填充会产生编程错误。问题5跳转后程序卡死或进入HardFault现象Bootloader显示升级成功但APP没有启动。排查方向首先看跳转前是否关闭了全局中断跳转后SCB-VTOR是否设置正确。其次看APP工程的Linker配置里IROM起始地址是否改成0x08010000。最后检查APP代码在跳转前有没有把外设重置。I2C、SPI这类外设在复位后状态不确定跳转前最好做一次HAL_RCC_DeInit()把时钟和中断清理干净。问题6界面进度条卡住但实际还在发数据现象界面进度条长时间不动任务管理器里看CPU占用却很高。排查方向大概率是在接收处理逻辑里死循环了。我在状态机里曾写过一个不带退出条件的循环导致状态卡住。后来把所有循环都加了超时判断循环次数上限这个问题的定位就简单多了。6.3 性能优化与实测数据升级大固件时速度就成了影响体验的因素。例如一个256KB的固件115200波特率下实际测试用时约30秒左右。这个时间在开发阶段没啥感觉量产阶段逐个刷写多个设备就会觉得有点慢了。优化方向有三个第一个是调整分包大小。在Bootloader接收缓冲区足够的前提下把单包数据从256字节提升到512字节甚至1024字节能减少包数量降低总发送时间。但有一个前提Bootloader的接收缓冲区必须能容纳一整包数据而且Flash编程要放在分包发送的间隙完成。我实测过Bootloader使用2048字节缓冲区、单包1024字节的方案速率大约提升30%左右。第二个是提高波特率。把115200提升到460800理论速率能提升4倍但前提是线材质量够好、USB转TTL模块支持高速率。我测试过CH340在460800下比较稳定而一些杂牌CP2102模块就经常丢数据。这个优化不能盲目追求高速要经过长时间压力测试验证后再投入使用。第三个是流水线重叠。发送数据包后不等ACK到来就准备下一包数据用双缓冲的方式接收线程预读ACK。实际效果提升大约8%~10%但代码复杂度会明显上升。对量产工具来说这个优化值得做对普通开发阶段的升级工具单包增大和波特率提升已经完全够用。实测数据参考表固件大小波特率单包大小总耗时256KB115200256B约30秒256KB115200512B约24秒256KB4608001024B约8秒512KB4608001024B约15秒我自己量产工具的最终配置是460800波特率、1024字节单包、256KB固件8秒完成稳定跑了几个月没有出过问题。6.4 我踩过的几个比较深的坑聊了这么多最后分享几个我在开发中真正陷进去过、花时间才爬出来的细节。第一个是拖拽文件时无意中把整个文件夹拖进去了。dropEvent里如果拿到的URL是一个目录后面的QFile读取会直接失败。我的解决方案是加了fileInfo.isFile()的判断是目录就提示“请选择bin文件而不是文件夹”。这个场景看起来概率不大但客户那边真的会有人拖文件夹防一下很有必要。第二个是Windows和Linux下路径分隔符的问题。Qt的QFile使用正斜杠/作为路径分隔符来处理Windows和Linux两种系统路径这一点虽小但如果在字符串拼接的时候我用了硬编码的反斜杠或正斜杠跨平台时就会出问题。Qt已经内置了QDir::separator()但更方便的是全部使用/Qt内部会自动转换。第三个是升级完跳转后芯片卡死的排查。我花了大半天时间排查最后发现原因在于我在跳转前没有做一次系统时钟复位。APP启动时使用的外部8MHz晶振而Bootloader初始化时把系统时钟配置成了72MHz。跳转后APP没有重新配置时钟就继续跑输出乱码、外设卡死。解决办法是在跳转前调用HAL_RCC_DeInit()做时钟复位强制APP从头开始初始化时钟系统。这个经验让我以后再遇到类似问题先检查时钟配置。
返回列表