ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32串口IAP升级实战:Qt上位机与Bootloader协同设计

STM32串口IAP升级实战:Qt上位机与Bootloader协同设计 1. 升级链路拆解Bootloader分区、通信载体与上位机职责先把这个项目的定位说清楚。STM32的IAPIn-Application Programming固件升级本质上是让设备在运行过程中通过通信接口完成自身固件的更新而不是依赖J-Link、ST-Link这类外部烧录器。很多做嵌入式的小伙伴第一次接触IAP时容易被 bootloader、app、跳转地址、中断向量偏移这些概念绕晕再加上一个上位机要配合就更觉得无从下手。实际上把链路拆开看整个系统只有三个角色Bootloader程序负责接收固件数据并写入Flash**应用程序App**是日常运行的固件升级前需要先跳转到Bootloader上位机则负责把固件文件解析成合适的数据帧通过串口或者其他通信方式发给Bootloader。我这里选择Qt作为上位机开发框架主要原因有两个。第一Qt的跨平台特性让同一套上位机代码可以在Windows、Linux、macOS上编译运行工控现场经常出现一台工控机是Windows、另一台是Ubuntu的情况跨平台能力能省下不少维护成本。第二Qt对串口通信有开箱即用的支持QSerialPort模块封装了跨平台的串口操作不需要自己调Windows API或者Linux的termios开发效率明显更高。如果你对Qt还不熟悉需要注意从5.5版本开始QSerialPort就是官方标准模块了不需要额外安装插件只需要在.pro文件里加一行QT serialport。有人可能会问套接字、USB、CAN这些通信方式也可以做IAP为什么偏偏用串口答案是普适性。串口是STM32最底层的通信外设之一几乎每个项目都会引出串口调试方便、协议透明、抓包容易。USB和CAN虽然速度更快但Bootloader阶段的枚举、配置复杂度会显著上升对于最常见的量产维护、现场升级场景串口的稳定性和易用性平衡得最好。1.1 三类常见升级方式的对比升级方式优点缺点适用场景J-Link/SWD在线烧录简单直接支持调试需要拆机接线无法远程开发调试阶段串口IAP无需外部烧录器可通过已有串口升级速度受波特率限制量产、现场维护网络OTATCP/HTTP可以远程升级无需人到现场需要网络协议栈Bootloader复杂度高已联网的物联网设备也可以这样理解J-Link是动手术IAP是自己吃药。当产品已经交付到用户手里、外壳已经密封、没有预留下载口的时候IAP几乎是唯一选择。而且IAP还有一个隐藏好处Bootloader本身可以固化很多诊断功能不只是升级固件还可以做Flash读写测试、参数标定、产线自检这套基础架构建好以后后续很多产线工具都能在此基础上长出来。1.2 整体架构与存储分区规划以常用的STM32F103系列为例Flash通常是256KB或512KB。IAP升级必须把Flash划分为两个区域Bootloader区和App区。Bootloader放在最低地址也就是0x08000000起始大小根据Bootloader功能复杂度来定建议留足16KB~32KB。App区从Bootloader结束的地址开始比如0x08004000。App工程的链接脚本.icf或.sct文件和编译选项都要同步修改同时还要处理中断向量表偏移——STM32的默认向量表在0x08000000App运行后必须通过SCB-VTOR把中断向量表指向App区的起始地址否则任何一个中断进来都会跑飞。这一步是整个IAP方案里最容易出错、也最需要在PC端和单片机端统一共识的地方。上位机并不关心App编译的具体地址它只负责把编译好的.bin文件按帧发送出去真正决定App能不能跑起来的是单片机端的分区规划和跳转逻辑。但上位机开发者必须理解这个概念否则你会不知道为什么Bootloader收到的数据要写到0x08004000而不是0x08000000也不明白为什么App代码启动后串口完全没反应。2. 上位机界面与文件处理从选择文件到拖拽放行Qt上位机的用户界面不需要很华丽但要做到信息清晰、操作路径短、容错能力强。我做的第一个版本界面是这样的上方是串口参数区中间是文件选择区下方是升级日志输出区最下面是进度条和开始升级按钮。这样一个窗口就能覆盖完整的升级操作流程。串口参数区包括端口号、波特率、数据位、停止位、校验位。端口号用QSerialPortInfo::availablePorts()动态枚举插拔串口设备后点击刷新按钮就能更新下拉列表。波特率固定支持115200这是STM32 Bootloader常用的值也是大多数USB转串口芯片稳定工作的区间。不要试图在这里把波特率做成随意输入的编辑框实测中很多人会把波特率填成一些奇怪的值导致通信失败用下拉框限制常见选项反而更友好。文件选择区涵盖了按钮选择、路径显示、文件大小显示、拖拽提示。很多开发者会忽略拖拽功能但实际到产线你会感激这个功能——工程师经常在多个窗口之间切换找文件按钮要打开文件对话框拖拽文件只需要从文件夹里把.bin拖进窗口。再配合自动开始升级的联动选项操作员甚至不需要碰键盘就能完成升级。2.1 启用拖放setAcceptDrops 与 event filterQt处理拖拽的核心是重写两个事件函数dragEnterEvent和dropEvent。我们先在MainWindow构造函数里调用setAcceptDrops(true)让窗口接收拖入事件。然后重写dragEnterEvent判断拖入的数据是否包含本地文件路径如果是就调用event-acceptProposedAction()放行。void MainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event-mimeData()-hasUrls()) { event-acceptProposedAction(); } }dropEvent里再取文件路径void MainWindow::dropEvent(QDropEvent *event) { const QListQUrl urls event-mimeData()-urls(); if (urls.isEmpty()) { return; } const QString filePath urls.first().toLocalFile(); if (filePath.isEmpty()) { return; } // 这里可以继续对 filePath 做后缀名校验、文件大小读取 loadFirmwareFile(filePath); }这里有一个非常典型的踩坑点QMimeData::urls()返回的是QUrl列表不是QString列表。很多人第一次写的时候会直接urls.first().toString()结果拿到的是file:///C:/firmware/xxx.bin这种带协议前缀的字符串后面去解析路径的时候各种不对。必须调用toLocalFile()才能转成本地文件路径没有这个转换打开文件永远失败。还有一个细节容易被忽略拖拽进来的路径可能是Linux路径也可能是Windows路径。Windows下拖拽文件夹进入Qt窗口时toLocalFile()拿到的路径里可能是正斜杠也可能是反斜杠甚至不同版本的Qt行为还不一样。稳妥的做法是后续操作前统一用QFileInfo(filePath).absoluteFilePath()做一次规范化。我最初忽略了这一点结果同一套程序在Windows上测试没问题拿到Ubuntu上一拖文件就崩排查半天才发现是路径格式的问题。2.2 拖拽后继的联动逻辑与体验优化文件拖进来之后不要只更新一个路径文本框就完事。合理的交互应该是加载文件后立刻解析文件大小和CRC校验值显示在界面上同时判断文件后缀是否合法.bin或.hex。如果拖进来的是.hex文件可以直接转换成.bin或者提示用户手动转换因为IAP传输最常用的还是.bin格式——它没有地址偏移和校验行协议处理最简单。我习惯在拖入文件后自动把波特率设为默认值、自动检测串口是否打开、自动刷新端口列表三个动作一步做完。这样用户从看到文件拖进去到点击开始升级最短路径只需要两次点击。交互设计的核心原则是让操作员不用思考、不用记忆界面上一目了然。另外要处理一个边界情况拖拽文件后设备可能还处于App运行状态没有进入Bootloader。如果用户没有先在设备端触发跳转比如按住按键上电就开始升级Bootloader不会应答上位机发送握手指令会超时。所以界面上需要明确提示用户请确认设备已进入Bootloader模式日志区也要把这个前置条件挂在最显眼的位置。3. 串口通信协议设计与分包传输能跑通和能可靠跑通是两回事上位机的核心不只是把文件读出来发送而是要把文件组织成一套通信协议。STM32端Bootloader需要知道一帧数据从哪里开始、数据长度是多少、如何校验、发生错误后怎么处理。协议设计得好不好直接决定了升级过程是顺利完成还是时好时坏尤其是串口这种极易受干扰的通信方式分包大小、帧间延时、重传机制都必须预先定义。我用的是自定义的帧协议格式如下帧头命令字数据长度数据区CRC16校验0xAA 0x551字节2字节N字节2字节帧头固定0xAA 0x55用于接收端的帧同步。命令字区分不同的帧类型握手、擦除、发送数据、校验、跳转、查询版本等等。数据长度2字节最大支持65535字节的数据区但实际使用中会限制到每帧1024字节以内。CRC16校验放在帧尾对从帧头到数据区的所有字节做校验。有人会问为什么不直接用现成的Ymodem协议Ymodem确实是一种被广泛使用的串口文件传输协议ST官方的CubeProgrammer也支持。但Ymodem有一些自己的短板协议状态机相对固定想加入版本校验、写Flash前擦除命令、针对具体MCU做定制化应答不那么方便。自定义协议虽然多写一些代码但灵活性完全在自己手里出问题的时候也容易定位。3.1 分包大小与波特率的关系分包大小怎么定这一步是有讲究的。STM32内置Flash的编程单位通常是1字节或2字节取决于芯片型号但擦除单位是扇区或页F103的页大小是1KB或2KB。所以Bootloader收到数据后怎么缓存、怎么对齐到页边界擦除都影响接收策略。从串口传输角度我推荐每帧数据区设为512字节或1024字节。理由有两点。第一512字节的数据区加上帧头帧尾约520字节在115200波特率下传输时间约45ms这个时间很短即使丢帧重传代价也可控。第二STM32内部Flash编程等待时间有限Bootloader在写Flash的时候会短暂停止接收串口数据如果单帧数据过大UART接收FIFO通常是1字节硬缓冲若干字节DMA缓冲很容易溢出丢数据512~1024字节是经验上比较平衡的值。计算一下115200波特率下8位数据位1位停止位每秒传输11520字节。512字节的一帧大约需要44.4ms发送完成。加上帧间等待5ms传完一个512KB固件大概需要512KB / 512B × 49.4ms ≈ 51.6秒。这个速度可能比ST-Link慢不少但胜在不需要拆机。如果你确实需要更快可以提高到460800或者921600波特率但要注意USB转串口芯片如CH340、CP2102的稳定性和线材屏蔽质量现场环境不比实验室过高的波特率在长距离传输时误码率会飙升。3.2 握手、擦除、发送、校验、跳转的完整状态机完整升级流程必须是一个可靠的状态机。我把上位机的发送逻辑划分为五个阶段握手阶段上位机发送握手请求帧命令字0x01Bootloader收到后回复设备信息包括Bootloader版本、App区起始地址、Flash大小等。上位机根据这些信息判断文件是否过大、地址是否匹配。擦除阶段上位机发送擦除命令命令字0x02Bootloader擦除App区占用的扇区。这一步通常耗时较长比如擦除256KB的Flash可能需要几秒钟期间Bootloader无法响应其他指令。上位机必须设置足够长的超时时间我设为10秒。数据传输阶段按固定分包大小发送固件数据。每发送一帧上位机等待Bootloader回复ACK收到ACK才发下一帧收到NACK或超时则重发当前帧连续重发3次失败则中止升级。校验阶段全部数据发送完成后上位机发送校验命令命令字0x03。Bootloader对Flash区重新计算CRC32并反馈结果。同时上位机在发送前也计算了固件文件的CRC32两边比对一致才能继续。跳转运行上位机发送跳转命令命令字0x04Bootloader跳转到App地址运行。如果App启动后主动向上位机发送一条启动成功的消息整个升级就形成了闭环确认。这套带握手确认逐帧ACKCRC双重校验的流程实测下来升级失败率能压在千分之一以下。如果省略握手和最终校验只做逐帧ACK万一最后一帧数据损坏且恰好CRC碰巧对上就可能出现升级显示成功设备运行异常的隐形故障排查起来非常痛苦。4. STM32 Bootloader侧的关键配合地址跳转与应答逻辑很多做Qt上位机的朋友没有单片机背景但既然要做IAP上位机至少需要理解Bootloader侧的配合逻辑否则你无法解释为什么某一步超时、为什么跳转后设备没反应。这一章我不展开完整的Bootloader代码只讲几个关键点。Bootloader程序跑在0x08000000它做的事情很简单初始化系统时钟、初始化串口、等待上位机的握手命令。收到握手命令后返回Bootloader版本号。之后根据上位机的指令执行擦除、接收数据、写Flash、校验、跳转。跳转是Bootloader里最微妙的一步。跳转前必须做三件事关闭全局中断__disable_irq()防止跳转过程中有中断进来。关闭并复位相关外设串口、DMA、定时器等都要关否则App启动时会发现外设状态异常。设置主堆栈指针MSP从App区的起始地址读取第一字作为MSP值第二个字作为复位向量地址然后跳转到复位向量。这条链路搞对了App才能正常启动。typedef void (*pFunction)(void); pFunction JumpToApp; void jump_to_app(uint32_t app_addr) { uint32_t app_msp *(volatile uint32_t *)app_addr; uint32_t app_reset *(volatile uint32_t *)(app_addr 4); __disable_irq(); // 关闭用到的外设USART、DMA、TIM等 SCB-VTOR app_addr; // 中断向量表偏移 __set_MSP(app_msp); // 设置主堆栈指针 JumpToApp (pFunction)app_reset; JumpToApp(); // 跳转 }这段代码里的SCB-VTOR app_addr特别关键。如果App工程编译时虽然设置了0x08004000的链接地址但没有设置向量表偏移Bootloader跳过去后一旦产生任何中断SysTick、串口中断、外部中断CPU会从0x08000000读中断向量也就是读到Bootloader的向量表App端的中断处理就全部错乱了。4.1 Bootloader如何应答ACK/NACK的超时设计上位机发送的每一帧Bootloader都必须以固定格式的应答包回复。上位机根据应答结果决定继续发送、重发还是中止。应答包的定义要简单明了0x06ACK操作成功可以继续。0x15NACK操作失败需要重发。0x18BUSY设备忙请稍后重试多用于擦除期间。我遇到过的一个场景是Bootloader在擦除Flash时耗时较长上位机等不及就发了下一帧数据导致数据丢失、升级失败。解决办法是上位机在发送数据前先等Bootloader回复一个擦除完成的ACK同时把擦除命令的超时时间设长一些至少覆盖最坏情况下的Flash擦除时间。计算擦除时间有一个保守公式假设每页擦除耗时100ms256KB固件占用128页每页2KB擦除总耗时约12.8秒因此擦除超时设到15秒更稳妥。4.2 固件内容合法性检查不能什么文件都往Flash里写这是很多人忽视的环节。在实际项目中不是随便一个.bin文件都能作为固件升级的。Bootloader或上位机至少要检查文件大小是否超过App区容量。比如App区是0x08004000到0x0803FFFF共224KB如果固件文件超过这个值写Flash就会溢出到Bootloader区甚至保护区轻则升级失败重则把Bootloader也冲掉设备变砖。在上位机侧做这个检查更容易也更及时。读取文件后立刻比较文件大小和Bootloader上报的Flash容量超了就直接拒绝日志区输出错误原因。这比等Bootloader写到一半再报错要好得多。我见过有工程师在升级过程中发现文件太大这时Flash已经被擦除了一半设备已经处于半升级状态只能重新发一份正常大小的固件才能救回来耽误不少时间。另外握手阶段Bootloader返回的App区地址和大小字段上位机应该显示在界面上。这样现场操作人员一看就知道当前设备支持多大的固件不需要翻文档。这也是上位机实用性的体现——把底层信息变成人眼可读的提示。5. 实测排错记录从卡在握手到重启后还是旧固件最后一个部分我想重点写排错经验。这些问题的共同点是表面现象千奇百怪根源却往往是在一个很小的点上。把这些坑记下来能帮你省下大量在串口日志里翻找的时间。5.1 卡在握手阶段多半是串口参数或接线问题握手超时是最常见的故障。排查时先看几个基础项端口号是否正确、波特率是否匹配、USB转串口模块有没有被系统识别。再进一步用示波器或逻辑分析仪看TX/RX引脚是否真的有数据波形。经常遇到的情况是上位机发数据了但STM32完全没反应原因可能是TTL电平不匹配USB转串口模块是3.3V还是5V有些芯片需要接VCC供电也可能是TX/RX交叉接反了。还有一个隐蔽问题串口打开时DTR/RTS引脚的默认电平会影响STM32的复位和BOOT0引脚。很多USB转串口模块的DTR/RTS引脚默认拉低/拉高这正好触发了STM32的复位电路或BOOT0选择逻辑。结果就是上位机点击打开串口的一瞬间STM32被复位了用户的Bootloader程序压根没跑起来。解决方法是在QSerialPort打开后显式设置setDataTerminalReady(false)和setRequestToSend(false)或者是在硬件上断开DTR/RTS与复位电路的连接。5.2 数据传着传着就超时问题大多出在分包和流控上传输过程中某一帧超时千奇百怪的原因都有但最常见的还是接收缓冲溢出。Bootloader在接收数据后要写Flash写Flash期间如果继续有数据从串口进来而波特率不高、UART接收缓冲已满数据就会丢。处理办法是调整Bootloader的接收逻辑每收到一帧完整数据立刻暂停接收关闭接收中断或DMA写完Flash后再恢复接收并在应答ACK中告知上位机我准备好了继续发。上位机侧则应该实现简单的滑动窗口或逐帧等待ACK不要一口气把整个文件怼出去。另外一个原因是流控配置错误。如果用USB转串口模块连接最好关闭硬件流控RTS/CTS因为很多便宜的模块对硬件流控的支持并不可靠。QSerialPort里默认不启用流控如果之前用别的串口工具时开了流控转到Qt上位机时忘了关就会出现设备能收到数据但回不了应答的怪现象。5.3 升级完成后重启设备还是跑旧固件这个现象通常意味着App区的代码根本没有被正确跳转或启动。排查步骤是先确认Bootloader发送的跳转地址是否正确再确认App工程是否设置了正确的SCB-VTOR偏移。如果你用Keil开发App工程需要在启动文件或者SystemInit函数里加上一句话#define APP_ADDR 0x08004000 SCB-VTOR APP_ADDR;Keil的IROM1起始地址也必须从0x08004000开始大小设为224KB0x38000。如果App工程仍然是默认的0x08000000那么即使Bootloader跳转过去App的向量表还是在Bootloader区跑起来就会异常甚至直接HardFault。还有一个容易忽略的点App启动后要重新初始化所有用到的外设。因为Bootloader在跳转前关闭了外设App启动时不能假设外设状态是干净的必须重新初始化时钟、串口、GPIO、中断等。如果App代码偷懒用了SystemInit默认配置却没重新拉高某些引脚就会出现看起来在跑实际上串口发不出数据的假死现象。5.4 进度条显示与日志输出的用户体验细节最后说一下上位机的进度体验。升级过程中进度条应该是平滑前进的而不是一卡一卡地跳。我采用的方法是每发送一帧数据就根据已发送字节数/总字节数计算一个百分比并更新进度条而不是等整包文件发送完再一次性跳到100%。这样操作人员能看到实时进展心里有底。日志区要记录每一步的关键事件格式类似[12:01:23] [INFO] 设备握手成功Bootloader版本 v1.2 [12:01:25] [INFO] 开始擦除Firmware区... [12:01:38] [INFO] 擦除完成开始传输固件 [12:01:45] [INFO] 已发送 32/512 KB (6%) [12:02:30] [INFO] 固件传输完成等待校验... [12:02:31] [INFO] CRC校验通过正在跳转运行...每一行日志都加上时间戳和级别标签问题定位会方便很多。我最初把日志当成普通文本框处理大量输出时界面会卡顿后来改用QPlainTextEdit配合定时刷新并在日志超过1000行时自动清理旧记录流畅度明显提升。Qt上位机加STM32 IAP这套组合做一次完整的打通并不难难的是在各种边界情况下的稳定性。我自己做完这套方案后最大的体会是上位机不是随便写写就行的辅助工具它承担了协议解析、错误处理、人机交互、状态管理这些大头质量直接决定现场人员愿不愿意用、升级稳不稳定。如果要从这里面挑一个最重要的事我会说先把通信协议定严谨再把每个阶段的超时和重传参数调好这两件事做好了后续的UI再简单都无所谓。
返回列表