
简介针对TMS570LS0432这款TI汽车级MCU提供了一套完整的片内Flash读写自建工程适合嵌入式开发者在实际项目中快速上手Flash的擦除、编程、读取与校验操作。工程涵盖初始化代码、擦除/编程/读取函数及错误处理机制并附有F021 Flash API库可帮助理解Bank、扇区、页的层级关系与锁区保护等要点。资源包共27个文件以17个C/C头文件、6个C源文件为主另含2个汇编启动文件、1个链接命令文件及1个F021 API静态库压缩包大小约936KB。头文件定义Flash寄存器与系统配置C源文件实现具体读写逻辑汇编文件负责中断向量与启动流程结构清晰便于二次开发。目前已有913人学习下载适合正在调试TMS570LS0432底层存储、需要参考官方API调用与工程组织方式的开发者。通过运行该测试工程能够直接验证片内Flash读写功能减少从零搭建环境的成本提升开发效率。 搞嵌入式这么久TMS570LS0432这颗芯片其实挺有代表性的。它属于TI Hercules系列基于ARM Cortex-R4F内核主打功能安全在很多车载和工业控制项目里都能见到。芯片本身不算难上手但不少人的第一道坎就卡在片内FLASH读写上照着网上的例程抄下来编译下载一路绿灯结果一跑起来要么擦除卡死要么写进去读回来完全不对严重一点的直接连调试器都连不上报一个“flash download failed - target dll has been cancelled”的错误整得人一头雾水。这篇文章我把自己做TMS570LS0432片内FLASH读写例程自建工程的完整过程捋一遍从芯片的Flash硬件特性、自建工程怎么组织到擦除、编程、读回校验的代码实现再把调试点和常见坑一次性说清楚。不管你是要做Bootloader、IAP升级还是想在片内FLASH里存校准参数这篇都能帮你少走不少弯路。1. 先搞明白TMS570LS0432的片内FLASH1.1 硬件底子容量、地址和扇区TMS570LS0432的片内FLASH属于NOR型Flash容量我记得是384KB以你实际芯片和数据手册为准TI不同批次型号细节可能有差异起始地址是0x00000000上电后CPU直接从这里取指执行也就是代码可以在原地运行XIP。片内RAM是48KB起始地址0x08000000有FLASH模块控制器FMC和F021 Flash控制器负责对FLASH的擦写操作。NOR Flash相比NAND Flash最大的特点就是支持随机读取所以才能直接映射到地址空间里当程序存储器用代价是容量做不大、擦写速度一般。TMS570这种车规MCU用NOR片内Flash图的就是可靠性和执行效率而不是容量。对于Cortex-R4F这类内核程序读Flash和读普通内存本质上走的是同一套总线地址但写Flash完全不同这点后面会详细讲。扇区划分方面TMS570LS0432不是整片均匀分块不同容量版本、不同Bank的扇区大小不一样一般低地址区会有几个比较小的扇区高地址区是大扇区。做自建工程前建议先把数据手册里“Memory Map”和“Flash Sector Configuration”两张表看明白然后用TI官方的UniFlash工具连上芯片把实际扇区读一遍这样后面选测试地址才不容易踩雷。1.2 NOR Flash为什么不能直接像RAM一样写很多从STM32转过来的朋友会下意识觉得片内FLASH也是内存映射的那我直接定义一个指针往地址里赋值就完了。这个想法在读取方向是对的但在写入方向是行不通的。Flash单元的特点是擦除动作会把单元恢复成“1”而编程动作只能把“1”变成“0”。也就是说你想把一个已经写过的扇区重新写入不同数据必须先擦除把它全部变回0xFF然后再编程把需要的位写成0。这和RAM“随便覆盖”的逻辑完全不同。而且擦除是按扇区来的不是按字节来的你不能只擦一个字节这也决定了你写参数存储功能时一定要做扇区级别的规划。另外擦除和编程操作并不是CPU直接通过写地址总线就能完成的而是由F021 Flash控制器里的一套状态机FSM来处理。你需要把命令、地址、数据写到控制寄存器里然后FSM接管执行高压擦写最后通过状态寄存器报告结果。这就是为什么我们需要用TI的FAPIFlash API库或者照着F021寄存器流程来操作。1.3 最关键的一条FSM工作期间不能读同一个FLASH BankTMS570LS0432只有一个FLASH Bank这一点和TMS570LS12x系列的双Bank不太一样。F021状态机执行擦除或编程命令期间同一个Bank的Flash内容是不能被CPU读取的因为Flash阵列本身正被高压操作占用。如果你让CPU从Flash里取指执行擦除/编程的代码就会出现总线停滞严重时直接总线错误或者复位。这就是为什么例程里必须把FLASH操作相关的函数复制到RAM里执行。不是“最好”而是“必须”。很多从HALCoGen生成的Flash例程之所以能跑通正是因为它在链接脚本和系统初始化里已经帮你做了RAM运行段的拷贝只是你没意识到而已。2. 自建工程不靠HALCoGen也不等于从零手撸2.1 先搞清楚要自建哪些内容所谓“自建工程”我个人的理解不是让你从零手写启动代码、手写器件初始化而是不依赖HALCoGen一键生成的那一套完整工程自己搭一个最小可运行的骨架每一行关键代码都知道它在干什么。HALCoGen确实能直接生成Flash操作例程但生成出来的东西会捎带一大堆外设初始化、诊断和ESM相关代码对只想搞明白Flash读写的人来说反而增加了阅读成本。自建工程的好处是链接脚本自己写启动流程自己控RAM段安排自己定后续扩展Bootloader或者OTA的时候内存布局、中断向量表、Flash分区这些都能做到心里有数。但要注意F021 Flash API这套库本身还是要用TI官方的这套东西包含了F021控制器底层状态机的完整驱动自己重写的风险很大也没必要。自建工程是“自建工程结构”不是“自建Flash驱动”。2.2 最小工程文件清单我用CCSCode Composer Studio搭了个最小工程目录结构大概长这样tms570ls0432_flash_test/ ├── include/ │ ├── F021.h // TI F021 Flash API 头文件 │ ├── reg_flash.h // FLASH/FMC寄存器定义 │ └── system.h // 系统函数声明 ├── src/ │ ├── startup_ccs.c // 启动复位向量、C运行时入口 │ ├── system.c // 时钟、Flash等待状态、看门狗 │ ├── flash_api.c // 封装擦除/编程/校验函数 │ ├── main.c // 测试主流程 │ └── tms570ls0432_flash.cmd // 链接脚本CCS里新建工程时要选对器件型号TMS570LS0432编译器用TI ARM编译器。F021库文件可以从TI官网下载注意选择匹配TMS570LS04x/03x的版本不要直接拿别的系列比如TMS570LS31x的库过来用定义和接口可能有差异。启动代码和系统时钟初始化我还是参考了HALCoGen生成的内容但只保留最小必要的部分然后把没用的外设初始化全部删掉。2.3 链接脚本FLASH和RAM的换位配合自建工程里最核心的其实是链接脚本。Flash擦写函数要放到RAM里运行意味着这部分代码的“加载地址”和“运行地址”是分开的加载时放在FLASH里上电后再拷贝到RAM里运行。链接脚本里我是这样安排的MEMORY { FLASH0 (RX) : origin 0x00000000, length 0x00060000 /* 384KB */ RAM0 (RW) : origin 0x08000000, length 0x0000C000 /* 48KB */ } SECTIONS { .text : {} FLASH0 .const : {} FLASH0 .cinit : {} FLASH0 .data : {} RAM0 .bss : {} RAM0 .stack : {} RAM0 .flashAPI : { F021.o(.text) flash_api.o(.text) } LOAD FLASH0, RUN RAM0, LOAD_START(flashApiLoadStart), RUN_START(flashApiRunStart), SIZE(flashApiSize) }这里把F021库和目标文件flash_api.o的代码段整体放进了.flashAPI段然后指定LOAD在FLASH、RUN在RAM。LOAD_START、RUN_START、SIZE这三个符号会在链接时自动生成供C代码做拷贝使用。有一点要提醒F021库里如果包含常量查表数据比如某些时序参数表默认放在.const段仍然在Flash里。执行FSM命令期间如果代码访问这些Flash里的常量表同样会出问题。所以拷贝时最好把涉及到的相关的配置数据也一并放到RAM段具体看你的库版本和map文件。TI官方多数版本的F021库已经做了RAM执行适配但自己还是确认一下更稳妥。2.4 系统初始化和看门狗别留隐患自建工程里系统初始化最少要做三件事配置PLL把主频提上来并等待锁相环稳定、配置Flash读取等待周期Wait State、把看门狗做初始化或者测试模式下直接关掉。Flash等待周期这块很容易被忽略。主频跑高以后Flash读取时序如果不匹配程序会随机跑飞而且这种问题最难查因为它不是每次上电必现。HALCoGen生成的sys_pmm.c里面有在不同频率下设置等待状态的表自建工程里我建议直接把这部分参考过来不要自己拍脑袋写寄存器。看门狗则是另一个隐形杀手。擦除一个扇区需要几十毫秒编程命令虽然短但整套流程走下来也够看门狗超时的。如果在擦写过程中没有及时喂狗芯片就会复位复位时Flash状态机还在忙下一次调试器连接就会出现各种各样奇怪的下载失败问题。调试期间最省事的办法是把看门狗关掉等真正做Bootloader时再严格设计喂狗时序。3. 读写FLASH的核心机制和关键代码3.1 F021状态机与FAPI初始化F021控制器本质上是一个独立的有限状态机。我们通过FAPI库向它下发命令它会自己去处理高压擦除、编程、ECC计算这些脏活。FAPI库里最常用的几个函数有函数作用Fapi_initializeFlashBanks初始化Flash时钟参数和Bank信息Fapi_enableMainBankSectors使能需要操作的扇区Fapi_issueAsyncCommandWithAddress下发异步命令比如擦除扇区Fapi_setupFlashSectorForProgram设置编程目标地址Fapi_issueProgrammingCommand下发编程命令并传入数据Fapi_checkFsmForReady轮询状态机是否空闲初始化时最关键的是正确填写Fapi_ClockInitTypeDef结构体里的时钟频率。sysClock填系统主频uoauClock和stmClock按照你的时钟树配置填。如果这三个频率填错FSM计算出来的擦写时序就不对轻则操作失败重则把扇区写成半残状态。void FlashInit(void) { Fapi_ClockInitTypeDef clk; clk.sysClock 80000000u; /* 系统时钟 */ clk.uoauClock 20000000u; /* 辅助时钟 */ clk.stmClock 20000000u; /* STM时钟 */ (void)Fapi_initializeFlashBanks(clk); /* 使能Bank0的所有扇区实际项目建议按需使能 */ Fapi_enableMainBankSectors(0xFFFFu); }在这里我把Fapi_initializeFlashBanks的返回值忽略掉了简单测试无所谓。如果你要做得严谨返回值里包含Bank信息可以对它做断言判断避免初始化就失败后面全白搭。3.2 擦除扇区的完整流程擦除操作我封装成下面这个函数注意它和后面编程函数一样都要放在RAM运行段里#pragma CODE_SECTION(FlashEraseSector, .flashAPI) uint32_t FlashEraseSector(uint32_t addr) { Fapi_StatusType st; volatile uint32_t timeout 2000000u; /* 等待状态机空闲 */ do { st Fapi_checkFsmForReady(); if (--timeout 0u) { return 0xFFFF0001u; } } while (st Fapi_Status_FsmBusy); if (st ! Fapi_Status_Success) { return st; } /* 下发擦除扇区命令 */ st Fapi_issueAsyncCommandWithAddress(Fapi_EraseSector, (uint32_t *)addr); if (st ! Fapi_Status_Success) { return st; } /* 等待擦除完成 */ timeout 2000000u; do { st Fapi_checkFsmForReady(); if (--timeout 0u) { return 0xFFFF0002u; } } while (st Fapi_Status_FsmBusy); return (st Fapi_Status_Success) ? 0u : st; }擦除前一定要确认目标地址是某个扇区的起始地址或者在扇区范围内都可以但不要跨扇区。如果地址落到不存在的扇区命令会返回地址错误。擦除完成后如果要验证可以用普通指针读这片区域理论上应该全是0xFF。不过要注意ECC的问题我会在3.4节单独说。3.3 编程写入的完整流程编程和擦除不太一样编程前要先用Fapi_setupFlashSectorForProgram设置目标扇区然后以16字节128位为单位下发编程命令#pragma CODE_SECTION(FlashWriteData, .flashAPI) uint32_t FlashWriteData(uint32_t addr, uint32_t *buf, uint32_t lenBytes) { Fapi_StatusType st; uint32_t offset; /* 地址和长度都要16字节对齐 */ if (((addr 0x0Fu) ! 0u) || ((lenBytes 0x0Fu) ! 0u)) { return 0xFFFF0010u; } st Fapi_setupFlashSectorForProgram((uint32_t *)addr); if (st ! Fapi_Status_Success) { return st; } for (offset 0u; offset lenBytes; offset 16u) { st Fapi_issueProgrammingCommand( (uint32_t *)(addr offset), buf offset / 4u, 16u); if (st ! Fapi_Status_Success) { return st; } st Fapi_checkFsmForReady(); while (st Fapi_Status_FsmBusy) { st Fapi_checkFsmForReady(); } if (st ! Fapi_Status_Success) { return st; } } return 0u; }这里有两个细节必须强调。第一编程数据长度参数的单位是字节而且必须是16的倍数。因为TMS570LS0432在使能ECC的情况下每次编程必须写满一个128位的字否则ECC校验位算不出来读回时会触发ECC错误。第二buf指针必须是32位对齐的传参前最好在定义时就用uint32_t数组避免用char数组转来转去导致非对齐访问。另外Fapi_issueProgrammingCommand实际上是把数据从RAM里拷到Flash控制器的一个缓冲区再由FSM写入Flash阵列。所以数据缓冲区本身必须在RAM里不能直接指向Flash里的某个常量区否则命令执行时FSM会反过来读Flash这就和我前面说的问题冲突了。3.4 读回校验和ECC问题片内FLASH是内存映射读取就是普通指针访问不需要专门的Read命令uint32_t ReadFlashWord(uint32_t addr) { return *(volatile uint32_t *)addr; }注意一定要加volatile不然优化器高优化级别下可能直接把读取结果优化掉导致你看到的数据和实际Flash内容对不上。ECC纠错码是Hercules系列的老朋友了。默认情况下TMS570LS0432的Flash ECC是使能的擦除后的Flash读取一般没问题因为硬件对全0xFF的擦除态有特殊处理。但如果你在一个128位字里只写了8字节的数据那么另外8字节还是0xFFECC和数据就不匹配读的时候就会报ECC错误甚至可能触发ESM错误中断。所以我的建议是做数据存储时始终以16字节为单位进行写入每次写满一个整数个128位字。如果要做类似EEPROM的磨损均衡、小数据频繁更新那就得参考TI官方的EEPROM Emulation应用笔记用一整个扇区做管理而不是指望片内Flash能按字节随机改。4. 完整例程从擦除到校验一步到位4.1 主流程怎么设计我把测试目标地址放在0x0005F000这个地址在LS0432的Flash高地址区离程序代码和向量表都足够远不容易误伤正在运行的程序。测试数据用16字节固定模式擦除、编程、读回三步走比对一致就打印PASS不一致打印FAIL。主流程如下int main(void) { uint32_t i; SystemInit(); /* 时钟、Flash等待状态、看门狗处理 */ FlashApiInit(); /* 把.flashAPI段从Flash拷贝到RAM */ FlashInit(); /* 初始化F021使能扇区 */ writeData[0] 0x12345678u; writeData[1] 0x9ABCDEF0u; writeData[2] 0x0FEDCBA9u; writeData[3] 0x87654321u; /* 1. 擦除目标扇区 */ if (FlashEraseSector(TEST_ADDR) ! 0u) { return -1; } /* 2. 编程16字节 */ if (FlashWriteData(TEST_ADDR, writeData, 16u) ! 0u) { return -2; } /* 3. 读回比对 */ for (i 0u; i 4u; i) { readData[i] ReadFlashWord(TEST_ADDR i * 4u); if (readData[i] ! writeData[i]) { return -3; } } return 0; }有个顺序问题要特别强调FlashApiInit()必须在FlashInit()之前调用。因为.flashAPI段里的所有函数包括Fapi_initializeFlashBanks链接器都已经把它们的入口地址定位到RAM区了。如果不先执行拷贝就直接调用FlashInit实际跳转到的是RAM区里还没被覆盖的内存跑的是什么内容完全不可控大概率直接HardFault。这个坑我见过好几个同事踩过都是把拷贝函数放在初始化之后调用才反应过来。4.2 RAM拷贝函数的写法FlashApiInit本质就是在main里手动做一次内存拷贝extern uint32_t flashApiLoadStart; extern uint32_t flashApiRunStart; extern uint32_t flashApiSize; void FlashApiInit(void) { memcpy((void *)flashApiRunStart, (void *)flashApiLoadStart, (uint32_t)flashApiSize); }这段代码自己必须放在普通Flash区域不要放进.flashAPI段否则就递归了。拷贝函数执行过程中Flash还没有开始擦写FSM是空闲状态从Flash里读源数据完全没有问题。如果你用的是GCC而不是TI编译器把#pragma CODE_SECTION(函数名, .flashAPI)改成__attribute__((section(.flashAPI)))形式就行链接脚本的思路完全一致。4.3 实测现象和调试方法实测下来80MHz主频下擦除一个扇区大约几十毫秒量级编程16字节是毫秒级以下这个时间足够肉眼观察串口打印和仿真器的状态变化。我习惯用串口打日志流程大概是TMS570LS0432 Flash R/W Test Target: 0x0005F000 Erase sector ... OK Program 16 bytes ... OK Readback: 0x12345678 0x9ABCDEF0 0x0FEDCBA9 0x87654321 Verify ... PASS如果没接串口也可以直接在CCS里用Expressions窗口查看目标地址的Memory内容。调试时记得把编译器优化级别调低一点至少优化级别O0否则某些局部变量的实时观察值会被优化掉。还有一个小技巧在擦除或编程前对比RAM段拷贝前后的指令确认.flashAPI段的代码确实存到了RAM里。具体做法是用map文件或者直接在仿真器里查看flashApiRunStart对应地址的反汇编看到指令长得和Flash里的源一致就说明拷贝成功了。这个验证特别适合第一次自建工程的朋友能帮你排除很多“代码其实根本没在RAM里跑”的隐性故障。5. 常见问题与排查清单5.1 flash download failed - target dll has been cancelled这个报错我遇到得太多了基本所有做TMS570自建工程的兄弟都会被它教育一次。报错发生的时候看起来是CCS下载程序失败实际原因千奇百怪我整理过排查顺序如果刚跑过片内Flash自擦写程序然后复位了先别急着下程序用UniFlash或CCS的On-Chip Flash工具做一次整片擦除Mass Erase再重新下载。因为目标芯片里可能残留了半擦除状态Flash装载器Flash Loader一跑就崩。检查调试器连接确认Target电压正常复位引脚波形正常。尝试在Debug Configuration里勾选“Connect under Reset”也就是复位时连接很多时候能救回来。降低JTAG/SWD时钟频率把JTAG TCLK降到1MHz以下再试。特别是自己画的板子走线长、电容大高速调试接口很容易在Flash装载阶段掉链子。确认CCS里选择的Flash Loader器件型号和芯片一致。TMS570LS0432和TMS570LS0332虽然引脚兼容但Flash容量和组织不一样选错了Loader当然会报target dll被取消。检查板上的晶振和系统时钟配置。Flash装载器需要正确的系统时钟才能算出擦写时序如果你的板和HALCoGen默认配置的晶振不一致Loader就会在某个环节直接放弃。5.2 擦除/写入时程序卡死或复位这类问题的根源一般集中在“从Flash执行了FSM命令”或者“中断在擦写过程中抢占了CPU”。先检查代码是不是真的在RAM里执行。flash_api.c里的擦写函数如果忘了加#pragma CODE_SECTION或者链接脚本里没有把相应目标文件放进去函数就会留在Flash里擦写命令一下发CPU还在从Flash取指直接卡死。再看中断。擦写期间如果来了中断ISR入口和向量表都在Flash里中断一触发就要去读Flash而FSM正占着Flash同样会卡死。最简单的处理就是在擦写操作前后关中断_disable_interrupts(); FlashEraseSector(TEST_ADDR); FlashWriteData(TEST_ADDR, writeData, 16u); _enable_interrupts();不过关中断的代价是擦写期间不能响应任何实时事件真正量产做OTA时不能这么简单粗暴要设计好擦写窗口和中断策略比如把向量表和ISR都放到RAM。但对于验证例程来说关中断是最直接可靠的。5.3 写入后读回数据不对如果写完之后读回来个别位不对或者整片都乱掉从这几个方向排查地址和长度没做16字节对齐。非对齐写入在ECC模式下结果不可预期。擦除没成功或者根本没有擦。编程只能把1变0如果扇区之前不是全0xFF读回来的数据就是旧数据和写入数据叠加的结果。FAPI初始化时时钟参数填错了。这个错误比较隐蔽因为表面上看擦除编程都“成功了”但时序偏差导致写入的内部电荷不足读回来就不稳定。缓冲区对齐问题。数据缓冲区如果不是32位对齐FAPI内部在处理数据时可能出错最好强制对齐。编译器优化导致的读回问题。使用volatile指针读取调试时看反汇编确认真的有LDR指令读内存。5.4 别忘了ECC和MPUECC问题前面提到了再补充一个和MPU有关的坑。自建工程如果你加了MPU配置把Flash区域设置成只读权限那FAPI的编程操作会被MPU拦截导致写失败。排查时如果确认FAPI返回成功但Flash内容没变或者返回权限错误优先查MPU的配置。另外擦写样例里最好只操作你明确规划的测试扇区。万一不小心把向量表所在的低地址扇区擦了调试器立刻失联下次下载就会报一堆莫名其妙的问题。我习惯在工程里用宏定义区分“当前程序运行区”和“测试区”并在擦写前做一次地址范围检查从源头避免低级的误操作。最后再分享一点个人的体会自建TMS570LS0432的Flash读写工程最值得花时间的反而不是代码本身而是链接脚本和内存布局的理解。把LOAD、RUN、拷贝动作搞明白之后你会发现不仅是Flash操作以后做Bootloader时把App从Flash搬到RAM执行、或者做OTA时规划双区切换思路都会变得很清晰。F021这套API从寄存器层面看其实并不复杂但它是一块很好的垫脚石。踩过“从Flash执行擦写命令导致卡死”这个坑之后我再看TI各种Flash应用笔记基本都是一遍就能看懂。希望这篇能帮你把这段路走得更顺一点。本文还有配套的精品资源点击获取