F28335 Flash擦写入门:RAM运行与API库实操
1. 项目背景与整体方案拆解1.1 F28335的Flash擦写到底解决什么问题搞过DSP嵌入式开发的朋友应该都有体会C2000系列特别是F28335这颗芯片在电机控制、逆变器、电源管理这些工业场景里服役多年现在依然有大量存量产品和新建项目在使用它。而只要涉及Bootloader在线升级、校准参数掉电保存、运行日志记录就绕不开一件事对片上Flash做擦除和写入。很多新手第一次接触F28335的Flash操作时会下意识把它当成普通单片机来用直接在应用代码里调用一个“写Flash”的函数结果发现程序一执行到擦写调用就卡死或者复位后数据还是老样子。这不是个例几乎所有初学F28335 Flash的人都会在同一个坑里栽跟头。原因我们后面会详细讲核心在于F28335的Flash控制器与CPU取指机制之间存在一种特殊的互锁关系导致执行擦写的代码必须放在RAM里运行。这篇博文就是围绕“F28335 Flash基础擦写操作入门演示”这个主题把我实际做过的一整套流程整理出来。包括Flash模块的基本结构、擦写操作的前因后果、工程里怎么配RAM段、代码怎么写、跑起来之后怎么校验以及我踩过的几个典型坑。适合刚接触F28335、准备做Bootloader或者需要保存参数的开发者参考我会尽量把每一处操作背后的原理也讲清楚而不是只丢一段能跑的代码。1.2 方案选型Flash API库与寄存器直操作怎么选F28335的Flash擦写业界主流做法有两条路。一条是直接用芯片原厂提供的Flash API库。这个库以静态库文件的形式提供内部封装了擦除、编程、校验三个核心函数对外暴露的接口很简单你用的时候只需要把库文件加进工程、处理好RAM段链接就行。优点是省事儿不必深抠内部寄存器时序复杂的状态机和等待逻辑原厂已经调好了缺点是它是个黑盒出了问题不好排查而且这个库对编译器版本和优化级别有讲究用不对就会莫名其妙失败。另一条是直接操作Flash控制器寄存器自己写擦写函数。这条路对芯片手册的熟悉程度要求很高你得清楚Flash控制器的状态机、命令字、各状态位之间的约束关系。好处是可控性强可以把底层逻辑完全掌握在自己手里出了问题能一步步追踪坏处是一旦某个细节没处理好轻则擦写失败重则把Flash搞到异常状态只能重新上电恢复。以入门演示为目的我建议先用官方Flash API库把整体流程跑通理解“搬进RAM、发擦除命令、发编程命令、校验”这一套逻辑然后有余力再去研究寄存器层的实现。我下面讲的整个Demo也是按这个思路组织的寄存器层面的概念会穿插讲但核心流程用API库来实现这样阅读门槛低一些大家照着也能复现。1.3 核心难点拆解为什么擦写代码必须挪进RAM这是整个F28335 Flash操作里最重要、也最反直觉的一点。F28335的程序默认从Flash启动执行CPU的取指总线在Flash阵列工作的时候是可以正常读指令的。问题在于当Flash进入擦除或编程流程时它的内部电荷泵和高压控制逻辑要占用Flash存储阵列的访问通道此时Flash阵列处于一种“不可读”的状态。如果CPU这时候还在从Flash取指令总线就会一直被挂起等待表现出来就是程序停在那里不动也就是大家常说的“跑飞但死不了”。想让擦写流程正常走完唯一的办法是把负责执行Flash擦除和编程操作的那些代码事先复制到片上RAM里然后让CPU跳到RAM里去执行这些代码。RAM是独立的存储介质不受Flash操作影响CPU取RAM指令时不会跟Flash控制器抢通道。这个“搬进RAM”的动作不是写代码时手动搬而是通过链接器cmd文件的段配置自动实现的。简单说就是给擦写相关函数定义一个专门的段在cmd里把这个段设置为“加载在Flash运行在RAM”启动代码运行时自动把内容从Flash拷贝到RAM之后调用该段里的函数就会走RAM。理解了这一层很多后续的诡异现象就都能解释了比如为什么擦写Demo单步调试没问题、全速跑就死为什么函数加了优化就失败为什么擦写自己的当前运行段会出灾难性后果。这都跟“到底从哪里取指”有关。2. F28335 Flash模块与擦写原理全景解析2.1 存储阵列和段结构先看清这256KB怎么划分F28335的片上Flash容量是256K×16位注意这个单位是字不是字节。很多习惯用Byte的人第一次算地址时会算错我们做参数存储时也容易踩这个坑。整个Flash阵列起始地址是0x300000结束地址是0x33FFFF。芯片原厂把这块空间划分成了8个段名字是A到H每个段都是32K×16位。为什么分这么多段因为Flash这种存储介质只能以“段”为最小单位整块擦除不能单独抹掉其中某一个字。如果你只想更新一小块参数也必须先把它所在的那个32K段整体擦掉再重新写入不需要变的数据和新的数据这个过程在很多文档里叫“读-改-写”或者“搬移回写”。我在做项目时一般会把Flash分区规划成几个用途段A放Bootloader引导程序段B和段C交替存放两个应用版本段H专门留出来存参数。这样做的原因是段与段之间可以独立擦除刷应用A的时候不影响应用B升级失败还能回滚到旧版本。对于入门Demo没必要规划得这么复杂选一两个空闲段练手就行。还需要知道的是F28335的Flash里面有Code Security Module模块简称CSM。CSM相当于一把锁如果被设置过密钥外部调试器和不相关的代码就无法读取或者擦写Flash。很多人买来开发板能直接烧程序是因为出厂时CSM处于解锁状态。但如果你之前烧过密钥进去后面擦写就会失败这是一个非常隐蔽的坑后面排障部分我会专门讲。2.2 关键寄存器与状态机FCMD/FSTAT/FBADDR/FDATA如何配合F28335的Flash控制器对外提供了一组控制寄存器在官方头文件里统一封装成一个FlashRegs结构体。我挑几个对理解擦写流程最关键的来说。FCMDFR是命令寄存器用来告诉Flash控制器当前要执行什么操作常用的命令字有擦除和编程两种。FSTAT是状态寄存器里面最关键的两个标志位一个叫PEND一个叫BUSY。PEND表示有没有尚未完成的操作挂在那儿BUSY表示Flash控制器当前忙不忙。在发起擦除或编程命令之后程序要不断查询这两个状态位等BUSY清零才表示操作真正完成。FBOANK是段选择寄存器用来指定当前操作作用于哪个或哪几个Flash段。擦除之前必须先在这个寄存器里把目标段选好否则命令发出去可能不会作用于你预期的地址。此外还有地址寄存器FADDRC和写数据寄存器FDATA编程时要把目标地址写到地址寄存器把要写入的字写到数据寄存器再触发编程命令。整个Flash控制器的运作其实是一个有限状态机空闲状态收到命令后进入忙碌状态直到操作完成自动回到空闲。这个过程里CPU不能打断它这也是为什么擦写阶段必须关中断的底层原因。你可以在理解“命令-触发-轮询-完成”这个主线上多看几遍数据手册里的寄存器位定义把状态机流转搞清楚很多问题就好定位了。2.3 擦除与编程的微观流程从命令触发到总线时序先说擦除流程。你在用户程序里调用Flash库的擦除接口时库函数内部做的事情大致是这么几步先把段选择寄存器配置成要擦除的段然后往命令寄存器写入擦除命令字再设置状态寄存器里的擦除触发位之后进入一个轮询循环反复读取状态寄存器直到忙碌标志清零。这个过程中Flash内部要做的工作远比表面看起来复杂。它要先对整个目标段施加高压脉冲把存储单元里的浮栅电荷全部释放掉让每个位都回到“1”状态然后再做一次全段校验确认确实擦干净了。整个过程耗时毫秒级别具体时间跟芯片型号和温度有关数据手册上一般会给一个典型值比如几十毫秒擦一个段。编程流程和擦除略有不同。编程是逐字进行的每写一个字Flash控制器会把目标地址上的存储单元暴露在编程电压下把“0”位对应的单元加上电荷。也就是说对未擦除过的Flash区域直接编程是不会得到你期望的数据的因为只有把“1”变成“0”的能力没有把“0”变回“1”的能力。这就是为什么编程之前必须先擦除所在段的硬性要求。在总线时序上擦写期间Flash阵列完全不可读CPU如果此时还在从Flash取指就会被挂起。所以库函数本身在链接时一定会被放到RAM。另外擦写过程中如果来了中断中断向量表在Flash里中断服务程序也在Flash里CPU为了响应中断又去读Flash照样卡住。因此擦写临界区必须关闭全局中断这是很多人容易漏掉的一个环节。3. 实操演示从工程配置到完整擦写Demo跑通3.1 开发环境与工程搭建硬件配置、晶振和RAM段的坑先交代一下我用到的开发环境。编译器用的Code Composer Studio版本比较旧但经典配合F28335是很成熟的一套组合。运行时支持库使用官方提供的自启动库。硬件上有开发板和仿真器不过要注意通过仿真器烧写Flash和脱机自跑Flash擦写是两个不同的场景仿真器烧写是工具帮你做了底层操作而后者的逻辑才是这篇Demo要演示的。工程搭建第一步是确认时钟配置。F28335内部有锁相环系统时钟可以倍频。Flash的擦写时序跟系统时钟频率有关所以要把InitSysCtrl的时钟配置确认好别用默认值稀里糊涂跑。我当时是在某个电机控制项目里调试这个功能整套系统的时钟是基于外部晶振倍频到150MHz运行的API库要求的时钟参数在这个频率下没问题跑起来很稳。第二步是CMD文件的段配置。前面反复强调的“搬进RAM”就在这里实现。官方Flash API库自带一个段名比如叫.FlashAPI或类似的名字你需要把它分配到一个可用的RAM区间同时指定代码的加载地址在Flash。下面是一段典型的配置写法SECTIONS { .flash_api : LOAD FLASH_B, RUN RAML0, LOAD_START(RamFuncsLoadStart), RUN_START(RamFuncsRunStart), SIZE(RamFuncsLoadSize) }这段配置的意思是擦写函数段.flash_api的初始内容LOAD放在Flash段B里但运行地址RUN在RAM的L0区启动代码会自动把这段内容从Flash搬运到RAM。这里有个细节如果你用的是官方API库库里函数的声明处往往带一段段名说明你必须把它的段名写进你自己的CMD文件里否则链接器找不到归属会报错。还有一点也容易踩坑RAML0这个区域的优先级是最高的如果工程里别的变量也放在RAML0可能会空间不够。我建议单独划一块RAM区间给Flash API用比如把L0到L3整段留出来不要跟普通变量混在一起。3.2 核心擦写流程实现代码级拆解工程配置好之后核心代码其实可以写得很简洁。我把演示Demo的逻辑分成三部分数据准备、擦除、编程、校验。数据准备阶段准备要写入Flash的数据缓冲区同时准备一个目标Flash地址数组。这里我用512个字作为演示长度实际项目里可以根据需要调整但要注意目标地址必须按字对齐而且不能超出所操作段的地址范围。擦除阶段调用库函数擦除目标段。官方API库的擦除接口通常长这样FLASH_Erase(sectorMask);sectorMask是段掩码比如要擦除B段和C段就把这两个段对应的掩码按位或起来。注意一次擦除多个段和逐个段擦除在耗时上有差别但在API使用上是一样的。编程阶段调用编程接口FLASH_Program(flashAddr, dataBuffer, length);第一个参数是Flash目标地址第二个是源数据缓冲区第三个是长度单位是字。编程接口内部会自动做逐字写入循环你不需要自己处理FADDR、FDATA这些寄存器。校验阶段调用校验接口FLASH_Verify(flashAddr, dataBuffer, length);这个函数会把Flash里的实际内容跟源缓冲区逐字比对返回0表示全部一致。对于正式项目校验这步是标配不是可选。擦写过程受各种因素影响可能局部失败没有校验就无法及时发现。下面是我整理的完整演示函数骨架// 该函数必须放在RAM中执行 #pragma CODE_SECTION(FlashWriteDemo, .flash_api) int FlashWriteDemo(Uint16 *dataBuf, Uint16 len) { Uint16 *flashAddr; // 目标地址根据实际规划赋值 Uint32 sectorMask; // 要擦除的段掩码 Uint16 status; // 1. 关闭全局中断 DINT; // 2. 配置目标地址和段掩码 flashAddr (Uint16 *)0x003E000; sectorMask FLASH_SECTOR_H; // 3. 擦除目标段 status FLASH_Erase(sectorMask); if (status ! FLASH_OK) { EINT; return -1; } // 4. 编程写入 status FLASH_Program(flashAddr, dataBuf, len); if (status ! FLASH_OK) { EINT; return -1; } // 5. 校验 status FLASH_Verify(flashAddr, dataBuf, len); if (status ! FLASH_OK) { EINT; return -1; } // 6. 恢复中断 EINT; return 0; }需要特别说明的是这里调用的FLASH_Erase、FLASH_Program、FLASH_Verify三个接口内部的实现细节包括状态机轮询、寄存器写入都在官方静态库里封装好了。如果你用的是直接操作寄存器的方案思路是一模一样的只是把库函数调用换成对FlashRegs的读写和状态位查询。3.3 运行验证与数据校验怎么确认Flash真的写对了代码写完烧进F28335跑起来之后怎么确认这个Demo真的成功了我的习惯是分三步做验证。第一步看返回值。Flash API库的接口都会返回执行状态常见的宏定义在不同版本里略有差异但整体逻辑一致返回0表示成功非0表示出错。我在Demo里已经把这些返回值都检查了任何一个异常就直接退出。第二步通过调试器看内存。程序跑完FlashWriteDemo之后暂停CPU在调试器的Memory窗口里输入0x003E000这个地址手动查看刚才写入的那一段Flash内容。如果看到的数据跟你准备的数据缓冲区内容一致说明编程确实生效了。第三步做断电保持测试。这是验证Flash写入有没有成功最硬核的方式把开发板断电等几秒重新上电让程序从Flash重新启动然后再次读取0x003E000地址的内容。如果之前写入的数据还在说明是真真切切写进了非易失存储介质而不是仿真器在线RAM里的假象。我遇到过不少朋友说“我写进去了程序里也能读出来”结果一断电数据就全没。这种情况多半是把数据写到了RAM里而不是真正的Flash地址——建议在工程里仔细检查你传给Flash_Program的地址有没有落到0x300000到0x33FFFF这个范围内。3.4 基于演示工程扩展Bootloader的思路入门Demo跑通之后很多人会问这东西离一个真正的Bootloader还有多远我根据自己的项目经验说下思路。Bootloader本质是“在应用运行期间擦写Flash里的另一块区域”核心正是这篇Demo里的代码。你把引导程序放在段A这个引导程序里只做三件事检查升级标志、接收外部通信接口传来的固件数据、调用Flash擦写函数把数据写入段B或段C。写完校验通过后跳转新程序执行。这里面要考虑的事情比入门Demo多很多比如通信协议要有帧校验和超时重传机制升级过程中意外断电要有恢复方案升级失败要能回滚。但这些高层逻辑都是建立在“Flash能可靠擦写”这个基础上的。先把Demo跑通后面很多问题都好解决。另外不管是Demo还是正式Bootloader我强烈建议把擦写操作封装成独立模块对外只提供擦写接口不让业务代码直接接触Flash寄存器。这样既方便复用也方便在出问题的时候单独排查。4. 独家排障速查Flash擦写常见坑与实战修复4.1 擦写后系统直接跑飞程序死在哪里这个问题排行榜第一名而且新手几乎必遇。现象是程序执行到擦写函数调用之后整颗芯片就像死了一样调试器暂停程序时看到的PC指针停在某个地址不动或者直接进入异常中断向量。我排查这个问题的经验是分两步走。先确认擦写函数是否真的运行在RAM里。这一步可以通过查看生成的map文件确认在map文件里找到擦写函数所在段的地址归属看它落在RAM地址范围还是Flash地址范围。只要运行地址在RAM里这一层基本没问题。再确认中断是否在擦写前被关闭。很多工程用了操作系统或者复杂的定时器中断中断服务函数本身在Flash里。擦写期间Flash不可读一旦中断进来CPU去取中断向量就卡死。我见过一个典型的案例某同事在Demo里加了串口打印用于调试结果擦写期间串口中断一触发系统就死机后来把打印挪到擦写完成之后才消除问题。如果你用的是寄存器直操作方案还有一个位置也很关键状态寄存器里的触发位操作顺序。顺序错了命令不会真正被触发轮询状态位就会一直卡在忙状态。这类问题只能对照数据手册里寄存器位的描述逐条检查代码逻辑。4.2 擦除后读取数据仍为原值段选择或锁定问题这个问题的表现是跑完擦除函数再读目标地址发现里面的数据还是老朋友一样原封不动。我排查后总结了两个高概率原因。第一个是段选择寄存器配置有误。擦除命令只能作用于段选择寄存器里指定的段如果你要擦的是H段但段选择寄存器里配置的却是A段那么H段当然毫发无损。这个错误的隐蔽性在于库函数本身不会报错因为它认为你想擦的就是A段。第二个是写保护或者CSM锁定。F28335的Flash有一种写保护机制一旦通过相关寄存器写入了保护标志后续的擦除和编程操作都会被拒之门外。CSM锁定则更严重相当于整块Flash被上了锁密钥不对的话不仅擦不了连读都读不了。做实验时如果发现Flash彻底无法擦写优先怀疑是不是之前误操作设置了CSM密钥用调试器查看CSM相关寄存器的状态即可确认。顺带说一个与硬件相关的原因有些开发板上的Flash供电引脚默认接得不规范或者没上拉到位。擦除操作需要内部高压泵正常工作供电不稳定就会导致擦除失败但不报错。这种情况比较少见但板子只能跑一次擦写、复擦就出问题的话就要考虑硬件连线了。4.3 编译优化导致擦写函数执行异常这是一个只有老手才懂的坑新手遇到了通常会一头雾水。现象是同一套代码在-O2优化等级下擦写失败或者死机改成-O0优化等级就一切正常。很多人以为是编译器bug其实多数时候是代码里存在对时序敏感的操作被编译器优化掉了。怎么排查第一步先把优化等级降回-O0或者-O1确认问题是否消失。如果消失再逐步开启更高优化级别同时检查哪些代码被改写了。第二步检查擦写函数里有没有volatile关键字的缺失。Flash控制寄存器必须声明为volatile否则编译器可能把对状态寄存器的轮询读取优化掉导致根本检测不到操作完成。官方头文件里寄存器结构体通常都有这个关键字但如果你自己封装了寄存器操作函数很容易漏。还有一个关联问题是官方Flash API库本身对编译器版本有适配性要求。不同版本的编译器编译出来的代码行为和库内部实现可能存在兼容问题表现就是库函数调用后返回异常值。遇到这种问题方案就两条换用和库匹配的编译器版本或者换成寄存器直操作方案绕开库。4.4 掉电与复位对Flash状态的破坏与恢复策略Flash擦写过程中掉电是最危险的情况它导致的后果不是简单的数据错误而是整块Flash可能停在一种不可预期的状态。先说现象。掉电时Flash内部高压操作刚进行到一半存储单元的电荷状态处于中间态。重新上电后你再去读那块区域可能读出一些既不是原始数据也不是新数据的非法值或者读操作本身就不正常最坏的情况是Flash控制器的状态机卡住后续所有擦写操作都失败。应对策略我总结为三条。第一条是在硬件上做电源监控检测到掉电瞬间立即封锁Flash操作允许信号宁可这次升级失败也不让Flash处于半写入状态。第二条是在软件上做好备份区设计A/B分区来回写写坏了可以从另一个区启动配合一个升级计数标志来判断哪个区是完好的。第三条是在升级流程协议上增加“写完成标志”这一步只有全部数据写完并且校验通过后才在专门的位置写入“新程序有效”标志Bootloader每次启动只信任这个标志。这三条本身就是很多商用Bootloader的核心设计理念。入门Demo阶段可能用不上但如果你以后要做正式产品建议一开始就把掉电恢复考虑进去不然上线之后迟早会被现场断电折磨一遍。我以前做某款设备在线升级功能时第一批样机烧了Flash API库就跑没有任何掉电保护结果在高温老化试验中连续遇到几台设备升级失败变砖。后来加上分区备份和完成标志才彻底解决这个问题。这里面每一行代码都是拿教训换的。最后再分享一个小技巧如果你的项目只需要保存一些不经常改的参数Flash擦写频率不高完全可以只在升级的时候打开全局中断其他时间保持关闭这样能最大程度避免擦写被中断打扰。Flash擦写这件事宁可慢一点也要稳一点因为一旦写坏代价是整个系统重新烧录甚至返厂。我把这些经验写出来希望能帮后来的人少走几个弯路。