news 2026/10/19 4:34:29

DSP28335 CAN在线升级方案:Bootloader与上位机从零到落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DSP28335 CAN在线升级方案:Bootloader与上位机从零到落地

前阵子整理了一套基于TMS320F28335(下面直接叫DSP28335)的CAN在线升级方案,bootloader、上位机、通信协议和辅助脚本全部自主开发。起因是手头一个电机控制器项目,设备装到现场之后要升级固件,拆壳插仿真器根本不现实,客户提的要求很直白:通过现成的CAN口把新程序刷进去,而且刷写工具要能对接他们的产线流程。

这套方案从协议定义、Flash分区、Bootloader代码到上位机界面,前后改了四五个版本,中间还经历过升级过程中掉电导致板子起不来的险情。我把它整理成这篇文字,重点讲方案是怎么搭起来的、协议为什么这么定、Flash资源怎么分配、工程文件怎么组织,以及实测下来遇到的那些坑。给正在折腾DSP28335或者其他C2000系列CAN升级的朋友一个完整可参考的范本。

1. 为什么放弃仿真器烧写,坚持自己搭CAN升级

1.1 现场升级的真实场景

我手头的项目是车载电机控制器,设备装到整车或者设备机柜里之后,外壳是密封的,电气接口只引出电源、CAN、旋变等接插件。以前改程序要拆盖子、找调试口、接仿真器,一次至少半小时,而且生产装机后基本留不出调试口。客户的要求是能不能不做任何拆解,通过现成的CAN口把程序换了。这就是CAN在线升级方案的核心动力。

这种情况在工业设备里特别常见。设备已经交付到客户现场,固件有问题或者要加功能,不可能让客户把设备寄回来,也不可能派工程师带着仿真器出差。CAN口是控制器最常见的通信接口,只要协议和引导程序设计得当,它就是一个天然的升级通道。

1.2 全自主开发到底图什么

一开始我也考虑过用芯片原厂的ROM引导,或者买第三方的CAN刷写工具。后来发现都不理想。原厂ROM的CAN引导功能虽然存在,但帧格式固定、校验简单、没有应用层应答重传机制,对接产线自动化流程很吃力;第三方刷写工具则是黑盒,协议不开放,固件格式受限,还涉及授权费用。

自主开发虽然前期投入不小,但协议自己定、上位机自己写,想要什么功能都能往上加。我后面陆续加了批量升级、掉线续刷、和诊断服务集成,这些在项目验收和售后阶段都是加分项。对需要长期迭代的控制器产品来说,这个投入非常划算。

1.3 方案适用范围

这套方案适合这几类场景:DSP28335/C2000系列芯片的板子,产品已经预留CAN接口,需要频繁升级固件,或者希望在产线和售后都用同一套刷写工具。只要Bootloader和上位机是同一套代码体系,后期移植到其他C2000型号也只是改改Flash地址、调调寄存器配置。

2. 升级链路全景:一次CAN刷写的完整流程

2.1 系统组成和角色分工

整套系统由四个部分组成:

  1. 上位机(PC端程序):负责解析固件文件、组帧、发送命令、显示进度。
  2. USB-CAN适配器:把PC的USB数据转成CAN帧,市面常见适配器SDK接口大同小异。
  3. 目标板上的Bootloader:常驻Flash,负责擦写App区、接收固件、校验并跳转。
  4. 目标板上的App:业务程序,收到特定命令可软复位进入Bootloader。

四个角色里最重要的是上位机和Bootloader这对关系,它们共同遵守一套通信协议。适配器只是透明的转发层,协议设计时不需要为它做特殊考虑。

2.2 一次升级的时序步骤

一次完整升级大致走下面这几步:

  1. 上位机打开固件文件,解析出连续数据,计算CRC32和总帧数。
  2. 上位机通过适配器发送握手命令,带一个随机数作为会话标识。
  3. Bootloader应答,回报自己的版本号、Flash剩余空间、最大支持载荷长度。
  4. 上位机发送擦除命令,Bootloader擦除App区,擦完回报成功。
  5. 上位机按帧序号连续发送数据帧,每帧带5字节有效载荷。
  6. Bootloader每收一帧回一个ACK;上位机超时未收到ACK则重传该帧。
  7. 全部数据发完后,上位机发校验命令,Bootloader对写入区重新读回并计算CRC32,与上位机比对。
  8. 校验通过则上位机发送跳转命令,Bootloader恢复现场并跳进App。
  9. App启动后跑一个自检,确认运行正常后再回报上位机“升级成功”。

这个流程看着不复杂,但每一步的细节都决定了升级的可靠性。后面我会把Bootloader和协议的部分单独拆开讲。

2.3 Flash分区规划

F28335片内Flash一共256K×16bit,分成8个Sector,每个32K,地址范围0x300000到0x33FFFF。我的分区是这样的:

区域地址范围大小用途
Sector H~C0x300000~0x31FFFF128KApp主程序
Sector B0x320000~0x32FFFF32KApp扩展/参数备份区
Sector A0x330000~0x33FFFF32KBootloader程序

Sector A的末尾0x33FFF6处放置一条跳转指令,这是芯片上电从Flash启动时的入口。Bootloader启动后先运行一小段自检,然后决定继续待在Bootloader还是跳去App。

提示:不要把Bootloader和App混在同一个Sector里。一旦擦错,Bootloader也会被一起抹掉,板子就只能拆壳上仿真器恢复了。

2.4 进入Bootloader的三种方式

我在实际项目中同时保留了三种进入方式:

  • 上电主动进入:按下升级按钮后,上位机在目标板上电前就持续发送握手命令,Bootloader启动后发现CAN口有合法握手命令,就待命升级。
  • App软命令进入:App收到特定诊断命令后,把一个标志位写入RAM,然后软复位。Bootloader检测到这个标志就不跳App。
  • 启动保护进入:Bootloader先检查App头部魔数和CRC,不合格就直接停在Bootloader等待升级。

三种方式互相配合,既能主动刷写,又能防止板子变砖。

3. Bootloader端实现:入口、驱动与Flash操作

3.1 从0x33FFF6说起的程序入口

F28335的Boot ROM在上电后根据GPIO状态选择启动方式。配置为“Boot to Flash”时,芯片会跳到0x33FFF6读取一条分支指令。所以链接脚本里必须保证0x33FFF6这个地址放的是跳转到Bootloader主函数的指令。我通常用一个位于Flash Sector A末尾的跳转函数来实现:

// 放置在Flash Sector A的末尾区域 __interrupt void BootEntry(void) { asm(" LB _mainBoot"); // 跳转到Bootloader主函数 }

链接脚本里要给这个函数安排固定地址,同时保证0x33FFF6就是这条分支指令的存放位置。这个位置错了,Bootloader根本跑不起来,上电就进非法指令。

3.2 eCAN初始化和500kbps波特率计算

Bootloader里的CAN驱动要尽量精简,不需要像App那样跑完整的协议栈,能用标准帧收发即可。波特率我用的是500kbps,这是工业CAN最常用的速率。

F28335的eCAN模块时钟来自系统时钟SYSCLKOUT。如果SYSCLK是150MHz,要让最终位时间等于500kbps,需要满足:

位时间 = (BRP + 1) × (1 + TSEG1 + TSEG2) BRP + 1 = 15,1 + TSEG1 + TSEG2 = 20 位时间 = 15 × 20 = 300个时钟周期 150MHz / 300 = 500kHz

对应寄存器设置:

ECanaRegs.CANBTC.bit.BRP = 14; // 分频系数15 ECanaRegs.CANBTC.bit.TSEG1 = 12; // 时间段1 ECanaRegs.CANBTC.bit.TSEG2 = 6; // 时间段2 ECanaRegs.CANBTC.bit.SJW = 2;

如果你的板子系统时钟不是150MHz,把公式代进去重算即可。收发双方波特率必须一致,差一点都会导致大量错误帧,最典型的现象就是握手命令发出去完全没回应。

3.3 Flash擦写为什么必须搬到RAM里执行

这是初学者最容易踩的坑。F28335的Flash不能一边擦写一边取指执行,芯片手册明确要求:Flash编程和擦除操作必须在RAM中执行。我一开始图省事,直接把Flash API函数放在Flash里调用,结果每次擦写到这里程序就飞了。

正确做法是把Flash操作函数放到RAM段里运行。在链接脚本里单独建一个RAM运行段,比如放在L0~L4这块RAM里:

ramfuncs : LOAD = FLASH_BOOT, RUN = RAM_L4, LOAD_START(_RamfuncsLoadStart), RUN_START(_RamfuncsRunStart), SIZE(_RamfuncsLoadSize)

配合启动代码把这几个函数从Flash复制到RAM,并在调用Flash API之前把时钟和等待状态配好。F28335在150MHz下必须配置对应数量的Flash等待周期,否则读Flash也会出错。

3.4 升级过程中的保护策略

Bootloader里必须处理三个问题:看门狗、掉电、非法写入。

看门狗的问题很现实:擦除和写入Flash需要几秒,如果看门狗一直在跑,烧到一半就会复位。我的做法是Bootloader启动后直接关闭看门狗,整个升级期间不喂狗,升级完成跳转前再重新初始化。前提是Bootloader本身的逻辑足够可靠,不能有死等分支。

掉电保护靠分区和标志位配合。升级前先把App区开头若干字写成“升级中”的标志,只有收到完整固件并校验通过后,才把“升级完成”的标志写进去。这样即使中途掉电,下次上电Bootloader也能识别出App不完整,不会盲目跳转。

非法写入防护主要面向地址越界。Bootloader必须对每个数据帧的目标地址做范围判断,超出App区直接回错误应答并中止升级。

4. 通信协议设计:帧格式、序号管理和容错机制

4.1 消息ID与数据域布局

CAN标准帧一次最多8个字节数据,必须精打细算。我用了两个CAN ID:0x101作为上位机发给目标的命令帧,0x102作为目标回报给上位机的应答帧。

数据域第一个字节是命令字,后续字节按命令不同解释。命令字定义如下表:

命令字含义数据域剩余字节说明
0x01握手请求会话种子、上位机版本号
0x02握手应答Bootloader版本、最大载荷、Flash容量
0x03擦除请求起始扇区索引
0x04擦除应答擦除结果
0x05数据写入帧序号(1) + 有效长度(1) + 数据(5)
0x06写入应答帧序号 + 写入结果
0x07校验请求无
0x08校验应答本机CRC32(4字节)
0x09跳转请求App起始地址
0x0A跳转应答跳转结果

应答帧的数据域里固定包含“命令回声”和“状态码”。状态码0表示成功,1表示校验错误,2表示地址非法,3表示序号不连续,4表示Flash操作失败。上位机拿到状态码就知道下一步该怎么处理。

4.2 数据分帧有效载荷

一帧CAN数据最多8字节,扣掉命令字、序号、长度,留给实际固件数据的空间就是5字节。我一开始也纠结过要不要省掉长度字段多塞两字节数据,后来还是保留了。

长度字段很有用:最后一帧往往不满5字节,Bootloader必须知道哪些字节是有效的。而且以后协议要扩展,有了长度字段兼容性会好很多。按5字节一帧算,100KB固件大概是20480帧,在500kbps下数据传送阶段约20秒,这个时间完全够用。

地址不用在帧里传,因为数据是顺序发送的。Bootloader内部按“基地址 + 帧序号×5”计算写入地址,上一帧校验通过后这个地址就是确定的,不会乱。

4.3 ACK/NACK与超时重传

CAN本身只有硬件层的错误检测和重发,不提供应用层确认。所以应用层必须自己做应答。我的策略是:

  • 握手时选择随机数作为会话ID,之后所有数据帧都带会话ID,防止把总线上的其他消息误收进来。
  • 上位机每发一帧数据,等待ACK最多200ms;超时未收到就重传,最多重传3次;3次都失败就停止并发提示。
  • Bootloader收到数据帧后先判断序号是否等于期望序号,不等就回状态码3的NACK,上位机收到NACK主动重发。
  • 发送过程中连续10秒没收到任何命令,Bootloader判定上位机已掉线,放弃升级并进入等待或跳转App。

重传逻辑一定要用状态机实现,不要用阻塞延时,否则上位机界面卡死,轮询式等待还会丢引用帧。

4.4 固件完整性校验

完整性的最后一道闸门是CRC32。固件bin在上位机侧预先算出CRC32,随校验命令下传。Bootloader写完所有数据后,从Flash读回全部App区,重新计算CRC32,两个值相等才允许跳转。

读回校验比写完直接跳多花一点时间,但能挡住绝大多数写入异常。实测100KB固件读回校验大约1秒,这个成本完全值得。

5. 上位机自主开发:从HEX解析到进度管理

5.1 技术选型与模块划分

上位机我最终用的是C# WinForms,开发快、调试方便、对接适配器SDK也简单。如果你更熟悉Python,用带CAN支持的库也可以,但在产线部署和代码保护上不如C#方便。无论用哪种语言,模块划分是通用的:

  1. 固件解析模块:读入Intel HEX或纯bin,输出按地址排好的二进制数据。
  2. 协议层:负责组帧、解帧、维护序号、超时重传。
  3. 适配器驱动:封装USB-CAN适配器的DLL调用。
  4. 界面层:进度条、日志、按钮。

协议层和界面层严格分离,这样以后换个适配器,或者想改成串口升级,只需要改驱动模块。

5.2 Intel HEX解析

编译环境生成的一般是COFF格式的.out文件,通过转换工具可以输出Intel HEX。上位机要做的就是把HEX解析成连续的地址加数据序列。

HEX记录的关键处理逻辑:

  • 记录类型00是数据记录,把地址和数据拼接起来。
  • 记录类型04是扩展线性地址,高16位地址以它为准。
  • 记录类型01是文件结束,停止解析。
  • 解析后按地址从低到高排序,检查连续区间的空洞。补0x00还是补0xFF要看硬件要求,DSP的Flash擦除后是0xFFFF,所以补0xFF更稳妥。

我在上位机里还做了一件事:解析结束后立即和工程导出的bin做一次字节级对比,防止HEX与实际烧录内容不一致。

5.3 发送状态机设计

上位机的发送逻辑不要写成顺序执行的循环,而是设计一个状态机:

  • 空闲态:选择文件、计算CRC、显示固件信息。
  • 握手态:发送0x01,收到0x02后校验会话ID,进入就绪态。
  • 擦除态:发送0x03,等待0x04应答,超时重发。
  • 传送态:按帧序号循环发送数据帧,每帧等待ACK,出现NACK按错误码处理。
  • 校验态:发送0x07,等待0x08并比对CRC32。
  • 跳转态:发送0x09,收到0x0A后提示“升级完成,目标机正在启动”。

每个状态都要有超时处理,也要允许用户手动取消。测试阶段这个功能特别有用。

5.4 日志与界面

日志是升级方案里最容易被低估的部分。我要求每次升级都生成一个带时间戳的日志文件,记录每条命令的收发时间、状态码、重传次数。产线上如果有一块板子刷不进去,拿着日志排查比现场猜快得多。

界面进度条不要按帧序号做线性,因为后面还有校验和跳转阶段。我的做法是把整个升级过程按工作量拆成三段:擦除占10%,数据传送占80%,校验跳转占10%,每一段内部再按帧粒度更新。

6. 工程文件结构与说明

6.1 目录总体布局

这套方案我一直用“按角色分目录、文档和代码同库”的方式组织,整套工程结构大致如下:

DSP28335_CAN_Upgrade/ ├── 01_Bootloader/ │ ├── src/ │ │ ├── main_boot.c │ │ ├── can_boot.c │ │ ├── flash_boot.c │ │ ├── protocol_boot.c │ │ ├── jump_boot.c │ │ └── BootEntry.asm │ ├── include/ │ │ └── boot.h │ ├── cmd/ │ │ ├── Flash_SectorA_lnk.cmd │ │ └── RAM_lnk.cmd │ └── proj/ ├── 02_App/ │ ├── src/ │ ├── include/ │ └── cmd/ ├── 03_Host/ │ ├── HostApp/ # 上位机源码 │ ├── HostTool/ # 固件打包工具 │ └── output/ ├── 04_Docs/ │ ├── 通信协议_V1.0.md │ ├── CAN升级总体设计.md │ └── 升级操作手册.md └── 05_Tools/ ├── hex2bin.py ├── crc32_tool.py └── batch_upgrade.bat

6.2 Bootloader工程文件

Bootloader源码按职责拆成几个文件,职责划分一定要清楚:

  • main_boot.c:上电流程、升级标志判断、状态机主循环。
  • can_boot.c:eCAN初始化、邮箱收发、波特率配置。
  • flash_boot.c:封装Flash擦除、写入、读回校验,内部调用原厂Flash API。
  • protocol_boot.c:协议帧解析、命令分发、应答生成。
  • jump_boot.c:处理全局变量恢复、设置App入口并跳转。
  • BootEntry.asm:启动入口跳转表,保证0x33FFF6正确。

cmd目录下最值得注意的是Flash_SectorA_lnk.cmd。Bootloader工程必须限制自己的代码只落在Sector A,App的入口和中断向量表由App工程自己管理。两块地址千万不要有交集,否则App写进去就把Bootloader覆盖了。

6.3 上位机与脚本

03_Host目录下的HostApp是完整的上位机工程,HostTool里是我写的辅助工具:把编译产出的固件打包成带版本头和CRC的升级包。升级包格式是自己定的:

[固件头 16字节] 魔数(2) + 版本号(2) + 目标地址(4) + 总长度(4) + CRC32(4) [固件体] 对齐后的bin数据

05_Tools目录下的脚本主要用于产线:hex2bin.py负责格式转换,crc32_tool.py负责单独计算校验值,batch_upgrade.bat可以循环对一批设备执行升级。

6.4 固件生成流程

一个App从源码到可以CAN升级,走的是这个流程:

  1. 在App工程里编译生成.out。
  2. 用编译链自带的HEX转换工具把.out转成Intel HEX。
  3. 上位机加载HEX后自行转成bin,并计算CRC32填进升级包头。
  4. 库内固化“加载HEX→解析→计算CRC→待发”流程,产线操作人员只需要选文件和点开始。

建议把“生成升级包”做成脚本固化,不要在产线上临时手工操作,减少人为出错。

7. 实测数据与踩坑记录

7.1 实测性能

以我这个项目为例,App固件大约96KB,Bootloader代码约6KB,运行在150MHz系统时钟、500kbps CAN波特率下,实测升级耗时如下:

阶段耗时
握手+擦除约2~3秒
数据传送(约19700帧)约20秒
读回校验约1秒
跳转启动小于1秒

整包刷完大约25秒。这个时间对产线批量升级和售后现场升级都能接受。如果想更快,可以把波特率提到1Mbps,或者把数据帧的有效载荷从5字节加到7字节,代价是协议要跟着改。

7.2 坑点清单

这套方案从头到尾踩过的坑不少,挑几个最有代表性的:

  1. Flash API放在Flash里执行,一调用就飞。解决办法前面说了,必须把Flash操作搬到RAM。
  2. 擦除时看门狗复位。升级期间关狗,但不是关掉就不管了,主状态机的每个超时都要有明确出口,不然板子会静默死掉。
  3. 中途掉电变砖。最严重的一次,现场刷到一半断电,重新上电后App和Bootloader都起不来。这就是分区和启动保护没做好的教训。
  4. 适配器DLL线程模型问题。某些适配器的接收回调在独立线程里,直接往界面控件写数据会闪退。处理办法是回调只进队列,UI线程定时取。
  5. CRC算错但程序没报错。这个问题最隐蔽,原因是HEX解析时补位策略不对,导致bin中间有空洞被补成了0x00,而Bootloader校验收到的是上位机直接发送的CRC,自洽但和实际固件不一致。所以一定要在上位机里冗余核对源文件。

7.3 几个提高成功率的细节

最后分享几个我实测下来对稳定性帮助很大的细节:

  • 升级开始前让Bootloader上报Flash最大可写地址,上位机发送前先校验固件总长度,避免写一半才发现放不下。
  • 数据帧序号用8位循环计数完全够用,因为重传机制保证了不乱序,序号只是为了检测丢帧,不是为了寻址。
  • CAN收发器一定选带唤醒功能的型号,板子上120欧终端电阻尽量靠近连接器。
  • 升级完成后让Bootloader先软复位再跳App,比直接跳转更干净,能把外设寄存器彻底复位干净。

这套方案从协议到代码都是自己磨出来的,过程中交了不少学费,但换来的是产线和现场升级都握在自己手里。如果你也正在做DSP28335的CAN升级,先把协议和Flash分区想清楚再动手写代码,方向上少走弯路,后面会省很多事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 4:34:27

基于NIOS II软核的FPGA数字时钟设计与SOPC实践

简介:本资源是一份完整的高校数字电子技术课程设计项目,面向嵌入式系统与FPGA初学者,聚焦SOPC架构下的数字电子时钟功能实现。项目基于Quartus II开发环境与NIOS II软核处理器,采用Verilog硬件描述语言设计,适配DE2-11…

作者头像 李华
网站建设 2026/10/19 4:34:04

Open Code Review:一套开放透明的代码评审实践方案

代码评审这事儿,干得好是团队的质量护城河,干不好就是互相甩锅的修罗场。我见过太多团队,号称在做 Code Review,实际就是把 MR 链接丢群里,半天没人理,最后合并按钮被催急了才被点掉。也有团队把评审搞成了…

作者头像 李华
网站建设 2026/10/19 4:34:02

open-code-review:用规则引擎驱动的高效代码评审工作流

1. 项目概述:为什么做 open-code-review聊到 open-code-review,先说个我在工作里经常遇到的场景:代码提交上去了,CI 全绿,测试也过了,结果合并之后没两天,同事突然在一片新改动里发现一个隐蔽的…

作者头像 李华
网站建设 2026/10/19 4:33:59

小波+原型增强Transformer:工业像素级缺陷检测实战解析

1. 项目概述:一道从顶会论文里走出来的检测方案先把这个方法讲清楚是什么。CVPR 2025 上有一篇工作,做的是一件很实际的事情:工业场景里的像素级缺陷检测。表面缺陷、晶圆划痕、焊缝气孔、织物瑕疵,这些细小的目标往往只有几个像素…

作者头像 李华
网站建设 2026/10/19 4:33:24

Windows下C语言TCP通信实战:从Winsock到多线程服务端

1. 从零拆解一个TCP通信项目的核心骨架Windows平台上用C语言写TCP通信,这个标题看起来简单,但真正动手做过的人都知道,坑一点都不少。我最早接触这块是在一个设备数据采集的小工具上,当时的需求很朴素:一台Windows机器…

作者头像 李华
网站建设 2026/10/19 4:32:14

Android X5 WebView 封装实战:内核初始化、生命周期管理与内存泄漏避坑

简介:这份资源是面向Android开发者的X5WebView基础封装代码,适合需要快速集成腾讯X5内核、处理H5混合开发场景的初中级工程师。包内围绕WebView常用能力做了系统整理:通过OkHttp拦截器与自定义CookieJar实现客户端与H5端Cookie同步管理&#…

作者头像 李华