news 2026/9/24 5:48:26

TMS320F28377D双核DSP的SCI在线升级Bootloader方案详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TMS320F28377D双核DSP的SCI在线升级Bootloader方案详解

做DSP开发的大多遇到过这种尴尬:样机在台架上跑得好好的,结果发现固件里有处参数要改,或者算法有个小bug要修。如果是开发阶段还好,抱着仿真器现场改就是了;可一旦设备已经装进机柜、发到现场,或者测试台架不方便开盖的时候,就只能干瞪眼。STM32用户早就习惯了IAP在线升级,但到了TI的C2000系列,尤其是TMS320F28377D这种双核DSP上,很多在ARM上顺理成章的事情都得重新捋一遍——启动流程不一样、Flash编程方式不一样、连接脚本也不是ld文件而是CMD文件。这篇文章就从零开始,把我实际调通的一套Bootloader方案完整拆开,包含Bootloader和App两侧的CMD文件配置、Flash API的用法、跳转逻辑、上位机协议,以及联调时踩过的坑。这里的DSP指的是TI C2000实时控制MCU,不是音频设备里做音效算法的那个DSP,别搜错方向。

这个方案适用于正在做2837x/28377D/28379D相关产品、需要SCI串口在线升级的工程师,也适合从ARM转过来、对C2000启动和存储体系还不太熟的开发者。读完你至少能自己搭出一版能跑的Bootloader,并且知道每一步为什么要这么做。

1. 项目目标与整体设计方案

1.1 这个Bootloader到底解决什么问题

Bootloader的本质是解决“设备出厂后固件怎么更新”的问题。对F28377D来说,最常用的更新方式有三种:仿真器(XDS110)、CAN、SCI串口。仿真器适合开发调试,但现场不可能人人配一个XDS110;CAN适合车载、工控这种本来就有CAN总线的场景;SCI串口则是最简单、最通用的方式——一根USB转串口线就能搞定,几乎所有设备都保留了调试串口。

本文的目标是做一个可靠的SCI在线升级Bootloader,实现以下功能:

  • 上电后Bootloader先运行,检测上位机是否有升级请求,有则进入升级模式,无则跳转App正常执行。
  • 升级模式下,通过串口接收上位机发来的固件数据,按帧写入App区Flash,支持CRC校验,校验失败返回错误码。
  • 升级完成后可以手动或自动跳转App,整个过程中不依赖仿真器。

项目采用Bootloader + App双区方案,物理上将F28377D的Flash划分为两个区域:Boot区存放Bootloader自身代码,App区存放用户应用程序。Boot区是“管家”,平时把CPU交给App,需要升级时接管Flash编程。

1.2 方案选型:为什么用SCI,CAN和网口怎么考虑

选SCI而不是CAN或者以太网,核心原因是通用性和调试成本。绝大部分F28377D的评估板、自制板都会引出SCI(UART)接口,USB转串口模块几块钱一个,115200波特率下传几百KB的固件也就一两分钟,完全够用。而且SCI的驱动实现比CAN简单得多,不需要操心CAN ID过滤、发送邮箱、总线仲裁这些概念,对刚接触C2000的人友好很多。

如果你的产品本身挂在CAN总线上,那用CAN做升级也很合理——传输层从UART换成CAN就行,帧结构、应答、超时重传这些逻辑可以原样保留。2837xD有多个CAN模块,带宽比SCI高,但CAN帧单次最多8字节数据,意味着同样的固件要拆成更多帧,协议层稍微复杂一点。网口方案不推荐,F28377D需要外接以太网控制器,成本和复杂度都上去了,只有极少数高端控制板才这么做。

我最终选SCI,还有一个现实原因:调试方便。串口助手上位机随便写,Python的pyserial几分钟就能跑通协议,出了问题打日志也直观。CAN调试还得挂CAN分析仪,现场不一定有。

1.3 升级流程与状态机总览

整个升级流程可以概括为一句话:上电进Boot、等握手、收数据、擦Flash、写Flash、校验、跳转。细化下来是这样:

  1. CPU上电复位,从Boot ROM启动,最终跳转到0x080000执行Bootloader。
  2. Bootloader初始化系统时钟、GPIO、SCI外设。
  3. 启动一个约500ms的握手窗口,等待上位机发送“进入升级”指令。
  4. 如果500ms内收到握手并校验通过,进入升级模式;否则直接跳转App。
  5. 升级模式下,上位机先发“擦除”命令,Bootloader擦除App区所有sector。
  6. 上位机按帧发送固件数据,Bootloader解析、校验、写入Flash。
  7. 全部数据写完,上位机发“校验”命令,Bootloader读取Flash内容做CRC比对。
  8. 校验通过后上位机发“跳转”命令,Bootloader关中断、跳转App。
  9. 如果中途断电或通信失败,重新上电后Bootloader再次进入握手窗口,可以重新升级。

这个流程的关键点是握手窗口和跳转逻辑。“握手窗口”决定了设备上电后能不能顺利进入升级模式,又不会影响正常启动速度;“跳转逻辑”决定了App能不能稳定跑起来。后面章节会逐个展开。

2. F28377D启动流程与内存分区基础

2.1 上电后代码从哪开始执行:Boot ROM与Boot模式

F28377D上电后,CPU1首先执行的是芯片内部Boot ROM里的固化代码。Boot ROM会根据Boot模式引脚的状态(GPIO72、GPIO73、GPIO84等)决定从哪个介质启动,最常见的三种是Flash启动、SCI启动、CAN启动。

注意一个容易混淆的点:即使把Boot模式引脚配成SCI启动,Boot ROM也只会帮你把代码从串口引导到RAM里运行,并不会帮你烧写Flash。我们要做的Bootloader是常驻Flash的,所以Boot模式引脚配置成Flash启动即可——上电后Boot ROM检测到Flash启动,跳转到0x080000,也就是Flash的起始地址。咱们的Bootloader代码就从这个地址开始跑。

这意味着两件事:第一,0x080000这个地址是硬件决定的,Bootloader必须放在这里,CMD文件里把Boot区的origin设为0x080000就是基于这个原因;第二,App的起始地址不能是0x080000,否则就把Bootloader覆盖了,所以App必须往后挪。

2.2 Flash和RAM资源盘点,空间够不够用

TMS320F28377D的资源对Bootloader方案来说是相当充裕的。Flash从0x080000开始,按sector管理,每个sector大小不同,具体边界以TI官方数据手册为准;RAM分为几类,这里只说和CMD分区直接相关的:

  • M0、M1:各1K words,地址0x000000~0x0007FF,是默认的栈和启动变量区。
  • LS0~LS5:每个2K words,地址从0x008000开始,连续排列,是常用的数据段区域。
  • GS0~GS15:每个4K words,地址从0x00C000开始,GS区默认可以由CPU1和CPU2共享,具体归属通过MEMCFG寄存器配置。

注意这里的单位是word,C28x的word是16位,一个word对应2字节。写CMD文件、计算地址时容易在这里搞混,我一开始就把LS区的长度算错过,导致链接直接报溢出。

Bootloader本身体积很小,一个带串口协议、Flash API、跳转逻辑的Bootloader,编译出来一般不超过8K words。但Flash API库本身会占用一部分空间,加上协议缓冲区、栈、全局变量,给Boot区规划16K~48K words比较合适。App区占剩下的空间,几百KB的固件存放毫无压力。

2.3 双区划分:Boot区、App区、共享RAM区如何分配

我的分区方案如下:

区域起始地址大小用途
Boot Flash0x0800000xC000(48K words)Bootloader代码、常量、Flash API加载段
App Flash0x08C000到Flash末尾用户应用程序
Boot RAM0x000000~0x0007FFM0+M1共2K wordsBootloader栈和少量全局变量
Flash API运行RAM0x008000~0x009FFFLS0~LS3共8K wordsFlash API库运行区域
App RAM0x00A000及之后LS4/LS5/GS区App的栈、数据段

Boot区给48K words是偏保守的,实际Bootloader占不了这么多,留点余量方便以后加功能。App从0x08C000开始,这个地址是Boot跳转目标,App的CMD文件里要严格对应。

RAM划分要特别注意:Bootloader占了M0/M1和LS0~LS3,那App就不能再用这些区域。最稳妥的做法是App只用LS4/LS5和GS区,M0/M1和LS0~LS3全部留作Bootloader专用。曾经有个同事把App的.stack放在LS0,结果Boot跳过去之后程序跑飞,查了半天才发现是RAM区覆盖——App的启动代码把Bootloader运行时的栈区数据清零了,这时候Bootloader还没完全退出。

3. Bootloader工程实现:核心代码逐段拆解

3.1 工程搭建与Flash API库的引入

在CCS(Code Composer Studio)里新建一个空工程,把F28377D的芯片支持包加进去,然后做两件事:第一,在链接器选项里添加Flash API库,C2000Ware里提供的库名类似Flash2837xD_API_F2837xD_Full_Library.lib,要选FULL版本,不要选BootROM版本。第二,在CMD文件里把Flash API的段单独拎出来,放到RAM区运行,具体方法下一章单独说。

有个常见误解:以为Bootloader不需要Flash API库,因为“反正Bootloader最后要烧进Flash”。但实际上Bootloader要实现升级功能,就必须在运行时擦写Flash,而擦写Flash的底层驱动就是Flash API库提供的。没有它,你就只能自己写Flash控制器寄存器操作,复杂度和风险都高很多。TI提供的API库封装了擦除、编程、状态查询等操作,是官方推荐做法。

3.2 串口通信与帧协议解析实现

Bootloader的串口接收我用的是轮询方式而不是中断。原因很简单:擦写Flash期间要关全局中断,如果串口依赖中断接收,关中断后数据就丢了。轮询方式可以灵活控制,在擦写Flash时暂停接收,擦写完成再恢复。

数据结构上,我自定义了一套简单帧协议,帧格式是:

帧头命令长度数据CRC16帧尾
0xAA 0x551字节2字节N字节2字节0x0D 0x0A

命令字暂时定义几个:0x01握手、0x02擦除、0x03写数据、0x04校验、0x05跳转。CRC16用CCITT多项式,数据域从命令字开始算到数据结束。这个协议不复杂,但够用。

接收解析用状态机,避免阻塞。核心逻辑是:

typedef enum { FRAME_WAIT_HEAD1, FRAME_WAIT_HEAD2, FRAME_WAIT_CMD, FRAME_WAIT_LEN_H, FRAME_WAIT_LEN_L, FRAME_WAIT_DATA, FRAME_WAIT_CRC_H, FRAME_WAIT_CRC_L, FRAME_WAIT_END1, FRAME_WAIT_END2 } FrameState; FrameState state = FRAME_WAIT_HEAD1; Uint16 frameLen = 0; Uint16 frameCnt = 0; Uint8 rxBuffer[1024]; Uint16 crcCalc = 0; void SCI_RxProcess(Uint8 byte) { switch (state) { case FRAME_WAIT_HEAD1: if (byte == 0xAA) state = FRAME_WAIT_HEAD2; break; case FRAME_WAIT_HEAD2: if (byte == 0x55) state = FRAME_WAIT_CMD; else state = FRAME_WAIT_HEAD1; break; case FRAME_WAIT_CMD: rxBuffer[0] = byte; state = FRAME_WAIT_LEN_H; break; // 后续状态类似,收满长度后进入数据接收,数据收满算CRC default: break; } }

这里有个实操细节:串口接收缓冲区大小要能容纳一帧完整数据。我建议写数据帧的载荷控制在512字节以内,缓冲区给到1K,既省RAM又不容易溢出。

3.3 Flash擦除与编程封装

Flash擦写是整个Bootloader最核心、也最容易出问题的部分。F28377D的Flash控制器在执行擦除和编程操作时,不能同时从同一块Flash取指令,否则总线访问冲突。这就有个硬性要求:执行Flash API的代码必须放在RAM里运行。

我这里用的Flash API调用逻辑是:

extern Uint16 flashApiLoadStart; extern Uint16 flashApiRunStart; extern Uint16 flashApiSize; void FlashApi_CopyToRam(void) { memcpy(&flashApiRunStart, &flashApiLoadStart, (Uint32)&flashApiSize); } void Flash_EraseSector(Uint32 sectorAddr) { // 擦除一个sector Fapi_issueAsyncCommandWithAddress(Fapi_EraseSector, (uint32 *)sectorAddr); // 等待擦除完成,期间喂狗 while (Fapi_getFapiStatus() == Fapi_Status_Busy) { ServiceDog(); } if (Fapi_getFapiStatus() != Fapi_Status_Success) { // 擦除失败,记录错误码,停止升级 UpgradeError = ERROR_ERASE_FAILED; } } void Flash_ProgramWord(Uint32 destAddr, Uint16 *srcData, Uint16 length) { // 把数据写入Flash,注意length是word个数 // 具体编程函数接口以你用的Flash API版本为准 Fapi_issueAsyncCommandWithAddress(Fapi_Program, (uint32 *)destAddr); // 写入数据,等待完成,检查状态 while (Fapi_getFapiStatus() == Fapi_Status_Busy) { ServiceDog(); } }

注意几个细节:第一,擦除和编程过程中必须关全局中断,否则Flash API状态机可能被中断服务函数打断,导致操作失败;第二,Flash API运行在RAM里,这个RAM区在Bootloader和App之间必须是隔离的,App不能用;第三,擦除一个sector可能要几百毫秒,期间如果不喂狗,看门狗会复位——如果你开了看门狗的话。

关于Flash API的具体函数名,不同版本的C2000Ware可能略有差异,我第一次用的时候也查了半天头文件。建议你以自己电脑上C2000Ware里的头文件为准,把API的调用流程理解成“下发命令、等待完成、检查状态”三步,具体函数对着头文件换就行。

3.4 跳转App前的“清场”操作

跳转不是简单一个函数指针调用就完事,跳转前的状态清理至关重要。如果Bootloader带着一堆中断、看门狗、外设状态跳进App,App很可能跑飞或者被异常中断打断。

我跳转前的清场代码如下:

#define APP_ENTRY_ADDR 0x08C000 void JumpToApp(void) { // 1. 关闭全局中断 DINT; // 2. 关闭PIE外设中断 PieCtrlRegs.PIECTRL.bit.ENPIE = 0; // 3. 清空全部中断使能和标志 IER = 0x0000; IFR = 0x0000; // 4. 关闭看门狗时钟,防止App启动期间被复位 EALLOW; CpuSysRegs.PCLKCR0.bit.WDCLK = 0; EDIS; // 5. 清空串口接收缓冲区,防止残留数据影响App while (SciaRegs.SCIFFRX.bit.RXFFST) { volatile Uint16 dummy = SciaRegs.SCIRXBUF.all; } // 6. 跳转到App入口 void (*AppEntry)(void) = (void (*)(void))APP_ENTRY_ADDR; AppEntry(); }

这里要补充一个C2000特有的概念:App的0x08C000处放的并不是main函数,而是启动代码段codestart,里面通常是一条跳转指令LB _c_int00_c_int00是C运行时初始化入口,它会初始化栈指针、清零BSS段、调用全局构造函数,最后才跳进main。所以Bootloader跳转到0x08C000,实际上跳的是App的启动引导指令,它会自动准备好C运行环境。这也是为什么在CMD文件里要让codestart段落在App区的起始位置。

4. Bootloader的CMD文件完整配置(重点)

4.1 我的CMD文件模板,可直接抄

CMD文件其实是C2000开发中特别容易被低估的一环。很多人Bootloader调不通,最后查下来都是CMD里地址写错或者区域重叠。这里给出我实际使用的Bootloader CMD模板,关键部分都加了注释:

MEMORY { BOOT_FLASH (RX) : origin = 0x080000, length = 0xC000 RAMM0 (RW) : origin = 0x000000, length = 0x400 RAMM1 (RW) : origin = 0x000400, length = 0x400 RAMLS0 (RW) : origin = 0x008000, length = 0x800 RAMLS1 (RW) : origin = 0x008800, length = 0x800 RAMLS2 (RW) : origin = 0x009000, length = 0x800 RAMLS3 (RW) : origin = 0x009800, length = 0x800 FLASH_API_RAM (RW) : origin = 0x00A000, length = 0x1000 } SECTIONS { codestart : > BOOT_FLASH .text : > BOOT_FLASH .cinit : > BOOT_FLASH .switch : > BOOT_FLASH .const : > BOOT_FLASH .stack : > RAMM0 .ebss : > RAMM1 .cio : > RAMLS0 .sysmem : > RAMLS1 .bss : > RAMLS2 .text:FlashAPI : LOAD = BOOT_FLASH, RUN = FLASH_API_RAM, LOAD_START(_flashApiLoadStart), RUN_START(_flashApiRunStart), SIZE(_flashApiSize) }

几个关键点解释一下。

codestart段放在BOOT_FLASH,这是必须的——0x080000处必须是Bootloader的启动指令,因为硬件复位后Boot ROM直接跳到这个地址。.text放BOOT_FLASH没悬念,Flash本身本来就要存代码。

.stack放在RAMM0,.ebss放在RAMM1。Bootloader运行期间栈和全局变量不能放在Flash里,所以必须占用RAM。M0/M1总共只有2K words,Bootloader自己跑跑串口协议、处理几十个变量够了,但别在Bootloader里开大数组。

FLASH_API_RAM区域是专门给Flash API库运行用的。.text:FlashAPI这个段比较特殊,它把Flash API库的代码放在Flash里作为加载副本,但运行时拷到RAM里执行。用LOAD_STARTRUN_STARTSIZE这三个符号把加载地址、运行地址、段大小暴露给C代码,然后调memcpy拷过去。这一手是Flash API运行的基础。

4.2 为什么Flash API必须放进RAM,链接符号怎么用

这里把前面提到的“Flash API必须从RAM运行”用CMD的角度再解释一遍。Flash API库是TI提供的Flash驱动代码,它本身是编译好的目标文件,库里的函数编译时默认链接到某个地址。如果直接把库链到Flash区,那调用擦除函数时CPU会从Flash取指令。但问题是,一旦发出擦除命令,该Flash bank正处于擦除状态,无法响应取指请求,CPU就卡死了——轻则操作失败,重则看门狗复位。

所以必须把API代码从Flash搬到RAM里执行。搬移的动作在代码里做,但搬哪一段、从哪搬过来、搬多长,这些信息就靠CMD文件里的LOAD_STARTRUN_STARTSIZE来告诉编译器生成符号。在C代码里声明这三个外部变量,然后memcpy就能完成搬运。

实际操作中要注意两点:第一,memcpy的size是字节还是word?C2000的memcpy按字节计算,但Flash API段的大小在CMD里默认是word数。我在工程里用(Uint32)&flashApiSize做size,然后memcpy内部自己按字节处理,具体要看编译器设置,最好在拷完后再用反汇编确认一下Flash API函数确实在RAM地址上运行了。第二,拷贝动作必须在第一次调用Flash API之前执行,这通常放在Bootloader的main函数最前面。

4.3 配置完成后如何验证分区是否合理

写完CMD后别急着烧录,先做两步检查,能省一晚上的调试时间。

第一步,编译后在工程目录下打开.map文件,搜索codestart,确认它的地址是0x080000;搜索.text:FlashAPI,确认LOAD地址在Flash区、RUN地址在RAM区(0x00A000附近)。如果RUN地址还在Flash区,说明CMD里分段没生效。

第二步,用CCS的Memory Browser烧完Bootloader后看0x080000处的第一条指令,应该是一个跳转指令。同时确认0x08C000处是0xFFFF(未编程状态),因为那里还没烧App。如果0x08C000处有内容,可能是之前用仿真器烧过东西,或者CMD分区错误把Bootloader写到这里了。

5. App端CMD文件与小技巧

5.1 App工程必须改的几个地方

App工程改起来比Bootloader简单,但容易漏。我总结了一个检查清单:

第一,CMD文件里Flash区起点改成0x08C000,长度按剩余Flash空间写。注意这里不能再叫BOOT_FLASH,我习惯叫APP_FLASH。

第二,App的RAM区一定不能包含M0/M1和LS0~LS3。严格来说,App只要不覆盖Bootloader跳转前还在用的RAM就行,但出于安全考虑,整段隔离最好。我的做法是App的.stack放LS4,.ebss放LS5,其余数据段放GS0~GS15。

第三,codestart段必须放在APP_FLASH的起始位置。编译完App后检查map文件,确认0x08C000处确实是最开始的分支指令。

第四,如果App用了看门狗,注意Bootloader跳转前关闭了WDCLK。App的InitSysCtrl()里面要重新使能看门狗时钟,否则看门狗根本不工作,App的喂狗代码相当于白写。这个坑我帮同事排过一次,现象就是App里开了看门狗但从来不复位,排查了半天才发现WDCLK没打开。

一个常见的疑问是:App需不需要改PIE向量表?不用。C2000的PIE向量表在RAM里,App启动时会重新调用InitPieVectTable(),把向量表填好。Bootloader跳转前已经把PIE关闭了,App启动时重新开启并初始化即可。这和Cortex-M里改VTOR完全不是一个路子,别拿ARM的思路硬套。

5.2 App编译生成升级文件的流程

Bootloader烧录用仿真器一次搞定,之后更新App就不要再用仿真器了,否则Bootloader就失去了意义。App编译后需要转成上位机能处理的格式,一般是Intel HEX。

在CCS里可以用hex2000工具做转换,把.out文件转成.hex。我用的方法是写一个批处理脚本,每次编译完自动转换:

hex2000 --intel -o app.hex app.out

转出来的HEX文件是文本格式,上位机解析时按:开头的数据记录读取地址和字节流,拼出完整的固件数据再按帧发给Bootloader。这里有个C2000特有的字节序问题:Flash是16位word组织,HEX里的数据是字节流,上位机发送时按字节发,Bootloader接收到后每两个字节组成一个word再写Flash。如果高位低位搞反了,写进去的代码就是乱的——不是跑飞,是反汇编全是乱码,看起来特别诡异。

6. 上位机与升级协议设计

6.1 帧格式与校验算法

上位机的设计复杂度不高,但可靠性要重视。帧格式用前面提到的自定义协议:

字段字节数说明
帧头20xAA 0x55
命令10x01握手 / 0x02擦除 / 0x03写数据 / 0x04校验 / 0x05跳转
长度2数据域字节数,小端
数据N命令附带数据
CRC162对“命令+长度+数据”计算CRC16-CCITT
帧尾20x0D 0x0A

每次握手成功,Bootloader返回ACK帧;处理失败返回NAK帧,上位机显示错误码。写数据帧的ACK很重要,它起到了流控作用,上位机只有收到上一帧ACK才发下一帧,避免Bootloader还没写完Flash、上位机就把数据喷了一地。

6.2 Python上位机示例

我平时调试用的上位机是Python写的,配合pyserial库,几百行代码就能跑通。核心发送逻辑大概这样:

import serial import struct import time def crc16_ccitt(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte << 8 for _ in range(8): if crc & 0x8000: crc = ((crc << 1) ^ 0x1021) & 0xFFFF else: crc = (crc << 1) & 0xFFFF return crc def send_frame(ser, cmd: int, payload: bytes = b''): length = len(payload).to_bytes(2, 'little') crc = crc16_ccitt(bytes([cmd]) + length + payload) frame = b'\xAA\x55' + bytes([cmd]) + length + payload + crc.to_bytes(2, 'little') + b'\x0D\x0A' ser.write(frame) def wait_ack(ser, timeout=0.5): # 读取一帧并判断是否为ACK ...

发送固件时,把HEX解析出来的字节流按512字节切块,每块发一帧0x03写数据命令,然后等ACK。如果超时或者收到NAK,重发当前块,连续重发5次失败就退出升级流程。

这里有个实用技巧:握手前最好先尝试打开串口并发送一个简单的查询命令,确认Bootloader处于升级模式再开始传数据。避免你以为是升级模式,实际Bootloader已经跳进App了,发什么它都不理你。

6.3 通讯异常与容错设计

做升级功能一定要假设链路是不靠谱的——串口报文可能丢、可能错、可能半截断掉。容错设计我从三个层面做了:

第一,帧级校验。每帧带CRC16,校验失败直接丢弃,上位机超时重发。

第二,命令级应答。Bootloader处理完一条命令才回ACK,上位机必须收到ACK才发下一条,否则视为超时重发。

第三,流程级保护。擦除Flash之前,Bootloader会发一个“准备擦除”的握手确认,上位机收到确认才发出擦除命令,防止误操作。整个升级过程中,如果连续多帧错误,Bootloader停在升级模式而不是跳App,避免半升级状态下把设备搞成砖。设备重新上电后,Bootloader会再次进入握手窗口,可以重新升级。

实际测试下来,这套容错设计在USB转串口、115200波特率、线长1米以内的场景下,几百KB固件一次成功率几乎是100%。如果线特别长或者干扰大,可以降低波特率到38400,重传率就显著下降。

7. 联调实录:常见问题与排查方法

7.1 现象一:跳转后程序不跑

这是Bootloader联调时最常遇到的现象。跳转执行了,但App没反应,看门狗复位或者停死在非法中断里。排查方向按优先级排序:

第一,确认App的起始地址。打开App的map文件,看codestart段在不在0x08C000。如果App的CMD是从原工程复制来的,很可能忘了改起始地址,代码还在0x080000——那就把Bootloader覆盖了。

第二,确认跳转前关闭了PIE。如果App启动过程中来了一个中断,PIE向量表还没初始化好,就会进非法中断。我在跳转函数里关PIE,就是为了避免这个问题。

第三,检查RAM区有没有重叠。App的.stack.ebss如果占了Bootloader的运行RAM(M0/M1或LS0~LS3),App启动时初始化C运行环境会把这块RAM清零,Bootloader正在执行跳转代码可能已经被破坏了。这是最隐蔽的情况,建议App侧直接用我们规划的隔离RAM区。

7.2 现象二:Flash写入成功但复位后App丢失

如果Bootloader上报写成功,但设备复位后App还是没跑,问题基本出在数据侧。

先怀疑字节序。F28377D的Flash是16位word组织,写入时要保证两字节拼成一个word的方向正确。如果上位机发送的字节顺序和Bootloader组word的方向不一致,写进去的代码就是反的。验证方法:用CCS的Memory Browser查看0x08C000处写进去的内容,跟HEX文件里的前几个字节对照,看是否符合。

再怀疑擦除范围。是不是擦除命令只擦了一部分App区,后面还有旧固件残留在Flash里?擦除命令应该擦除整个App区,包括App区的所有sector。有些sector大小不一致,擦除循环必须遍历所有sector,不要想当然地认为“从起始地址擦到结束地址就行”。

最后检查Flash API的状态反馈。编程函数返回后必须确认状态是Fapi_Status_Success,如果只是发了命令就往下走,可能编程还没完成就复位了,数据实际上没写进去。

7.3 现象三:升级中途死机或看门狗复位

中途中断的排查点主要在Bootloader侧的时序。擦除一个sector需要几百毫秒,这段时间内如果开了看门狗又不喂,看门狗一到时间就复位。解决办法是在擦除和编程的等待循环里喂狗。

另外,擦写Flash期间中断必须关掉。如果串口接收中断在擦写过程中触发,中断服务函数会打断Flash API的操作序列,导致Flash控制器状态异常。这里有个相对稳妥的设计:擦写Flash时关全局中断,擦完再打开,然后处理串口接收。

如果在现场升级时频繁出现这个问题,还要考虑电源因素。Flash擦写瞬间电流需求会变大,如果板上电源余量不足或者线缆压降大,可能导致Flash编程失败甚至芯片复位。我在一块自制板上遇到过,后来在电源输入端加了个大电容就好了。

7.4 常见问题速查表

现象可能原因排查/解决
跳转后无反应App起始地址错检查App map里codestart地址
跳转后进非法中断PIE未关闭 / 中断标志残留跳转前关PIE、清IER/IFR
跳转后运行异常RAM区被App覆盖检查App CMD中RAM分配
Flash写成功但App不启动字节序反了 / 写入地址错Memory Browser查看写入内容
升级中途复位看门狗未喂 / 电源跌落擦写等待循环喂狗,加强供电
擦除失败Flash API不在RAM运行确认.text:FlashAPI的RUN地址
握手成功但传数据失败帧格式/CRC算法不一致上位机与Bootloader逐字段比对
串口收到乱码波特率不匹配确认SCI初始化和上位机设置

8. 双核与量产扩展

8.1 CPU2的固件升级思路

F28377D是双核芯片,CPU1和CPU2各自有独立的Flash。CPU1的Bootloader能管自己,但管不了CPU2的Flash——CPU2的Flash只能由CPU2自己擦写,这是一个硬约束。

如果产品里CPU2也跑了固件,升级方案就得多一层协作。简单思路是:CPU1和CPU2各烧一个Bootloader,CPU1 Bootloader负责通过SCI接收整个升级镜像,里面同时包含CPU1 App和CPU2 App两部分;CPU1 Bootloader先升级自己的Flash,然后通过IPC(核间通信)发送一个“开始升级”的IPC中断给CPU2,CPU2收到后进入自己的Bootloader,CPU1再把CPU2的App数据通过共享RAM或IPC消息转发过去,由CPU2 Bootloader写入CPU2 Flash。

这个方案比单核复杂不少,但原理还是那套:握手、擦除、写Flash、跳转。等单核Bootloader跑通了,再按这个思路扩展即可。

8.2 A/B备份区与回滚设计

本文用的是Boot + App双区方案,升级失败还能重新进Boot再次刷写。但对于某些不能接受现场返工的设备,建议升级到A/B备份方案。

A/B方案的思路是:Flash里放两份App,AppA和AppB。Bootloader启动时根据一个标志位选择启动哪一份。升级时只写当前不用的那份,写完后修改标志位并复位,Bootloader启动新的那份;如果新固件连续启动失败(比如App在main里喂狗超时导致复位,Bootloader检测到连续多次快速复位),自动回滚到另一份。

这个方案的代价是Flash占用翻倍,但换来的是升级过程几乎免疫断电、刷错固件这类问题。F28377D的Flash容量比较充裕,很多控制算法固件只有几十K words,A/B完全放得下。等基础Bootloader稳定后,强烈建议往这个方向演进。

8.3 最后的一点个人经验

回看整个Bootloader的开发过程,最折磨人的不是Flash API调用,也不是上位机协议,反而是不起眼的CMD文件。C2000的CMD机制看起来就几百行,但每一条mapping都直接决定了链接结果的正确性。我最早一次联调,App跳转后跑飞,排查了半天,最后只是App的CMD里把.stack放到了Bootloader的栈区。打那以后,我每次写Bootloader都先在纸上画一张内存分区图,把Flash、RAM、API区各自的位置和大小标清楚,再写CMD,出问题的概率直线下降。

另外,Flash API从RAM运行这个坑,是C2000 Bootloader跟ARM IAP最不一样的地方。ARM的Flash驱动可以在Flash里执行,C2000不行,这个认知一旦建立,很多现象就都能解释了。字节序那个坑也很典型——C2000是16位word寻址,而PC端一切按字节流看,两边的“字”概念不一样,传输和写Flash时一定要对齐。

如果你正在做2837x系列或者其他C2000芯片的Bootloader,建议先把本文的流程走通,再根据你自己的外设和协议需求去改。等这版跑稳了,再去考虑双核协作、A/B区、加密签名这些进阶功能——基础设施打扎实了,上面盖楼才不慌。

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

专知智库·研发增长系统:一套系统,双重合规,三重增长

专知智库研发增长系统&#xff1a;一套系统&#xff0c;双重合规&#xff0c;三重增长研发费用管理&#xff0c;早已超越简单的税务申报范畴&#xff01;在金税四期的严密监管下&#xff0c;税务合规是底线&#xff1b;而对于谋求上市的企业&#xff0c;证监会《第九号指引》对…

作者头像 李华
网站建设 2026/9/24 5:44:25

CSP-J2021 分糖果题解

这题非常简单&#xff0c;其实就if判断一下就好了。针对于每个最大值&#xff0c;有两种情况&#xff1a;1.最大值等于n-1。2.最大值等于r%n。只有(n-1)n<r和(l/n1)*n<r这两种情况才满足n-1就是答案。因为要保证n-1在这个区间内&#xff0c;即l<n-1<r。本题代码&am…

作者头像 李华
网站建设 2026/9/24 5:39:51

Thonny+MicroPython开发ESP32中文UI实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 5:34:33

224.从零精通安卓维修!Bootloader 分区原理 + 救砖实操全教程

摘要 本文从安卓系统启动链的底层原理出发,系统讲解刷机与维修的核心知识,包括Bootloader、分区表、Fastboot协议、Recovery与OTA机制。通过一个真实维修案例,演示如何利用fastboot与adb工具链修复因OTA失败导致无法开机的设备。文章提供完整可运行的Python脚本,用于自动化…

作者头像 李华