ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C54x DSP Bootloader实战:TMS320VC5415/5416从Flash启动设计与实现

C54x DSP Bootloader实战:TMS320VC5415/5416从Flash启动设计与实现 简介面向TMS320VC5416数字信号处理器开发者的引导加载实验资料包以ICETEK-VC5416-C实验板为对象覆盖微处理器工作方式、自启动方式、闪存扩展与烧写流程、自启动程序设计等学习目标适合正在学习引导加载、准备课程设计或需要为自制板卡实现上电自启的读者。压缩包共13个文件整体仅64KB包含十六进制转换工具、CCS工程文件、C语言源程序、链接命令文件、映射文件、可执行映像及编译日志覆盖从源码编译、内存映射、格式转换到烧写验证的完整中间产物。已有210人学习下载。通过源码与链接命令可理解启动代码如何初始化系统借助映射文件可核对段地址分配参考编译日志与十六进制转换工具可掌握烧写镜像生成过程。若实际烧写闪存或搬运程序时遇到异常也能依照这套资料快速定位问题为后续二级引导或固件开发提供可复用的实验模板。1. 为什么 TMS320VC5415 实验必然绕不开 BootloaderTMS320VC5415/5416 这类 C54x 定点 DSP 的片内 RAM 断电即失程序只有放进外部并行 NOR Flash 才能在单板上自举运行。上电后芯片内部固化的 Bootloader 会从 Flash 逐段读取用户代码搬进 RAM再跳到入口地址执行——这个过程一旦不成功表现就是DSP 没反应、IO 电平不动、JTAG 又能连得上。很多第一次在 C54x 上把程序从仿真器搬到 Flash 的工程师最容易卡在这一步。本文用 C 语言实现一个最小 Bootloader 实验覆盖 boot table 结构、EMIF 与 Flash 的接线换算以及最常踩的地址偏移和启动时序问题。内容以 TMS320VC5416 为主体展开但 5415 与 5416 的 Bootloader 机制同源代码稍改寄存器即可通用于 TMS320VC5415。这个实验适合三类人刚把 CCS 工程跑通、想脱离仿真器独立运行的新手要把产品从 JTAG 调试模式转向 Flash 启动的嵌入式工程师以及需要给 DSP 加在线升级功能的开发者。后面的方案都用 C 语言写不依赖手写汇编也能看清启动流程。所有步骤围绕“从 Flash 启动”这一件事展开照着做就能跑通。2. 理解 TMS320VC5415 / 5416 的 Bootloader 机制与 Boot Table 格式2.1 从 MP/MC 引脚开始的启动流程C54x 系列 DSP 复位后从程序空间的 0xFF80 处取复位向量但具体走哪条启动路径由 MP/MC 引脚的电平决定。这个引脚在TMS320VC5416数据手册里叫 MP/MC在TMS320VC5415上同样存在作用是选择微处理器模式还是微计算机模式。MC0接地时内部 ROM 被使能复位向量指向芯片出厂固化的 Bootloader 代码MC1接高时内部 ROM 被禁用CPU 直接从外部 EMIF 空间的 0xFF80 位置开始取指。实验板最常见的接法是 MC 接地Flash 挂在 CE1 空间。这样上电后片上 Bootloader 先识别外部并行启动模式从 CE1 起始地址处寻找 boot table 关键字 0x10AA找不到就进入死循环等待。调试时先用万用表量一下 MP/MC 引脚电平再查 Flash 首地址的 16 位数据是否为 0x10AA这一步能排除一半的“启动失败”。我之前遇到过一块 5416 的板子MC 引脚被一个 GPIO 输出驱动上电瞬间电平还没稳定Bootloader 已经跑偏了——量的是静态电平错的是上电时序。2.1.1 Bootloader 选择外部并行启动的关键寄存器片上 Bootloader 运行后会读一组寄存器确定启动模式。TMS320VC5416 通过 INT3 到 INT0 的电平组合或者 IO 空间的某些引脚状态来选择外围设备外部并行 Flash、HPI 主机引导、串口引导等。实验中固定使用外部并行 Flash 模式所以不需要动态判断模式。Bootloader 搬运前会先把 SWWSR软件等待状态寄存器和 BSCRBank 切换控制寄存器设为默认值等工作完成后跳转到用户程序。用户程序入口处的第一条代码通常要重新初始化这两个寄存器否则后续访问外部存储器时会按 Bootloader 设的保守时序运行读写速度偏慢但不至于出错。2.2 Boot Table 数据格式0x10AA 关键字与段布局的对应关系外部并行启动模式下Bootloader 从 Flash 的第一个 16 位字读启动表关键字固定是 0x10AA。这个关键字相当于整个启动流程的入场券读不到它后面所有段都不会被搬运。实验中最常犯的错是把 boot table 写到 Flash 的偏移 1 或偏移 2 位置或者 8 位 Flash 按字节写入后关键字变成 0x10 0xAA 两个字节顺序搞反。下面是一个典型的 boot table 在 Flash 中的排列方式偏移字内容说明00x10AA启动关键字固定值1段长度 N该段包含的数据字数不含段头本身2段起始地址目标 RAM 的 16 位地址3(可选) XPC 页号目标位于扩展程序页时出现3 至 3N-1段数据按 16 位字排列的代码或数据后续下一段重复“长度-地址-数据”的段结构结束处0x0000段长度为 0表示 boot table 到此结束结束处之后入口 PC用户程序的入口地址低 16 位入口之后(可选) 入口 XPC入口地址的高 7 位页号这个格式的关键点所有地址和数据都以 16 位字为单位计数不是字节。TMS320VC5416 的程序地址空间有扩展页机制超过 64K 字的部分用 XPC 寄存器的高 7 位做页选择boot table 中的 XPC 字段告诉 Bootloader 把段放到哪一页。很多用 C 语言做实验的人会忽略 XPC代码刚好落在低 64K 页内Bootloader 照常能跑。可一旦工程启用了外部程序段跳转就莫名失败——你的入口在 0x18000Bootloader 却把它当作低页地址写入 PC。提前把 XPC 考虑进 boot table 设计实验代码的通用性会好很多。3. 用 C 语言写 Bootloader 前必须处理的三件事3.1 链接脚本与存储器映射Bootloader 本体不能放 Flash用 C 语言写 Bootloader第一步不是写逻辑而是改.cmd链接脚本。Bootloader 自身的代码必须放在片内 DARAM 里不能留在 Flash 中执行。原因有两条。第一NOR Flash 的随机读速度比 DARAM 慢一个数量级CPU 直接从 Flash 取指会拖慢整个启动流程而且每次读 Flash 都要插入等待周期时序上更容易出问题。第二Bootloader 要擦写 Flash而 NOR Flash 在编程/擦除期间内部写状态机会占用数据线如果当前执行的指令还在同一片 Flash 上取指会产生不可预期的读错——严重的直接死机。因此 cmd 文件的思路是 Bootloader 代码段全部定位到 DARAM。以 TMS320VC5416 为例最小配置如下/* boot.cmd 链接脚本 */ MEMORY { DARAM_PROG : origin 0x100, length 0x3F00 /* 程序段放到片内 DARAM */ DARAM_DATA : origin 0x4000, length 0x4000 /* 数据段单独放 */ FLASH_CE1 : origin 0x400000, length 0x80000 /* CE1 挂的 Flash */ } SECTIONS { .text DARAM_PROG .data DARAM_DATA .bss DARAM_DATA .stack DARAM_DATA }origin和length按实际芯片手册调整即可。注意.text段放 DARAM_PROG意味着所有函数都从 RAM 取指FLASH_CE1这个区域只用来存放 boot table 数据和应用程序镜像。如果你在 Bootloader 里声明了大型const数组编译器可能把它们放进.const段该段默认合并到.data不会出问题。真正的坑在于const数组太大把 DARAM_DATA 撑爆链接报错时先查.map文件里哪个段超限。3.2 EMIF 数据宽度与 Flash 地址线的换算TMS320VC5416 的 EMIF 有 CE0 到 CE3 四个片选空间每个空间可以独立配置为 8 位、16 位或 32 位数据宽度。实验中默认以 16 位总线接 NOR Flash这是最不容易出错的组合。16 位模式下的关键换算只有一条DSP 的 A0 地址线不接 Flash 的 A0Flash 的 A0 接到 DSP 的 A1 上。DSP 内部看到的地址是字地址16 位为单位Flash 侧看到的是字节地址8 位为单位。16 位总线下DSP 每发出一个字地址对应 Flash 的两个字节地址。这个映射关系写进 C 语言宏就是一次左移一位/* emif_flash.h 地址换算宏 */ #define FLASH_BASE 0x400000u /* CE1 空间基址DSP 字地址 */ #define FLASH_BYTE_OFF(off) ((volatile Uint16 *)(FLASH_BASE ((off) 1)))为什么要左移一位因为 16 位字 2 字节DSP 的字地址偏移 1对应 Flash 的字节地址偏移 2。这个细节在原理图里看不出问题——接线图上 DSP 的 A1 到 Flash 的 A0 明明白白只有在调试时往 Flash 的 0 位置写 0x10AA读回来却出现在偏移 2 的地方才意识到换算错了。每个 CE 空间还有独立的等待周期配置寄存器是 SWWSR。NOR Flash 的随机读时间大多在 70 ns 到 120 ns 之间TMS320VC5416 在 100 MHz 主频下时钟周期是 10 ns必须插入等待状态。保守的做法是给 Flash 所在 CE 设 4 个等待周期跑通后再逐个减少。等待周期设太少症状和 Flash 虚焊几乎一样——随机出错、偶发启动失败极具迷惑性。3.2.1 不同 CE 空间映射与 Boot 模式的关系有经验的工程师会把 Bootloader 放在 CE0、应用程序放在 CE1这种布局在收到软件升级包时特别方便——不用动 Bootloader 所在区只擦写应用区。但要注意 Bootloader 的启动模式选择并不是由 CE 编号决定的而是由 Bootloader 在启动时读取的外部引脚状态决定。外部并行启动模式默认从 CE1 空间读取 boot table而 CE1 的基址在 5416 上是 0x400000在 5415 上则要查具体映射。如果你把 Flash 接到了 CE0 而 Bootloader 仍按默认 CE1 访问首个读到的地址不对Bootloader 直接放弃。解决办法是配一个小的 CPLD 做地址译码让 CE0 和 CE1 都映射到同一片 Flash 的不同偏移或者用寄存器把启动空间重映射到 CE0。实验环境下最简单的方式就是按 CE1 布局布线。3.3 C 运行时环境的建立栈指针与中断向量表Bootloader 是 C 代码但它运行在更苛刻的环境中上电后没有调试器没有操作系统连完整的 C 运行库都没有。C 语言写 Bootloader 需要手动处理两件事。第一件是栈指针。CCS 的 C 入口_c_int00会自动初始化栈前提是链接脚本里.stack段存在且栈指针寄存器的初始值没有被破坏。从 Flash 启动场景中片上 Bootloader 跳转到你程序的复位向量时已经设好 CPU 状态通常直接进_c_int00没问题。但如果 Bootloader 在早期就想用 C 局部变量安全做法是在汇编启动文件里先写一条STM #STACK_ADDR, SP把栈顶指到安全区域。C 语言里无法直接操作 SP 寄存器这是少数必须保留汇编代码的地方。第二件是中断向量表。Bootloader 运行期间发生中断时CPU 会到当前向量表位置取中断向量。如果你的向量表还放在 Flash 里中断服务程序可能在 Flash 尚未初始化时被调用——轻则跑飞重则进入不可屏蔽中断死循环。最简单的处理是 Bootloader 阶段全程关中断asm( RSBX INTM); /* 关闭所有可屏蔽中断 */然后在跳到应用入口前把应用的中断向量表地址写入相应寄存器重新开中断。很多实验者跳过这一步Bootloader 本身运行正常应用启动后第一个定时器中断就死机——问题就出在向量表没有跟着程序一起搬到 RAM。4. 一个最小可运行的 C 语言 Bootloader代码骨架与参数解析4.1 主流程代码读取 Boot Table 并逐段搬移下面代码以 TMS320VC5416、16 位外部并行 Flash、CE1 空间为例实现最小 Bootloader。它主动完成三件事初始化 EMIF、遍历 boot table 将各段搬到 DARAM、跳转到入口地址。/* bootloader.c */ #include dsp5416.h /* 寄存器定义头文件按实际芯片替换 */ #define BOOT_TABLE_KEY 0x10AA #define FLASH_CE1_BASE 0x400000u volatile Uint16 * const flash (volatile Uint16 *)FLASH_CE1_BASE; void emif_init_16bit(void) { /* SWWSR 低 8 位决定各 CE 的等待周期数 这里给 CE1 设 4 个等待周期其他 CE 保持默认 */ SWWSR 0x0400; /* BSCR 控制bank切换时序 读/写切换间隔设为 1 个周期 */ BSCR 0x0002; } void main(void) { Uint16 len, addr, xpc; Uint16 *dest; Uint32 src 0; Uint16 entry_pc; emif_init_16bit(); asm( RSBX INTM); /* 关闭可屏蔽中断防止搬运被中断打断 */ if (flash[src] ! BOOT_TABLE_KEY) { while (1); /* 关键字读不到死循环等待调试 */ } src; /* 跳过关键字 0x10AA */ while (1) { len flash[src]; /* 读段长度 */ if (len 0) break; /* 长度0 表示 boot table 结束 */ xpc flash[src]; /* 读页号实验在低 64K 页内暂不使用 */ addr flash[src]; /* 读目标地址 */ dest (Uint16 *)addr; /* 按字搬运逐字拷贝到目标 RAM */ while (len--) { *dest flash[src]; } } entry_pc flash[src]; /* 表结束后第一个字是入口 PC */ /* 跳转前加一条 nop 排空流水线 */ asm( nop); /* 通过函数指针跳转到应用入口 */ ((void (*)(void))entry_pc)(); }逻辑说明整个搬运以字为单位长度和地址都是 16 位。代码里暂时没有写 XPC 寄存器默认所有段都落在程序空间低 64K 页内。如果你的应用代码超过 64K 字需要把xpc写入XPC寄存器后再搬运——这在后面排错部分会专门讲。跳转前加nop是为了避免流水线里残留的预取指令污染新代码这是 C54x 处理器特有的注意事项。重要提示while(1)死循环不是偷懒。调试时 JTAG 断点打在while(1)所在行仿真器立刻能告诉你 Flash 首字读回来是什么值比任何日志都直接。4.2 NOR Flash 擦写操作命令序列与地址换算表Bootloader 实验必须先把 boot table 写进 Flash而 NOR Flash 写入前必须先擦除。以下命令序列以常见的 16 位 NOR Flash 为例不同厂商器件命令码略有差异但结构一致/* flash_program.c */ void flash_program_word(Uint32 byte_addr, Uint16 data) { volatile Uint16 *f flash; /* NOR Flash 编程解锁 编程指令 */ f[0xAAA] 0xAA; /* 字地址 0xAAA对应字节地址 0x5554 */ f[0x554] 0x55; /* 字地址 0x554对应字节地址 0xAA8 */ f[0xAAA] 0xA0; /* 编程命令 */ f[byte_addr 1] data; /* byte_addr 右移 1 位转为字地址 */ /* 等待编程完成查询 DQ6 是否停止翻转 */ while ((f[byte_addr 1] 0x40) 0); }参数说明byte_addr是 Flash 字节地址右移 1 位得到 DSP 侧的字地址。命令地址 0xAAA 和 0x554 是 DSP 字地址对应 Flash 的字节地址是 0x5554 和 0xAA8——这里最容易混乱每颗 Flash 手册给的都是字节地址写到 DSP 代码里必须左移一位做换算。烧写 boot table 前还要擦除整个扇区。扇区擦除命令是 0x80 解锁后跟 0x30块擦除是 0x80 后跟 0x50全片擦除是 0x80 后跟 0x10。实验中最稳妥的做法是只擦 boot table 所在的扇区不要动不动全片擦除——我之前就吃过亏升级程序时把 Bootloader 自己所在的扇区一并擦掉板子直接变砖只能上 JTAG 重新烧。4.3 三段式启动从 Bootloader 到应用的跳转细节C54x 的 Bootloader 实验虽然是“搬完就跳”但生产级产品一般会拆成三段Bootloaderstage0、搬移器stage1、应用stage2。stage0 是出厂固化的stage1 是我们自己写的stage2 是真正的应用代码。本文代码相当于把 stage1 和 stage2 合并了适合验证原理。跳转前的细节值得再强调关闭可屏蔽中断只是最基础的一步。C54x 还有不可屏蔽中断 NMI上电后定时器如果默认使能定时时间一到就会触发 NMI。如果应用侧没有把 NMI 向量准备好CPU 会跳到一个未初始化的向量表位置表现就是“搬移完成了应用没跑起来”。在跳转前把定时器控制寄存器里的使能位清零能排除这个隐患。还有一个流水线问题。C54x 有 6 级流水线跳转指令之后可能还有几条预取的指令残留在流水线缓存中。如果这些残留指令来自 Flash而 Flash 此刻已经被 reprogram流水线里就是旧指令CPU 执行完跳转后可能直接跑飞。网上讨论里常说的 DSP bootloader 随机死机十有八九是流水线没有排空。nop指令能解决大部分情况极端情况下需要做一次远跳转强制清空流水线。; 远跳转汇编模板确保流水线完全刷新 FB entry_pc ; 远跳转C54x 支持的最强跳转指令5. Bootloader 实验的验证手法与三个容易搞错的细节5.1 用 LED 状态机把启动流程可视化Bootloader 阶段没有任何打印机制串口外设在 Flash 搬运完成前也不可用LED 状态机是最廉价的反馈方案。我在实验里分配两个 GPIO按执行进度切换电平组合LED1LED2运行状态灭灭上电复位Bootloader 开始执行亮灭EMIF 初始化完成等待读 Flash灭亮关键字 0x10AA 读到进入段搬移亮亮所有段搬运完成即将跳转应用代码里在 main 函数的四个关键节点分别写一次 GPIO 输出程序卡在哪个阶段灯就停在哪个状态。配合 JTAG 再次连接时查看 PC 值能快速排除是 Flash 访问失败、boot table 格式错误还是地址换算错误。这是实验最容易上手的验证手段不需要任何额外工具。5.2 XPC 页寄存器的处理代码超过 64K 字时怎么办TMS320VC5416 的程序空间最大 8M×16 位通过 XPC 寄存器分页管理。前面 4.1 节的代码把 xpc 读出来却没用一旦应用代码超过 64K 字就会出错。正确处理是搬移每个段之前先写 XPC 寄存器/* bootloader.c 中 段搬移的完整版 */ xpc flash[src]; /* 读该段的页号 */ addr flash[src]; /* 读页内偏移地址 */ /* 如果目标在程序空间扩展页则设置 XPC */ if (xpc ! 0) { XPC xpc; /* XPC 是映射在数据空间的寄存器 */ } dest (Uint16 *)addr; while (len--) { *dest flash[src]; }注意段头中 XPC 字段的有无取决于链接器生成的 boot table 格式。CCS 的 hex 工具生成 boot table 时默认只在目标地址超过 64K 时才插入 XPC 字段不会每个段都带。代码读到错误的位置段数据会整体错位跳转自然失败。实验时先用小工程验证不代 XPC 的流程再逐步加大代码量测试分页逻辑。5.3 用 CCS 的 Memory 窗口验证 boot table 内容Flash 烧写完成后先不急着复位运行而是用 JTAG 连接在 CCS 中打开 Memory 窗口直接查看 Flash 起始处的几百个字。对照第 2.2 节的表格逐项检查第一字必须是 0x10AA第二字是段长度第三字是目标地址后面跟着数据。这个动作能发现 90% 的烧写错误。还有一种常见的错误用 8 位编程器给 Flash 烧写boot table 被按字节拆开0x10AA 变成 0x10 0xAA 两个字节写入相邻地址。Bootloader 在 16 位模式下读回的是 0xAA10字节序反了。验证方法是在 Memory 窗口里把显示格式设为 16 位 Hex确认读到的地址和值符合预期再执行复位。实验做完后可以再做一个进阶练习修改 Flash 中一个数据字节故意破坏 boot table 的校验位观察 Bootloader 是否会拒绝跳转。这个过程能帮你理解启动流程中哪一部分是可靠的哪一部分需要应用侧自己补强。整个 Bootloader 机制用熟了后续做在线升级、远程固件更新都会顺手很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进