拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C6678 SPI Boot烧写指南:.out转bin与避坑

C6678 SPI Boot烧写指南:.out转bin与避坑 前阵子调一块TMS320C6678的板子仿真器下一切正常下载、单步、全速跑都没毛病客户一句“总不能一直挂仿真器”把我拉回现实——C6678这种DSP芯片本身没有片内非易失存储要想上电自启动绕不开SPI NOR Flash烧写这条链路。而这条链路比想象中要长得多编译出来的是.outFlash里要的却是.bin中间还得处理Boot Table格式、转换参数、烧写程序、回读校验每一步都有坑。这篇文章把我从“仿真器跑得好好的”到“脱机上电稳定启动”的完整过程整理出来重点讲清楚两件事第一.out怎么正确转成SPI Boot能认的.bin第二烧写过程中那些让人抓狂的坑是怎么一步步排查出来的。无论你是刚拿到C6678开发板准备做启动引导还是量产阶段需要固化固件这篇文章都能让你少走不少弯路。1. 先搞懂SPI Boot烧写自然就不慌1.1 C6678上电后发生了什么C6678是TI的8核C66x DSP片上有大量SRAM每核L2有1MB共享MSMC有4MB但没有任何非易失Flash。也就是说芯片一上电内核里其实是没有用户代码的。它内部有一个固化的ROM Bootloader上电后CPU先执行这段ROM代码然后根据BOOTMODE[12:0]引脚在复位时的电平组合决定下一步从哪个介质加载程序。BOOTMODE可以选择No Boot等仿真器连接、SPI Boot、EMIF16 Boot、SRIO Boot、PCIe Boot等好几种方式。对大多数板卡来说最经济实用的就是SPI Boot——外挂一颗SPI接口的NOR Flash成本低、布线简单、启动速度也够用。理解了这一点就明白为什么要有“烧写Flash”这件事你的程序跑在SRAM或DDR3里但断电就丢要让它每次上电都能自动跑起来就必须把程序以特定格式放到那颗外部NOR Flash里让ROM Bootloader上电后能读出来、搬到RAM里、再跳过去执行。C6678的ROM Bootloader在SPI Boot模式下的加载流程大致是这样初始化SPI0外设从SPI0的片选0CS0所挂的NOR Flash起始地址开始读取解析一种叫Boot Table的数据结构把数据块搬到对应的内存地址全部搬完后跳转到入口地址执行。这个过程不需要用户参与但前提是Flash里的数据格式必须完全符合Boot Table规范而且目标内存必须已经可用。1.2 SPI Boot的Boot Table格式Boot Table是C66x DSP从串行Flash启动的核心数据结构。它的本质是“带地址的镜像文件”——普通bin文件只是一堆连续的数据不知道哪段该放哪Boot Table则把可执行程序按段拆开每一段都带上目的地址和长度信息Bootloader读到后就知道直接把数据拷到哪个地址。每一段记录的典型结构是32位目标起始地址、32位数据长度、然后是长度对应的数据内容数据不足32位对齐时会填充到字边界。多个段记录依次排列全部段结束后有一个结束标记。为什么要理解这个格式因为在排查启动失败时最直接的方法就是把Flash里的数据读出来人工对照Boot Table格式检查是否正确。如果你根本不知道正确格式长什么样就只能瞎猜。实际排查中我用Hex Editor打开烧写后的Flash数据对照编译出来的段地址一核对问题立刻现形。这块后面避坑实录里会细说。1.3 为什么必须用hex6x重新打包而不是直接改后缀很多新手第一反应是把.out文件改个后缀名不就是.bin了吗或者拿一个通用的ELF转bin工具转换一下我刚开始也试过结果烧进去完全起不来。原因在于.out是ELF/DWARF格式里面除了代码和数据还包含大量的符号表、调试信息、段定义、重定位表。这些信息是给链接器和调试器用的不是给Bootloader用的。直接把ELF的二进制内容搬到Flash里Bootloader读到的是大量无意义的数据。更关键的是SPI Boot需要的是Boot Table格式而通用转换工具生成的是“连续地址镜像”或者“纯代码段拼接”都不带Bootloader需要的地址映射信息。TI的hex6x工具则专门处理这个过程它会解析.out文件里的各个可加载段按照C66x Boot Table规范重新组织生成一个按固定格式排列的二进制文件。这个文件才是真正可以烧进SPI NOR Flash的东西。换句话说从.out到.bin不是一次“改名”而是一次“格式重打包”。理解了这点就不会用错工具了。2. hex6x转换实战一条命令把.out变.bin2.1 工具版本和调用方式hex6x是TI C6000编译器工具链CGT自带的转换程序。装好CCS之后它通常位于CCS安装目录下的compiler子目录里。我的机器上路径是这样的C:\ti\ccs931\ccs\tools\compiler\ti-cgt-c6000_8.3.2\bin\hex6x.exe不同的CCS版本、CGT版本路径会略有差异建议直接用文件管理器搜索hex6x.exe确认一下。另外注意C6000系列的hex工具是hex6x不是ARM系列的hex470也不是MSP430的mhex430别下错工具。调用方式有两种一种是直接把参数写在命令行里另一种是把参数写成一个选项文件后缀习惯用.cmd然后让hex6x读取这个文件。命令行适合偶尔转一次选项文件适合工程化使用——参数可维护、可纳版本管理。2.2 命令参数逐项解析先给一个我在C6678 SPI Boot场景下实测可行的转换命令hex6x.exe -boot -bootorg SPI -spi 4 -b app.out -o app.bin各参数含义如下参数含义说明-boot生成Boot Table格式核心参数少了它生成的bin完全不能用-bootorg SPI指定引导设备为SPIhex6x会根据这个参数生成SPI Boot能识别的表格式-spi 4SPI引导表字宽为32位这是C66x SPI Boot的常规配置4字节对齐-b输出二进制格式生成.bin而非hex文本格式-o指定输出文件名默认文件名可能不带.bin后缀建议显式指定有几个参数需要单独展开说。-boot是整个命令的灵魂。它指示hex6x把section数据重新封装成Boot Table。不加这个参数时hex6x只是把段数据按顺序拼接输出虽然也是.bin文件但Bootloader按Boot Table格式去解析时完全对不上。很多“转换了也启动不了”的案例根源就是少了-boot。-bootsection参数可加可不加但用了可以更精细地控制。有些工程会把.boot_load段专门定义出来放启动相关代码这时可以用类似这样的形式指定hex6x.exe -boot -bootorg SPI -spi 4 -bootsection .boot_load 0x0C000000 -b app.out -o app.bin-bootsection后面跟的是段名和地址含义是告诉hex6x生成Boot Table后把这张表放到名为.boot_load的段里并把这个段的装载地址设为0x0C000000。如果不太确定自己的段名怎么定义的直接用不带-bootsection的简洁命令即可。用选项文件的方式可以写成这样--boot --bootorgSPI --spi4 --binary --outputapp.bin app.out然后执行hex6x.exe hex6x_config.cmd这种写法的优点是把转换参数固化下来不容易漏。我后来都改成这种模式了因为项目里固件更新频繁每次手动敲一串命令很容易出错。2.3 转换结果怎么验证转出来的app.bin到底对不对不能直接烧先验证一下。用二进制查看器打开app.bin你会看到文件开头不是程序的入口代码而是一串看起来“有规律”的数据。对照Boot Table格式前4字节应该是一个32位地址紧接着4字节是长度然后是数据。如果你编译时把程序链接到了0x0C000000MSMC地址文件开头应该能看到类似0C 00 00 00这样的地址字节序排列。同时用一个叫hexdump的命令可以快速查看hexdump -C app.bin | head -n 20另外一个有用的验证手段是看文件大小。假设你的.out编译出来代码段有80KB数据那.app.bin的大小应该是地址(4) 长度(4) 数据(80KB不足字边界补齐) 可能的段间填充 结束标记整体会比原始段数据略大一点点。如果转换出来的bin文件大小和.out文件差不多或者小得离谱就要怀疑参数是不是漏了。需要说明一点不同CGT版本对Boot Table的具体格式细节可能有细微差别比如结束标记字节序、填充策略这些不用死记硬背。转换后保留好hex6x生成的.map文件可以加-map app.map参数生成里面记录了Boot Table的布局排查问题时非常有用。3. 烧写程序的核心设计让bin稳妥地进Flash3.1 Flash选型与硬件连接注意C6678的SPI Boot对NOR Flash本身没有严格的“专用型号”要求市面上常见的SPI NOR Flash基本都能用Micron的N25Q系列、Winbond的W25Q系列、ST的M25P系列都兼容标准SPI指令集。我板子上用的是一颗W25Q128128Mbit容量16MB空间存一个小型DSP固件绰绰有余。选型时有几个点要特别注意。第一是供电电压C6678的SPI端口电平是1.8V还是3.3V取决于板级设计Flash的VCC和I/O电平必须匹配电平不一致会导致通信不稳定甚至烧坏器件。第二是片选连接SPI Boot默认从SPI0控制器的CS0启动你的Flash片选必须接到SPI0_SCS0引脚。第三是写保护引脚WP和保持引脚HOLD这两个引脚在正常读写时要处于非激活状态硬件上最好直接上拉到高电平否则程序写一半Flash进入保持模式数据就错乱了。3.2 SPI初始化与NOR操作指令烧写程序本质上就是一个“通过SPI操作NOR Flash的裸机程序”。它运行在DSP的RAM里通过仿真器加载运行完就完成任务。它的SPI初始化要求不高但有一些细节值得留意。C6678的SPI外设初始化核心是这几件事复位SPI控制器、配置为主模式、设置时钟极性/相位、设置数据传输位宽、设定分频系数。时钟相位极性要和你的Flash匹配绝大多数SPI NOR Flash支持模式0CPOL0CPHA0和模式3CPOL1CPHA1我用的是模式0。NOR Flash的标准指令集在不同厂商间基本兼容下面是几个最核心的操作码指令操作码说明Write Enable0x06写使能任何写操作之前必须先发Read Status0x05读状态寄存器判断WIP忙标志Read Data0x03普通读低时钟速率下使用Page Program0x02页编程单次最多写256字节视型号Sector Erase0xD864KB扇区擦除Read JEDEC ID0x9F读3字节厂商标识SPI发送接收的核心函数很直接把要发的字节写入数据寄存器等待接收寄存器非空再读回数据。C6678的SPI是全双工的发一个字节的同时会收到一个字节这就是为什么读数据时主机要持续发0x00。3.3 烧写主流程与回读校验烧写程序的主流程可以概括为初始化SPI → 识别Flash ID → 擦除目标扇区 → 按页写入数据 → 回读校验。一个关键的坑是bin文件本身在程序里怎么访问。我用的方案是把bin转成C语言数组直接编译进烧写工程。小固件几百KB以内用这种方式最方便。用Python几行代码就能把bin文件转成C数组with open(app.bin, rb) as f: data f.read() with open(app_bin.c, w) as f: f.write(const unsigned char app_bin[] {\n) for i in range(0, len(data), 12): chunk data[i:i12] f.write( , .join(f0x{b:02x} for b in chunk) ,\n) f.write(};\n) f.write(fconst unsigned int app_bin_len {len(data)};\n)烧写主流程的C代码骨架大致长这样int flash_write_all(uint32_t flash_base, const uint8_t *buf, uint32_t len) { // 1. 擦除需要覆盖的扇区64KB一扇区 for (uint32_t off 0; off len; off 64 * 1024) { nor_sector_erase(flash_base off); } // 2. 按页写入标准页256字节 for (uint32_t off 0; off len; off 256) { uint32_t chunk (len - off) 256 ? 256 : (len - off); nor_page_program(flash_base off, buf off, chunk); } // 3. 回读校验 uint8_t check[256]; for (uint32_t off 0; off len; off 256) { uint32_t chunk (len - off) 256 ? 256 : (len - off); nor_read(flash_base off, check, chunk); if (memcmp(buf off, check, chunk) ! 0) { return -1; // 校验失败 } } return 0; }注意扇区擦除和页编程之间一定要等Flash内部操作完成也就是轮询读状态寄存器直到WIP位清零。NOR Flash擦除一个扇区可能耗时几百毫秒到一两秒不等页编程通常在一毫秒以内但都必须等待不能偷懒。回读校验是烧写流程里绝对不能省的一步。SPI通信本身可能因为电平噪声、时钟过快等因素出现偶发错误只有“读回数据与源数据完全一致”才能确认烧写真正成功。如果校验失败程序会返回出错位置方便定位是哪段地址出了问题。整个烧写的操作流程在实际项目中是这样的编译烧写工程 → 通过CCS/XDS仿真器加载到DSP的MSMC或L2 SRAM里运行 → 运行后程序把app_bin数组写入Flash → 完成后串口打印校验结果 → 校验OK则断电、断开仿真器、把BOOTMODE拨到SPI Boot、重新上电。整个过程不碰用户的业务代码烧写程序和业务程序完全独立互不影响。4. 避坑实录从“上电不起”到“稳定启动”的完整排查4.1 BOOTMODE拨码不对一切白搭第一次烧写完成后我信心满满地断电、拔仿真器、把BOOTMODE拨到SPI Boot然后上电——板子毫无反应LED一个都不亮串口也没输出。第一反应是Flash没烧对但回读校验明明通过了。仔细一想才意识到问题可能根本不在Flash内容而在C6678根本没有进入SPI Boot模式。BOOTMODE是12根引脚组成的配置总线的复位采样值不只是“SPI还是非SPI”一个选项它还包括端序小端/大端、PLL配置、PCIe模式等多个字段。查阅板卡的原理图和C6678数据手册里的Boot Mode表格后发现我拨码开关的组合虽然把启动设备选成了SPI但端序位配置成了大端而固件是编译成小端的Bootloader按照大端方式解析Boot Table自然全错。这个坑的教训是烧写之前先确认BOOTMODE的组合值完全符合硬件设计文档和数据手册要求不要单凭“拨到SPI就行”。不同板卡对应BOOTMODE的物理开关位置也不一样必须对照原理图逐一确认。4.2 转换参数少一个-bootbin文件立刻变废另一个让我印象深刻的坑是转换参数踩坑。有一次我为了验证一个替换固件图省事用了一个通用脚本直接把.out里的段提取出来拼成bin也没走hex6x结果烧进去后跟预期一样——启动失败。排查时我用烧写程序里的Flash读取函数把Flash起始的256字节读到内存里然后通过CCS的Memory窗口查看。发现数据内容和我烧进去的bin完全一致没有任何“格式重组”的迹象没有Boot Table的地址、长度结构开头直接就是代码数据。对照boot table应有的结构就明白了这个bin缺少段地址和长度信息Bootloader把它当作Boot Table解析第一项把地址字段和长度字段当成了“起始地址0x某个值和长度0x某个值”要么加载到错误地址要么长度错误直接放弃。所以转换必须走hex6x并带上-boot参数这是铁律。任何“手动拼接”或者“通用工具转换”的方案在这个场景下都不可靠。4.3 链接地址放在DDR3脱机后CPU根本取不到代码这是最隐蔽、最能解释“CCS仿真正常脱机完全死寂”的坑。我的业务程序原来是为了调试方便把所有代码段链接在DDR3地址空间0x80000000开始的区域。CCS仿真时调试器负责初始化DDR3控制器加载程序后再跑一切都正常。但SPI Boot时ROM Bootloader只做了一件简单的事按Boot Table把数据搬到目标地址。它不会替你初始化DDR3的PHY和控制器。结果就是Bootloader把代码“搬”到了0x80000000但DDR3控制器处于未初始化状态那段物理地址根本不可寻址。CPU复位后跳到目标地址执行等于访问了一块“不存在”的内存总线错误程序彻底飞了。排查到这一步时我先用CCS连接仿真器手动初始化DDR3后去读0x80000000发现数据确实在——说明Boot Table本身没问题问题出在DDR3不可用。最终解决方案是把业务程序的链接脚本改成把代码段放到MSMC0x0C000000MSMC是片上SRAM上电即可访问不需要初始化。程序不大放MSMC完全够用。如果你的固件很大必须跑在DDR3里那需要引入二级引导程序如TI的IBL步骤是先在Flash的低地址放一个小的二级引导它上电后先初始化PLL和DDR3再从Flash后面加载大固件到DDR3并跳转。这是另一种玩法了但核心思路就是“先让CPU跑起来一个能初始化DDR3的小程序”。4.4 Flash写保护没解除回读永远是FF还有一次遇到的现象很诡异烧写程序把数据“写完”后回读校验的前半段通过后半段全是0xFF。一开始以为是地址越界或者页编程长度有误排查了很多天。后来用逻辑分析仪抓SPI总线发现写入命令序列完全正常Flash也返回了成功状态。直到我翻到Flash数据手册里状态寄存器那页才意识到问题可能出在写保护位。有些SPI NOR Flash出厂时状态寄存器的BP位可能处于非零状态把整个芯片或部分区域设为写保护。这时候发Write Enable虽然有响应但实际编程/擦除操作会被Flash忽略。解决方法是在擦除之前先读取状态寄存器检查BP位如果有保护就发送写状态寄存器指令0x01把BP位清零同时确保WP引脚硬件上拉到高电平。这之后再进行擦除、编程操作。其实这个坑和硬件设计还相关如果WP引脚悬空或拉低Flash会一直处于硬件写保护状态再怎么样也写不进去。硬件设计时WP和HOLD必须正确处理否则后续量产烧写会非常痛苦。4.5 扇区擦除和页编程的边界问题NOR Flash的特性是擦除以扇区为单位写则只能把1写成0反过来不行。这意味着每次擦除必须覆盖整个扇区而你的程序数据长度大概率不会正好是扇区大小的整数倍剩下未擦除的部分可能残留旧数据。比如bin长度是100KB你只擦除了前两个64KB扇区128KB数据写入后没有问题但如果bin长度恰好是130KB你只按len向上取整到3个扇区去擦第一个扇区擦掉了第三个扇区擦到一半边界残留数据就会和合法数据混在一起。我的做法是不管bin长度多少先计算需要擦除的扇区数确保覆盖全部数据区间多余的空白数据填充为0xFF。然后再按页编程写入。编程时还要注意每页不能跨256字节边界写如果一页的剩余空间不够下一段数据就得从下一页重新开始。页编程边界问题在数据量大了以后很容易被忽略但如果你的固件数组有两个位于同一页的数据块程序逻辑没有做分页处理一次页编程就会写进错误的Flash位置。把“按偏移计算当前所属页、单页不足则拆分”这个逻辑提前写好能省很多烦恼。4.6 SPI时钟分频太高偶发校验失败最头疼最后说一个最难排查的软故障偶发校验失败。不是每次都失败而是十次里偶尔一次失败位置还不固定。一开始我怀疑是电源噪声或者Flash质量问题换了一颗Flash后依然如此。后来把SPI时钟从高速降下来问题就消失了。原因其实很朴素C6678的SPI主设备时钟分频配置得太高Flash的响应速度跟不上或者信号完整性在高速翻转时出现问题导致某个字节读回错误。SPI NOR Flash通常支持几十MHz的读取时钟但写入和擦除尤其是内部操作对时序要求没那么高。在烧写场景里完全没必要把SPI时钟跑到极限。把分频系数调低让SPI时钟控制在20MHz到30MHz左右稳定性会好很多。这个参数在SPI初始化时配好即可对烧写速度的影响几乎可以忽略。如果你的板子信号完整性不好排查这类偶发问题时可以先从降低SPI时钟试起再用逻辑分析仪观察波形确认数据线上的建立保持时间是否充足。5. 跑通之后我建议你固定下来的几个习惯整个流程走通后我把下面几件事固化成了项目里的标准操作确实省了后面很多事第一把hex6x转换参数写成.cmd选项文件纳入工程版本管理。这样每次编译完固件执行一条命令就能生成对应的.bin不会因为漏参数或者手误导致生产一个废固件出来。第二转换时加上-map参数生成转换后的map文件和bin文件一起归档。后面一旦需要排查“启动后跑飞”问题这个map文件能看到Boot Table各段的位置比对着Hex Editor猜地址高效得多。第三烧写完成后强制回读校验并打印每个扇区的校验状态。校验不是“可有可无的保险”它是确认整个链路可靠性的最后一道关口。哪怕产品已经量产我也会在烧写工装里保留回读校验这一步宁可多花一秒也要确保每一片板子烧进去的固件是正确的。第四烧写程序独立于业务工程维护。烧写程序运行在RAM里不影响业务固件它只需要SPI驱动、Nor Flash命令函数和串口打印即可保持简单可靠。最后每次给新板卡做首次烧写时不要急着上电验证先按顺序自查BOOTMODE配置、flash焊接方向、WP/HOLD电平、SPI时钟速率、hex6x参数。这五样东西有一项不对后边排查的时间都是以小时计的。我个人最大的体会是C6678从仿真器跑通到脱机启动真正的分水岭不是你会不会写代码而是能不能把“编译→转换→烧写→校验→引导”这条链路完整地打通。只要链路通了一次后面所有固件迭代都只是重复执行而已。希望这篇记录能帮你把这条链路一次走通。
返回列表