简介:面向STM32H743的IAP开发需求,这份资源提供了完整的bootloader工程、可升级的测试主程序以及支持Ymodem协议的上位机软件及源码。开发者可据此搭建串口固件升级方案,理解bootloader跳转与协议解析等关键实现,适合具备一定STM32基础、正在做产品远程升级功能的嵌入式工程师。资源共包含约2000个文件,除大量C/H源码与Keil工程配置外,还带有编译生成的o、crf、axf、hex、bin等中间与最终固件,以及上位机所需的exe、dll等文件,压缩包整体约214MB。文件中还整合了bat脚本、ini配置等辅助内容,便于直接复用工程体系与界面程序。目前已有591人学习该资源。通过对照bootloader与主程序源码,可快速掌握Ymodem分包传输、串口驱动、Flash写入及跳转逻辑,配合上位机可视化操作,能有效缩短IAP功能开发周期,尤其适合需要自行定制升级协议或移植到其他H7系列芯片的设计场景。
1. 拿到H743先别急着写IAP:它和F1/F4的差异从根上就不一样
做嵌入式固件升级,STM32F1、F4的IAP方案网上确实一抓一大把,但直接搬到H743上,十有八九会翻车。这不是说H743的IAP有多玄学,而是它底层架构变了:从单Bank Flash变成了双Bank,从普通Flash变成了带Cache的Flash,从单核简单中断变成了更复杂的NVIC和时钟树。这几个变化叠加在一起,Bootloader的写法必须跟着调整。
我最早在H743上套用过F407的IAP代码,结果非常典型:串口能收包、Flash能写入、跳转也能跳,但APP一启动就卡死,用调试器看PC指针已经跳到APP的Reset_Handler了,可就是跑不起来。查了很久,最后发现是跳转前没处理Cache和中断向量表偏移的问题,H7的M7内核跟F4的M4内核在这方面完全是两种玩法。
所以这篇博文我会从一个可落地的角度来讲:Bootloader怎么规划Flash分区、Ymodem协议在串口链路上怎么工程化实现、上位机怎么配合、以及那些"跳转卡死hal_delay"之类的经典坑到底是怎么踩进去又怎么爬出来的。文中给出的方案,我实际在H743板子上跑通过,配合自己写的C#上位机全流程升级过几十次固件。
先看H743的Flash结构。它内部Flash总共2MB,分成了8个Bank区,每个区128KB,每个区又拆成16个8KB大小的SubSector。这个"8KB最小擦除单位"跟F1的1KB、F4的4KB都不一样。写Bootloader时你直接操作的是SubSector,擦除和编程粒度都要按这个来算。
再说Cache。M7内核带了I-Cache和D-Cache,Flash里的代码要经过I-Cache读取,数据要经过D-Cache。跳转进APP后,如果APP里跑的是裸机程序、没有初始化MPU和Cache,而Bootloader里又开着Cache且没做Clean和Invalidate,那APP读到的可能是Cache里的陈旧数据,表现就是各种莫名其妙的跑飞或者外设寄存器值不对。这一点F4上几乎不用考虑,H7上必须处理。
2. Bootloader端:分区规划、跳转细节与hal_delay卡死根因
2.1 Flash分区怎么划才够用
H743的IAP方案,最简单的做法是分成Bootloader区和APP区两块。Bootloader区放引导程序,APP区放用户固件。如果还想做得稳一点,可以再加一个临时存储区,用于接收固件完成后做校验、或者做备份回滚。
我实际用的分区如下:
| 区域 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 128KB | 存放Bootloader程序 |
| APP | 0x08020000 | 1MB | 存放用户固件 |
| 备份区 | 0x08120000 | 剩余空间 | 可选,用于存放备份固件做回滚 |
Bootloader放128KB是相当宽松的,实际上Ymodem协议加串口驱动加Flash驱动全写完,也就占20KB出头。留这么大是因为H743的擦除粒度是8KB,如果Bootloader本身占了40KB,那也得按8KB的倍数向上取整,给后面的APP区留出对齐的起始地址。
APP起始地址选0x08020000,对应的是Bank1偏移128KB的位置。选这个地址还得注意一个点:H743的中断向量表VTOR寄存器是个至少0x200对齐的地址,0x08020000是满足对齐的,没问题。
2.2 跳转代码为什么必须这样写
跳转是IAP里最容易出问题的一环。网上很多代码精简成了三步:关中断、改VTOR、跳转。但在H743上,光做这三步是不够的。
我实际使用的跳转代码结构如下:
void JumpToApp(uint32_t app_addr) { uint32_t app_stack_addr = *(volatile uint32_t *)app_addr; uint32_t app_reset_addr = *(volatile uint32_t *)(app_addr + 4); if ((app_stack_addr & 0xFFF00000) != 0x20000000) { // 栈顶指针不合法,说明APP区没有有效固件 return; } // 关闭全局中断 __disable_irq(); // 恢复SysTick为默认状态,HAL_Delay卡死的关键就在这里 SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; // 关闭并复位所有外设时钟(可选但推荐) __HAL_RCC_DEINIT(); // 关闭I-Cache和D-Cache,跳转前必须做 SCB_DisableICache(); SCB_DisableDCache(); // 设置中断向量表偏移 SCB->VTOR = app_addr; // 跳转到APP的Reset_Handler void (*reset_handler)(void) = (void (*)(void))app_reset_addr; reset_handler(); while (1); }这里每一步都有明确目的,不是凑代码量:
- 检查APP区的栈顶指针是不是指向RAM区域,是为了防止Flash里全是0xFF或者乱数据时跳过去直接HardFault。这个检查至少能排除一半的"跳转卡死"问题。
- 关闭全局中断是必须的。Bootloader运行期间可能开了串口中断、定时器中断,如果不关就跳走,APP里没初始化完中断控制器、外设中断就来了,很容易跑飞。
- 复位SysTick是专门针对那个经典症状的。网上经常有人问"IAP跳转后卡死hal_delay",根因大多是:Bootloader用HAL库的时候SysTick已经跑起来了,跳转后APP里HAL_Init又会重新配置SysTick,但SysTick的CALIB、LOAD值处于一个并不干净的状态,再加上中断优先级分组被改过了,最终导致HAL_Delay永远等不到SysTick中断触发。最省心的做法就是跳转前把SysTick彻底踩回复位状态。
- 关闭Cache这步在H743上必须做。你只有两条路:要么跳转前Disable掉,让APP自己按需重新使能;要么Bootloader里干脆从头到尾就不开Cache。我选择跳转前关掉,最稳妥,也最不容易出玄学问题。
有个细节容易忽略:__HAL_RCC_DEINIT()会关闭全部外设时钟,如果你在Bootloader里已经用HAL库初始化过串口、LED、Flash等外设,跳转前把它们时钟关了可以帮助APP获得一个更干净的外设状态。但注意不能在关闭时钟后还去访问这些外设的寄存器,否则会HardFault。
2.3 跳转地址和链接脚本怎么配合
Bootloader可以放在0x08000000直接编译,APP工程则必须改两处:一是链接脚本里的FLASH起始地址改为0x08020000,长度改为1MB;二是在系统初始化代码里设置SCB->VTOR = 0x08020000。
很多人在APP工程里忘了改VTOR,结果APP的串口中断一进来就找不到正确的向量地址,表现就是"APP能启动、LED能闪,但一开中断就跑飞"。在H743上,你可以在main函数最前面加这样一段:
SCB->VTOR = 0x08020000;也有用__attribute__((section(".isr_vector")))配合HAL库的HAL_Init自动设置的,但裸写VTOR最直接。
3. Ymodem协议工程化:不能只跑通Demo,要能应对真实传输
3.1 协议帧结构先记牢
Ymodem传输有两种数据包:128字节包和1024字节包。起始帧是128字节的,包含文件名和文件大小;数据帧大部分是1024字节,最后不足1024字节时自动降级。
协议基本流程是:接收方先发C,发送方才开始发送起始帧;起始帧收到并确认后,发送方连续发送数据帧,每包带2字节序号(从1递增,溢出归零)+2字节序号取反+数据+2字节CRC16;全部数据发完后,发送方发EOT,接收方回ACK和C,发送方再发一个结束帧(内容为空),接收方回ACK,整个传输结束。
这里最容易被忽视的是:Ymodem的包序号不是按字节数算的,是按包个数算的,而且是4位16进制ASCII码。比如第1个数据包序号是"0001",包序号在255之后归零从"00"重新计数。如果你拿协议说明文档时没注意这个ASCII序号问题,解析会对不上,表现就是收不到几帧就报序号错误。
我把关键帧格式整理成了一张表,方便写代码时对照:
| 帧类型 | SOH/STX | 序号 | 序号取反 | 数据长度 | 校验 |
|---|---|---|---|---|---|
| 起始帧 | SOH(0x01) | 0x00 | 0xFF | 128字节,含文件名和大小 | CRC16 |
| 数据帧 | STX(0x02)或SOH | 递增 | 取反 | 1024或128字节 | CRC16 |
| 结束帧 | SOH(0x01) | 0x00 | 0xFF | 128字节,数据区全0 | CRC16 |
| EOT | 0x04 | - | - | - | - |
1024字节的帧用STX标志(0x02),128字节的帧用SOH标志(0x01)。接收端要根据第一个字节来判断后面数据区块长度,不能想当然按1024收。
3.2 接收端状态机与超时重传
Ymodem协议本质是接收方主导的。接收方发C,发送方等着发第一帧,整个过程是一个有限状态机。工程上最稳的做法是把状态机拆成几个状态:
typedef enum { YMODEM_WAIT_START, // 等待起始帧 YMODEM_RECV_DATA, // 接收数据帧 YMODEM_WAIT_EOT, // 等待EOT YMODEM_FINAL_ACK, // 等待结束帧 YMODEM_DONE // 传输完成 } ymodem_state_t;写接收逻辑时,有几个点关系到整个传输链路稳不稳定:
第一,串口中断收到的每个字节都要喂进状态机,不要用简单的"等一帧收完再处理"的阻塞方式。因为Ymodem的帧在串口线路上是一个字节一个字节到达的,用阻塞方式很容易因为应答不及时导致发送方超时重传。
第二,每收到一帧都要做CRC16校验,校验不过要回NAK而不是ACK。很多发送端上位机对NAK的处理是重发当前帧,而不一定重新开始整个传输,所以收到NAK时千万别傻等。
第三,超时处理一定要有。接收方发了C之后,如果等不到起始帧,一般3~5秒后就重发C。如果传输中途某个帧超过3秒没到,接收方可以继续发C请求对方重发,也可以直接判超时失败,具体看你想要哪种容错机制。我测试下来,把"等待应答"超时设在2秒比较合适,既不会让异常传输拖太久,也不会因为串口速度慢导致误判。
直接分享一个调试经验:很多人一开始用串口助手手动发Ymodem文件,发现接收板总是卡在"等待起始帧"这个状态。原因很可能是串口助手发送方没有先收到接收方的C字符,导致发送方一直等待不动作。Ymodem是接收方先开口的协议,你如果只是点一下"发送文件",但接收方的代码里没有主动发C,那就永远等不到数据。
3.3 固件写入Flash的时序
收到数据帧之后,是边收边写Flash还是收完了再整包写入?对H743来说,边收边写是没问题的,但要注意它内部的Flash编程特性:编程完成后要等待BSY位清零才能进行下一次操作。H7的Flash编程需要等待,哪怕是一个64位数据的编程,也要等BSY清掉再发下一条命令。如果你串口波特率是115200,一帧1024字节大概90ms左右,这期间Flash编程早就完成了,等待不会影响接收节奏。
但有一个必须注意的边界条件:如果最后一帧数据不满1024字节,收完最后一帧后要先把剩余缓冲区里的数据刷进Flash,然后再回ACK给发送方。不然你回完ACK,发送方就发EOT了,但你还有一截数据没写进去。
另外还有一个细节,App固件的大小最好记录成文件信息里的"文件大小",而不是按实际收到多少包来算。因为最后一包可能不足1024字节,Ymodem会用0x1A(SUB)填充。如果你把填充内容也写进Flash,固件会多出无用的尾部数据。虽然不影响执行,但如果后面要做固件A/B区升级和校验,这个误差就可能出问题。
4. 上位机源码的技术要点:以C#为例的完整传输链路
4.1 为什么选C#写上位机
做Ymodem上位机,语言选择挺多的:Python的pyserial写起来快,但打包给客户用不如C#方便;LabVIEW拖控件快,但做协议逻辑和异常处理比较别扭;C#用Visual Studio开发,底层SerialPort类封装得很成熟,界面也容易做,所以我自己最终选了C#。题目标题里提到"带上位机源码",我这里就把C#的工程化思路重点讲一下。
上位机的角色是发送方,跟Bootloader接收逻辑正好对称。Ymodem协议的上位机核心就三件事:
- 读文件、拆包;
- 按协议帧格式逐字节构造数据;
- 等待接收方应答,按状态机推进或重发。
这里最关键的代码不是界面Show,而是什么时候发C、什么时候发数据。很多人写上位机的时候直接上来就发文件头,结果Bootloader那边还在等C字符,两边就僵住了。顺序应该是:串口打开、初始化完成后,上位机先等待接收方发来C,再发送起始帧。
用C#写一个简化的发送状态机核心逻辑:
private async void SendFileViaYmodem(string path) { byte[] fileBytes = File.ReadAllBytes(path); uint fileLength = (uint)fileBytes.Length; byte[] fileName = Encoding.ASCII.GetBytes(Path.GetFileName(path)); // 等待接收方发出 'C' bool gotStart = await WaitForCharAsync((byte)'C', 3000); if (!gotStart) { ShowLog("未收到Bootloader的C字符,请检查连接"); return; } // 发送起始帧 byte[] headerFrame = BuildStartFrame(fileName, fileLength); SerialPort.Write(headerFrame, 0, headerFrame.Length); // 等待接收方ACK // ... }注意文件名的编码,Ymodem起始帧里文件名部分是ASCII字节流,如果你的文件名带中文,这里必须做一个决定:要么在起始帧里用ASCII码形式传一个纯英文别名,要么在发送前跟Bootloader约好编码规则。我测试中直接传中文文件名,很多Bootloader解析会乱,最终我统一在上位机侧把文件名改成纯英文+版本号,比如app_v123.bin,简单省事。
4.2 上位机的几个容易崩的细节
如果你自己写上位机,有几个地方经常出错,我直接列出来,你写完代码后对着自查一遍:
第一,SerialPort的ReceivedBytesThreshold要设为1。Ymodem的应答都是单字节的ACK(0x06)或NAK(0x15),如果你把这个阈值设成默认的1,是对的;但如果你把数据接收当成"收一个完整Ymodem帧"来处理,就会出问题,因为接收方(Bootloader)返回的永远只是单字节应答,不是整帧。
第二,发送数据帧时的包序号要用ASCII码表示,而不是二进制字节。也就是"包1"要发送字符0x30 0x30 0x30 0x31,不是发送一个0x01。这是我见过上位机端最常见的Ymodem实现错误,十个人里有三四个会栽在这里。
第三,发送完一个数据帧后要等待ACK或NAK,而且等待要有超时。超时时间不能太短,我设在1秒钟,因为Bootloader在写Flash期间虽然很快,但串口缓冲区可能还有残留数据,应答会稍有延迟。超时后可以直接重发当前帧,重发超过3次就判定失败,提示用户检查硬件。
第四,EOT阶段完成后,Bootloader还会回一个C并等待结束帧。很多上位机发完EOT收到ACK就直接关闭串口了,这会导致Bootloader那边一直等结束帧等不到,最后超时。完整流程是:发EOT -> 收ACK -> 收C-> 发结束帧 -> 收ACK,才算真正完成。
4.3 进度条与错误处理的人机交互设计
这次升级固件动辄几百KB,如果上位机的进度条不准确,用户会以为程序卡死了。所以进度的计算必须基于"当前发送的字节数/文件总字节数",而不是"当前发送的包数/总包数"。因为最后一个包可能不满1024字节,按包算会提前跳满进度条,显得不专业。
错误处理方面,我设计了三级异常处理:
- 第一级是重试机制:单帧无应答,重发同一帧,最多3次。
- 第二级是传输中断处理:如果3次重发仍然失败,弹窗提示用户检查串口连接,保留已经接收的部分数据,让用户选择"继续"还是"放弃并重新开始"。Bootloader那边也要有对应的"重新开始"逻辑,即收到起始帧就重新擦除Flash重新收。
- 第三级是文件本身的校验:传输完成后,上位机可以再读一遍本地文件计算CRC32或者MD5,然后发一个自定义的校验命令给Bootloader,让Bootloader把刚写入Flash的固件读出来做同样的校验。两边一致才算升级成功,这个步骤能避免掉"明明提示成功了但APP跑不起来"的尴尬。
5. 实测中的坑与验证方法:从H743到上位机的完整排查
5.1 一张表说清常见故障和根因
我把这段时间调试IAP遇到的所有故障整理成了一张对照表,排查问题时直接对照查:
| 故障现象 | 直接原因 | 根因/修复方法 |
|---|---|---|
| 跳转后卡死在hal_delay | SysTick中断无法触发 | 跳转前将SysTick控制寄存器全部清零复位 |
| 跳转后进HardFault | 未检查APP栈顶合法性 | 跳转前检查首字是否为合法RAM地址 |
| APP能运行,但一开串口就死 | VTOR未设置为APP首地址 | APP工程main最前列设置SCB->VTOR |
| APP运行时读Flash数据错误 | D-Cache没有清理或提前关闭 | 跳转前执行SCB_DisableDCache() |
| 传输到一半CRC不过 | 串口波特率误差大 | 检查外部晶振配置,H743的串口时钟要确认 |
| 反复接收到起始帧,但总在开头卡住 | 上位机未先等C字符就发起始帧 | 按"等待C -> 发起始帧 -> 等ACK"的流程修正 |
| 收包序号错乱 | 没有正确识别1024字节STX和128字节SOH | 按帧首字节判断帧类型后再读后续数据 |
| 升级完成后App无法运行 | 文件末尾的填充字节0x1A被写入了Flash | 按文件大小精确控制写入长度,多余填充不写入 |
| 传输过程中串口助手显示乱码 | 上位机和Bootloader波特率不一致 | 统一配置,并用示波器或逻辑分析仪确认实际波特率 |
5.2 一次完整的实测调试验证步骤
做完整个系统之后,我每次改完Bootloader或者上位机,都会走一遍相同的验证流程,这样能快速定位是哪一端出了问题:
第一步,先用串口助手接上Bootloader,手动发送一个C字符,观察Bootloader是否有回应。正常的Bootloader在收到C后不会立刻回复什么,但如果你在Bootloader代码里加了调试日志,这一步能看到它从"等待串口数据"状态切到"等待起始帧"状态,说明串口链路是通的。
第二步,用Ymodem发送一个很小的人工构造bin文件(比如32字节),看Bootloader能不能完整接收并写入Flash。这个步骤能快速排除大文件传输中的分包问题。
第三步,正常发送一个几十KB的真实APP固件,传输完成后直接在调试器里读Flash数据,跟电脑上的bin文件做一次逐字节比对。如果一致再跳转,排除"数据传错了但校验没发现"的情况。
第四步,验证完正常流程后,故意在传输到50%时拔掉串口线,过几秒再插回去,看上位机能不能正确识别并提示传输中断,Bootloader的重传逻辑能不能正确处理。这一步很关键,因为实际用户现场不可能全程线路稳定。
5.3 几个容易被忽略的工程建议
最后再聊几个踩出来的经验:
Bootloader里建议保留一个简单的自检功能:上电后短按某个按键或者上位机发一个固定命令,就进入"固件更新模式",如果超时没有进入更新模式,就直接跳转到APP正常执行。这样量产时可以先通过Bootloader烧第一版固件,后面升级也不用担心设备因为没有APP可跳而变砖。
Flash擦写操作尽量放在低优先级中断里执行。H7的Flash编程需要关中断或者用Flash控制器自己的忙等待状态,如果你在写Flash期间还有高优先级串口中断在跑,会导致写Flash操作跟串口收发互相干扰,极难排查。
不要把整个Flash擦除,只擦APP区。我见过有人图省事在接收固件前把整个芯片Flash擦一遍,结果把Bootloader自己擦没了,直接变砖。正确做法是只擦APP起始地址之后的几个SubSector。
上位机源码里建议加入"固件版本号"解析功能。把版本号编译进bin文件固定偏移的位置(比如文件头前16字节),上位机读取bin时自动解析出版本号,显示在界面上。这样升级时能一眼看出要烧的是哪个版本,避免拿错固件文件刷进去的乌龙。
这套方案从Bootloader到上位机全部走通后,整个升级过程稳定在一分钟内完成。H743的大Flash和大RAM让整个IAP方案从存储容量到执行速度都有足够的余量,把这些基础工作做好,后面再做双分区A/B升级、OTA协议扩展、固件加密等进阶功能时,都不会被底层的传输和跳转逻辑卡住。
本文还有配套的精品资源,点击获取