
说实话给TMS320F28377D做Bootloader这件事我开始是拒绝的。这芯片是双核200MHzFlash有512KB又有DCSM安全模块光看TI的Boot ROM文档就能把人绕晕。但当我真正开始做电机控制产品板子装到机箱里客户一个电话说要改控制参数我就知道没有Bootloader后面的维护成本会高到让人崩溃。这篇文章把我在F28377D上设计串口Bootloader的全过程、完整CMD配置和踩过的坑都写出来了。不保证是最优解但每一步都是实打实跑通的。适合正在做C2000系列产品化、需要实现固件升级功能的工程师也适合对F28377D启动机制和Flash分区还比较模糊的入门者。看完你至少能获得一个可以照抄的框架。1. 为什么F28377D必须做Bootloader先搞懂启动链路1.1 应用场景从量产到现场维护都躲不开我做这个Bootloader的初衷很朴素产品已经交付不能每次升级都拆机接JTAG。F28377D虽然在板子上留了JTAG接口但实际工业现场根本没条件让你扛着仿真器到处跑。有了Bootloader串口或者CAN就能完成固件刷新维护成本直接降一个量级。另外一个很重要的场景是量产烧录。生产线上使用TI的UniFlash批量烧录Bootloader和App效率其实不高尤其是每个板子都要单独烧一遍完整镜像。有了Bootloader之后工厂只需JTAG烧录一次Bootloader剩下全部走串口升级。这样烧录速度快而且烧录程序一致性好。我们实际用921600波特率刷192KB的App大概十几秒就完成比UniFlash快太多。1.2 F28377D上电后的启动链路F28377D上电后的执行流程大概是这样的CPU1先被释放从Boot ROM开始执行。Boot ROM会读取Boot Mode引脚或对应的寄存器配置然后根据不同的模式跳转到不同入口比如SCI Boot、CAN Boot、Flash Boot等。生产环境中我们几乎总是使用Flash Boot。在这个模式下Boot ROM会跳到Flash地址0x080000处也就是Flash的起始位置。0x080000处存放的是谁的代码CPU就跑谁的代码。所以整个Bootloader的核心思想就是0x080000放BootloaderApp放到后面的Flash区域Bootloader通过判断标志决定是直接跳App还是进入升级模式。这里有个关键点容易被忽略F28377D的App入口不是一条固定地址的跳转指令而是一段叫codestart的段。CCS工程模板会自动生成这个段里面放着一条LB _c_int00指令用来跳转到C运行环境初始化。所以我们在做Bootloader时必须把这个codestart段放到指定Flash地址App工程的codestart也要放到约定的App起始地址。后面讲CMD配置时会详细展开。1.3 Bootloader的职责边界设计Bootloader之前一定要想清楚它该干什么、不该干什么。我的原则是Bootloader越简单越好复杂功能尽量全部交给App处理。Bootloader只做这几件事上电后读取升级标志决定进入升级模式还是跳转App。通过串口接收固件数据写入Flash。校验固件完整性。跳转到App。它不应该做的事很多不要在里面跑复杂的文件系统不要做大量的协议解析更不要写业务逻辑。Bootloader一旦做大出问题的概率就成倍增加而Bootloader出问题往往是灾难性的。我见过有人想在Bootloader里加入加密算法和压缩解压最后把自己绕进去了这里强烈不建议。2. 总体设计先把Flash分区和通信协议定死2.1 F28377D的Flash/RAM资源盘点TMS320F28377D的Flash总容量是512KB起始地址0x080000结束地址0x0FFFFF。官方按Sector管理总共16个Sector每个Sector大小32KB其中Bank 0对应Sector 0到7Bank 1对应Sector 8到15。擦除操作的最小单位是一个Sector写操作则按128位16字节对齐执行。RAM方面F28377D总共有200KB左右的RAM分布在M0、M1、LS0-LS7以及全局RAM区域。Bootloader运行时虽然占不了多少RAM但因为Bootloader需要把Flash API拷贝到RAM中执行所以至少要预留一个较大的RAM段做代码执行区。在动手写代码之前把这些资源搞清楚很重要。很多人做Bootloader翻车就是因为在Flash擦写过程中CPU去读了一段已经被擦掉或者正在被写入的Flash内容导致CPU直接跑飞。后面的设计会专门规避这个问题。2.2 Flash分区对齐Sector是底线给F28377D做分区时我强烈建议所有区域都对齐到Sector边界也就是32KB的整数倍。这样擦除时只擦目标区域不会误伤相邻区域。我的实际分区如下区域地址范围大小用途Sector 00x080000 - 0x087FFF32KBBootloaderSector 10x088000 - 0x08FFFF32KB升级标志区 / 参数存储Sector 2 - 70x090000 - 0x0AFFFF192KBApp主程序区Sector 8 - 150x0B0000 - 0x0FFFFF320KB预留 / 备用镜像区Bootloader只占32KB实际上我们的Bootloader在优化后不到16KB非常宽裕。Sector 1专门用来保存升级标志和App的有效信息这个区域在App的正常运行中不会去写只有升级流程才会修改。预留区目前没用到但将来如果要支持双镜像备份和回滚机制这个区域就是A/B镜像的舞台。现在先留着不影响当前功能。App区起始地址是0x090000跳转目标地址也就是它。需要提醒的是一旦分区定了两个工程Bootloader和App的CMD文件就要把这些地址用宏或者常量统一约定好不要在代码里到处飘数字。2.3 串口升级协议帧格式、命令字与校验通信协议是整个Bootloader另一条命脉。我选择SCI串口作为升级通道因为F28377D的SCI外设稳定、实现简单多数产品板上也都会引出串口。波特率我用的是460800实测在一般的连接线缆下非常稳定线材差一点也能扛住。如果想更快921600也没问题但需要保证连接线短、地线可靠。协议帧格式如下// 帧结构帧头 命令 地址 长度 数据 CRC32 // 帧头0xAA 0x55 // 命令1字节 // 地址4字节小端 // 长度2字节小端最多2048 // 数据N字节N 长度 // CRC324字节覆盖命令地址长度数据命令字定义命令值说明CMD_SYNC0x10握手查询Bootloader版本号CMD_ERASE0x20擦除App区指定SectorCMD_PROGRAM0x30写入一帧数据CMD_VERIFY0x40回读校验App区CRCCMD_JUMP0x50跳转App协议交互流程是上位机先发SYNC握手成功后发ERASE擦除整个App区然后按帧发送PROGRAM每帧最大2048字节Bootloader写完一帧后回ACK上位机收到ACK再发下一帧。全部发完后发VERIFYBootloader将Flash中的数据和CRC结果返回上位机对比通过后发JUMP命令Bootloader跳转App。每帧都要等ACK的设计看起来会牺牲一点速度但可靠性大幅提升。尤其是在整个Flash擦写过程中Bootloader需要时间完成操作上位机等ACK反而是天然的保护机制。3. CMD文件配置让链接器把代码放对位置3.1 Bootloader工程的CMD关键段解析CMD文件的作用就是告诉链接器什么段放在什么内存区域。Bootloader工程里最关键的一点是让codestart段落在Flash起始地址0x080000其他代码段紧随其后同时把Flash API相关函数放到RAM中执行。下面是我整理后的Bootloader工程CMD文件为了便于理解删减了F28377D用不到的内存区域MEMORY { /* RAM区域 */ RAMM0 : origin 0x000000, length 0x000400 RAMM1 : origin 0x000400, length 0x000400 RAMGS0 : origin 0x00C000, length 0x001000 RAMGS1 : origin 0x00D000, length 0x001000 /* Flash区域 */ FLASH_BL : origin 0x080000, length 0x007000 FLASH_PARAM : origin 0x087000, length 0x001000 } SECTIONS { codestart : FLASH_BL .text : FLASH_BL .cinit : FLASH_BL .const : FLASH_BL .switch : FLASH_BL .stack : RAMM1 .ebss : RAMGS0 .cio : RAMGS0 .sysmem : RAMGS1 /* Flash API代码段放到RAM运行 */ .TI.ramfunc : LOAD FLASH_BL, RUN RAMGS1, LOAD_START(_RamfuncsLoadStart), RUN_START(_RamfuncsRunStart), SIZE(_RamfuncsLoadSize) }codestart是F28377D上电从Flash启动时最先执行的段里面就是一条跳转到_c_int00的指令它必须严格放在0x080000处。如果你在CCS工程中查看生成的map文件应该能看到codestart的地址就是0x080000。.TI.ramfunc段非常重要。Flash API库的所有函数都要标成ramfunc属性或者用#pragma CODE_SECTION把它们放到.TI.ramfunc段里。因为Bootloader自身运行在Flash中而Flash API要做的事情恰恰是擦除/写入Flash。如果在擦写Flash的过程中CPU去取一条还在Flash里的指令而那个区域恰好正在被擦掉CPU直接取指失败。解决方式就是把这些函数在启动时拷贝到RAM中执行。拷贝的代码在main函数开头用memcpy完成配合_RamfuncsLoadStart等链接器生成的符号。这几个符号由CMD文件中的LOAD_START、RUN_START、SIZE自动生成不需要手动定义。3.2 App工程CMD如何避开BootloaderApp工程的CMD文件同样重要而且更容易出错。App不能覆盖Bootloader的Flash区域也不能在RAM使用上和Bootloader起冲突。好消息是跳转到App之后Bootloader所占用的RAM可以被完全覆盖因为App会重新初始化自己的运行环境。App工程的CMD文件中主要改动如下MEMORY { RAMM0 : origin 0x000000, length 0x000400 RAMM1 : origin 0x000400, length 0x000400 RAMGS0 : origin 0x00C000, length 0x001000 RAMGS1 : origin 0x00D000, length 0x001000 RAMGS2 : origin 0x00E000, length 0x001000 RAMGS3 : origin 0x00F000, length 0x001000 FLASH_APP : origin 0x090000, length 0x030000 } SECTIONS { codestart : FLASH_APP .text : FLASH_APP .cinit : FLASH_APP .const : FLASH_APP .switch : FLASH_APP .stack : RAMM1 .ebss : RAMGS0 .cio : RAMGS0 .sysmem : RAMGS1 .TI.ramfunc : LOAD FLASH_APP, RUN RAMGS2, LOAD_START(_RamfuncsLoadStart), RUN_START(_RamfuncsRunStart), SIZE(_RamfuncsLoadSize) }App的codestart要放在0x090000因为Bootloader跳转时就跳到这个地址。你可能会问Bootloader跳过来时codestart里是什么和Bootloader自己的启动过程一样codestart是一段跳转到_c_int00的指令App正是在这里完成C环境的初始化。CMD文件是Bootloader和App之间的隐藏契约。建议在两个工程中都加上编译期检查比如在代码里定义一个常量APP_START_ADDRBootloader在跳转前用它来校验当前地址App工程里用它来确认链接器指令位置防止改CMD时两边没同步。3.3 两个工程RAM段不冲突的注意点F28377D的双核架构下CPU1和CPU2各有自己的RAM和共享RAM。默认情况下CPU1的局部RAM段从0x000000开始全局共享RAM段从0x00C000开始。Bootloader跳转前虽然不需要刻意清理RAM但有一个细节必须注意App的.stack段不要和Bootloader跳转前正在使用的栈区域完全错开以免App初始化前压栈出问题。实操中两个工程都使用RAMM1作为.stackRAMGS0作为.ebss这在跳转时没有问题。因为Bootloader通过函数指针跳转时压栈在栈顶而进入App的codestart后第一条指令就是跳到_c_int00它会重新初始化SP。只要SP所指的内存物理存在就不会出问题。唯一的坑是如果你在Bootloader中用了大量全局变量并且这些变量所在的RAM区域被App的.ebss覆盖跳转瞬间会怎样答案是没问题因为Bootloader已经不再使用这些变量了。但如果Bootloader的某些外设中断在跳转瞬间还在产生中断向量表又在RAM中而App还没重新初始化PIE就可能触发一次异常。所以跳转前关全局中断、清外设中断标志非常必要这个在第4节会详细讲。4. 关键代码实现接收、擦写、跳转的完整闭环4.1 Flash API的正确使用姿势TI在C2000Ware里提供了F2837xD的Flash API库实际上是一组带ramfunc属性的C函数。这组函数不能直接从Flash里调用必须拷贝到RAM。使用流程分三步。第一步在链接层面把Flash API代码段放到.TI.ramfunc这个上面CMD已经做了。第二步在代码中声明外部符号并拷贝extern uint16_t RamfuncsLoadStart; extern uint16_t RamfuncsLoadEnd; extern uint16_t RamfuncsRunStart; void initFlashRamFuncs(void) { memcpy((uint32_t *)RamfuncsRunStart, (uint32_t *)RamfuncsLoadStart, (uint32_t)RamfuncsLoadEnd - (uint32_t)RamfuncsLoadStart); }第三步调用Flash API执行擦除和编程。这里要特别提醒不同版本的C2000Ware中Flash API函数原型有所区别。我以当前常用版本为例#include FlashAPI_F2837xD.h // 擦除App区Sector 2-7 #include flash_api.h uint16_t eraseAppSectors(void) { uint16_t status 0; uint16_t sectorMask 0; // 按位设置要擦除的SectorSector 2到7 sectorMask FLASH_SECTOR_2 | FLASH_SECTOR_3 | FLASH_SECTOR_4 | FLASH_SECTOR_5 | FLASH_SECTOR_6 | FLASH_SECTOR_7; status Flash_Erase(FLASH0CTRL_BASE, sectorMask, FLASH_WRITE_WAIT_STATE_COUNT); if (status ! STATUS_SUCCESS) { return status; } // 擦除后校验确认全部为0xFF status Flash_VerifyErase(FLASH0CTRL_BASE, sectorMask, FLASH_WRITE_WAIT_STATE_COUNT); return status; }编程函数则按帧写入uint16_t programAppData(uint32_t dstAddr, uint16_t *buf, uint16_t len) { return Flash_Program(FLASH0CTRL_BASE, dstAddr, buf, len, FLASH_WRITE_WAIT_STATE_COUNT); }有两个参数需要特别关注。一个是FLASH0CTRL_BASE这是F28377D Flash控制器寄存器的基地址具体值在头文件里定义直接用宏即可。另一个是等待状态数FLASH_WRITE_WAIT_STATE_COUNT。F28377D在200MHz主频下Flash读取等待状态一般配置为4写入等待状态要参照Flash API手册。如果这个参数设错了Flash操作会不稳定甚至直接失败。我在第一次调试时曾经在200MHz下用过默认的等待状态值3结果擦除偶尔成功、偶尔返回错误排查了很久才发现是等待状态位数不对。后来把每个API调用都打印返回状态才定位到这个问题。建议你用仿真器跟踪一下Flash API的返回值不要盲目信任函数执行成功。4.2 串口状态机与解析流程串口接收不能简单用一个中断然后逐字节进数组就完事了。Bootloader的接收必须设计成状态机才能处理粘包、断帧、错位这些实际串口通信中一定会遇到的问题。我设计的接收状态机如下#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define FRAME_BUF_LEN 2060 // 2048数据 帧头2 cmd1 addr4 len2 crc4 typedef enum { WAIT_HEAD1 0, WAIT_HEAD2, WAIT_CMD, WAIT_ADDR, WAIT_LEN, WAIT_DATA, WAIT_CRC, FRAME_DONE } FrameState; FrameState frameState WAIT_HEAD1; uint8_t rxBuf[FRAME_BUF_LEN]; uint16_t rxIndex 0; uint16_t frameDataLen 0; void sciRxISR(void) { uint8_t ch ScibRegs.SCIRXBUF.all 0xFF; switch (frameState) { case WAIT_HEAD1: if (ch FRAME_HEAD1) { rxBuf[0] ch; frameState WAIT_HEAD2; } break; case WAIT_HEAD2: if (ch FRAME_HEAD2) { rxBuf[1] ch; rxIndex 2; frameState WAIT_CMD; } else { frameState WAIT_HEAD1; // 重新等帧头 } break; case WAIT_CMD: rxBuf[rxIndex] ch; frameState WAIT_ADDR; break; case WAIT_ADDR: rxBuf[rxIndex] ch; if (rxIndex 6) // 2 1 4 7? 这里根据实际索引调整 frameState WAIT_LEN; break; // ... 后续按帧结构推进 } }这个状态机的核心思路是任何一字节不符合当前状态就回到WAIT_HEAD1重新开始这样可以保证即使上位机发来一堆垃圾字节也能在正确的AA 55帧头出现时恢复同步。在接收数据帧时我还会维护一个接收超时计数器。如果超过一定时间没有收到下一字节就认为当前帧已经结束丢弃并回到初始状态等待新帧。这个细节在SDRAM式的调试中看起来没什么但实际现场线缆干扰、静电跳变都可能造成数据中断。没有超时机制Bootloader会在WAIT_DATA状态卡死后续所有命令都无法响应。代码里的rxIndex 6这类边界判断容易写错建议用结构体解析而不是靠裸数组索引。我工作里常用的方式是定义一个Union把收到的字节流直接映射到帧结构体上既清晰又不容易下标越界。4.3 从Bootloader安全跳转到App跳转看起来就是调用一个函数指针但实际操作中有很多现场才爆的坑。我的跳转函数长这样#define APP_START_ADDR 0x090000 #define APP_ENTRY_MODE_BIT 0x1 // C28x函数指针最低位 void jumpToApp(void) { // 1. 关闭全局中断和PIE DINT; IER 0x0000; IFR 0x0000; PieCtrlRegs.PIECTRL.bit.ENPIE 0; // 2. 禁看门狗App重新配置前防止意外复位 ServiceDog(); WatchdogRegs.WDCR 0x0068; // 3. 关闭Bootloader用到的外设 // 这里以SCI-B为例把所有用过的外设都复位 CpuSysRegs.PCLKCR0.bit.SCI_B 0; CpuSysRegs.SOFRST3.bit.SCI_B 1; // 软复位 CpuSysRegs.SOFRST3.bit.SCI_B 0; // 4. 设置SP指向App约定的栈顶 // 这个栈顶地址要和App工程的CMDB文件中的.stack段保持一致 __asm( MOV SP, #0x000500); // 5. 取入口地址并跳转 void (*appEntry)(void) (void (*)(void))(APP_START_ADDR | APP_ENTRY_MODE_BIT); appEntry(); // 理论上不会走到这里 while(1); }关于第2步禁看门狗有的人会选择在跳转前喂一下狗再离开让App自己接管。我建议禁掉防止App启动过程较长Flash API重新初始化、外设配置等导致看门狗在App初始化中途超时。第4步SP的设置要单独解释。F28377D的C28x内核栈指针在跳转瞬间必须有效。虽然App的_c_int00会重新设置SP但函数指针调用时C编译器会使用当前的SP来压栈保存返回地址和寄存器。如果Bootloader的SP区域和App的初始化代码冲突跳转过程可能压栈压到App要清掉的区域造成返回地址丢失。我踩过一次这个坑现象是跳转后程序跑飞追踪了半天最后发现是SP挂在RAMM1的尾部而App的BSS清零过程把这个区域清成了0导致返回地址被抹掉。所以跳转前手动把SP设置到App的.stack区域就彻底规避了这类问题。APP_ENTRY_MODE_BIT这个最低位也很关键。C28x指令是16位宽字节寻址时一条指令的地址最低位用来标记处理器模式0表示汇编模式1表示C模式。如果你直接拿0x090000当函数指针调用等于告诉编译器这是汇编模式跳过去之后可能执行异常。加| 1这个操作虽然看起来别扭但它是C28x平台上跳转的标准做法。跳转不是终点。App启动后应该主动通过串口发一个APP_READY握手包上位机收到后才能确认升级成功。否则Bootloader跳过去之后上位机不知道App有没有跑起来屏幕上就会一直卡在“等待跳转”这个状态。5. 常见问题与可靠性设计把坑提前填平5.1 问题排查速查表做Bootloader过程中我积累了不少排查经验整理成一张表遇到问题先对号入座。问题现象可能原因解决思路上电后一直停在升级模式不跳AppApp区有效标志没写或者App CRC校验失败确认App区是否正确写入确认Bootloader读取标志的地址与上位机写入地址一致跳转App后死机或复位SP没有预先设置到App栈区跳转前显式MOV SP到App.stack段地址Flash擦除或编程返回失败Flash等待状态配置错误检查FLASH_WRITE_WAIT_STATE_COUNT用手册推荐值擦除过程中CPU跑飞Flash API没有在RAM中执行确认.TI.ramfunc段是否设置正确并完成memcpy串口收到乱码Bootloader和上位机波特率不一致或时钟配置不同统一两端SCIHBAUD/SCILBAUD计算方式建议直接用整数波特率寄存器值升级到一半上位机发下一帧Bootloader没有响应Bootloader在擦写期间中断被关闭后续帧丢失升级协议中每帧必须等待ACK或Bootloader擦写间隔加长等待窗口现场设备升级后App跑起来但功能异常跳转前外设没有复位中断标志残留跳转前对Bootloader使用过的外设做软复位关PIE中断其中“跳转App后死机”和“Flash API执行跑飞”是两个最高频的坑我甚至遇到过一个同事在F28377D上照搬F280049C的Flash API用法结果因为API版本不匹配导致错误。强烈建议每次拿到新的C2000Ware版本都以头文件里的Flash API函数原型为准。5.2 可靠性设计让升级过程“摔不坏”Bootloader设计得再好也无法避免现场遇到掉电、线缆松动、静电干扰。我们要做的不是保证一次都不出错而是出错之后还能恢复到可升级状态。我采用的策略是在任何时刻Flash中都不存在一个“半个App”。具体做法是把“升级完成”的标记放在最后一步。升级流程严格按这个顺序执行上位机发送SYNCBootloader确认通信正常。上位机发送ERASE命令Bootloader擦除App区Sector 2-7。在标志区的upgradeInProgress字段写入“0x0000”表示当前正在升级App不可用。上位机分帧发送PROGRAMBootloader逐帧写入Flash。全部写完后上位机发VERIFYBootloader回读Flash并计算CRC。校验通过后上位机发JUMP命令。Bootloader先更新标志区写入App版本和CRC结果再设置“App有效”标志最后跳转App。如果升级在步骤4或5中掉电下次上电Bootloader检查标志区发现“App无效”就直接进入升级模式等待重新刷写不会尝试跳转一个不完整的App。因为App不足32KB对齐的部分会保留为0xFFCRC校验自然通不过所以不会误判。这个方案不是万能的比如App固件本身有bug导致跑飞Bootloader无法自动回滚但至少能够保证升级过程本身不把设备变成砖。如果要支持回滚可以把预留区做成A/B双镜像这里就不展开了。另一个容易被忽视的可靠性细节是Bootloader自身的喂狗策略。在串口等待升级时Bootloader不能一直喂狗否则看门狗失去意义。但如果长时间不进数据看门狗就会复位整个芯片然后又进入Bootloader等待升级看起来像没有复位一样。这里我建议给升级模式加一个超时自动跳转App的逻辑如果检测到App有效且3分钟内没有收到任何合法帧Bootloader直接跳转App。这套逻辑在实际产品中很管用因为现场工程师发完升级命令后可能忘了关设备等几分钟后设备要能自动恢复运行正常App。写在最后的实操体会如果让我从头再做一次F28377D的Bootloader我会把更多时间花在两件事上一是把CMD文件对齐和跳转栈设置作为优先级最高的检查项二是多花时间把上位机的升级进度显示和错误重传机制写好。底层Bootloader本身代码量并不大真正的复杂度在于两端的时序配合和异常处理。做这个Bootloader过程中我最大的感受是不要迷信网上流传的C2000老系列Bootloader代码直接搬过来F28377D的双核架构、Flash API接口和内存映射细节都变了不少。踏踏实实看一遍TRM里的Boot ROM章节和Flash API文档比什么都强。这套方案现在已经在我的项目里稳定跑了一年多刷过新版固件也有几十次了。如果你正准备给F28377D做Bootloader按我的这个框架走应该能少走不少弯路。