2026/10/30 10:30:05

DSP28335片内Flash擦写实战:RAM搬运、参数保存与固件升级

DSP28335片内Flash擦写实战:RAM搬运、参数保存与固件升级 简介Flash28335_API_V210 是 TI 面向 C2000 系列 DSP28335 提供的官方 Flash 操作固件库适合从事电机控制、工业自动化、电源管理等嵌入式开发的中高级工程师及学习者用于解决片内闪存的扇区擦除、数据写入、读取校验与固件升级等底层管理问题。压缩包共 16 个文件约 248KB以 h 头文件、cmd 链接命令、asm 汇编和 c 源文件为主另有 lib 库、pdf 说明文档与 pjt 工程文件覆盖接口声明、底层实现、内存分配和示例工程配置便于在 CCS 环境中对照集成。已有 2101 人学习下载说明其在 DSP 闪存开发中具有较高参考价值。库中提供 API 配置、全局变量定义、CSM 密钥及读写擦除示例读者可据此理解 Flash API 的调用约束、错误处理与寿命管理思路并迁移到自己的 DSP28335 项目中减少底层驱动摸索成本。1. 一块被反复擦写的 Flash这份 API 包到底解决什么问题设备装到现场之后最怕的不是算法跑不动而是参数丢了、升级包写一半断电了。F28335 这颗片子片内自带 Flash很多人第一反应是直接往地址上怼指针结果第一次跑通、第二次擦挂、第三次整片起不来。Flash28335_API_V210就是厂商把“擦除、编程、校验”这一套时序和算法封装成静态库之后放出来的东西配套头文件、参考手册和示例工程专门给 DSP28335 这类 C2000 内核的芯片做片内 Flash 的读写。它适合两类人一类是要在板子上做参数区掉电保存的一类是要做固件在线升级的。前者需求简单但要求稳后者链路长、坑多。我这次接手的板子两样都占了所以把这份 DSP28335 Flash_API 从头到尾拆了一遍下面按“包在哪、怎么调、跑在哪个 RAM、哪里会翻车、怎么改成自己的业务”这个顺序讲。2. 包内结构与工程配置先把这几处对齐再动手拿到一个库压缩包最容易犯的错是先看代码。我的习惯是先看目录因为目录结构基本泄露了作者的意图哪些是给人看的、哪些是给编译器看的、哪些只是示例。这个包的结构不复杂但有几处命名约定如果一开始没对齐后面链接期的报错会让你怀疑人生。2.1 目录构成与三份关键文件我手上这份解压出来大致是这么个形态不同打包方式名称可能略有出入但职能是一致的Flash28335_API_V210/ ├── doc/ │ └── Flash28335_API_Reference.pdf # 接口说明、状态码、时序约束 ├── include/ │ └── Flash28335_API.h # 唯一需要 include 的头 ├── lib/ │ └── Flash28335_API_Library.lib # 预编译好的擦写算法 └── example/ ├── Flash28335_API_Example.c # 最小可运行示例 └── F28335.cmd # 段分配参考四类东西的定位要说清楚。doc里那份 PDF 不是用来通读的是用来在写状态判断时查错误码的尤其FLASH_ST里那几个字段含义头文件里只有声明没有解释。include目录下只有一个头说明接口面很窄这是好事意味着你能调的函数就那么几个不存在“用错函数”的空间。lib是预编译库源码不开源所以擦写时序你没法改只能顺着它来——这也是后面很多坑的根源它内部的延时循环依赖于你告诉它的主频告诉错了它就按错的节奏去擦芯片当然不认。example是最值钱的部分虽然只有一两百行但 RAM 搬运、cmd 段分配、中断处理这三个最容易写错的地方它都示范了一遍我基本是照着它改的。提示先把 example 原封不动编译烧录一次确认板子能跑通再动自己的代码。这一步能排除掉九成的环境问题。2.2 工程侧要配的三样东西在 CCS 里新建或者打开自己的工程之后需要配的不是一堆选项就三样头文件搜索路径、库文件链接、cmd 段分配。头文件路径加include目录这个没什么可说的。库文件在工程属性的链接器选项里把Flash28335_API_Library.lib加进去注意顺序——如果工程里还有别的手写汇编库把这个 Flash 库放在靠前的位置因为符号解析是从前往后走的放后面容易出现“符号重复定义”或者“找不到符号”这种看着矛盾的报错。cmd 文件是三样里最需要动手改的。示例里那份 cmd 通常已经写好了ramfuncs段的加载地址和运行地址你要做的是确认这段空间在你的工程里没有被别的代码占用。常见的写法是给 Flash API 单独划一小块 RAM比如 1K 字左右专门放搬运过去的擦写函数和它用到的常量。这块空间不需要大但必须固定否则编译器一优化函数被挪到别处搬运就搬了个寂寞。还有一个容易忽略的点编译器版本。这类老库通常是用比较早期的编译器版本编出来的如果你用的是新版本的编译器默认的 ABI 或者目标文件格式可能不一样链接时会报格式不匹配。解决办法一般是在工程属性里把运行时支持库选成静态链接或者干脆用库里示例工程指定的那个编译器版本去编。我踩过一次报错信息完全不指向真正原因查了半天才发现是工具链版本的事情。2.3 先做一次版本自检正式调用擦写之前建议在初始化阶段加一句版本检查。库提供了一个读版本号的接口返回的是十六进制编码比如0x0210表示 2.10 版。写这一句的好处是万一你从别处拷了一份头文件、库文件版本对不上编译能过但运行时行为诡异这一句能让你第一时间发现问题。#include Flash28335_API.h void FlashApi_CheckVersion(void) { Uint16 ver Flash_APIVersionHex(); /* 0x0210 对应 2.10 版与头文件声明的宏比对 */ if (ver ! 0x0210U) { /* 版本不一致不要继续往下走先把库换对 */ while (1) { ; } } }Flash_APIVersionHex()没有入参返回Uint16高字节是主版本、低字节是次版本。这里用死循环兜底而不是打印日志是因为这种板子往往没有串口输出跑飞了也不好查卡死在一个明确的判断点上比带着错版本继续擦 Flash 安全得多。实际项目里可以改成点亮一个故障灯或者写一个错误码到备份寄存器里方便复位后回读。3. 擦写校验三件套核心 API 逐段拆解接口面窄归窄但每个函数的参数都有讲究尤其是扇区掩码和长度单位这两个地方写错一个数就是整片数据没了。这一章按调用顺序把初始化、擦除、编程、校验四个环节拆开讲每个环节给一段能直接抄的代码然后说清楚参数为什么这么传。3.1 初始化缩放因子和回调是两个必设项库内部有一段纯软件延时循环用来满足 Flash 控制器对擦写时序的要求。这段循环跑多少圈取决于 CPU 当前的主频所以必须由调用方告诉它。告诉它的方式就是设缩放因子。/* 假设 SYSCLKOUT 跑到 150MHz按库文档给的比例换算 */ #define CPU_FREQ_MHZ 150.0 #define FLASH_SCALE_VAL ((Uint32)((CPU_FREQ_MHZ * 1000000.0) / 65536.0)) void FlashApi_Init(void) { /* 1. 先设主频缩放因子必须在任何擦写动作之前 */ Flash_CPUScaleFactor(FLASH_SCALE_VAL); /* 2. 注册回调擦除长扇区时库会周期性调用它 */ Flash_CallbackPointer(FlashApi_Callback); } /* 回调里做喂狗和轻量轮询千万不要在这里调擦写函数 */ void FlashApi_Callback(void) { /* 看门狗喂狗 */ /* 如果有心跳灯或者通讯轮询也可以放这里 */ }Flash_CPUScaleFactor的入参是个Uint32量纲是“每毫秒对应的循环计数”具体公式以参考手册为准150MHz 下算出来在两千多这个量级。这里最容易翻车的是主频改了、这里没跟着改——比如调试阶段为了降功耗把主频降到 100MHz缩放因子还是按 150MHz 算的库内部按更慢的节奏等看起来好像更保险实际上 Flash 控制器的时序窗口是双向的等太久同样会失败。Flash_CallbackPointer注册的回调是个无参无返回的函数指针传NULL也可以但如果你要擦除的扇区比较大建议老老实实注册一个把喂狗放进去否则擦到一半看门狗复位前功尽弃。还有一点这两个初始化函数必须在EALLOW/EDIS保护之外调用还是之内早期版本和后期版本有差异。稳妥的做法是照着示例工程的写法来别自己发明。3.2 Flash_Erase扇区掩码是怎么拼出来的擦除是按扇区来的最小擦除单位就是一整个扇区没有“只擦一个字”这种操作。要擦哪些扇区用一个 16 位掩码表示位 0 对应扇区 A位 1 对应扇区 B依次往下。FLASH_ST flashStatus; void FlashApi_EraseSectors(Uint16 sectorMask) { /* 擦除前把全局中断关掉擦写期间不要响应中断 */ DINT; /* 清状态结构体避免用上一次的残留值做判断 */ flashStatus.Module 0; flashStatus.Bank 0; flashStatus.Sector 0; flashStatus.Status 0xFFFF; /* 调用擦除掩码决定擦哪几个扇区状态回填到 flashStatus */ Flash_Erase(sectorMask, flashStatus); EINT; /* 判断真正要看的是 Status 字段不是函数返回值 */ if (flashStatus.Status ! STATUS_SUCCESS) { /* 擦除失败按 Status 查手册定位 */ FlashApi_HandleError(flashStatus.Status); } }Flash_Erase的两个参数第一个是扇区掩码第二个是指向FLASH_ST的指针用来回填执行结果。注意这个函数没有返回值成没成要看结构体里的Status。FLASH_ST有四个字段Module和Bank在某些器件上是保留的Sector表示出错时定位到的扇区号Status才是错误码等于 0 表示成功。我见过有人在函数返回后直接写if (Flash_Erase(...))编译都过不去因为返回类型是 void这属于没看头文件就动手。掩码拼装的时候要格外小心。擦扇区 C 就是0x0004擦扇区 A 和 B 是0x0003千万别写成0x0005还以为自己擦了 A 和 B。我一般会在头文件里给每个扇区定义一个宏用的时候按位或比直接写十六进制数可读性好得多。注意擦除是不可逆操作。调试阶段强烈建议先拿一块不存关键数据的扇区练手确认时序和状态判断都对了再动程序区。3.3 Flash_Program 与 Flash_Verify必须成对出现编程是以字为单位往已擦除的区域写数据校验是拿写进去的内容和源缓冲区逐字比对。这两个必须成对用写完立刻校验别等到下次上电才发现数据是坏的。/* 从 buffer 往 flashAddr 写 length 个字写完立刻校验 */ Uint16 FlashApi_WriteBlock(Uint16 *flashAddr, Uint16 *buffer, Uint32 length) { FLASH_ST st; /* 1. 编程 */ Flash_Program(flashAddr, buffer, length, st); if (st.Status ! STATUS_SUCCESS) { return st.Status; } /* 2. 校验参数顺序和编程一致目标地址、源地址、长度 */ Flash_Verify(flashAddr, buffer, length, st); if (st.Status ! STATUS_SUCCESS) { return st.Status; } return STATUS_SUCCESS; }Flash_Program和Flash_Verify的参数表完全一样四个参数目标 Flash 地址、源缓冲区地址、长度、状态指针。这里长度单位是“字”不是“字节”F28335 是 16 位机一个地址对应一个字所以缓冲区的元素类型用Uint16最自然。如果从通讯口收到的是字节流要么在接收阶段就拼成字要么在这里做转换我一般选前者因为字节转字的代码写多了容易在奇偶长度上出错。编程还有个隐含前提目标区域必须是空的也就是已经被擦成全0xFFFF。往非空区域写会失败或者写出脏数据所以每次写之前问自己一句“这块擦过了吗”。如果业务上需要频繁改一小段参数聪明的做法是留出两三个扇区做轮转每次写新的擦旧的而不是反复擦同一个扇区因为 Flash 的擦写寿命是有限的反复擦同一个地方早晚会坏。3.4 状态码怎么读错误怎么定位Status字段的取值在头文件里有宏定义成功就是STATUS_SUCCESS其余是各类失败。定位问题的顺序我一般是这样先看是不是Sector字段有值有值说明错误和特定扇区相关多半是那块区域没擦干净或者被锁了再看错误码是不是“编程失败”如果是回头查源缓冲区长度是不是超了扇区边界如果错误码比较泛比如“操作失败”那就往上查时钟和缩放因子因为时序不对的表现往往就是这种泛泛的失败。这里有个经验擦除失败的报错通常比编程失败更有信息量因为擦除涉及的硬件状态更多。如果连续两次擦同一个扇区都失败基本可以断定是硬件层面的事情比如供电不稳或者片子本身有问题别再在软件里找了。4. 让代码跑在 RAM 里cmd 段搬运的完整链路Flash 的擦写算法执行时Flash 控制器本身处于忙状态这时候 CPU 从 Flash 里取指令会读到不确定的内容。所以擦写函数必须搬到一个不依赖 Flash 的地方执行也就是 RAM。这一步是整个项目里最容易出错、报错信息最不友好的环节值得单独拿一章说。4.1 为什么不能从 Flash 里擦 Flash这个限制是硬件决定的不是软件偷懒。Flash 控制器在擦除或编程期间会占用 Flash 阵列的访问通路任何从 Flash 取指的读操作都会拿到无效数据轻则跑飞重则触发总线错误。而中断向量表默认也在 Flash 里所以擦写期间如果来了中断CPU 去向量表取地址同样会出事。理解这一点之后后面所有配置就都顺了搬运的目的地是 RAM搬运的对象是所有会在擦写期间执行的代码包括擦写函数本身、它调用的辅助函数、以及中断服务程序。4.2 memcpy 搬运的写法与符号含义示例工程里的搬运代码通常长这样/* 这些符号由链接器在 cmd 文件里生成不是变量别赋值 */ extern Uint16 RamfuncsLoadStart; extern Uint16 RamfuncsLoadEnd; extern Uint16 RamfuncsRunStart; void FlashApi_CopyToRam(void) { Uint16 *src RamfuncsLoadStart; Uint16 *dst RamfuncsRunStart; Uint32 len (Uint32)(RamfuncsLoadEnd) - (Uint32)(RamfuncsLoadStart); /* 按字搬运len 单位是字 */ while (len--) { *dst *src; } }这三个符号是链接器根据 cmd 文件里的段定义自动生成的RamfuncsLoadStart和RamfuncsLoadEnd标出这段代码在 Flash 里的存放范围加载地址RamfuncsRunStart标出它在 RAM 里的运行地址。搬运就是把加载地址到结束地址之间的内容按顺序复制到运行地址开始的地方。注意长度算的是字节差还是字差Uint16*指针相减得到的是元素个数但如果直接拿地址做减法再赋给Uint32得到的是字节数这时候搬运循环里要按字节处理或者除以 2。我在两个不同工程里见过这两种写法都能跑但混着写就会搬少一半或者搬越界。搬运的调用时机很关键必须在第一次调用 Flash 擦写之前而且只能搬一次。放在main函数开头、外设初始化之后、业务逻辑之前是稳妥的位置。4.3 cmd 文件里把函数钉在 RAM光搬运还不够得让编译器知道哪些函数要放进去。/* 简化示意实际按工程情况调整 */ SECTIONS { .text : FLASH, PAGE 0 .ramfuncs : LOAD FLASH, RUN RAML0, LOAD_START(RamfuncsLoadStart), LOAD_END(RamfuncsLoadEnd), RUN_START(RamfuncsRunStart), PAGE 0 }然后给需要搬进 RAM 的函数加编译指示或者用段属性声明。库里的擦写函数已经编译进.lib了它的段名可能是.TI.ramfunc或者类似的具体看手册。你要做的是把这个段名和上面 cmd 里的ramfuncs对应上或者直接用库示例里那份 cmd 改。易错点在 RAM 空间大小。RAML0要足够放下整段代码不够的话链接器会报段溢出这个错误信息还算直白。但也有链接能过、运行却挂的情况RAM 区域和别的变量段重叠了搬运的时候把变量覆盖了表现是程序在别的地方莫名其妙跑飞。我一般会在搬运前先把RamfuncsRunStart和RamfuncsLoadStart的地址打出来看一眼确认那块区域是干净的。4.4 擦写期间的中断与看门狗中断的处理有两种思路选一种就行别混用。第一种是擦写全程关中断简单粗暴适合擦写时间短、对实时性要求不高的场景。第二种是把中断向量表也搬进 RAM擦写期间中断照常响应但中断服务程序必须也在 RAM 里否则跳过去就挂。我这次的项目选的是第一种因为参数区写入量小全程关中断也就几十毫秒对电机控制环的影响可以接受。但如果你的业务里有不能丢的通讯中断那就得走第二种代价是向量表和所有 ISR 都要搬RAM 占用和配置复杂度都上去了。看门狗必须处理。擦除一个扇区的时间在几十毫秒量级超过看门狗默认溢出时间的工程很常见。解法有两种一是在回调里喂狗前面 3.1 已经说了二是擦写前临时关掉看门狗擦完再开。我倾向回调喂狗因为关看门狗会把保护窗口也丢掉万一擦写本身卡住了你连复位都不会有。注意回调函数的执行环境是库内部调用的不要在回调里做耗时操作也不要调任何带阻塞的接口喂狗、置标志位就够了。5. 常见问题排查五个当场能定位的翻车点这一章是我在调试过程中真实遇到并记录下来的按“现象、原因、解决”的结构写每个都尽量写到能直接对着改的程度。5.1 卡在擦除函数里出不来现象程序烧录后一运行就停在Flash_Erase内部断点进去发现出不来复位也没用只能重新上电。原因最常见的是搬运动作没做或者做晚了。擦写代码还在 Flash 里跑Flash 控制器一忙取指失败程序就卡死了。第二个可能是主频缩放因子设错库内部按错误的延时等待 Flash 控制器就绪永远等不到。解决先确认搬运函数在擦除调用之前被执行过可以在搬运后打个断点看 RAM 里的内容和 Flash 里的内容是否一致。再确认缩放因子是不是按实际SYSCLKOUT算的临时把主频配置和缩放因子都改回示例工程的值跑一次能跑通就说明是这两个之一的锅。5.2 擦完再读数据没变现象擦除返回成功状态码也是 0但是回读那块区域还是老数据。原因多半是掩码写错了扇区。你算了半天应该擦的是 D 区掩码却给了 B 区擦是擦了擦的是别的地方。还有一种可能是地址映错误代码里用的基地址和实际要擦的地址不是一个区。解决把掩码和扇区编号的对应关系写成宏别手写十六进制。然后在擦除前后各回读一次目标地址的头几个字对比一下是不是从老数据变成了0xFFFF。几个字的对比成本很低能省掉很多猜测。5.3 编程成功但校验失败现象Flash_Program返回成功紧接着Flash_Verify报错比对不上。原因第一嫌疑是目标区域没擦干净编程是往全 1 的区域写如果有残留数据写进去的结果是“与”运算的结果自然和源数据对不上。第二嫌疑是长度单位传的是字节数但函数按字数处理结果只写了一部分。解决编程前先读一遍目标区域确认是全0xFFFF。长度参数统一用元素个数并且把缓冲区的类型固定成Uint16从根上杜绝单位混淆。5.4 擦写中途复位现象擦除执行到一半板子自己重启了重新上电后那块 Flash 是半擦除状态数据全乱。原因看门狗溢出。擦大扇区的时候时间超了回调没喂狗或者根本没注册回调。解决注册回调并在里面喂狗同时把看门狗的超时时间往上调一档。另外可以在擦除前把一个标志字写到备份寄存器复位后读这个标志如果是“擦除中”说明上次异常走恢复流程。5.5 链接报错或者运行到一半跑飞现象编译链接时提示段溢出或者符号重复或者链接过了但运行到某处就飞。原因cmd 文件里ramfuncs段的大小和实际需要的代码量不匹配小了报溢出大了可能和别的段重叠。重叠这种情况链接器不一定报错运行时才炸。解决先看链接生成的 map 文件找ramfuncs段占了多大再对照 cmd 里分配的空间。把搬运前后的地址都打印出来确认目标区域没有被别的变量占用。这一步做一次以后换工程可以直接复用。6. 二次开发把裸接口包成参数区和升级流程库给的是原子操作项目要的是业务能力。我这次的改造是把擦写接口包成两层底层是参数区读写上层是升级流程两层的失败处理策略完全不同。6.1 参数区封装加头加校验再加双备份裸接口写进去的是原始数据掉电掉在写一半的时候你没法知道数据是新的还是坏的。解法是给每次写入加一个头和一段校验。/* 参数块头魔数 长度 校验写在数据前面 */ typedef struct { Uint32 magic; /* 固定值用来判断这块区域是否被初始化过 */ Uint16 length; /* 数据区字节数 */ Uint16 crc; /* 数据区的 CRC16 */ Uint16 version; /* 参数结构版本升级时用来做兼容 */ } PARAM_HEAD; Uint16 Param_Save(Uint16 *data, Uint16 words) { PARAM_HEAD head; Uint16 buf[PARAM_MAX_WORDS 4]; head.magic 0x50415241UL; /* PARA */ head.length words * 2; head.version PARAM_VERSION; head.crc Crc16((Uint8*)data, words * 2); /* 头和数据拼成一整块一次写下去减少中间态 */ memcpy(buf, head, sizeof(PARAM_HEAD)); memcpy(buf[4], data, words * 2); return FlashApi_WriteBlock(PARAM_FLASH_ADDR, buf, words 4); }这段代码的关键不是 API 调用本身而是“一次写完整块”这个原则。头和数据分两次写中间掉电就会出现头是新的、数据是旧的这种最难查的状态。拼成一次写最坏情况是整块都没写进去读的时候 CRC 对不上直接回退到备份区。备份区就是另一个扇区写入策略是主备轮换每次写都写备份、校验通过后再擦主区虽然多一次擦除但换来的是任何时刻都至少有一份完整数据。magic的值别随便换换一次等于所有老设备的数据作废。我第一次改这个结构的时候顺手换了个魔数结果现场升级之后参数全丢用户配置得重新设一遍。6.2 升级流程分段校验比整体校验更实用固件升级的流程大致是收到数据、写进暂存区、全部收完后校验、校验通过再拷贝到运行区。这里的技巧是在接收阶段就做分段校验每收到一个扇区的量就校验一次写入暂存区之后再回读校验。这样出问题的时候能精确到是哪个扇区坏了而不是最后整体校验失败重来一遍。暂存区和运行区的划分要提前规划好运行区在低地址、暂存区在高地址两边各留出至少一个扇区的余量做状态标记。状态标记用一个单独的字写进去之后读回来确认确认成功再进行下一步。这个习惯救过我一次暂存区快写满的时候供电波动状态字读回来不对流程直接中止没有往下走运行区里的旧固件完好无损。从那以后我每次做 Flash 相关的改动都强制先在一台空板子上跑一遍“写、断电、上电读、比对”的完整流程而不是在调试器里连着跑通过就算完。调试器连着的时候供电稳、时序松很多问题根本暴露不出来一旦脱机运行才现原形。希望这些经验能帮你少走几个弯路。本文还有配套的精品资源点击获取