资讯动态

TMS320F28335 DSP代码从Flash到RAM的搬运与烧写实战

发布时间:2026/10/6 9:04:31 来源:尧图企业网站定制
先把型号说准TI没有TMS320FF28335这个型号实际是TMS320F28335标题里多半是手误多打了一个F。F28335是C2000家族里非常经典的一块DSPFlash容量256K×16RAM却只有34K×16这种“大Flash、小RAM”的架构决定了你早晚会遇到同一个问题程序烧在Flash里能跑但跑不快尤其是中断、浮点运算和实时控制类代码必须从FLASH搬到RAM里跑。这篇我把工程里最常见的两种搬运方式和完整的FLASH烧写流程都拆开讲一遍适合正在做F28335开发、被“Flash等待周期”“ISR卡顿”“烧写失败”折腾过的工程师也适合刚入门C2000、想把启动过程和存储映射搞明白的同学。1. 为什么F28335要把代码从FLASH搬到RAM1.1 Flash和RAM的访问速度差异到底有多大F28335主频可以跑到150MHz这时候片内Flash的访问其实是有“等待周期”的不是零等待。Flash控制器有流水线缓冲顺序执行时能掩盖一部分等待时间但代码只要出现跳转、分支、调用子函数流水线就会打断CPU不得不重新去Flash里取指那等待周期就暴露出来了。而片内RAM是零等待或者极低等待确实能跑出150MHz的满速。对于ADC中断、PWM占空比更新、PLL锁相控制、电机FOC这种高频实时任务哪怕一次中断多几十个周期累积起来就是百分比级别的性能损失。所以F28335工程做到中后期几乎人手一份“从FLASH拷到RAM”的代码。另一个原因是有些“非标准”操作绕不开RAM。比如做Flash参数区擦写时擦写期间CPU不能从同一块Flash取指如果擦写代码本身就在Flash里执行那程序就直接飞了。经验做法是把擦写函数搬到RAM执行把Flash让出来。1.2 哪些代码必须搬进RAM哪些可以留在FlashF28335的RAM很小总容量34K×16还有一部分被栈、全局变量、堆占掉能用来放代码的空间更是紧巴巴所以不可能像ARM那样把整个固件搬到RAM运行。我平时只搬这几类所有中断服务函数尤其是ADC、EPWM、EQEP这类高频中断时间敏感的计算函数比如复杂浮点运算、三角函数库、滤波运算涉及Flash擦写的底层驱动函数被高频调用的小工具函数比如Delay_us这类自旋等待函数。像初始化函数、主循环里的低频率逻辑、配置寄存器这类对时间不敏感的代码留在Flash跑完全没问题。判断标准很简单看这条代码路径上的执行频率和实时性要求频率高或者要求确定时延就搬否则不搬。1.3 搬运要放在main函数最早期程序上电后Boot ROM根据GPIO引脚状态跳到Flash的入口再进入C运行时初始化最后才到main。如果你在main后面某个外设中断里调用了一个本来应该搬进RAM的函数而搬运动作还没发生那么调用时仍然走Flash地址时间照样慢。所以我通常把拷贝动作放在main的最开始系统时钟初始化完成后立即执行确保后续所有初始化代码里如果调用了ramfuncs段的函数都已经在RAM里了。2. 方法一memcpy手动搬运适合精细控制2.1 原理一句话在CMD文件中把一个自定义段设置成“加载在FLASH、运行在RAM”链接器会为它生成三个定位符号加载起始地址、运行起始地址、段长度。程序里拿到这三个符号用memcpy把Flash处的代码逐个字拷到RAM处然后CPU后续对函数的调用就自动指向RAM地址了。这个方案的好处是完全可控搬哪些函数、什么时候搬、搬完要不要校验全由代码决定不管Linker版本和编译选项怎么变行为都是确定的。2.2 具体操作三步完成memcpy搬运第一步源文件里用#pragma CODE_SECTION把需要搬的函数指定到ramfuncs段#pragma CODE_SECTION(adc_isr, ramfuncs) interrupt void adc_isr(void) { // ADC采样处理 } #pragma CODE_SECTION(pwm_update, ramfuncs) void pwm_update(Uint32 period, Uint32 compare) { // 更新PWM周期和比较值 }注意一个#pragma只管一个函数多个函数需要每个都写一遍。如果是一整个.c文件里的函数都要搬也可以编译选项或头文件里统一声明但单个函数逐个指定最不容易出错。第二步声明链接器符号并执行拷贝。在C文件里加外部声明extern Uint16 RamfuncsLoadStart; extern Uint16 RamfuncsRunStart; extern Uint16 RamfuncsLoadSize;然后在main最开始加拷贝操作void main(void) { InitSysCtrl(); #ifdef FLASH memcpy(RamfuncsRunStart, RamfuncsLoadStart, (Uint32)RamfuncsLoadSize); #endif DINT; InitPieCtrl(); InitPeripherals(); // ... }为什么用Uint16而不是指针因为F28335的地址是16位字地址C2000编译器里char实际是16位所以这里的大小单位是“字”不是普通MCU的“字节”。很多第一次做C2000的人在这里踩坑直接把sizeof套上去结果拷多了或拷少了程序跑飞都找不到原因。第三步CMD文件里定义段位置和符号ramfuncs : LOAD FLASH, RUN RAML0, LOAD_START(_RamfuncsLoadStart), RUN_START(_RamfuncsRunStart), SIZE(_RamfuncsLoadSize), PAGE 0这里的FLASH和RAML0是你在Memory段里定义好的两块地址区域。FLASH通常是0x3F0000之后的区域RAML0是片内RAM某段。LOAD_START、RUN_START、SIZE是固定写法生成的符号名在C代码里要去掉前面的下划线使用也就是RamfuncsLoadStart、RamfuncsRunStart、RamfuncsLoadSize。2.3 复制完成后怎么确认真的在RAM里跑这个点不验证拷了等于没拷。我常用的有两种方式。第一种看map文件。编译后打开生成的.map文件找到ramfuncs这段ramfuncs 0 008000 00000400 00000002前面的地址是RUN地址后面还有个LOAD地址两者不同说明链器已经按照LOAD/RUN分离处理了。如果LOAD和RUN地址一样说明CMD里根本没配好拷贝循环等于把Flash数据拷回Flash自己。第二种直接看反汇编或运行时地址。调试状态下在函数里头打断点看当前PC和函数地址如果落在0x8000~0x87FF这类RAM区域就说明跑的是RAM版本。如果PC落在0x3Fxxxx那抱歉还在Flash里回去检查上面三步哪里漏了。还有个细节如果工程既有Flash烧写版本又要保留RAM仿真调试版本建议用编译宏把拷贝过程括起来比如上面代码里的#ifdef FLASH。RAM仿真时所有代码本来就在RAM不需要拷烧写到Flash后才需要这段拷贝逻辑。3. 方法二链接器Copy Table自动搬运适合批量函数3.1 链接器copy表到底是什么如果你要搬的函数不止两三个而是分布在多个源文件里的十几二十个函数逐个写#pragma CODE_SECTION也能做但代码里会很啰嗦改起来也容易漏。TI的链接器提供了另一种机制Copy Table也就是拷贝表。原理不复杂当你把一个输出段的LOAD地址和RUN地址设置成不同的位置时链接器会为这个段生成一条“从哪里拷贝到哪里、拷多长”的记录所有这样的记录汇总成一张copy表。运行时执行一段复制逻辑遍历这张表把表里记录的段全部从Flash搬运到RAM。调用这个复制逻辑既可以在C启动代码里自动做也可以手动触发。F28335上常用的做法有两种一种是使用编译器的运行时支持函数另一种是把拷贝动作做成一个独立函数在main早期调用。相比方法一它最大的优势是“集中管理”你只需要在CMD文件里声明好段把函数用#pragma丢进对应的段Linker自动计算所有地址和长度不用手写一堆extern Uint16符号。3.2 操作步骤pragma CMD 自动/手动触发源文件里同样用#pragma CODE_SECTION把函数指定到一个或多个段#pragma CODE_SECTION(adc_isr, ramfuncs) #pragma CODE_SECTION(eqep_isr, ramfuncs) #pragma CODE_SECTION(macro_loop, ramfuncs) #pragma CODE_SECTION(fp_filter, ramfuncs)CMD文件里把这些段集中到一条RUN/LOAD定义里ramfuncs : LOAD FLASH, RUN RAML0, LOAD_START(_RamfuncsLoadStart), RUN_START(_RamfuncsRunStart), SIZE(_RamfuncsLoadSize), PAGE 0到这里其实和方法一的准备步骤是一样的。真正的区别在触发方式。方法一是你在main里手动调memcpy而copy表方式是把工程里所有需要搬运的段交给一个统一的拷贝例程处理void CopyRamfuncsFromFlash(void) { extern Uint16 RamfuncsLoadStart; extern Uint16 RamfuncsRunStart; extern Uint16 RamfuncsLoadSize; memcpy(RamfuncsRunStart, RamfuncsLoadStart, (Uint32)RamfuncsLoadSize); }如果你想连这个动作都省掉使用支持copy表自动初始化的编译器或启动代码可以配置成上电后在c_int00启动阶段自动把表中记录的代码段搬好。不过底层自动化的项目排查起来稍微麻烦我实际工程里更倾向写一个明确的CopyRamfuncsFromFlash()函数在InitSysCtrl()之后立刻调用。每个人风格不同但“明确可见”的拷贝流程对后续维护友好得多。3.3 两种方案怎么选我把选择建议整理成对比方便你做技术选型维度方法一memcpy手动搬运方法二链接器Copy Table控制粒度函数级精确可控段级可以集中管理代码侵入性每个函数都要写pragmamain里手动拷贝同样要写pragma但拷贝动作可统一封装适合场景只搬两三个关键函数搬十几个甚至更多函数排查难度直观地址和大小都好查需要懂copy表机制遇到问题稍微绕升级兼容性不依赖编译器版本老版本C2000编译器支持度有差异我的倾向项目早期、函数数量少直接用方法一逻辑简单不会出幺蛾子等代码量上来或者移植别人的工程时发现对方用的就是copy表结构再按方法二重构。没必要一开始就上复杂方案。4. F28335的FLASH烧写方法4.1 用CCS的On-Chip Flash烧写这是开发阶段最常用的方式XDS100v2或XDS110仿真器连接目标板直接在CCS里把.out烧进Flash。基本流程确认工程是Release或对应的烧写配置编译生成.out文件仿真器连接目标板确保Target Configuration里的器件选择的是TMS320F28335菜单里打开Flash工具CCS不同版本入口不一样有的在Target菜单下有的在Project右键菜单里选“Flash”相关工具添加.out文件配置系统时钟参数这点非常关键。F28335的Flash烧写工具需要知道外部晶振频率和PLL配置最常见的是30MHz晶振锁相环倍频到150MHz但不同板子可能板载晶振是20MHz或25MHz填错会导致烧写时序不对选择擦除策略。可以全片擦除也可以只擦几个扇区。F28335的Flash分成多个扇区代码跨扇区时要全部擦对应区域否则残留跳转地址会导致启动异常执行Program后再Verify校验能过才算烧写完整。整个烧写过程不要断电不要拔仿真器。Flash操作不具备掉电保护写到一半断电扇区内容和校验值全部作废只能重新擦除再写。注意烧写频率不要拉太高。有些人仿真器链路长、线材差直接用默认最高JTAG频率结果就是烧写到一半报错。我习惯先把TCK降到中低档确认能稳定烧写后再调高省去很多“离奇失败”。4.2 用SCI串口Bootloader烧写量产阶段没有仿真器或者想通过生产工装快速烧写可以用F28335片内Boot ROM里的SCI引导程序。F28335上电后Boot ROM根据GPIO引脚状态决定启动方式。把相关引脚配置到SCI boot模式复位后芯片的SCI-A外设会等待接收数据上位机通过串口发送烧写文件Boot ROM里的引导代码接收后写入Flash。步骤大致是编译生成.out后用工具转换成Boot ROM要求的串口烧写格式常见的是.hex或C2Prog工程支持的格式目标板断电调整boot引脚跳线到SCI boot模式打开C2Prog或自研烧写上位机选择对应器件为F28335设置正确的串口号和波特率目标板上电复位上位机连接后开始发送烧写完成后提示成功断电把boot引脚拨回Flash启动模式重新上电验证程序。这里最容易翻车的是波特率。F28335的Boot ROM有内置波特率检测逻辑但也依赖外部时钟准确。如果板子用内部时钟或者晶振偏差大建议用较低波特率比如9600或19200成功率明显更高。另外串口烧写前要确认SCI-A的引脚有没有被外设占用如果有外接收发器要保证默认电平不会干扰Boot ROM接收数据。4.3 烧写中的安全与校验细节第二个坑是密码区。F28335的Flash里有密码区域烧写工具默认会把你烧写镜像里的内容往该区域写。如果这一块内容不是0xFFFF而是某个随机值芯片就可能被“锁死”之后JTAG再也连不上。很多“芯片变砖”现象其实不是硬件坏了是密码区被写入了错误密码。所以工程里如果不需要密码保护烧写配置里把密码区域强制填成0xFFFF烧写完再读校验一遍。如果已经有密码且你不知道密码就别尝试擦除失去口令的芯片几乎等于报废只能换新片。第三个细节是烧写后的启动引脚状态。F28335有多种启动方式Flash启动、SCI boot、SPI boot等由GPIO引脚在上电瞬间的电平决定。烧写完程序后如果boot引脚还停在SCI boot模式上电不会跑到你烧好的用户程序。这个不是烧写失败是“根本没启动你的程序”排查时先确认拨码开关。5. 常见问题与排查实录5.1 Flash下载时报“Flash download failed - target DLL has been cancelled”这个报错非常典型我在各种F28335、F28069项目里都被恶心过。表面意思是“Flash下载失败目标DLL被取消了”实际上原因五花八门仿真器线太长、接触不良首当其冲目标板供电不稳尤其烧写瞬间Flash需要加大电流烧写工具里器件型号和实际芯片不符合TI的Flash插件版本和CCS版本不匹配PLL时钟配置与实际晶振不符导致烧写工具算错等待参数。排查顺序我建议这样来先用最普通的Dumb Mode或纯JTAG连接测试排除线材问题然后在烧写工具里把TCK频率往下降再核对晶振和PLL设置最后重装驱动或升级CCS的Flash插件。按这个顺序走一遍八成能解决剩下的两成基本是板子硬件问题。5.2 明明拷了函数还是从Flash地址跑这种情况我见过N次。拷贝代码写在main里但main本身在调用某个“需要搬进RAM”的函数时链接器计算出的调用地址仍然指向Flash里的位置。为什么因为你只做了数据拷贝没有让后续调用跳转到RAM地址。先说结论F28335上只要段定义正确链接器在编译时就会把对该函数的调用解析成RAM地址拷贝完成后再调用走的肯定是RAM。如果实际反汇编看到的还是Flash地址先回查以下三点CMD里RUN地址和LOAD地址是否确实不一样拷贝前是否访问了ramfuncs里的函数导致那段代码还在Flash里执行时被中断打断产生绝对地址调用编译优化选项是否把段合并或重命名了去map文件里搜索函数名确认最终落在哪个段。还有一个隐蔽点函数内部如果有静态局部变量或字符串常量这些数据可能也在Flash里搬运代码本身不会自动搬运数据段。程序跑到RAM版本的函数后弹数据仍然从Flash读速度瓶颈不会完全消除。需要把相关数据段也用类似方式搬到RAM或者改用常量访问优化。5.3 重新烧写时提示目标被锁上一条已经提到密码区问题。如果第一次烧写时不小心把密码区写了非0xFFFF的值第二次想通过JTAG烧写就会失败因为调试接口已经被芯片的安全机制锁住。现象是连接仿真器时CCS能识别器件但一执行Flash操作就报错。处理办法分两种情况如果你还知道密码在CCS的连接配置里填上正确的密码解锁后烧写如果不知道密码JTAG这条路基本断了只能用SCI boot的解锁流程进行一次强制擦除。我在项目里都是直接把密码区用0xFFFF占位用不到加密功能就不给自己找麻烦。5.4 烧写完上电程序不跑但仿真能跑“仿真时一切正常烧写到Flash后一上电就死”也是高频问题。先说结论这种情况大概率不是代码逻辑问题而是启动环境不同。F28335在仿真调试时CCS会自动初始化一部分寄存器甚至会帮你初始化RAM段。脱离仿真器后Boot ROM到main之间的时间窗口里看门狗是否关闭、时钟模块是否配置正确、关键外设引脚是否存在上电毛刺都会影响程序启动。排查时先做三件事第一确认InitSysCtrl()在最前面且看门狗一开始就关掉第二确认Flash等待状态初始化正确很多例程里都有一句InitFlash()这个不能省第三确认代码里没有依赖仿真器才能初始化的变量比如extern仿真变量。如果还不行在烧写版本里临时加一个GPIO翻转看代码执行到哪一步停了比盲猜高效得多。5.5 RAM容量不够怎么挤出空间F28335一共就34K×16的RAM全局变量、栈、堆占掉一部分后能放代码的空间更有限。我常用三个方法腾空间把堆栈段尽量压缩到实际使用量将某些外设库申请的RAM段重新映射到最小的可用SRAM只搬“确定性时间”最关键的函数把偶发性高耗时逻辑留在Flash不追求所有代码零等待检查是否有重复代码段被链接器多次展开比如printf这类重量级函数没必要搬进RAM让它留在Flash调用。RAM紧张的项目我倾向于在.map文件里把RAM占用按段列个清单看哪个段占用最大、哪段利用率最低再针对性地调整CMD。6. 一点个人实操体会从F28335一路做过来最大的感触是这段从FLASH搬到RAM的过程不复杂但只要对链接器符号和CMD文件不够敏感掉进去就是半天起步。我现在固定套路是新项目只要确定要用Flash运行先把ramfuncs段、拷贝函数和密码区占位这三件事写进工程模板后面做啥都不慌。还有个小建议每次准备烧Flash前都把.out和.map文件留个底记录编译时间、代码段大小、RAM占用数据。出问题后对比记录能立刻看出是代码膨胀导致段冲突还是CMD分配变化导致地址错位。这种习惯看着琐碎真出线上问题时候能救你一命。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑