ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TMS320F28377D双核DSP Bootloader设计:启动流程与Flash分区

TMS320F28377D双核DSP Bootloader设计:启动流程与Flash分区 最近调试一块基于TMS320F28377D的驱动板被Bootloader折腾得够呛。网上关于这颗芯片启动流程的资料很零散大多是直接讲ST系列或者单核C2000的真要落到双核DSP上尤其是要自己做远程升级坑比想象中多。这篇文章把这段时间摸爬滚打总结出来的东西梳理一遍从启动ROM行为、双核引导关系、Flash分区规划到跳转APP的代码细节以及实测中遇到的各种诡异问题一次性讲清楚。1. 为什么要折腾28377D的BootloaderC2000系列的Bootloader一直是个让人又爱又恨的话题。爱的是TI在芯片出厂时固化了一段BootROM支持SCI、SPI、I2C、CAN、并行GPIO等多种引导方式恨的是这套机制和用户真正想要的“自定义引导程序”其实有本质区别。28377D的BootROM只负责最原始的启动引导它从预设的接口把一段数据搬进RAM然后跳转执行或者直接跳转到Flash固定地址。它做得并不少但真正想要实现“上电后先跑一段用户代码判断是进入App还是等待升级然后从外部接口接收固件并写入Flash”就必须自己写一套用户级Bootloader。至于为什么非要自己写核心原因一般是升级维护。趁手的JTAG仿真器不是每个现场都有的有的设备在恶劣环境下一干就是几年随着算法迭代或者BUG修复OTA升级能力几乎变成硬性需求。28377D这种双核DSP在电机控制、数字电源、并网逆变器等领域用得非常多这些设备往往不允许停机拆机Bootloader就成了唯一的软件升级通道。这颗芯片和普通单片机的另一个不同点在于是双核架构一个C28x内核加一个C28x协处理器两个都是32位浮点DSP核心。CPU1是主核有一块独立的FlashCPU2没有独立的Flash它的程序存储在CPU1的Flash里启动时必须由CPU1把代码搬过去或者在CPU1的引导代码里通过配置IPC机制来加载CPU2。这意味着Bootloader的设计不是单核思维要同时考虑两个核的协同启动。2. 28377D的启动流程和双核引导关系2.1 BootROM到底做了什么芯片上电后CPU1会从地址0x3F8000开始执行BootROM里的固化代码。这段代码首先做的是基本的芯片初始化工作比如把系统时钟从默认的OSC状态切换到用户配置的PLL状态这里实际上是读取了OTP里烧录的DCSM配置来决定最终的时钟值。接着BootROM就会根据引导模式引脚的状态去查找引导表判定该从哪个外设加载数据。值得注意的是28377D的引导源选择逻辑比老款多了好几个步骤。除了简单的GPIO状态判断外它还会检查Z1OTP里的Boot Pin配置如果OTP里没烧录就退回到默认的GPIO配置。使用的引导源可以是并列GPIO引导即通过GPIO84和GPIO72的电平组合选择SCI引导通过SCIA的RX和TX引脚接收数据CAN引导通过CAN-A的报文接收I2C引导支持主从模式SPI引导支持主从模式直接跳转到Flash地址0x080000执行BootROM会把这些接口收到的第一个16位字当作要跳转的入口地址然后把后续数据复制到RAM并执行。这里有个非常关键的点BootROM默认是把数据段复制到RAM里的所以它更侧重于RAM引导而不是直接帮用户烧Flash。想通过BootROM本身来完成Flash编程需要有一个驻留在RAM里的接收程序配合这也是为什么很多参考设计里都会在BootROM引导后马上接一个RAM里的小段程序由这个小段程序完成整个升级流程。2.2 CPU2从哪儿启动28377D的CPU2没有Flash它的程序必须存在于CPU1的Flash中。上电时CPU2默认处于复位状态CPU1通过IPC机制把CPU2从复位状态释放然后CPU2就去执行自己的BootROM。CPU2的BootROM和CPU1类似也会尝试从外部接口加载程序但注意CPU2的BootROM默认不会去访问CPU1的Flash空间因为RAM和共享内存的映射机制需要额外驱动。TI提供的SYSBIOS或者裸机开发环境下一般是由CPU1的应用程序主动拷贝CPU2的镜像到CPU2的RAM并初始化入口地址。这带来一个很实际的问题如果Bootloader只是简单地把CPU1的App从Flash跳转过去就完事儿CPU2很有可能跑不起来。必须在跳转前或者在App运行后的极早期把CPU2的镜像准备好。比较常见的做法是在上位机生成固件时把CPU1和CPU2的镜像打包成一个文件或者干脆在Bootloader阶段就把CPU2的镜像放在CPU1 Flash的固定位置由App启动后再完成CPU2的加载。我在实测中发现如果CPU1的App入口处代码没有在几十个时钟周期内把CPU2从复位释放CPU2的BootROM会进入它的默认引导序列假如恰好在某个接口上有意外数据可能导致CPU2跑飞。2.3 C2000的启动模式与28377D的差异以前用28335之类的单核DSP时Boot模式引脚无非是XRS、TRST这些引脚组合代码里一般会专门留出一个GPIO来指示是否进入Bootloader。但28377D上面不再是简单的引脚跳线它加入了更复杂的DCSM区域和安全机制而且BOOT引脚在OTP里可以被重新映射。换句话说如果OTP里烧写过自定义配置外部引脚可能不再是决定性因素代码要能主动识别当前是在Bootloader环境还是在App环境里。另一个差异是地址分配。28377D的Flash起始地址是0x080000而BootROM占据了0x3F8000到0x3FFFFF的顶部区域这直接影响CMD文件的编写。很多人拿老工程的CMD文件往上套结果链接都过不了因为Memory区域的分配已经完全变了。3. 一个典型28377D Bootloader的设计框架3.1 存储分区与地址分配一个可靠的Bootloader设计方案用的是三段式分区Boot区、App区、备份区。Boot区放在Flash的最前面实际上是0x080000之后的第一个扇区。App区放在中段备份区放在后段。以28377D为例它的Flash总容量是512KB实际可用空间在链接脚本里分配。一个参考分区如下区域地址范围大小用途Bootloader0x080000 - 0x08FFFF64KB自举代码、升级逻辑参数区0x090000 - 0x090FFF4KB启动标志、版本号、校验信息App区0x091000 - 0x097FFF28KB应用程序主区备份区0x098000 - 0x09FFFF28KB新固件暂存区需要注意28377D的Flash扇区大小并不是均匀的不同型号和不同起始位置扇区边界差别很大。分配地址前一定要翻开对应型号的DataSheet去查扇区表比如有些Flash区域是16KB一个扇区有些是8KB一个扇区如果跨越了扇区边界擦除时就会连带把别的数据擦掉。我的做法是先在Excel里把扇区表列出来再根据扇区边界去对齐分区不用绝对的整十整百地址。3.2 代码框架和状态机整体状态机设计成四个核心状态检测启动标志、擦写Flash、跳转App、升级失败回退。启动后最先读取参数区里的启动标志如果标志显示请求升级就停留在Bootloader主循环等待数据接收如果标志显示App有效就做一个简单的CRC校验通过后直接跳转App。跳转前的现场准备很重要。首先需要把所有中断关闭尤其是PIE模块里的中断不然带着残留的中断标志跳到AppApp里的中断服务程序很可能在初始化前就被异常触发。其次建议把看门狗喂满或者直接禁用防止Bootloader到App跳转期间看门狗超时复位。最后在跳转前可以把系统时钟恢复成默认状态因为Bootloader里如果配置了高频PLL而App又完全重新初始化时钟两个初始化过程之间可能出现短暂的时钟毛刺。这里给出一段跳转App的参考代码void jump_to_app(uint32_t app_addr) { // 关闭全局中断 DINT; // 禁用看门狗 SysCtl_disableWatchDog(); // 关闭PIE及所有中断 PieCtrlRegs.PIECTRL.bit.ENPIE 0; // 清空流水线 asm( FSETC RNDFEN); asm( FBranch #0); // 读取App入口地址存放在App区起始位置的前4个字节 uint32_t entry_addr *(uint32_t *)(app_addr 4); // 定义函数指针并跳转 void (*app_entry)(void) (void (*)(void))entry_addr; app_entry(); }之所以要从app_addr 4读取入口地址是因为C2000在编译链接时默认会在段起始位置放一个跳转指令而不是直接放函数入口地址。更稳妥的做法是在链接脚本里显式定义一个入口符号然后通过入口符号直接获取地址。两种方式都行但读取跳转指令的方式要注意字节对齐和指令编码格式不同编译器版本有差异。3.3 Flash擦写流程的底层细节28377D的Flash编程需要严格按照流程操作和ST的MCU不太一样。它内部有Flash状态机最核心的就是FMC寄存器组。编程时先写命令寄存器再通过状态寄存器轮询完成状态。一个典型的数据写入序列是解除写保护配置等待周期通过FMC_CTRL寄存器设置编程模式写入目标地址和数据等待FMC_STATUS的BUF_LOADED和PROG_COMPLETE标志重复直到整个扇区写满执行Flash擦除时不可对正在执行的代码所在的扇区操作踩过最深的坑是Flash操作必须在RAM中执行。不论在哪个C2000芯片上擦写Flash的函数都不能在Flash里跑。28677D的Flash控制器虽然允许在执行Flash代码的同时擦写另一个分区但安全起见把Flash操作函数全部放到RAM里执行才是最稳妥的。使用TI编译器时可以通过#pragma CODE_SECTION(flash_write_function, .TI.ramfunc)把它挪到RAM运行。擦除扇区前必须确保目标地址的Flash已经被解锁。28377D在DCSM里配置了Zone1和Zone2的安全机制如果DCSM把某些扇区设为安全区而当前CPU运行在非安全模式那么擦写会直接被拒绝。调试时最容易忽略的是JTAG连接时芯片默认处于安全模式直接用CCS的Memory Browser去改数据可能触发安全违规导致芯片进入异常状态需要重新上电。4. 上位机通信协议与双区备份策略4.1 通信协议设计Bootloader需要和上位机交互通信接口最先考虑的是SCI因为28377D的SCI支持FIFO和中断、波特率可以做到很高而且几乎所有C2000工程师都对SCI很熟悉。CAN也不错针对工业现场的强干扰环境更可靠但协议处理稍微复杂一些。SPI的缺点是引脚固定不太灵活而且一般要额外处理片选时序。我在项目中用的是SCI波特率设置为115200每个数据帧采用“帧头命令字长度数据CRC32”的结构。帧头固定为0xAA55命令字分为握手、擦除、写入、校验、跳转、查询版本六种。每次上位机发一帧Bootloader执行完操作后返回状态应答帧。通信时序必须约定超时时间我设定为500ms无数据判定为超时自动复位并跳转App防止现场升级过程中通信断线导致设备变砖。帧结构示例字段长度说明帧头2字节固定0xAA55命令字1字节0x01握手0x02擦除0x03写入0x04校验0x05跳转数据长度2字节小端模式数据体N字节如固件数据、地址、长度等CRC324字节对整帧计算握手阶段用来做版本确认和整体擦除确认。收到“进入Bootloader”命令后我先返回固件版本号然后等待上位机的“擦除命令”。擦除命令里带有待擦除的区域地址和长度Bootloader逐扇区擦除擦完一个扇区就应答一个进度标识。写入阶段每帧固定携带256字节写入前会先做一次地址连续性检查防止跳帧错位。校验阶段则把整个App区的数据读出来重新计算CRC与上位机发来的CRC比对不一致就返回失败。4.2 双区备份与失败回退很多Bootloader方案只维护一个App区升级过程一旦中途断电或者通信异常Flash里的App就处于半擦除半写入的状态设备直接变砖。虽然没有绝对的“永不砖”方案但通过双区备份能把风险降到一个很低的水平。我给App区设计了A/B两个区域。平时运行在A区当新固件下载时先写入B区等B区数据完整校验通过后才更新启动标志把引导目标从A区切换到B区然后跳转B区执行。如果B区校验失败启动标志不被修改继续从A区运行这样就实现了失败回退。Flash容量大、能腾出两个App区自然可以这么玩。但如果Flash比较紧张另一种简化的方案是保留一个很小的Recovery区里面放一个精简的应急应用只负责接收固件重新烧写主App区。不管用哪种策略核心原则都是升级过程中至少要保证有一个完整的可执行程序存活在Flash里不要把旧程序放在被擦除的同一扇区里。启动标志区我放在一个独立扇区里并且保留三份冗余存储。每次更新标志时先擦除该扇区再写三份相同的值读取时采用少数服从多数的仲裁逻辑。这样做是因为Flash擦写操作本身就有一定概率受干扰冗余存储能有效减少这种风险导致的误判。5. 跳转App后无法触发中断的排查升级完成后Bootloader跳转到AppApp能跑起来但所有中断都不响应。这个现象非常典型而且网上能查到的答案大多比较模糊这里把排查链路完整走一遍。5.1 问题现象与直接原因现象是App的主循环已经开始运行LED在闪烁但按键中断、定时器中断全都进不去。通过CCS连接调试器发现PIE中断标志位有置位但CPU始终不跳进ISR。直观怀疑是中断向量表的问题。C2000的中断向量表默认放在0x000D00开始的PIE向量区在Bootloader阶段PIE向量表可能被Bootloader的ISR填满了。跳转App时如果仅仅跳到了main函数而没有重新初始化PIE向量表CPU的中断查询机制拿到的仍然是Bootloader的ISR地址那个地址处的代码执行完就返回看起来就像中断没有响应。具体原因有四种PIE控制寄存器里的向量表基地址被Bootloader改过PIE向量表的内容没有被App重新装载中断使能位在跳转时被置乱CPU的中断标志寄存器没有清干净5.2 逐步排查过程排查时先看了一个关键点跳转前是否执行了完整的PIE初始化。如果App的main函数里调用了InitPieVectTable()且进入main后第一时间就执行那么问题基本出在跳转瞬间的EALLOW/EDIS保护和INTM标志上。我在自己的代码里反复对比最后定位在跳转函数里遗漏了清除IFR寄存器的操作。跳转前的准备操作最终调整成如下顺序DINT禁止全局中断防止跳转过程中断介入将所有外设中断标志清零尤其注意PIEACK把PIE向量表恢复成复位默认值防止重复初始化时产生歧义清除CPU级IFR和IER恢复系统控制寄存器为复位默认状态跳转其中第3步在大部分参考代码里都没有正是这个细节解决了问题。因为App的InitPieVectTable()函数大多是先关闭PIE再重新填充表项但如果PIE里的向量表指针已经被Bootloader指向别的地方或者向量表内容被意外修改即使重新填充也可能跳到一个错误的位置。追加的代码片段// 重置PIE向量表到复位默认 extern void (*PieVectTable[])(void); PieCtrlRegs.PIECTRL.bit.ENPIE 0; // 先禁用PIE PieCtrlRegs.PIEACK.all 0xFFFF; // 清空中断应答 PieCtrlRegs.PIECTRL.bit.ENPIE 1; // 重新使能PIE IER 0x0000; // 禁用所有CPU中断 IFR 0xFFFF; // 清空中断标志5.3 另一个隐藏得更深的坑——入口地址错位继续往下排查时发现即使PIE全部重新初始化中断能触发了但ISR执行后返回地址错乱程序莫名跳到非法地址。用反汇编一看原来是使用函数指针读取入口地址的偏移计算错了。28377D的Flash被映射到0x080000但链接脚本里把codestart段放在了Bootloader区域的末尾附近Bootloader跳转时读的是codestart的值结果跳到的是一个指向Flash上某段数据的地址代码执行后立即进入非法指令陷阱。这个问题的本质是没搞清楚C2000在复位时是怎么找到入口的。BootROM在完成引导后会读复位向量复位向量的内容存放在0x080000处由codestart段提供内容是一条指向_c_int00的跳转指令。用户Bootloader在跳转App时不应该直接读codestart的机器码再跳过去而是应该让链接器生成一个全局符号然后在Bootloader里直接拿到这个符号的地址。简易做法是在App工程的链接脚本里定义extern void _c_int00(void); #define APP_ENTRY_ADDR ((uint32_t)_c_int00)然后在Bootloader跳转时直接用APP_ENTRY_ADDR避免解析Flash里的跳转指令带来的不确定性。实测改成这种方式后跳转的稳定性明显提升连续上电测试升级100次没有再出现跳飞的情况。6. 命令流与常见异常处理6.1 升级过程中的看门狗处理Bootloader在等待上位机数据时看门狗如果没关很容易造成升级中途复位。28377D的看门狗默认是开启状态我建议在进入Bootloader主循环前就使用SysCtl_disableWatchDog()关闭看门狗。如果出于安全考虑不想完全关闭看门狗那就必须在接收数据的循环里周期性地喂狗并且喂狗操作不能阻塞在等待数据上否则通信慢一点就会复位。个人经验是关闭看门狗最省心前提是Bootloader代码本身经过了充分测试。因为在升级过程中如果真出现异常卡死看门狗复位最多也只是回到Bootloader重新开始等待数据并不会对Flash造成额外破坏。6.2 Flash等待周期设置不当导致操作失败28377D可以在很高的主频下运行比如200MHz而Flash的操作是有等待周期限制的。Flash编程时等待周期设置不当会导致写入错误或者状态机超时。在Bootloader阶段建议把Flash的等待周期配置成最保守的值例如把FlashWaitState设置在8以上等到跳转App后再由App重新精确配置为和其系统时钟匹配的值。写Flash时还要留意地址对齐。28377D的Flash编程宽度是128位写入数据时必须一次写16字节并且地址要与16字节边界对齐。如果上位机发过来的数据不够16字节的整数倍在最后一帧要补零凑满16字节。经验做法是上位机就把固件文件按16字节对齐填充后再分包发送Bootloader收到的每一帧都天然满足编程宽度要求。6.3 升级中断电和数据校验策略设计中最担心但必须面对的是升级中掉电。G防的主要手段是双区备份但双区备份也怕擦写顺序问题。假设当前运行在A区新固件写入B区时断电此时B区数据不完整但A区完好重新上电后Bootloader检查启动标志仍然指向A区还能正常运行。最危险的情况是正在更新启动标志的那一瞬间断电标志区可能写了一半。这就是前面提到为什么要用三份冗余标志的原因。固件校验我选用CRC32因为它计算速度快且碰撞概率极低。接收完整包后先将固件暂存到RAM buffer中校验CRC通过后才正式写Flash。另外理论上还应支持对每帧的单独校验这样能更早发现传输错误并请求重传省去整包重传的时间。帧CRC和整体CRC可以共用同一个CRC32算法只是计算范围不同。我在通信协议里做了双层校验帧尾部带帧CRC用于单包传输完整性检查固件文件尾部带文件CRC用于整体一致性校验。两层校验都通过后才会修改启动标志并跳转。7. 安全模式和JTAG调试的冲突处理28377D支持JTAG调试但一旦DCSM区域被锁定JTAG可能无法正常访问Flash区域。调试Bootloader时建议先把DCSM的安全功能关闭或者设置为调试模式否则代码在Flash写入阶段会因为安全违规直接卡死在ESM异常里。有一种情况特别容易遇到之前用CCS烧录过一个带安全配置的程序后来重新开发Bootloader时发现无法擦除Flash的某些扇区。这是因为DCSM的Zone1和Zone2的写保护配置在OTP里OTP只能烧一次。如果前期调试阶段不小心把安全配置烧到位了后面就没法改只能换芯片或者用解锁命令解锁。教训是调试阶段不要轻易烧录DCSM配置这点和ST芯片的读保护有几分相似但C2000的OTP烧录更不可逆。8. 实际测试中的验证清单折腾完这套Bootloader后我整理了一份验证清单每次修改代码后过一遍能省去很多重复问题冷启动Bootloader后能否进入升级模式升级模式下跳转App正常升级模式下关闭看门狗等待数据30分钟不复位升级成功后设备重启能准确跳转AppApp所有中断正常升级失败时设备重启后仍然运行旧版本App不进入死循环升级过程中人为断电重新上电后设备不砖芯片工作在最高主频200MHz时Flash读写正常CPU2能否正常启动并运行协同程序其中CPU2的启动测试尤其重要。因为很多开发者的App里包含了CPU2固件如果Bootloader只处理了CPU1的跳转而遗漏了CPU2的加载整个系统算力直接少一半现象还不容易察觉只有跑某些特定算法时才会表现出异常卡顿或者结果错误。9. 二次引导的一些心得28377D的Bootloader不是写完就一劳永逸的。它和App之间的耦合关系非常密切尤其是向量表、CMD文件、Flash分区这些底层配置App工程和Bootloader工程必须严格保持一致任何一边改动都要重新编译两边并回归测试。我在实际项目中把Bootloader和App工程放在了同一个CCS Workspace里共享一套Flash分区头文件和链接器脚本从根上杜绝了两边地址配置不一致的可能。另外强烈建议在Bootloader里加一个辅助调试手段比如把当前引导状态通过LED或者串口打印出来。别小看这个操作调试时它能省一半的精力。我的Bootloader在进入升级模式时黄灯快闪跳转App时绿灯常亮升级失败时红灯两短一长循环。现场工程师只需要看灯的状态就能初步判断设备处于什么状态不需要接串口或者仿真器。关于双核联动目前采用的是CPU1主引导方案Bootloader运行在CPU1上升级流程只负责把CPU1的App和CPU2的镜像一并烧录到Flash对应区域启动时CPU1的App先完成自身初始化再通过IPC释放CPU2并加载其镜像到共享RAM区域。这个方案实现简单稳定性高也是TI官方推荐的方式之一。最终确认一下如果你正在基于28377D做自己的Bootloader项目我建议在动手前先花一天时间把下面几份文档啃下来能避免后面至少一周的弯路包括《TMS320F2837xD Dual-Core Microcontrollers Technical Reference Manual》里的Boot ROM章节、《TMS320F2837xD Flash Programming Reference Guide》以及你选用的IDE版本对应的编译器链接器用户指南。文档虽厚但按键搜索定向查找效率会比盲目试错高得多。
返回列表