ARTICLE DETAIL

资讯详情

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

DSP28335远程升级方案:Bootloader设计、Flash分区与踩坑实录

DSP28335远程升级方案:Bootloader设计、Flash分区与踩坑实录 DSP28335远程升级这事儿我愿把它称为“原理十分钟联调一礼拜”。Bootloader放哪、Flash擦写时CPU到底该在哪跑、中断向量表怎么搬、跳转前哪些寄存器必须清零随便一个环节没伺候好板子就是砖头一块。这段时间我把整套流程从头到尾啃了一遍踩的坑比过去一年加起来还多最后终于跑通了一条相对稳的链路。这篇不整虚的直接把方案选型、代码框架、踩坑记录全部分享出来给正在搞变频器、伺服驱动器、电源类产品在线固件升级或者想让28335具备远程升级能力的兄弟做个参考。1. 远程升级方案怎么定先把Flash分区想明白1.1 为什么非要自己搞Bootloader很多人第一反应是不就用个烧录器吗开发阶段确实如此JTAG一插CCS里点个Load Program完事。但设备一旦装到现场不管是电梯井里的变频器还是户外机柜里的电源模块要升级固件就得派人过去拆机、开壳、接仿真器。这已经不是技术问题是运维成本和响应速度的问题。远程升级的本质就是让设备自己具备“接收固件包、写入Flash、校验跳转”的能力。28335没有以太网口也没有USB最常见的落地方式就是走现有通信口串口RS485、CAN或者通过上位机中转的以太网网关。原理都一样上位机把固化好的镜像分包发下去Bootloader收完写进Flash最后跳转到新程序。你可以在Bootloader里集成一个简单的通信协议也可以做得更重比如支持A/B双镜像、加密、断点续传。但第一步永远是分区规划和引导流程设计这个没想清楚后面全是坑。1.2 28335的Flash分区Boot区、App区、参数区28335的片内Flash总共512KB物理上被分成多个扇区擦除的最小单位是“扇区”。我们做分区规划时最重要的一件事就是所有区域边界必须卡在物理扇区边界上否则擦除一个扇区会把这个扇区里其他区域的内容一起抹掉。这是第一个大坑后面踩坑实录里会细说。我们项目里用的典型分区方式是“Boot App 参数”三段式。Boot区放在Flash起始地址因为芯片上电复位后如果配置为Boot to Flash模式CPU会从0x300000开始取指Bootloader必须住在那里。App区从Boot区之后开始参数区单独留一小块用来存升级标志、版本号、校准参数这类掉电不能丢的数据。这里给一段简化的CMD文件体现分层思路实际地址大家要按自己芯片手册里的扇区尺寸来定MEMORY { PAGE 0: BOOT_FLASH : origin 0x300000, length 0x004000 APP_FLASH : origin 0x304000, length 0x7B800 PARAM_FLASH : origin 0x3FF800, length 0x000400 } SECTIONS { codestart : BOOT_FLASH, PAGE 0 .text : APP_FLASH, PAGE 0 .cinit : APP_FLASH, PAGE 0 .switch : APP_FLASH, PAGE 0 updateFlag : PARAM_FLASH, PAGE 0 }Boot区一般16KB左右足够放通信驱动和擦写逻辑。App区不要用到Flash最后那一段因为那里还涉及CSM密钥和一些保留空间留一点余量永远不是坏事。参数区可以单独用一个小扇区但要注意这个扇区的擦写寿命和擦除粒度不能频繁写。分区定完之后引导流程就清晰了上电 → 系统初始化 → 关中断 → 等待升级握手 → 没握手就跳转App → 收到握手就进入下载模式。整个过程不依赖外部存储器全部在片内完成。2. 通信协议与Bootloader代码落地2.1 帧格式定成这样上位机和DSP都不遭罪远程升级的通信链路说白了就是上位机往DSP发一堆数据包。链路可以是SCI、CAN或者走网关的以太网但无论底层是什么建议在应用层自己定一套简单的帧格式。协议要满足三件事能判断帧边界、能发现数据错误、能处理丢包重传。我用的是经典“帧头 命令字 长度 序号 数据 CRC16”结构#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define CMD_HANDSHAKE 0x01 #define CMD_START 0x02 #define CMD_DATA 0x03 #define CMD_END 0x04 #define CMD_ACK 0x10 #define CMD_NACK 0x11 #pragma pack(push, 1) typedef struct { Uint8 head1; Uint8 head2; Uint8 cmd; Uint8 len; Uint16 seq; Uint8 data[512]; Uint16 crc; } UpgradeFrame; #pragma pack(pop)为什么帧头用两个字节因为单字节帧头在实际链路里太容易伪同步了尤其是RS485这种电平翻转的链路噪声进来一个字节就可能把状态机带偏。双字节帧头加上CRC校验基本能保证误判概率低到可以忽略。seq序号是重传机制的基础。DSP每收到一帧数据解析通过后回一个ACK给上位机上位机根据ACK的seq判断这一帧是否已确认超时没收到就重发当前帧。这套机制虽然朴素但在115200波特率下跑512字节一帧整包1MB固件也就十几分钟稳定性够用。2.2 CRC16查表实现DSP端基本不耗时CRC算法我建议用CRC16-Modbus查表方式实现。1035上跑查表法非常轻松单帧512字节的校验耗时可以忽略而且相比异或和校验它能发现的错误类型多得多。Uint16 Crc16Update(Uint16 crc, Uint8 byte) { Uint16 i; crc ^ byte; for (i 0; i 8; i) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } return crc; } Uint16 Crc16Compute(Uint8 *buf, Uint16 len) { Uint16 crc 0xFFFF; while (len--) { crc Crc16Update(crc, *buf); } return crc; }这段代码不用查表也能跑但如果传输量大建议把查表版本加上省下的时间留给其他逻辑。注意CRC的计算范围上位机和DSP必须完全一致我是从帧头开始算到数据段结束然后把CRC追加在末尾DSP收到帧后先暂存数据计算CRC再和帧尾的CRC比对。这个约定一定要在协议文档里写死否则两端校验永远对不上。2.3 Bootloader主流程从握手到跳转Bootloader的主流程不复杂但它决定了整个升级过程的可靠性。核心原则是上电先把控制权交给“升级判断逻辑”如果没有升级需求再把控制权转给App。绝对不能一上电就直接进App不然升级程序根本没有机会启动。void main(void) { // 1. 基础初始化 InitSysCtrl(); InitGpio(); // 2. 关闭全局中断清中断标志 DINT; IER 0x0000; IFR 0x0000; // 3. 初始化升级通信口这里用 SCIB 作示例 InitSciB(); EnableInterrupts(); // 4. 等待握手窗口上位机在窗口期内发 CMD_HANDSHAKE Uint32 waitMs 0; while (waitMs HANDSHAKE_TIMEOUT_MS) { if (ReceiveHandshake()) { // 收到升级请求进入下载流程 DownloadFirmware(); break; } DELAY_US(1000); waitMs; } // 5. 没有升级请求跳转App JumpToApp(APP_FLASH_START); }握手窗口的时间我一般给300到500ms。这个窗口的意义在于如果没有它Bootloader只要收不到数据就得一直等待设备上电到正常运行的时间会被拖长。有了超时窗口正常运行时几乎感觉不到引导延迟。如果现场设备要求上电秒级响应还可以加一个外部GPIO跳线判断上电时检测某个IO电平高电平强制进升级模式低电平直接跳App。两种方式可以组合看具体产品需求。下载流程内部就是完整的状态机擦除、收包、写Flash、校验、跳转。收包状态机必须处理好超时和重传不能一卡死就永久阻塞在那里。3. App侧要配合的事PIE向量表与升级入口3.1 为什么跳转后中断会跑飞很多人在Bootloader里辛辛苦苦把固件写进去了结果跳转后程序要么死在原地要么进了一个莫名其妙的中断。最常见的原因就是中断向量表没有处理好。28335的中断系统依赖PIE外设中断扩展模块。PIE向量表本质上是一张ISR地址表默认情况下所有中断都指向一条空循环或者默认ISR。Bootloader自己也有这套表当Bootloader跳转到App时如果PIE模块还指向Bootloader的向量表App里任何一个中断触发CPU都可能跳回Bootloader的Flash区域取指那基本就是程序跑飞。解决思路是Bootloader跳转前关闭PIEApp启动后自己重新初始化PIE向量表把每个中断服务函数重新挂上去。App侧做这件事的代码很标准就是我们平时写的InitPieVectTable()InitPieVect()。extern void (*PieVectTable[])(void); void InitAppPieVector(void) { // 复制默认向量表到RAM中 memcpy(PieVectTable, (void *)PIE_VECT_TABLE_RAM, sizeof(PieVectTable)); // 重新挂载自己的ISR EALLOW; PieVectTable.SCIRXINTB SciBRxISR; PieVectTable.TINT0 Timer0ISR; EDIS; }注意PieVectTable在CMD里要放到RAM段这样它才是可写的。TI的例程里通常已经把PieVectTable安排在0x000D00这块RAM上我们只需要保证App的CMD文件里也包含这个定义不要让链接器把它扔到Flash里去。App的main函数开头顺序应该是关中断 → 初始化系统时钟 → 初始化PIE控制 → 重新挂向量表 → 初始化外设 → 开中断。这个顺序不能乱尤其是向量表初始化一定要在开中断之前完成否则第一个中断进来就又跳飞了。3.2 App收到升级指令把控制权交回Bootloader远程升级的触发方式有两种一种是上位机在设备上电时发握手命令直接和Bootloader对话这种适合现场有人操作、设备可以重启的场景另一种是设备正常运行中App通过通信口收到升级指令先存一个升级标志到参数区然后软复位让Bootloader去读这个标志并进入升级模式。第二种方式对用户体验更友好也是我更推荐的。App侧的任务很简单收到指令后先把关键外设停掉电机/PWM/继电器该封的输出都封掉然后写升级标志最后触发软复位。void HandleUpgradeCmd(Uint16 cmd) { if (cmd ! UPGRADE_REQUEST) return; // 1. 关闭关键中断封锁输出 DINT; IER 0x0000; // 2. 写升级标志到参数区防止误入App WriteParam(APP_UPDATE_FLAG_ADDR, 0xA5A5); // 3. 等待看门狗复位或直接软件复位 EALLOW; SysCtrlRegs.WDCR 0x68; // 使看门狗复位 EDIS; while (1); }这里有个细节写升级标志是在Flash参数区写一个魔数而不是在RAM里。因为软复位之后RAM内容不会保留准确说是不保证保留Bootloader上电后必须能从Flash里读到这个标志才能知道“上次是正常断电还是App主动请求升级”。如果不用看门狗复位也可以配置一个软件复位但原理都一样让CPU从0x300000重新开始执行。3.3 App的CMD文件与Bootloader的分区隔离App侧编译时CMD文件里Flash区域的origin必须和Bootloader分区里的APP_FLASH一致。这是纯配置层面的问题但错一个地址程序就能跑出花来。App的CMD文件片段大概长这样MEMORY { PAGE 0: APP_FLASH : origin 0x304000, length 0x7B800 PAGE 1: RAML0 : origin 0x008000, length 0x001000 RAML1 : origin 0x009000, length 0x001000 }codestart段要人为放在App起始地址并且和Bootloader跳转的目标地址严格对应。怎么确认入口地址对不对编译完打开生成的map文件查看_c_int00或者codestart符号所在地址那才是真正的入口。跳转函数里写死地址的时候一定要拿map文件地址来核对不能靠感觉。4. 踩坑实录五个能让你怀疑人生的地方4.1 Flash擦写时必须保证CPU在RAM里跑这是在28335上做在线升级最反直觉的一个坑。之前我按照MCU时代的经验直接在Flash里调用擦除函数结果程序跑着跑着就死机了。原因在于擦除/编程Flash期间CPU不能从同一块Flash区域取指令。而Bootloader本身就在Flash里它调用的Flash API函数也在Flash里擦除操作一开始CPU下一条指令可能就是从正在被擦除的地址取直接导致总线错误。解决办法有两个。第一个是把Flash API函数整体拷贝到RAM里执行这也是TI官方推荐的方式。第二个是把Bootloader里所有涉及Flash擦写的代码段用#pragma显式指定到RAM段。我用的是第一种因为TI官方提供的Flash API本身就支持这种用法。// 将Flash API从Flash拷贝到RAM extern Uint16 Flash_API_Size; extern void (*Flash_API_RunAddr)(); void CopyFlashApiToRam(void) { memcpy(Flash_API_RunAddr, Flash_API_Start, (Uint32)Flash_API_Size); }擦写期间还必须把全局中断关掉。中断一进CPU就要去PIE向量表取ISR地址、跳到ISR执行这个过程中任何一个步骤涉及Flash操作都会踩雷。所以我的EraseAppSectors()函数里是这么处理的void EraseAppSectors(void) { Uint16 status; DINT; Flash_Init(); // 指定要擦除的扇区 status Flash_Erase(SECTOR_A | SECTOR_B, Flash_EraseStatus); EINT; if (status ! PASS) { // 擦除失败进入等待重试 } }4.2 跳转成功却不停复位第一次跑通跳转时我特别兴奋结果板子表现为一种诡异的“活过来又死回去”的循环App能跑一小会儿然后立刻复位再进Bootloader、超时、再跳转反复重启。排查了很久问题出在两个地方。第一跳转前没有关闭看门狗。Bootloader的初始化代码里使能了看门狗跳转到App后App可能还没跑完初始化看门狗就开始计数一旦App初始化耗时超过了看门狗超时时间就直接复位了。第二跳转前没有彻底清理外设中断状态。PIE模块里有些中断标志位还是置位状态App初始化时一开中断旧的中断请求立刻进来CPU跳到一个还没初始化好的ISR里非法操作导致复位。跳转函数的最终形态是把能清的都清一遍别嫌麻烦void JumpToApp(Uint32 appAddr) { void (*entry)(void); // 关全局中断和PIE DINT; IER 0x0000; IFR 0x0000; EALLOW; PieCtrlRegs.PIECTRL.bit.ENPIE 0; EDIS; // 复位看门狗给App一个干净的环境 EALLOW; SysCtrlRegs.WDCR 0x6F; // 禁用看门狗 EDIS; // 跳转 entry (void (*)(void))appAddr; entry(); }App侧也要在main最前面把看门狗重新配置成自己的模式不能依赖Bootloader遗留的状态。谁初始化谁负责到底这个原则在跳转场景里特别重要。4.3 Flash等待周期设置不当导致偶发写失败这个坑比较隐蔽。28335的Flash编程需要配置等待周期也就是Flash_CTRL寄存器里的FWAIT_CNT字段。这个值跟系统时钟频率有关系统时钟越高需要的等待周期越多。我一开始图省事直接用了TI例程里的默认值结果发现升级成功率很不稳定有时候整包固件一次过有时候写到一半某个扇区里的数据全是0xFFFF的残留。后来查了手册才知道FWAIT_CNT没配够Flash内部电压和时序准备不足编程操作在高速时钟下会偶发失败。这不是逻辑错误是时序裕量不够所以特别难复现。解决方式很简单按芯片最高工作频率对应的最大值去配置等待周期宁多勿少。擦写性能差那几十个周期对一次几分钟的升级来说完全无感。void Flash_Init(void) { EALLOW; Flash_CTRL-FWAIT_CNT 0x001F; // 按150MHz主频配置 Flash_CTRL-FBANK_WAIT 0x001F; EDIS; }4.4 通信丢包与帧超时重传机制一定要有升级到一半丢包是一个看起来没有技术含量、但实际最折磨人的问题。最初我设计的是“上位机发完所有帧DSP写完拉倒”结果大固件传到60%左右就开始出乱码偶尔还烧进去一个坏固件导致设备启动失败。问题根源在于串口链路不是无损的。485总线上干扰、上位机USB转串口的驱动延时、DSP接收缓冲区溢出任何一个因素都会导致某帧数据丢了或者坏了。如果没有重传机制坏数据会被当成好固件写进Flash。后来我在协议里加了两件事一是seq序号每帧数据都有唯一的序号二是ACK/NACK应答DSP每收到一帧就回一个ACKCRC不对就回NACK上位机收不到ACK就重发。同时把单帧数据长度从1024字节降到512字节单帧传输时间缩短后出错概率下降了一大截。bool ProcessDataFrame(UpgradeFrame *frame) { if (frame-seq ! expectedSeq) { // 序号不连续丢弃并要求重传 SendNack(frame-seq); return false; } // 校验CRC if (frame-crc ! Crc16Compute((Uint8 *)frame, OFFSET_OF_CRC)) { SendNack(frame-seq); return false; } // 写入Flash ProgramFlash((Uint16 *)frame-data, frame-len / 2); SendAck(frame-seq); expectedSeq; return true; }这里要注意DSP接收到完整一帧后先做CRC校验再写Flash千万不要边收边写。边收边写表面省时间但一旦帧被拆包或者发生误码写进去的就是残缺数据后续根本没法校验。4.5 升级中断电怎么救Bootloader要永远可进最后一个大坑也是最吓人的一个升级过程中直接断电。第一次遇到这个场景是调测时我手一抖把电源掐了再上电板子毫无反应App区已经被擦了一半Bootloader也进不去。这逼迫我重新审视了分区设计。为什么Bootloader会进不去因为我把Boot区和App区放得太紧了或者更准确说升级流程里Bootloader自己也依赖Flash里的某些数据App擦除时破坏了Bootloader需要的上下文。最终的解决思路是三层防护第一Bootloader代码所在的Boot区在升级流程中永远不擦除这是原则性问题。擦除范围严格限定在App区对应扇区。第二Bootloader上电先检测“App区是否是一个可用的完整程序”怎么检测在App区末尾或者参数区保存一个CRC值上电后对整个App区做一次CRC回读和保存值比对不匹配就不跳转留在引导模式等待上位机重新下载。第三进入App之前把App区的有效标志写到参数区App正常运行时也会定期刷新这个标志。Bootloader判断流程变成先看有没有升级请求再看App标志和CRC是否有效都满足才跳转。这样即使升级断电导致App区变成半残状态Bootloader也能安全引导到下载模式。bool CheckAppValid(void) { Uint32 crc CalculateAppCrc(); Uint32 savedCrc ReadFromParam(APP_CRC_ADDR); return (crc savedCrc); }这层防护加上之后升级断电就再也不会把设备变成彻头彻尾的砖头最多是需要重新下载一次固件。设备的安全性底线算是保住了。5. 联调心得和几个实用技巧5.1 固件镜像怎么生成out转bin才是正经事CCS编译完默认产出的是.out文件带调试信息和复杂段布局不能直接作为升级固件发送。需要先用TI的hex2000工具把它转换成纯二进制镜像。我用CCS工程里的post-build步骤编译完自动调用hex2000生成binhex2000 --bin --boot --serial8 -o App.bin App.out然后通过一个Python上位机脚本读取bin文件按协议分包发送。这样整个流程就串起来了编译产出bin → Python脚本分帧发送 → Bootloader收包写Flash → 校验跳转。这里再提一个我自己实测下来的经验生成bin之前建议把.text段、.cinit段都放在App区中连续地址不要让段跨区域散布否则bin文件里可能出现大片空洞白白增加传输数据量。可以用map文件检查段的分布是否紧凑。5.2 先用仿真器跑通再烧Bootloader顺序真的很重要。我一开始直接把Bootloader烧进Flash再联调出了问题连断点都没法打只能靠串口打印日志痛苦得要命。后来总结出一个稳妥的调试路径先用仿真器把Bootloader和App都加载到RAM里或者Flash里在CCS里单步调试握手、擦除、写入、跳转四个阶段确认整个逻辑没问题之后再烧Bootloader进Flash之后才做脱离仿真器的整包升级测试。为什么这么麻烦也要坚持因为Bootloader一旦烧死想再改就得用JTAG连回来重新擦除而现场设备不一定有JTAG口。开发阶段多花半小时后面能省一天。5.3 串口打印是调试的神器Bootloader里保留一个调试用的串口输出用最简单的方式打印状态信息比如擦除完成、写入进度、校验结果。这个串口可以和升级通信口复用在协议上区分调试帧也可以用单独的UART。条件允许的话单独一个调试口更省心因为它不会干扰升级流程。我的习惯是每个关键节点打印一行带标识的日志比如[BOOT] Erase OK、[BOOT] Program 0x305000 done、[BOOT] CRC PASS, jump to App。联调时看着日志找问题比盲猜高效太多。5.4 升级完成后回读校验比什么都稳写Flash完成不代表写入成功必须回读验证。我在DownloadFirmware的末尾加了一整遍App区CRC回读和接收过程计算的CRC比对。如果回读CRC不对直接报失败让上位机重新走一遍流程绝不会跳转到一个坏固件里。很多方案文档里把“回读校验”一笔带过但我的经验是这一步是防止“假成功”的关键。擦写Flash偶发个别字写飞了光靠写完之后的编程API返回状态是发现不了的只有全量回读CRC才能兜底。6. 项目落地的几个收尾建议6.1 关于升级超时给现场留足容错空间设备在现场运行通信状况千奇百怪总线上可能同时有其他设备在通信上位机调度会有延迟。升级协议里一定要留超时重传的窗口不要一发完整包就当成功。我通常的做法是Bootloader侧每收到一帧数据刷新一个喂狗计时器超过2秒没收到合法帧就主动回到等待握手状态而不是卡死在下载流程里。如果设备在电站、工厂这类环境里建议在升级前通过App侧主动上报版本号和固件大小上位机拿到版本号后再决定发不发。这样能避免误升级、降级升级这些不必要的麻烦。6.2 版本管理和升级记录的落地写法代码上跑通了现场管理也别落后。我在App里加了一个简单的固件版本结构体放在Flash参数区Bootloader握手时会把版本号回传给上位机。上位机软件里记录每次升级的时间、版本、包序号这样出问题的时候能快速回退定位。版本结构体定义也很简单typedef struct { Uint16 magic; Uint16 major; Uint16 minor; Uint16 build; Uint32 crc; } FirmwareVer;magic字段用固定魔数0x5A5ACRC字段存App区整体校验值这样Bootloader上电后读一次就能判断App是否完整、版本是否匹配省去现场拆开设备看标签的麻烦。6.3 最后一个实用小技巧升级标志就用Flash别额外加芯片很多方案会倾向外挂一片EEPROM或者铁电来存升级标志理由是Flash擦写寿命有限。但如果只是存一个魔数或者版本号频率很低Flash剩余寿命完全够用。28335片内Flash就够没必要增加物料和成本。我在参数区写升级标志前先读一下该地址当前值如果已经是想要的魔数就不重复擦写减少损耗。升级完成后App启动的第一件事是把标志清掉保证下次正常启动不会误入升级模式。整个DSP28335远程升级方案从分区设计、Bootloader开发到App侧配合其实没有哪一步是“秒懂”的难度但每一步都有能把人卡住一天的细节。上面这些代码和踩坑记录是我最终跑通整套流程后沉淀下来的版本。个人建议是任何一次改动先保住Boot区不被破坏再谈功能迭代。升级这个功能稳定永远比花哨重要。
返回列表