做嵌入式产品最怕遇到什么?我的答案是:产品已经焊好、装进外壳、灌了胶,结果程序要更新。尤其是DSP28035这类C2000系列芯片的应用,很多设备装进现场后根本没有条件拆开接仿真器,这时候一套可靠的串口升级方案就成刚需了。
这套"DSP28035串口升级方案"实际上给了完整的三件套:BootLoader源码、测试用的App工程源码、PC端上位机源码,外加一份说明文档。意思是说,你拿到的不是零散的思路,而是一套可以直接跑通的代码。文章后面我会把BootLoader怎么做、Flash分区怎么划、上下位机协议怎么定、App工程怎么配合,以及我实际调试时踩过的那些坑全部拆开讲清楚。如果你是刚接触28035想做在线升级的开发者,或者手头有板子想快速搭一套离线升级工具,这套内容可以直接当模板用。
1. 这套串口升级方案到底要解决什么问题
1.1 现场升级的痛点
先说说为什么要做串口升级。28035在电机控制、电源管理、工业仪表里用得很多,这些产品有个共同特点:设备装好之后,基本不会再给你留一个仿真器接口。就算留了,现场环境往往也不允许你随时拆机。传统开发阶段用JTAG烧程序没问题,但量产之后要改Bug、加功能,你不可能把每台设备都运回实验室。
串口升级解决的就是这个"后装"问题。芯片本身有SCI串口,只要产品引出了UART的RX/TX两根线,哪怕设备在用户现场,也能通过一个串口工具把新固件灌进去。这个方案里包含的BootLoader源码,本质上就是一个常驻在Flash开头的引导程序,它在上电时判断要不要进入升级模式,如果要,就通过串口接收上位机发来的App固件,写入Flash指定区域,然后跳转运行。
1.2 方案整体构成与工作流程
这套方案的组成很标准,我把它整理成一张分工表:
| 组成部分 | 作用 | 运行位置 |
|---|---|---|
| BootLoader工程 | 串口接收、Flash擦写、跳转控制 | Flash引导区 |
| App测试工程 | 模拟实际业务程序,响应升级指令 | Flash应用区 |
| 上位机源码 | 发送固件、握手、校验、进度显示 | PC端 |
| 说明文档 | 移植步骤、硬件接线、协议说明和常见问题 | 非代码形式交付 |
工作流程其实就是一个"引导+跳转"的状态机。上电先跑BootLoader,它检查升级标志和App区域是否有效:有升级请求就留在BootLoader里等待串口数据;没有升级请求且App有效,就跳转去执行App。App运行过程中如果收到升级指令,会设置一个升级标志然后软复位,重新回到BootLoader,这时候BootLoader检测到标志,进入升级模式。上位机再依次执行握手、擦除、分帧发送、校验、跳转。
这个流程看起来简单,但每步都有不少细节。下面我把代码里最能影响成败的几个模块单独拎出来讲。
2. BootLoader的框架设计与Flash分区
2.1 存储分区与链接脚本(CMD)规划
DSP28035的Flash分多个Sector,擦除必须按Sector整段操作,不能像EEPROM那样按字节擦。所以分区是第一件要确定的事。常见做法是BootLoader占用前面两个Sector,App从后面开始,再单独留一个Sector做参数区。我用的一个示例分区如下:
| 区域 | 示例起始地址 | 容量示例 | 内容 |
|---|---|---|---|
| BootLoader区 | Flash起始地址 | 2个Sector | BootLoader代码与Flash API运行区 |
| App区 | Flash起始地址 + 偏移 | 剩余Sector | 应用固件 |
| 参数区 | Flash末尾1个Sector | 1个Sector | 升级标志、版本号、固件长度 |
注意这里的地址具体是多少,要看28035实际Memory Map以及你手头工程的CMD文件,上面只是分区思路。实际做的时候,BootLoader工程的CMD和App工程的CMD必须各自独立,而且绝对不能重叠。我见过太多人把两个工程共用一个CMD,结果BootLoader一擦Flash,把App区地址也覆盖了,直接变砖。
CMD里除了划分Flash地址,还要特别处理RAM段。关键的一点是:Flash API库和部分关键代码段必须在RAM里运行,所以CMD里通常会有类似LOAD = FLASH, RUN = RAM的段分配写法。这说明代码本身存在Flash里,但启动时会被拷贝到RAM执行,这个在2.3里细说。
2.2 启动模式选择与BootLoader主流程
28035芯片内部有Boot ROM,上电后会根据Boot Mode引脚的状态决定启动方式。第一次烧录BootLoader一般直接用仿真器,之后量产就可以完全脱离仿真器,靠串口升级。但要注意,芯片自带的SCI Boot模式是ROM里的固定引导程序,和我们自己写的BootLoader是两回事。ROM的SCI Boot只能把程序下载到RAM,没法直接管理你的Flash分区,所以做产品升级还是要自己写BootLoader,ROM的SCI Boot主要用来救急或者首次刷BootLoader。
BootLoader主流程我建议按这个框架写:
void main(void) { // 1. 关看门狗,初始化PLL与串口 DisableWatchdog(); InitPll(); InitSci(); // 2. 检查升级标志与App有效性 if (CheckUpgradeFlag() || !CheckAppValid()) { // 3. 进入串口升级流程,直到收到完整固件 UpdateBySerial(); } else { // 4. 跳转App JumpToApp(); } }几个容易被忽略的点。第一,PLL初始化一定要先做,因为它决定了SCI波特率的基准时钟,PLL没配好,后面串口全是乱码。第二,等待升级时建议加一个超时判断,比如几秒内上位机没发握手包,就自动跳转现有App,不然设备如果没接串口线,会一直卡在BootLoader里不干活。第三,跳转App前必须把用到的外设中断全部关掉,并复位PIE向量表,否则App一跑就进中断异常。
2.3 Flash擦写:必须跑到RAM里执行的API
Flash擦写是整个方案里最核心、也最容易翻车的一步。28035的Flash编程需要调用厂商提供的Flash API库,这个库有一个硬性要求:必须在RAM中运行。原因是Flash控制器在擦写期间不能从Flash取指令,如果你让Flash API在Flash里执行,轻则写入失败,重则直接触发非法指令复位。
具体做法是在工程里把Flash API库文件加进来,通过CMD文件分配RAM地址给它。代码启动时先做一个段拷贝,把API从Flash搬到RAM,然后才开始擦写操作。每次擦一个Sector的最小单位,写入则按16位字为单位。写入完成后要读回校验,确认数据确实写进去了,再回复上位机"这一帧成功"。
还有一个经验:擦写过程中千万不要被中断打断。尤其是定时器中断或串口接收中断,如果在Flash编程中途触发,会导致编程时序被破坏。实际操作中,我会在擦写前屏蔽可屏蔽中断,等写完了再恢复,上位机那边重发机制可以兜底。
3. 通信协议与上位机源码实现
3.1 帧格式与交互命令定义
上下位机之间的协议是整个升级方案的中枢。代码里设计了一套比较稳的帧格式,我建议你直接或者微调后使用:
typedef struct { uint16_t head; // 帧头,固定 0xA55A uint8_t cmd; // 命令字 uint8_t frameNo; // 帧序号 uint32_t addr; // 目标Flash地址 uint16_t len; // 数据长度 uint8_t data[]; // 负载数据 uint32_t crc32; // 对以上字段的CRC32校验 } UpgradeFrame;命令字至少要有这几种:握手请求、版本查询、擦除命令、编程数据、校验请求、跳转执行、复位。所有命令都要有ACK/NAK应答,不能只发数据不回包。帧头和CRC32是防呆和防错的关键:帧头防止串口收到垃圾数据后误解析,CRC32保证传输过程中即使出现个位级别的bit翻转也能被发现。
为什么用CRC32而不用简单的累加和?因为串口在工业现场很容易受干扰,累加和纠错能力太弱,可能数据悄悄坏了但校验和碰巧对上了。CRC32虽然计算量大一点,但28035主频不低,上位机更不在乎这点开销,换来的是传输可靠性提升一大截。
3.2 上位机的工作流程与界面逻辑
上位机我用C#写过一版,界面不算复杂:串口号选择、波特率、固件文件路径、升级进度条、日志输出框。核心逻辑不是界面,而是发送状态机。流程是:打开串口 -> 加载Hex或Bin文件 -> 发送握手包等待ACK -> 发送擦除命令 -> 按帧拆包连续发送 -> 发校验命令 -> 发跳转命令。每一步都必须等收到ACK再继续,超时则重发,连续重发多次失败就报错停止。
分包发送是必须的。不能把整个固件一次性塞进一帧里,28035的SCI接收缓存有限,上位机内存再大也没用。我的做法是每帧最多256字节,发完一帧必须等ACK再发下一帧。传输大固件时这个流程会比较慢,但胜在稳定,不会出现缓冲区溢出导致丢包。
日志框很重要。我在上位机里每一帧的收发都打日志,方便现场排查。比如"发送擦除命令,等待ACK... 收到0x06,擦除成功",这种日志看着啰嗦,但一旦升级失败,能立刻定位到是哪一步出的问题。
4. App工程侧要做的配合
4.1 App的入口、向量表与CMD映射
很多人以为有了BootLoader和上位机就够了,App工程随便编一个就能跑,这是误解。App工程有两点必须配合好。
第一,链接地址必须从App区起始。BootLoader跳转时是直接跳到一个固定地址,如果你的App链接地址不是从这个地址开始,那么跳过去执行的第一条指令可能就是错误数据。CMD里要把origin设置为App区起始地址,同时Flash和RAM的分区都要和BootLoader错开。
第二,中断向量表要在App里重新初始化。28035用PIE中断管理器,BootLoader跳转前虽然会关中断,但跳过去后App必须自己完成PIE初始化、外设初始化和中断使能。如果App里漏了这一步,最常见的情况是程序跑起来但任何中断都不响应,定时器不转,串口收不到数据,整机像卡死一样。
4.2 升级请求标志与软复位跳转
App侧要实现一个"收到升级指令后进入BootLoader"的机制。最简单实用的方案是:App串口收到升级命令后,往一个固定的RAM地址写入升级标志,然后触发软复位。BootLoader启动时读取这个RAM地址,发现有标志就进入升级模式,升级成功后清除标志。
这里有个细节:标志写在RAM里,掉电就没了。也就是说,如果App收到升级命令后还没软复位时断电了,下次上电BootLoader不会进入升级模式,会正常跳转App。这通常不是问题,因为正常情况下指令发出后马上复位,间隔很短。但如果你的产品对这点比较敏感,可以把标志写进Flash参数区,设计成"写两个翻转值"的形式,防止Flash写入半途掉电导致标志错乱。
软复位的方法我用的是看门狗溢出复位:关闭看门狗喂狗,设置一个很短的时间窗口,然后进入死循环等看门狗超时复位。这样做的好处是不用去翻芯片手册找特殊寄存器,逻辑上也简单可靠。触发软复位前,一定要把外设、中断清理干净,尤其是串口和定时器,免得复位瞬间还在触发中断,干扰标志写入。
5. 实战中踩过的坑与排查清单
5.1 连不上串口、乱码这类基础问题
先说说概率最高的问题:上位机发送握手请求,BootLoader没反应。我遇到过的情况主要有三类。第一是USB转串口模块供电不稳,或者RX/TX接反了,这个用万用表量一下就能确认。第二是BootLoader根本没有进入等待状态,比如升级标志没写成功,BootLoader检查后直接跳App了。第三是波特率对不上,我在BootLoader里配置了115200,但LSPCLK没设置正确,实际波特率偏离了百分之几,上位机和板子各说各话。
乱码的问题我也踩过。很多人第一反应是换线、换USB转串口,其实根源往往是PLL初始化顺序不对,导致SCI波特率分频不准。28035的SCI波特率由外设时钟和BRR寄存器共同决定,外设时钟不对,波特率一定偏。排查这类问题,我建议先用示波器看TX引脚的波形,数一下实际波特率和理论值差多少,比盲猜高效得多。
我整理了一张排查速查表,可以直接对照处理:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 完全无响应 | RX/TX接反、USB转串口坏、BootLoader未进入等待 | 量线序,换模块,检查标志位逻辑 |
| 乱码 | PLL配置不对、波特率偏差大、地线没共地 | 重新计算BRR,检查PCB共地,降低波特率 |
| 握手成功但传输中途卡住 | 帧序号乱、CRC计算不一致 | 上位机与BootLoader统一CRC算法和初始值 |
| 擦除失败 | Flash API在Flash里执行 | 检查CMD是否把API段搬到了RAM |
5.2 Flash校验失败与写不进去的问题
校验失败是最让人头疼的,因为现象都一样,原因能差出十万八千里。我碰到过的原因有四种:Flash API没在RAM运行、擦写期间被中断打断、写入地址越界、CRC32实现不一致。前三个是程序问题,第四个是协议问题。
解决思路是给BootLoader加详细的错误返回码。比如擦除失败返回0x10,写入失败返回0x11,校验失败返回0x12,上位机日志里直接显示。这样出问题后不需要盲猜,看错误码就能缩小范围。我在这个方案里是把错误码直接打在ACK响应里,上位机每个回包都解析并记录。
另外,Flash写入和擦除是有寿命的,虽然28035的Flash寿命撑产品周期没问题,但调试阶段反复擦写同一个Sector,日积月累还是会有风险。调试时我会把上位机工具设置为"固定版本号+自动生成随机擦写次数"来动态分散磨损,真正量产时则不会反复擦同一片区域。
5.3 跳转死机与变砖后的挽救策略
还有一种崩溃现场:固件写进去了,校验也过了,但一执行跳转,设备马上死机。排查方向基本是这三个:App的CMD分区和BootLoader重叠了、App的入口地址不对、跳转前没有复位PIE和关中断。
最危险的情况是升级中途断电,导致Flash里既没有完整的新App,也没有旧App。如果BootLoader本身也被误擦,那就真变砖了。所以我在BootLoader里永远保留一个原则:BootLoader区绝对不允许在升级流程中被擦除,就算App固件本身有BootLoader区的内容,校验阶段也应该拒掉。另外,BootLoader在设计上要能接受"App无效"这个状态——只要App有效性校验不通过,就始终停留在串口等待,不跳转,不跑飞,这样即使App区域全是0xFF,设备也能重新接收固件,不至于彻底废掉。
再保险一点的做法是把App区做成双备份,一个"当前运行区"和一个"待升级区",升级时先写待升级区,校验通过后再原子切换。这个方案对Flash容量有要求,28035如果资源够,强烈建议这么做。
最后再分享一个个人习惯:BootLoader里的代码和业务逻辑完全隔离,它只干串口接收、Flash写入、跳转这三件事。我见过有人把很多应用功能塞进BootLoader里,觉得能省代码,结果每次改BootLoader都要提心吊胆,生怕把自己锁死在Flash里。代码越简单,越不容易出问题,这套方案的源码也保持了这种风格,你拿过来改改地址和协议就能用。