先把型号说准:TI没有TMS320FF28335这个型号,实际是TMS320F28335,标题里多半是手误多打了一个F。F28335是C2000家族里非常经典的一块DSP,Flash容量256K×16,RAM却只有34K×16,这种“大Flash、小RAM”的架构决定了你早晚会遇到同一个问题:程序烧在Flash里能跑,但跑不快,尤其是中断、浮点运算和实时控制类代码,必须从FLASH搬到RAM里跑。这篇我把工程里最常见的两种搬运方式和完整的FLASH烧写流程都拆开讲一遍,适合正在做F28335开发、被“Flash等待周期”“ISR卡顿”“烧写失败”折腾过的工程师,也适合刚入门C2000、想把启动过程和存储映射搞明白的同学。
1. 为什么F28335要把代码从FLASH搬到RAM
1.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,哪些可以留在Flash
F28335的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 |
|---|---|---|
| 控制粒度 | 函数级,精确可控 | 段级,可以集中管理 |
| 代码侵入性 | 每个函数都要写pragma,main里手动拷贝 | 同样要写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分配变化导致地址错位。这种习惯看着琐碎,真出线上问题时候能救你一命。