资讯动态

基于DSP28335的CAN在线升级:Bootloader方案全解析

发布时间:2026/10/19 2:57:05 来源:尧图企业网站定制
做工业控制的人最怕的就是设备已经发到现场产品固件出了问题要改。程序本身早就写好了但拆开机箱重新接JTAG下载器或者整体返厂代价都不是一般的高。我之前负责的那个基于DSP28335的控制板就撞上了这个问题算法参数需要根据现场工况微调而设备装在一个很难拆卸的机壳里接线口只甩出了电源和一组CAN总线。那段时间被迫把CAN升级从“远期需求”提成“必须马上落地”最终做出来的方案是Bootloader、上位机、固件打包工具全部自主开发不依赖第三方IAP方案。这套东西实测用了很长时间踩过的坑也不少这里把整体设计和关键细节完整梳理一遍。如果你正准备给F28335这类C2000系列DSP做在线固件升级或者手头有类似的老项目想加上CAN升级能力却不知道从何下手这篇文章应该能帮你节省大量排查时间。整篇文章会从方案架构讲起然后依次拆解Bootloader内存规划、CAN通信协议、上位机软件设计、实际代码实现最后是调试中遇到的典型问题和解决办法。1. 方案设计初衷与整体架构为什么自研CAN升级1.1 先搞清楚升级到底发生在哪个环节在DSP28335上做CAN升级本质上是做IAP也就是在应用编程。上电之后芯片先运行一段固定不变的Bootloader代码由它决定两件事如果检测到上位机要求升级就进入CAN接收流程把固件数据一块块写入Flash如果没有升级要求就直接跳转到用户App继续工作。这里有个容易被新手忽略的前提Bootloader本身绝对不能放在会被App覆盖的区域否则升级过程中把Bootloader自己擦掉整台设备就变砖了。所以第一步不是写代码而是先规划好F28335的Flash空间。F28335的片内Flash按16位字寻址总共256K字被硬件分成A到H共8个扇区每个扇区32K字。我用的划分方式是Bootloader独占H扇区也就是Flash末尾区域用户App从A扇区开始放下参数区单独摘出来专门存放升级标志、版本号等关键信息。这种“Bootloader固定、App可擦写、参数区独立”的格局是整个升级方案的地基。分区一旦定死后面所有工程的链接脚本、擦写范围、跳转地址都要围着它转。所以建议动手写代码之前先用一张表把地址规划列明白后面能少走很多弯路。1.2 为什么走CAN而不是串口或其他总线很多老工程师会问串口也能做升级为什么非要选CAN我当时的考虑有几个。一是现场环境只有CAN可用设备对外通讯靠的就是CAN总线现场基本没有单独引出的串口。二是CAN是差分总线抗干扰能力强工业现场电机变频器一大堆串口走线长一点就容易丢数据CAN虽然也会受干扰但物理层和纠错机制要可靠得多。三是F28335自带eCAN模块不需要外扩CAN控制器硬件成本只增加一个CAN收发器芯片。当然CAN也有它的麻烦单帧最多8字节载荷传输效率低升级一个几百KB的固件往往要传几万帧协议必须仔细考虑分包、编号和重传机制。这些细节在后面的协议设计章节里展开。结论就是如果设备本身就有CAN总线接口用CAN做升级通道几乎是零成本的最佳选择。2. Bootloader分区与CAN协议升级过程的核心2.1 Flash分区给升级腾出安全空间下面是我最终确定的Flash分区表实际工程的CMD文件就是照这个写的区域起始地址结束地址用途App区0x3000000x32FFFF用户业务代码192K字参数区0x3300000x330FFF升级标志、版本记录4K字预留区0x3310000x337FFF备用参数或第二份App区28K字Bootloader区0x3380000x33FFFFBootloader代码32K字这个布局的核心思想是Bootloader和App物理隔离。App区放在Flash开头编译App时CMD文件里要把代码段、数据段的起始地址改到0x300000。参数区单独摘出来是因为升级过程中要频繁读写标志位如果和App或Bootloader混在一起擦除操作一旦越界后果非常严重。为什么Bootloader放在H扇区而不是A扇区原因是F28335的Flash boot模式依赖Flash末尾的引导入口信息。把Bootloader放在Flash末尾扇区链接时可以让其入口点自然落在boot ROM跳转逻辑所要求的位置省掉了很多手动搬移入口符号的麻烦。另外32K字的H扇区放一个小型Bootloader绰绰有余我最终的Bootloader代码编译出来不到20K字。2.2 CAN升级协议设计每一帧都要有明确身份因为CAN数据场只有8字节承载能力有限协议设计要同时兼顾传输效率和出错恢复能力。我的做法是引入“块”和“帧”两级概念帧CAN总线上的物理传输单位每次发送8字节。块上层逻辑上的数据单元一块固定64字节由16个CAN帧组成。CAN数据帧的8字节数据场格式如下字节0字节1字节2字节3-6字节7块号高8位块号低8位帧内索引4字节有效数据累加和每帧携带4字节有效数据块号支持65536块按64字节一块算理论最大可以支撑4MB固件完全覆盖F28335的应用场景。帧内索引从0到15用来标记这是本块的第几帧。最后一字节累加和对前面7字节求和用于快速校验。不同功能的帧用CAN ID区分我只用了五个ID尽量保持简单CAN ID方向含义0x100上位机 - 设备升级握手请求0x101设备 - 上位机握手应答0x102上位机 - 设备固件数据帧0x103设备 - 上位机块接收确认/状态通知0x104上位机 - 设备升级结束/跳转命令0x105上位机 - App请求进入升级模式握手帧负责建立连接数据帧按前面表格连续发送块确认帧由设备在收完完整一块数据后回复上位机只有收到确认才发下一块否则超时重发。这套“停等协议”传输速率不算最高但极大简化了调试难度对出厂升级场景来说完全够用。需要补充的是块内累加和只用于快速排查CAN帧传输出错最终固件完整性还需要一个全局校验。我在结束帧里传了整个固件镜像的CRC32值Bootloader收完所有数据后对整个Flash区域的App代码重新算一次CRC32不一致就报错并保留升级标志不执行跳转。2.3 Bootloader状态机上电到进入App的关键路径Bootloader的主流程可以归纳成几个状态系统初始化关看门狗、初始化时钟、初始化eCAN、配置邮箱。读取参数区升级标志如果标志为0x5A5A说明上位机要求升级进入升级等待状态否则走跳转逻辑。升级状态等待接收握手帧握手成功后打开CAN接收中断开始接收固件数据帧。每收满一块校验累加和把数据写到App区对应位置写完后回一个块确认帧。所有块收完之后收到结束帧对整体固件再做一次CRC校验校验通过就把升级标志清掉并跳转App。跳转状态关闭中断重置PIE向量表重新配置看门狗通过函数指针跳转到App入口地址。状态机的核心原则是任何异常都要在有限时间内退出不能卡死在某个等待状态。我在Bootloader里加了超时计数如果连续3秒收不到任何有效帧就自动复位重新进入跳转逻辑避免设备因为通信中断而一直挂在升级模式上这也是防止“假砖”的重要保护机制。3. 上位机及固件打包工具开发文件解析与升级控制3.1 上位机技术选型与功能拆解上位机是整套方案里最“重”的一环因为要干的事不少加载编译生成的固件文件、连接CAN分析仪、配置波特率、发送固件、处理应答重传、显示进度、记录日志。我用C#在WinForm上做了一版主要是因为现场电脑基本都是Windows环境而且使用的某型号CAN分析仪官方SDK提供了C#接口开发起来顺手同时也在Linux上搭过一版Qt的逻辑完全一样只是换了一层传输接口。上位机界面功能拆开看其实只有四个模块CAN设备管理、固件文件管理、升级控制、状态日志。文件管理和升级控制是核心CAN设备管理负责打开关闭设备和设置波特率状态日志把所有收发帧和异常信息打印出来方便后期去现场排查问题。3.2 固件文件解析与打包不要直接翻hex用CCS编译完F28335工程默认产出的是.out文件它带符号表和各种调试信息不能直接用于CAN传输。实际用于升级的二进制数据需要从.out里提取出来。选择方案时我对比过两条路直接用工具把.out转成Intel HEX上位机再解析HEX逻辑不算复杂缺点是HEX的记录带地址和校验数据密度低解析的时候要处理很多细节。用脚本直接读取.out里的代码段RawData按“段起始地址 段长度 段数据”打包成自定义的镜像文件CAN发送时按这个镜像顺序分包上位机逻辑最简单。我最终走了第二条路写了个Python脚本在CCS编译完成后自动调用它把.out解析成自定义二进制镜像。镜像文件头部是段表每个段记录起始地址、长度、数据CRC后面紧跟原始数据。上位机读这个镜像文件按段信息依次发送Bootloader则根据段地址把数据写到正确位置。这样做还有个好处如果App工程未来增加了自定义段只要脚本里识别新段名并保留地址段即可升级协议本身不用动。这里有个容易踩的细节F28335的Flash以16位字为最小单位而编译出来的.out里代码是按字组织的Python脚本打包时要把字节流按小端方式转换成字数组。这个转换如果搞错会出现“程序能写进去但一跑就飞”的诡异现象排查半天找不到原因。3.3 升级控制的交互逻辑能重试就绝不轻易失败上位机升级的主流程是这样的点击“开始升级”后先发握手帧等待设备应答握手机制超时3秒自动重试3次3次都失败就提示检查CAN连接。握手成功后上位机按顺序取出固件镜像的每一个段逐块下发。每发完一块数据上位机在4秒内等待块确认确认超时就重发整块最多重发5次仍失败则中断升级并记录失败原因。进度条不是简单按文件百分比而是按“已下载字节数除以镜像总字节数”计算。这里有个很实用的细节分段结构下段头和段数据要分开统计进度才能算准。第一次写的时候我拿文件总长度直接算结果某个段的头部信息没算进去进度条到98%就不动了看起来很唬人其实早就发送完了。排查后才发现是段表长度没计入总长度。4. 核心代码实现Flash擦写、跳转与App配合4.1 Bootloader的Flash擦写与跳转代码写Bootloader最险的一步是擦写Flash。F28335片上Flash的擦除和编程必须调用Flash API而且Flash API不能放在Flash里执行。原因很简单擦写过程中Flash控制器要操作当前代码所在的同一个Flash存储阵列如果CPU还在Flash里取指令就会直接卡死甚至触发异常。正确做法是把Flash API相关的代码段链接到RAM中执行擦写时在RAM里跑完整个流程。我用的是芯片原厂Flash API静态库模式在Bootloader工程里把Flash API库添加进来通过CMD文件把库函数的代码段分配到RAM段。关键代码如下// 擦除一个扇区sector对应0到7起始地址按前面的分区表算 void erase_flash_sector(Uint16 sector) { // Flash_Erase的第一个参数是要擦除区域的起始地址 // 每个扇区32K字A扇区起始地址为0x300000 Uint16 *sector_addr (Uint16 *)(0x300000 sector * 0x8000); Flash_Erase(sector_addr, 1); // 擦除1个扇区 } // 把一块数据写入Flash Uint16 program_flash_block(Uint32 dest_addr, Uint16 *src, Uint16 len) { return Flash_Program((Uint16 *)dest_addr, src, len, NULL); }擦写时的一个教训是Flash_Erase一次尽量只擦一个扇区不要图省事写一个遍历所有扇区的循环。如果擦写过程中发生掉电或总线干扰中间某扇区挂了整个升级就失败。分批擦除的好处是每擦完一个扇区可以给上位机回一个进度状态万一出错也能定位到具体扇区方便排查。跳转代码的实现逻辑是跳转前一定要恢复一个干净的环境包括关总中断、清中断标志、恢复默认PIE向量表、复位外设。否则App里中断一触发就可能跳到错误向量上App直接异常。跳转本身用函数指针完成void jump_to_app(void) { Uint32 app_entry 0x300000; void (*entry)(void); IER 0x0000; // 关中断 IFR 0x0000; InitPieVectTable(); // 恢复PIE向量表为默认值 entry (void (*)(void))app_entry; entry(); // 跳到App入口 }这里跳的是App工程的入口地址也就是C运行环境初始化入口而不是main函数。为了让Bootloader准确知道这个入口我在App工程的map文件里查到实际入口符号再在跳转前通过镜像头部信息校验一下。如果App不是从0x300000开头编译的跳转前会先校验镜像段表里的入口地址再跳避免跳到错误位置。4.2 App端需要配合做的事App端改造相对少但有一件事绕不开必须能把系统主动拉进升级模式。我用的方案是在App里监听一个自定义CAN ID当上位机需要升级时先发一条升级请求给AppApp收到后把升级标志0x5A5A写入参数区然后主动软复位。复位后Bootloader看到标志存在就进入升级模式不需要用户短接任何跳线现场操作非常省事。App的CMD文件也要跟着动。CCS默认的CMD文件把Flash段放在0x300000这没问题但如果Bootloader占用了H扇区App的Flash段范围就要明确限制在0x300000到0x32FFFF之间防止链接器把某些段溢到Bootloader区。我在App工程里把Flash段裁剪成Bootloader留给App的地址范围编译完成后检查map文件确认最大使用的Flash地址没有越界。App里写升级标志同样要用Flash API也要在RAM里执行。软复位可以采用喂一个不完整的看门狗触发复位或者直接调用软件复位指令。看门狗复位最省事因为Bootloader启动时第一件事就是重新初始化正好把看门狗接管过来。4.3 上位机核心状态机实现思路上位机发送逻辑本质是一个状态机我用一个枚举字段保存当前状态配合计时器做超时控制。状态迁移简化为enum UpgradeState { Idle, // 空闲 HandshakeSent, // 已发握手帧 HandshakeReceived, // 握手成功 SendingBlock, // 正在发送一块数据 WaitingBlockAck, // 等待当前块的确认 SendingEnd, // 发送结束帧 Verify, // 等待最终校验结果 Complete, // 升级完成 Error // 错误状态 }发送一块64字节数据的流程是先把块内16个CAN帧逐个组装按帧内索引0到15发送发完整个块后进入WaitingBlockAck状态开启4秒计时器收到确认帧就切换到SendingBlock状态发下一块超时未收到则重发当前块重发计数超过5次进入Error状态。这块逻辑虽然简单但把超时处理和重发计数提前画成状态图后代码写起来会非常清晰也不容易漏状态。5. 现场调试与常见问题踩过的坑和解决办法5.1 在Flash里跑Flash API导致死机的完整排查第一次联调时Bootloader刚进升级模式就死了。现象是上位机握手成功开始发送第一块数据收到第一个擦除命令后设备完全无响应看门狗都没拉回来。排查过程很快定位到问题Flash API库虽然被调用了但库函数对应的代码段还在Flash里压根没有搬到RAM。改完CMD文件之后在map文件里确认Flash API所有相关段都落在RAM区才恢复正常。这件事的教训是不要只检查自己的代码放在哪里还要检查库函数的段在哪里编译完必须打开map文件核对一遍。5.2 CAN波特率配错导致的“看似不通”F28335的eCAN波特率取决于外设时钟、BRP预分频和位段寄存器配置。有一次我把通信参数从500Kbps改成250Kbps上位机界面也改好了但设备端忘了改结果用分析仪抓总线全是错误帧。折腾了近一个小时才发现设备端还是老配置。控制器和收发器其实都正常但两边的位时序不匹配CAN控制器会一直报错。后来我把波特率配置做成一个带参数说明的公共函数设备端和上位机的配置值只能从一个配置文件里拷贝从根上杜绝了两边不一致。CAN总线的终端电阻也是绕不开的坑。实测发现短距离桌面联调不接120欧姆终端电阻也能通但把CAN线延长到几十米后波形边沿明显变差偶发错帧。给总线两端各接一个120欧姆电阻后问题消失。这点很多刚入门的朋友容易忽略一定要提前规划好。5.3 升级到一半意外断电的“变砖”与自救最担心的事是升级中途断电。第一次模拟这种场景时升级到一半直接断电重新上电后设备完全没有响应——App区只写了一半Bootloader发现升级标志还在继续尝试接收数据但上位机已经不会主动发握手帧了于是设备卡在升级等待里。这不算真砖解决办法是上位机重新连接CAN设备后主动多发几轮握手帧把设备重新拉回升级流程然后把固件完整重发一遍即可。为了彻底避免“升级一半App废了”的情形我还加了一道保险Bootloader跳转前对App区的关键数据做CRC校验CRC不对就不跳转。同时升级流程里严格执行“先擦目标区、再写新数据、最后清标志”的顺序保证断电重启后至少能进入升级模式而不是去执行半残的App。如果工程上Flash空间足够更保险的做法是搞双App区写入时写备份区成功后切换标志这次因为扇区紧张没有做但方案设计上已经预留好了位置。5.4 交付文件说明一套能真正交接的方案代码写完、功能调通之后真正让方案可交付的是工程文件的组织。下面是我们最终交付时的目录结构/bootloader_工程Bootloader完整工程含CMD文件和Flash API库/app_工程用户App工程含修改后的Flash段CMD文件/host_tool上位机源码含CAN设备驱动封装层/tools固件打包Python脚本、镜像头部生成工具、HEX转换脚本/docs/通信协议文档.mdCAN帧格式、升级流程、异常码定义/docs/使用说明书.md上位机操作说明、现场升级操作步骤/docs/常见问题.md调试中遇到的现象和解决方法这份文件说明看着简单实际上是我最想强调的部分。见过太多项目代码写得很漂亮但交付时文档一团糟后面接手的同事等于从零开始猜协议。特别是CAN通信协议文档一定要把每个帧的字节位定义、每个ID的功能、所有状态迁移条件写清楚哪怕只是为了自己维护过三个月再回头看也能快速上手。6. 经验总结与可扩展方向6.1 三个最值得沉淀的工程习惯这套CAN升级方案从设计到最终跑通大概花了两周多时间真正写代码的时间不到六成剩下全在排查各种“看似不可能”的问题。整个过程最深的体会是升级这种功能设计阶段想清楚内存布局和通信协议比最后把代码调通更重要。分区定不下来后续所有代码都要返工协议没定清楚上位机和Bootloader会互相甩锅。其次是文档与代码同步走。通信协议文档不是最后补的而是在定义帧格式当天就写好初版之后每次改协议立刻同步。这么做的好处是调试时随时有人能查“这个ID到底是干什么的”不会出现口口相传的信息断层。最后是保留诊断接口。升级功能上线后我在Bootloader里留了一个诊断模式入口通过特定CAN ID发命令可以查询当前Bootloader版本、App CRC结果、上次升级失败原因等。这个设计在现场排障时帮了大忙很多问题不用接JTAG就能远程确认状态强烈建议做在线升级的朋友都预留类似接口。6.2 后续扩展建议如果要在现有方案上继续扩展我有几个方向上的建议。第一是加双备份App区升级时先写备份区写入成功后再切换标志这样即使升级过程断电下次启动仍然可以运行上一版固件体验会好很多。第二是把传输层抽象出来现在CAN传输和升级逻辑耦合在一起如果未来换用串口或以太网升级只需要替换传输层实现升级状态机完全不用动。第三是固件加密对镜像文件做异或或AES混淆防止固件被轻易抄出去尤其是设备交付给最终用户后这一步会越来越重要。代码之外我更想传递的是这类功能背后的设计思维先把升级链条上的每个环节想清楚再从第一行代码开始动手成功的概率会高很多。希望这篇分享能给正在做DSP在线升级的朋友带来一些帮助。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑