
说起DSP的串口升级很多人第一反应是“不就是IAP嘛”真上手做的时候才发现从bootloader怎么跳转到App、Flash擦写时代码放哪儿跑、上位机怎么分包校验每一个环节都有坑等着你。我手里这套基于F28035的串口升级方案是在实际产品项目里反复调过的整体拆成四块bootloader源码、测试app工程源码、上位机源码、说明文档。四块单独看都不复杂但串在一起就有不少细节容易翻车尤其是跳转App那一下和Flash擦写期间代码执行的区域这两处理解不到位轻则升级失败重则把整片Flash写坏返厂。这篇文章就按这套方案实际的组织方式来讲正在给产品做串口升级、想给老项目加一个IAP通道、或者刚拿到F28035不知道Boot和App该怎么切的人都可以直接对照着抄作业。1. 串口升级方案到底包含哪几块以及它适合什么场景1.1 源码清单与实际用途先说清楚这套方案交付到你手里是哪些东西免得对着目录发懵。整个工程分为四块bootloader工程这是上电后最先跑的引导程序。它负责初始化CPU时钟和串口检查有没有升级请求有请求就通过串口接收上位机发来的固件包写入Flash指定的App区域没有请求就直接跳转启动已有的App程序。测试app工程一个最简单的应用示例功能就是点灯、串口打印版本号。它的存在意义是为验证升级通道量产前的产线测试也能直接拿它当空白App刷进去确认bootloader和上位机的链路是通的。实际项目里你只需要把业务功能往这个App里填就行。上位机源码PC端工具打开串口、选择固件文件、发送升级命令、分包下发、校验结果、进度显示。我这边是用C#写的串口操作直接用系统自带的串口类UI就是串口选择、文件选择、进度条、日志框。说明文档包含Flash分区说明、两个工程的链接文件改动点、烧录顺序、命令交互格式。你没看错这不是一个单文件夹的“芯片代码”而是一个完整的闭环。我见过不少方案只给bootloader源码上位机用串口助手手动发HEX那样排查问题非常痛苦因为你看不到协议走到哪一步。所以我整理这套方案时坚持把上位机和App也一并放进去任何一环出问题都能直接定位。1.2 为什么选SCI串口作为升级通道而不是CAN或USBF28035这颗芯片支持好几种通信外设CAN、SCI、SPI、I2C都有但我在实际项目里最终锁定了SCI串口。原因很简单排第一的是通用性SCI本质就是增强型UART全双工、带FIFO接一个USB转TTL模块就能调试现场售后的人随便拎一根线就能连上。CAN固然抗干扰强、适合工业现场但需要额外挂CAN收发器协议也要自己封一层除非你产品本身就必须走CAN总线否则我不建议为了升级专门上CAN。还有一个因素是F28035的Boot ROM其实自带SCI加载模式用来烧写程序。那为什么我还要自己做一套bootloader因为Boot ROM的加载模式有几个量产痛点每次都要通过启动引脚配置进入SCI加载模式产品已经装进壳子、端子焊死之后引脚的拉法很难再改而且它使用的是固定的私下协议交互时序、确认机制都不够可定制。自己写bootloader的本质就是把“升级入口”从硬件引脚改成了“软件标志”这样可以通过App里的菜单、上位机指令、甚至远程网络指令来触发升级完全不需要动外壳。串口升级还有一个隐藏优势是上位机开发成本极低。C#、Python、LabVIEW里的串口库都是成熟得不能再成熟的不需要额外装驱动绝大多数系统自带USB转串口驱动比起USB烧写器那种需要装SDK的开发模式要轻量得多。1.3 Flash分区规划升级标志、Boot区、App区怎么排F28035内置Flash容量对于这个量级的DSC来说称得上宽裕但分区没规划好后面每一步都会别扭。我在这套方案里的分区逻辑是固定三段式区域存放内容说明Boot区Bootloader代码、Flash API运行副本这里的代码上电即运行一般占16KB左右App区测试App/业务App、App中断服务函数需要升级替换的代码全部在这里参数区升级标志、固件版本号、校验信息独立留一小块防止被App业务数据覆盖可能的读者第一反应是升级标志为什么要单独放一块因为Flash的写操作有个特性——写入只能把1变成0要把0恢复成1必须先整片擦除。如果你把升级标志和App数据放在同一个扇区每次App想更新自己的运行参数就得被迫擦除整个扇区万一擦除过程中掉了电升级标志也一起丢了。这个设计失误我在早期一个项目里踩过后来所有带IAP的方案都强制切开。分区用多大取决于你的实际需求。Boot区我习惯给16KB里面放了bootloader代码和Flash API库的RAM副本之后还有富余App区就是Flash总容量减去Boot区和参数区剩下的部分参数区哪怕只有256字节也够存标志和版本了。换算原则是够用、留余量别把Boot区抠得刚刚好后面想加功能又要重新挪分区非常麻烦。2. Bootloader工程核心逻辑复位后到跳转App之间发生了什么2.1 Bootloader主流程先看标志再决定去向Bootloader代码初始化的动作并不复杂主流程我用一段伪代码来解释比看完整工程好懂void main(void) { // 1. 先把狗关掉避免初始化期间被看门狗复位 DisableDog(); // 2. 初始化系统时钟、外设时钟 InitSysCtrl(); // 3. 初始化SCI串口配置波特率、FIFO InitSci(); // 4. 读取参数区的升级标志 uint16 flag ReadUpgradeFlag(); if (flag UPGRADE_REQUEST) { // 5. 有升级请求进入升级流程 UpgradeProcess(); } // 6. 没有升级请求跳转到App JumpToApp(); }这个流程的先后顺序其实是有讲究的看门狗必须在最开头就处理否则稍慢一点的Flash擦写过程就会被狗咬复位SCI初始化要在检查标志之前因为进入升级模式后立刻就要用串口跟上位机握手而跳转App这一步必须放在所有初始化完成之后因为App的启动代码默认CPU时钟、外设时钟已经是配置好的状态你跳转前就把这些初始化做完App里就不用再做第二遍。有一点需要特别注意Bootloader里不要放业务逻辑它只管三件事判断是否升级、接收并写Flash、跳转。任何多余的功能塞进Boot区都会压缩App可用空间也会增大bootloader本身出错的概率。这是我在项目复盘里最想强调的纪律。2.2 Flash擦写为什么必须跑到RAM里执行这是F28035甚至整个C2000系列IAP最容易翻车的地方也是很多人第一次跑通串口升级后依然提心吊胆的原因。F28035的Flash擦除和编程过程有个硬件限制当Flash控制器在执行擦除或编程操作时它自己所在的Flash存储区是不能被CPU读取指令的。如果CPU此刻正从Flash取指令就会当场抓瞎表现为程序跑飞、进入非法指令陷阱。解决方法是把执行Flash操作的代码官方Flash API库先从Flash复制到RAM里然后跳到RAM里执行擦写函数。具体落在工程上需要做两件事。第一CMD链接文件里必须开辟一个Ramfuncs段专门用来存放Flash API和相关辅助函数。第二在启动代码里把这个段从Flash搬运到RAM搬运完成后调用的函数指针实际指向RAM地址。别小看这个“搬运”我见过有人直接在Flash里调用官方擦写函数结果一执行就死机排查了一个星期才发现原因是API没驻留RAM。还有两个和擦写相关的坑一并说了一是擦除长时间扇区时要把看门狗临时关掉或者在擦写循环里喂狗否则中间一个复位Flash处于半擦除状态App区域就废了二是要正确配置Flash等待状态寄存器F28035在60MHz主频下跑Flash等待状态配错会导致读出来的数据不稳定表现就是明明写进去是对的校验却不过。2.3 跳转App之前的“清场”动作中断、狗、PIE向量表Bootloader完成自己的使命后要把完好的控制权交给App。这个“交权”过程如果做得不干净App启动后会出现各种诡异的随机故障。首先是关闭中断。直接操作中断使能寄存器和中断标志寄存器把它们清零。这是必须的第一步因为从你关中断到App完成自己的中断向量表初始化这个窗口期任何一个外设中断进来都会让CPU不知所措。然后是清理PIE向量表。这一点很多人会忽视F28035的中断向量表不是像某些MCU那样放在Flash固定地址、需要重定位的它的可编程中断向量表PIE在RAM里。Bootloader运行期间SCI接收中断等可能已经把PIE向量表项指向了Bootloader自己的中断服务函数。如果你跳过清理直接跳进AppApp初始化又没那么快这时候来一个串口中断或者定时器中断CPU就会沿着残留的向量表跳回Bootloader的ISR那个ISR再一返回栈已经错乱了。最终表现就是跳转前一切正常跳转后秒死。我的做法是在跳转前执行一个清场函数清零中断使能、清零中断标志、把PIE向量表所有条目恢复成出厂默认值、把看门狗复位到默认配置App启动会重新配置。最后再用一个函数指针指向App区的入口地址完成跳转typedef void (*pFunction)(void); void JumpToApp(void) { // 关闭所有中断 DINT(); IER 0x0000; IFR 0x0000; // 恢复PIE向量表到默认状态 ResetPieVectTable(); // 跳转到App区入口 pFunction appEntry (pFunction)APP_START_ADDR; appEntry(); }这里的APP_START_ADDR不是你在心里想的“App的main函数地址”而是App在链接时确定的程序入口地址。它通常是App工程里_c_int00这个C运行时启动函数的位置一般落在App区Flash的起始地址附近。具体值看App编译生成的map文件别拍脑袋猜。2.4 串口帧协议设计帧头、命令、长度、CRCBootloader要可靠接收固件向上位机的通信协议必须设计得足够简单明了。我这套方案用的是定长帧头加变长数据的结构字段长度说明帧头2字节固定0xAA 0x55命令1字节0x01擦除请求0x02数据帧0x03结束校验0x04查询版本数据长度2字节大端表示后面负载区字节数负载变长根据命令不同可能是空、固件数据、版本号CRC324字节对整个帧除CRC外的所有字节做CRC32校验协议设计的基本原则是Bootloader越“笨”越好。不要让它做解析复杂格式的事上位机负责把固件文件拆成包、算好CRCBootloader只做“收到校验通过的包写进Flash指定偏移处回一个ACK”。每收一个有效数据包Bootloader回一字节0x80表示成功收到校验错误就回0x81上位机收到0x81就重发当前包最多重发三次。帧和帧之间的区分用超时法上位机发完一包等ACKBootloader在接收状态里如果超过一定字符间隔没有新字节进来就认为本包结束开始解析。串口的波特率我固定用115200-8-N-1不开流控这个速率在产线和售后场景都足够又不至于因为线缆质量导致误码率升高。3. App端必须配合的改动向量表处理、CMD分区与升级触发3.1 F28035没有“中断向量重映射”但要处理的是PIE向量表做过STM32的人听到“App中断向量要重定位”往往习惯性想找一个偏移寄存器但在F28035上这套经验反而会带偏你。F28035的C28x内核本身不采用在Flash里搬移向量表的做法它的可编程中断向量表就是固定挂在RAM里的PIE模块。Bootloader和App其实共享同一个PIE向量表谁启动了谁就负责往这个表里填自己的中断服务函数地址。所以App工程里不需要任何“把向量表搬到某个地址”的操作正常调用初始化PIE向量表的库函数把默认ISR载入再把自己特有的中断服务函数挂进去即可。真正决定App能不能安全跑起来的关键在前面已经说过——Bootloader跳转前必须把PIE向量表清干净不要让旧的中断残留。这两个环节一配合App启动后重新初始化PIE中断体系立刻切换到App自己的版本。我见过有人为了让“App能独立跑”强行给两个程序都做向量表独立偏移结果跳转后中断乱飞。其实完全没必要理解F28035的PIE机制之后App端的中断处理反而比Cortex-M那套简单。3.2 链接文件CMD怎么把App放到Boot后面的Flash区App工程相比普通工程改动点最核心的就是CMD链接文件。默认工程里Flash段是从整个Flash起始地址开始的而你要让App跑在Boot区之后的地址上就必须在CMD文件里重新声明可用的Flash区域。一套标准的改法是在App工程的CMD里把MEMORY部分原先定义的Flash区起始地址改成“Boot区结束的地址”。比如Boot区占用了Flash前16KB那App区的起始地址就是Flash基地址加0x4000。这个值不一定是整数扇区对齐但最好对齐到Flash扇区边界后面做擦除操作会比较方便。同时要注意App的代码段、只读数据段、初始化数据段都要放在这个新定义的Flash区里而RAM段的定义基本不用动。改完后编译打开生成的map文件确认入口地址落在你期望的App区起始位置附近。这一步是对照检查如果map文件里入口地址还指向Flash的低地址说明CMD改得不彻底跳转过去必死。这里还有个容易忽略的文本App里如果用到版本号字符串、升级标志定义等尽量安排在固定偏移的位置或者干脆放到参数区方便上位机读取版本。App和Boot之间通过参数区的约定字段通信比在App内部猜偏移量要清晰得多。3.3 App里触发升级的常用姿势置标志加软复位App在正常运行时怎么进入升级模式行业里最稳妥的方案是“写标志加软复位”。具体流程App收到上位机下发的“进入升级模式”的指令或者用户在本地按键确认。App先把一个约定值比如0xA5A5写入参数区的升级标志位置。App触发一次系统软复位让CPU回到复位向量过程重新从Bootloader开始。Bootloader上电后读到升级标志为0xA5A5判断有升级请求进入接收固件流程。有人会问为什么不在App里直接跳转到Bootloader的入口技术上可以但实际不推荐。软复位的好处是整个芯片的外设、中断、栈指针全部回到出厂态Bootloader可以在一个完全干净的起点开始工作。直接调用Bootloader入口则可能残留App设置过的外设状态比如SCI波特率被App改过、看门狗喂狗方式不同这些残留状态会在升级过程中埋雷。触发软复位在F28035上最通用的办法是故意触发看门狗复位向看门狗控制寄存器写入一个故意的错误喂狗序列让狗超时复位。不推荐用某些芯片特有的“软复位指令”的原因很简单不同版本芯片行为可能有差异而看门狗复位在每颗芯片上都是必有的机制。还有一个细节App写升级标志前必须先把参数区对应扇区擦除再写入标志值。因为Flash写操作不能把0写成1如果不擦就直接写结果会和你预想完全不同。擦除这个操作耗时几十毫秒App要做好提示别让用户以为卡死了。4. 上位机源码与固件转换从out到bin再到烧写完成4.1 上位机的功能划分和协议解析流程上位机源码我按三个模块划分串口管理模块、协议收发模块、界面控制模块。串口管理负责枚举端口、开关串口、收发字节缓冲协议收发负责解析Bootloader回传的ACK和错误码界面控制负责文件选择、进度更新、日志打印。串口这块C#里有现成的串口类设好波特率、数据位、停止位、校验位就能用。关键点在字节缓冲处理上位机收ACK时很可能会把之前Bootloader还没来得及发的信息混在一批里所以不能简单“读一字节处理一字节”而是维护一个接收缓存按帧头扫描、拼接、解析。我对帧的识别逻辑是在缓存里找0xAA 0x55帧头找到后按帧头后的命令字节解析剩余长度取够一帧再做CRC校验校验通过转成对应事件交给界面更新。协议收发还有一个必须做的机制是超时重发。发送每个数据包后上位机最多等500毫秒的ACK收不到就重发连续重发3次仍无响应直接上报“通信超时”终止升级流程。这个机制看着简单但能从根上解决串口偶发丢包导致升级中断一半的问题。4.2 如何把CCS工程产物转换成可以下发的bin文件这里新手最容易卡住使用开发环境编译出来的固件是.out格式上位机直接按bin发过去Bootloader写进Flash也跑不起来。原因很简单编译工具链链接的结果是COFF格式除了代码段还打包了符号表、调试信息、段定位信息并不是Flash上的纯二进制镜像。要把.out转成纯bin文件用官方提供的十六进制转换工具一般叫hex2000就能完成。命令行大致思路是指定输入文件为out文件指定输出格式为二进制指定需要导出的段然后生成一个.bin文件。具体命令参数因版本略有差异但操作思路是通用的hex2000 -boot -b -o app.bin app.out其中关键的是-b参数表示输出二进制格式而-o指定输出文件。转出来之后你还可以用bin查看器或上位机自己的校验功能确认文件大小、头部几个字节是不是预期的入口指令这样可以提前拦截“根本没转对”的问题。另一种备选方案是用Python脚本解析COFF文件结构自己提取代码段拼成bin。这种方案对我来说只有当转换工具在公司加密环境里拿不到时才值得做正常开发直接用官方工具最省心。4.3 一次完整升级的实测流程和预期输出拿这套方案在板子上走一遍完整升级流程我预期会出现下面的节奏给板子上电Bootloader运行因为没有升级标志直接跳转AppApp串口打印“App v1.0”LED常亮。上位机选择app.bin点“开始升级”。上位机先发0x01擦除命令Bootloader返回0x80表示已进入升级模式并擦除App区完成此时App之前的代码其实已经没了但板子还在裸跑Bootloader的串口服务。上位机看到擦除ACK后开始逐包发送固件数据每包256字节。Bootloader每收到一包写一包写成功后回0x80。上位机进度条不断前进同时日志框打印“包序号/总包数”。最后一包发完上位机发送0x03结束校验命令。Bootloader对整片App区计算校验值和上位机发来的预期值比对一致则回0x80并自动清除升级标志、执行软复位。复位后再次从Bootloader启动这回读到升级标志是空直接跳转AppApp串口打印“App v1.1”。升级结束。理想情况下整个升级过程在一次串口连接里自动完成不需要人工干预。如果哪一步没有如期走到日志框里的“最后一次成功交互的帧类型”就是排查最关键的线索。5. 升级失败排查表与几个我踩过的坑5.1 常见故障现象的根因对照表这套方案调试过程中我整理了一份故障对照表基本覆盖了90%的串口升级问题现象可能根因定位方法解决思路上电后一直停在Boot不跳App参数区残留升级标志读标志值看是否为0xA5A5手动擦除参数区或用上位机发清除标志命令跳转App后秒死/反复复位入口地址错误、PIE向量表残留、App区实际没写入查App的map文件、检查跳转地址、读回Flash比对修正APP_START_ADDR清理PIE后再跳串口收发不稳定偶发卡包波特率误差、有粘包、线材质量差串口助手看原始hex字节流核对系统时钟配置、增加超时拆包、换短线升级过程中写Flash卡死狗没关、Flash API跑在Flash里、供电跌落确认擦写前关狗、查看API是否在RAM段修正CMD段分配、临时关狗、加强供电升级显示成功但版本没变发错文件、bin转换不对、包序乱核对bin大小和头部、打印实际写入地址重新生成bin、检查发送顺序与写入偏移擦除后App区不敢写擦除过程被打断查看Flash状态寄存器擦除前关中断、关狗擦除后读取空值确认这张表是我排查问题时的第一参考因为大多数问题其实都不是“代码逻辑漏洞”而是“环境或流程没配对”。先看现象属于哪一类再决定往哪个方向深挖比漫无目的地打断点强得多。5.2 “跳转App跑飞”的完整排查链路这里展开一次我印象最深的排障过程供你参考排查思路。现象Bootloader跑通升级也显示成功但软复位后App跑不起来看门狗反复复位串口一直无输出。我第一反应是升级写入的数据不对于是先在上位机里把固件读回来对比发现Flash内容与bin文件完全一致排除了写错的问题。接着怀疑入口地址我打开App编译生成的map文件确认入口地址就是我JumpToApp函数里用的APP_START_ADDR。仔细核对过后发现问题恰恰不在地址本身而在这个地址上的指令含义。我用的跳转方式是直接调用入口地址但App的C运行时启动代码需要一个正确的栈指针和全局状态初始化。我这边跳转前虽然关了中断但忘了把SP栈指针恢复成App预期的值。Bootloader跑了一路函数调用栈指针早就不在App启动代码期待的位置了App一进_c_int00就把栈操作搞乱。修复方式是在跳转函数入口处先把栈指针切回Bootloader启动时的初始栈或者干脆在跳转前重新调用App的复位向量流程让App的启动代码自己完成栈设置。从那以后我再写跳转逻辑都会刻意检查“程序入口入口指令是否是栈指针设置指令”而不是只盯地址数值。这个坑不深入链接器层面很难发现但排查链路其实是清晰可复现的。5.3 串口老是不通、写一半卡死这类问题的处理串口升级里最让人烦躁的其实是“串口握手就不通”和“写一半卡死”这类环境型问题代码逻辑完全没毛病的状况下也能反复出现。串口不通我建议第一步不要看协议先用串口助手裸收一下Bootloader的启机打印或ACK字节确认物理链路通不通。如果裸收乱码先看波特率误差。F28035如果用内部振荡器跑PLL时钟精度本来就拼人品和上位机的115200对不上很正常换成外部晶振或者做内部振荡器校准之后这个现象会大幅缓解。第二步确认TX/RX交叉连接不少新手把直连和交叉接反板子的TX接板子的TX折腾半小时才反应过来。写一半卡死大半原因在前面说过的“Flash API没在RAM里跑”和“看门狗没管住”。测这个问题的笨办法是先关闭看门狗、把擦写函数使用段改成RAM段再跑一次升级如果能写完那大概率先前就是这两处。另外还有可能是供电问题Flash擦除瞬间电流比运行稍大如果板子用的是劣质USB线供电电压一跌落Flash状态机就会异常。排查时用示波器测一下擦除瞬间的3.3V纹波能排除掉设计隐患。串口粘包和半包的问题我现在的标准做法是Bootloader端用“超过4个字符时间未收到新字节”作为一帧结束的判决条件上位机端则用500毫秒ACK超时来兜底。这样即使偶尔拆包拆错了最多重发一次不会累积成数据错乱。最后再分享一个经验这套串口升级方案真正稳定运行的前提是把交互时序当成状态机来设计——Bootloader这边明确区分“等待帧头”“等待长度”“等待负载”“等待CRC”“回复ACK”几个状态每个状态有超时退出上位机那边明确记录当前发到第几包。两边都做成可观测的状态机升级过程就再也不是玄学而是每一步都能落到日志里的确定性流程。