ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C6678多核DSP在线升级:Bootloader设计、TFTP/串口通道与掉电自恢复

2026/10/5 8:19:13 拓冰建站 浏览量
C6678多核DSP在线升级:Bootloader设计、TFTP/串口通道与掉电自恢复 1. 为什么要给C6678单独做一套在线升级方案做嵌入式设备维保的人大多有过这种经历现场几十块板子某个算法参数要调整或者发现了修复的bug结果谁都不敢动因为芯片是TMS320C6678程序固化在Nor Flash或NAND里老老实实拆机、拿仿真器、连CCS一套流程下来少说半小时。要是设备装在机柜深处、野外站点那更麻烦光拆装就够折腾一天。所以“在线升级”这四个字对C6678这种多核DSP来说不是锦上添花而是刚需。C6678作为TI KeyStone架构的旗舰多核DSP内部集成了8个C66x内核主频可以跑到1.25GHz定位是高性能信号处理。这类芯片通常用在雷达、声呐、通信基带、图像处理这类“设备一旦上线就很难停机”的场合。正因为它的工作环境往往不允许频繁拆机在线升级的价值就格外突出通过网络或者串口把新的固件数据发过去设备自己在本地完成Flash擦除、写入、校验、重启整个过程不需要人工干预太多。不过C6678的在线升级有它特殊的地方不能照搬单片机那套“Bootloader App”的思路。首先是启动方式复杂C6678支持多种启动模式Nor Flash、NAND Flash、SPI从设备、EMIF16、PCIe、SRIO等其次是多核结构8个核各自的入口地址、镜像段加载方式不同升级一个核和升级整个系统是两个概念还有一点容易被忽略C6678的主频高Cache和DMA参与数据搬运后很多按单片机思路写的Flash烧录逻辑会莫名其妙出错——不是擦写不成功而是校验和读出来对不上。本文要聊的就是一套在TMS320C6678上实际跑通的在线升级方案。升级通道有两条一条是千兆以太网适合数据量大的完整镜像升级另一条是串口适合现场调试、小镜像修补、或者网络不通时的兜底。文章会覆盖Flash分区设计、Bootloader改造、传输协议选择、镜像格式处理、以及我实际踩过的坑。适合正在调C6678启动流程、或者在老平台上加升级功能的工程师参考也适合刚接触DSP固件开发、想搞明白“到底怎么让板子自己更新程序”的入门者。2. 启动链与Flash分区设计在线升级的地基2.1 C6678的启动流程到底经历了什么C6678从上电到App运行中间有一个固定的启动链。芯片内部ROM里的RBLROM Boot Loader会根据Boot Mode引脚的电平配置决定从哪里加载用户代码。最常见的是EMIF16 Nor Flash启动RBL把Flash起始地址处的“启动表头”Boot Table读进L2 SRAM根据表头中的段信息把各个段的代码和数据拷贝到指定地址然后跳转到入口地址执行。这个机制在线升级里非常关键因为它意味着“C6678上电后第一个运行的程序不是你的App而是RBL”。我们可以在用户代码里再放一个Bootloader让RBL加载Bootloader再由Bootloader决定加载哪个App镜像。换句话说Flash里要放的东西至少有三块RBL负责引导Bootloader负责选择App才是真正干活的部分。实际设计时我习惯把Bootloader做得很小很稳只做四件事初始化时钟和DDR、初始化通信外设EMAC或者UART、检测升级请求、从Flash读取App镜像并跳转。其他功能一律不往Bootloader里塞功能越少出问题的概率越低。2.2 一份可落地的Flash分区表C6678外接Nor Flash的容量常见的是16MB或32MB地址总线是EMIF16的A[0..25]或更大。容量看起来不小但别急着一整块都用上分区必须提前规划否则后面升级逻辑会非常尴尬。我用的分区结构是这样的分区名称地址范围大小用途Bootloader区0x00000000 - 0x0003FFFF256KB存放Bootloader镜像参数区0x00040000 - 0x0007FFFF256KB版本号、升级标志、CRC值App A区0x00080000 - 0x003FFFFF3.5MB正式运行的应用镜像App B区0x00400000 - 0x0077FFFF3.5MB备用镜像/升级暂存区日志区0x00780000 - 0x007FFFFF512KB记录升级日志、错误信息为什么用双App区这是在线升级最重要的一个设计决策不删旧版本先把新版本写到App B区写完校验通过后再把“当前启动区”标志从A切到B。下次启动时Bootloader根据标志位启动B区。万一B区校验失败还能回退到A区。这套思路和路由器固件升级的A/B分区翻写异曲同工原理不复杂但能避免大量现场事故。需要注意一个细节C6678在EMIF16 Nor Flash上启动时RBL要求Flash起始处是一个合法的Boot Table格式。如果Bootloader本身放在0x00000000那么它必须符合这一格式。App镜像则不一定需要Boot Table因为Bootloader可以用自己的加载方式搬运这给了我们更多灵活性。2.3 Bootloader里的“升级判定”设计Bootloader启动后需要判断是正常进入App还是进入升级模式。判定条件我一般同时设计几个谁满足谁触发通信接口收到上位机发送的“升级请求帧”且帧中包含正确的产品标识和版本信息参数区的“升级标志位”被置为有效值表示上次异常中断导致升级未完成外部拨码开关或GPIO电平组合指定了强制升级模式App A和App B的CRC校验都失败时自动进入升级模式等待救砖。这里有个经验升级标志位不能是一个简单的临时变量因为掉电后就没了。要放到Flash参数区并且要设计成“先写一个无效标志再开始擦写App区”的顺序。也就是说在真正开始写入新镜像之前先写标志“升级进行中”等新镜像写入完成并校验通过后把这个标志改成“升级完成”。这样一旦中途掉电Bootloader上电后看到“升级进行中”就知道上次升级没成功会自动回退到另一个App区。3. 网络通道升级TFTP协议与千兆以太网下的细节把控3.1 为什么放弃TCP改选UDPC6678内部有SGMII接口外接PHY芯片比如88E1111、RTL8211等可以实现千兆以太网。很多人第一反应是“用TCP嘛可靠有重传”。但实际做过C6678在线升级后我反而强烈推荐UDP 自定义重传机制或者直接使用TFTP协议。原因有三个。第一TCP协议栈在C6678这种DSP上要么用第三方库要么自己移植LwIP占用内存和CPU都不小。Bootloader阶段DDR初始化虽然已经完成但代码越简单越可靠TCP的状态机在嵌入式环境里一旦遇到半开连接、超时重传异常排查成本很高。第二TFTP底层就是UDP它自带ACK确认、超时重传、块序号校验已经足够满足固件传输需求。第三UDP没有连接状态Bootloader重启后上位机可以立刻重新发包不会出现“TIME_WAIT”这种等半天的情况。我这边实际的方案是直接移植了TFTP Server到C6678的Bootloader里上位机用常见的TFTP客户端工具就能推送固件。像Tftpd64、或者自己写个Python脚本调tftpy库都很方便。移植时注意C6678是大端字节序TFTP协议头里的操作码、块号都要做字节序转换这个在后面踩坑部分细说。3.2 TFTP帧格式确认和内存搬运策略TFTP的读请求RRQ和写请求WRQ格式是固定的操作码2字节、文件名、0分隔符、模式octet/binary、0。在线升级场景下Bootloader这边是Server上位机是Client所以上位机发WRQBootloader回ACK然后上位机依次发DATA块Bootloader收到后回ACK。每个DATA块最大512字节。文件超过512字节就分成多块块号从1开始递增。最后一块不够512字节就表示传输结束。如果文件大小恰好是512的整数倍协议要求发送一个0字节的DATA块来结束传输——这个边界情况特别容易漏漏了之后Bootloader会一直等下一个块超时后才报错。实际写代码时我建议不要每收到一个512字节块就立刻擦写Flash。更稳的做法是在DDR里开一块接收缓冲至少1MB先积累到一定量再一次性写入Flash。原因很简单Nor Flash的擦除是以扇区为单位的典型扇区4KB或64KB频繁擦写会严重拖慢速度。千兆网卡一个512字节包可能只有几十微秒的间隔但Flash扇区擦除一次可能要几百毫秒如果一边收包一边擦写缓存区会被瞬间填满丢包几乎避免不了。一个现实的数据C6678通过千兆以太网接收DDR带宽足够TFTP传一个3.5MB的镜像大概需要7万多个包。如果每收到一个包就写一次Flash以常见的25MHz SPI Nor Flash或较慢的EMIF16 Nor Flash写速度来看可能要十几分钟。但按我说的“先收到DDR攒够1MB再批量写入”整个传输加烧录通常在1分钟左右完成。3.3 NET升级的协议帧自定义扩展TFTP能解决“文件怎么传”的问题但解决不了“这个文件是不是给我这块板子的”“版本对不对”“烧完后怎么验证”的问题。所以我在TFTP传输之前加了一层简单的握手协议。具体流程是上位机向板卡发送“升级握手请求帧”内容包括魔数0xC667、产品类型、当前版本号、目标版本号、镜像长度、镜像CRC32。Bootloader收到后回复“握手应答帧”内容包括板卡当前固件版本、是否允许升级、最大支持镜像长度。上位机确认对方允许升级后再发起TFTP写请求文件名固定为app.bin。TFTP传输完成后Bootloader对DDR里的镜像做一次完整CRC32校验把校验值和握手阶段收到的CRC32比对。一致则写入App B区写完后回读整片再校验一次不一致则直接丢弃保持原有App不变。这套流程看着多实际代码量不大但效果非常好。它把“传输错误”和“文件不匹配”这两类问题在写入Flash之前就拦截掉了。我见过很多团队的在线升级失败不是传输过程丢包而是上位机把别的工程编译出来的旧镜像发过去了没有握手校验的话Bootloader根本不知道文件不对照样擦Flash结果就是设备变砖只能拆机上仿真器。3.4 网络升级的实测参数我用一块C6678评估板搭配88E1111 PHY做了实测链路自适应协商到1000Mbps全双工。镜像大小3MB用TFTP推流结果如下项目数值单次512字节块传输平均耗时约0.4ms镜像接收总耗时约2.8sDDR缓存后批量写入Flash耗时约35s回读校验耗时约20s总升级耗时约60s这个耗时在实际工程里完全可以接受。真正让总时间拉长的往往不是网络传输而是Flash擦写和回读校验。Nor Flash写入速度本身就慢加上C6678的EMIF16接口访问Nor Flash时如果配置了等待周期速度进一步受限。想在同样硬件条件下缩短时间可以把Flash从EMIF16换成SPI接口的高性能Nor Flash或者用NAND Flash但这会改动硬件不在本文讨论范围。4. 串口通道升级救砖的最后一根稻草4.1 为什么串口升级依然不可替代网络升级快是快但它有一个前提Bootloader里的网卡驱动和协议栈运行正常而且现场的网络环境允许你访问设备的IP。真实工程里经常遇到的情况是设备新出厂时还没配置IP、网络模块损坏、现场只有一根调试串口线。这时候串口升级就是最后的救命通道。串口升级的速度确实慢115200波特率下每秒理论传输约11.5KB3MB的镜像需要接近5分钟。但串口升级的价值不在速度在“一定能用”。只要Bootloader里的UART初始化没问题哪怕网络协议栈写得完全不能用也可以通过串口把固件救回来。4.2 串口升级的波特率选择和流控C6678的UART是TI标准UART IP支持可编程波特率。升级用的波特率我建议固定为115200或460800不要选921600。看似921600更快但很多USB转串口线在高波特率下误差偏大特别是现场用的工控机、笔记本USB转串口芯片质量参差不齐921600下很容易出现偶发错位表现为“校验和总是差几个字节”。另一个坑是流控。硬件流控RTS/CTS在实验室里很好用但现场串口线不一定把这几根线都接出来。所以我的Bootloader串口升级固件只做“软件流控”或者干脆不做流控纯靠ACK确认和超时重发机制。实现方式很简单每收到一包数据Bootloader校验通过后回一个ACK字符0x06上位机收到ACK后才发下一包如果Bootloader校验失败回一个NAK字符0x15上位机重发当前包。这就是典型的XMODEM思路。自己实现一次之后你会发现这比用复杂协议更容易排查问题。4.3 串口升级中的Flash扇区对齐问题串口单包数据量比较小普遍取256字节或512字节。但前面提到了Nor Flash擦除是按扇区来的如果Bootloader收到256字节就写Flash会频繁擦同一个扇区速度极慢而且Flash有擦写寿命典型10万次这样用不了多久Flash就废了。解决方法和网络通道类似在DDR里开一个4KB缓冲凑满一个扇区大小再写一次。如果镜像最后不足4KB就先把剩余数据写入缓冲再补全为全0xFF后整体写入。这有个隐患镜像文件的实际大小和补全后的大小可能不一致所以必须在握手协议阶段把“有效镜像长度”传过来Flash的其他区域保持0xFF即可。启动时Bootloader也只读取有效长度范围内的内容做CRC校验不会多读。串口通道的具体跑法我建议这样上位机发送“串口升级开始帧”包含魔数、镜像长度、CRC32。Bootloader回ACK并主动擦除App B区所有扇区。上位机逐包发送数据每包256字节带包序号。Bootloader收到后先校验包序号是否连续再校验帧内CRC通过后写入DDR缓冲满4KB则写Flash。全部发完后Bootloader对Flash中的完整App做回读CRC与开始帧的CRC32比对通过则更新参数区启动标志。这里要特别提醒擦除App B区的动作放在收到开始帧后就执行不要放在数据传完之后。因为擦除时间比较久如果边收边擦DDR缓冲区很容易被突破。先擦完后面直接写反而更利索。5. 镜像格式、版本管理与掉电自恢复的工程化处理5.1 C6678的App镜像必须处理好段信息C6678是多核DSP8个核的代码可以编译成一个公共镜像也可以用多个.out文件分别生成各自的镜像。在线升级场景下我强烈建议用SPI或EMIF16的Boot Table格式把所有核的启动段打包成一个文件。打包工具用TI官方的hex6x或者新版CCS里的tiobj2bin配合boot image工具。一个容易出错的地方是Boot Table的入口地址和RBL的跳转地址必须匹配。C6678的RBL从Boot Table里解析出的entry point会写入对应核的程序计数器。如果你的Bootloader跳转App时是手动设置PC地址而不是走RBL解析那就要保证App的入口地址和编译链接时一致。实际操作中我在Bootloader里保留了一份“App启动描述表”放在参数区。描述表里记录App A和App B的起始地址、长度、入口地址、CRC校验值。这样Bootloader跳转前先去参数区查表比硬编码灵活很多。将来如果Flash分区调整只需要升级Bootloader和参数区App本体可以不动。5.2 版本号和校验值放哪里最稳妥版本号只放在App内部肯定不行因为Bootloader跳转前需要先判断版本总不能每次都要先把整个App读一遍解析吧。我的做法是在App镜像的最前面放一个固定长度的“镜像头”结构大致如下typedef struct { uint32_t magic; /* 0xA5A5C667 */ uint32_t version; /* 版本号如0x01020300表示V1.2.3 */ uint32_t length; /* 有效代码段长度 */ uint32_t crc32; /* 整个镜像的CRC32 */ uint32_t entry_point; /* 入口地址 */ uint32_t reserved[3]; } image_header_t;Bootloader只需要从Flash读出前32字节就能拿到版本号、长度和CRC。不需要把整个镜像读完就能做版本比较速度非常快。注意这个image_header_t本身不能参与CRC32计算CRC是“头之后的数据”的校验值因为你在计算CRC时还没法确定头部CRC字段的值。5.3 升级掉电自恢复的完整状态机在线升级最怕的就是升级到一半断电。如果没有状态机保护Flash里可能是一个写了一半的镜像Bootloader启动时校验失败设备直接变砖。有了A/B双分区还不够必须配合参数区里的升级状态来引导启动流程。我的状态机设计如下状态值含义启动时的动作0x00正常状态启动当前激活分区0x01升级准备中启动当前激活分区等待上位机连接0x02正在写A区禁止启动A区若A区是激活分区则回退到B区0x03正在写B区禁止启动B区若B区是激活分区则回退到A区0x04升级校验中只允许进入Bootloader升级模式0x05升级完成启动新分区正常状态每一步写Flash操作之前先更新状态值。更新方式是擦除参数区状态字段所在的扇区然后写入新状态。不要用“读-改-写”因为掉电可能出现在“读”和“写”之间导致数据还是旧值。必须保证状态字段是“独享一个扇区”或“独享一个页”这样才能用擦除加写入的方式原子更新。这片Flash参数区还有一个用途记录“最近一次启动成功”。Bootloader在跳转进App之前先把当前分区标记为“待启动”App正常运行30秒后主动向参数区写一个“启动成功”标志。如果Bootloader发现上次“待启动”没有变成“启动成功”说明App运行异常自动回退到另一个分区。这个机制能兜底一类非常隐蔽的bug新镜像能通过静态CRC校验但一跑起来就崩溃如果没有这个“运行后确认”机制设备会反复启动崩溃镜像只能人工干预。5.4 串口救砖小工具还是得留一手即使有A/B分区和状态机也保不齐调试期间把Bootloader自己写坏了。这时网络升级和串口升级都用不了唯一办法是仿真器重新烧录。但仿真器并不是现场标配所以我会在硬件上预留一个“强制恢复模式”引脚。这个引脚的逻辑是Bootloader启动时检测到该引脚为低电平就跳过所有校验逻辑直接从Flash固定地址比如App B区的顶部加载一个“最小恢复镜像”。这个恢复镜像只包含UART驱动和最基本的Flash操作函数通过串口收一段极小的升级指令就能把Bootloader区重新刷写回来。代码量不大几百行C搞定但救过我好几次强烈建议保留。6. 实测踩坑记录这些坑比想象中多6.1 Cache一致性引发“回读CRC总是对不上”第一次调试C6678在线升级时我遇到了一个非常诡异的问题。Bootloader把镜像通过EMIF16写入Nor Flash后紧跟着回读CRC校验结果怎么比对都是失败。但是用CCS的Memory Browser手动去读Flash数据明明是对的。排查到最后根因是C6678的L1D/L2 Cache。Bootloader里初始化了Cache写Flash时CPU会把数据写入Cache而EMIF16外设控制器访问的是物理地址。如果没有做Cache clean操作Cache里的数据可能还没有真正刷到内存总线上去回读时CPU又从Cache里读到旧数据两边就对不上。解决办法很简单但很容易漏写Flash操作前执行CACHE_wbL2把L2 Cache写回回读校验前执行CACHE_invL2把Cache无效化强制从内存重新加载。C6678的CSL库提供了CACHE_wbAll和CACHE_invAll这类函数直接调用就行。这个坑对很多人来说很隐蔽因为单片机上根本没有Cache这回事。到了C6678这种高性能DSP上Cache一致性是绕不开的基本功不只是升级功能任何DMA和CPU共享数据的场景都会遇到。6.2 大端字节序让TFTP包校验失败C6678默认是大端模式也就是内存里的高字节存低地址。而TFTP协议是一个继承自上世纪的标准网络协议它在RFC文档里明确定义了字段都是网络字节序也就是大端。理论上C6678大端处理器处理TFTP天然合适但问题出现在上位机。我最早用Windows上某个TFTP工具做测试它发送的DATA块块号是以小端方式填充的。Bootloader按大端解析块号结果永远是0x0100而不是0x0001导致块号连续性判断永远失败反复重传。后来我改用Python的tftpy库在客户端里手动指定字节序问题才解决。这里不是某个工具的问题而是提醒大家自定义协议和标准协议混用的时候字节序一定要在联调前达成一致。更保险的做法是Bootloader里对块号字段同时做大端和小端解析如果两种解析值有一个连续就按对应的方式继续这被称为“宽严相济”的容错设计。6.3 千兆网卡第一次收发前要等待自协商完成我调试网络升级时遇到过这样一个情况Bootloader启动后立即初始化EMAC并打开TFTP服务上位机ping设备不通但等个十几秒后又正常了。原因不是代码逻辑问题而是PHY芯片的自动协商没有完成。C6678通过SGMII连接到PHYPHY上电后需要一段时间完成和交换机或PC网卡的自协商协商期间链路状态是Down的。如果Bootloader在自协商完成之前就去操作EMAC后面的状态机可能进入一个错误分支导致始终收不到包。解决方案有两种。一种是在初始化网络前轮询PHY寄存器的链接状态位等它变成Link Up之后再继续。另一种是复位PHY后加一个1到2秒的延时实测下来效果也可以但不严谨。我是按第一种来的读取PHY的Basic Status寄存器地址0x01的第2位Link Status该位为1才继续。注意Link Status位是锁存的读取时如果状态改变了需要用“先读再读”的方式清除锁存具体可以参考PHY芯片的数据手册。6.4 串口升级中途出现“最后一个包永远发不出去”串口升级时有一个边界条件让我印象很深。上位机发送的镜像长度正好是256字节的整数倍比如1MB镜像按256字节分包正好4096包。最后一包发出去后Bootloader校验包序号和处理逻辑结束回ACK然后启动回读校验。但由于最后一块数据长度等于包长上限Bootloader的协议状态机里可能有一个“等下一包”的逻辑分支导致它不会自动进入“接收完成”状态。我的解决方式是在协议里显式增加一个“传输结束帧”长度字段为0。上位机在所有数据包发完后再发一个结束帧。Bootloader收到结束帧先确认前面所有包的序号都连续、CRC都正确然后才开始回读校验。这样无论最后一块是否满包都能正确收尾。6.5 擦写Flash时被看门狗打断C6678的Bootloader里通常会开硬件看门狗防止程序跑飞但擦写Flash是个耗时的操作尤其是擦除一个64KB扇区可能耗时几百毫秒到一秒。如果看门狗超时时间设置太短擦写过程中会被看门狗重启导致升级流程反复重启。我建议Bootloader里升级期间的看门狗策略有两种选择要么在进入升级模式后直接关闭看门狗要么在看门狗喂狗服务程序里登记“正在擦写”标志如果正在擦写喂狗函数就暂时不重置计数器等擦写完成后再恢复正常。更推荐第一种因为Bootloader升级模式本来就是一个受控环境人为操作也不频繁关掉看门狗反而省心。但要注意跳转到App之前必须把看门狗恢复为初始状态并重新启动否则App里的看门狗配置会受影响。最后再分享一点我做升级功能的经验在线升级这件事做完功能只是第一步真正的考验是“敢不敢在客户现场用”。我见过不少产品升级功能写得天花乱坠最后真出事的时候连Log都没有完全不知道设备在哪个环节挂掉的。所以我的习惯是参数区里至少存一个8字节的升级日志每完成一个关键步骤写入一个事件码比如“擦除完成”“写入完成”“校验失败”“回退启动”。以后任何一台设备升级异常只要拆开机箱读一下参数区日志问题定位就在半小时以内。另一个建议是上线前一定要做一次“断电打靶测试”。升级过程中随机拔电连续测几十次每次拔电后上电都要求能自动回退到旧版本正常启动。这种测试很伤Flash寿命所以用测试板来测别拿正式设备的板卡测。但这一步能筛掉绝大多数设计漏洞性价比极高。最后想说的是网络升级和串口升级不是二选一的关系。网络负责快串口负责稳两者共用同一套Bootloader和Flash分区设计代码架构上把“传输层”和“存储层”分开后续不管加什么传输介质——USB、PCIe、SRIO——都只需要替换传输层核心的镜像校验、A/B分区、状态机代码可以整体复用。TMS320C6678这块芯片虽然老但在工业设备存量市场里还大量服役把它的在线升级做扎实是件长期有价值的事。